Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117746343
D5766.1775173044.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
78 KB
Referenced Files
None
Subscribers
None
D5766.1775173044.diff
View Options
diff --git a/config.demo/src/database/seeds/PackageSeeder.php b/config.demo/src/database/seeds/PackageSeeder.php
--- a/config.demo/src/database/seeds/PackageSeeder.php
+++ b/config.demo/src/database/seeds/PackageSeeder.php
@@ -19,6 +19,7 @@
$skuGroupware = Sku::where(['title' => 'groupware', 'tenant_id' => \config('app.tenant_id')])->first();
$skuMailbox = Sku::where(['title' => 'mailbox', 'tenant_id' => \config('app.tenant_id')])->first();
$skuStorage = Sku::where(['title' => 'storage', 'tenant_id' => \config('app.tenant_id')])->first();
+ $skuFsQuota = Sku::where(['title' => 'fs-quota', 'tenant_id' => \config('app.tenant_id')])->first();
$package = Package::create(
[
@@ -32,18 +33,16 @@
$skus = [
$skuMailbox,
$skuGroupware,
+ $skuFsQuota,
$skuStorage
];
$package->skus()->saveMany($skus);
- // This package contains 2 units of the storage SKU, which just so happens to also
+ // This package contains 5 units of the storage SKU, which just so happens to also
// be the number of SKU free units.
- $package->skus()->updateExistingPivot(
- $skuStorage,
- ['qty' => 5],
- false
- );
+ $package->skus()->updateExistingPivot($skuStorage, ['qty' => 5], false);
+ $package->skus()->updateExistingPivot($skuFsQuota, ['qty' => 5], false);
$package = Package::create(
[
@@ -56,16 +55,14 @@
$skus = [
$skuMailbox,
+ $skuFsQuota,
$skuStorage
];
$package->skus()->saveMany($skus);
- $package->skus()->updateExistingPivot(
- $skuStorage,
- ['qty' => 5],
- false
- );
+ $package->skus()->updateExistingPivot($skuStorage, ['qty' => 5], false);
+ $package->skus()->updateExistingPivot($skuFsQuota, ['qty' => 5], false);
$package = Package::create(
[
@@ -113,11 +110,7 @@
// This package contains 2 units of the storage SKU, which just so happens to also
// be the number of SKU free units.
- $package->skus()->updateExistingPivot(
- $skuStorage,
- ['qty' => 5],
- false
- );
+ $package->skus()->updateExistingPivot($skuStorage,['qty' => 5], false);
$package = Package::create(
[
@@ -138,11 +131,7 @@
$package->skus()->saveMany($skus);
- $package->skus()->updateExistingPivot(
- $skuStorage,
- ['qty' => 5],
- false
- );
+ $package->skus()->updateExistingPivot($skuStorage, ['qty' => 5], false);
$package = Package::create(
[
diff --git a/config.demo/src/database/seeds/SkuSeeder.php b/config.demo/src/database/seeds/SkuSeeder.php
--- a/config.demo/src/database/seeds/SkuSeeder.php
+++ b/config.demo/src/database/seeds/SkuSeeder.php
@@ -72,6 +72,16 @@
'handler_class' => 'App\Handlers\Storage',
'active' => true,
],
+ [
+ 'title' => 'fs-quota',
+ 'name' => 'File Storage Quota',
+ 'description' => 'Some wiggle room for files',
+ 'cost' => 25,
+ 'units_free' => 5,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\FsQuota',
+ 'active' => true,
+ ],
[
'title' => 'groupware',
'name' => 'Groupware Features',
diff --git a/src/app/Console/Commands/Fs/QuotaUsageCommand.php b/src/app/Console/Commands/Fs/QuotaUsageCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Fs/QuotaUsageCommand.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Console\Commands\Fs;
+
+use App\Console\Command;
+use App\Fs\Quota;
+
+class QuotaUsageCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'fs:quota-usage';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Display quota usage of the file storage for all users';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ foreach (Quota::stats() as $email => $quota) {
+ $usage = Quota::bytes($quota['usage']);
+ $this->info("{$email} {$usage}");
+ }
+ }
+}
diff --git a/src/app/Fs/Item.php b/src/app/Fs/Item.php
--- a/src/app/Fs/Item.php
+++ b/src/app/Fs/Item.php
@@ -5,8 +5,10 @@
use App\Backends\Storage;
use App\Traits\BelongsToUserTrait;
use App\Traits\UuidStrKeyTrait;
+use App\User;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -305,4 +307,14 @@
{
return $this->belongsToMany(self::class, 'fs_relations', 'related_id', 'item_id');
}
+
+ /**
+ * The user the item belongs to.
+ *
+ * @return BelongsTo<User, $this>
+ */
+ public function item()
+ {
+ return $this->belongsTo(User::class);
+ }
}
diff --git a/src/app/Fs/Quota.php b/src/app/Fs/Quota.php
new file mode 100644
--- /dev/null
+++ b/src/app/Fs/Quota.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace App\Fs;
+
+use App\Handlers\FsQuota;
+use App\User;
+use Illuminate\Support\Collection;
+
+/**
+ * A utility for storage quota.
+ */
+class Quota
+{
+ /**
+ * Returns size of an available space for a user (in bytes).
+ */
+ public static function available(User $user): int
+ {
+ $free = self::limit($user) - self::usage($user);
+
+ return max(0, $free);
+ }
+
+ /**
+ * Converts quota size bytes into human readable string with an appropriate size unit
+ */
+ public static function bytes(int $bytes): string
+ {
+ if ($bytes >= 1073741824) {
+ return sprintf('%.01f GB', $bytes / 1073741824);
+ }
+
+ if ($bytes >= 1048576) {
+ $ret = sprintf('%.01f MB', $bytes / 1048576);
+ return $ret == '1024.0 MB' ? '1.0 GB' : $ret;
+ }
+
+ if ($bytes >= 1024) {
+ $ret = sprintf('%.01f KB', $bytes / 1024);
+ return $ret == '1024.0 KB' ? '1.0 MB' : $ret;
+ }
+
+ return "{$bytes} B";
+ }
+
+ /**
+ * Returns current quota limit for a user (in bytes).
+ */
+ public static function limit(User $user): int
+ {
+ $limit = $user->entitlements()
+ ->join('skus', 'skus.id', '=', 'sku_id')
+ ->where('handler_class', FsQuota::class)
+ ->count();
+
+ // FIXME: Should we check if user has the groupware entitlement here?
+ // FIXME: Should we consider a lack of limit an error case?
+ // FIXME: Does zero mean unlimited or not allowed?
+
+ return $limit * 1048576;
+ }
+
+ /**
+ * Returns current quota usage for a user (in bytes).
+ */
+ public static function usage(User $user): int
+ {
+ return (int) $user->fsItems()
+ ->join('fs_chunks', 'fs_items.id', '=', 'fs_chunks.item_id')
+ ->whereNot('type', '&', Item::TYPE_INCOMPLETE)
+ ->whereNull('fs_chunks.deleted_at')
+ ->sum('fs_chunks.size');
+ }
+
+ /**
+ * Returns current quota usage for all users.
+ *
+ * @return Collection<string, array{usage: int}>
+ */
+ public static function stats(): Collection
+ {
+ // TODO: Include quota limits?
+
+ return User::query()
+ ->join('fs_items', 'fs_items.user_id', '=', 'users.id')
+ ->join('fs_chunks', 'fs_items.id', '=', 'fs_chunks.item_id')
+ // FIXME: should we include incomplete files?
+ ->whereNot('fs_items.type', '&', Item::TYPE_INCOMPLETE)
+ ->whereNull('fs_chunks.deleted_at')
+ ->selectRaw('users.email, sum(fs_chunks.size) as `usage`')
+ ->groupBy('users.email')
+ ->orderByDesc('usage')
+ ->get()
+ ->mapWithKeys(function ($item, int $key) {
+ return [
+ $item->email => [
+ 'usage' => (int) $item->usage, // @phpstan-ignore-line
+ ],
+ ];
+ });
+ }
+}
diff --git a/src/app/Handlers/Base.php b/src/app/Handlers/Base.php
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Base.php
@@ -53,6 +53,8 @@
public static function metadata(Sku $sku): array
{
return [
+ // Inactive handlers will not appear on the subscriptions list
+ 'active' => true,
// entitleable type
'type' => \lcfirst(\class_basename(static::entitleableClass())),
// handler
diff --git a/src/app/Handlers/Storage.php b/src/app/Handlers/FsQuota.php
copy from src/app/Handlers/Storage.php
copy to src/app/Handlers/FsQuota.php
--- a/src/app/Handlers/Storage.php
+++ b/src/app/Handlers/FsQuota.php
@@ -2,12 +2,10 @@
namespace App\Handlers;
-use App\Entitlement;
-use App\Jobs\User\UpdateJob;
use App\Sku;
use App\User;
-class Storage extends Base
+class FsQuota extends Base
{
public const MAX_ITEMS = 100;
public const ITEM_UNIT = 'GB';
@@ -20,24 +18,6 @@
return User::class;
}
- /**
- * Handle entitlement creation event.
- */
- public static function entitlementCreated(Entitlement $entitlement): void
- {
- // Update the user IMAP mailbox quota
- UpdateJob::dispatch($entitlement->entitleable_id);
- }
-
- /**
- * Handle entitlement deletion event.
- */
- public static function entitlementDeleted(Entitlement $entitlement): void
- {
- // Update the user IMAP mailbox quota
- UpdateJob::dispatch($entitlement->entitleable_id);
- }
-
/**
* SKU handler metadata.
*/
@@ -45,8 +25,10 @@
{
$data = parent::metadata($sku);
+ $data['required'] = ['Beta'];
+ $data['active'] = \config('app.with_files');
$data['readonly'] = true; // only the checkbox will be disabled, not range
- $data['enabled'] = true;
+ $data['enabled'] = true; // default checkbox state
$data['range'] = [
'min' => $sku->units_free,
'max' => self::MAX_ITEMS,
@@ -62,6 +44,6 @@
*/
public static function priority(): int
{
- return 90;
+ return 0;
}
}
diff --git a/src/app/Handlers/Storage.php b/src/app/Handlers/Storage.php
--- a/src/app/Handlers/Storage.php
+++ b/src/app/Handlers/Storage.php
@@ -62,6 +62,6 @@
*/
public static function priority(): int
{
- return 90;
+ return 5;
}
}
diff --git a/src/app/Http/Controllers/API/V4/FsController.php b/src/app/Http/Controllers/API/V4/FsController.php
--- a/src/app/Http/Controllers/API/V4/FsController.php
+++ b/src/app/Http/Controllers/API/V4/FsController.php
@@ -4,11 +4,13 @@
use App\Fs\Item;
use App\Fs\Property;
+use App\Fs\Quota;
use App\Http\Controllers\RelationController;
use App\Http\Resources\FsItemInfoResource;
use App\Http\Resources\FsItemResource;
use App\Rules\FileName;
use App\Support\Facades\Storage;
+use App\User;
use App\Utils;
use Dedoc\Scramble\Attributes\QueryParameter;
use Illuminate\Http\JsonResponse;
@@ -407,6 +409,10 @@
}
}
+ if ($this->isOverQuota($this->guard()->user())) {
+ return $this->errorResponse(507, self::trans('validation.overquota'));
+ }
+
DB::beginTransaction();
$file = $this->deduplicateOrCreate($request, Item::TYPE_INCOMPLETE | Item::TYPE_FILE);
@@ -551,6 +557,8 @@
* @param string $id Upload (not file) identifier
*
* @return JsonResponse The response
+ *
+ * @unauthenticated
*/
public function upload(Request $request, $id)
{
@@ -576,6 +584,19 @@
return response()->json($response);
}
+ /**
+ * Check if user is over-quota.
+ */
+ protected function isOverQuota(User $user): bool
+ {
+ // On a file upload we don't know what is the input stream size. We should
+ // not trust Content-Length header (?), and fstat() does not return the size.
+ // It's much easier to check if user is over-quota early than check after
+ // every chunk has been saved into the disk (and if exceeded revert everything).
+ // This means we're relaxed about users being able to exceed the quota slightly.
+ return Quota::available($user) === 0;
+ }
+
/**
* Create a new collection.
*
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\API\V4;
+use App\Handlers\FsQuota;
use App\Handlers\Mailbox;
use App\Http\Controllers\ResourceController;
use App\Http\Resources\SkuResource;
@@ -26,10 +27,10 @@
$list = Sku::withSubjectTenantContext()->where('active', true)->orderBy('title')
->get()
->transform(function ($sku, $type) {
- return $this->skuElement($sku, $type);
+ return $this->skuElement($sku, !empty($type));
})
- ->filter(static function ($sku) use ($type) {
- return $sku && (!$type || $sku->metadata['type'] === $type);
+ ->filter(static function ($resource) use ($type) {
+ return $resource && (!$type || $resource->metadata['type'] === $type);
})
->sortByDesc('prio')
->values();
@@ -59,11 +60,13 @@
&& $object::class == $sku->handler_class::entitleableClass()
&& $sku->handler_class::isAvailable($sku, $object);
})
- ->transform(function ($sku) {
+ ->transform(static function ($sku) {
return self::skuElement($sku);
})
- ->filter(static function ($sku) use ($user) {
- return !empty($sku) && (empty($sku->controllerOnly) || $user->wallet()->isController($user));
+ ->filter(static function ($resource) use ($user) {
+ return !empty($resource) && (empty($resource->controllerOnly) || $user->wallet()->isController($user))
+ // For now we hide FsQuota SKU for users without the beta entitlement
+ && ($resource->resource->handler_class != FsQuota::class || $user->hasSku('beta'));
})
->sortByDesc('prio')
->values();
@@ -136,10 +139,10 @@
* Convert SKU information to metadata used by UI to
* display the form control
*
- * @param Sku $sku SKU object
- * @param string $type Type filter
+ * @param Sku $sku SKU object
+ * @param bool $ext Use extended resource
*/
- protected static function skuElement($sku, $type = null): ?SkuResource
+ protected static function skuElement($sku, $ext = false): ?SkuResource
{
if (!class_exists($sku->handler_class)) {
\Log::warning("Missing handler {$sku->handler_class}");
@@ -148,13 +151,18 @@
$resource = new SkuResource($sku);
+ // ignore inactive handlers
+ if (empty($resource->metadata['active'])) {
+ return null;
+ }
+
// ignore incomplete handlers
if (empty($resource->metadata['type'])) {
\Log::warning("Incomplete handler {$sku->handler_class}");
return null;
}
- $resource->type = $type;
+ $resource->ext = $ext;
return $resource;
}
diff --git a/src/app/Http/DAV/Collection.php b/src/app/Http/DAV/Collection.php
--- a/src/app/Http/DAV/Collection.php
+++ b/src/app/Http/DAV/Collection.php
@@ -4,6 +4,8 @@
use App\Backends\Storage;
use App\Fs\Item;
+use App\Fs\Quota;
+use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\DB;
use Sabre\DAV\Exception;
use Sabre\DAV\ICollection;
@@ -12,11 +14,12 @@
use Sabre\DAV\INode;
use Sabre\DAV\INodeByPath;
use Sabre\DAV\IProperties;
+use Sabre\DAV\IQuota;
/**
* Sabre DAV Collection interface implemetation
*/
-class Collection extends Node implements ICollection, ICopyTarget, IMoveTarget, INodeByPath, IProperties
+class Collection extends Node implements ICollection, ICopyTarget, IMoveTarget, INodeByPath, IProperties, IQuota
{
/**
* Checks if a child-node exists.
@@ -67,6 +70,8 @@
\Log::debug("[DAV] COPY-INTO: {$sourcePath} > {$path}, Depth:{$depth}");
+ $this->quotaCheck();
+
$item = $sourceNode->fsItem(); // @phpstan-ignore-line
$item->copy($this->data, $targetName, $depth);
@@ -113,6 +118,8 @@
throw new Exception\Forbidden('Hidden files are not accepted');
}
+ $this->quotaCheck();
+
DB::beginTransaction();
$file = Auth::$user->fsItems()->create(['type' => Item::TYPE_FILE]);
@@ -312,6 +319,32 @@
return $result;
}
+ /**
+ * Returns the quota information.
+ *
+ * This method MUST return an array with 2 values, the first being the total used space,
+ * the second the available space (in bytes)
+ */
+ public function getQuotaInfo()
+ {
+ \Log::debug("[DAV] GET-QUOTA-INFO: {$this->path}");
+
+ // This method will be called for every collection in a PROPFIND response
+ // when quota-used-bytes or quota-available-bytes was requested.
+ // So, we have to optimize this to be done only once per request.
+ $quota = Context::getHidden('quota');
+
+ if (empty($quota)) {
+ $limit = Quota::limit(Auth::$user);
+ $used = Quota::usage(Auth::$user);
+ $quota = [$used, max(0, $limit - $used)];
+
+ Context::addHidden('quota', $quota);
+ }
+
+ return $quota;
+ }
+
/**
* Moves a node into this collection.
*
diff --git a/src/app/Http/DAV/File.php b/src/app/Http/DAV/File.php
--- a/src/app/Http/DAV/File.php
+++ b/src/app/Http/DAV/File.php
@@ -143,8 +143,6 @@
{
\Log::debug('[DAV] PUT: ' . $this->path);
- // TODO: fileInput() method creates a non-chunked file, we need another way.
-
$result = Storage::fileInput($data, [], $this->data);
// Refresh the internal state for getETag()
diff --git a/src/app/Http/DAV/Node.php b/src/app/Http/DAV/Node.php
--- a/src/app/Http/DAV/Node.php
+++ b/src/app/Http/DAV/Node.php
@@ -3,6 +3,7 @@
namespace App\Http\DAV;
use App\Fs\Item;
+use App\Fs\Quota;
use Illuminate\Support\Facades\Context;
use Sabre\DAV\Exception;
use Sabre\DAV\INode;
@@ -268,4 +269,16 @@
// that's why we store all lookup results including `false`.
Context::addHidden('fs:' . $path, $item);
}
+
+ /**
+ * Check if there's any space left
+ *
+ * @throws Exception\InsufficientStorage
+ */
+ protected static function quotaCheck(): void
+ {
+ if (!Quota::available(Auth::$user)) {
+ throw new Exception\InsufficientStorage();
+ }
+ }
}
diff --git a/src/app/Http/Resources/SkuResource.php b/src/app/Http/Resources/SkuResource.php
--- a/src/app/Http/Resources/SkuResource.php
+++ b/src/app/Http/Resources/SkuResource.php
@@ -14,7 +14,7 @@
class SkuResource extends ApiResource
{
public bool $controllerOnly = false;
- public ?string $type = null;
+ public bool $ext = false;
public int $prio = 0;
public array $metadata = [];
@@ -75,7 +75,7 @@
'range' => $this->when(isset($this->metadata['range']), $this->metadata['range'] ?? []),
// Cost for a new object of the specified type
- 'nextCost' => $this->when(!empty($this->type), fn () => $this->nextCost()),
+ 'nextCost' => $this->when($this->ext, fn () => $this->nextCost()),
];
}
diff --git a/src/resources/js/files.js b/src/resources/js/files.js
--- a/src/resources/js/files.js
+++ b/src/resources/js/files.js
@@ -136,7 +136,7 @@
}
})
.catch(error => {
- console.log(error)
+ // console.log(error)
// TODO: Depending on the error consider retrying the request
// if it was one of many chunks of a bigger file?
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -180,6 +180,7 @@
'referralcodeinvalid' => 'The referral program code is invalid.',
'signuptokeninvalid' => 'The signup token is invalid.',
'verificationcodeinvalid' => 'The verification code is invalid or expired.',
+ 'overquota' => 'The storage quota has been exceeded.',
/*
|--------------------------------------------------------------------------
diff --git a/src/resources/vue/File/List.vue b/src/resources/vue/File/List.vue
--- a/src/resources/vue/File/List.vue
+++ b/src/resources/vue/File/List.vue
@@ -201,6 +201,11 @@
this.uploads[params.id].delete() // close the toast message
delete this.uploads[params.id]
+ // Display over-quota error
+ if (params.error && params.error.status == 507) {
+ this.$toast.error(params.error.response.data.message)
+ }
+
// TODO: Reloading the list is probably not the best solution
this.loadFiles({ reset: true })
} else {
diff --git a/src/resources/vue/Widgets/SubscriptionSelect.vue b/src/resources/vue/Widgets/SubscriptionSelect.vue
--- a/src/resources/vue/Widgets/SubscriptionSelect.vue
+++ b/src/resources/vue/Widgets/SubscriptionSelect.vue
@@ -180,7 +180,7 @@
// Uncheck all dependent SKUs, e.g. when unchecking Groupware we also uncheck Activesync
// TODO: Should we display an alert instead?
this.skus.forEach(item => {
- if (item.required && item.required.indexOf(sku.handler) > -1) {
+ if (!item.readonly && item.required && item.required.indexOf(sku.handler) > -1) {
$('#s' + item.id).find('input[type=checkbox]').prop('checked', false)
}
})
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -157,10 +157,10 @@
$browser->assertElementsCount('table tbody tr', 3)
->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '5,00 CHF')
- ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
- ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF')
- ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
- ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,90 CHF')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '4,90 CHF')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Storage Quota 5 GB')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '0,00 CHF')
->assertMissing('table tfoot')
->assertMissing('#reset2fa');
});
@@ -323,10 +323,10 @@
$browser->assertElementsCount('table tbody tr', 3)
->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
- ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
- ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '4,41 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Storage Quota 5 GB')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '0,00 CHF/month¹')
->assertMissing('table tfoot')
->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher');
});
@@ -484,16 +484,16 @@
$browser->assertElementsCount('table tbody tr', 6)
->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 6 GB')
- ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '45,00 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
- ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '4,41 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Activesync')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '0,00 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', '2-Factor Authentication')
->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,00 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication')
- ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(6) td:first-child', 'Private Beta (invitation only)')
- ->assertSeeIn('table tbody tr:nth-child(6) td:last-child', '45,09 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', 'Private Beta (invitation only)')
+ ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '45,09 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(6) td:first-child', 'Storage Quota 6 GB')
+ ->assertSeeIn('table tbody tr:nth-child(6) td:last-child', '45,00 CHF/month¹')
->assertMissing('table tfoot')
->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher')
->assertSeeIn('#reset2fa', 'Reset 2-Factor Auth')
diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php
--- a/src/tests/Browser/Reseller/UserTest.php
+++ b/src/tests/Browser/Reseller/UserTest.php
@@ -138,10 +138,10 @@
$browser->assertElementsCount('table tbody tr', 3)
->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '5,00 CHF/month')
- ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
- ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month')
- ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
- ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,90 CHF/month')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '4,90 CHF/month')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Storage Quota 5 GB')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '0,00 CHF/month')
->assertMissing('table tfoot')
->assertMissing('#reset2fa');
});
@@ -272,10 +272,10 @@
$browser->assertElementsCount('table tbody tr', 3)
->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
- ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
- ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '4,41 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Storage Quota 5 GB')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '0,00 CHF/month¹')
->assertMissing('table tfoot')
->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher');
});
@@ -397,13 +397,13 @@
$browser->assertElementsCount('table tbody tr', 5)
->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,50 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 5 GB')
- ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
- ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,41 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '4,41 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Activesync')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '0,00 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', '2-Factor Authentication')
->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,00 CHF/month¹')
- ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication')
+ ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', 'Storage Quota 5 GB')
->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹')
->assertMissing('table tfoot')
->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher')
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -212,47 +212,48 @@
'tbody tr:nth-child(1) td.buttons button',
'Just a mailbox'
)
- // Storage SKU
- ->assertSeeIn('tbody tr:nth-child(2) td.name', 'Storage Quota')
- ->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month')
+ // Groupware SKU
+ ->assertSeeIn('tbody tr:nth-child(2) td.name', 'Groupware Features')
+ ->assertSeeIn('tbody tr:nth-child(2) td.price', '4,90 CHF/month')
->assertChecked('tbody tr:nth-child(2) td.selection input')
- ->assertDisabled('tbody tr:nth-child(2) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(2) td.selection input')
->assertTip(
'tbody tr:nth-child(2) td.buttons button',
- 'Some wiggle room'
+ 'Groupware functions like Calendar, Tasks, Notes, etc.'
)
- ->with(new QuotaInput('tbody tr:nth-child(2) .range-input'), static function ($browser) {
- $browser->assertQuotaValue(5)->setQuotaValue(6);
- })
- ->assertSeeIn('tr:nth-child(2) td.price', '0,25 CHF/month')
- // Groupware SKU
- ->assertSeeIn('tbody tr:nth-child(3) td.name', 'Groupware Features')
- ->assertSeeIn('tbody tr:nth-child(3) td.price', '4,90 CHF/month')
- ->assertChecked('tbody tr:nth-child(3) td.selection input')
+ // ActiveSync SKU
+ ->assertSeeIn('tbody tr:nth-child(3) td.name', 'Activesync')
+ ->assertSeeIn('tbody tr:nth-child(3) td.price', '0,00 CHF/month')
+ ->assertNotChecked('tbody tr:nth-child(3) td.selection input')
->assertEnabled('tbody tr:nth-child(3) td.selection input')
->assertTip(
'tbody tr:nth-child(3) td.buttons button',
- 'Groupware functions like Calendar, Tasks, Notes, etc.'
+ 'Mobile synchronization'
)
- // ActiveSync SKU
- ->assertSeeIn('tbody tr:nth-child(4) td.name', 'Activesync')
+ // 2FA SKU
+ ->assertSeeIn('tbody tr:nth-child(4) td.name', '2-Factor Authentication')
->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(4) td.selection input')
->assertEnabled('tbody tr:nth-child(4) td.selection input')
->assertTip(
'tbody tr:nth-child(4) td.buttons button',
- 'Mobile synchronization'
+ 'Two factor authentication for webmail and administration panel'
)
- // 2FA SKU
- ->assertSeeIn('tbody tr:nth-child(5) td.name', '2-Factor Authentication')
- ->assertSeeIn('tbody tr:nth-child(5) td.price', '0,00 CHF/month')
- ->assertNotChecked('tbody tr:nth-child(5) td.selection input')
- ->assertEnabled('tbody tr:nth-child(5) td.selection input')
+ // Storage SKU
+ ->assertSeeIn('tbody tr:nth-child(5) td.name', 'Storage Quota')
+ ->assertSeeIn('tr:nth-child(5) td.price', '0,00 CHF/month')
+ ->assertChecked('tbody tr:nth-child(5) td.selection input')
+ ->assertDisabled('tbody tr:nth-child(5) td.selection input')
->assertTip(
'tbody tr:nth-child(5) td.buttons button',
- 'Two factor authentication for webmail and administration panel'
+ 'Some wiggle room'
)
- ->click('tbody tr:nth-child(4) td.selection input');
+ ->with(new QuotaInput('tbody tr:nth-child(5) .range-input'), static function ($browser) {
+ $browser->assertQuotaValue(5)->setQuotaValue(6);
+ })
+ ->assertSeeIn('tr:nth-child(5) td.price', '0,25 CHF/month')
+ // enable Activesync
+ ->click('tbody tr:nth-child(3) td.selection input');
})
->assertMissing('@skus table + .hint')
->click('button[type=submit]')
@@ -262,8 +263,12 @@
->click('@table tr:nth-child(3) a')
->on(new UserInfo());
- $expected = ['activesync', 'groupware', 'mailbox',
- 'storage', 'storage', 'storage', 'storage', 'storage', 'storage'];
+ $expected = [
+ 'activesync',
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
+ 'groupware', 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
+ ];
$this->assertEntitlements($john->fresh(), $expected);
// Test subscriptions interaction
@@ -612,7 +617,11 @@
$alias = UserAlias::where('user_id', $julia->id)->where('alias', 'julia.roberts2@kolab.org')->first();
$this->assertTrue(!empty($alias));
- $this->assertEntitlements($julia, ['mailbox', 'storage', 'storage', 'storage', 'storage', 'storage']);
+ $this->assertEntitlements($julia, [
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
+ 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]);
$this->assertSame('Julia', $julia->getSetting('first_name'));
$this->assertSame('Roberts', $julia->getSetting('last_name'));
$this->assertSame('Test Org', $julia->getSetting('organization'));
@@ -757,23 +766,23 @@
->on(new UserInfo())
->with('@general', static function (Browser $browser) {
$browser->whenAvailable('@skus', static function (Browser $browser) {
- $quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
+ $quota_input = new QuotaInput('tbody tr:nth-child(5) .range-input');
$browser->waitFor('tbody tr')
->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '4,50 CHF/month¹')
// Storage SKU
- ->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month¹')
+ ->assertSeeIn('tr:nth-child(5) td.price', '0,00 CHF/month¹')
->with($quota_input, static function (Browser $browser) {
$browser->setQuotaValue(100);
})
- ->assertSeeIn('tr:nth-child(2) td.price', '21,37 CHF/month¹')
+ ->assertSeeIn('tr:nth-child(5) td.price', '21,37 CHF/month¹')
// Groupware SKU
- ->assertSeeIn('tbody tr:nth-child(3) td.price', '4,41 CHF/month¹')
+ ->assertSeeIn('tbody tr:nth-child(2) td.price', '4,41 CHF/month¹')
// ActiveSync SKU
- ->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month¹')
+ ->assertSeeIn('tbody tr:nth-child(3) td.price', '0,00 CHF/month¹')
// 2FA SKU
- ->assertSeeIn('tbody tr:nth-child(5) td.price', '0,00 CHF/month¹');
+ ->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month¹');
})
->assertSeeIn('@skus table + .hint', '¹ applied discount: 10% - Test voucher');
});
@@ -820,20 +829,20 @@
->on(new UserInfo())
->with('@general', static function (Browser $browser) {
$browser->whenAvailable('@skus', static function (Browser $browser) {
- $quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
+ $quota_input = new QuotaInput('tbody tr:nth-child(6) .range-input');
$browser->waitFor('tbody tr')
// Beta SKU
- ->assertSeeIn('tbody tr:nth-child(6) td.price', '45,09 CHF/month¹')
+ ->assertSeeIn('tbody tr:nth-child(5) td.price', '45,09 CHF/month¹')
// Storage SKU
- ->assertSeeIn('tr:nth-child(2) td.price', '45,00 CHF/month¹')
+ ->assertSeeIn('tr:nth-child(6) td.price', '45,00 CHF/month¹')
->with($quota_input, static function (Browser $browser) {
$browser->setQuotaValue(7);
})
- ->assertSeeIn('tr:nth-child(2) td.price', '45,22 CHF/month¹')
+ ->assertSeeIn('tr:nth-child(6) td.price', '45,22 CHF/month¹')
->with($quota_input, static function (Browser $browser) {
$browser->setQuotaValue(5);
})
- ->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month¹');
+ ->assertSeeIn('tr:nth-child(6) td.price', '0,00 CHF/month¹');
})
->assertSeeIn('@skus table + .hint', '¹ applied discount: 10% - Test voucher');
});
@@ -924,17 +933,17 @@
->on(new UserInfo())
->with('@general', static function (Browser $browser) {
$browser->whenAvailable('@skus', static function (Browser $browser) {
- $quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
+ $quota_input = new QuotaInput('tbody tr:nth-child(5) .range-input');
$browser->waitFor('tbody tr')
->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '5,00 €/month')
// Storage SKU
- ->assertSeeIn('tr:nth-child(2) td.price', '0,00 €/month')
+ ->assertSeeIn('tr:nth-child(5) td.price', '0,00 €/month')
->with($quota_input, static function (Browser $browser) {
$browser->setQuotaValue(100);
})
- ->assertSeeIn('tr:nth-child(2) td.price', '23,75 €/month');
+ ->assertSeeIn('tr:nth-child(5) td.price', '23,75 €/month');
});
});
});
diff --git a/src/tests/Feature/Console/Data/Import/LdifTest.php b/src/tests/Feature/Console/Data/Import/LdifTest.php
--- a/src/tests/Feature/Console/Data/Import/LdifTest.php
+++ b/src/tests/Feature/Console/Data/Import/LdifTest.php
@@ -85,6 +85,7 @@
$wallet = $owner->wallets->first();
$this->assertEntitlements($owner, [
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
'groupware',
'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
@@ -107,6 +108,7 @@
$this->assertSame(['alias2@kolab3.com'], $aliases);
$this->assertEntitlements($users['user@kolab3.com'], [
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
'groupware',
'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage',
diff --git a/src/tests/Feature/Console/Fs/QuotaUsageTest.php b/src/tests/Feature/Console/Fs/QuotaUsageTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Fs/QuotaUsageTest.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Tests\Feature\Console\Fs;
+
+use Tests\TestCaseFs;
+
+class QuotaUsageTest extends TestCaseFs
+{
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ $user = $this->getTestUser('john@kolab.org');
+
+ // Expect empty result
+ $code = \Artisan::call('fs:quota-usage');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+
+ $file = $this->getTestFile($user, 'test1.txt', '123');
+
+ $code = \Artisan::call('fs:quota-usage');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertStringContainsString('john@kolab.org 3 B', $output);
+ $this->assertStringNotContainsString('ned@kolab.org', $output);
+ }
+}
diff --git a/src/tests/Feature/Console/User/ForceDeleteTest.php b/src/tests/Feature/Console/User/ForceDeleteTest.php
--- a/src/tests/Feature/Console/User/ForceDeleteTest.php
+++ b/src/tests/Feature/Console/User/ForceDeleteTest.php
@@ -50,9 +50,6 @@
$user->assignPackage($package_kolab);
$domain->assignPackage($package_domain, $user);
$wallet = $user->wallets()->first();
- $entitlements = $wallet->entitlements->pluck('id')->all();
-
- $this->assertCount(8, $entitlements);
// Non-deleted user
$this->artisan('user:force-delete user@force-delete.com')
diff --git a/src/tests/Feature/Console/User/RestoreTest.php b/src/tests/Feature/Console/User/RestoreTest.php
--- a/src/tests/Feature/Console/User/RestoreTest.php
+++ b/src/tests/Feature/Console/User/RestoreTest.php
@@ -49,9 +49,6 @@
$user->assignPackage($package_kolab);
$domain->assignPackage($package_domain, $user);
$wallet = $user->wallets()->first();
- $entitlements = $wallet->entitlements->pluck('id')->all();
-
- $this->assertCount(8, $entitlements);
// Non-deleted user
$code = \Artisan::call("user:restore {$user->email}");
diff --git a/src/tests/Feature/Controller/Admin/SkusTest.php b/src/tests/Feature/Controller/Admin/SkusTest.php
--- a/src/tests/Feature/Controller/Admin/SkusTest.php
+++ b/src/tests/Feature/Controller/Admin/SkusTest.php
@@ -74,7 +74,7 @@
$json = $response->json();
- $this->assertCount(12, $json['list']);
+ $this->assertCount(13, $json['list']);
$this->assertSame(100, $json['list'][0]['prio']);
$this->assertSame($sku->id, $json['list'][0]['id']);
$this->assertSame($sku->title, $json['list'][0]['title']);
@@ -109,7 +109,7 @@
$json = $response->json();
- $this->assertCount(5, $json['list']);
+ $this->assertCount(6, $json['list']);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/DAVTest.php b/src/tests/Feature/Controller/DAVTest.php
--- a/src/tests/Feature/Controller/DAVTest.php
+++ b/src/tests/Feature/Controller/DAVTest.php
@@ -133,6 +133,11 @@
$response = $this->davRequest('COPY', "{$root}/folderB", '', $john, ['Destination' => "{$host}/{$root}/folderB/folderX", 'Depth' => 'infinity']);
$response->assertStatus(409);
+
+ // Test over-quota condition
+ $this->turnUserOverQuota($john);
+ $response = $this->davRequest('COPY', "{$root}/folderA", '', $john, ['Destination' => "{$host}/{$root}/folderD", 'Depth' => '0']);
+ $response->assertNoContent(507);
}
/**
@@ -664,7 +669,6 @@
$this->assertSame("/{$root}/", $responses[0]->getElementsByTagName('href')->item(0)->textContent);
$this->assertCount(1, $responses[0]->getElementsByTagName('resourcetype')->item(0)->childNodes);
$this->assertSame('collection', $responses[0]->getElementsByTagName('resourcetype')->item(0)->firstChild->localName);
- $this->assertCount(1, $responses[0]->getElementsByTagName('prop')->item(0)->childNodes);
$this->assertStringContainsString('200 OK', $responses[0]->getElementsByTagName('status')->item(0)->textContent);
// the subfolder folder
@@ -750,6 +754,46 @@
$this->assertSame("/{$root}/folder1/test4.txt", $responses[3]->getElementsByTagName('href')->item(0)->textContent);
}
+ /**
+ * Test PROPFIND requests with quota properties
+ */
+ public function testPropfindQuota(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $root = trim(\config('services.dav.webdav_root'), '/') . '/user/john@kolab.org';
+
+ [$folders, $files] = $this->initTestStorage($john);
+
+ // Test requesting quota properties on the root
+ $xml = '<d:propfind xmlns:d="DAV:"><d:prop><d:quota-used-bytes/><d:quota-available-bytes/></d:prop></d:propfind>';
+ $response = $this->davRequest('PROPFIND', $root, $xml, $john, ['Depth' => 0]);
+ $response->assertStatus(207);
+
+ $doc = $this->responseXML($response);
+
+ $this->assertSame('multistatus', $doc->documentElement->localName);
+ $this->assertCount(1, $responses = $doc->documentElement->getElementsByTagName('response'));
+ $this->assertSame('64', $responses[0]->getElementsByTagName('quota-used-bytes')->item(0)->textContent);
+ $this->assertSame('5242816', $responses[0]->getElementsByTagName('quota-available-bytes')->item(0)->textContent);
+
+ // Test with '<allprop>'
+ // It should not include quota properties according to the RFC, but Sabre does otherwise
+ // See https://github.com/sabre-io/dav/issues/1616
+ /*
+ $xml = '<d:propfind xmlns:d="DAV:"><d:allprop/></d:propfind>';
+ $response = $this->davRequest('PROPFIND', $root, $xml, $john, ['Depth' => 0]);
+ $response->assertStatus(207);
+
+ $doc = $this->responseXML($response);
+
+ $this->assertSame('multistatus', $doc->documentElement->localName);
+ $this->assertCount(1, $responses = $doc->documentElement->getElementsByTagName('response'));
+ $this->assertCount(0, $responses[0]->getElementsByTagName('quota-used-bytes'));
+ $this->assertCount(0, $responses[0]->getElementsByTagName('quota-available-bytes'));
+ */
+ $this->markTestIncomplete();
+ }
+
/**
* Test basic PUT requests
*/
@@ -819,6 +863,11 @@
$this->assertSame('4', $files[0]->getProperty('size'));
$this->assertSame('text/plain', $files[0]->getProperty('mimetype'));
$this->assertSame('Test', $this->getTestFileContent($files[0]));
+
+ // Test over-quota condition
+ $this->turnUserOverQuota($john);
+ $response = $this->davRequest('PUT', "{$root}/folder1/test1234.txt", 'Test', $john, ['Content-Type' => 'text/plain']);
+ $response->assertNoContent(507);
}
/**
diff --git a/src/tests/Feature/Controller/FsTest.php b/src/tests/Feature/Controller/FsTest.php
--- a/src/tests/Feature/Controller/FsTest.php
+++ b/src/tests/Feature/Controller/FsTest.php
@@ -538,6 +538,16 @@
$this->assertSame($parent->id, $file->parents()->first()->id);
// TODO: Test X-Kolab-Parents
+
+ // Test over-quota condition (John has 5GB quota)
+ $this->turnUserOverQuota($john);
+ $response = $this->sendRawBody($john, 'POST', "api/v4/fs?name=test1.txt", $headers, $body);
+ $response->assertStatus(507);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The storage quota has been exceeded.", $json['message']);
}
/**
diff --git a/src/tests/Feature/Controller/Reseller/SkusTest.php b/src/tests/Feature/Controller/Reseller/SkusTest.php
--- a/src/tests/Feature/Controller/Reseller/SkusTest.php
+++ b/src/tests/Feature/Controller/Reseller/SkusTest.php
@@ -91,8 +91,7 @@
$json = $response->json();
- $this->assertCount(12, $json['list']);
-
+ $this->assertCount(13, $json['list']);
$this->assertSame(100, $json['list'][0]['prio']);
$this->assertSame($sku->id, $json['list'][0]['id']);
$this->assertSame($sku->title, $json['list'][0]['title']);
@@ -159,7 +158,7 @@
$json = $response->json();
- $this->assertCount(5, $json['list']);
+ $this->assertCount(6, $json['list']);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -59,8 +59,7 @@
$json = $response->json();
- $this->assertCount(12, $json['list']);
-
+ $this->assertCount(13, $json['list']);
$this->assertSame(100, $json['list'][0]['prio']);
$this->assertSame($sku->id, $json['list'][0]['id']);
$this->assertSame($sku->title, $json['list'][0]['title']);
@@ -95,6 +94,16 @@
$this->assertCount(1, $json['list']);
$this->assertSame('domain-hosting', $json['list'][0]['title']);
$this->assertSame(0, $json['list'][0]['nextCost']); // first domain costs 0
+
+ // Test de-activating handlers (fs-quota requires app.with_files)
+ \config(['app.with_files' => false]);
+ $response = $this->actingAs($john)->get("api/v4/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(12, $json['list']);
+ $this->assertFalse(in_array('fs-quota', array_column($json['list'], 'title')));
}
/**
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -442,21 +442,22 @@
$json = $response->json();
$storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first();
+ $fsquota_sku = Sku::withEnvTenantContext()->where('title', 'fs-quota')->first();
$groupware_sku = Sku::withEnvTenantContext()->where('title', 'groupware')->first();
$mailbox_sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
$secondfactor_sku = Sku::withEnvTenantContext()->where('title', '2fa')->first();
- $this->assertCount(5, $json['skus']);
-
+ $this->assertCount(6, $json['skus']);
$this->assertSame(5, $json['skus'][$storage_sku->id]['count']);
$this->assertSame([0, 0, 0, 0, 0], $json['skus'][$storage_sku->id]['costs']);
+ $this->assertSame(5, $json['skus'][$fsquota_sku->id]['count']);
+ $this->assertSame([0, 0, 0, 0, 0], $json['skus'][$fsquota_sku->id]['costs']);
$this->assertSame(1, $json['skus'][$groupware_sku->id]['count']);
$this->assertSame([490], $json['skus'][$groupware_sku->id]['costs']);
$this->assertSame(1, $json['skus'][$mailbox_sku->id]['count']);
$this->assertSame([500], $json['skus'][$mailbox_sku->id]['costs']);
$this->assertSame(1, $json['skus'][$secondfactor_sku->id]['count']);
$this->assertSame([0], $json['skus'][$secondfactor_sku->id]['costs']);
-
$this->assertSame([], $json['aliases']);
}
@@ -499,20 +500,7 @@
'readonly' => true,
]);
- $this->assertSkuElement('storage', $json['list'][1], [
- 'prio' => 90,
- 'type' => 'user',
- 'handler' => 'Storage',
- 'enabled' => true,
- 'readonly' => true,
- 'range' => [
- 'min' => 5,
- 'max' => 100,
- 'unit' => 'GB',
- ],
- ]);
-
- $this->assertSkuElement('groupware', $json['list'][2], [
+ $this->assertSkuElement('groupware', $json['list'][1], [
'prio' => 80,
'type' => 'user',
'handler' => 'Groupware',
@@ -520,7 +508,7 @@
'readonly' => false,
]);
- $this->assertSkuElement('activesync', $json['list'][3], [
+ $this->assertSkuElement('activesync', $json['list'][2], [
'prio' => 70,
'type' => 'user',
'handler' => 'Activesync',
@@ -529,7 +517,7 @@
'required' => ['Groupware'],
]);
- $this->assertSkuElement('2fa', $json['list'][4], [
+ $this->assertSkuElement('2fa', $json['list'][3], [
'prio' => 60,
'type' => 'user',
'handler' => 'Auth2F',
@@ -538,6 +526,19 @@
'forbidden' => ['Activesync'],
]);
+ $this->assertSkuElement('storage', $json['list'][4], [
+ 'prio' => 5,
+ 'type' => 'user',
+ 'handler' => 'Storage',
+ 'enabled' => true,
+ 'readonly' => true,
+ 'range' => [
+ 'min' => 5,
+ 'max' => 100,
+ 'unit' => 'GB',
+ ],
+ ]);
+
// Test inclusion of beta SKUs
$sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$user->assignSku($sku);
@@ -546,15 +547,41 @@
$json = $response->json();
- $this->assertCount(6, $json['list']);
+ $this->assertCount(7, $json['list']);
- $this->assertSkuElement('beta', $json['list'][5], [
+ $this->assertSkuElement('beta', $json['list'][4], [
'prio' => 10,
'type' => 'user',
'handler' => 'Beta',
'enabled' => false,
'readonly' => false,
]);
+
+ $this->assertSkuElement('storage', $json['list'][5], [
+ 'prio' => 5,
+ 'type' => 'user',
+ 'handler' => 'Storage',
+ 'enabled' => true,
+ 'readonly' => true,
+ 'range' => [
+ 'min' => 5,
+ 'max' => 100,
+ 'unit' => 'GB',
+ ],
+ ]);
+
+ $this->assertSkuElement('fs-quota', $json['list'][6], [
+ 'prio' => 0,
+ 'type' => 'user',
+ 'handler' => 'FsQuota',
+ 'enabled' => true,
+ 'readonly' => true,
+ 'range' => [
+ 'min' => 5,
+ 'max' => 100,
+ 'unit' => 'GB',
+ ],
+ ]);
}
/**
@@ -1006,8 +1033,11 @@
$this->assertSame('deleted@kolab.org', $aliases[0]->alias);
$this->assertSame('useralias1@kolab.org', $aliases[1]->alias);
// Assert the new user entitlements
- $this->assertEntitlements($user, ['groupware', 'mailbox',
- 'storage', 'storage', 'storage', 'storage', 'storage']);
+ $this->assertEntitlements($user, [
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
+ 'groupware', 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]);
// Assert the wallet to which the new user should be assigned to
$wallet = $user->wallet();
$this->assertSame($john->wallets->first()->id, $wallet->id);
@@ -1032,8 +1062,11 @@
$this->assertSame('Doe2', $user->getSetting('last_name'));
$this->assertSame('TestOrg', $user->getSetting('organization'));
$this->assertCount(0, $user->aliases()->get());
- $this->assertEntitlements($user, ['groupware', 'mailbox',
- 'storage', 'storage', 'storage', 'storage', 'storage']);
+ $this->assertEntitlements($user, [
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
+ 'groupware', 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]);
// Test password reset link "mode"
$code = new VerificationCode(['mode' => VerificationCode::MODE_PASSWORD, 'active' => false]);
@@ -1323,7 +1356,11 @@
$this->assertEntitlements(
$user,
- ['groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage']
+ [
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
+ 'groupware', 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]
);
$this->assertSame([0, 0, 0, 0, 0, 25], $storage_cost);
@@ -1380,15 +1417,10 @@
$jane,
[
'activesync',
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
'groupware',
'mailbox',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
]
);
@@ -1408,17 +1440,10 @@
$this->assertEntitlements(
$jane,
[
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
'groupware',
'mailbox',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
]
);
@@ -1438,17 +1463,10 @@
$this->assertEntitlements(
$jane,
[
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
'groupware',
'mailbox',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
]
);
@@ -1468,17 +1486,10 @@
$this->assertEntitlements(
$jane,
[
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
'groupware',
'mailbox',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
]
);
@@ -1498,13 +1509,10 @@
$this->assertEntitlements(
$jane,
[
+ 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota', 'fs-quota',
'groupware',
'mailbox',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
- 'storage',
+ 'storage', 'storage', 'storage', 'storage', 'storage',
]
);
}
diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php
--- a/src/tests/Feature/EntitlementTest.php
+++ b/src/tests/Feature/EntitlementTest.php
@@ -149,10 +149,10 @@
$wallet = $owner->wallets->first();
- $this->assertCount(7, $owner->entitlements()->get());
+ $this->assertCount(12, $owner->entitlements()->get());
$this->assertCount(1, $skuDomain->entitlements()->where('wallet_id', $wallet->id)->get());
$this->assertCount(2, $skuMailbox->entitlements()->where('wallet_id', $wallet->id)->get());
- $this->assertCount(15, $wallet->entitlements);
+ $this->assertCount(12 + 12 + 1, $wallet->entitlements);
$this->backdateEntitlements(
$owner->entitlements,
diff --git a/src/tests/Feature/Fs/QuotaTest.php b/src/tests/Feature/Fs/QuotaTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Fs/QuotaTest.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Fs\Item;
+use App\Fs\Quota;
+use App\Handlers\FsQuota;
+use App\Sku;
+use Tests\TestCaseFs;
+
+class QuotaTest extends TestCaseFs
+{
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('jane@kolabnow.com');
+ }
+
+ protected function tearDown(): void
+ {
+ $this->deleteTestUser('jane@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test Quota::available()
+ */
+ public function testAvailable(): void
+ {
+ $user = $this->getTestUser('jane@kolabnow.com');
+
+ // No quota entitlements, no files
+ $this->assertSame(0, Quota::available($user));
+
+ // No quota entitlements, existing file
+ $file = $this->getTestFile($user, 'test1.txt', '123456789');
+ $this->assertSame(0, Quota::available($user));
+
+ // Add quota entitlement
+ $sku = Sku::withObjectTenantContext($user)->where('handler_class', FsQuota::class)->first();
+ $user->assignSku($sku);
+ $this->assertSame(1048576 - 9, Quota::available($user));
+ }
+
+ /**
+ * Test Quota::limit()
+ */
+ public function testLimit(): void
+ {
+ $user = $this->getTestUser('jane@kolabnow.com');
+
+ // No quota entitlements
+ $this->assertSame(0, Quota::limit($user));
+
+ // With 2 quota entitlements
+ $sku = Sku::withObjectTenantContext($user)->where('handler_class', FsQuota::class)->first();
+ $user->assignSku($sku, 2);
+ $this->assertSame(2 * 1048576, Quota::limit($user));
+ }
+
+ /**
+ * Test Quota::usage()
+ */
+ public function testUsage(): void
+ {
+ $user = $this->getTestUser('john@kolab.org');
+
+ $this->assertSame(0, Quota::usage($user));
+
+ $file1 = $this->getTestFile($user, 'test1.txt', '1');
+ $file2 = $this->getTestFile($user, 'test2.txt', '20');
+ $file3 = $this->getTestFile($user, 'test3.txt', '3000');
+ $file3->type |= Item::TYPE_INCOMPLETE;
+ $file3->save();
+ $file4 = $this->getTestFile($user, 'test4.txt', '40000');
+ $file4->delete();
+
+ $folder = $this->getTestCollection($user, 'folder1');
+ $folder->children()->attach([$file1, $file3]);
+
+ $this->assertSame(3, Quota::usage($user));
+
+ // Test if it handles numbers over 4GB
+ $file1->chunks()->first()->update(['size' => 4294967295]);
+
+ $this->assertSame(8, \PHP_INT_SIZE); // make sure we're on 64bit
+ $this->assertSame(4294967295 + 2, Quota::usage($user));
+ }
+}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -134,7 +134,7 @@
$user->assignPlan($plan);
$this->assertSame((string) $plan->id, $user->getSetting('plan_id'));
- $this->assertSame(7, $user->entitlements()->count()); // 5 storage + 1 mailbox + 1 groupware
+ $this->assertSame(12, $user->entitlements()->count()); // 5 storage + 1 mailbox + 1 groupware + 5 fs-quota
$user = $this->getTestUser('useraccountb@' . $domain->namespace);
@@ -142,7 +142,7 @@
$user->assignPlan($plan, $domain);
$this->assertSame((string) $plan->id, $user->getSetting('plan_id'));
- $this->assertSame(7, $user->entitlements()->count()); // 5 storage + 1 mailbox + 1 groupware
+ $this->assertSame(12, $user->entitlements()->count()); // 5 storage + 1 mailbox + 1 groupware + 5 fs-quota
$this->assertSame(1, $domain->entitlements()->count());
// Individual plan (domain is not allowed)
@@ -768,10 +768,10 @@
$wallet = $userA->wallets->first();
- $this->assertSame(7, $entitlementsA->count());
- $this->assertSame(7, $entitlementsB->count());
- $this->assertSame(7, $entitlementsA->whereDate('updated_at', $yesterday->toDateString())->count());
- $this->assertSame(7, $entitlementsB->whereDate('updated_at', $yesterday->toDateString())->count());
+ $this->assertSame(12, $entitlementsA->count());
+ $this->assertSame(12, $entitlementsB->count());
+ $this->assertSame(12, $entitlementsA->whereDate('updated_at', $yesterday->toDateString())->count());
+ $this->assertSame(12, $entitlementsB->whereDate('updated_at', $yesterday->toDateString())->count());
$this->assertSame(0, $wallet->balance);
$this->fakeQueueReset();
@@ -789,8 +789,8 @@
$balance = $wallet->fresh()->balance;
$this->assertTrue($balance < 0);
- $this->assertSame(7, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count());
- $this->assertSame(7, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count());
+ $this->assertSame(12, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count());
+ $this->assertSame(12, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count());
// Expect one update job for every user
// @phpstan-ignore-next-line
@@ -821,8 +821,8 @@
// Expect no balance change, degraded account entitlements are free
$this->assertSame($balance, $wallet->fresh()->balance);
- $this->assertSame(7, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count());
- $this->assertSame(7, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count());
+ $this->assertSame(12, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count());
+ $this->assertSame(12, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count());
// Expect one update job for every user
// @phpstan-ignore-next-line
@@ -846,7 +846,7 @@
$id = $user->id;
- $this->assertCount(7, $user->entitlements()->get());
+ $this->assertCount(12, $user->entitlements()->get());
Queue::fake();
@@ -903,9 +903,10 @@
$entitlementsResource = Entitlement::where('entitleable_id', $resource->id);
$entitlementsFolder = Entitlement::where('entitleable_id', $folder->id);
- $this->assertSame(7, $entitlementsA->count());
- $this->assertSame(7, $entitlementsB->count());
- $this->assertSame(7, $entitlementsC->count());
+ $all_count = 12;
+ $this->assertSame($all_count, $entitlementsA->count());
+ $this->assertSame($all_count, $entitlementsB->count());
+ $this->assertSame($all_count, $entitlementsC->count());
$this->assertSame(1, $entitlementsDomain->count());
$this->assertSame(1, $entitlementsGroup->count());
$this->assertSame(1, $entitlementsResource->count());
@@ -927,9 +928,9 @@
$this->assertSame(0, $entitlementsGroup->count());
$this->assertSame(0, $entitlementsResource->count());
$this->assertSame(0, $entitlementsFolder->count());
- $this->assertSame(7, $entitlementsA->withTrashed()->count());
- $this->assertSame(7, $entitlementsB->withTrashed()->count());
- $this->assertSame(7, $entitlementsC->withTrashed()->count());
+ $this->assertSame($all_count, $entitlementsA->withTrashed()->count());
+ $this->assertSame($all_count, $entitlementsB->withTrashed()->count());
+ $this->assertSame($all_count, $entitlementsC->withTrashed()->count());
$this->assertSame(1, $entitlementsDomain->withTrashed()->count());
$this->assertSame(1, $entitlementsGroup->withTrashed()->count());
$this->assertSame(1, $entitlementsResource->withTrashed()->count());
@@ -1351,7 +1352,7 @@
$this->assertFalse($domainA->fresh()->trashed());
// Assert entitlements
- $this->assertSame(7, $entitlementsA->count()); // mailbox + groupware + 5 x storage
+ $this->assertSame(12, $entitlementsA->count()); // mailbox + groupware + 5 x storage + 5 x fs-quota
$this->assertTrue($ent1->fresh()->trashed());
$entitlementsA->get()->each(function ($ent) {
$this->assertTrue($ent->updated_at->greaterThan(Carbon::now()->subSeconds(5)));
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -346,8 +346,7 @@
// Test second month
$this->backdateEntitlements($wallet->entitlements, Carbon::now()->subMonthsWithoutOverflow(2));
- $this->assertCount(7, $wallet->entitlements);
-
+ $this->assertCount(12, $wallet->entitlements);
$this->assertSame(1980, $wallet->expectedCharges());
$entitlement = Entitlement::create([
@@ -501,7 +500,7 @@
$this->assertSame(0, $wallet->balance);
$this->assertSame(0, $reseller_wallet->balance);
$this->assertSame(0, $wallet->transactions()->count());
- $this->assertSame(12, $user->entitlements()->where('updated_at', $backdate)->count());
+ $this->assertSame(17, $user->entitlements()->where('updated_at', $backdate)->count());
// ------------------------------------
// Test normal charging of entitlements
@@ -546,7 +545,7 @@
// Assert all entitlements' updated_at timestamp
$date = $backdate->addMonthsWithoutOverflow(1);
- $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get());
+ $this->assertCount(17, $wallet->entitlements()->where('updated_at', $date)->get());
// Assert per-entitlement transactions
$entitlement_transactions = Transaction::where('transaction_id', $transactions[0]->id)
@@ -676,7 +675,7 @@
// Assert all entitlements' updated_at timestamp
$date = $backdate->addMonthsWithoutOverflow(1);
- $this->assertSame(9, $wallet->entitlements()->where('updated_at', $date)->count());
+ $this->assertSame(14, $wallet->entitlements()->where('updated_at', $date)->count());
// There should be only one transaction at this point (for the reseller wallet)
$this->assertSame(1, Transaction::count());
}
@@ -742,7 +741,7 @@
// Assert all entitlements' updated_at timestamp
$date = $backdate->addMonthsWithoutOverflow(1);
- $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get());
+ $this->assertCount(17, $wallet->entitlements()->where('updated_at', $date)->get());
// Run again, expect no changes
$charge = $wallet->chargeEntitlements();
@@ -751,7 +750,7 @@
$this->assertSame(0, $charge);
$this->assertSame($balance, $wallet->balance);
$this->assertCount(1, $wallet->transactions()->get());
- $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get());
+ $this->assertCount(17, $wallet->entitlements()->where('updated_at', $date)->get());
// -----------------------------------
// Test charging deleted entitlements
@@ -944,7 +943,7 @@
// Assert all entitlements' updated_at timestamp
$date = $now->copy()->setTimeFrom($backdate);
- $this->assertCount(7, $wallet->entitlements()->where('updated_at', $date)->get());
+ $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get());
$reseller_transactions = $reseller_wallet->transactions()->get();
$this->assertCount(1, $reseller_transactions);
@@ -992,7 +991,7 @@
// Assert all entitlements' updated_at timestamp
$date = $now->copy()->setTimeFrom($backdate);
- $this->assertCount(7, $wallet->entitlements()->where('updated_at', $date)->get());
+ $this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get());
// Assert per-entitlement transactions
$groupware_entitlement = $user->entitlements->where('sku_id', '===', $groupware->id)->first();
diff --git a/src/tests/TestCaseFs.php b/src/tests/TestCaseFs.php
--- a/src/tests/TestCaseFs.php
+++ b/src/tests/TestCaseFs.php
@@ -161,4 +161,25 @@
// TODO: Make sure this does not use "acting user" set earlier
return $this->call($method, $uri, [], $cookies, [], $server, $content);
}
+
+ /**
+ * Fake user files size over-quota
+ */
+ protected static function turnUserOverQuota($user): void
+ {
+ // Assume user has 5GB quota for now
+ $user->fsItems()->where('type', '&', Item::TYPE_FILE)->first()
+ ->chunks()->createMany([
+ [
+ 'chunk_id' => Utils::uuidStr(),
+ 'sequence' => 2,
+ 'size' => 2 * 1024 * 1024 * 1024,
+ ],
+ [
+ 'chunk_id' => Utils::uuidStr(),
+ 'sequence' => 3,
+ 'size' => 2 * 1024 * 1024 * 1024,
+ ],
+ ]);
+ }
}
diff --git a/src/tests/Unit/Fs/QuotaTest.php b/src/tests/Unit/Fs/QuotaTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Fs/QuotaTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Tests\Unit\Fs;
+
+use App\Fs\Quota;
+use Tests\TestCase;
+
+class QuotaTest extends TestCase
+{
+ /**
+ * Test Quota::bytes()
+ */
+ public function testBytes()
+ {
+ $this->assertSame('0 B', Quota::bytes(0));
+ $this->assertSame('101 B', Quota::bytes(101));
+ $this->assertSame('1.0 KB', Quota::bytes(1024));
+ $this->assertSame('1.5 KB', Quota::bytes(1500));
+ $this->assertSame('1.0 MB', Quota::bytes(1024 * 1024 - 1));
+ $this->assertSame('1.0 MB', Quota::bytes(1024 * 1024));
+ $this->assertSame('1.0 GB', Quota::bytes(1024 * 1024 * 1024 - 1));
+ $this->assertSame('1.0 GB', Quota::bytes(1024 * 1024 * 1024));
+ $this->assertSame('1.1 GB', Quota::bytes(1024 * 1024 * 1024 + 1024 * 1024 * 100));
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Apr 2, 11:37 PM (20 h, 26 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821518
Default Alt Text
D5766.1775173044.diff (78 KB)
Attached To
Mode
D5766: Files Quota
Attached
Detach File
Event Timeline