diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php --- a/src/app/Console/Command.php +++ b/src/app/Console/Command.php @@ -126,17 +126,6 @@ return $model; } - $modelsWithTenant = [ - \App\Discount::class, - \App\Domain::class, - \App\Group::class, - \App\Package::class, - \App\Plan::class, - \App\Resource::class, - \App\Sku::class, - \App\User::class, - ]; - $modelsWithOwner = [ \App\Wallet::class, ]; @@ -144,7 +133,7 @@ $tenantId = \config('app.tenant_id'); // Add tenant filter - if (in_array($objectClass, $modelsWithTenant)) { + if (in_array(\App\Traits\BelongsToTenantTrait::class, class_uses($objectClass))) { $model = $model->withEnvTenantContext(); } elseif (in_array($objectClass, $modelsWithOwner)) { $model = $model->whereExists(function ($query) use ($tenantId) { @@ -172,6 +161,19 @@ } /** + * Find a shared folder. + * + * @param string $folder Folder ID or email + * @param bool $withDeleted Include deleted + * + * @return \App\SharedFolder|null + */ + public function getSharedFolder($folder, $withDeleted = false) + { + return $this->getObject(\App\SharedFolder::class, $folder, 'email', $withDeleted); + } + + /** * Find the user. * * @param string $user User ID or email diff --git a/src/app/Console/Commands/Scalpel/SharedFolder/CreateCommand.php b/src/app/Console/Commands/Scalpel/SharedFolder/CreateCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Scalpel/SharedFolder/CreateCommand.php @@ -0,0 +1,15 @@ +argument('domain'); + $name = $this->argument('name'); + $type = $this->option('type'); + $acl = $this->option('acl'); + + if (empty($type)) { + $type = 'mail'; + } + + $domain = $this->getDomain($domainName); + + if (!$domain) { + $this->error("No such domain {$domainName}."); + return 1; + } + + if ($domain->isPublic()) { + $this->error("Domain {$domainName} is public."); + return 1; + } + + $owner = $domain->wallet()->owner; + + // Validate folder name and type + $rules = [ + 'name' => ['required', 'string', new SharedFolderName($owner, $domain->namespace)], + 'type' => ['required', 'string', new SharedFolderType()] + ]; + + $v = Validator::make(['name' => $name, 'type' => $type], $rules); + + if ($v->fails()) { + $this->error($v->errors()->all()[0]); + return 1; + } + + DB::beginTransaction(); + + // Create the shared folder + $folder = new SharedFolder(); + $folder->name = $name; + $folder->type = $type; + $folder->domain = $domainName; + $folder->save(); + + $folder->assignToWallet($owner->wallets->first()); + + if (!empty($acl)) { + $errors = $folder->setConfig(['acl' => $acl]); + + if (!empty($errors)) { + $this->error("Invalid --acl entry."); + DB::rollBack(); + return 1; + } + } + + DB::commit(); + + $this->info($folder->id); + } +} diff --git a/src/app/Console/Commands/SharedFolder/DeleteCommand.php b/src/app/Console/Commands/SharedFolder/DeleteCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/SharedFolder/DeleteCommand.php @@ -0,0 +1,40 @@ +argument('folder'); + $folder = $this->getSharedFolder($input); + + if (empty($folder)) { + $this->error("Shared folder {$input} does not exist."); + return 1; + } + + $folder->delete(); + } +} diff --git a/src/app/Console/Commands/SharedFolder/ForceDeleteCommand.php b/src/app/Console/Commands/SharedFolder/ForceDeleteCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/SharedFolder/ForceDeleteCommand.php @@ -0,0 +1,47 @@ +getSharedFolder($this->argument('folder'), true); + + if (!$folder) { + $this->error("Shared folder not found."); + return 1; + } + + if (!$folder->trashed()) { + $this->error("The shared folder is not yet deleted."); + return 1; + } + + DB::beginTransaction(); + $folder->forceDelete(); + DB::commit(); + } +} diff --git a/src/tests/Feature/Console/SharedFolder/CreateTest.php b/src/tests/Feature/Console/SharedFolder/CreateTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/SharedFolder/CreateTest.php @@ -0,0 +1,92 @@ +where('name', 'Tasks')->first()) { + $folder->forceDelete(); + } + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + if ($folder = SharedFolder::withTrashed()->where('name', 'Tasks')->first()) { + $folder->forceDelete(); + } + + parent::tearDown(); + } + + /** + * Test command runs + */ + public function testHandle(): void + { + Queue::fake(); + + // Warning: We're not using artisan() here, as this will not + // allow us to test "empty output" cases + + $user = $this->getTestUser('john@kolab.org'); + + // Domain not existing + $code = \Artisan::call("sharedfolder:create unknown.org test"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("No such domain unknown.org.", $output); + + // Public domain not allowed + $code = \Artisan::call("sharedfolder:create kolabnow.com Test --type=event"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("Domain kolabnow.com is public.", $output); + + // Existing folder + $code = \Artisan::call("sharedfolder:create kolab.org Calendar"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("The specified name is not available.", $output); + + // Invalid type + $code = \Artisan::call("sharedfolder:create kolab.org Test --type=unknown"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("The specified type is invalid.", $output); + + // Invalid acl + $code = \Artisan::call("sharedfolder:create kolab.org Test --type=task --acl=\"anyone,unknown\""); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Invalid --acl entry.", $output); + $this->assertSame(0, SharedFolder::where('name', 'Test')->count()); + + // Create a folder + $acl = '--acl="anyone, read-only" --acl="jack@kolab.org, full"'; + $code = \Artisan::call("sharedfolder:create kolab.org Tasks --type=task $acl"); + $output = trim(\Artisan::output()); + + $folder = SharedFolder::find($output); + + $this->assertSame(0, $code); + $this->assertSame('Tasks', $folder->name); + $this->assertSame('task', $folder->type); + $this->assertSame($user->wallets->first()->id, $folder->wallet()->id); + $this->assertSame(['anyone, read-only', 'jack@kolab.org, full'], $folder->getConfig()['acl']); + } +} diff --git a/src/tests/Feature/Console/SharedFolder/DeleteTest.php b/src/tests/Feature/Console/SharedFolder/DeleteTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/SharedFolder/DeleteTest.php @@ -0,0 +1,61 @@ +deleteTestSharedFolder('folder-test@kolabnow.com'); + $this->deleteTestUser('folder-owner@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestSharedFolder('folder-test@kolabnow.com'); + $this->deleteTestUser('folder-owner@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command runs + */ + public function testHandle(): void + { + Queue::fake(); + + // Warning: We're not using artisan() here, as this will not + // allow us to test "empty output" cases + + // Non-existing folder + $code = \Artisan::call("sharedfolder:delete test@folder.com"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Shared folder test@folder.com does not exist.", $output); + + $user = $this->getTestUser('folder-owner@kolabnow.com'); + $folder = $this->getTestSharedFolder('folder-test@kolabnow.com'); + + // Existing folder + $code = \Artisan::call("sharedfolder:delete {$folder->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame('', $output); + $this->assertTrue($folder->refresh()->trashed()); + } +} diff --git a/src/tests/Feature/Console/SharedFolder/ForceDeleteTest.php b/src/tests/Feature/Console/SharedFolder/ForceDeleteTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/SharedFolder/ForceDeleteTest.php @@ -0,0 +1,71 @@ +deleteTestSharedFolder('folder-test@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestSharedFolder('folder-test@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command runs + */ + public function testHandle(): void + { + Queue::fake(); + + // Warning: We're not using artisan() here, as this will not + // allow us to test "empty output" cases + + $folder = $this->getTestSharedFolder('folder-test@kolabnow.com'); + + // Non-existing folder + $code = \Artisan::call("sharedfolder:force-delete test@folder.com"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Shared folder not found.", $output); + + // Non-deleted folder + $code = \Artisan::call("sharedfolder:force-delete {$folder->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("The shared folder is not yet deleted.", $output); + + $folder->delete(); + $this->assertTrue($folder->trashed()); + + // Existing and deleted folder + $code = \Artisan::call("sharedfolder:force-delete {$folder->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame('', $output); + $this->assertCount( + 0, + SharedFolder::withTrashed()->where('email', $folder->email)->get() + ); + } +}