From b58b068434718c00679ee0dffad371b13fc33e13 Mon Sep 17 00:00:00 2001 From: Jasper Zonneveld Date: Wed, 3 Nov 2021 10:21:51 +0100 Subject: [PATCH 01/51] Add missing import for Domain model (#745) --- src/TenancyServiceProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index cf326792..4faaccf3 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy; use Illuminate\Cache\CacheManager; use Illuminate\Support\ServiceProvider; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; +use Stancl\Tenancy\Contracts\Domain; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Resolvers\DomainTenantResolver; From f12c826df52c91fa5a024e8b1982b1056baed728 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Thu, 18 Nov 2021 01:45:44 +0500 Subject: [PATCH 02/51] Use GitHub forms for issues template. (#755) * Create bug-report_new.md * wip * Delete bug-report.md * Update bug-report.yml --- .github/ISSUE_TEMPLATE/bug-report.md | 21 ------------ .github/ISSUE_TEMPLATE/bug-report.yml | 48 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 21 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 9f3d2e65..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: "\U0001F41B Bug Report" -about: Report unexpected behavior with stancl/tenancy. -title: '' -labels: bug -assignees: stancl - ---- - -#### Describe the bug - - -#### Steps to reproduce - - -#### Expected behavior -A clear and concise description of what you expected to happen. - -#### Your setup - - Laravel version: [e.g. 8.2.0] - - stancl/tenancy version: [e.g. 3.1.0] diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..75e345b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,48 @@ +name: 🐛 Bug Report +description: Report unexpected behavior with stancl/tenancy. +labels: ["bug"] +assignees: + - stancl +body: + - type: markdown + attributes: + value: | + Before opening a bug report, please search for the behaviour in the existing issues. + --- + Thank you for taking the time to file a bug report. To address this bug as fast as possible, we need some information. + - type: textarea + id: bug-description + attributes: + label: Bug description + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Step-by-step guide for reproducing the bug in a fresh Laravel application. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: input + id: laravel-version + attributes: + label: Laravel version + placeholder: "e.g. 8.2.0" + validations: + required: true + - type: input + id: tenancy-version + attributes: + label: stancl/tenancy version + placeholder: "e.g. 3.1.0" + validations: + required: true From 98832195448eb781235d9e1abcfa1de4b44104c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Mon, 13 Dec 2021 13:09:16 +0100 Subject: [PATCH 03/51] wip --- .github/workflows/ci.yml | 2 +- composer.json | 18 ++++++++++++------ .../FilesystemTenancyBootstrapper.php | 3 +++ tests/Etc/ConsoleKernel.php | 7 +------ 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efb8ad02..37e3d449 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - php: ["7.4", "8.0"] + php: ["7.4", "8.0.11"] laravel: ["^6.0", "^8.0"] steps: diff --git a/composer.json b/composer.json index b10f2d16..faeb3d5e 100644 --- a/composer.json +++ b/composer.json @@ -11,17 +11,17 @@ ], "require": { "ext-json": "*", - "illuminate/support": "^6.0|^7.0|^8.0", + "illuminate/support": "dev-master", "facade/ignition-contracts": "^1.0", "ramsey/uuid": "^3.7|^4.0", - "stancl/jobpipeline": "^1.0", - "stancl/virtualcolumn": "^1.0" + "stancl/jobpipeline": "dev-master", + "stancl/virtualcolumn": "dev-master" }, "require-dev": { "vlucas/phpdotenv": "^3.3|^4.0|^5.0", - "laravel/framework": "^6.0|^7.0|^8.0", - "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0", - "league/flysystem-aws-s3-v3": "~1.0", + "laravel/framework": "dev-master", + "orchestra/testbench-browser-kit": "dev-master", + "league/flysystem-aws-s3-v3": "*", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5" }, @@ -49,6 +49,12 @@ } } }, + "scripts": { + "docker-up": "PHP_VERSION=8.0.11 docker-compose up -d", + "docker-down": "PHP_VERSION=8.0.11 docker-compose down", + "docker-rebuild": "PHP_VERSION=8.0.11 docker-compose up -d --no-deps --build", + "test": "./test" + }, "minimum-stability": "dev", "prefer-stable": true } diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index d5ae2d50..157a1337 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -57,6 +57,9 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { /** @var FilesystemAdapter $filesystemDisk */ $filesystemDisk = Storage::disk($disk); + + // todo0 @v4 \League\Flysystem\PathPrefixer is making this a lot more painful in flysystem v2 + $this->originalPaths['disks'][$disk] = $filesystemDisk->getAdapter()->getPathPrefix(); if ($root = str_replace( diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/ConsoleKernel.php index 1bc66365..a548f113 100644 --- a/tests/Etc/ConsoleKernel.php +++ b/tests/Etc/ConsoleKernel.php @@ -4,15 +4,10 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests\Etc; -use Orchestra\Testbench\Console\Kernel; +use Orchestra\Testbench\Foundation\Console\Kernel; class ConsoleKernel extends Kernel { - /** - * The Artisan commands provided by your application. - * - * @var array - */ protected $commands = [ ExampleCommand::class, AddUserCommand::class, From 2726f07bcaecca6351ad2716a04df591042f4972 Mon Sep 17 00:00:00 2001 From: Frederic Habich <53627251+CodeAdminDe@users.noreply.github.com> Date: Wed, 22 Dec 2021 13:24:07 +0100 Subject: [PATCH 04/51] fixed typo (#766) fixed typo within description 'searhced' => 'searched' --- src/Features/UniversalRoutes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/UniversalRoutes.php b/src/Features/UniversalRoutes.php index 970dcd7b..6b729962 100644 --- a/src/Features/UniversalRoutes.php +++ b/src/Features/UniversalRoutes.php @@ -40,7 +40,7 @@ class UniversalRoutes implements Feature } // Loop one level deep and check if the route's middleware - // groups have the searhced middleware group inside them + // groups have the searched middleware group inside them $middlewareGroups = Router::getMiddlewareGroups(); foreach ($route->gatherMiddleware() as $inner) { if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) { From 08bfd6f9bb04e5364f6d9855e216c9c1bfcf54de Mon Sep 17 00:00:00 2001 From: sort72 <47725212+sort72@users.noreply.github.com> Date: Sat, 25 Dec 2021 09:24:34 -0500 Subject: [PATCH 05/51] Use tenant key on console commands instead of id (#768) --- src/Commands/Migrate.php | 2 +- src/Commands/Rollback.php | 2 +- src/Commands/Run.php | 2 +- src/Commands/Seed.php | 2 +- src/Commands/TenantList.php | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 4bf8408c..bf92dfcd 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -55,7 +55,7 @@ class Migrate extends MigrateCommand } tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); event(new MigratingDatabase($tenant)); diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index ec9cc461..081872c8 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -53,7 +53,7 @@ class Rollback extends RollbackCommand } tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); event(new RollingBackDatabase($tenant)); diff --git a/src/Commands/Run.php b/src/Commands/Run.php index c2770825..4216d1c6 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -33,7 +33,7 @@ class Run extends Command public function handle() { tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); tenancy()->initialize($tenant); $callback = function ($prefix = '') { diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 43038107..dc97ae71 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -51,7 +51,7 @@ class Seed extends SeedCommand } tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { - $this->line("Tenant: {$tenant['id']}"); + $this->line("Tenant: {$tenant->getTenantKey()}"); event(new SeedingDatabase($tenant)); diff --git a/src/Commands/TenantList.php b/src/Commands/TenantList.php index 493b5a93..d01afcb9 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -36,9 +36,9 @@ class TenantList extends Command ->cursor() ->each(function (Tenant $tenant) { if ($tenant->domains) { - $this->line("[Tenant] id: {$tenant['id']} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); + $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()} @ " . implode('; ', $tenant->domains->pluck('domain')->toArray() ?? [])); } else { - $this->line("[Tenant] id: {$tenant['id']}"); + $this->line("[Tenant] {$tenant->getTenantKeyName()}: {$tenant->getTenantKey()}"); } }); } From 435d8528a75acdbdc2994962bc47e90f4288aed1 Mon Sep 17 00:00:00 2001 From: Stefan Ninic Date: Sat, 25 Dec 2021 22:10:34 +0100 Subject: [PATCH 06/51] Fixed array to string conversion (#718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed array to string conversion Previous code would give this warning before actually showing exception message `PHP Warning: Array to string conversion in .../vendor/stancl/tenancy/src/CacheManager.php on line 24` * Update variable & syntax Co-authored-by: Samuel Štancl --- src/CacheManager.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CacheManager.php b/src/CacheManager.php index f7190842..88428353 100644 --- a/src/CacheManager.php +++ b/src/CacheManager.php @@ -20,8 +20,10 @@ class CacheManager extends BaseCacheManager $tags = [config('tenancy.cache.tag_base') . tenant()->getTenantKey()]; if ($method === 'tags') { - if (count($parameters) !== 1) { - throw new \Exception("Method tags() takes exactly 1 argument. {count($parameters)} passed."); + $count = count($parameters); + + if ($count !== 1) { + throw new \Exception("Method tags() takes exactly 1 argument. $count passed."); } $names = $parameters[0]; From 5980c46449482169890618ca697dfc8809e321be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 30 Dec 2021 19:46:48 +0100 Subject: [PATCH 07/51] wip --- composer.json | 2 +- src/Exceptions/TenantCouldNotBeIdentifiedById.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index faeb3d5e..1eeed2bf 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "docker-up": "PHP_VERSION=8.0.11 docker-compose up -d", "docker-down": "PHP_VERSION=8.0.11 docker-compose down", "docker-rebuild": "PHP_VERSION=8.0.11 docker-compose up -d --no-deps --build", - "test": "./test" + "test": "PHP_VERSION=8.0.11 ./test" }, "minimum-stability": "dev", "prefer-stable": true diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedById.php b/src/Exceptions/TenantCouldNotBeIdentifiedById.php index 8fa103ea..5c2e562c 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedById.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedById.php @@ -9,7 +9,7 @@ use Facade\IgnitionContracts\ProvidesSolution; use Facade\IgnitionContracts\Solution; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; -// todo: in v3 this should be suffixed with Exception +// todo: in v4 this should be suffixed with Exception class TenantCouldNotBeIdentifiedById extends TenantCouldNotBeIdentifiedException implements ProvidesSolution { public function __construct($tenant_id) From 73a4a3018cadca2ba0fb5f2130fca1718a2b3670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:10:03 +0100 Subject: [PATCH 08/51] Improve queue tenancy --- docker-compose.yml | 2 +- .../DatabaseTenancyBootstrapper.php | 1 + .../QueueTenancyBootstrapper.php | 80 +++++++-- src/Database/DatabaseManager.php | 23 ++- .../TenantCouldNotBeIdentifiedById.php | 2 +- src/Tenancy.php | 4 +- tests/QueueTest.php | 152 ++++++++++++++++-- tests/TenantDatabaseManagerTest.php | 51 ++++++ 8 files changed, 279 insertions(+), 36 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 30d87dfd..e8e8d418 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: build: context: . args: - PHP_VERSION: ${PHP_VERSION} + PHP_VERSION: ${PHP_VERSION:-8.1} depends_on: mysql: condition: service_healthy diff --git a/src/Bootstrappers/DatabaseTenancyBootstrapper.php b/src/Bootstrappers/DatabaseTenancyBootstrapper.php index a107fc0d..59ee0aec 100644 --- a/src/Bootstrappers/DatabaseTenancyBootstrapper.php +++ b/src/Bootstrappers/DatabaseTenancyBootstrapper.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Bootstrappers; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; +use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException; diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 6fefaad2..5706963e 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -7,7 +7,10 @@ namespace Stancl\Tenancy\Bootstrappers; use Illuminate\Config\Repository; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Queue\Events\JobFailed; +use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Queue\Events\JobRetryRequested; use Illuminate\Queue\QueueManager; use Illuminate\Support\Testing\Fakes\QueueFake; use Stancl\Tenancy\Contracts\TenancyBootstrapper; @@ -28,7 +31,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper */ public static function __constructStatic(Application $app) { - static::setUpJobListener($app->make(Dispatcher::class)); + static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests()); } public function __construct(Repository $config, QueueManager $queue) @@ -39,25 +42,70 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper $this->setUpPayloadGenerator(); } - protected static function setUpJobListener($dispatcher) + protected static function setUpJobListener($dispatcher, $runningTests) { - $dispatcher->listen(JobProcessing::class, function ($event) { - $tenantId = $event->job->payload()['tenant_id'] ?? null; + $previousTenant = null; - // The job is not tenant-aware - if (! $tenantId) { - return; - } + $dispatcher->listen(JobProcessing::class, function ($event) use (&$previousTenant) { + $previousTenant = tenant(); - // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) - if (tenancy()->initialized && tenant()->getTenantKey() === $tenantId) { - return; - } - - // Tenancy was either not initialized, or initialized for a different tenant. - // Therefore, we initialize it for the correct tenant. - tenancy()->initialize(tenancy()->find($tenantId)); + static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); }); + + $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { + $previousTenant = tenant(); + + static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); + }); + + // If we're running tests, we make sure to clean up after any artisan('queue:work') calls + $revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) { + if ($runningTests) { + static::revertToPreviousState($event, $previousTenant); + } + }; + + $dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds + $dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails + } + + protected static function initializeTenancyForQueue($tenantId) + { + // The job is not tenant-aware + if (! $tenantId) { + return; + } + + if (tenancy()->initialized) { + if (tenant()->getTenantKey() === $tenantId) { + // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) + return; + } + } + + // Tenancy was either not initialized, or initialized for a different tenant. + // Therefore, we initialize it for the correct tenant. + tenancy()->initialize(tenancy()->find($tenantId)); + } + + protected static function revertToPreviousState($event, &$previousTenant) + { + $tenantId = $event->job->payload()['tenant_id'] ?? null; + + // The job was not tenant-aware + if (! $tenantId) { + return; + } + + // Revert back to the previous tenant + if (tenant() && $previousTenant && $previousTenant->isNot(tenant())) { + tenancy()->initialize($previousTenant); + } + + // End tenancy + if (tenant() && (! $previousTenant)) { + tenancy()->end(); + } } protected function setUpPayloadGenerator() diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index dd30f443..e85fd659 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -38,7 +38,7 @@ class DatabaseManager */ public function connectToTenant(TenantWithDatabase $tenant) { - $this->database->purge('tenant'); + $this->purgeTenantConnection(); $this->createTenantConnection($tenant); $this->setDefaultConnection('tenant'); } @@ -48,10 +48,7 @@ class DatabaseManager */ public function reconnectToCentral() { - if (tenancy()->initialized) { - $this->database->purge('tenant'); - } - + $this->purgeTenantConnection(); $this->setDefaultConnection($this->config->get('tenancy.database.central_connection')); } @@ -60,7 +57,7 @@ class DatabaseManager */ public function setDefaultConnection(string $connection) { - $this->app['config']['database.default'] = $connection; + $this->config['database.default'] = $connection; $this->database->setDefaultConnection($connection); } @@ -69,7 +66,19 @@ class DatabaseManager */ public function createTenantConnection(TenantWithDatabase $tenant) { - $this->app['config']['database.connections.tenant'] = $tenant->database()->connection(); + $this->config['database.connections.tenant'] = $tenant->database()->connection(); + } + + /** + * Purge the tenant database connection. + */ + public function purgeTenantConnection() + { + if (array_key_exists('tenant', $this->database->getConnections())) { + $this->database->purge('tenant'); + } + + unset($this->config['database.connections.tenant']); } /** diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedById.php b/src/Exceptions/TenantCouldNotBeIdentifiedById.php index 8fa103ea..5c2e562c 100644 --- a/src/Exceptions/TenantCouldNotBeIdentifiedById.php +++ b/src/Exceptions/TenantCouldNotBeIdentifiedById.php @@ -9,7 +9,7 @@ use Facade\IgnitionContracts\ProvidesSolution; use Facade\IgnitionContracts\Solution; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; -// todo: in v3 this should be suffixed with Exception +// todo: in v4 this should be suffixed with Exception class TenantCouldNotBeIdentifiedById extends TenantCouldNotBeIdentifiedException implements ProvidesSolution { public function __construct($tenant_id) diff --git a/src/Tenancy.php b/src/Tenancy.php index 864c00f0..30f138e3 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -66,10 +66,10 @@ class Tenancy return; } - $this->initialized = false; - event(new Events\TenancyEnded($this)); + $this->initialized = false; + $this->tenant = null; } diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 41d71320..75c727ce 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -4,18 +4,30 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; use Spatie\Valuestore\Valuestore; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; +use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Tests\Etc\User; class QueueTest extends TestCase { @@ -31,15 +43,49 @@ class QueueTest extends TestCase config([ 'tenancy.bootstrappers' => [ QueueTenancyBootstrapper::class, + DatabaseTenancyBootstrapper::class, ], 'queue.default' => 'redis', ]); Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush(); } + protected function withFailedJobs() + { + Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + protected function withUsers() + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + protected function withTenantDatabases() + { + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + } + /** @test */ public function tenant_id_is_passed_to_tenant_queues() { @@ -49,7 +95,7 @@ class QueueTest extends TestCase tenancy()->initialize($tenant); - Event::fake([JobProcessing::class]); + Event::fake([JobProcessing::class, JobProcessed::class]); dispatch(new TestJob($this->valuestore)); @@ -79,21 +125,91 @@ class QueueTest extends TestCase }); } - /** @test */ - public function tenancy_is_initialized_inside_queues() + /** + * @test + * + * @testWith [true] + * [false] + */ + public function tenancy_is_initialized_inside_queues(bool $shouldEndTenancy) { - $tenant = Tenant::create([ - 'id' => 'acme', - ]); + $this->withTenantDatabases(); + $this->withFailedJobs(); + + $tenant = Tenant::create(); tenancy()->initialize($tenant); - dispatch(new TestJob($this->valuestore)); + $this->withUsers(); + + $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + + $this->valuestore->put('userName', 'Bar'); + + dispatch(new TestJob($this->valuestore, $user)); $this->assertFalse($this->valuestore->has('tenant_id')); + + if ($shouldEndTenancy) { + tenancy()->end(); + } + $this->artisan('queue:work --once'); - $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); + $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count()); + + $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id')); + + $tenant->run(function () use ($user) { + $this->assertSame('Bar', $user->fresh()->name); + }); + } + + /** + * @test + * + * @testWith [true] + * [false] + */ + public function tenancy_is_initialized_when_retrying_jobs(bool $shouldEndTenancy) + { + $this->withFailedJobs(); + $this->withTenantDatabases(); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + $this->withUsers(); + + $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + + $this->valuestore->put('userName', 'Bar'); + $this->valuestore->put('shouldFail', true); + + dispatch(new TestJob($this->valuestore, $user)); + + $this->assertFalse($this->valuestore->has('tenant_id')); + + if ($shouldEndTenancy) { + tenancy()->end(); + } + + $this->artisan('queue:work --once'); + + $this->assertSame(1, DB::connection('central')->table('failed_jobs')->count()); + $this->assertNull($this->valuestore->get('tenant_id')); // job failed + + $this->artisan('queue:retry all'); + $this->artisan('queue:work --once'); + + $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count()); + + $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id')); // job succeeded + + $tenant->run(function () use ($user) { + $this->assertSame('Bar', $user->fresh()->name); + }); } /** @test */ @@ -127,13 +243,31 @@ class TestJob implements ShouldQueue /** @var Valuestore */ protected $valuestore; - public function __construct(Valuestore $valuestore) + /** @var User|null */ + protected $user; + + public function __construct(Valuestore $valuestore, User $user = null) { $this->valuestore = $valuestore; + $this->user = $user; } public function handle() { + if ($this->valuestore->get('shouldFail')) { + $this->valuestore->put('shouldFail', false); + + throw new Exception('failing'); + } + + if ($this->user) { + assert($this->user->getConnectionName() === 'tenant'); + } + $this->valuestore->put('tenant_id', 'The current tenant id is: ' . tenant('id')); + + if ($userName = $this->valuestore->get('userName')) { + $this->user->update(['name' => $userName]); + } } } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index ead2bba8..f64770b1 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -4,17 +4,21 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use PDO; use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Database\DatabaseManager; +use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager; @@ -102,6 +106,52 @@ class TenantDatabaseManagerTest extends TestCase ]; } + /** @test */ + public function the_tenant_connection_is_fully_removed() + { + config([ + 'tenancy.boostrappers' => [ + DatabaseTenancyBootstrapper::class, + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + $tenant = Tenant::create(); + + $this->assertSame(['central'], array_keys(app('db')->getConnections())); + $this->assertArrayNotHasKey('tenant', config('database.connections')); + + tenancy()->initialize($tenant); + + $this->createUsersTable(); + + $this->assertSame(['central', 'tenant'], array_keys(app('db')->getConnections())); + $this->assertArrayHasKey('tenant', config('database.connections')); + + tenancy()->end(); + + $this->assertSame(['central'], array_keys(app('db')->getConnections())); + $this->assertNull(config('database.connections.tenant')); + } + + protected function createUsersTable() + { + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + /** @test */ public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() { @@ -217,5 +267,6 @@ class TenantDatabaseManagerTest extends TestCase /** @test */ public function path_used_by_sqlite_manager_can_be_customized() { + $this->markTestIncomplete(); } } From 49ef28da059bb0423f9ccd8f9f88b43cd58cfb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:19:53 +0100 Subject: [PATCH 09/51] 6.x support --- tests/QueueTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 75c727ce..9758c9c8 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -57,7 +57,7 @@ class QueueTest extends TestCase protected function withFailedJobs() { Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { - $table->id(); + $table->increments('id'); $table->string('uuid')->unique(); $table->text('connection'); $table->text('queue'); From a83568ded280ebca364ad898f28db2ec047f87a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:28:37 +0100 Subject: [PATCH 10/51] Only use JobRetryRequested in Laravel 8 --- .../QueueTenancyBootstrapper.php | 20 ++++++---- tests/Etc/tmp/queuetest.json | 2 +- tests/QueueTest.php | 37 +++++++++++-------- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 5706963e..c94d6749 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -4,17 +4,18 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; +use Illuminate\Support\Str; use Illuminate\Config\Repository; -use Illuminate\Contracts\Events\Dispatcher; -use Illuminate\Contracts\Foundation\Application; +use Illuminate\Queue\QueueManager; +use Stancl\Tenancy\Contracts\Tenant; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\Events\JobRetryRequested; -use Illuminate\Queue\QueueManager; use Illuminate\Support\Testing\Fakes\QueueFake; +use Illuminate\Contracts\Foundation\Application; use Stancl\Tenancy\Contracts\TenancyBootstrapper; -use Stancl\Tenancy\Contracts\Tenant; class QueueTenancyBootstrapper implements TenancyBootstrapper { @@ -52,11 +53,14 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); }); - $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { - $previousTenant = tenant(); + if (Str::startsWith(app()->version(), '8')) { + // queue:retry tenancy is only supported in Laravel 8 + $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { + $previousTenant = tenant(); - static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); - }); + static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); + }); + } // If we're running tests, we make sure to clean up after any artisan('queue:work') calls $revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) { diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json index 00cf7c37..dc158c6b 100644 --- a/tests/Etc/tmp/queuetest.json +++ b/tests/Etc/tmp/queuetest.json @@ -1 +1 @@ -{"tenant_id":"The current tenant id is: acme"} \ No newline at end of file +{"userName":"Bar","shouldFail":false,"tenant_id":"The current tenant id is: a7f73c10-9879-40ae-b7b0-1ded7c1f7b1b"} \ No newline at end of file diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 9758c9c8..158ad56b 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -5,29 +5,30 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; use Exception; +use Illuminate\Support\Str; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use Spatie\Valuestore\Valuestore; +use Illuminate\Support\Facades\DB; +use Stancl\Tenancy\Tests\Etc\User; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Support\Facades\Event; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Schema; +use Stancl\Tenancy\Events\TenancyEnded; +use Stancl\Tenancy\Jobs\CreateDatabase; +use Illuminate\Queue\InteractsWithQueue; +use Stancl\Tenancy\Events\TenantCreated; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Schema; -use Spatie\Valuestore\Valuestore; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; -use Stancl\Tenancy\Events\TenancyEnded; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Events\TenantCreated; -use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Tests\Etc\Tenant; -use Stancl\Tenancy\Tests\Etc\User; +use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; class QueueTest extends TestCase { @@ -173,6 +174,10 @@ class QueueTest extends TestCase */ public function tenancy_is_initialized_when_retrying_jobs(bool $shouldEndTenancy) { + if (! Str::startsWith(app()->version(), '8')) { + $this->markTestSkipped('queue:retry tenancy is only supported in Laravel 8'); + } + $this->withFailedJobs(); $this->withTenantDatabases(); From e442bdb64419038758d4fd6e10946735050b5a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 31 Dec 2021 18:29:05 +0100 Subject: [PATCH 11/51] Only use JobRetryRequested in Laravel 8 --- src/Bootstrappers/QueueTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index c94d6749..d2ce52d0 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -54,7 +54,7 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper }); if (Str::startsWith(app()->version(), '8')) { - // queue:retry tenancy is only supported in Laravel 8 + // JobRetryRequested only exists since Laravel 8 $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { $previousTenant = tenant(); From 96d9ad13d821b1619062e930c2bce54642b1ffd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 6 Jan 2022 16:57:01 +0100 Subject: [PATCH 12/51] Add a note about 'tenant' connection being reserved (fixes #774) --- assets/config.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/config.php b/assets/config.php index 029591ad..85592d14 100644 --- a/assets/config.php +++ b/assets/config.php @@ -42,7 +42,8 @@ return [ 'central_connection' => env('DB_CONNECTION', 'central'), /** - * Connection used as a "template" for the tenant database connection. + * Connection used as a "template" for the dynamically created tenant database connection. + * Note: don't name your template connection tenant. That name is reserved by package. */ 'template_tenant_connection' => null, From f08e33afd80d14d99eb0595261cb100f981d4b52 Mon Sep 17 00:00:00 2001 From: Jori Stein <44996807+stein-j@users.noreply.github.com> Date: Thu, 6 Jan 2022 21:35:56 +0100 Subject: [PATCH 13/51] Remove redondant initialization (#775) --- src/Commands/Run.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 4216d1c6..aa518d7a 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -34,7 +34,6 @@ class Run extends Command { tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { $this->line("Tenant: {$tenant->getTenantKey()}"); - tenancy()->initialize($tenant); $callback = function ($prefix = '') { return function ($arguments, $argument) use ($prefix) { From 9c79267e2444b5df4c32123f62387b71953f0a26 Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Mon, 14 Feb 2022 14:31:01 +0100 Subject: [PATCH 14/51] Fix .env loading in development (#799) * Upgrade vlucas/phpdotenv to ^5.0 `Dotenv::create($paths)` was the syntax for releases before v4 * Remove vlucas/phpdotenv dependency and make it work with newer versions. --- composer.json | 1 - tests/TestCase.php | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b10f2d16..bf66e1f2 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,6 @@ "stancl/virtualcolumn": "^1.0" }, "require-dev": { - "vlucas/phpdotenv": "^3.3|^4.0|^5.0", "laravel/framework": "^6.0|^7.0|^8.0", "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0", "league/flysystem-aws-s3-v3": "~1.0", diff --git a/tests/TestCase.php b/tests/TestCase.php index bbc42489..d3e42ea1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -48,7 +48,11 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase protected function getEnvironmentSetUp($app) { if (file_exists(__DIR__ . '/../.env')) { - \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); + if (method_exists(\Dotenv\Dotenv::class, 'createImmutable')) { + \Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->load(); + } else { + \Dotenv\Dotenv::create(__DIR__ . '/..')->load(); + } } $app['config']->set([ From 27f916c3239fb4dbad44f1cfa74fbde4bd9d656d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:12:38 +0100 Subject: [PATCH 15/51] end tenancy in queue if the next job is not tenant aware --- src/Bootstrappers/QueueTenancyBootstrapper.php | 10 ++++++++-- tests/Etc/tmp/queuetest.json | 1 - tests/QueueTest.php | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) delete mode 100644 tests/Etc/tmp/queuetest.json diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index d2ce52d0..2e9aa051 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -75,14 +75,20 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper protected static function initializeTenancyForQueue($tenantId) { - // The job is not tenant-aware if (! $tenantId) { + // The job is not tenant-aware + if (tenancy()->initialized) { + // Tenancy was initialized, so we revert back to the central context + tenancy()->end(); + } + return; } if (tenancy()->initialized) { + // Tenancy is already initialized if (tenant()->getTenantKey() === $tenantId) { - // Tenancy is already initialized for the tenant (e.g. dispatchNow was used) + // It's initialized for the same tenant (e.g. dispatchNow was used, or the previous job also ran for this tenant) return; } } diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json deleted file mode 100644 index dc158c6b..00000000 --- a/tests/Etc/tmp/queuetest.json +++ /dev/null @@ -1 +0,0 @@ -{"userName":"Bar","shouldFail":false,"tenant_id":"The current tenant id is: a7f73c10-9879-40ae-b7b0-1ded7c1f7b1b"} \ No newline at end of file diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 158ad56b..afe64fea 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -55,6 +55,11 @@ class QueueTest extends TestCase $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush(); } + public function tearDown(): void + { + $this->valuestore->flush(); + } + protected function withFailedJobs() { Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { From 368d3cc99f7916af5e8290cddabd4968eeec73ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:21:27 +0100 Subject: [PATCH 16/51] add forceRefresh option to QueueTenancyBootstrapper --- .../QueueTenancyBootstrapper.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 2e9aa051..6a88f701 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -25,6 +25,14 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper /** @var QueueManager */ protected $queue; + /** + * Don't persist the same tenant across multiple jobs even if they have the same tenant ID. + * + * This is useful when you're changing the tenant's state (e.g. properties in the `data` column) and want the next job to initialize tenancy again + * with the new data. Features like the Tenant Config are only executed when tenancy is initialized, so the re-initialization is needed in some cases. + */ + public static bool $forceRefresh = false; + /** * The normal constructor is only executed after tenancy is bootstrapped. * However, we're registering a hook to initialize tenancy. Therefore, @@ -85,6 +93,17 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper return; } + if (static::$forceRefresh) { + // Re-initialize tenancy between all jobs + if (tenancy()->initialized) { + tenancy()->end(); + } + + tenancy()->initialize(tenancy()->find($tenantId)); + + return; + } + if (tenancy()->initialized) { // Tenancy is already initialized if (tenant()->getTenantKey() === $tenantId) { From 8e9485f9b1f51d066a791d5cf94834738383ab76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:31:31 +0100 Subject: [PATCH 17/51] add empty queuetest.json --- tests/Etc/tmp/queuetest.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/Etc/tmp/queuetest.json diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json new file mode 100644 index 00000000..e69de29b From 5249ec7c82013a5122f076d9fac6ed2a04b8f532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:31:45 +0100 Subject: [PATCH 18/51] ignore changes to queuetest.json --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1d03dbec..b3223156 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ psysh phpunit_var_*.xml coverage/ clover.xml +tests/Etc/tmp/queuetest.json From 5b9b3845261fcd5f24181f2a92911db62b26364f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 19 Feb 2022 16:33:59 +0100 Subject: [PATCH 19/51] Remove codecov --- .github/workflows/ci.yml | 4 ---- README.md | 1 - 2 files changed, 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efb8ad02..f7b64d2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,3 @@ jobs: run: docker-compose exec -T test composer require --no-interaction "laravel/framework:${{ matrix.laravel }}" - name: Run tests run: ./test - - name: Send code coverage to codecov - env: - CODECOV_TOKEN: 24382d15-84e7-4a55-bea4-c4df96a24a9b - run: bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index 46f1b097..f4d28288 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Laravel 6.x/7.x/8.x Latest Stable Version GitHub Actions CI status - codecov Donate

From b4a4eab949481d2f96b0b2c1c21aa8db92a2147e Mon Sep 17 00:00:00 2001 From: masiorama Date: Tue, 22 Feb 2022 16:26:07 +0100 Subject: [PATCH 20/51] Add drop of db views on migrate fresh command (#812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Optionally handle drop of table views on MigrateFresh @stancl I managed to make the modification discussed here #811 Afaik (and I can understand) this is the easiest way to handle it, but I'm open to discuss. * Remove redundant store variable * code style Co-authored-by: Samuel Å tancl --- src/Commands/MigrateFresh.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index f50e2f5f..4d003db0 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; use Stancl\Tenancy\Concerns\DealsWithMigrations; use Stancl\Tenancy\Concerns\HasATenantsOption; +use Symfony\Component\Console\Input\InputOption; final class MigrateFresh extends Command { @@ -22,6 +23,8 @@ final class MigrateFresh extends Command public function __construct() { parent::__construct(); + + $this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null); $this->setName('tenants:migrate-fresh'); } @@ -37,6 +40,7 @@ final class MigrateFresh extends Command $this->info('Dropping tables.'); $this->call('db:wipe', array_filter([ '--database' => 'tenant', + '--drop-views' => $this->option('drop-views'), '--force' => true, ])); From 79e3d53b06f33aa6e30ee4454177e1d798918704 Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Tue, 8 Mar 2022 01:50:25 +0100 Subject: [PATCH 21/51] [3.x] Compatibility with Laravel 9 (#802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test on Laravel 9 * Don't extend final Kernel class * Make FilesystemTenancyBootstrapper compatible with Flysystem v3 Co-authored-by: George * Update tenant maintenance mode te be in line with Laravel * Exclude PHP 7.4 <> L9 combination from testing * add root_override-related assertions * getPrefix -> getPathPrefix * handle / inconsistency in s3 prefix * Refactor Storage facade changes Co-authored-by: George Co-authored-by: Samuel Å tancl --- .github/workflows/ci.yml | 5 +- composer.json | 8 +-- .../FilesystemTenancyBootstrapper.php | 31 ++++----- .../CheckTenantForMaintenanceMode.php | 9 ++- tests/BootstrapperTest.php | 69 +++++++++++-------- tests/Etc/ConsoleKernel.php | 2 +- tests/MaintenanceModeTest.php | 4 +- tests/TestCase.php | 1 + 8 files changed, 74 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7b64d2d..1303061a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,10 @@ jobs: strategy: matrix: php: ["7.4", "8.0"] - laravel: ["^6.0", "^8.0"] + laravel: ["^6.0", "^8.0", "^9.0"] + exclude: + - laravel: "^9.0" + php: "7.4" steps: - uses: actions/checkout@v2 diff --git a/composer.json b/composer.json index bf66e1f2..88bfea29 100644 --- a/composer.json +++ b/composer.json @@ -11,16 +11,16 @@ ], "require": { "ext-json": "*", - "illuminate/support": "^6.0|^7.0|^8.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0", "facade/ignition-contracts": "^1.0", "ramsey/uuid": "^3.7|^4.0", "stancl/jobpipeline": "^1.0", "stancl/virtualcolumn": "^1.0" }, "require-dev": { - "laravel/framework": "^6.0|^7.0|^8.0", - "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0", - "league/flysystem-aws-s3-v3": "~1.0", + "laravel/framework": "^6.0|^7.0|^8.0|^9.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", + "league/flysystem-aws-s3-v3": "^1.0|^3.0", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5" }, diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index d5ae2d50..418be93f 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -54,20 +54,20 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper } // Storage facade - foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { - /** @var FilesystemAdapter $filesystemDisk */ - $filesystemDisk = Storage::disk($disk); - $this->originalPaths['disks'][$disk] = $filesystemDisk->getAdapter()->getPathPrefix(); + Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']); - if ($root = str_replace( + foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { + $originalRoot = $this->app['config']["filesystems.disks.{$disk}.root"]; + $this->originalPaths['disks'][$disk] = $originalRoot; + + $finalPrefix = str_replace( '%storage_path%', storage_path(), - $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '' - )) { - $filesystemDisk->getAdapter()->setPathPrefix($finalPrefix = $root); - } else { - $root = $this->app['config']["filesystems.disks.{$disk}.root"]; - $filesystemDisk->getAdapter()->setPathPrefix($finalPrefix = $root . "/{$suffix}"); + $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '', + ); + + if (! $finalPrefix) { + $finalPrefix = $originalRoot . '/'. $suffix; } $this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix; @@ -84,14 +84,9 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper $this->app['url']->setAssetRoot($this->app['config']['app.asset_url']); // Storage facade + Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']); foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) { - /** @var FilesystemAdapter $filesystemDisk */ - $filesystemDisk = Storage::disk($disk); - - $root = $this->originalPaths['disks'][$disk]; - - $filesystemDisk->getAdapter()->setPathPrefix($root); - $this->app['config']["filesystems.disks.{$disk}.root"] = $root; + $this->app['config']["filesystems.disks.{$disk}.root"] = $this->originalPaths['disks'][$disk]; } } } diff --git a/src/Middleware/CheckTenantForMaintenanceMode.php b/src/Middleware/CheckTenantForMaintenanceMode.php index 5554663f..8e29a31e 100644 --- a/src/Middleware/CheckTenantForMaintenanceMode.php +++ b/src/Middleware/CheckTenantForMaintenanceMode.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Middleware; use Closure; -use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException; +use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; use Symfony\Component\HttpFoundation\IpUtils; @@ -29,7 +29,12 @@ class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode return $next($request); } - throw new MaintenanceModeException($data['time'], $data['retry'], $data['message']); + throw new HttpException( + 503, + 'Service Unavailable', + null, + isset($data['retry']) ? ['Retry-After' => $data['retry']] : [] + ); } return $next($request); diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 1b0c880d..29aa7dc9 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -4,23 +4,27 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; -use Illuminate\Support\Facades\Cache; +use Illuminate\Filesystem\FilesystemAdapter; +use ReflectionObject; +use ReflectionProperty; +use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Events\TenancyEnded; -use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; class BootstrapperTest extends TestCase { @@ -165,6 +169,7 @@ class BootstrapperTest extends TestCase $tenant2 = Tenant::create(); tenancy()->initialize($tenant1); + Storage::disk('public')->put('foo', 'bar'); $this->assertSame('bar', Storage::disk('public')->get('foo')); @@ -184,30 +189,38 @@ class BootstrapperTest extends TestCase $this->assertFalse(Storage::disk('public')->exists('foo')); $this->assertFalse(Storage::disk('public')->exists('abc')); + $expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base + + // Check that disk prefixes respect the root_override logic + $this->assertSame($expected_storage_path . '/app/', $this->getDiskPrefix('local')); + $this->assertSame($expected_storage_path . '/app/public/', $this->getDiskPrefix('public')); + $this->assertSame('tenant' . tenant('id') . '/', ltrim($this->getDiskPrefix('s3'), '/')); + // Check suffixing logic $new_storage_path = storage_path(); - $this->assertEquals($old_storage_path . '/' . config('tenancy.filesystem.suffix_base') . tenant('id'), $new_storage_path); + $this->assertEquals($expected_storage_path, $new_storage_path); + } - foreach (config('tenancy.filesystem.disks') as $disk) { - $suffix = config('tenancy.filesystem.suffix_base') . tenant('id'); + protected function getDiskPrefix(string $disk): string + { + /** @var FilesystemAdapter $disk */ + $disk = Storage::disk($disk); + $adapter = $disk->getAdapter(); - /** @var FilesystemAdapter $filesystemDisk */ - $filesystemDisk = Storage::disk($disk); - - $current_path_prefix = $filesystemDisk->getAdapter()->getPathPrefix(); - - if ($override = config("tenancy.filesystem.root_override.{$disk}")) { - $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override); - } else { - if ($base = $old_storage_facade_roots[$disk]) { - $correct_path_prefix = $base . "/$suffix/"; - } else { - $correct_path_prefix = "$suffix/"; - } - } - - $this->assertSame($correct_path_prefix, $current_path_prefix); + if (! Str::startsWith(app()->version(), '9.')) { + return $adapter->getPathPrefix(); } + + $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); + $prefixer->setAccessible(true); + + // reflection -> instance + $prefixer = $prefixer->getValue($adapter); + + $prefix = (new ReflectionProperty($prefixer, 'prefix')); + $prefix->setAccessible(true); + + return $prefix->getValue($prefixer); } // for queues see QueueTest diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/ConsoleKernel.php index 1bc66365..9d37d3c6 100644 --- a/tests/Etc/ConsoleKernel.php +++ b/tests/Etc/ConsoleKernel.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests\Etc; -use Orchestra\Testbench\Console\Kernel; +use Orchestra\Testbench\Foundation\Console\Kernel; class ConsoleKernel extends Kernel { diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index a8ecb064..4a8d8d0c 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -4,12 +4,14 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException; use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Tests\Etc\Tenant; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; class MaintenanceModeTest extends TestCase { @@ -32,7 +34,7 @@ class MaintenanceModeTest extends TestCase $tenant->putDownForMaintenance(); - $this->expectException(MaintenanceModeException::class); + $this->expectException(HttpException::class); $this->withoutExceptionHandling() ->get('http://acme.localhost/foo'); } diff --git a/tests/TestCase.php b/tests/TestCase.php index d3e42ea1..cea669a1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -87,6 +87,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'public', 's3', ], + 'filesystems.disks.s3.bucket' => 'foo', 'tenancy.redis.tenancy' => env('TENANCY_TEST_REDIS_TENANCY', true), 'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'), 'tenancy.redis.prefixed_connections' => ['default'], From eb1a2ebe32a3dd9e5941c81602d55aab2268db06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 17 Mar 2022 12:24:57 +0100 Subject: [PATCH 22/51] Use 7.2 instead of 7.4 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1303061a..3d1698b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: strategy: matrix: - php: ["7.4", "8.0"] + php: ["7.2", "8.0"] laravel: ["^6.0", "^8.0", "^9.0"] exclude: - laravel: "^9.0" - php: "7.4" + php: "7.2" steps: - uses: actions/checkout@v2 From fa2a61fcd74126587312567d5e07e273c8ea43b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 17 Mar 2022 12:30:14 +0100 Subject: [PATCH 23/51] Use PHP 7.3 instead of 7.2 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d1698b4..cc8ad985 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,11 @@ jobs: strategy: matrix: - php: ["7.2", "8.0"] + php: ["7.3", "8.0"] laravel: ["^6.0", "^8.0", "^9.0"] exclude: - laravel: "^9.0" - php: "7.2" + php: "7.3" steps: - uses: actions/checkout@v2 From 49ebb75f007d649465fc3f847ddc76f6396b0337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 17 Mar 2022 12:46:49 +0100 Subject: [PATCH 24/51] Fixes #827 --- src/Bootstrappers/QueueTenancyBootstrapper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 6a88f701..666e29ed 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -30,8 +30,10 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper * * This is useful when you're changing the tenant's state (e.g. properties in the `data` column) and want the next job to initialize tenancy again * with the new data. Features like the Tenant Config are only executed when tenancy is initialized, so the re-initialization is needed in some cases. + * + * @var bool */ - public static bool $forceRefresh = false; + public static $forceRefresh = false; /** * The normal constructor is only executed after tenancy is bootstrapped. From 5026f54a6d4482226951d3a8196218ea41434db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:48:55 +0100 Subject: [PATCH 25/51] fix path prefixing --- CONTRIBUTING.md | 13 +++++++++++++ Dockerfile | 2 +- docker-compose.override.yml | 5 +++++ src/Bootstrappers/FilesystemTenancyBootstrapper.php | 4 +++- tests/BootstrapperTest.php | 2 +- tests/Etc/tmp/queuetest.json | 0 6 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 docker-compose.override.yml delete mode 100644 tests/Etc/tmp/queuetest.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7dce1b82..a5a6ec3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,3 +9,16 @@ StyleCI will flag code style violations in your pull requests. Run `docker-compose up -d` to start the containers. Then run `./test` to run the tests. When you're done testing, run `docker-compose down` to shut down the containers. + +### Docker on M1 + +You can add: +```yaml +services: + mysql: + platform: linux/amd64 + mysql2: + platform: linux/amd64 +``` + +to `docker-compose.override.yml` to make `docker-compose up-d` work on M1. diff --git a/Dockerfile b/Dockerfile index 06d97aea..36f52d6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ # && if [ "${PHP_VERSION}" = "7.4" ]; then docker-php-ext-configure gd --with-freetype --with-jpeg; else docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; fi \ && docker-php-ext-install -j$(nproc) gd pdo pdo_mysql pdo_pgsql pdo_sqlite pgsql zip gmp bcmath pcntl ldap sysvmsg exif \ # install the redis php extension - && pecl install redis-5.3.2 \ + && pecl install redis-5.3.7 \ && docker-php-ext-enable redis \ # install the pcov extention && pecl install pcov \ diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..29e9fb37 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,5 @@ +services: + mysql: + platform: linux/amd64 + mysql2: + platform: linux/amd64 diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index 418be93f..dcd7299e 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -67,7 +67,9 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper ); if (! $finalPrefix) { - $finalPrefix = $originalRoot . '/'. $suffix; + $finalPrefix = $originalRoot + ? $originalRoot . '/'. $suffix + : $suffix; } $this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix; diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 29aa7dc9..588fadd8 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -194,7 +194,7 @@ class BootstrapperTest extends TestCase // Check that disk prefixes respect the root_override logic $this->assertSame($expected_storage_path . '/app/', $this->getDiskPrefix('local')); $this->assertSame($expected_storage_path . '/app/public/', $this->getDiskPrefix('public')); - $this->assertSame('tenant' . tenant('id') . '/', ltrim($this->getDiskPrefix('s3'), '/')); + $this->assertSame('tenant' . tenant('id') . '/', $this->getDiskPrefix('s3'), '/'); // Check suffixing logic $new_storage_path = storage_path(); diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json deleted file mode 100644 index e69de29b..00000000 From 600bb823de185ef02c3ff5aeba0c75f41409bafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:49:25 +0100 Subject: [PATCH 26/51] avoid double // in prefix --- src/Bootstrappers/FilesystemTenancyBootstrapper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index dcd7299e..da1e5e2a 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -68,7 +68,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper if (! $finalPrefix) { $finalPrefix = $originalRoot - ? $originalRoot . '/'. $suffix + ? rtrim($originalRoot, '/') . '/'. $suffix : $suffix; } From e1ae6f4380bf2062adfb44bc1acb83e603b95ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:57:15 +0100 Subject: [PATCH 27/51] re-add queuetest.json --- .gitignore | 1 - tests/Etc/tmp/queuetest.json | 0 2 files changed, 1 deletion(-) create mode 100644 tests/Etc/tmp/queuetest.json diff --git a/.gitignore b/.gitignore index b3223156..1d03dbec 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ psysh phpunit_var_*.xml coverage/ clover.xml -tests/Etc/tmp/queuetest.json diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json new file mode 100644 index 00000000..e69de29b From 4e717236f9c1aa5d4e4aa588bace783d4d7bcde2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:57:32 +0100 Subject: [PATCH 28/51] revert gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1d03dbec..b3223156 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ psysh phpunit_var_*.xml coverage/ clover.xml +tests/Etc/tmp/queuetest.json From 4f196097979653fa7b7522ac339116d88b5baae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 23 Mar 2022 20:58:47 +0100 Subject: [PATCH 29/51] remove docker-compose.override.yml --- .gitignore | 1 + docker-compose.override.yml | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 docker-compose.override.yml diff --git a/.gitignore b/.gitignore index b3223156..f470ba75 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ phpunit_var_*.xml coverage/ clover.xml tests/Etc/tmp/queuetest.json +docker-compose.override.yml diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index 29e9fb37..00000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - mysql: - platform: linux/amd64 - mysql2: - platform: linux/amd64 From 349125c02ebe71216bfbbb4ea0c3b955e03ba474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 30 Mar 2022 18:00:55 +0200 Subject: [PATCH 30/51] Merge hotfix branch (#834) * try specifying the signature in __construct * constructor doesn't work since Reflection is used, try specifying getDefaultName() instead * Fixed: make migration commands compatible * Fix failing tests * Fix username generation * Re-create tmp dir as well if needed * wip --- src/Commands/Migrate.php | 20 ++++++---------- src/Commands/MigrateFresh.php | 2 +- src/Commands/Rollback.php | 11 ++++++--- src/Concerns/ExtendsLaravelCommand.php | 23 +++++++++++++++++++ src/Database/DatabaseManager.php | 10 +++++++- src/Jobs/CreateDatabase.php | 2 +- ...rmissionControlledMySQLDatabaseManager.php | 5 ---- tests/DatabaseUsersTest.php | 9 ++++++-- tests/Etc/tmp/queuetest.json | 0 tests/QueueTest.php | 20 +++++++++++++++- 10 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 src/Concerns/ExtendsLaravelCommand.php delete mode 100644 tests/Etc/tmp/queuetest.json diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index bf92dfcd..c67d3598 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -8,32 +8,26 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Console\Migrations\MigrateCommand; use Illuminate\Database\Migrations\Migrator; use Stancl\Tenancy\Concerns\DealsWithMigrations; +use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Events\DatabaseMigrated; use Stancl\Tenancy\Events\MigratingDatabase; class Migrate extends MigrateCommand { - use HasATenantsOption, DealsWithMigrations; + use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand; - /** - * The console command description. - * - * @var string - */ protected $description = 'Run migrations for tenant(s)'; - /** - * Create a new command instance. - * - * @param Migrator $migrator - * @param Dispatcher $dispatcher - */ + protected static function getTenantCommandName(): string + { + return 'tenants:migrate'; + } + public function __construct(Migrator $migrator, Dispatcher $dispatcher) { parent::__construct($migrator, $dispatcher); - $this->setName('tenants:migrate'); $this->specifyParameters(); } diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 4d003db0..283d70b0 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -23,7 +23,7 @@ final class MigrateFresh extends Command public function __construct() { parent::__construct(); - + $this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null); $this->setName('tenants:migrate-fresh'); diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index 081872c8..e60d974b 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -7,13 +7,19 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Migrations\Migrator; use Stancl\Tenancy\Concerns\DealsWithMigrations; +use Stancl\Tenancy\Concerns\ExtendsLaravelCommand; use Stancl\Tenancy\Concerns\HasATenantsOption; use Stancl\Tenancy\Events\DatabaseRolledBack; use Stancl\Tenancy\Events\RollingBackDatabase; class Rollback extends RollbackCommand { - use HasATenantsOption, DealsWithMigrations; + use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand; + + protected static function getTenantCommandName(): string + { + return 'tenants:rollback'; + } /** * The console command description. @@ -31,8 +37,7 @@ class Rollback extends RollbackCommand { parent::__construct($migrator); - $this->setName('tenants:rollback'); - $this->specifyParameters(); + $this->specifyTenantSignature(); } /** diff --git a/src/Concerns/ExtendsLaravelCommand.php b/src/Concerns/ExtendsLaravelCommand.php new file mode 100644 index 00000000..bdafc8f7 --- /dev/null +++ b/src/Concerns/ExtendsLaravelCommand.php @@ -0,0 +1,23 @@ +specifyParameters(); + } + + public function getName(): ?string + { + return static::getTenantCommandName(); + } + + public static function getDefaultName(): ?string + { + return static::getTenantCommandName(); + } + + abstract protected static function getTenantCommandName(): string; +} diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php index e85fd659..6242ffa9 100644 --- a/src/Database/DatabaseManager.php +++ b/src/Database/DatabaseManager.php @@ -7,10 +7,12 @@ namespace Stancl\Tenancy\Database; use Illuminate\Config\Repository; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\DatabaseManager as BaseDatabaseManager; +use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException; use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException; use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; +use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; /** * @internal Class is subject to breaking changes in minor and patch versions. @@ -90,8 +92,14 @@ class DatabaseManager */ public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void { - if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) { + $manager = $tenant->database()->manager(); + + if ($manager->databaseExists($database = $tenant->database()->getName())) { throw new TenantDatabaseAlreadyExistsException($database); } + + if ($manager instanceof ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) { + throw new TenantDatabaseUserAlreadyExistsException($username); + } } } diff --git a/src/Jobs/CreateDatabase.php b/src/Jobs/CreateDatabase.php index 3a74534d..3cb2a6b4 100644 --- a/src/Jobs/CreateDatabase.php +++ b/src/Jobs/CreateDatabase.php @@ -36,8 +36,8 @@ class CreateDatabase implements ShouldQueue return false; } - $databaseManager->ensureTenantCanBeCreated($this->tenant); $this->tenant->database()->makeCredentials(); + $databaseManager->ensureTenantCanBeCreated($this->tenant); $this->tenant->database()->manager()->createDatabase($this->tenant); event(new DatabaseCreated($this->tenant)); diff --git a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php index f8bedc97..918601a8 100644 --- a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php @@ -7,7 +7,6 @@ namespace Stancl\Tenancy\TenantDatabaseManagers; use Stancl\Tenancy\Concerns\CreatesDatabaseUsers; use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\DatabaseConfig; -use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager implements ManagesDatabaseUsers { @@ -26,10 +25,6 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl $hostname = $databaseConfig->connection()['host']; $password = $databaseConfig->getPassword(); - if ($this->userExists($username)) { - throw new TenantDatabaseUserAlreadyExistsException($username); - } - $this->database()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'"); $grants = implode(', ', static::$grants); diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php index 0b095024..344239d1 100644 --- a/tests/DatabaseUsersTest.php +++ b/tests/DatabaseUsersTest.php @@ -10,6 +10,7 @@ use Illuminate\Support\Str; use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; +use Stancl\Tenancy\Events\DatabaseCreated; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; @@ -67,14 +68,18 @@ class DatabaseUsersTest extends TestCase $this->assertTrue($manager->databaseExists($tenant->database()->getName())); $this->expectException(TenantDatabaseUserAlreadyExistsException::class); + Event::fake([DatabaseCreated::class]); + $tenant2 = Tenant::create([ 'tenancy_db_username' => $username, ]); /** @var ManagesDatabaseUsers $manager */ - $manager = $tenant2->database()->manager(); + $manager2 = $tenant2->database()->manager(); + // database was not created because of DB transaction - $this->assertFalse($manager->databaseExists($tenant2->database()->getName())); + $this->assertFalse($manager2->databaseExists($tenant2->database()->getName())); + Event::assertNotDispatched(DatabaseCreated::class); } /** @test */ diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/QueueTest.php b/tests/QueueTest.php index afe64fea..a3df9cd7 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Closure; use Exception; use Illuminate\Support\Str; use Illuminate\Bus\Queueable; @@ -24,6 +25,7 @@ use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use PDO; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; @@ -52,7 +54,7 @@ class QueueTest extends TestCase Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); - $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush(); + $this->createValueStore(); } public function tearDown(): void @@ -60,6 +62,22 @@ class QueueTest extends TestCase $this->valuestore->flush(); } + protected function createValueStore(): void + { + $valueStorePath = __DIR__ . '/Etc/tmp/queuetest.json'; + + if (! file_exists($valueStorePath)) { + // The directory sometimes goes missing as well when the file is deleted in git + if (! is_dir(__DIR__ . '/Etc/tmp')) { + mkdir(__DIR__ . '/Etc/tmp'); + } + + file_put_contents($valueStorePath, ''); + } + + $this->valuestore = Valuestore::make($valueStorePath)->flush(); + } + protected function withFailedJobs() { Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { From f065ea60b0e56c6a22cd75c476ca448649f8ffe2 Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Fri, 1 Apr 2022 22:53:09 +0200 Subject: [PATCH 31/51] Update QueueTenancyBootstrapper.php (#836) --- src/Bootstrappers/QueueTenancyBootstrapper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 666e29ed..790e1344 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -63,8 +63,8 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null); }); - if (Str::startsWith(app()->version(), '8')) { - // JobRetryRequested only exists since Laravel 8 + if (version_compare(app()->version(), '8.64', '>=')) { + // JobRetryRequested only exists since Laravel 8.64 $dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) { $previousTenant = tenant(); From 40bf576e0087f3350fa5f91c2c73962a7dcdff50 Mon Sep 17 00:00:00 2001 From: Nathan Dunn Date: Fri, 8 Apr 2022 02:13:29 +0100 Subject: [PATCH 32/51] [3.x] Update PostgreSQLSchemaManager to set correct config key value (#840) * Update PostgreSQLSchemaManager to set correct config key value * Update to use version_compare * Update TenantDatabaseManagerTest * Improve TenantDatabaseManagerTest * Update TenantDatabaseManager --- src/TenantDatabaseManagers/PostgreSQLSchemaManager.php | 6 +++++- tests/TenantDatabaseManagerTest.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php index 9d815b25..55f049d0 100644 --- a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -46,7 +46,11 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager public function makeConnectionConfig(array $baseConfig, string $databaseName): array { - $baseConfig['schema'] = $databaseName; + if (version_compare(app()->version(), '9.0', '>=')) { + $baseConfig['search_path'] = $databaseName; + } else { + $baseConfig['schema'] = $databaseName; + } return $baseConfig; } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index f64770b1..3d45d96f 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -194,7 +194,11 @@ class TenantDatabaseManagerTest extends TestCase ]); tenancy()->initialize($tenant); - $this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema')); + $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? + config('database.connections.' . config('database.default') . '.search_path') : + config('database.connections.' . config('database.default') . '.schema'); + + $this->assertSame($tenant->database()->getName(), $schemaConfig); $this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database'])); } From 0569bf5a3495a194079540f8c7096ab6aac68117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sun, 1 May 2022 12:56:25 +0200 Subject: [PATCH 33/51] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4d28288..95fb7c60 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Laravel 6.x/7.x/8.x + Laravel 9.x Latest Stable Version GitHub Actions CI status Donate From a1c34421488b8eb4c5776cbf457f0719c79d742e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sun, 15 May 2022 13:32:09 +0200 Subject: [PATCH 34/51] Resolve #854 --- src/Database/Concerns/BelongsToTenant.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index 5410758d..fc899411 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -16,7 +16,7 @@ trait BelongsToTenant public function tenant() { - return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn); + return $this->belongsTo(config('tenancy.tenant_model'), static::$tenantIdColumn); } public static function bootBelongsToTenant() @@ -24,9 +24,9 @@ trait BelongsToTenant static::addGlobalScope(new TenantScope); static::creating(function ($model) { - if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { + if (! $model->getAttribute(static::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { if (tenancy()->initialized) { - $model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey()); + $model->setAttribute(static::$tenantIdColumn, tenant()->getTenantKey()); $model->setRelation('tenant', tenant()); } } From 4d95e88e272d5c3f4beebd10a8e549d17259a079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sun, 15 May 2022 13:45:54 +0200 Subject: [PATCH 35/51] Revert "Resolve #854" This reverts commit a1c34421488b8eb4c5776cbf457f0719c79d742e. --- src/Database/Concerns/BelongsToTenant.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index fc899411..5410758d 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -16,7 +16,7 @@ trait BelongsToTenant public function tenant() { - return $this->belongsTo(config('tenancy.tenant_model'), static::$tenantIdColumn); + return $this->belongsTo(config('tenancy.tenant_model'), BelongsToTenant::$tenantIdColumn); } public static function bootBelongsToTenant() @@ -24,9 +24,9 @@ trait BelongsToTenant static::addGlobalScope(new TenantScope); static::creating(function ($model) { - if (! $model->getAttribute(static::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { + if (! $model->getAttribute(BelongsToTenant::$tenantIdColumn) && ! $model->relationLoaded('tenant')) { if (tenancy()->initialized) { - $model->setAttribute(static::$tenantIdColumn, tenant()->getTenantKey()); + $model->setAttribute(BelongsToTenant::$tenantIdColumn, tenant()->getTenantKey()); $model->setRelation('tenant', tenant()); } } From 51228defc68a6362e31f486a04e3117105abf3e0 Mon Sep 17 00:00:00 2001 From: Vincent GS Date: Thu, 26 May 2022 04:51:27 -0500 Subject: [PATCH 36/51] [3.x][Filesystem] Provide an additional argument for tenant name path (#817) * Let the user pass the tenant suffix by %tenant% In this PR we let the user pass an additional parameter using `%tenant%` so the user can additionally pass the folder corresponding to each tenant. This is my proposal, because if I try to use %storage_path% within Linux, I get the full path to the project when I use Google Cloud Storage * Missing missing updates Moving from $subject to $root when %storage_path% has been replaced --- src/Bootstrappers/FilesystemTenancyBootstrapper.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index da1e5e2a..346892b3 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Facades\Storage; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\Tenant; @@ -61,8 +60,8 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper $this->originalPaths['disks'][$disk] = $originalRoot; $finalPrefix = str_replace( - '%storage_path%', - storage_path(), + ['%storage_path%', '%tenant%'], + [storage_path(), $tenant->getTenantKey()], $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '', ); From d0de09aa5360b17033b81726d38b18272462f6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Wed, 1 Jun 2022 15:36:46 +0200 Subject: [PATCH 37/51] remove old versions from CI --- .github/workflows/ci.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc8ad985..81d37af5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,9 @@ env: on: push: - branches: [ 3.x, 2.x, master ] + branches: [ master ] pull_request: - branches: [ 3.x, 2.x, master ] + branches: [ master ] jobs: tests: @@ -15,11 +15,8 @@ jobs: strategy: matrix: - php: ["7.3", "8.0"] - laravel: ["^6.0", "^8.0", "^9.0"] - exclude: - - laravel: "^9.0" - php: "7.3" + php: ["8.1"] + laravel: ["^9.0"] steps: - uses: actions/checkout@v2 From 7d98ebb5d177a1ac30ec0a7d248c4cd254abce4a Mon Sep 17 00:00:00 2001 From: Victor R <39545521+viicslen@users.noreply.github.com> Date: Wed, 1 Jun 2022 10:12:59 -0400 Subject: [PATCH 38/51] [4.x] Add tenant schema dump command (#807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add tenant dump command * Register tenant schema dump command * Added tests for tenant schema dump command * remove docblocks, fix tenant() logic * trigger ci * Install mysql-client * mysql-client -> mariadb-client * add tenant-schema-test.dump to .gitignore Co-authored-by: Samuel Å tancl Co-authored-by: Samuel Å tancl --- .gitignore | 1 + Dockerfile | 2 +- src/Commands/TenantDump.php | 54 ++++++++++++++++++++++++++++ src/TenancyServiceProvider.php | 1 + tests/CommandsTest.php | 32 +++++++++++++++++ tests/Etc/tenant-schema.dump | 66 ++++++++++++++++++++++++++++++++++ 6 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/Commands/TenantDump.php create mode 100644 tests/Etc/tenant-schema.dump diff --git a/.gitignore b/.gitignore index f470ba75..95522c34 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,6 @@ psysh phpunit_var_*.xml coverage/ clover.xml +tenant-schema-test.dump tests/Etc/tmp/queuetest.json docker-compose.override.yml diff --git a/Dockerfile b/Dockerfile index 36f52d6a..a636bc45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ENV LANG=en_GB.UTF-8 # install some OS packages we need RUN apt-get update -RUN apt-get install -y --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git +RUN apt-get install -y --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl mariadb-client sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git # install php extensions RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ # && if [ "${PHP_VERSION}" = "7.4" ]; then docker-php-ext-configure gd --with-freetype --with-jpeg; else docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; fi \ diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php new file mode 100644 index 00000000..557c6975 --- /dev/null +++ b/src/Commands/TenantDump.php @@ -0,0 +1,54 @@ +setName('tenants:dump'); + $this->specifyParameters(); + } + + + public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int + { + $this->tenant()->run(fn() => parent::handle($connections, $dispatcher)); + + return Command::SUCCESS; + } + + public function tenant(): Tenant + { + $tenant = $this->option('tenant') + ?? tenant() + ?? $this->ask('What tenant do you want to dump the schema for?') + ?? tenancy()->query()->first(); + + if (! $tenant instanceof Tenant) { + $tenant = tenancy()->find($tenant); + } + + throw_if(! $tenant, 'Could not identify the tenant to use for dumping the schema.'); + + return $tenant; + } + + protected function getOptions(): array + { + return array_merge([ + ['tenant', null, InputOption::VALUE_OPTIONAL, '', null], + ], parent::getOptions()); + } +} diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 4faaccf3..dd061af3 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -88,6 +88,7 @@ class TenancyServiceProvider extends ServiceProvider Commands\Migrate::class, Commands\Rollback::class, Commands\TenantList::class, + Commands\TenantDump::class, Commands\MigrateFresh::class, ]); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index d7da0cab..145a93c5 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -91,6 +91,38 @@ class CommandsTest extends TestCase $this->assertTrue(Schema::hasTable('users')); } + /** @test */ + public function migrate_command_loads_schema_state() + { + $tenant = Tenant::create(); + + $this->assertFalse(Schema::hasTable('schema_users')); + $this->assertFalse(Schema::hasTable('users')); + + Artisan::call('tenants:migrate --schema-path="tests/Etc/tenant-schema.dump"'); + + $this->assertFalse(Schema::hasTable('schema_users')); + $this->assertFalse(Schema::hasTable('users')); + + tenancy()->initialize($tenant); + + // Check for both tables to see if missing migrations also get executed + $this->assertTrue(Schema::hasTable('schema_users')); + $this->assertTrue(Schema::hasTable('users')); + } + + /** @test */ + public function dump_command_works() + { + $tenant = Tenant::create(); + Artisan::call('tenants:migrate'); + + tenancy()->initialize($tenant); + + Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"'); + $this->assertFileExists('tests/Etc/tenant-schema-test.dump'); + } + /** @test */ public function rollback_command_works() { diff --git a/tests/Etc/tenant-schema.dump b/tests/Etc/tenant-schema.dump new file mode 100644 index 00000000..6af9f019 --- /dev/null +++ b/tests/Etc/tenant-schema.dump @@ -0,0 +1,66 @@ +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +DROP TABLE IF EXISTS `failed_jobs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `failed_jobs` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `connection` text COLLATE utf8mb4_unicode_ci NOT NULL, + `queue` text COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `migrations` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `batch` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `password_resets`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `password_resets` ( + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + KEY `password_resets_email_index` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `schema_users` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email_verified_at` timestamp NULL DEFAULT NULL, + `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `users_email_unique` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +INSERT INTO `migrations` VALUES (2,'2014_10_12_100000_testbench_create_password_resets_table',1); +INSERT INTO `migrations` VALUES (3,'2019_08_19_000000_testbench_create_failed_jobs_table',1); From 72c41ea9938eef4964ef9469d829c16536f69e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Mon, 13 Jun 2022 19:16:35 +0200 Subject: [PATCH 39/51] Discord link --- SUPPORT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SUPPORT.md b/SUPPORT.md index b7caaa5c..24be468b 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,5 +1,5 @@ # Get Support -If you need help with implementing the package, you can join our community [Discord server](https://discord.gg/7cpgPxv) and ask in `#help`. +If you need help with implementing the package, you can join our community [Discord server](https://archte.ch/discord) and ask in `#help`. If you're interested in paid consulting from the maintainer, see the [contact page](https://tenancyforlaravel.com/contact/) on our website. From cc6d4fe0ddcec405f645fbd2d8e90ab71afb1bec Mon Sep 17 00:00:00 2001 From: Nick Kitchen <61636526+nickakitch@users.noreply.github.com> Date: Thu, 23 Jun 2022 21:04:53 +1000 Subject: [PATCH 40/51] [4.x] Added support for Microsoft Sql Server (#715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added support for microsoft sql server database * added support for microsoft sql server database * trigger ci * revert change * trigger ci * Try installing pdo_sqlsrv * different approach for installing sqlsrv via pecl * add dependencies * add gnupg2 * Update Dockerfile * try skipping msodbcsql17 * Update Dockerfile * add dependency back * update before installing * try to add mssql * mssql host * TENANCY_TEST_MSSQL_HOST env var * add env vars for mssql * add sqlsrv vars to TestCase * rename vars to SQLSRV [skip ci] * MSSQL -> SQLSRV Co-authored-by: Samuel Å tancl Co-authored-by: Samuel Å tancl --- Dockerfile | 21 +++++-- assets/config.php | 1 + docker-compose.yml | 11 ++++ .../MicrosoftSQLDatabaseManager.php | 57 +++++++++++++++++++ tests/TenantDatabaseManagerTest.php | 2 + tests/TestCase.php | 4 ++ 6 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php diff --git a/Dockerfile b/Dockerfile index a636bc45..fb63afe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG PHP_VERSION=7.4 ARG PHP_TARGET=php:${PHP_VERSION}-cli -FROM ${PHP_TARGET} +FROM --platform=linux/amd64 ${PHP_TARGET} ARG COMPOSER_TARGET=2.0.3 @@ -22,10 +22,16 @@ ENV LANG=en_GB.UTF-8 # Dockerfile _and pin the versions_! Eg: # RUN pecl install memcached-2.2.0 && docker-php-ext-enable memcached -# install some OS packages we need -RUN apt-get update -RUN apt-get install -y --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl mariadb-client sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git - # install php extensions + +RUN apt-get update \ + && apt-get install -y gnupg2 \ + && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ + && curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list \ + && apt-get update \ + && ACCEPT_EULA=Y apt-get install -y unixodbc-dev msodbcsql17 + +RUN apt-get install -y --no-install-recommends locales apt-transport-https libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl mariadb-client sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git + RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ # && if [ "${PHP_VERSION}" = "7.4" ]; then docker-php-ext-configure gd --with-freetype --with-jpeg; else docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; fi \ && docker-php-ext-install -j$(nproc) gd pdo pdo_mysql pdo_pgsql pdo_sqlite pgsql zip gmp bcmath pcntl ldap sysvmsg exif \ @@ -35,7 +41,10 @@ RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ # install the pcov extention && pecl install pcov \ && docker-php-ext-enable pcov \ - && echo "pcov.enabled = 1" > /usr/local/etc/php/conf.d/pcov.ini + && echo "pcov.enabled = 1" > /usr/local/etc/php/conf.d/pcov.ini \ + # install sqlsrv + && pecl install sqlsrv pdo_sqlsrv \ + && docker-php-ext-enable sqlsrv pdo_sqlsrv # clear the apt cache RUN rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \ diff --git a/assets/config.php b/assets/config.php index 85592d14..e1c82e6b 100644 --- a/assets/config.php +++ b/assets/config.php @@ -61,6 +61,7 @@ return [ 'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class, 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class, 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class, + 'sqlsrv' => Stancl\Tenancy\TenantDatabaseManagers\MicrosoftSQLDatabaseManager::class, /** * Use this database manager for MySQL to have a DB user created for each tenant database. diff --git a/docker-compose.yml b/docker-compose.yml index e8e8d418..7b635637 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,9 @@ services: TENANCY_TEST_REDIS_HOST: redis TENANCY_TEST_MYSQL_HOST: mysql TENANCY_TEST_PGSQL_HOST: postgres + TENANCY_TEST_SQLSRV_HOST: mssql + TENANCY_TEST_SQLSRV_USERNAME: sa + TENANCY_TEST_SQLSRV_PASSWORD: P@ssword stdin_open: true tty: true mysql: @@ -64,3 +67,11 @@ services: interval: 1s timeout: 3s retries: 30 + mssql: + image: mcr.microsoft.com/mssql/server:2019-latest + ports: + - 1433:1433 + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=P@ssword # todo reuse values from env above + # todo missing health check diff --git a/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php b/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php new file mode 100644 index 00000000..0bc34623 --- /dev/null +++ b/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php @@ -0,0 +1,57 @@ +connection === null) { + throw new NoConnectionSetException(static::class); + } + + return DB::connection($this->connection); + } + + public function setConnection(string $connection): void + { + $this->connection = $connection; + } + + public function createDatabase(TenantWithDatabase $tenant): bool + { + $database = $tenant->database()->getName(); + $charset = $this->database()->getConfig('charset'); + $collation = $this->database()->getConfig('collation'); + + return $this->database()->statement("CREATE DATABASE [{$database}]"); + } + + public function deleteDatabase(TenantWithDatabase $tenant): bool + { + return $this->database()->statement("DROP DATABASE [{$tenant->database()->getName()}]"); + } + + public function databaseExists(string $name): bool + { + return (bool) $this->database()->select("SELECT name FROM master.sys.databases WHERE name = '$name'"); + } + + public function makeConnectionConfig(array $baseConfig, string $databaseName): array + { + $baseConfig['database'] = $databaseName; + + return $baseConfig; + } +} diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 3d45d96f..0e1464c0 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -19,6 +19,7 @@ use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException; use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; +use Stancl\Tenancy\TenantDatabaseManagers\MicrosoftSQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager; @@ -103,6 +104,7 @@ class TenantDatabaseManagerTest extends TestCase ['sqlite', SQLiteDatabaseManager::class], ['pgsql', PostgreSQLDatabaseManager::class], ['pgsql', PostgreSQLSchemaManager::class], + ['sqlsrv', MicrosoftSQLDatabaseManager::class] ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index cea669a1..75fe51fd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -81,6 +81,10 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase ], 'database.connections.sqlite.database' => ':memory:', 'database.connections.mysql.host' => env('TENANCY_TEST_MYSQL_HOST', '127.0.0.1'), + 'database.connections.sqlsrv.username' => env('TENANCY_TEST_SQLSRV_USERNAME', 'sa'), + 'database.connections.sqlsrv.password' => env('TENANCY_TEST_SQLSRV_PASSWORD', 'P@ssword'), + 'database.connections.sqlsrv.host' => env('TENANCY_TEST_SQLSRV_HOST', '127.0.0.1'), + 'database.connections.sqlsrv.database' => null, 'database.connections.pgsql.host' => env('TENANCY_TEST_PGSQL_HOST', '127.0.0.1'), 'tenancy.filesystem.disks' => [ 'local', From 4aec6bfda20e659c4e62d2527ead99fbf44926cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Tue, 28 Jun 2022 21:47:33 +0200 Subject: [PATCH 41/51] add phpunit dependency --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 8e932658..8123944a 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "require-dev": { "laravel/framework": "^6.0|^7.0|^8.0|^9.0", "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", + "phpunit/phpunit": "*", "league/flysystem-aws-s3-v3": "^1.0|^3.0", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5" From 627233d07aa173893cc2afd6484d44f8455c7362 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Wed, 20 Jul 2022 16:02:33 +0500 Subject: [PATCH 42/51] [4.x] Don't use onDeleteCascade in the migrations (#883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * removed `cascade` on delete for domains * removed only `onDelete` cascade * keep impersonation migrations unchanged * domains set null on delete * Update 2019_09_15_000020_create_domains_table.php * Added DeleteDomain Job while deleting tenant. * Update assets/TenancyServiceProvider.stub.php Co-authored-by: Samuel Å tancl * renamed class * Update DeleteDomains.php * onDelete restrict * revert nullable * removed `shouldQueue` interface * Update TenancyServiceProvider.stub.php * fetch and delete domains individually * Update DeleteDomains.php * tests for `DeleteDomains` job Co-authored-by: Samuel Å tancl --- assets/TenancyServiceProvider.stub.php | 8 +++- ...2019_09_15_000020_create_domains_table.php | 2 +- src/Jobs/DeleteDomains.php | 35 ++++++++++++++++ tests/DeleteDomainsJobTest.php | 42 +++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/Jobs/DeleteDomains.php create mode 100644 tests/DeleteDomainsJobTest.php diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php index 1d15f418..865bb93d 100644 --- a/assets/TenancyServiceProvider.stub.php +++ b/assets/TenancyServiceProvider.stub.php @@ -40,7 +40,13 @@ class TenancyServiceProvider extends ServiceProvider Events\TenantSaved::class => [], Events\UpdatingTenant::class => [], Events\TenantUpdated::class => [], - Events\DeletingTenant::class => [], + Events\DeletingTenant::class => [ + JobPipeline::make([ + Jobs\DeleteDomains::class, + ])->send(function (Events\DeletingTenant $event) { + return $event->tenant; + })->shouldBeQueued(false), + ], Events\TenantDeleted::class => [ JobPipeline::make([ Jobs\DeleteDatabase::class, diff --git a/assets/migrations/2019_09_15_000020_create_domains_table.php b/assets/migrations/2019_09_15_000020_create_domains_table.php index 77c1b88a..17f706c2 100644 --- a/assets/migrations/2019_09_15_000020_create_domains_table.php +++ b/assets/migrations/2019_09_15_000020_create_domains_table.php @@ -21,7 +21,7 @@ class CreateDomainsTable extends Migration $table->string('tenant_id'); $table->timestamps(); - $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade'); }); } diff --git a/src/Jobs/DeleteDomains.php b/src/Jobs/DeleteDomains.php new file mode 100644 index 00000000..fac60e43 --- /dev/null +++ b/src/Jobs/DeleteDomains.php @@ -0,0 +1,35 @@ +tenant = $tenant; + } + + public function handle() + { + $this->tenant->domains->each->delete(); + } +} diff --git a/tests/DeleteDomainsJobTest.php b/tests/DeleteDomainsJobTest.php new file mode 100644 index 00000000..7fce9cf3 --- /dev/null +++ b/tests/DeleteDomainsJobTest.php @@ -0,0 +1,42 @@ + DatabaseAndDomainTenant::class]); + } + + /** @test */ + public function job_delete_domains_successfully() + { + $tenant = DatabaseAndDomainTenant::create(); + + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + $tenant->domains()->create([ + 'domain' => 'bar.localhost', + ]); + + $this->assertSame($tenant->domains()->count(), 2); + + (new DeleteDomains($tenant))->handle(); + + $this->assertSame($tenant->refresh()->domains()->count(), 0); + } +} + +class DatabaseAndDomainTenant extends Etc\Tenant +{ + use HasDomains; +} From 97ab483173b38d9233082b349a9f73f268784449 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Wed, 20 Jul 2022 18:28:45 +0500 Subject: [PATCH 43/51] Completing PR #881 (#902) * install PHP CS Fixer * Fix styling * remove StyleCI config * use config from archtechx/template * Fix styling * added `php-cs-fixer` * Update .php-cs-fixer.php * added GitHub token * Update ci.yml * Update ci.yml * Update ci.yml * php-cs-fixer workflow same as template Co-authored-by: Erik Gaal Co-authored-by: erikgaal --- .github/workflows/ci.yml | 18 +++ .gitignore | 1 + .php-cs-fixer.php | 141 ++++++++++++++++++ .styleci.yml | 7 - .../FilesystemTenancyBootstrapper.php | 2 +- .../QueueTenancyBootstrapper.php | 9 +- src/CacheManager.php | 3 +- src/Commands/Install.php | 2 - src/Commands/Migrate.php | 2 - src/Commands/MigrateFresh.php | 2 - src/Commands/Rollback.php | 2 - src/Commands/Run.php | 2 - src/Commands/Seed.php | 2 - src/Commands/TenantDump.php | 3 +- src/Commands/TenantList.php | 2 - src/Concerns/ExtendsLaravelCommand.php | 2 + src/Concerns/HasATenantsOption.php | 2 +- src/Contracts/TenantDatabaseManager.php | 10 -- src/Database/Concerns/TenantRun.php | 3 - src/Database/Models/ImpersonationToken.php | 5 + src/Database/Models/Tenant.php | 2 + src/DatabaseConfig.php | 5 +- src/Features/UserImpersonation.php | 1 - src/Listeners/UpdateSyncedResource.php | 3 +- .../CheckTenantForMaintenanceMode.php | 2 +- src/Middleware/InitializeTenancyByDomain.php | 6 +- .../InitializeTenancyByDomainOrSubdomain.php | 2 - src/Middleware/InitializeTenancyByPath.php | 4 +- .../InitializeTenancyByRequestData.php | 2 - .../InitializeTenancyBySubdomain.php | 2 - .../Contracts/CachedTenantResolver.php | 1 - src/Tenancy.php | 5 - src/TenancyServiceProvider.php | 4 - tests/BootstrapperTest.php | 26 ++-- tests/Etc/ExampleSeeder.php | 2 +- tests/EventListenerTest.php | 1 - tests/MaintenanceModeTest.php | 4 +- tests/QueueTest.php | 36 +++-- tests/ResourceSyncingTest.php | 4 + tests/SingleDatabaseTenancyTest.php | 3 + tests/TenantDatabaseManagerTest.php | 4 +- tests/TenantModelTest.php | 1 + tests/TenantUserImpersonationTest.php | 2 + 43 files changed, 231 insertions(+), 111 deletions(-) create mode 100644 .php-cs-fixer.php delete mode 100644 .styleci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81d37af5..3cbda814 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,8 @@ name: CI env: COMPOSE_INTERACTIVE_NO_CLI: 1 + PHP_CS_FIXER_IGNORE_ENV: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} on: push: @@ -26,3 +28,19 @@ jobs: run: docker-compose exec -T test composer require --no-interaction "laravel/framework:${{ matrix.laravel }}" - name: Run tests run: ./test + + php-cs-fixer: + name: Code style (php-cs-fixer) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install php-cs-fixer + run: composer global require friendsofphp/php-cs-fixer + - name: Run php-cs-fixer + run: $HOME/.composer/vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php + - name: Commit changes from php-cs-fixer + uses: EndBug/add-and-commit@v5 + with: + author_name: "PHP CS Fixer" + author_email: "phpcsfixer@example.com" + message: Fix code style (php-cs-fixer) diff --git a/.gitignore b/.gitignore index 95522c34..64d9dc21 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ clover.xml tenant-schema-test.dump tests/Etc/tmp/queuetest.json docker-compose.override.yml +.php-cs-fixer.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000..589838bc --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,141 @@ + ['syntax' => 'short'], + 'binary_operator_spaces' => [ + 'default' => 'single_space', + 'operators' => [ + '=>' => null, + '|' => 'no_space', + ] + ], + 'blank_line_after_namespace' => true, + 'blank_line_after_opening_tag' => true, + 'no_superfluous_phpdoc_tags' => true, + 'blank_line_before_statement' => [ + 'statements' => ['return'] + ], + 'braces' => true, + 'cast_spaces' => true, + 'class_definition' => true, + 'concat_space' => [ + 'spacing' => 'one' + ], + 'declare_equal_normalize' => true, + 'elseif' => true, + 'encoding' => true, + 'full_opening_tag' => true, + 'declare_strict_types' => true, + 'fully_qualified_strict_types' => true, // added by Shift + 'function_declaration' => true, + 'function_typehint_space' => true, + 'heredoc_to_nowdoc' => true, + 'include' => true, + 'increment_style' => ['style' => 'post'], + 'indentation_type' => true, + 'linebreak_after_opening_tag' => true, + 'line_ending' => true, + 'lowercase_cast' => true, + 'constant_case' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, // added from Symfony + 'magic_method_casing' => true, // added from Symfony + 'magic_constant_casing' => true, + 'method_argument_space' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'extra', + 'throw', + 'use', + 'use_trait', + ] + ], + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_closing_tag' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => [ + 'use' => 'echo' + ], + 'no_multiline_whitespace_around_double_arrow' => true, + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line' + ], + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'not_operator_with_successor_space' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'phpdoc_indent' => true, + 'general_phpdoc_tag_rename' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'psr_autoloading' => true, + 'self_accessor' => true, + 'short_scalar_cast' => true, + 'simplified_null_return' => false, // disabled by Shift + 'single_blank_line_at_eof' => true, + 'single_blank_line_before_namespace' => true, + 'single_class_element_per_statement' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'no_unused_imports' => true, + 'single_line_comment_style' => [ + 'comment_types' => ['hash'] + ], + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, +]; + +$project_path = getcwd(); +$finder = Finder::create() + ->in([ + $project_path . '/src', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new Config()) + ->setFinder($finder) + ->setRules($rules) + ->setRiskyAllowed(true) + ->setUsingCache(true); diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index e6d2b2c1..00000000 --- a/.styleci.yml +++ /dev/null @@ -1,7 +0,0 @@ -risky: true -preset: laravel -enabled: -- declare_strict_types -disabled: -- concat_without_spaces -- ternary_operator_spaces diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php index 2b4f8dfe..6f720e7c 100644 --- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php +++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php @@ -69,7 +69,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper if (! $finalPrefix) { $finalPrefix = $originalRoot - ? rtrim($originalRoot, '/') . '/'. $suffix + ? rtrim($originalRoot, '/') . '/' . $suffix : $suffix; } diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php index 790e1344..2f859ecd 100644 --- a/src/Bootstrappers/QueueTenancyBootstrapper.php +++ b/src/Bootstrappers/QueueTenancyBootstrapper.php @@ -4,18 +4,17 @@ declare(strict_types=1); namespace Stancl\Tenancy\Bootstrappers; -use Illuminate\Support\Str; use Illuminate\Config\Repository; -use Illuminate\Queue\QueueManager; -use Stancl\Tenancy\Contracts\Tenant; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Foundation\Application; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; -use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\Events\JobRetryRequested; +use Illuminate\Queue\QueueManager; use Illuminate\Support\Testing\Fakes\QueueFake; -use Illuminate\Contracts\Foundation\Application; use Stancl\Tenancy\Contracts\TenancyBootstrapper; +use Stancl\Tenancy\Contracts\Tenant; class QueueTenancyBootstrapper implements TenancyBootstrapper { diff --git a/src/CacheManager.php b/src/CacheManager.php index 88428353..09581201 100644 --- a/src/CacheManager.php +++ b/src/CacheManager.php @@ -13,7 +13,6 @@ class CacheManager extends BaseCacheManager * * @param string $method * @param array $parameters - * @return mixed */ public function __call($method, $parameters) { @@ -21,7 +20,7 @@ class CacheManager extends BaseCacheManager if ($method === 'tags') { $count = count($parameters); - + if ($count !== 1) { throw new \Exception("Method tags() takes exactly 1 argument. $count passed."); } diff --git a/src/Commands/Install.php b/src/Commands/Install.php index dd2dd280..41492b26 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -24,8 +24,6 @@ class Install extends Command /** * Execute the console command. - * - * @return mixed */ public function handle() { diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index c67d3598..52ecd47f 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -33,8 +33,6 @@ class Migrate extends MigrateCommand /** * Execute the console command. - * - * @return mixed */ public function handle() { diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 283d70b0..63860153 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -31,8 +31,6 @@ final class MigrateFresh extends Command /** * Execute the console command. - * - * @return mixed */ public function handle() { diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index e60d974b..1c434189 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -42,8 +42,6 @@ class Rollback extends RollbackCommand /** * Execute the console command. - * - * @return mixed */ public function handle() { diff --git a/src/Commands/Run.php b/src/Commands/Run.php index aa518d7a..2b20d9c3 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -27,8 +27,6 @@ class Run extends Command /** * Execute the console command. - * - * @return mixed */ public function handle() { diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index dc97ae71..8c525208 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -35,8 +35,6 @@ class Seed extends SeedCommand /** * Execute the console command. - * - * @return mixed */ public function handle() { diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 557c6975..9c8698c6 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -21,10 +21,9 @@ class TenantDump extends DumpCommand $this->specifyParameters(); } - public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int { - $this->tenant()->run(fn() => parent::handle($connections, $dispatcher)); + $this->tenant()->run(fn () => parent::handle($connections, $dispatcher)); return Command::SUCCESS; } diff --git a/src/Commands/TenantList.php b/src/Commands/TenantList.php index d01afcb9..13775676 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -25,8 +25,6 @@ class TenantList extends Command /** * Execute the console command. - * - * @return mixed */ public function handle() { diff --git a/src/Concerns/ExtendsLaravelCommand.php b/src/Concerns/ExtendsLaravelCommand.php index bdafc8f7..d08ad6b6 100644 --- a/src/Concerns/ExtendsLaravelCommand.php +++ b/src/Concerns/ExtendsLaravelCommand.php @@ -1,5 +1,7 @@ manager()->makeConnectionConfig( - array_merge($templateConnection, $this->tenantConfig()), $this->getName() + array_merge($templateConnection, $this->tenantConfig()), + $this->getName() ); } diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 48d65bb9..f96465ff 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -33,7 +33,6 @@ class UserImpersonation implements Feature * * @param string|ImpersonationToken $token * @param int $ttl - * @return RedirectResponse */ public static function makeResponse($token, int $ttl = null): RedirectResponse { diff --git a/src/Listeners/UpdateSyncedResource.php b/src/Listeners/UpdateSyncedResource.php index 40d4d644..9be290f0 100644 --- a/src/Listeners/UpdateSyncedResource.php +++ b/src/Listeners/UpdateSyncedResource.php @@ -48,8 +48,7 @@ class UpdateSyncedResource extends QueueableListener protected function updateResourceInCentralDatabaseAndGetTenants($event, $syncedAttributes) { /** @var Model|SyncMaster $centralModel */ - $centralModel = $event->model->getCentralModelName() - ::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey()) + $centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey()) ->first(); // We disable events for this call, to avoid triggering this event & listener again. diff --git a/src/Middleware/CheckTenantForMaintenanceMode.php b/src/Middleware/CheckTenantForMaintenanceMode.php index 8e29a31e..c1c734f5 100644 --- a/src/Middleware/CheckTenantForMaintenanceMode.php +++ b/src/Middleware/CheckTenantForMaintenanceMode.php @@ -5,10 +5,10 @@ declare(strict_types=1); namespace Stancl\Tenancy\Middleware; use Closure; -use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; use Symfony\Component\HttpFoundation\IpUtils; +use Symfony\Component\HttpKernel\Exception\HttpException; class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode { diff --git a/src/Middleware/InitializeTenancyByDomain.php b/src/Middleware/InitializeTenancyByDomain.php index 24a1abb7..5a07112d 100644 --- a/src/Middleware/InitializeTenancyByDomain.php +++ b/src/Middleware/InitializeTenancyByDomain.php @@ -29,13 +29,13 @@ class InitializeTenancyByDomain extends IdentificationMiddleware * Handle an incoming request. * * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed */ public function handle($request, Closure $next) { return $this->initializeTenancy( - $request, $next, $request->getHost() + $request, + $next, + $request->getHost() ); } } diff --git a/src/Middleware/InitializeTenancyByDomainOrSubdomain.php b/src/Middleware/InitializeTenancyByDomainOrSubdomain.php index 94217bba..9b153db3 100644 --- a/src/Middleware/InitializeTenancyByDomainOrSubdomain.php +++ b/src/Middleware/InitializeTenancyByDomainOrSubdomain.php @@ -13,8 +13,6 @@ class InitializeTenancyByDomainOrSubdomain * Handle an incoming request. * * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed */ public function handle($request, Closure $next) { diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index 6289199b..e66400c5 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -38,7 +38,9 @@ class InitializeTenancyByPath extends IdentificationMiddleware // simply injected into some route controller action. if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) { return $this->initializeTenancy( - $request, $next, $route + $request, + $next, + $route ); } else { throw new RouteIsMissingTenantParameterException; diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php index de75d8c5..4e1d33ff 100644 --- a/src/Middleware/InitializeTenancyByRequestData.php +++ b/src/Middleware/InitializeTenancyByRequestData.php @@ -36,8 +36,6 @@ class InitializeTenancyByRequestData extends IdentificationMiddleware * Handle an incoming request. * * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed */ public function handle($request, Closure $next) { diff --git a/src/Middleware/InitializeTenancyBySubdomain.php b/src/Middleware/InitializeTenancyBySubdomain.php index 55d76b05..76389df7 100644 --- a/src/Middleware/InitializeTenancyBySubdomain.php +++ b/src/Middleware/InitializeTenancyBySubdomain.php @@ -28,8 +28,6 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain * Handle an incoming request. * * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed */ public function handle($request, Closure $next) { diff --git a/src/Resolvers/Contracts/CachedTenantResolver.php b/src/Resolvers/Contracts/CachedTenantResolver.php index 968ac794..e84f1fb1 100644 --- a/src/Resolvers/Contracts/CachedTenantResolver.php +++ b/src/Resolvers/Contracts/CachedTenantResolver.php @@ -75,7 +75,6 @@ abstract class CachedTenantResolver implements TenantResolver /** * Get all the arg combinations for resolve() that can be used to find this tenant. * - * @param Tenant $tenant * @return array[] */ abstract public function getArgsForTenant(Tenant $tenant): array; diff --git a/src/Tenancy.php b/src/Tenancy.php index 30f138e3..6873f93b 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -27,7 +27,6 @@ class Tenancy /** * Initializes the tenant. * @param Tenant|int|string $tenant - * @return void */ public function initialize($tenant): void { @@ -106,9 +105,6 @@ class Tenancy /** * Run a callback in the central context. * Atomic, safely reverts to previous context. - * - * @param callable $callback - * @return mixed */ public function central(callable $callback) { @@ -132,7 +128,6 @@ class Tenancy * More performant than running $tenant->run() one by one. * * @param Tenant[]|\Traversable|string[]|null $tenants - * @param callable $callback * @return void */ public function runForMultiple($tenants, callable $callback) diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index dd061af3..e23200a6 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -15,8 +15,6 @@ class TenancyServiceProvider extends ServiceProvider { /** * Register services. - * - * @return void */ public function register(): void { @@ -76,8 +74,6 @@ class TenancyServiceProvider extends ServiceProvider /** * Bootstrap services. - * - * @return void */ public function boot(): void { diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 588fadd8..a0320282 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -5,26 +5,26 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; use Illuminate\Filesystem\FilesystemAdapter; -use ReflectionObject; -use ReflectionProperty; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\DB; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; -use Stancl\Tenancy\Events\TenancyEnded; -use Stancl\Tenancy\Jobs\CreateDatabase; -use Stancl\Tenancy\Events\TenantCreated; -use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Listeners\BootstrapTenancy; -use Stancl\Tenancy\Listeners\RevertToCentralContext; +use Illuminate\Support\Str; +use ReflectionObject; +use ReflectionProperty; +use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; +use Stancl\Tenancy\Events\TenancyEnded; +use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Listeners\RevertToCentralContext; +use Stancl\Tenancy\Tests\Etc\Tenant; class BootstrapperTest extends TestCase { diff --git a/tests/Etc/ExampleSeeder.php b/tests/Etc/ExampleSeeder.php index a3e36123..2f97787e 100644 --- a/tests/Etc/ExampleSeeder.php +++ b/tests/Etc/ExampleSeeder.php @@ -19,7 +19,7 @@ class ExampleSeeder extends Seeder { DB::table('users')->insert([ 'name' => Str::random(10), - 'email' => Str::random(10).'@gmail.com', + 'email' => Str::random(10) . '@gmail.com', 'password' => bcrypt('password'), ]); } diff --git a/tests/EventListenerTest.php b/tests/EventListenerTest.php index 4a45205c..02ed8b3b 100644 --- a/tests/EventListenerTest.php +++ b/tests/EventListenerTest.php @@ -20,7 +20,6 @@ use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Jobs\MigrateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\QueueableListener; -use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tests\Etc\Tenant; class EventListenerTest extends TestCase diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index 4a8d8d0c..90232932 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -4,14 +4,12 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; -use Symfony\Component\HttpKernel\Exception\HttpException; -use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException; use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Tests\Etc\Tenant; -use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; class MaintenanceModeTest extends TestCase { diff --git a/tests/QueueTest.php b/tests/QueueTest.php index a3df9cd7..fe34ba92 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -4,33 +4,31 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; -use Closure; use Exception; -use Illuminate\Support\Str; use Illuminate\Bus\Queueable; -use Spatie\Valuestore\Valuestore; -use Illuminate\Support\Facades\DB; -use Stancl\Tenancy\Tests\Etc\User; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Tests\Etc\Tenant; -use Illuminate\Support\Facades\Event; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Schema; -use Stancl\Tenancy\Events\TenancyEnded; -use Stancl\Tenancy\Jobs\CreateDatabase; -use Illuminate\Queue\InteractsWithQueue; -use Stancl\Tenancy\Events\TenantCreated; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Foundation\Bus\Dispatchable; -use PDO; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Str; +use Spatie\Valuestore\Valuestore; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; +use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Tests\Etc\User; class QueueTest extends TestCase { diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 570448d1..0ff95a52 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -589,7 +589,9 @@ class CentralUser extends Model implements SyncMaster use ResourceSyncing, CentralConnection; protected $guarded = []; + public $timestamps = false; + public $table = 'users'; public function tenants(): BelongsToMany @@ -633,7 +635,9 @@ class ResourceUser extends Model implements Syncable use ResourceSyncing; protected $table = 'users'; + protected $guarded = []; + public $timestamps = false; public function getGlobalIdentifierKey() diff --git a/tests/SingleDatabaseTenancyTest.php b/tests/SingleDatabaseTenancyTest.php index b64478cc..d0425dd9 100644 --- a/tests/SingleDatabaseTenancyTest.php +++ b/tests/SingleDatabaseTenancyTest.php @@ -329,6 +329,7 @@ class Post extends Model use BelongsToTenant; protected $guarded = []; + public $timestamps = false; public function comments() @@ -345,6 +346,7 @@ class Post extends Model class Comment extends Model { protected $guarded = []; + public $timestamps = false; public function post() @@ -368,5 +370,6 @@ class ScopedComment extends Comment class GlobalResource extends Model { protected $guarded = []; + public $timestamps = false; } diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 0e1464c0..12273c85 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -104,7 +104,7 @@ class TenantDatabaseManagerTest extends TestCase ['sqlite', SQLiteDatabaseManager::class], ['pgsql', PostgreSQLDatabaseManager::class], ['pgsql', PostgreSQLSchemaManager::class], - ['sqlsrv', MicrosoftSQLDatabaseManager::class] + ['sqlsrv', MicrosoftSQLDatabaseManager::class], ]; } @@ -196,7 +196,7 @@ class TenantDatabaseManagerTest extends TestCase ]); tenancy()->initialize($tenant); - $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? + $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? config('database.connections.' . config('database.default') . '.search_path') : config('database.connections.' . config('database.default') . '.schema'); diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index 46dc6a00..2d46c233 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -172,6 +172,7 @@ class MyTenant extends Tenant class AnotherTenant extends Model implements Contracts\Tenant { protected $guarded = []; + protected $table = 'tenants'; public function getTenantKeyName(): string diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index c5e83853..b50db84b 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -274,11 +274,13 @@ class TenantUserImpersonationTest extends TestCase class ImpersonationUser extends Authenticable { protected $guarded = []; + protected $table = 'users'; } class AnotherImpersonationUser extends Authenticable { protected $guarded = []; + protected $table = 'users'; } From c0f97fa04ea2c7fb2ab6e353842d5e30dcd545e2 Mon Sep 17 00:00:00 2001 From: PHP CS Fixer Date: Wed, 20 Jul 2022 13:29:11 +0000 Subject: [PATCH 44/51] Fix code style (php-cs-fixer) --- src/Jobs/DeleteDomains.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Jobs/DeleteDomains.php b/src/Jobs/DeleteDomains.php index fac60e43..4ea92b7f 100644 --- a/src/Jobs/DeleteDomains.php +++ b/src/Jobs/DeleteDomains.php @@ -5,16 +5,10 @@ declare(strict_types=1); namespace Stancl\Tenancy\Jobs; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Contracts\TenantWithDatabase; -use Stancl\Tenancy\Database\Models\Domain; -use Stancl\Tenancy\Events\DatabaseDeleted; -use Stancl\Tenancy\Events\DeletingDatabase; -use Stancl\Tenancy\Events\DeletingDomain; -use Stancl\Tenancy\Events\DomainDeleted; class DeleteDomains { From 69de181b7def49be614ab3a67ec76544c32284d6 Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Fri, 22 Jul 2022 22:22:33 +0500 Subject: [PATCH 45/51] removed PHP_CS_FIXER_IGNORE_ENV (#904) --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cbda814..f0fe927a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,6 @@ name: CI env: COMPOSE_INTERACTIVE_NO_CLI: 1 - PHP_CS_FIXER_IGNORE_ENV: 1 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} on: From b47c5549ef86486a993482ce10b973dd7ed50e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 22 Jul 2022 19:26:59 +0200 Subject: [PATCH 46/51] [4.x] Migrate tests to Pest (#884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Pest dependencies * Add base Pest file * Convert test cases * Remove non-compound imports * Adopt expectation API * Optimize uses * Shift cleanup * phpunit -> pest * Fix tests in PR #884 PHPUnit to Pest Converter (#885) * fixed tests, remove method duplications, restore necessary inner classes * Update CommandsTest.php * temporary checks run on `shift-64622` on branch. * fixed `TestSeeder` class not resolved * fixed messed up names * removed `uses` from individual files and add it in `Pest` * extract tests to helpers * use pest dataset * Update AutomaticModeTest.php * newline * todo convention * resolve reviews * added `// todo@tests` * remove shift branch from CI workflow Co-authored-by: Samuel Å tancl * check if I have write permission * Convert newly added tests to Pest Co-authored-by: Shift Co-authored-by: Abrar Ahmad --- composer.json | 11 +- test | 2 +- tests/AutomaticModeTest.php | 194 ++-- tests/BootstrapperTest.php | 388 ++++---- tests/CacheManagerTest.php | 170 ++-- tests/CachedTenantResolverTest.php | 149 ++- ...edDomainAndSubdomainIdentificationTest.php | 92 +- tests/CommandsTest.php | 403 ++++---- tests/DatabasePreparationTest.php | 145 ++- tests/DatabaseUsersTest.php | 160 ++- tests/DeleteDomainsJobTest.php | 44 +- tests/DomainTest.php | 146 ++- tests/Etc/TestSeeder.php | 23 + tests/EventListenerTest.php | 267 +++-- tests/Features/RedirectTest.php | 54 +- tests/Features/TenantConfigTest.php | 131 ++- tests/GlobalCacheTest.php | 71 +- tests/MaintenanceModeTest.php | 43 +- tests/PathIdentificationTest.php | 195 ++-- tests/Pest.php | 3 + tests/QueueTest.php | 438 ++++----- tests/RequestDataIdentificationTest.php | 85 +- tests/ResourceSyncingTest.php | 920 +++++++++--------- tests/ScopeSessionsTest.php | 98 +- tests/SingleDatabaseTenancyTest.php | 554 +++++------ tests/SubdomainTest.php | 204 ++-- tests/TenantAssetTest.php | 201 ++-- tests/TenantAwareCommandTest.php | 37 +- tests/TenantDatabaseManagerTest.php | 473 +++++---- tests/TenantModelTest.php | 221 ++--- tests/TenantUserImpersonationTest.php | 461 +++++---- tests/UniversalRouteTest.php | 105 +- 32 files changed, 3010 insertions(+), 3478 deletions(-) create mode 100644 tests/Etc/TestSeeder.php create mode 100644 tests/Pest.php diff --git a/composer.json b/composer.json index 8123944a..3143175a 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,12 @@ { "name": "stancl/tenancy", "description": "Automatic multi-tenancy for your Laravel application.", - "keywords": ["laravel", "multi-tenancy", "multi-database", "tenancy"], + "keywords": [ + "laravel", + "multi-tenancy", + "multi-database", + "tenancy" + ], "license": "MIT", "authors": [ { @@ -20,10 +25,10 @@ "require-dev": { "laravel/framework": "^6.0|^7.0|^8.0|^9.0", "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", - "phpunit/phpunit": "*", "league/flysystem-aws-s3-v3": "^1.0|^3.0", "doctrine/dbal": "^2.10", - "spatie/valuestore": "^1.2.5" + "spatie/valuestore": "^1.2.5", + "pestphp/pest": "^1.21" }, "autoload": { "psr-4": { diff --git a/test b/test index 49535a7a..d8de021e 100755 --- a/test +++ b/test @@ -1,3 +1,3 @@ #!/bin/bash -docker-compose exec -T test vendor/bin/phpunit "$@" +docker-compose exec -T test vendor/bin/pest "$@" diff --git a/tests/AutomaticModeTest.php b/tests/AutomaticModeTest.php index 714092c3..7b5d5ded 100644 --- a/tests/AutomaticModeTest.php +++ b/tests/AutomaticModeTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Event; use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Events\TenancyEnded; @@ -12,113 +10,99 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\Tenant; -class AutomaticModeTest extends TestCase +beforeEach(function () { + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); +}); + +test('context is switched when tenancy is initialized', function () { + contextIsSwitchedWhenTenancyInitialized(); +}); + +test('context is reverted when tenancy is ended', function () { + contextIsSwitchedWhenTenancyInitialized(); + + tenancy()->end(); + + expect(app('tenancy_ended'))->toBe(true); +}); + +test('context is switched when tenancy is reinitialized', function () { + config(['tenancy.bootstrappers' => [ + MyBootstrapper::class, + ]]); + + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant); + + expect(app('tenancy_initialized_for_tenant'))->toBe('acme'); + + $tenant2 = Tenant::create([ + 'id' => 'foobar', + ]); + + tenancy()->initialize($tenant2); + + expect(app('tenancy_initialized_for_tenant'))->toBe('foobar'); +}); + +test('central helper runs callbacks in the central state', function () { + tenancy()->initialize($tenant = Tenant::create()); + + tenancy()->central(function () { + expect(tenant())->toBe(null); + }); + + expect(tenant())->toBe($tenant); +}); + +test('central helper returns the value from the callback', function () { + tenancy()->initialize(Tenant::create()); + + $this->assertSame('foo', tenancy()->central(function () { + return 'foo'; + })); +}); + +test('central helper reverts back to tenant context', function () { + tenancy()->initialize($tenant = Tenant::create()); + + tenancy()->central(function () { + // + }); + + expect(tenant())->toBe($tenant); +}); + +test('central helper doesnt change tenancy state when called in central context', function () { + expect(tenancy()->initialized)->toBeFalse(); + expect(tenant())->toBeNull(); + + tenancy()->central(function () { + // + }); + + expect(tenancy()->initialized)->toBeFalse(); + expect(tenant())->toBeNull(); +}); + +// todo@tests +function contextIsSwitchedWhenTenancyInitialized() { - public function setUp(): void - { - parent::setUp(); + config(['tenancy.bootstrappers' => [ + MyBootstrapper::class, + ]]); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); - } + $tenant = Tenant::create([ + 'id' => 'acme', + ]); - /** @test */ - public function context_is_switched_when_tenancy_is_initialized() - { - config(['tenancy.bootstrappers' => [ - MyBootstrapper::class, - ]]); + tenancy()->initialize($tenant); - $tenant = Tenant::create([ - 'id' => 'acme', - ]); - - tenancy()->initialize($tenant); - - $this->assertSame('acme', app('tenancy_initialized_for_tenant')); - } - - /** @test */ - public function context_is_reverted_when_tenancy_is_ended() - { - $this->context_is_switched_when_tenancy_is_initialized(); - - tenancy()->end(); - - $this->assertSame(true, app('tenancy_ended')); - } - - /** @test */ - public function context_is_switched_when_tenancy_is_reinitialized() - { - config(['tenancy.bootstrappers' => [ - MyBootstrapper::class, - ]]); - - $tenant = Tenant::create([ - 'id' => 'acme', - ]); - - tenancy()->initialize($tenant); - - $this->assertSame('acme', app('tenancy_initialized_for_tenant')); - - $tenant2 = Tenant::create([ - 'id' => 'foobar', - ]); - - tenancy()->initialize($tenant2); - - $this->assertSame('foobar', app('tenancy_initialized_for_tenant')); - } - - /** @test */ - public function central_helper_runs_callbacks_in_the_central_state() - { - tenancy()->initialize($tenant = Tenant::create()); - - tenancy()->central(function () { - $this->assertSame(null, tenant()); - }); - - $this->assertSame($tenant, tenant()); - } - - /** @test */ - public function central_helper_returns_the_value_from_the_callback() - { - tenancy()->initialize(Tenant::create()); - - $this->assertSame('foo', tenancy()->central(function () { - return 'foo'; - })); - } - - /** @test */ - public function central_helper_reverts_back_to_tenant_context() - { - tenancy()->initialize($tenant = Tenant::create()); - - tenancy()->central(function () { - // - }); - - $this->assertSame($tenant, tenant()); - } - - /** @test */ - public function central_helper_doesnt_change_tenancy_state_when_called_in_central_context() - { - $this->assertFalse(tenancy()->initialized); - $this->assertNull(tenant()); - - tenancy()->central(function () { - // - }); - - $this->assertFalse(tenancy()->initialized); - $this->assertNull(tenant()); - } + expect(app('tenancy_initialized_for_tenant'))->toBe('acme'); } class MyBootstrapper implements TenancyBootstrapper diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index a0320282..929c4e47 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -2,226 +2,204 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Filesystem\FilesystemAdapter; -use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Str; use Illuminate\Support\Facades\DB; +use Stancl\JobPipeline\JobPipeline; +use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; -use ReflectionObject; -use ReflectionProperty; -use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Events\TenancyEnded; -use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Tests\Etc\Tenant; +use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -class BootstrapperTest extends TestCase +beforeEach(function () { + Event::listen( + TenantCreated::class, + JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); +}); + +test('database data is separated', function () { + config(['tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class, + ]]); + + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + $this->artisan('tenants:migrate'); + + tenancy()->initialize($tenant1); + + // Create Foo user + DB::table('users')->insert(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + expect(DB::table('users')->get())->toHaveCount(1); + + tenancy()->initialize($tenant2); + + // Assert Foo user is not in this DB + expect(DB::table('users')->get())->toHaveCount(0); + // Create Bar user + DB::table('users')->insert(['name' => 'Bar', 'email' => 'bar@bar.com', 'password' => 'secret']); + expect(DB::table('users')->get())->toHaveCount(1); + + tenancy()->initialize($tenant1); + + // Assert Bar user is not in this DB + expect(DB::table('users')->get())->toHaveCount(1); + expect(DB::table('users')->first()->name)->toBe('Foo'); +}); + +test('cache data is separated', function () { + config([ + 'tenancy.bootstrappers' => [ + CacheTenancyBootstrapper::class, + ], + 'cache.default' => 'redis', + ]); + + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + cache()->set('foo', 'central'); + expect(Cache::get('foo'))->toBe('central'); + + tenancy()->initialize($tenant1); + + // Assert central cache doesn't leak to tenant context + expect(Cache::has('foo'))->toBeFalse(); + + cache()->set('foo', 'bar'); + expect(Cache::get('foo'))->toBe('bar'); + + tenancy()->initialize($tenant2); + + // Assert one tenant's data doesn't leak to another tenant + expect(Cache::has('foo'))->toBeFalse(); + + cache()->set('foo', 'xyz'); + expect(Cache::get('foo'))->toBe('xyz'); + + tenancy()->initialize($tenant1); + + // Asset data didn't leak to original tenant + expect(Cache::get('foo'))->toBe('bar'); + + tenancy()->end(); + + // Asset central is still the same + expect(Cache::get('foo'))->toBe('central'); +}); + +test('redis data is separated', function () { + config(['tenancy.bootstrappers' => [ + RedisTenancyBootstrapper::class, + ]]); + + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + tenancy()->initialize($tenant1); + Redis::set('foo', 'bar'); + expect(Redis::get('foo'))->toBe('bar'); + + tenancy()->initialize($tenant2); + expect(Redis::get('foo'))->toBe(null); + Redis::set('foo', 'xyz'); + Redis::set('abc', 'def'); + expect(Redis::get('foo'))->toBe('xyz'); + expect(Redis::get('abc'))->toBe('def'); + + tenancy()->initialize($tenant1); + expect(Redis::get('foo'))->toBe('bar'); + expect(Redis::get('abc'))->toBe(null); + + $tenant3 = Tenant::create(); + tenancy()->initialize($tenant3); + expect(Redis::get('foo'))->toBe(null); + expect(Redis::get('abc'))->toBe(null); +}); + +test('filesystem data is separated', function () { + config(['tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + ]]); + + $old_storage_path = storage_path(); + $old_storage_facade_roots = []; + foreach (config('tenancy.filesystem.disks') as $disk) { + $old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root"); + } + + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + tenancy()->initialize($tenant1); + + Storage::disk('public')->put('foo', 'bar'); + expect(Storage::disk('public')->get('foo'))->toBe('bar'); + + tenancy()->initialize($tenant2); + expect(Storage::disk('public')->exists('foo'))->toBeFalse(); + Storage::disk('public')->put('foo', 'xyz'); + Storage::disk('public')->put('abc', 'def'); + expect(Storage::disk('public')->get('foo'))->toBe('xyz'); + expect(Storage::disk('public')->get('abc'))->toBe('def'); + + tenancy()->initialize($tenant1); + expect(Storage::disk('public')->get('foo'))->toBe('bar'); + expect(Storage::disk('public')->exists('abc'))->toBeFalse(); + + $tenant3 = Tenant::create(); + tenancy()->initialize($tenant3); + expect(Storage::disk('public')->exists('foo'))->toBeFalse(); + expect(Storage::disk('public')->exists('abc'))->toBeFalse(); + + $expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base + + // Check that disk prefixes respect the root_override logic + expect(getDiskPrefix('local'))->toBe($expected_storage_path . '/app/'); + expect(getDiskPrefix('public'))->toBe($expected_storage_path . '/app/public/'); + $this->assertSame('tenant' . tenant('id') . '/', getDiskPrefix('s3'), '/'); + + // Check suffixing logic + $new_storage_path = storage_path(); + expect($new_storage_path)->toEqual($expected_storage_path); +}); + +function getDiskPrefix(string $disk): string { - public $mockConsoleOutput = false; + /** @var FilesystemAdapter $disk */ + $disk = Storage::disk($disk); + $adapter = $disk->getAdapter(); - public function setUp(): void - { - parent::setUp(); - - Event::listen( - TenantCreated::class, - JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); + if (! Str::startsWith(app()->version(), '9.')) { + return $adapter->getPathPrefix(); } - /** @test */ - public function database_data_is_separated() - { - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); + $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); + $prefixer->setAccessible(true); - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); + // reflection -> instance + $prefixer = $prefixer->getValue($adapter); - $this->artisan('tenants:migrate'); + $prefix = (new ReflectionProperty($prefixer, 'prefix')); + $prefix->setAccessible(true); - tenancy()->initialize($tenant1); - - // Create Foo user - DB::table('users')->insert(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); - $this->assertCount(1, DB::table('users')->get()); - - tenancy()->initialize($tenant2); - - // Assert Foo user is not in this DB - $this->assertCount(0, DB::table('users')->get()); - // Create Bar user - DB::table('users')->insert(['name' => 'Bar', 'email' => 'bar@bar.com', 'password' => 'secret']); - $this->assertCount(1, DB::table('users')->get()); - - tenancy()->initialize($tenant1); - - // Assert Bar user is not in this DB - $this->assertCount(1, DB::table('users')->get()); - $this->assertSame('Foo', DB::table('users')->first()->name); - } - - /** @test */ - public function cache_data_is_separated() - { - config([ - 'tenancy.bootstrappers' => [ - CacheTenancyBootstrapper::class, - ], - 'cache.default' => 'redis', - ]); - - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - - cache()->set('foo', 'central'); - $this->assertSame('central', Cache::get('foo')); - - tenancy()->initialize($tenant1); - - // Assert central cache doesn't leak to tenant context - $this->assertFalse(Cache::has('foo')); - - cache()->set('foo', 'bar'); - $this->assertSame('bar', Cache::get('foo')); - - tenancy()->initialize($tenant2); - - // Assert one tenant's data doesn't leak to another tenant - $this->assertFalse(Cache::has('foo')); - - cache()->set('foo', 'xyz'); - $this->assertSame('xyz', Cache::get('foo')); - - tenancy()->initialize($tenant1); - - // Asset data didn't leak to original tenant - $this->assertSame('bar', Cache::get('foo')); - - tenancy()->end(); - - // Asset central is still the same - $this->assertSame('central', Cache::get('foo')); - } - - /** @test */ - public function redis_data_is_separated() - { - config(['tenancy.bootstrappers' => [ - RedisTenancyBootstrapper::class, - ]]); - - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - - tenancy()->initialize($tenant1); - Redis::set('foo', 'bar'); - $this->assertSame('bar', Redis::get('foo')); - - tenancy()->initialize($tenant2); - $this->assertSame(null, Redis::get('foo')); - Redis::set('foo', 'xyz'); - Redis::set('abc', 'def'); - $this->assertSame('xyz', Redis::get('foo')); - $this->assertSame('def', Redis::get('abc')); - - tenancy()->initialize($tenant1); - $this->assertSame('bar', Redis::get('foo')); - $this->assertSame(null, Redis::get('abc')); - - $tenant3 = Tenant::create(); - tenancy()->initialize($tenant3); - $this->assertSame(null, Redis::get('foo')); - $this->assertSame(null, Redis::get('abc')); - } - - /** @test */ - public function filesystem_data_is_separated() - { - config(['tenancy.bootstrappers' => [ - FilesystemTenancyBootstrapper::class, - ]]); - - $old_storage_path = storage_path(); - $old_storage_facade_roots = []; - foreach (config('tenancy.filesystem.disks') as $disk) { - $old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root"); - } - - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - - tenancy()->initialize($tenant1); - - Storage::disk('public')->put('foo', 'bar'); - $this->assertSame('bar', Storage::disk('public')->get('foo')); - - tenancy()->initialize($tenant2); - $this->assertFalse(Storage::disk('public')->exists('foo')); - Storage::disk('public')->put('foo', 'xyz'); - Storage::disk('public')->put('abc', 'def'); - $this->assertSame('xyz', Storage::disk('public')->get('foo')); - $this->assertSame('def', Storage::disk('public')->get('abc')); - - tenancy()->initialize($tenant1); - $this->assertSame('bar', Storage::disk('public')->get('foo')); - $this->assertFalse(Storage::disk('public')->exists('abc')); - - $tenant3 = Tenant::create(); - tenancy()->initialize($tenant3); - $this->assertFalse(Storage::disk('public')->exists('foo')); - $this->assertFalse(Storage::disk('public')->exists('abc')); - - $expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base - - // Check that disk prefixes respect the root_override logic - $this->assertSame($expected_storage_path . '/app/', $this->getDiskPrefix('local')); - $this->assertSame($expected_storage_path . '/app/public/', $this->getDiskPrefix('public')); - $this->assertSame('tenant' . tenant('id') . '/', $this->getDiskPrefix('s3'), '/'); - - // Check suffixing logic - $new_storage_path = storage_path(); - $this->assertEquals($expected_storage_path, $new_storage_path); - } - - protected function getDiskPrefix(string $disk): string - { - /** @var FilesystemAdapter $disk */ - $disk = Storage::disk($disk); - $adapter = $disk->getAdapter(); - - if (! Str::startsWith(app()->version(), '9.')) { - return $adapter->getPathPrefix(); - } - - $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); - $prefixer->setAccessible(true); - - // reflection -> instance - $prefixer = $prefixer->getValue($adapter); - - $prefix = (new ReflectionProperty($prefixer, 'prefix')); - $prefix->setAccessible(true); - - return $prefix->getValue($prefixer); - } - - // for queues see QueueTest + return $prefix->getValue($prefixer); } diff --git a/tests/CacheManagerTest.php b/tests/CacheManagerTest.php index a54aaa67..7b34a7df 100644 --- a/tests/CacheManagerTest.php +++ b/tests/CacheManagerTest.php @@ -2,136 +2,110 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Event; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Tests\Etc\Tenant; -class CacheManagerTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + config(['tenancy.bootstrappers' => [ + CacheTenancyBootstrapper::class, + ]]); - config(['tenancy.bootstrappers' => [ - CacheTenancyBootstrapper::class, - ]]); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); +}); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - } +test('default tag is automatically applied', function () { + tenancy()->initialize(Tenant::create()); - /** @test */ - public function default_tag_is_automatically_applied() - { - tenancy()->initialize(Tenant::create()); + $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); +}); - $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); - } +test('tags are merged when array is passed', function () { + tenancy()->initialize(Tenant::create()); - /** @test */ - public function tags_are_merged_when_array_is_passed() - { - tenancy()->initialize(Tenant::create()); + $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; + expect(cache()->tags(['foo', 'bar'])->getTags()->getNames())->toEqual($expected); +}); - $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; - $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); - } +test('tags are merged when string is passed', function () { + tenancy()->initialize(Tenant::create()); - /** @test */ - public function tags_are_merged_when_string_is_passed() - { - tenancy()->initialize(Tenant::create()); + $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo']; + expect(cache()->tags('foo')->getTags()->getNames())->toEqual($expected); +}); - $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo']; - $this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames()); - } +test('exception is thrown when zero arguments are passed to tags method', function () { + tenancy()->initialize(Tenant::create()); - /** @test */ - public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method() - { - tenancy()->initialize(Tenant::create()); + $this->expectException(\Exception::class); + cache()->tags(); +}); - $this->expectException(\Exception::class); - cache()->tags(); - } +test('exception is thrown when more than one argument is passed to tags method', function () { + tenancy()->initialize(Tenant::create()); - /** @test */ - public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method() - { - tenancy()->initialize(Tenant::create()); + $this->expectException(\Exception::class); + cache()->tags(1, 2); +}); - $this->expectException(\Exception::class); - cache()->tags(1, 2); - } +test('tags separate cache well enough', function () { + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); - /** @test */ - public function tags_separate_cache_well_enough() - { - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); + cache()->put('foo', 'bar', 1); + expect(cache()->get('foo'))->toBe('bar'); - cache()->put('foo', 'bar', 1); - $this->assertSame('bar', cache()->get('foo')); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant2); - $tenant2 = Tenant::create(); - tenancy()->initialize($tenant2); + $this->assertNotSame('bar', cache()->get('foo')); - $this->assertNotSame('bar', cache()->get('foo')); + cache()->put('foo', 'xyz', 1); + expect(cache()->get('foo'))->toBe('xyz'); +}); - cache()->put('foo', 'xyz', 1); - $this->assertSame('xyz', cache()->get('foo')); - } +test('invoking the cache helper works', function () { + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); - /** @test */ - public function invoking_the_cache_helper_works() - { - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); + cache(['foo' => 'bar'], 1); + expect(cache('foo'))->toBe('bar'); - cache(['foo' => 'bar'], 1); - $this->assertSame('bar', cache('foo')); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant2); - $tenant2 = Tenant::create(); - tenancy()->initialize($tenant2); + $this->assertNotSame('bar', cache('foo')); - $this->assertNotSame('bar', cache('foo')); + cache(['foo' => 'xyz'], 1); + expect(cache('foo'))->toBe('xyz'); +}); - cache(['foo' => 'xyz'], 1); - $this->assertSame('xyz', cache('foo')); - } +test('cache is persisted', function () { + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); - /** @test */ - public function cache_is_persisted() - { - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); + cache(['foo' => 'bar'], 10); + expect(cache('foo'))->toBe('bar'); - cache(['foo' => 'bar'], 10); - $this->assertSame('bar', cache('foo')); + tenancy()->end(); - tenancy()->end(); + tenancy()->initialize($tenant1); + expect(cache('foo'))->toBe('bar'); +}); - tenancy()->initialize($tenant1); - $this->assertSame('bar', cache('foo')); - } +test('cache is persisted when reidentification is used', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant1); - /** @test */ - public function cache_is_persisted_when_reidentification_is_used() - { - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - tenancy()->initialize($tenant1); + cache(['foo' => 'bar'], 10); + expect(cache('foo'))->toBe('bar'); - cache(['foo' => 'bar'], 10); - $this->assertSame('bar', cache('foo')); + tenancy()->initialize($tenant2); + tenancy()->end(); - tenancy()->initialize($tenant2); - tenancy()->end(); - - tenancy()->initialize($tenant1); - $this->assertSame('bar', cache('foo')); - } -} + tenancy()->initialize($tenant1); + expect(cache('foo'))->toBe('bar'); +}); diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index e7eb52d3..dad0c010 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -2,111 +2,94 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; -class CachedTenantResolverTest extends TestCase -{ - public function tearDown(): void - { - DomainTenantResolver::$shouldCache = false; +afterEach(function () { + DomainTenantResolver::$shouldCache = false; +}); - parent::tearDown(); - } +test('tenants can be resolved using the cached resolver', function () { + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'acme', + ]); - /** @test */ - public function tenants_can_be_resolved_using_the_cached_resolver() - { - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'acme', - ]); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue()->toBeTrue(); +}); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - } +test('the underlying resolver is not touched when using the cached resolver', function () { + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'acme', + ]); - /** @test */ - public function the_underlying_resolver_is_not_touched_when_using_the_cached_resolver() - { - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'acme', - ]); + DB::enableQueryLog(); - DB::enableQueryLog(); + DomainTenantResolver::$shouldCache = false; - DomainTenantResolver::$shouldCache = false; + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + DB::flushQueryLog(); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + $this->assertNotEmpty(DB::getQueryLog()); // not empty - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - DB::flushQueryLog(); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertNotEmpty(DB::getQueryLog()); // not empty + DomainTenantResolver::$shouldCache = true; - DomainTenantResolver::$shouldCache = true; + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + DB::flushQueryLog(); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + expect(DB::getQueryLog())->toBeEmpty(); // empty +}); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - DB::flushQueryLog(); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertEmpty(DB::getQueryLog()); // empty - } +test('cache is invalidated when the tenant is updated', function () { + $tenant = Tenant::create(); + $tenant->createDomain([ + 'domain' => 'acme', + ]); - /** @test */ - public function cache_is_invalidated_when_the_tenant_is_updated() - { - $tenant = Tenant::create(); - $tenant->createDomain([ - 'domain' => 'acme', - ]); + DB::enableQueryLog(); - DB::enableQueryLog(); + DomainTenantResolver::$shouldCache = true; - DomainTenantResolver::$shouldCache = true; + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + DB::flushQueryLog(); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + expect(DB::getQueryLog())->toBeEmpty(); // empty - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - DB::flushQueryLog(); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertEmpty(DB::getQueryLog()); // empty + $tenant->update([ + 'foo' => 'bar', + ]); - $tenant->update([ - 'foo' => 'bar', - ]); + DB::flushQueryLog(); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + $this->assertNotEmpty(DB::getQueryLog()); // not empty +}); - DB::flushQueryLog(); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertNotEmpty(DB::getQueryLog()); // not empty - } +test('cache is invalidated when a tenants domain is changed', function () { + $tenant = Tenant::create(); + $tenant->createDomain([ + 'domain' => 'acme', + ]); - /** @test */ - public function cache_is_invalidated_when_a_tenants_domain_is_changed() - { - $tenant = Tenant::create(); - $tenant->createDomain([ - 'domain' => 'acme', - ]); + DB::enableQueryLog(); - DB::enableQueryLog(); + DomainTenantResolver::$shouldCache = true; - DomainTenantResolver::$shouldCache = true; + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + DB::flushQueryLog(); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + expect(DB::getQueryLog())->toBeEmpty(); // empty - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - DB::flushQueryLog(); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertEmpty(DB::getQueryLog()); // empty + $tenant->createDomain([ + 'domain' => 'bar', + ]); - $tenant->createDomain([ - 'domain' => 'bar', - ]); + DB::flushQueryLog(); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); + $this->assertNotEmpty(DB::getQueryLog()); // not empty - DB::flushQueryLog(); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertNotEmpty(DB::getQueryLog()); // not empty - - DB::flushQueryLog(); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('bar'))); - $this->assertNotEmpty(DB::getQueryLog()); // not empty - } -} + DB::flushQueryLog(); + expect($tenant->is(app(DomainTenantResolver::class)->resolve('bar')))->toBeTrue(); + $this->assertNotEmpty(DB::getQueryLog()); // not empty +}); diff --git a/tests/CombinedDomainAndSubdomainIdentificationTest.php b/tests/CombinedDomainAndSubdomainIdentificationTest.php index 6712458c..db01ef99 100644 --- a/tests/CombinedDomainAndSubdomainIdentificationTest.php +++ b/tests/CombinedDomainAndSubdomainIdentificationTest.php @@ -2,76 +2,64 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Database\Concerns\HasDomains; -use Stancl\Tenancy\Database\Models; use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain; +use Stancl\Tenancy\Database\Models; -class CombinedDomainAndSubdomainIdentificationTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); - - Route::group([ - 'middleware' => InitializeTenancyByDomainOrSubdomain::class, - ], function () { - Route::get('/foo/{a}/{b}', function ($a, $b) { - return "$a + $b"; - }); +beforeEach(function () { + Route::group([ + 'middleware' => InitializeTenancyByDomainOrSubdomain::class, + ], function () { + Route::get('/foo/{a}/{b}', function ($a, $b) { + return "$a + $b"; }); + }); - config(['tenancy.tenant_model' => CombinedTenant::class]); - } + config(['tenancy.tenant_model' => CombinedTenant::class]); +}); - /** @test */ - public function tenant_can_be_identified_by_subdomain() - { - config(['tenancy.central_domains' => ['localhost']]); +test('tenant can be identified by subdomain', function () { + config(['tenancy.central_domains' => ['localhost']]); - $tenant = CombinedTenant::create([ - 'id' => 'acme', - ]); + $tenant = CombinedTenant::create([ + 'id' => 'acme', + ]); - $tenant->domains()->create([ - 'domain' => 'foo', - ]); + $tenant->domains()->create([ + 'domain' => 'foo', + ]); - $this->assertFalse(tenancy()->initialized); + expect(tenancy()->initialized)->toBeFalse(); - $this - ->get('http://foo.localhost/foo/abc/xyz') - ->assertSee('abc + xyz'); + $this + ->get('http://foo.localhost/foo/abc/xyz') + ->assertSee('abc + xyz'); - $this->assertTrue(tenancy()->initialized); - $this->assertSame('acme', tenant('id')); - } + expect(tenancy()->initialized)->toBeTrue(); + expect(tenant('id'))->toBe('acme'); +}); - /** @test */ - public function tenant_can_be_identified_by_domain() - { - config(['tenancy.central_domains' => []]); +test('tenant can be identified by domain', function () { + config(['tenancy.central_domains' => []]); - $tenant = CombinedTenant::create([ - 'id' => 'acme', - ]); + $tenant = CombinedTenant::create([ + 'id' => 'acme', + ]); - $tenant->domains()->create([ - 'domain' => 'foobar.localhost', - ]); + $tenant->domains()->create([ + 'domain' => 'foobar.localhost', + ]); - $this->assertFalse(tenancy()->initialized); + expect(tenancy()->initialized)->toBeFalse(); - $this - ->get('http://foobar.localhost/foo/abc/xyz') - ->assertSee('abc + xyz'); + $this + ->get('http://foobar.localhost/foo/abc/xyz') + ->assertSee('abc + xyz'); - $this->assertTrue(tenancy()->initialized); - $this->assertSame('acme', tenant('id')); - } -} + expect(tenancy()->initialized)->toBeTrue(); + expect(tenant('id'))->toBe('acme'); +}); class CombinedTenant extends Models\Tenant { diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 145a93c5..8ad67538 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; @@ -19,219 +17,196 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\ExampleSeeder; use Stancl\Tenancy\Tests\Etc\Tenant; -class CommandsTest extends TestCase +beforeEach(function () { + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + config(['tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class, + ]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); +}); + +afterEach(function () { + // Cleanup tenancy config cache + if (file_exists(base_path('config/tenancy.php'))) { + unlink(base_path('config/tenancy.php')); + } +}); + +test('migrate command doesnt change the db connection', function () { + expect(Schema::hasTable('users'))->toBeFalse(); + + $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); + Artisan::call('tenants:migrate'); + $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); + + expect(Schema::hasTable('users'))->toBeFalse(); + expect($new_connection_name)->toEqual($old_connection_name); + $this->assertNotEquals('tenant', $new_connection_name); +}); + +test('migrate command works without options', function () { + $tenant = Tenant::create(); + + expect(Schema::hasTable('users'))->toBeFalse(); + Artisan::call('tenants:migrate'); + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant); + + expect(Schema::hasTable('users'))->toBeTrue(); +}); + +test('migrate command works with tenants option', function () { + $tenant = Tenant::create(); + Artisan::call('tenants:migrate', [ + '--tenants' => [$tenant['id']], + ]); + + expect(Schema::hasTable('users'))->toBeFalse(); + tenancy()->initialize(Tenant::create()); + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant); + expect(Schema::hasTable('users'))->toBeTrue(); +}); + +test('migrate command loads schema state', function () { + $tenant = Tenant::create(); + + expect(Schema::hasTable('schema_users'))->toBeFalse(); + expect(Schema::hasTable('users'))->toBeFalse(); + + Artisan::call('tenants:migrate --schema-path="tests/Etc/tenant-schema.dump"'); + + expect(Schema::hasTable('schema_users'))->toBeFalse(); + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant); + + // Check for both tables to see if missing migrations also get executed + expect(Schema::hasTable('schema_users'))->toBeTrue(); + expect(Schema::hasTable('users'))->toBeTrue(); +}); + +test('dump command works', function () { + $tenant = Tenant::create(); + Artisan::call('tenants:migrate'); + + tenancy()->initialize($tenant); + + Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"'); + expect('tests/Etc/tenant-schema-test.dump')->toBeFile(); +}); + +test('rollback command works', function () { + $tenant = Tenant::create(); + Artisan::call('tenants:migrate'); + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant); + + expect(Schema::hasTable('users'))->toBeTrue(); + Artisan::call('tenants:rollback'); + expect(Schema::hasTable('users'))->toBeFalse(); +}); + +// Incomplete test +test('seed command works'); + +test('database connection is switched to default', function () { + databaseConnectionSwitchedToDefault(); +}); + +test('database connection is switched to default when tenancy has been initialized', function () { + tenancy()->initialize(Tenant::create()); + + databaseConnectionSwitchedToDefault(); +}); + +test('run command works', function () { + runCommandWorks(); +}); + +test('install command works', function () { + if (! is_dir($dir = app_path('Http'))) { + mkdir($dir, 0777, true); + } + if (! is_dir($dir = base_path('routes'))) { + mkdir($dir, 0777, true); + } + + $this->artisan('tenancy:install'); + expect(base_path('routes/tenant.php'))->toBeFile(); + expect(base_path('config/tenancy.php'))->toBeFile(); + expect(app_path('Providers/TenancyServiceProvider.php'))->toBeFile(); + expect(database_path('migrations/2019_09_15_000010_create_tenants_table.php'))->toBeFile(); + expect(database_path('migrations/2019_09_15_000020_create_domains_table.php'))->toBeFile(); + expect(database_path('migrations/tenant'))->toBeDirectory(); +}); + +test('migrate fresh command works', function () { + $tenant = Tenant::create(); + + expect(Schema::hasTable('users'))->toBeFalse(); + Artisan::call('tenants:migrate-fresh'); + expect(Schema::hasTable('users'))->toBeFalse(); + + tenancy()->initialize($tenant); + + expect(Schema::hasTable('users'))->toBeTrue(); + + expect(DB::table('users')->exists())->toBeFalse(); + DB::table('users')->insert(['name' => 'xxx', 'password' => bcrypt('password'), 'email' => 'foo@bar.xxx']); + expect(DB::table('users')->exists())->toBeTrue(); + + // test that db is wiped + Artisan::call('tenants:migrate-fresh'); + expect(DB::table('users')->exists())->toBeFalse(); +}); + +test('run command with array of tenants works', function () { + $tenantId1 = Tenant::create()->getTenantKey(); + $tenantId2 = Tenant::create()->getTenantKey(); + Artisan::call('tenants:migrate-fresh'); + + $this->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'") + ->expectsOutput('Tenant: ' . $tenantId1) + ->expectsOutput('Tenant: ' . $tenantId2); +}); + +// todo@tests +function runCommandWorks(): void { - public function setUp(): void - { - parent::setUp(); + $id = Tenant::create()->getTenantKey(); - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); + Artisan::call('tenants:migrate', ['--tenants' => [$id]]); - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); - } - - public function tearDown(): void - { - parent::tearDown(); - - // Cleanup tenancy config cache - if (file_exists(base_path('config/tenancy.php'))) { - unlink(base_path('config/tenancy.php')); - } - } - - /** @test */ - public function migrate_command_doesnt_change_the_db_connection() - { - $this->assertFalse(Schema::hasTable('users')); - - $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); - Artisan::call('tenants:migrate'); - $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); - - $this->assertFalse(Schema::hasTable('users')); - $this->assertEquals($old_connection_name, $new_connection_name); - $this->assertNotEquals('tenant', $new_connection_name); - } - - /** @test */ - public function migrate_command_works_without_options() - { - $tenant = Tenant::create(); - - $this->assertFalse(Schema::hasTable('users')); - Artisan::call('tenants:migrate'); - $this->assertFalse(Schema::hasTable('users')); - - tenancy()->initialize($tenant); - - $this->assertTrue(Schema::hasTable('users')); - } - - /** @test */ - public function migrate_command_works_with_tenants_option() - { - $tenant = Tenant::create(); - Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant['id']], - ]); - - $this->assertFalse(Schema::hasTable('users')); - tenancy()->initialize(Tenant::create()); - $this->assertFalse(Schema::hasTable('users')); - - tenancy()->initialize($tenant); - $this->assertTrue(Schema::hasTable('users')); - } - - /** @test */ - public function migrate_command_loads_schema_state() - { - $tenant = Tenant::create(); - - $this->assertFalse(Schema::hasTable('schema_users')); - $this->assertFalse(Schema::hasTable('users')); - - Artisan::call('tenants:migrate --schema-path="tests/Etc/tenant-schema.dump"'); - - $this->assertFalse(Schema::hasTable('schema_users')); - $this->assertFalse(Schema::hasTable('users')); - - tenancy()->initialize($tenant); - - // Check for both tables to see if missing migrations also get executed - $this->assertTrue(Schema::hasTable('schema_users')); - $this->assertTrue(Schema::hasTable('users')); - } - - /** @test */ - public function dump_command_works() - { - $tenant = Tenant::create(); - Artisan::call('tenants:migrate'); - - tenancy()->initialize($tenant); - - Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"'); - $this->assertFileExists('tests/Etc/tenant-schema-test.dump'); - } - - /** @test */ - public function rollback_command_works() - { - $tenant = Tenant::create(); - Artisan::call('tenants:migrate'); - $this->assertFalse(Schema::hasTable('users')); - - tenancy()->initialize($tenant); - - $this->assertTrue(Schema::hasTable('users')); - Artisan::call('tenants:rollback'); - $this->assertFalse(Schema::hasTable('users')); - } - - /** @test */ - public function seed_command_works() - { - $this->markTestIncomplete(); - } - - /** @test */ - public function database_connection_is_switched_to_default() - { - $originalDBName = DB::connection()->getDatabaseName(); - - Artisan::call('tenants:migrate'); - $this->assertSame($originalDBName, DB::connection()->getDatabaseName()); - - Artisan::call('tenants:seed', ['--class' => ExampleSeeder::class]); - $this->assertSame($originalDBName, DB::connection()->getDatabaseName()); - - Artisan::call('tenants:rollback'); - $this->assertSame($originalDBName, DB::connection()->getDatabaseName()); - - $this->run_commands_works(); - $this->assertSame($originalDBName, DB::connection()->getDatabaseName()); - } - - /** @test */ - public function database_connection_is_switched_to_default_when_tenancy_has_been_initialized() - { - tenancy()->initialize(Tenant::create()); - - $this->database_connection_is_switched_to_default(); - } - - /** @test */ - public function run_commands_works() - { - $id = Tenant::create()->getTenantKey(); - - Artisan::call('tenants:migrate', ['--tenants' => [$id]]); - - $this->artisan("tenants:run foo --tenants=$id --argument='a=foo' --option='b=bar' --option='c=xyz'") - ->expectsOutput("User's name is Test command") - ->expectsOutput('foo') - ->expectsOutput('xyz'); - } - - /** @test */ - public function install_command_works() - { - if (! is_dir($dir = app_path('Http'))) { - mkdir($dir, 0777, true); - } - if (! is_dir($dir = base_path('routes'))) { - mkdir($dir, 0777, true); - } - - $this->artisan('tenancy:install'); - $this->assertFileExists(base_path('routes/tenant.php')); - $this->assertFileExists(base_path('config/tenancy.php')); - $this->assertFileExists(app_path('Providers/TenancyServiceProvider.php')); - $this->assertFileExists(database_path('migrations/2019_09_15_000010_create_tenants_table.php')); - $this->assertFileExists(database_path('migrations/2019_09_15_000020_create_domains_table.php')); - $this->assertDirectoryExists(database_path('migrations/tenant')); - } - - /** @test */ - public function migrate_fresh_command_works() - { - $tenant = Tenant::create(); - - $this->assertFalse(Schema::hasTable('users')); - Artisan::call('tenants:migrate-fresh'); - $this->assertFalse(Schema::hasTable('users')); - - tenancy()->initialize($tenant); - - $this->assertTrue(Schema::hasTable('users')); - - $this->assertFalse(DB::table('users')->exists()); - DB::table('users')->insert(['name' => 'xxx', 'password' => bcrypt('password'), 'email' => 'foo@bar.xxx']); - $this->assertTrue(DB::table('users')->exists()); - - // test that db is wiped - Artisan::call('tenants:migrate-fresh'); - $this->assertFalse(DB::table('users')->exists()); - } - - /** @test */ - public function run_command_with_array_of_tenants_works() - { - $tenantId1 = Tenant::create()->getTenantKey(); - $tenantId2 = Tenant::create()->getTenantKey(); - Artisan::call('tenants:migrate-fresh'); - - $this->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'") - ->expectsOutput('Tenant: ' . $tenantId1) - ->expectsOutput('Tenant: ' . $tenantId2); - } + test()->artisan("tenants:run foo --tenants=$id --argument='a=foo' --option='b=bar' --option='c=xyz'") + ->expectsOutput("User's name is Test command") + ->expectsOutput('foo') + ->expectsOutput('xyz'); +} + +// todo@tests +function databaseConnectionSwitchedToDefault() +{ + $originalDBName = DB::connection()->getDatabaseName(); + + Artisan::call('tenants:migrate'); + expect(DB::connection()->getDatabaseName())->toBe($originalDBName); + + Artisan::call('tenants:seed', ['--class' => ExampleSeeder::class]); + expect(DB::connection()->getDatabaseName())->toBe($originalDBName); + + Artisan::call('tenants:rollback'); + expect(DB::connection()->getDatabaseName())->toBe($originalDBName); + + runCommandWorks(); + + expect(DB::connection()->getDatabaseName())->toBe($originalDBName); } diff --git a/tests/DatabasePreparationTest.php b/tests/DatabasePreparationTest.php index 12d30059..64fa1e0f 100644 --- a/tests/DatabasePreparationTest.php +++ b/tests/DatabasePreparationTest.php @@ -2,11 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - -use Illuminate\Database\Seeder; -use Illuminate\Foundation\Auth\User as Authenticable; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; use Stancl\JobPipeline\JobPipeline; @@ -16,111 +11,85 @@ use Stancl\Tenancy\Jobs\MigrateDatabase; use Stancl\Tenancy\Jobs\SeedDatabase; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Foundation\Auth\User as Authenticable; +use Stancl\Tenancy\Tests\Etc\TestSeeder; -class DatabasePreparationTest extends TestCase -{ - /** @test */ - public function database_can_be_created_after_tenant_creation() - { - config(['tenancy.database.template_tenant_connection' => 'mysql']); +test('database can be created after tenant creation', function () { + config(['tenancy.database.template_tenant_connection' => 'mysql']); - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - $tenant = Tenant::create(); + $tenant = Tenant::create(); - $manager = app(MySQLDatabaseManager::class); - $manager->setConnection('mysql'); + $manager = app(MySQLDatabaseManager::class); + $manager->setConnection('mysql'); - $this->assertTrue($manager->databaseExists($tenant->database()->getName())); - } + expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue(); +}); - /** @test */ - public function database_can_be_migrated_after_tenant_creation() - { - Event::listen(TenantCreated::class, JobPipeline::make([ - CreateDatabase::class, - MigrateDatabase::class, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); +test('database can be migrated after tenant creation', function () { + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - $tenant = Tenant::create(); + $tenant = Tenant::create(); - $tenant->run(function () { - $this->assertTrue(Schema::hasTable('users')); - }); - } + $tenant->run(function () { + expect(Schema::hasTable('users'))->toBeTrue(); + }); +}); - /** @test */ - public function database_can_be_seeded_after_tenant_creation() - { - config(['tenancy.seeder_parameters' => [ - '--class' => TestSeeder::class, - ]]); +test('database can be seeded after tenant creation', function () { + config(['tenancy.seeder_parameters' => [ + '--class' => TestSeeder::class, + ]]); - Event::listen(TenantCreated::class, JobPipeline::make([ - CreateDatabase::class, - MigrateDatabase::class, - SeedDatabase::class, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + SeedDatabase::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - $tenant = Tenant::create(); + $tenant = Tenant::create(); - $tenant->run(function () { - $this->assertSame('Seeded User', User::first()->name); - }); - } + $tenant->run(function () { + expect(User::first()->name)->toBe('Seeded User'); + }); +}); - /** @test */ - public function custom_job_can_be_added_to_the_pipeline() - { - config(['tenancy.seeder_parameters' => [ - '--class' => TestSeeder::class, - ]]); +test('custom job can be added to the pipeline', function () { + config(['tenancy.seeder_parameters' => [ + '--class' => TestSeeder::class, + ]]); - Event::listen(TenantCreated::class, JobPipeline::make([ - CreateDatabase::class, - MigrateDatabase::class, - SeedDatabase::class, - CreateSuperuser::class, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + SeedDatabase::class, + CreateSuperuser::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - $tenant = Tenant::create(); + $tenant = Tenant::create(); - $tenant->run(function () { - $this->assertSame('Foo', User::all()[1]->name); - }); - } -} + $tenant->run(function () { + expect(User::all()[1]->name)->toBe('Foo'); + }); +}); class User extends Authenticable { protected $guarded = []; } -class TestSeeder extends Seeder -{ - /** - * Run the database seeds. - * - * @return void - */ - public function run() - { - DB::table('users')->insert([ - 'name' => 'Seeded User', - 'email' => 'seeded@user', - 'password' => bcrypt('password'), - ]); - } -} - class CreateSuperuser { protected $tenant; diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php index 344239d1..93ac7ec3 100644 --- a/tests/DatabaseUsersTest.php +++ b/tests/DatabaseUsersTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; @@ -20,111 +18,97 @@ use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; use Stancl\Tenancy\Tests\Etc\Tenant; -class DatabaseUsersTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + config([ + 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, + 'tenancy.database.suffix' => '', + 'tenancy.database.template_tenant_connection' => 'mysql', + ]); - config([ - 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, - 'tenancy.database.suffix' => '', - 'tenancy.database.template_tenant_connection' => 'mysql', - ]); + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); +}); - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - } +test('users are created when permission controlled mysql manager is used', function () { + $tenant = new Tenant([ + 'id' => 'foo' . Str::random(10), + ]); + $tenant->database()->makeCredentials(); - /** @test */ - public function users_are_created_when_permission_controlled_mysql_manager_is_used() - { - $tenant = new Tenant([ - 'id' => 'foo' . Str::random(10), - ]); - $tenant->database()->makeCredentials(); + /** @var ManagesDatabaseUsers $manager */ + $manager = $tenant->database()->manager(); + expect($manager->userExists($tenant->database()->getUsername()))->toBeFalse(); - /** @var ManagesDatabaseUsers $manager */ - $manager = $tenant->database()->manager(); - $this->assertFalse($manager->userExists($tenant->database()->getUsername())); + $tenant->save(); - $tenant->save(); + expect($manager->userExists($tenant->database()->getUsername()))->toBeTrue(); +}); - $this->assertTrue($manager->userExists($tenant->database()->getUsername())); - } +test('a tenants database cannot be created when the user already exists', function () { + $username = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_username' => $username, + ]); - /** @test */ - public function a_tenants_database_cannot_be_created_when_the_user_already_exists() - { - $username = 'foo' . Str::random(8); - $tenant = Tenant::create([ - 'tenancy_db_username' => $username, - ]); + /** @var ManagesDatabaseUsers $manager */ + $manager = $tenant->database()->manager(); + expect($manager->userExists($tenant->database()->getUsername()))->toBeTrue(); + expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue(); - /** @var ManagesDatabaseUsers $manager */ - $manager = $tenant->database()->manager(); - $this->assertTrue($manager->userExists($tenant->database()->getUsername())); - $this->assertTrue($manager->databaseExists($tenant->database()->getName())); + $this->expectException(TenantDatabaseUserAlreadyExistsException::class); + Event::fake([DatabaseCreated::class]); - $this->expectException(TenantDatabaseUserAlreadyExistsException::class); - Event::fake([DatabaseCreated::class]); + $tenant2 = Tenant::create([ + 'tenancy_db_username' => $username, + ]); - $tenant2 = Tenant::create([ - 'tenancy_db_username' => $username, - ]); + /** @var ManagesDatabaseUsers $manager */ + $manager2 = $tenant2->database()->manager(); - /** @var ManagesDatabaseUsers $manager */ - $manager2 = $tenant2->database()->manager(); + // database was not created because of DB transaction + expect($manager2->databaseExists($tenant2->database()->getName()))->toBeFalse(); + Event::assertNotDispatched(DatabaseCreated::class); +}); - // database was not created because of DB transaction - $this->assertFalse($manager2->databaseExists($tenant2->database()->getName())); - Event::assertNotDispatched(DatabaseCreated::class); - } +test('correct grants are given to users', function () { + PermissionControlledMySQLDatabaseManager::$grants = [ + 'ALTER', 'ALTER ROUTINE', 'CREATE', + ]; - /** @test */ - public function correct_grants_are_given_to_users() - { - PermissionControlledMySQLDatabaseManager::$grants = [ - 'ALTER', 'ALTER ROUTINE', 'CREATE', - ]; + $tenant = Tenant::create([ + 'tenancy_db_username' => $user = 'user' . Str::random(8), + ]); - $tenant = Tenant::create([ - 'tenancy_db_username' => $user = 'user' . Str::random(8), - ]); + $query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`%`")[1]; + expect($query->{"Grants for {$user}@%"})->toStartWith('GRANT CREATE, ALTER, ALTER ROUTINE ON'); // @mysql because that's the hostname within the docker network +}); - $query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`%`")[1]; - $this->assertStringStartsWith('GRANT CREATE, ALTER, ALTER ROUTINE ON', $query->{"Grants for {$user}@%"}); // @mysql because that's the hostname within the docker network - } +test('having existing databases without users and switching to permission controlled mysql manager doesnt break existing dbs', function () { + config([ + 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class, + 'tenancy.database.suffix' => '', + 'tenancy.database.template_tenant_connection' => 'mysql', + 'tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class, + ], + ]); - /** @test */ - public function having_existing_databases_without_users_and_switching_to_permission_controlled_mysql_manager_doesnt_break_existing_dbs() - { - config([ - 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class, - 'tenancy.database.suffix' => '', - 'tenancy.database.template_tenant_connection' => 'mysql', - 'tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ], - ]); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + $tenant = Tenant::create([ + 'id' => 'foo' . Str::random(10), + ]); - $tenant = Tenant::create([ - 'id' => 'foo' . Str::random(10), - ]); + expect($tenant->database()->manager() instanceof MySQLDatabaseManager)->toBeTrue(); - $this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager); + tenancy()->initialize($tenant); // check if everything works + tenancy()->end(); - tenancy()->initialize($tenant); // check if everything works - tenancy()->end(); + config(['tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class]); - config(['tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class]); + tenancy()->initialize($tenant); // check if everything works - tenancy()->initialize($tenant); // check if everything works - - $this->assertTrue($tenant->database()->manager() instanceof PermissionControlledMySQLDatabaseManager); - $this->assertSame('root', config('database.connections.tenant.username')); - } -} + expect($tenant->database()->manager() instanceof PermissionControlledMySQLDatabaseManager)->toBeTrue(); + expect(config('database.connections.tenant.username'))->toBe('root'); +}); diff --git a/tests/DeleteDomainsJobTest.php b/tests/DeleteDomainsJobTest.php index 7fce9cf3..bdee14dd 100644 --- a/tests/DeleteDomainsJobTest.php +++ b/tests/DeleteDomainsJobTest.php @@ -2,41 +2,31 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Stancl\Tenancy\Database\Concerns\HasDomains; use Stancl\Tenancy\Jobs\DeleteDomains; -class DeleteDomainsJobTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + config(['tenancy.tenant_model' => DatabaseAndDomainTenant::class]); +}); - config(['tenancy.tenant_model' => DatabaseAndDomainTenant::class]); - } +test('job delete domains successfully', function (){ + $tenant = DatabaseAndDomainTenant::create(); - /** @test */ - public function job_delete_domains_successfully() - { - $tenant = DatabaseAndDomainTenant::create(); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + $tenant->domains()->create([ + 'domain' => 'bar.localhost', + ]); - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); - $tenant->domains()->create([ - 'domain' => 'bar.localhost', - ]); + expect($tenant->domains()->count())->toBe(2); - $this->assertSame($tenant->domains()->count(), 2); + (new DeleteDomains($tenant))->handle(); - (new DeleteDomains($tenant))->handle(); + expect($tenant->refresh()->domains()->count())->toBe(0); +}); - $this->assertSame($tenant->refresh()->domains()->count(), 0); - } -} - -class DatabaseAndDomainTenant extends Etc\Tenant +class DatabaseAndDomainTenant extends \Stancl\Tenancy\Tests\Etc\Tenant { use HasDomains; -} +} \ No newline at end of file diff --git a/tests/DomainTest.php b/tests/DomainTest.php index 9c1bac28..907681ff 100644 --- a/tests/DomainTest.php +++ b/tests/DomainTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Database\Concerns\HasDomains; use Stancl\Tenancy\Database\Models; @@ -13,110 +11,92 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Resolvers\DomainTenantResolver; -class DomainTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); - - Route::group([ - 'middleware' => InitializeTenancyByDomain::class, - ], function () { - Route::get('/foo/{a}/{b}', function ($a, $b) { - return "$a + $b"; - }); +beforeEach(function () { + Route::group([ + 'middleware' => InitializeTenancyByDomain::class, + ], function () { + Route::get('/foo/{a}/{b}', function ($a, $b) { + return "$a + $b"; }); + }); - config(['tenancy.tenant_model' => DomainTenant::class]); - } + config(['tenancy.tenant_model' => DomainTenant::class]); +}); - /** @test */ - public function tenant_can_be_identified_using_hostname() - { - $tenant = DomainTenant::create(); +test('tenant can be identified using hostname', function () { + $tenant = DomainTenant::create(); - $id = $tenant->id; + $id = $tenant->id; - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); - $resolvedTenant = app(DomainTenantResolver::class)->resolve('foo.localhost'); + $resolvedTenant = app(DomainTenantResolver::class)->resolve('foo.localhost'); - $this->assertSame($id, $resolvedTenant->id); - $this->assertSame(['foo.localhost'], $resolvedTenant->domains->pluck('domain')->toArray()); - } + expect($resolvedTenant->id)->toBe($id); + expect($resolvedTenant->domains->pluck('domain')->toArray())->toBe(['foo.localhost']); +}); - /** @test */ - public function a_domain_can_belong_to_only_one_tenant() - { - $tenant = DomainTenant::create(); +test('a domain can belong to only one tenant', function () { + $tenant = DomainTenant::create(); - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); - $tenant2 = DomainTenant::create(); + $tenant2 = DomainTenant::create(); - $this->expectException(DomainOccupiedByOtherTenantException::class); - $tenant2->domains()->create([ - 'domain' => 'foo.localhost', - ]); - } + $this->expectException(DomainOccupiedByOtherTenantException::class); + $tenant2->domains()->create([ + 'domain' => 'foo.localhost', + ]); +}); - /** @test */ - public function an_exception_is_thrown_if_tenant_cannot_be_identified() - { - $this->expectException(TenantCouldNotBeIdentifiedOnDomainException::class); +test('an exception is thrown if tenant cannot be identified', function () { + $this->expectException(TenantCouldNotBeIdentifiedOnDomainException::class); - app(DomainTenantResolver::class)->resolve('foo.localhost'); - } + app(DomainTenantResolver::class)->resolve('foo.localhost'); +}); - /** @test */ - public function tenant_can_be_identified_by_domain() - { - $tenant = DomainTenant::create([ - 'id' => 'acme', - ]); +test('tenant can be identified by domain', function () { + $tenant = DomainTenant::create([ + 'id' => 'acme', + ]); - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); - $this->assertFalse(tenancy()->initialized); + expect(tenancy()->initialized)->toBeFalse(); - $this - ->get('http://foo.localhost/foo/abc/xyz') - ->assertSee('abc + xyz'); + $this + ->get('http://foo.localhost/foo/abc/xyz') + ->assertSee('abc + xyz'); - $this->assertTrue(tenancy()->initialized); - $this->assertSame('acme', tenant('id')); - } + expect(tenancy()->initialized)->toBeTrue(); + expect(tenant('id'))->toBe('acme'); +}); - /** @test */ - public function onfail_logic_can_be_customized() - { - InitializeTenancyByDomain::$onFail = function () { - return 'foo'; - }; +test('onfail logic can be customized', function () { + InitializeTenancyByDomain::$onFail = function () { + return 'foo'; + }; - $this - ->get('http://foo.localhost/foo/abc/xyz') - ->assertSee('foo'); - } + $this + ->get('http://foo.localhost/foo/abc/xyz') + ->assertSee('foo'); +}); - /** @test */ - public function domains_are_always_lowercase() - { - $tenant = DomainTenant::create(); +test('domains are always lowercase', function () { + $tenant = DomainTenant::create(); - $tenant->domains()->create([ - 'domain' => 'CAPITALS', - ]); + $tenant->domains()->create([ + 'domain' => 'CAPITALS', + ]); - $this->assertSame('capitals', Domain::first()->domain); - } -} + expect(Domain::first()->domain)->toBe('capitals'); +}); class DomainTenant extends Models\Tenant { diff --git a/tests/Etc/TestSeeder.php b/tests/Etc/TestSeeder.php new file mode 100644 index 00000000..3412948e --- /dev/null +++ b/tests/Etc/TestSeeder.php @@ -0,0 +1,23 @@ +insert([ + 'name' => 'Seeded User', + 'email' => 'seeded@user', + 'password' => bcrypt('password'), + ]); + } +} diff --git a/tests/EventListenerTest.php b/tests/EventListenerTest.php index 02ed8b3b..66dcedc8 100644 --- a/tests/EventListenerTest.php +++ b/tests/EventListenerTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Events\CallQueuedListener; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; @@ -22,183 +20,164 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\QueueableListener; use Stancl\Tenancy\Tests\Etc\Tenant; -class EventListenerTest extends TestCase -{ - /** @test */ - public function listeners_can_be_synchronous() - { - Queue::fake(); - Event::listen(TenantCreated::class, FooListener::class); +test('listeners can be synchronous', function () { + Queue::fake(); + Event::listen(TenantCreated::class, FooListener::class); - Tenant::create(); + Tenant::create(); - Queue::assertNothingPushed(); + Queue::assertNothingPushed(); - $this->assertSame('bar', app('foo')); - } + expect(app('foo'))->toBe('bar'); +}); - /** @test */ - public function listeners_can_be_queued_by_setting_a_static_property() - { - Queue::fake(); +test('listeners can be queued by setting a static property', function () { + Queue::fake(); - Event::listen(TenantCreated::class, FooListener::class); - FooListener::$shouldQueue = true; + Event::listen(TenantCreated::class, FooListener::class); + FooListener::$shouldQueue = true; - Tenant::create(); + Tenant::create(); - Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) { - return $job->class === FooListener::class; - }); + Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) { + return $job->class === FooListener::class; + }); - $this->assertFalse(app()->bound('foo')); - } + expect(app()->bound('foo'))->toBeFalse(); +}); - /** @test */ - public function ing_events_can_be_used_to_cancel_tenant_model_actions() - { - Event::listen(CreatingTenant::class, function () { - return false; - }); +test('ing events can be used to cancel tenant model actions', function () { + Event::listen(CreatingTenant::class, function () { + return false; + }); - $this->assertSame(false, Tenant::create()->exists); - $this->assertSame(0, Tenant::count()); - } + expect(Tenant::create()->exists)->toBe(false); + expect(Tenant::count())->toBe(0); +}); - /** @test */ - public function ing_events_can_be_used_to_cancel_domain_model_actions() - { - $tenant = Tenant::create(); +test('ing events can be used to cancel domain model actions', function () { + $tenant = Tenant::create(); - Event::listen(UpdatingDomain::class, function () { - return false; - }); + Event::listen(UpdatingDomain::class, function () { + return false; + }); - $domain = $tenant->domains()->create([ - 'domain' => 'acme', - ]); + $domain = $tenant->domains()->create([ + 'domain' => 'acme', + ]); - $domain->update([ - 'domain' => 'foo', - ]); + $domain->update([ + 'domain' => 'foo', + ]); - $this->assertSame('acme', $domain->refresh()->domain); - } + expect($domain->refresh()->domain)->toBe('acme'); +}); - /** @test */ - public function ing_events_can_be_used_to_cancel_db_creation() - { - Event::listen(CreatingDatabase::class, function (CreatingDatabase $event) { - $event->tenant->setInternal('create_database', false); - }); +test('ing events can be used to cancel db creation', function () { + Event::listen(CreatingDatabase::class, function (CreatingDatabase $event) { + $event->tenant->setInternal('create_database', false); + }); - $tenant = Tenant::create(); - dispatch_now(new CreateDatabase($tenant)); + $tenant = Tenant::create(); + dispatch_now(new CreateDatabase($tenant)); - $this->assertFalse($tenant->database()->manager()->databaseExists( - $tenant->database()->getName() - )); - } + $this->assertFalse($tenant->database()->manager()->databaseExists( + $tenant->database()->getName() + )); +}); - /** @test */ - public function ing_events_can_be_used_to_cancel_tenancy_bootstrapping() - { - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - RedisTenancyBootstrapper::class, - ]]); +test('ing events can be used to cancel tenancy bootstrapping', function () { + config(['tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class, + RedisTenancyBootstrapper::class, + ]]); - Event::listen( - TenantCreated::class, - JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); + Event::listen( + TenantCreated::class, + JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(BootstrappingTenancy::class, function (BootstrappingTenancy $event) { - $event->tenancy->getBootstrappersUsing = function () { - return [DatabaseTenancyBootstrapper::class]; - }; - }); + Event::listen(BootstrappingTenancy::class, function (BootstrappingTenancy $event) { + $event->tenancy->getBootstrappersUsing = function () { + return [DatabaseTenancyBootstrapper::class]; + }; + }); - tenancy()->initialize(Tenant::create()); + tenancy()->initialize(Tenant::create()); - $this->assertSame([DatabaseTenancyBootstrapper::class], array_map('get_class', tenancy()->getBootstrappers())); - } + expect(array_map('get_class', tenancy()->getBootstrappers()))->toBe([DatabaseTenancyBootstrapper::class]); +}); - /** @test */ - public function individual_job_pipelines_can_terminate_while_leaving_others_running() - { - $executed = []; +test('individual job pipelines can terminate while leaving others running', function () { + $executed = []; - Event::listen( - TenantCreated::class, - JobPipeline::make([ - function () use (&$executed) { - $executed[] = 'P1J1'; - }, + Event::listen( + TenantCreated::class, + JobPipeline::make([ + function () use (&$executed) { + $executed[] = 'P1J1'; + }, - function () use (&$executed) { - $executed[] = 'P1J2'; - }, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); + function () use (&$executed) { + $executed[] = 'P1J2'; + }, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); - Event::listen( - TenantCreated::class, - JobPipeline::make([ - function () use (&$executed) { - $executed[] = 'P2J1'; + Event::listen( + TenantCreated::class, + JobPipeline::make([ + function () use (&$executed) { + $executed[] = 'P2J1'; - return false; - }, + return false; + }, - function () use (&$executed) { - $executed[] = 'P2J2'; - }, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); + function () use (&$executed) { + $executed[] = 'P2J2'; + }, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); - Tenant::create(); + Tenant::create(); - $this->assertSame([ - 'P1J1', - 'P1J2', - 'P2J1', // termminated after this - // P2J2 was not reached - ], $executed); - } + $this->assertSame([ + 'P1J1', + 'P1J2', + 'P2J1', // termminated after this + // P2J2 was not reached + ], $executed); +}); - /** @test */ - public function database_is_not_migrated_if_creation_is_disabled() - { - Event::listen( - TenantCreated::class, - JobPipeline::make([ - CreateDatabase::class, - function () { - $this->fail("The job pipeline didn't exit."); - }, - MigrateDatabase::class, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); +test('database is not migrated if creation is disabled', function () { + Event::listen( + TenantCreated::class, + JobPipeline::make([ + CreateDatabase::class, + function () { + $this->fail("The job pipeline didn't exit."); + }, + MigrateDatabase::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); - Tenant::create([ - 'tenancy_create_database' => false, - 'tenancy_db_name' => 'already_created', - ]); + Tenant::create([ + 'tenancy_create_database' => false, + 'tenancy_db_name' => 'already_created', + ]); - $this->assertFalse($this->hasFailed()); - } -} + expect($this->hasFailed())->toBeFalse(); +}); class FooListener extends QueueableListener { diff --git a/tests/Features/RedirectTest.php b/tests/Features/RedirectTest.php index 4f7f77a1..7686867e 100644 --- a/tests/Features/RedirectTest.php +++ b/tests/Features/RedirectTest.php @@ -2,45 +2,35 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests\Features; - use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Features\CrossDomainRedirect; use Stancl\Tenancy\Tests\Etc\Tenant; -use Stancl\Tenancy\Tests\TestCase; -class RedirectTest extends TestCase -{ - /** @test */ - public function tenant_redirect_macro_replaces_only_the_hostname() - { - config([ - 'tenancy.features' => [CrossDomainRedirect::class], - ]); +test('tenant redirect macro replaces only the hostname', function () { + config([ + 'tenancy.features' => [CrossDomainRedirect::class], + ]); - Route::get('/foobar', function () { - return 'Foo'; - })->name('home'); + Route::get('/foobar', function () { + return 'Foo'; + })->name('home'); - Route::get('/redirect', function () { - return redirect()->route('home')->domain('abcd'); - }); + Route::get('/redirect', function () { + return redirect()->route('home')->domain('abcd'); + }); - $tenant = Tenant::create(); - tenancy()->initialize($tenant); + $tenant = Tenant::create(); + tenancy()->initialize($tenant); - $this->get('/redirect') - ->assertRedirect('http://abcd/foobar'); - } + $this->get('/redirect') + ->assertRedirect('http://abcd/foobar'); +}); - /** @test */ - public function tenant_route_helper_generates_correct_url() - { - Route::get('/abcdef/{a?}/{b?}', function () { - return 'Foo'; - })->name('foo'); +test('tenant route helper generates correct url', function () { + Route::get('/abcdef/{a?}/{b?}', function () { + return 'Foo'; + })->name('foo'); - $this->assertSame('http://foo.localhost/abcdef/as/df', tenant_route('foo.localhost', 'foo', ['a' => 'as', 'b' => 'df'])); - $this->assertSame('http://foo.localhost/abcdef', tenant_route('foo.localhost', 'foo', [])); - } -} + expect(tenant_route('foo.localhost', 'foo', ['a' => 'as', 'b' => 'df']))->toBe('http://foo.localhost/abcdef/as/df'); + expect(tenant_route('foo.localhost', 'foo', []))->toBe('http://foo.localhost/abcdef'); +}); diff --git a/tests/Features/TenantConfigTest.php b/tests/Features/TenantConfigTest.php index 37e26198..21c92592 100644 --- a/tests/Features/TenantConfigTest.php +++ b/tests/Features/TenantConfigTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests\Features; - use Illuminate\Support\Facades\Event; use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyInitialized; @@ -11,84 +9,73 @@ use Stancl\Tenancy\Features\TenantConfig; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\Tenant; -use Stancl\Tenancy\Tests\TestCase; -class TenantConfigTest extends TestCase -{ - public function tearDown(): void - { - TenantConfig::$storageToConfigMap = []; +afterEach(function () { + TenantConfig::$storageToConfigMap = []; +}); - parent::tearDown(); - } +test('config is merged and removed', function () { + expect(config('services.paypal'))->toBe(null); + config([ + 'tenancy.features' => [TenantConfig::class], + 'tenancy.bootstrappers' => [], + ]); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); - /** @test */ - public function config_is_merged_and_removed() - { - $this->assertSame(null, config('services.paypal')); - config([ - 'tenancy.features' => [TenantConfig::class], - 'tenancy.bootstrappers' => [], - ]); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); + TenantConfig::$storageToConfigMap = [ + 'paypal_api_public' => 'services.paypal.public', + 'paypal_api_private' => 'services.paypal.private', + ]; - TenantConfig::$storageToConfigMap = [ - 'paypal_api_public' => 'services.paypal.public', - 'paypal_api_private' => 'services.paypal.private', - ]; + $tenant = Tenant::create([ + 'paypal_api_public' => 'foo', + 'paypal_api_private' => 'bar', + ]); - $tenant = Tenant::create([ - 'paypal_api_public' => 'foo', - 'paypal_api_private' => 'bar', - ]); + tenancy()->initialize($tenant); + expect(config('services.paypal'))->toBe(['public' => 'foo', 'private' => 'bar']); - tenancy()->initialize($tenant); - $this->assertSame(['public' => 'foo', 'private' => 'bar'], config('services.paypal')); + tenancy()->end(); + $this->assertSame([ + 'public' => null, + 'private' => null, + ], config('services.paypal')); +}); - tenancy()->end(); - $this->assertSame([ - 'public' => null, - 'private' => null, - ], config('services.paypal')); - } +test('the value can be set to multiple config keys', function () { + expect(config('services.paypal'))->toBe(null); + config([ + 'tenancy.features' => [TenantConfig::class], + 'tenancy.bootstrappers' => [], + ]); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); - /** @test */ - public function the_value_can_be_set_to_multiple_config_keys() - { - $this->assertSame(null, config('services.paypal')); - config([ - 'tenancy.features' => [TenantConfig::class], - 'tenancy.bootstrappers' => [], - ]); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); + TenantConfig::$storageToConfigMap = [ + 'paypal_api_public' => [ + 'services.paypal.public1', + 'services.paypal.public2', + ], + 'paypal_api_private' => 'services.paypal.private', + ]; - TenantConfig::$storageToConfigMap = [ - 'paypal_api_public' => [ - 'services.paypal.public1', - 'services.paypal.public2', - ], - 'paypal_api_private' => 'services.paypal.private', - ]; + $tenant = Tenant::create([ + 'paypal_api_public' => 'foo', + 'paypal_api_private' => 'bar', + ]); - $tenant = Tenant::create([ - 'paypal_api_public' => 'foo', - 'paypal_api_private' => 'bar', - ]); + tenancy()->initialize($tenant); + $this->assertSame([ + 'public1' => 'foo', + 'public2' => 'foo', + 'private' => 'bar', + ], config('services.paypal')); - tenancy()->initialize($tenant); - $this->assertSame([ - 'public1' => 'foo', - 'public2' => 'foo', - 'private' => 'bar', - ], config('services.paypal')); - - tenancy()->end(); - $this->assertSame([ - 'public1' => null, - 'public2' => null, - 'private' => null, - ], config('services.paypal')); - } -} + tenancy()->end(); + $this->assertSame([ + 'public1' => null, + 'public2' => null, + 'private' => null, + ], config('services.paypal')); +}); diff --git a/tests/GlobalCacheTest.php b/tests/GlobalCacheTest.php index a39a1f55..8a13395c 100644 --- a/tests/GlobalCacheTest.php +++ b/tests/GlobalCacheTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Event; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Events\TenancyEnded; @@ -13,49 +11,42 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\Tenant; -class GlobalCacheTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + config(['tenancy.bootstrappers' => [ + CacheTenancyBootstrapper::class, + ]]); - config(['tenancy.bootstrappers' => [ - CacheTenancyBootstrapper::class, - ]]); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); +}); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); - } +test('global cache manager stores data in global cache', function () { + expect(cache('foo'))->toBe(null); + GlobalCache::put(['foo' => 'bar'], 1); + expect(GlobalCache::get('foo'))->toBe('bar'); - /** @test */ - public function global_cache_manager_stores_data_in_global_cache() - { - $this->assertSame(null, cache('foo')); - GlobalCache::put(['foo' => 'bar'], 1); - $this->assertSame('bar', GlobalCache::get('foo')); + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); + expect(GlobalCache::get('foo'))->toBe('bar'); - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); - $this->assertSame('bar', GlobalCache::get('foo')); + GlobalCache::put(['abc' => 'xyz'], 1); + cache(['def' => 'ghi'], 10); + expect(cache('def'))->toBe('ghi'); - GlobalCache::put(['abc' => 'xyz'], 1); - cache(['def' => 'ghi'], 10); - $this->assertSame('ghi', cache('def')); + tenancy()->end(); + expect(GlobalCache::get('abc'))->toBe('xyz'); + expect(GlobalCache::get('foo'))->toBe('bar'); + expect(cache('def'))->toBe(null); - tenancy()->end(); - $this->assertSame('xyz', GlobalCache::get('abc')); - $this->assertSame('bar', GlobalCache::get('foo')); - $this->assertSame(null, cache('def')); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant2); + expect(GlobalCache::get('abc'))->toBe('xyz'); + expect(GlobalCache::get('foo'))->toBe('bar'); + expect(cache('def'))->toBe(null); + cache(['def' => 'xxx'], 1); + expect(cache('def'))->toBe('xxx'); - $tenant2 = Tenant::create(); - tenancy()->initialize($tenant2); - $this->assertSame('xyz', GlobalCache::get('abc')); - $this->assertSame('bar', GlobalCache::get('foo')); - $this->assertSame(null, cache('def')); - cache(['def' => 'xxx'], 1); - $this->assertSame('xxx', cache('def')); + tenancy()->initialize($tenant1); + expect(cache('def'))->toBe('ghi'); +}); - tenancy()->initialize($tenant1); - $this->assertSame('ghi', cache('def')); - } -} diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index 90232932..dace6c51 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -2,41 +2,34 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - -use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Tests\Etc\Tenant; -use Symfony\Component\HttpKernel\Exception\HttpException; -class MaintenanceModeTest extends TestCase -{ - /** @test */ - public function tenant_can_be_in_maintenance_mode() - { - Route::get('/foo', function () { - return 'bar'; - })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]); +test('tenant can be in maintenance mode', function () { + Route::get('/foo', function () { + return 'bar'; + })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]); - $tenant = MaintenanceTenant::create(); - $tenant->domains()->create([ - 'domain' => 'acme.localhost', - ]); + $tenant = MaintenanceTenant::create(); + $tenant->domains()->create([ + 'domain' => 'acme.localhost', + ]); - $this->get('http://acme.localhost/foo') - ->assertSuccessful(); + $this->get('http://acme.localhost/foo') + ->assertSuccessful(); - tenancy()->end(); // flush stored tenant instance + tenancy()->end(); // flush stored tenant instance - $tenant->putDownForMaintenance(); + $tenant->putDownForMaintenance(); - $this->expectException(HttpException::class); - $this->withoutExceptionHandling() - ->get('http://acme.localhost/foo'); - } -} + $this->expectException(HttpException::class); + $this->withoutExceptionHandling() + ->get('http://acme.localhost/foo'); +}); class MaintenanceTenant extends Tenant { diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index 7a408ed0..4cd793d7 100644 --- a/tests/PathIdentificationTest.php +++ b/tests/PathIdentificationTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException; @@ -11,138 +9,117 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; -class PathIdentificationTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + PathTenantResolver::$tenantParameterName = 'tenant'; - PathTenantResolver::$tenantParameterName = 'tenant'; - - Route::group([ - 'prefix' => '/{tenant}', - 'middleware' => InitializeTenancyByPath::class, - ], function () { - Route::get('/foo/{a}/{b}', function ($a, $b) { - return "$a + $b"; - }); + Route::group([ + 'prefix' => '/{tenant}', + 'middleware' => InitializeTenancyByPath::class, + ], function () { + Route::get('/foo/{a}/{b}', function ($a, $b) { + return "$a + $b"; }); - } + }); +}); - public function tearDown(): void - { - parent::tearDown(); +afterEach(function () { + // Global state cleanup + PathTenantResolver::$tenantParameterName = 'tenant'; +}); - // Global state cleanup - PathTenantResolver::$tenantParameterName = 'tenant'; - } +test('tenant can be identified by path', function () { + Tenant::create([ + 'id' => 'acme', + ]); - /** @test */ - public function tenant_can_be_identified_by_path() - { - Tenant::create([ - 'id' => 'acme', - ]); + expect(tenancy()->initialized)->toBeFalse(); - $this->assertFalse(tenancy()->initialized); + $this->get('/acme/foo/abc/xyz'); - $this->get('/acme/foo/abc/xyz'); + expect(tenancy()->initialized)->toBeTrue(); + expect(tenant('id'))->toBe('acme'); +}); - $this->assertTrue(tenancy()->initialized); - $this->assertSame('acme', tenant('id')); - } +test('route actions dont get the tenant id', function () { + Tenant::create([ + 'id' => 'acme', + ]); - /** @test */ - public function route_actions_dont_get_the_tenant_id() - { - Tenant::create([ - 'id' => 'acme', - ]); + expect(tenancy()->initialized)->toBeFalse(); - $this->assertFalse(tenancy()->initialized); + $this + ->get('/acme/foo/abc/xyz') + ->assertContent('abc + xyz'); - $this - ->get('/acme/foo/abc/xyz') - ->assertContent('abc + xyz'); + expect(tenancy()->initialized)->toBeTrue(); + expect(tenant('id'))->toBe('acme'); +}); - $this->assertTrue(tenancy()->initialized); - $this->assertSame('acme', tenant('id')); - } +test('exception is thrown when tenant cannot be identified by path', function () { + $this->expectException(TenantCouldNotBeIdentifiedByPathException::class); - /** @test */ - public function exception_is_thrown_when_tenant_cannot_be_identified_by_path() - { - $this->expectException(TenantCouldNotBeIdentifiedByPathException::class); + $this + ->withoutExceptionHandling() + ->get('/acme/foo/abc/xyz'); - $this - ->withoutExceptionHandling() - ->get('/acme/foo/abc/xyz'); + expect(tenancy()->initialized)->toBeFalse(); +}); - $this->assertFalse(tenancy()->initialized); - } +test('onfail logic can be customized', function () { + InitializeTenancyByPath::$onFail = function () { + return 'foo'; + }; - /** @test */ - public function onfail_logic_can_be_customized() - { - InitializeTenancyByPath::$onFail = function () { - return 'foo'; - }; + $this + ->get('/acme/foo/abc/xyz') + ->assertContent('foo'); +}); - $this - ->get('/acme/foo/abc/xyz') - ->assertContent('foo'); - } - - /** @test */ - public function an_exception_is_thrown_when_the_routes_first_parameter_is_not_tenant() - { - Route::group([ - // 'prefix' => '/{tenant}', -- intentionally commented - 'middleware' => InitializeTenancyByPath::class, - ], function () { - Route::get('/bar/{a}/{b}', function ($a, $b) { - return "$a + $b"; - }); +test('an exception is thrown when the routes first parameter is not tenant', function () { + Route::group([ + // 'prefix' => '/{tenant}', -- intentionally commented + 'middleware' => InitializeTenancyByPath::class, + ], function () { + Route::get('/bar/{a}/{b}', function ($a, $b) { + return "$a + $b"; }); + }); - Tenant::create([ - 'id' => 'acme', - ]); + Tenant::create([ + 'id' => 'acme', + ]); - $this->expectException(RouteIsMissingTenantParameterException::class); + $this->expectException(RouteIsMissingTenantParameterException::class); - $this - ->withoutExceptionHandling() - ->get('/bar/foo/bar'); - } + $this + ->withoutExceptionHandling() + ->get('/bar/foo/bar'); +}); - /** @test */ - public function tenant_parameter_name_can_be_customized() - { - PathTenantResolver::$tenantParameterName = 'team'; +test('tenant parameter name can be customized', function () { + PathTenantResolver::$tenantParameterName = 'team'; - Route::group([ - 'prefix' => '/{team}', - 'middleware' => InitializeTenancyByPath::class, - ], function () { - Route::get('/bar/{a}/{b}', function ($a, $b) { - return "$a + $b"; - }); + Route::group([ + 'prefix' => '/{team}', + 'middleware' => InitializeTenancyByPath::class, + ], function () { + Route::get('/bar/{a}/{b}', function ($a, $b) { + return "$a + $b"; }); + }); - Tenant::create([ - 'id' => 'acme', - ]); + Tenant::create([ + 'id' => 'acme', + ]); - $this - ->get('/acme/bar/abc/xyz') - ->assertContent('abc + xyz'); + $this + ->get('/acme/bar/abc/xyz') + ->assertContent('abc + xyz'); - // Parameter for resolver is changed, so the /{tenant}/foo route will no longer work. - $this->expectException(RouteIsMissingTenantParameterException::class); + // Parameter for resolver is changed, so the /{tenant}/foo route will no longer work. + $this->expectException(RouteIsMissingTenantParameterException::class); - $this - ->withoutExceptionHandling() - ->get('/acme/foo/abc/xyz'); - } -} + $this + ->withoutExceptionHandling() + ->get('/acme/foo/abc/xyz'); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 00000000..9325cf53 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,3 @@ +in(__DIR__); diff --git a/tests/QueueTest.php b/tests/QueueTest.php index fe34ba92..1e662645 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -2,264 +2,229 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - -use Exception; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\Events\JobProcessed; -use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Spatie\Valuestore\Valuestore; +use Illuminate\Support\Facades\DB; +use Stancl\Tenancy\Tests\Etc\User; use Stancl\JobPipeline\JobPipeline; -use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; +use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Schema; use Stancl\Tenancy\Events\TenancyEnded; -use Stancl\Tenancy\Events\TenancyInitialized; -use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Events\TenantCreated; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Queue\Events\JobProcessing; +use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; -use Stancl\Tenancy\Tests\Etc\Tenant; -use Stancl\Tenancy\Tests\Etc\User; +use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; +use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -class QueueTest extends TestCase +beforeEach(function () { + config([ + 'tenancy.bootstrappers' => [ + QueueTenancyBootstrapper::class, + DatabaseTenancyBootstrapper::class, + ], + 'queue.default' => 'redis', + ]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + createValueStore(); +}); + +afterEach(function () { + $this->valuestore->flush(); +}); + +test('tenant id is passed to tenant queues', function () { + config(['queue.default' => 'sync']); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + Event::fake([JobProcessing::class, JobProcessed::class]); + + dispatch(new TestJob($this->valuestore)); + + Event::assertDispatched(JobProcessing::class, function ($event) { + return $event->job->payload()['tenant_id'] === tenant('id'); + }); +}); + +test('tenant id is not passed to central queues', function () { + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + Event::fake(); + + config(['queue.connections.central' => [ + 'driver' => 'sync', + 'central' => true, + ]]); + + dispatch(new TestJob($this->valuestore))->onConnection('central'); + + Event::assertDispatched(JobProcessing::class, function ($event) { + return ! isset($event->job->payload()['tenant_id']); + }); +}); + +test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) { + withTenantDatabases(); + withFailedJobs(); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + withUsers(); + + $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + + $this->valuestore->put('userName', 'Bar'); + + dispatch(new TestJob($this->valuestore, $user)); + + expect($this->valuestore->has('tenant_id'))->toBeFalse(); + + if ($shouldEndTenancy) { + tenancy()->end(); + } + + $this->artisan('queue:work --once'); + + expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0); + + expect($this->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); + + $tenant->run(function () use ($user) { + expect($user->fresh()->name)->toBe('Bar'); + }); +})->with([true, false]);; + +test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy) { + if (! Str::startsWith(app()->version(), '8')) { + $this->markTestSkipped('queue:retry tenancy is only supported in Laravel 8'); + } + + withFailedJobs(); + withTenantDatabases(); + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + withUsers(); + + $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + + $this->valuestore->put('userName', 'Bar'); + $this->valuestore->put('shouldFail', true); + + dispatch(new TestJob($this->valuestore, $user)); + + expect($this->valuestore->has('tenant_id'))->toBeFalse(); + + if ($shouldEndTenancy) { + tenancy()->end(); + } + + $this->artisan('queue:work --once'); + + expect(DB::connection('central')->table('failed_jobs')->count())->toBe(1); + expect($this->valuestore->get('tenant_id'))->toBeNull(); // job failed + + $this->artisan('queue:retry all'); + $this->artisan('queue:work --once'); + + expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0); + + expect($this->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); // job succeeded + + $tenant->run(function () use ($user) { + expect($user->fresh()->name)->toBe('Bar'); + }); +})->with([true, false]); + +test('the tenant used by the job doesnt change when the current tenant changes', function () { + $tenant1 = Tenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant1); + + dispatch(new TestJob($this->valuestore)); + + $tenant2 = Tenant::create([ + 'id' => 'foobar', + ]); + + tenancy()->initialize($tenant2); + + expect($this->valuestore->has('tenant_id'))->toBeFalse(); + $this->artisan('queue:work --once'); + + expect($this->valuestore->get('tenant_id'))->toBe('The current tenant id is: acme'); +}); + +function createValueStore(): void { - public $mockConsoleOutput = false; + $valueStorePath = __DIR__ . '/Etc/tmp/queuetest.json'; - /** @var Valuestore */ - protected $valuestore; - - public function setUp(): void - { - parent::setUp(); - - config([ - 'tenancy.bootstrappers' => [ - QueueTenancyBootstrapper::class, - DatabaseTenancyBootstrapper::class, - ], - 'queue.default' => 'redis', - ]); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); - - $this->createValueStore(); - } - - public function tearDown(): void - { - $this->valuestore->flush(); - } - - protected function createValueStore(): void - { - $valueStorePath = __DIR__ . '/Etc/tmp/queuetest.json'; - - if (! file_exists($valueStorePath)) { - // The directory sometimes goes missing as well when the file is deleted in git - if (! is_dir(__DIR__ . '/Etc/tmp')) { - mkdir(__DIR__ . '/Etc/tmp'); - } - - file_put_contents($valueStorePath, ''); + if (! file_exists($valueStorePath)) { + // The directory sometimes goes missing as well when the file is deleted in git + if (! is_dir(__DIR__ . '/Etc/tmp')) { + mkdir(__DIR__ . '/Etc/tmp'); } - $this->valuestore = Valuestore::make($valueStorePath)->flush(); + file_put_contents($valueStorePath, ''); } - protected function withFailedJobs() - { - Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { - $table->increments('id'); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); - $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); - }); - } + test()->valuestore = Valuestore::make($valueStorePath)->flush(); +} - protected function withUsers() - { - Schema::create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('name'); - $table->string('email')->unique(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - } +function withFailedJobs() +{ + Schema::connection('central')->create('failed_jobs', function (Blueprint $table) { + $table->increments('id'); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); +} - protected function withTenantDatabases() - { - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - } +function withUsers() +{ + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); +} - /** @test */ - public function tenant_id_is_passed_to_tenant_queues() - { - config(['queue.default' => 'sync']); - - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - Event::fake([JobProcessing::class, JobProcessed::class]); - - dispatch(new TestJob($this->valuestore)); - - Event::assertDispatched(JobProcessing::class, function ($event) { - return $event->job->payload()['tenant_id'] === tenant('id'); - }); - } - - /** @test */ - public function tenant_id_is_not_passed_to_central_queues() - { - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - Event::fake(); - - config(['queue.connections.central' => [ - 'driver' => 'sync', - 'central' => true, - ]]); - - dispatch(new TestJob($this->valuestore))->onConnection('central'); - - Event::assertDispatched(JobProcessing::class, function ($event) { - return ! isset($event->job->payload()['tenant_id']); - }); - } - - /** - * @test - * - * @testWith [true] - * [false] - */ - public function tenancy_is_initialized_inside_queues(bool $shouldEndTenancy) - { - $this->withTenantDatabases(); - $this->withFailedJobs(); - - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - $this->withUsers(); - - $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); - - $this->valuestore->put('userName', 'Bar'); - - dispatch(new TestJob($this->valuestore, $user)); - - $this->assertFalse($this->valuestore->has('tenant_id')); - - if ($shouldEndTenancy) { - tenancy()->end(); - } - - $this->artisan('queue:work --once'); - - $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count()); - - $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id')); - - $tenant->run(function () use ($user) { - $this->assertSame('Bar', $user->fresh()->name); - }); - } - - /** - * @test - * - * @testWith [true] - * [false] - */ - public function tenancy_is_initialized_when_retrying_jobs(bool $shouldEndTenancy) - { - if (! Str::startsWith(app()->version(), '8')) { - $this->markTestSkipped('queue:retry tenancy is only supported in Laravel 8'); - } - - $this->withFailedJobs(); - $this->withTenantDatabases(); - - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - $this->withUsers(); - - $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); - - $this->valuestore->put('userName', 'Bar'); - $this->valuestore->put('shouldFail', true); - - dispatch(new TestJob($this->valuestore, $user)); - - $this->assertFalse($this->valuestore->has('tenant_id')); - - if ($shouldEndTenancy) { - tenancy()->end(); - } - - $this->artisan('queue:work --once'); - - $this->assertSame(1, DB::connection('central')->table('failed_jobs')->count()); - $this->assertNull($this->valuestore->get('tenant_id')); // job failed - - $this->artisan('queue:retry all'); - $this->artisan('queue:work --once'); - - $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count()); - - $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id')); // job succeeded - - $tenant->run(function () use ($user) { - $this->assertSame('Bar', $user->fresh()->name); - }); - } - - /** @test */ - public function the_tenant_used_by_the_job_doesnt_change_when_the_current_tenant_changes() - { - $tenant1 = Tenant::create([ - 'id' => 'acme', - ]); - - tenancy()->initialize($tenant1); - - dispatch(new TestJob($this->valuestore)); - - $tenant2 = Tenant::create([ - 'id' => 'foobar', - ]); - - tenancy()->initialize($tenant2); - - $this->assertFalse($this->valuestore->has('tenant_id')); - $this->artisan('queue:work --once'); - - $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); - } +function withTenantDatabases() +{ + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); } class TestJob implements ShouldQueue @@ -297,3 +262,4 @@ class TestJob implements ShouldQueue } } } + diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php index 52a502f9..81bdda53 100644 --- a/tests/RequestDataIdentificationTest.php +++ b/tests/RequestDataIdentificationTest.php @@ -2,64 +2,49 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Tests\Etc\Tenant; -class RequestDataIdentificationTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + config([ + 'tenancy.central_domains' => [ + 'localhost', + ], + ]); - config([ - 'tenancy.central_domains' => [ - 'localhost', - ], - ]); + Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () { + return 'Tenant id: ' . tenant('id'); + }); +}); - Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () { - return 'Tenant id: ' . tenant('id'); - }); - } +afterEach(function () { + InitializeTenancyByRequestData::$header = 'X-Tenant'; + InitializeTenancyByRequestData::$queryParameter = 'tenant'; +}); - public function tearDown(): void - { - InitializeTenancyByRequestData::$header = 'X-Tenant'; - InitializeTenancyByRequestData::$queryParameter = 'tenant'; +test('header identification works', function () { + InitializeTenancyByRequestData::$header = 'X-Tenant'; + $tenant = Tenant::create(); + $tenant2 = Tenant::create(); - parent::tearDown(); - } + $this + ->withoutExceptionHandling() + ->get('test', [ + 'X-Tenant' => $tenant->id, + ]) + ->assertSee($tenant->id); +}); - /** @test */ - public function header_identification_works() - { - InitializeTenancyByRequestData::$header = 'X-Tenant'; - $tenant = Tenant::create(); - $tenant2 = Tenant::create(); +test('query parameter identification works', function () { + InitializeTenancyByRequestData::$header = null; + InitializeTenancyByRequestData::$queryParameter = 'tenant'; - $this - ->withoutExceptionHandling() - ->get('test', [ - 'X-Tenant' => $tenant->id, - ]) - ->assertSee($tenant->id); - } + $tenant = Tenant::create(); + $tenant2 = Tenant::create(); - /** @test */ - public function query_parameter_identification_works() - { - InitializeTenancyByRequestData::$header = null; - InitializeTenancyByRequestData::$queryParameter = 'tenant'; - - $tenant = Tenant::create(); - $tenant2 = Tenant::create(); - - $this - ->withoutExceptionHandling() - ->get('test?tenant=' . $tenant->id) - ->assertSee($tenant->id); - } -} + $this + ->withoutExceptionHandling() + ->get('test?tenant=' . $tenant->id) + ->assertSee($tenant->id); +}); diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 0ff95a52..99b41f23 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Events\CallQueuedListener; @@ -30,549 +28,521 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\UpdateSyncedResource; use Stancl\Tenancy\Tests\Etc\Tenant; -class ResourceSyncingTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + config(['tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class, + ]]); - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); + DatabaseConfig::generateDatabaseNamesUsing(function () { + return 'db' . Str::random(16); + }); - DatabaseConfig::generateDatabaseNamesUsing(function () { - return 'db' . Str::random(16); - }); + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); + UpdateSyncedResource::$shouldQueue = false; // global state cleanup + Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class); - UpdateSyncedResource::$shouldQueue = false; // global state cleanup - Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class); + test()->artisan('migrate', [ + '--path' => [ + __DIR__ . '/Etc/synced_resource_migrations', + __DIR__ . '/Etc/synced_resource_migrations/users', + ], + '--realpath' => true, + ])->assertExitCode(0); +}); - $this->artisan('migrate', [ - '--path' => [ - __DIR__ . '/Etc/synced_resource_migrations', - __DIR__ . '/Etc/synced_resource_migrations/users', - ], - '--realpath' => true, - ])->assertExitCode(0); - } +test('an event is triggered when a synced resource is changed', function () { + Event::fake([SyncedResourceSaved::class]); - protected function migrateTenants() - { - $this->artisan('tenants:migrate', [ - '--path' => __DIR__ . '/Etc/synced_resource_migrations/users', - '--realpath' => true, - ])->assertExitCode(0); - } + $user = ResourceUser::create([ + 'name' => 'Foo', + 'email' => 'foo@email.com', + 'password' => 'secret', + 'global_id' => 'foo', + 'role' => 'foo', + ]); - /** @test */ - public function an_event_is_triggered_when_a_synced_resource_is_changed() - { - Event::fake([SyncedResourceSaved::class]); + Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) { + return $event->model === $user; + }); +}); - $user = ResourceUser::create([ - 'name' => 'Foo', - 'email' => 'foo@email.com', - 'password' => 'secret', - 'global_id' => 'foo', - 'role' => 'foo', - ]); +test('only the synced columns are updated in the central db', function () { + // Create user in central DB + $user = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'superadmin', // unsynced + ]); - Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) { - return $event->model === $user; - }); - } + $tenant = ResourceTenant::create(); + migrateTenantsResource(); - /** @test */ - public function only_the_synced_columns_are_updated_in_the_central_db() - { - // Create user in central DB - $user = CentralUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'superadmin', // unsynced - ]); + tenancy()->initialize($tenant); - $tenant = ResourceTenant::create(); - $this->migrateTenants(); + // Create the same user in tenant DB + $user = ResourceUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); - tenancy()->initialize($tenant); + // Update user in tenant DB + $user->update([ + 'name' => 'John Foo', // synced + 'email' => 'john@foreignhost', // synced + 'role' => 'admin', // unsynced + ]); - // Create the same user in tenant DB - $user = ResourceUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'commenter', // unsynced - ]); + // Assert new values + $this->assertEquals([ + 'id' => 1, + 'global_id' => 'acme', + 'name' => 'John Foo', + 'email' => 'john@foreignhost', + 'password' => 'secret', + 'role' => 'admin', + ], $user->getAttributes()); - // Update user in tenant DB - $user->update([ - 'name' => 'John Foo', // synced - 'email' => 'john@foreignhost', // synced - 'role' => 'admin', // unsynced - ]); + tenancy()->end(); - // Assert new values - $this->assertEquals([ - 'id' => 1, - 'global_id' => 'acme', - 'name' => 'John Foo', - 'email' => 'john@foreignhost', - 'password' => 'secret', - 'role' => 'admin', - ], $user->getAttributes()); + // Assert changes bubbled up + $this->assertEquals([ + 'id' => 1, + 'global_id' => 'acme', + 'name' => 'John Foo', // synced + 'email' => 'john@foreignhost', // synced + 'password' => 'secret', // no changes + 'role' => 'superadmin', // unsynced + ], ResourceUser::first()->getAttributes()); +}); - tenancy()->end(); +test('creating the resource in tenant database creates it in central database and creates the mapping', function () { + creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase(); +}); - // Assert changes bubbled up - $this->assertEquals([ - 'id' => 1, - 'global_id' => 'acme', - 'name' => 'John Foo', // synced - 'email' => 'john@foreignhost', // synced - 'password' => 'secret', // no changes - 'role' => 'superadmin', // unsynced - ], ResourceUser::first()->getAttributes()); - } +test('trying to update synced resources from central context using tenant models results in an exception', function () { + creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase(); - /** @test */ - public function creating_the_resource_in_tenant_database_creates_it_in_central_database_and_creates_the_mapping() - { - // Assert no user in central DB - $this->assertCount(0, ResourceUser::all()); + tenancy()->end(); + expect(tenancy()->initialized)->toBeFalse(); - $tenant = ResourceTenant::create(); - $this->migrateTenants(); + $this->expectException(ModelNotSyncMasterException::class); + ResourceUser::first()->update(['role' => 'foobar']); +}); - tenancy()->initialize($tenant); +test('attaching a tenant to the central resource triggers a pull from the tenant db', function () { + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); - // Create the same user in tenant DB + $tenant = ResourceTenant::create([ + 'id' => 't1', + ]); + migrateTenantsResource(); + + $tenant->run(function () { + expect(ResourceUser::all())->toHaveCount(0); + }); + + $centralUser->tenants()->attach('t1'); + + $tenant->run(function () { + expect(ResourceUser::all())->toHaveCount(1); + }); +}); + +test('attaching users to tenants does not do anything', function () { + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); + + $tenant = ResourceTenant::create([ + 'id' => 't1', + ]); + migrateTenantsResource(); + + $tenant->run(function () { + expect(ResourceUser::all())->toHaveCount(0); + }); + + // The child model is inaccessible in the Pivot Model, so we can't fire any events. + $tenant->users()->attach($centralUser); + + $tenant->run(function () { + // Still zero + expect(ResourceUser::all())->toHaveCount(0); + }); +}); + +test('resources are synced only to workspaces that have the resource', function () { + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); + + $t1 = ResourceTenant::create([ + 'id' => 't1', + ]); + + $t2 = ResourceTenant::create([ + 'id' => 't2', + ]); + + $t3 = ResourceTenant::create([ + 'id' => 't3', + ]); + migrateTenantsResource(); + + $centralUser->tenants()->attach('t1'); + $centralUser->tenants()->attach('t2'); + // t3 is not attached + + $t1->run(function () { + // assert user exists + expect(ResourceUser::all())->toHaveCount(1); + }); + + $t2->run(function () { + // assert user exists + expect(ResourceUser::all())->toHaveCount(1); + }); + + $t3->run(function () { + // assert user does NOT exist + expect(ResourceUser::all())->toHaveCount(0); + }); +}); + +test('when a resource exists in other tenant dbs but is created in a tenant db the synced columns are updated in the other dbs', function () { + // create shared resource + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); + + $t1 = ResourceTenant::create([ + 'id' => 't1', + ]); + $t2 = ResourceTenant::create([ + 'id' => 't2', + ]); + migrateTenantsResource(); + + // Copy (cascade) user to t1 DB + $centralUser->tenants()->attach('t1'); + + $t2->run(function () { + // Create user with the same global ID in t2 database ResourceUser::create([ 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', + 'name' => 'John Foo', // changed + 'email' => 'john@foo', // changed 'password' => 'secret', - 'role' => 'commenter', // unsynced + 'role' => 'superadmin', // unsynced + ]); + }); + + $centralUser = CentralUser::first(); + expect($centralUser->name)->toBe('John Foo'); // name changed + expect($centralUser->email)->toBe('john@foo'); // email changed + expect($centralUser->role)->toBe('commenter'); // role didn't change + + $t1->run(function () { + $user = ResourceUser::first(); + expect($user->name)->toBe('John Foo'); // name changed + expect($user->email)->toBe('john@foo'); // email changed + expect($user->role)->toBe('commenter'); // role didn't change, i.e. is the same as from the original copy from central + }); +}); + +test('the synced columns are updated in other tenant dbs where the resource exists', function () { + // create shared resource + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); + + $t1 = ResourceTenant::create([ + 'id' => 't1', + ]); + $t2 = ResourceTenant::create([ + 'id' => 't2', + ]); + $t3 = ResourceTenant::create([ + 'id' => 't3', + ]); + migrateTenantsResource(); + + // Copy (cascade) user to t1 DB + $centralUser->tenants()->attach('t1'); + $centralUser->tenants()->attach('t2'); + $centralUser->tenants()->attach('t3'); + + $t3->run(function () { + ResourceUser::first()->update([ + 'name' => 'John 3', + 'role' => 'employee', // unsynced ]); - tenancy()->end(); + expect(ResourceUser::first()->role)->toBe('employee'); + }); - // Asset user was created - $this->assertSame('acme', CentralUser::first()->global_id); - $this->assertSame('commenter', CentralUser::first()->role); + // Check that change was cascaded to other tenants + $t1->run($check = function () { + $user = ResourceUser::first(); - // Assert mapping was created - $this->assertCount(1, CentralUser::first()->tenants); + expect($user->name)->toBe('John 3'); // synced + expect($user->role)->toBe('commenter'); // unsynced + }); + $t2->run($check); - // Assert role change doesn't cascade - CentralUser::first()->update(['role' => 'central superadmin']); - tenancy()->initialize($tenant); - $this->assertSame('commenter', ResourceUser::first()->role); - } + // Check that change bubbled up to central DB + expect(CentralUser::count())->toBe(1); + $centralUser = CentralUser::first(); + expect($centralUser->name)->toBe('John 3'); // synced + expect($centralUser->role)->toBe('commenter'); // unsynced +}); - /** @test */ - public function trying_to_update_synced_resources_from_central_context_using_tenant_models_results_in_an_exception() - { - $this->creating_the_resource_in_tenant_database_creates_it_in_central_database_and_creates_the_mapping(); +test('global id is generated using id generator when its not supplied', function () { + $user = CentralUser::create([ + 'name' => 'John Doe', + 'email' => 'john@doe', + 'password' => 'secret', + 'role' => 'employee', + ]); - tenancy()->end(); - $this->assertFalse(tenancy()->initialized); + $this->assertNotNull($user->global_id); +}); - $this->expectException(ModelNotSyncMasterException::class); - ResourceUser::first()->update(['role' => 'foobar']); - } +test('when the resource doesnt exist in the tenant db non synced columns will cascade too', function () { + $centralUser = CentralUser::create([ + 'name' => 'John Doe', + 'email' => 'john@doe', + 'password' => 'secret', + 'role' => 'employee', + ]); - /** @test */ - public function attaching_a_tenant_to_the_central_resource_triggers_a_pull_from_the_tenant_db() - { - $centralUser = CentralUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'commenter', // unsynced - ]); + $t1 = ResourceTenant::create([ + 'id' => 't1', + ]); - $tenant = ResourceTenant::create([ - 'id' => 't1', - ]); - $this->migrateTenants(); + migrateTenantsResource(); - $tenant->run(function () { - $this->assertCount(0, ResourceUser::all()); - }); + $centralUser->tenants()->attach('t1'); - $centralUser->tenants()->attach('t1'); + $t1->run(function () { + expect(ResourceUser::first()->role)->toBe('employee'); + }); +}); - $tenant->run(function () { - $this->assertCount(1, ResourceUser::all()); - }); - } +test('when the resource doesnt exist in the central db non synced columns will bubble up too', function () { + $t1 = ResourceTenant::create([ + 'id' => 't1', + ]); - /** @test */ - public function attaching_users_to_tenants_DOES_NOT_DO_ANYTHING() - { - $centralUser = CentralUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'commenter', // unsynced - ]); + migrateTenantsResource(); - $tenant = ResourceTenant::create([ - 'id' => 't1', - ]); - $this->migrateTenants(); - - $tenant->run(function () { - $this->assertCount(0, ResourceUser::all()); - }); - - // The child model is inaccessible in the Pivot Model, so we can't fire any events. - $tenant->users()->attach($centralUser); - - $tenant->run(function () { - // Still zero - $this->assertCount(0, ResourceUser::all()); - }); - } - - /** @test */ - public function resources_are_synced_only_to_workspaces_that_have_the_resource() - { - $centralUser = CentralUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'commenter', // unsynced - ]); - - $t1 = ResourceTenant::create([ - 'id' => 't1', - ]); - - $t2 = ResourceTenant::create([ - 'id' => 't2', - ]); - - $t3 = ResourceTenant::create([ - 'id' => 't3', - ]); - $this->migrateTenants(); - - $centralUser->tenants()->attach('t1'); - $centralUser->tenants()->attach('t2'); - // t3 is not attached - - $t1->run(function () { - // assert user exists - $this->assertCount(1, ResourceUser::all()); - }); - - $t2->run(function () { - // assert user exists - $this->assertCount(1, ResourceUser::all()); - }); - - $t3->run(function () { - // assert user does NOT exist - $this->assertCount(0, ResourceUser::all()); - }); - } - - /** @test */ - public function when_a_resource_exists_in_other_tenant_dbs_but_is_CREATED_in_a_tenant_db_the_synced_columns_are_updated_in_the_other_dbs() - { - // create shared resource - $centralUser = CentralUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'commenter', // unsynced - ]); - - $t1 = ResourceTenant::create([ - 'id' => 't1', - ]); - $t2 = ResourceTenant::create([ - 'id' => 't2', - ]); - $this->migrateTenants(); - - // Copy (cascade) user to t1 DB - $centralUser->tenants()->attach('t1'); - - $t2->run(function () { - // Create user with the same global ID in t2 database - ResourceUser::create([ - 'global_id' => 'acme', - 'name' => 'John Foo', // changed - 'email' => 'john@foo', // changed - 'password' => 'secret', - 'role' => 'superadmin', // unsynced - ]); - }); - - $centralUser = CentralUser::first(); - $this->assertSame('John Foo', $centralUser->name); // name changed - $this->assertSame('john@foo', $centralUser->email); // email changed - $this->assertSame('commenter', $centralUser->role); // role didn't change - - $t1->run(function () { - $user = ResourceUser::first(); - $this->assertSame('John Foo', $user->name); // name changed - $this->assertSame('john@foo', $user->email); // email changed - $this->assertSame('commenter', $user->role); // role didn't change, i.e. is the same as from the original copy from central - }); - } - - /** @test */ - public function the_synced_columns_are_updated_in_other_tenant_dbs_where_the_resource_exists() - { - // create shared resource - $centralUser = CentralUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'commenter', // unsynced - ]); - - $t1 = ResourceTenant::create([ - 'id' => 't1', - ]); - $t2 = ResourceTenant::create([ - 'id' => 't2', - ]); - $t3 = ResourceTenant::create([ - 'id' => 't3', - ]); - $this->migrateTenants(); - - // Copy (cascade) user to t1 DB - $centralUser->tenants()->attach('t1'); - $centralUser->tenants()->attach('t2'); - $centralUser->tenants()->attach('t3'); - - $t3->run(function () { - ResourceUser::first()->update([ - 'name' => 'John 3', - 'role' => 'employee', // unsynced - ]); - - $this->assertSame('employee', ResourceUser::first()->role); - }); - - // Check that change was cascaded to other tenants - $t1->run($check = function () { - $user = ResourceUser::first(); - - $this->assertSame('John 3', $user->name); // synced - $this->assertSame('commenter', $user->role); // unsynced - }); - $t2->run($check); - - // Check that change bubbled up to central DB - $this->assertSame(1, CentralUser::count()); - $centralUser = CentralUser::first(); - $this->assertSame('John 3', $centralUser->name); // synced - $this->assertSame('commenter', $centralUser->role); // unsynced - } - - /** @test */ - public function global_id_is_generated_using_id_generator_when_its_not_supplied() - { - $user = CentralUser::create([ + $t1->run(function () { + ResourceUser::create([ 'name' => 'John Doe', 'email' => 'john@doe', 'password' => 'secret', 'role' => 'employee', ]); + }); - $this->assertNotNull($user->global_id); - } + expect(CentralUser::first()->role)->toBe('employee'); +}); - /** @test */ - public function when_the_resource_doesnt_exist_in_the_tenant_db_non_synced_columns_will_cascade_too() - { - $centralUser = CentralUser::create([ +test('the listener can be queued', function () { + Queue::fake(); + UpdateSyncedResource::$shouldQueue = true; + + $t1 = ResourceTenant::create([ + 'id' => 't1', + ]); + + migrateTenantsResource(); + + Queue::assertNothingPushed(); + + $t1->run(function () { + ResourceUser::create([ 'name' => 'John Doe', 'email' => 'john@doe', 'password' => 'secret', 'role' => 'employee', ]); + }); - $t1 = ResourceTenant::create([ - 'id' => 't1', + Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) { + return $job->class === UpdateSyncedResource::class; + }); +}); + +test('an event is fired for all touched resources', function () { + Event::fake([SyncedResourceChangedInForeignDatabase::class]); + + // create shared resource + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); + + $t1 = ResourceTenant::create([ + 'id' => 't1', + ]); + $t2 = ResourceTenant::create([ + 'id' => 't2', + ]); + $t3 = ResourceTenant::create([ + 'id' => 't3', + ]); + migrateTenantsResource(); + + // Copy (cascade) user to t1 DB + $centralUser->tenants()->attach('t1'); + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return $event->tenant->getTenantKey() === 't1'; + }); + + $centralUser->tenants()->attach('t2'); + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return $event->tenant->getTenantKey() === 't2'; + }); + + $centralUser->tenants()->attach('t3'); + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return $event->tenant->getTenantKey() === 't3'; + }); + + // Assert no event for central + Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return $event->tenant === null; + }); + + // Flush + Event::fake([SyncedResourceChangedInForeignDatabase::class]); + + $t3->run(function () { + ResourceUser::first()->update([ + 'name' => 'John 3', + 'role' => 'employee', // unsynced ]); - $this->migrateTenants(); + expect(ResourceUser::first()->role)->toBe('employee'); + }); - $centralUser->tenants()->attach('t1'); + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return optional($event->tenant)->getTenantKey() === 't1'; + }); + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return optional($event->tenant)->getTenantKey() === 't2'; + }); - $t1->run(function () { - $this->assertSame('employee', ResourceUser::first()->role); - }); - } + // Assert NOT dispatched in t3 + Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return optional($event->tenant)->getTenantKey() === 't3'; + }); - /** @test */ - public function when_the_resource_doesnt_exist_in_the_central_db_non_synced_columns_will_bubble_up_too() - { - $t1 = ResourceTenant::create([ - 'id' => 't1', - ]); + // Assert dispatched in central + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return $event->tenant === null; + }); - $this->migrateTenants(); + // Flush + Event::fake([SyncedResourceChangedInForeignDatabase::class]); - $t1->run(function () { - ResourceUser::create([ - 'name' => 'John Doe', - 'email' => 'john@doe', - 'password' => 'secret', - 'role' => 'employee', - ]); - }); + $centralUser->update([ + 'name' => 'John Central', + ]); - $this->assertSame('employee', CentralUser::first()->role); - } + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return optional($event->tenant)->getTenantKey() === 't1'; + }); + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return optional($event->tenant)->getTenantKey() === 't2'; + }); + Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return optional($event->tenant)->getTenantKey() === 't3'; + }); + // Assert NOT dispatched in central + Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { + return $event->tenant === null; + }); +}); - /** @test */ - public function the_listener_can_be_queued() - { - Queue::fake(); - UpdateSyncedResource::$shouldQueue = true; +// todo@tests +function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase() +{ + // Assert no user in central DB + expect(ResourceUser::all())->toHaveCount(0); - $t1 = ResourceTenant::create([ - 'id' => 't1', - ]); + $tenant = ResourceTenant::create(); + migrateTenantsResource(); - $this->migrateTenants(); + tenancy()->initialize($tenant); - Queue::assertNothingPushed(); + // Create the same user in tenant DB + ResourceUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); - $t1->run(function () { - ResourceUser::create([ - 'name' => 'John Doe', - 'email' => 'john@doe', - 'password' => 'secret', - 'role' => 'employee', - ]); - }); + tenancy()->end(); - Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) { - return $job->class === UpdateSyncedResource::class; - }); - } + // Asset user was created + expect(CentralUser::first()->global_id)->toBe('acme'); + expect(CentralUser::first()->role)->toBe('commenter'); - /** @test */ - public function an_event_is_fired_for_all_touched_resources() - { - Event::fake([SyncedResourceChangedInForeignDatabase::class]); + // Assert mapping was created + expect(CentralUser::first()->tenants)->toHaveCount(1); - // create shared resource - $centralUser = CentralUser::create([ - 'global_id' => 'acme', - 'name' => 'John Doe', - 'email' => 'john@localhost', - 'password' => 'secret', - 'role' => 'commenter', // unsynced - ]); + // Assert role change doesn't cascade + CentralUser::first()->update(['role' => 'central superadmin']); + tenancy()->initialize($tenant); + expect(ResourceUser::first()->role)->toBe('commenter'); +} - $t1 = ResourceTenant::create([ - 'id' => 't1', - ]); - $t2 = ResourceTenant::create([ - 'id' => 't2', - ]); - $t3 = ResourceTenant::create([ - 'id' => 't3', - ]); - $this->migrateTenants(); - - // Copy (cascade) user to t1 DB - $centralUser->tenants()->attach('t1'); - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return $event->tenant->getTenantKey() === 't1'; - }); - - $centralUser->tenants()->attach('t2'); - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return $event->tenant->getTenantKey() === 't2'; - }); - - $centralUser->tenants()->attach('t3'); - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return $event->tenant->getTenantKey() === 't3'; - }); - - // Assert no event for central - Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return $event->tenant === null; - }); - - // Flush - Event::fake([SyncedResourceChangedInForeignDatabase::class]); - - $t3->run(function () { - ResourceUser::first()->update([ - 'name' => 'John 3', - 'role' => 'employee', // unsynced - ]); - - $this->assertSame('employee', ResourceUser::first()->role); - }); - - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return optional($event->tenant)->getTenantKey() === 't1'; - }); - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return optional($event->tenant)->getTenantKey() === 't2'; - }); - - // Assert NOT dispatched in t3 - Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return optional($event->tenant)->getTenantKey() === 't3'; - }); - - // Assert dispatched in central - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return $event->tenant === null; - }); - - // Flush - Event::fake([SyncedResourceChangedInForeignDatabase::class]); - - $centralUser->update([ - 'name' => 'John Central', - ]); - - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return optional($event->tenant)->getTenantKey() === 't1'; - }); - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return optional($event->tenant)->getTenantKey() === 't2'; - }); - Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return optional($event->tenant)->getTenantKey() === 't3'; - }); - // Assert NOT dispatched in central - Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) { - return $event->tenant === null; - }); - } +function migrateTenantsResource() +{ + test()->artisan('tenants:migrate', [ + '--path' => __DIR__ . '/Etc/synced_resource_migrations/users', + '--realpath' => true, + ])->assertExitCode(0); } class ResourceTenant extends Tenant diff --git a/tests/ScopeSessionsTest.php b/tests/ScopeSessionsTest.php index b5fb962a..b1b6a05e 100644 --- a/tests/ScopeSessionsTest.php +++ b/tests/ScopeSessionsTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Session\Middleware\StartSession; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; @@ -13,69 +11,57 @@ use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Middleware\ScopeSessions; use Stancl\Tenancy\Tests\Etc\Tenant; -class ScopeSessionsTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); - - Route::group([ - 'middleware' => [StartSession::class, InitializeTenancyBySubdomain::class, ScopeSessions::class], - ], function () { - Route::get('/foo', function () { - return 'true'; - }); +beforeEach(function () { + Route::group([ + 'middleware' => [StartSession::class, InitializeTenancyBySubdomain::class, ScopeSessions::class], + ], function () { + Route::get('/foo', function () { + return 'true'; }); + }); - Event::listen(TenantCreated::class, function (TenantCreated $event) { - $tenant = $event->tenant; + Event::listen(TenantCreated::class, function (TenantCreated $event) { + $tenant = $event->tenant; - /** @var Tenant $tenant */ - $tenant->domains()->create([ - 'domain' => $tenant->id, - ]); - }); - } - - /** @test */ - public function tenant_id_is_auto_added_to_session_if_its_missing() - { - $tenant = Tenant::create([ - 'id' => 'acme', + /** @var Tenant $tenant */ + $tenant->domains()->create([ + 'domain' => $tenant->id, ]); + }); +}); - $this->get('http://acme.localhost/foo') - ->assertSessionHas(ScopeSessions::$tenantIdKey, 'acme'); - } +test('tenant id is auto added to session if its missing', function () { + $tenant = Tenant::create([ + 'id' => 'acme', + ]); - /** @test */ - public function changing_tenant_id_in_session_will_abort_the_request() - { - $tenant = Tenant::create([ - 'id' => 'acme', - ]); + $this->get('http://acme.localhost/foo') + ->assertSessionHas(ScopeSessions::$tenantIdKey, 'acme'); +}); - $this->get('http://acme.localhost/foo') - ->assertSuccessful(); +test('changing tenant id in session will abort the request', function () { + $tenant = Tenant::create([ + 'id' => 'acme', + ]); - session()->put(ScopeSessions::$tenantIdKey, 'foobar'); + $this->get('http://acme.localhost/foo') + ->assertSuccessful(); - $this->get('http://acme.localhost/foo') - ->assertStatus(403); - } + session()->put(ScopeSessions::$tenantIdKey, 'foobar'); - /** @test */ - public function an_exception_is_thrown_when_the_middleware_is_executed_before_tenancy_is_initialized() - { - Route::get('/bar', function () { - return true; - })->middleware([StartSession::class, ScopeSessions::class]); + $this->get('http://acme.localhost/foo') + ->assertStatus(403); +}); - $tenant = Tenant::create([ - 'id' => 'acme', - ]); +test('an exception is thrown when the middleware is executed before tenancy is initialized', function () { + Route::get('/bar', function () { + return true; + })->middleware([StartSession::class, ScopeSessions::class]); - $this->expectException(TenancyNotInitializedException::class); - $this->withoutExceptionHandling()->get('http://acme.localhost/bar'); - } -} + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + + $this->expectException(TenancyNotInitializedException::class); + $this->withoutExceptionHandling()->get('http://acme.localhost/bar'); +}); diff --git a/tests/SingleDatabaseTenancyTest.php b/tests/SingleDatabaseTenancyTest.php index d0425dd9..83807d14 100644 --- a/tests/SingleDatabaseTenancyTest.php +++ b/tests/SingleDatabaseTenancyTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Database\Eloquent\Model; use Illuminate\Database\QueryException; use Illuminate\Database\Schema\Blueprint; @@ -14,309 +12,293 @@ use Stancl\Tenancy\Database\Concerns\BelongsToTenant; use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules; use Stancl\Tenancy\Tests\Etc\Tenant as TestTenant; -class SingleDatabaseTenancyTest extends TestCase +beforeEach(function () { + BelongsToTenant::$tenantIdColumn = 'tenant_id'; + + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); + + $table->string('tenant_id'); + + $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + }); + + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); + + $table->unsignedInteger('post_id'); + + $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade'); + }); + + config(['tenancy.tenant_model' => Tenant::class]); +}); + +test('primary models are scoped to the current tenant', function () { + primaryModelsScopedToCurrentTenant(); +}); + +test('primary models are not scoped in the central context', function () { + primaryModelsScopedToCurrentTenant(); + + tenancy()->end(); + + expect(Post::count())->toBe(2); +}); + +test('secondary models are scoped to the current tenant when accessed via primary model', function () { + secondaryModelsAreScopedToCurrentTenant(); +}); + +test('secondary models are not scoped to the current tenant when accessed directly', function () { + secondaryModelsAreScopedToCurrentTenant(); + + // We're in acme context + expect(tenant('id'))->toBe('acme'); + + expect(Comment::count())->toBe(2); +}); + +test('secondary models a r e scoped to the current tenant when accessed directly and parent relationship traitis used', function () { + $acme = Tenant::create([ + 'id' => 'acme', + ]); + + $acme->run(function () { + $post = Post::create(['text' => 'Foo']); + $post->scoped_comments()->create(['text' => 'Comment Text']); + + expect(Post::count())->toBe(1); + expect(ScopedComment::count())->toBe(1); + }); + + $foobar = Tenant::create([ + 'id' => 'foobar', + ]); + + $foobar->run(function () { + expect(Post::count())->toBe(0); + expect(ScopedComment::count())->toBe(0); + + $post = Post::create(['text' => 'Bar']); + $post->scoped_comments()->create(['text' => 'Comment Text 2']); + + expect(Post::count())->toBe(1); + expect(ScopedComment::count())->toBe(1); + }); + + // Global context + expect(ScopedComment::count())->toBe(2); +}); + +test('secondary models are not scoped in the central context', function () { + secondaryModelsAreScopedToCurrentTenant(); + + tenancy()->end(); + + expect(Comment::count())->toBe(2); +}); + +test('global models are not scoped at all', function () { + Schema::create('global_resources', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); + }); + + GlobalResource::create(['text' => 'First']); + GlobalResource::create(['text' => 'Second']); + + $acme = Tenant::create([ + 'id' => 'acme', + ]); + + $acme->run(function () { + expect(GlobalResource::count())->toBe(2); + + GlobalResource::create(['text' => 'Third']); + GlobalResource::create(['text' => 'Fourth']); + }); + + expect(GlobalResource::count())->toBe(4); +}); + +test('tenant id and relationship is auto added when creating primary resources in tenant context', function () { + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); + + $post = Post::create(['text' => 'Foo']); + + expect($post->tenant_id)->toBe('acme'); + expect($post->relationLoaded('tenant'))->toBeTrue(); + expect($post->tenant)->toBe($acme); + expect($post->tenant)->toBe(tenant()); +}); + +test('tenant id is not auto added when creating primary resources in central context', function () { + $this->expectException(QueryException::class); + + Post::create(['text' => 'Foo']); +}); + +test('tenant id column name can be customized', function () { + BelongsToTenant::$tenantIdColumn = 'team_id'; + + Schema::drop('comments'); + Schema::drop('posts'); + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); + + $table->string('team_id'); + + $table->foreign('team_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + }); + + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); + + $post = Post::create(['text' => 'Foo']); + + expect($post->team_id)->toBe('acme'); + + // ====================================== + // foobar context + tenancy()->initialize($foobar = Tenant::create([ + 'id' => 'foobar', + ])); + + $post = Post::create(['text' => 'Bar']); + + expect($post->team_id)->toBe('foobar'); + + $post = Post::first(); + + expect($post->team_id)->toBe('foobar'); + + // ====================================== + // acme context again + + tenancy()->initialize($acme); + + $post = Post::first(); + expect($post->team_id)->toBe('acme'); + + // Assert foobar models are inaccessible in acme context + expect(Post::count())->toBe(1); +}); + +test('the model returned by the tenant helper has unique and exists validation rules', function () { + Schema::table('posts', function (Blueprint $table) { + $table->string('slug')->nullable(); + $table->unique(['tenant_id', 'slug']); + }); + + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); + + Post::create(['text' => 'Foo', 'slug' => 'foo']); + $data = ['text' => 'Foo 2', 'slug' => 'foo']; + + $uniqueFails = Validator::make($data, [ + 'slug' => 'unique:posts', + ])->fails(); + $existsFails = Validator::make($data, [ + 'slug' => 'exists:posts', + ])->fails(); + + // Assert that 'unique' and 'exists' aren't scoped by default + // $this->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' + // $this->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' + + $uniqueFails = Validator::make($data, [ + 'slug' => tenant()->unique('posts'), + ])->fails(); + $existsFails = Validator::make($data, [ + 'slug' => tenant()->exists('posts'), + ])->fails(); + + // Assert that tenant()->unique() and tenant()->exists() are scoped + expect($uniqueFails)->toBeTrue(); + expect($existsFails)->toBeFalse(); +}); + +// todo@tests +function primaryModelsScopedToCurrentTenant() { - public function setUp(): void - { - parent::setUp(); + // acme context + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); - BelongsToTenant::$tenantIdColumn = 'tenant_id'; + $post = Post::create(['text' => 'Foo']); - Schema::create('posts', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); + expect($post->tenant_id)->toBe('acme'); + expect($post->tenant->id)->toBe('acme'); - $table->string('tenant_id'); + $post = Post::first(); - $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); - }); + expect($post->tenant_id)->toBe('acme'); + expect($post->tenant->id)->toBe('acme'); - Schema::create('comments', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); + // ====================================== + // foobar context + tenancy()->initialize($foobar = Tenant::create([ + 'id' => 'foobar', + ])); - $table->unsignedInteger('post_id'); + $post = Post::create(['text' => 'Bar']); - $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade'); - }); + expect($post->tenant_id)->toBe('foobar'); + expect($post->tenant->id)->toBe('foobar'); - config(['tenancy.tenant_model' => Tenant::class]); - } + $post = Post::first(); - /** @test */ - public function primary_models_are_scoped_to_the_current_tenant() - { - // acme context - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); + expect($post->tenant_id)->toBe('foobar'); + expect($post->tenant->id)->toBe('foobar'); - $post = Post::create(['text' => 'Foo']); + // ====================================== + // acme context again - $this->assertSame('acme', $post->tenant_id); - $this->assertSame('acme', $post->tenant->id); + tenancy()->initialize($acme); - $post = Post::first(); + $post = Post::first(); + expect($post->tenant_id)->toBe('acme'); + expect($post->tenant->id)->toBe('acme'); - $this->assertSame('acme', $post->tenant_id); - $this->assertSame('acme', $post->tenant->id); + // Assert foobar models are inaccessible in acme context + expect(Post::count())->toBe(1); +} - // ====================================== - // foobar context - tenancy()->initialize($foobar = Tenant::create([ - 'id' => 'foobar', - ])); +// todo@tests +function secondaryModelsAreScopedToCurrentTenant() +{ + // acme context + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); - $post = Post::create(['text' => 'Bar']); + $post = Post::create(['text' => 'Foo']); + $post->comments()->create(['text' => 'Comment text']); - $this->assertSame('foobar', $post->tenant_id); - $this->assertSame('foobar', $post->tenant->id); + // ================ + // foobar context + tenancy()->initialize($foobar = Tenant::create([ + 'id' => 'foobar', + ])); - $post = Post::first(); + $post = Post::create(['text' => 'Bar']); + $post->comments()->create(['text' => 'Comment text 2']); - $this->assertSame('foobar', $post->tenant_id); - $this->assertSame('foobar', $post->tenant->id); - - // ====================================== - // acme context again - - tenancy()->initialize($acme); - - $post = Post::first(); - $this->assertSame('acme', $post->tenant_id); - $this->assertSame('acme', $post->tenant->id); - - // Assert foobar models are inaccessible in acme context - $this->assertSame(1, Post::count()); - } - - /** @test */ - public function primary_models_are_not_scoped_in_the_central_context() - { - $this->primary_models_are_scoped_to_the_current_tenant(); - - tenancy()->end(); - - $this->assertSame(2, Post::count()); - } - - /** @test */ - public function secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model() - { - // acme context - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); - - $post = Post::create(['text' => 'Foo']); - $post->comments()->create(['text' => 'Comment text']); - - // ================ - // foobar context - tenancy()->initialize($foobar = Tenant::create([ - 'id' => 'foobar', - ])); - - $post = Post::create(['text' => 'Bar']); - $post->comments()->create(['text' => 'Comment text 2']); - - // ================ - // acme context again - tenancy()->initialize($acme); - $this->assertSame(1, Post::count()); - $this->assertSame(1, Post::first()->comments->count()); - } - - /** @test */ - public function secondary_models_are_NOT_scoped_to_the_current_tenant_when_accessed_directly() - { - $this->secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model(); - - // We're in acme context - $this->assertSame('acme', tenant('id')); - - $this->assertSame(2, Comment::count()); - } - - /** @test */ - public function secondary_models_ARE_scoped_to_the_current_tenant_when_accessed_directly_AND_PARENT_RELATIONSHIP_TRAIT_IS_USED() - { - $acme = Tenant::create([ - 'id' => 'acme', - ]); - - $acme->run(function () { - $post = Post::create(['text' => 'Foo']); - $post->scoped_comments()->create(['text' => 'Comment Text']); - - $this->assertSame(1, Post::count()); - $this->assertSame(1, ScopedComment::count()); - }); - - $foobar = Tenant::create([ - 'id' => 'foobar', - ]); - - $foobar->run(function () { - $this->assertSame(0, Post::count()); - $this->assertSame(0, ScopedComment::count()); - - $post = Post::create(['text' => 'Bar']); - $post->scoped_comments()->create(['text' => 'Comment Text 2']); - - $this->assertSame(1, Post::count()); - $this->assertSame(1, ScopedComment::count()); - }); - - // Global context - $this->assertSame(2, ScopedComment::count()); - } - - /** @test */ - public function secondary_models_are_NOT_scoped_in_the_central_context() - { - $this->secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model(); - - tenancy()->end(); - - $this->assertSame(2, Comment::count()); - } - - /** @test */ - public function global_models_are_not_scoped_at_all() - { - Schema::create('global_resources', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); - }); - - GlobalResource::create(['text' => 'First']); - GlobalResource::create(['text' => 'Second']); - - $acme = Tenant::create([ - 'id' => 'acme', - ]); - - $acme->run(function () { - $this->assertSame(2, GlobalResource::count()); - - GlobalResource::create(['text' => 'Third']); - GlobalResource::create(['text' => 'Fourth']); - }); - - $this->assertSame(4, GlobalResource::count()); - } - - /** @test */ - public function tenant_id_and_relationship_is_auto_added_when_creating_primary_resources_in_tenant_context() - { - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); - - $post = Post::create(['text' => 'Foo']); - - $this->assertSame('acme', $post->tenant_id); - $this->assertTrue($post->relationLoaded('tenant')); - $this->assertSame($acme, $post->tenant); - $this->assertSame(tenant(), $post->tenant); - } - - /** @test */ - public function tenant_id_is_not_auto_added_when_creating_primary_resources_in_central_context() - { - $this->expectException(QueryException::class); - - Post::create(['text' => 'Foo']); - } - - /** @test */ - public function tenant_id_column_name_can_be_customized() - { - BelongsToTenant::$tenantIdColumn = 'team_id'; - - Schema::drop('comments'); - Schema::drop('posts'); - Schema::create('posts', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); - - $table->string('team_id'); - - $table->foreign('team_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); - }); - - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); - - $post = Post::create(['text' => 'Foo']); - - $this->assertSame('acme', $post->team_id); - - // ====================================== - // foobar context - tenancy()->initialize($foobar = Tenant::create([ - 'id' => 'foobar', - ])); - - $post = Post::create(['text' => 'Bar']); - - $this->assertSame('foobar', $post->team_id); - - $post = Post::first(); - - $this->assertSame('foobar', $post->team_id); - - // ====================================== - // acme context again - - tenancy()->initialize($acme); - - $post = Post::first(); - $this->assertSame('acme', $post->team_id); - - // Assert foobar models are inaccessible in acme context - $this->assertSame(1, Post::count()); - } - - /** @test */ - public function the_model_returned_by_the_tenant_helper_has_unique_and_exists_validation_rules() - { - Schema::table('posts', function (Blueprint $table) { - $table->string('slug')->nullable(); - $table->unique(['tenant_id', 'slug']); - }); - - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); - - Post::create(['text' => 'Foo', 'slug' => 'foo']); - $data = ['text' => 'Foo 2', 'slug' => 'foo']; - - $uniqueFails = Validator::make($data, [ - 'slug' => 'unique:posts', - ])->fails(); - $existsFails = Validator::make($data, [ - 'slug' => 'exists:posts', - ])->fails(); - - // Assert that 'unique' and 'exists' aren't scoped by default - // $this->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' - // $this->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' - - $uniqueFails = Validator::make($data, [ - 'slug' => tenant()->unique('posts'), - ])->fails(); - $existsFails = Validator::make($data, [ - 'slug' => tenant()->exists('posts'), - ])->fails(); - - // Assert that tenant()->unique() and tenant()->exists() are scoped - $this->assertTrue($uniqueFails); - $this->assertFalse($existsFails); - } + // ================ + // acme context again + tenancy()->initialize($acme); + expect(Post::count())->toBe(1); + expect(Post::first()->comments->count())->toBe(1); } class Tenant extends TestTenant diff --git a/tests/SubdomainTest.php b/tests/SubdomainTest.php index 17fbc1b3..6cbe1f05 100644 --- a/tests/SubdomainTest.php +++ b/tests/SubdomainTest.php @@ -2,151 +2,129 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Database\Concerns\HasDomains; -use Stancl\Tenancy\Database\Models; use Stancl\Tenancy\Exceptions\NotASubdomainException; use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; +use Stancl\Tenancy\Database\Models; -class SubdomainTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + // Global state cleanup after some tests + InitializeTenancyBySubdomain::$onFail = null; - // Global state cleanup after some tests - InitializeTenancyBySubdomain::$onFail = null; - - Route::group([ - 'middleware' => InitializeTenancyBySubdomain::class, - ], function () { - Route::get('/foo/{a}/{b}', function ($a, $b) { - return "$a + $b"; - }); + Route::group([ + 'middleware' => InitializeTenancyBySubdomain::class, + ], function () { + Route::get('/foo/{a}/{b}', function ($a, $b) { + return "$a + $b"; }); + }); - config(['tenancy.tenant_model' => SubdomainTenant::class]); - } + config(['tenancy.tenant_model' => SubdomainTenant::class]); +}); - /** @test */ - public function tenant_can_be_identified_by_subdomain() - { - $tenant = SubdomainTenant::create([ - 'id' => 'acme', - ]); +test('tenant can be identified by subdomain', function () { + $tenant = SubdomainTenant::create([ + 'id' => 'acme', + ]); - $tenant->domains()->create([ - 'domain' => 'foo', - ]); + $tenant->domains()->create([ + 'domain' => 'foo', + ]); - $this->assertFalse(tenancy()->initialized); + expect(tenancy()->initialized)->toBeFalse(); - $this - ->get('http://foo.localhost/foo/abc/xyz') - ->assertSee('abc + xyz'); + $this + ->get('http://foo.localhost/foo/abc/xyz') + ->assertSee('abc + xyz'); - $this->assertTrue(tenancy()->initialized); - $this->assertSame('acme', tenant('id')); - } + expect(tenancy()->initialized)->toBeTrue(); + expect(tenant('id'))->toBe('acme'); +}); - /** @test */ - public function onfail_logic_can_be_customized() - { - InitializeTenancyBySubdomain::$onFail = function () { - return 'foo'; - }; +test('onfail logic can be customized', function () { + InitializeTenancyBySubdomain::$onFail = function () { + return 'foo'; + }; - $this - ->get('http://foo.localhost/foo/abc/xyz') - ->assertSee('foo'); - } + $this + ->get('http://foo.localhost/foo/abc/xyz') + ->assertSee('foo'); +}); - /** @test */ - public function localhost_is_not_a_valid_subdomain() - { - $this->expectException(NotASubdomainException::class); +test('localhost is not a valid subdomain', function () { + $this->expectException(NotASubdomainException::class); - $this - ->withoutExceptionHandling() - ->get('http://localhost/foo/abc/xyz'); - } + $this + ->withoutExceptionHandling() + ->get('http://localhost/foo/abc/xyz'); +}); - /** @test */ - public function ip_address_is_not_a_valid_subdomain() - { - $this->expectException(NotASubdomainException::class); +test('ip address is not a valid subdomain', function () { + $this->expectException(NotASubdomainException::class); - $this - ->withoutExceptionHandling() - ->get('http://127.0.0.1/foo/abc/xyz'); - } + $this + ->withoutExceptionHandling() + ->get('http://127.0.0.1/foo/abc/xyz'); +}); - /** @test */ - public function oninvalidsubdomain_logic_can_be_customized() - { - // in this case, we need to return a response instance - // since a string would be treated as the subdomain - InitializeTenancyBySubdomain::$onFail = function ($e) { - if ($e instanceof NotASubdomainException) { - return response('foo custom invalid subdomain handler'); - } +test('oninvalidsubdomain logic can be customized', function () { + // in this case, we need to return a response instance + // since a string would be treated as the subdomain + InitializeTenancyBySubdomain::$onFail = function ($e) { + if ($e instanceof NotASubdomainException) { + return response('foo custom invalid subdomain handler'); + } - throw $e; - }; + throw $e; + }; - $this - ->withoutExceptionHandling() - ->get('http://127.0.0.1/foo/abc/xyz') - ->assertSee('foo custom invalid subdomain handler'); - } + $this + ->withoutExceptionHandling() + ->get('http://127.0.0.1/foo/abc/xyz') + ->assertSee('foo custom invalid subdomain handler'); +}); - /** @test */ - public function we_cant_use_a_subdomain_that_doesnt_belong_to_our_central_domains() - { - config(['tenancy.central_domains' => [ - '127.0.0.1', - // not 'localhost' - ]]); +test('we cant use a subdomain that doesnt belong to our central domains', function () { + config(['tenancy.central_domains' => [ + '127.0.0.1', + // not 'localhost' + ]]); - $tenant = SubdomainTenant::create([ - 'id' => 'acme', - ]); + $tenant = SubdomainTenant::create([ + 'id' => 'acme', + ]); - $tenant->domains()->create([ - 'domain' => 'foo', - ]); + $tenant->domains()->create([ + 'domain' => 'foo', + ]); - $this->expectException(NotASubdomainException::class); + $this->expectException(NotASubdomainException::class); - $this - ->withoutExceptionHandling() - ->get('http://foo.localhost/foo/abc/xyz'); - } + $this + ->withoutExceptionHandling() + ->get('http://foo.localhost/foo/abc/xyz'); +}); - /** @test */ - public function central_domain_is_not_a_subdomain() - { - config(['tenancy.central_domains' => [ - 'localhost', - ]]); +test('central domain is not a subdomain', function () { + config(['tenancy.central_domains' => [ + 'localhost', + ]]); - $tenant = SubdomainTenant::create([ - 'id' => 'acme', - ]); + $tenant = SubdomainTenant::create([ + 'id' => 'acme', + ]); - $tenant->domains()->create([ - 'domain' => 'acme', - ]); + $tenant->domains()->create([ + 'domain' => 'acme', + ]); - $this->expectException(NotASubdomainException::class); + $this->expectException(NotASubdomainException::class); - $this - ->withoutExceptionHandling() - ->get('http://localhost/foo/abc/xyz'); - } -} + $this + ->withoutExceptionHandling() + ->get('http://localhost/foo/abc/xyz'); +}); class SubdomainTenant extends Models\Tenant { diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 77a130b4..93a0d3b3 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; @@ -15,115 +13,94 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Tests\Etc\Tenant; -class TenantAssetTest extends TestCase +beforeEach(function () { + config(['tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class, + ]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); +}); + +afterEach(function () { + // Cleanup + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class; +}); + +test('asset can be accessed using the url returned by the tenant asset helper', function () { + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + $filename = 'testfile' . $this->randomString(10); + Storage::disk('public')->put($filename, 'bar'); + $path = storage_path("app/public/$filename"); + + // response()->file() returns BinaryFileResponse whose content is + // inaccessible via getContent, so ->assertSee() can't be used + expect($path)->toBeFile(); + $response = $this->get(tenant_asset($filename), [ + 'X-Tenant' => $tenant->id, + ]); + + $response->assertSuccessful(); + + $f = fopen($path, 'r'); + $content = fread($f, filesize($path)); + fclose($f); + + expect($content)->toBe('bar'); +}); + +test('asset helper returns a link to tenant asset controller when asset url is null', function () { + config(['app.asset_url' => null]); + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo'])); +}); + +test('asset helper returns a link to an external url when asset url is not null', function () { + config(['app.asset_url' => 'https://an-s3-bucket']); + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + expect(asset('foo'))->toBe("https://an-s3-bucket/tenant{$tenant->id}/foo"); +}); + +test('global asset helper returns the same url regardless of tenancy initialization', function () { + $original = global_asset('foobar'); + expect(global_asset('foobar'))->toBe(asset('foobar')); + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + expect(global_asset('foobar'))->toBe($original); +}); + +test('asset helper tenancy can be disabled', function () { + $original = asset('foo'); + + config([ + 'app.asset_url' => null, + 'tenancy.filesystem.asset_helper_tenancy' => false, + ]); + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + expect(asset('foo'))->toBe($original); +}); + +function getEnvironmentSetUp($app) { - public function getEnvironmentSetUp($app) - { - parent::getEnvironmentSetUp($app); - - $app->booted(function () { - if (file_exists(base_path('routes/tenant.php'))) { - Route::middleware(['web']) - ->namespace($this->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers') - ->group(base_path('routes/tenant.php')); - } - }); - } - - public function setUp(): void - { - parent::setUp(); - - config(['tenancy.bootstrappers' => [ - FilesystemTenancyBootstrapper::class, - ]]); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - } - - public function tearDown(): void - { - parent::tearDown(); - - // Cleanup - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class; - } - - /** @test */ - public function asset_can_be_accessed_using_the_url_returned_by_the_tenant_asset_helper() - { - TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; - - $tenant = Tenant::create(); - tenancy()->initialize($tenant); - - $filename = 'testfile' . $this->randomString(10); - Storage::disk('public')->put($filename, 'bar'); - $path = storage_path("app/public/$filename"); - - // response()->file() returns BinaryFileResponse whose content is - // inaccessible via getContent, so ->assertSee() can't be used - $this->assertFileExists($path); - $response = $this->get(tenant_asset($filename), [ - 'X-Tenant' => $tenant->id, - ]); - - $response->assertSuccessful(); - - $f = fopen($path, 'r'); - $content = fread($f, filesize($path)); - fclose($f); - - $this->assertSame('bar', $content); - } - - /** @test */ - public function asset_helper_returns_a_link_to_TenantAssetController_when_asset_url_is_null() - { - config(['app.asset_url' => null]); - - $tenant = Tenant::create(); - tenancy()->initialize($tenant); - - $this->assertSame(route('stancl.tenancy.asset', ['path' => 'foo']), asset('foo')); - } - - /** @test */ - public function asset_helper_returns_a_link_to_an_external_url_when_asset_url_is_not_null() - { - config(['app.asset_url' => 'https://an-s3-bucket']); - - $tenant = Tenant::create(); - tenancy()->initialize($tenant); - - $this->assertSame("https://an-s3-bucket/tenant{$tenant->id}/foo", asset('foo')); - } - - /** @test */ - public function global_asset_helper_returns_the_same_url_regardless_of_tenancy_initialization() - { - $original = global_asset('foobar'); - $this->assertSame(asset('foobar'), global_asset('foobar')); - - $tenant = Tenant::create(); - tenancy()->initialize($tenant); - - $this->assertSame($original, global_asset('foobar')); - } - - /** @test */ - public function asset_helper_tenancy_can_be_disabled() - { - $original = asset('foo'); - - config([ - 'app.asset_url' => null, - 'tenancy.filesystem.asset_helper_tenancy' => false, - ]); - - $tenant = Tenant::create(); - tenancy()->initialize($tenant); - - $this->assertSame($original, asset('foo')); - } + $app->booted(function () { + if (file_exists(base_path('routes/tenant.php'))) { + Route::middleware(['web']) + ->namespace(test()->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers') + ->group(base_path('routes/tenant.php')); + } + }); } diff --git a/tests/TenantAwareCommandTest.php b/tests/TenantAwareCommandTest.php index b8d75aed..1332ccfd 100644 --- a/tests/TenantAwareCommandTest.php +++ b/tests/TenantAwareCommandTest.php @@ -2,32 +2,25 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Tests\Etc\Tenant; -class TenantAwareCommandTest extends TestCase -{ - /** @test */ - public function commands_run_globally_are_tenant_aware_and_return_valid_exit_code() - { - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant1['id'], $tenant2['id']], - ]); +test('commands run globally are tenant aware and return valid exit code', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + Artisan::call('tenants:migrate', [ + '--tenants' => [$tenant1['id'], $tenant2['id']], + ]); - $this->artisan('user:add') - ->assertExitCode(0); + $this->artisan('user:add') + ->assertExitCode(0); - tenancy()->initialize($tenant1); - $this->assertNotEmpty(DB::table('users')->get()); - tenancy()->end(); + tenancy()->initialize($tenant1); + $this->assertNotEmpty(DB::table('users')->get()); + tenancy()->end(); - tenancy()->initialize($tenant2); - $this->assertNotEmpty(DB::table('users')->get()); - tenancy()->end(); - } -} + tenancy()->initialize($tenant2); + $this->assertNotEmpty(DB::table('users')->get()); + tenancy()->end(); +}); diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 12273c85..5a470972 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -2,13 +2,10 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; -use PDO; use Stancl\JobPipeline\JobPipeline; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Database\DatabaseManager; @@ -27,252 +24,228 @@ use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager; use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager; use Stancl\Tenancy\Tests\Etc\Tenant; -class TenantDatabaseManagerTest extends TestCase +test('databases can be created and deleted', function ($driver, $databaseManager) { + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + config()->set([ + "tenancy.database.managers.$driver" => $databaseManager, + ]); + + $name = 'db' . $this->randomString(); + + $manager = app($databaseManager); + $manager->setConnection($driver); + + expect($manager->databaseExists($name))->toBeFalse(); + + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_connection' => $driver, + ]); + + expect($manager->databaseExists($name))->toBeTrue(); + $manager->deleteDatabase($tenant); + expect($manager->databaseExists($name))->toBeFalse(); +})->with('database_manager_provider'); + +test('dbs can be created when another driver is used for the central db', function () { + expect(config('database.default'))->toBe('central'); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $database = 'db' . $this->randomString(); + + $mysqlmanager = app(MySQLDatabaseManager::class); + $mysqlmanager->setConnection('mysql'); + + expect($mysqlmanager->databaseExists($database))->toBeFalse(); + Tenant::create([ + 'tenancy_db_name' => $database, + 'tenancy_db_connection' => 'mysql', + ]); + + expect($mysqlmanager->databaseExists($database))->toBeTrue(); + + $postgresManager = app(PostgreSQLDatabaseManager::class); + $postgresManager->setConnection('pgsql'); + + $database = 'db' . $this->randomString(); + expect($postgresManager->databaseExists($database))->toBeFalse(); + + Tenant::create([ + 'tenancy_db_name' => $database, + 'tenancy_db_connection' => 'pgsql', + ]); + + expect($postgresManager->databaseExists($database))->toBeTrue(); +}); + +test('the tenant connection is fully removed', function () { + config([ + 'tenancy.boostrappers' => [ + DatabaseTenancyBootstrapper::class, + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + $tenant = Tenant::create(); + + expect(array_keys(app('db')->getConnections()))->toBe(['central']); + $this->assertArrayNotHasKey('tenant', config('database.connections')); + + tenancy()->initialize($tenant); + + createUsersTable(); + + expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant']); + $this->assertArrayHasKey('tenant', config('database.connections')); + + tenancy()->end(); + + expect(array_keys(app('db')->getConnections()))->toBe(['central']); + expect(config('database.connections.tenant'))->toBeNull(); +}); + +test('db name is prefixed with db path when sqlite is used', function () { + if (file_exists(database_path('foodb'))) { + unlink(database_path('foodb')); // cleanup + } + config([ + 'database.connections.fooconn.driver' => 'sqlite', + ]); + + $tenant = Tenant::create([ + 'tenancy_db_name' => 'foodb', + 'tenancy_db_connection' => 'fooconn', + ]); + app(DatabaseManager::class)->createTenantConnection($tenant); + + expect(database_path('foodb'))->toBe(config('database.connections.tenant.database')); +}); + +test('schema manager uses schema to separate tenant dbs', function () { + config([ + 'tenancy.database.managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, + 'tenancy.boostrappers' => [ + DatabaseTenancyBootstrapper::class, + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + + $originalDatabaseName = config(['database.connections.pgsql.database']); + + $tenant = Tenant::create([ + 'tenancy_db_connection' => 'pgsql', + ]); + tenancy()->initialize($tenant); + + $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? + config('database.connections.' . config('database.default') . '.search_path') : + config('database.connections.' . config('database.default') . '.schema'); + + expect($schemaConfig)->toBe($tenant->database()->getName()); + expect(config(['database.connections.pgsql.database']))->toBe($originalDatabaseName); +}); + +test('a tenants database cannot be created when the database already exists', function () { + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $name = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + ]); + + $manager = $tenant->database()->manager(); + expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue(); + + $this->expectException(TenantDatabaseAlreadyExistsException::class); + $tenant2 = Tenant::create([ + 'tenancy_db_name' => $name, + ]); +}); + +test('tenant database can be created on a foreign server', function () { + config([ + 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, + 'database.connections.mysql2' => [ + 'driver' => 'mysql', + 'host' => 'mysql2', // important line + 'port' => 3306, + 'database' => 'main', + 'username' => 'root', + 'password' => 'password', + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $name = 'foo' . Str::random(8); + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_connection' => 'mysql2', + ]); + + /** @var PermissionControlledMySQLDatabaseManager $manager */ + $manager = $tenant->database()->manager(); + + $manager->setConnection('mysql'); + expect($manager->databaseExists($name))->toBeFalse(); + + $manager->setConnection('mysql2'); + expect($manager->databaseExists($name))->toBeTrue(); +}); + +test('path used by sqlite manager can be customized', function () { + $this->markTestIncomplete(); +}); + +// Datasets +dataset('database_manager_provider', [ + ['mysql', MySQLDatabaseManager::class], + ['mysql', PermissionControlledMySQLDatabaseManager::class], + ['sqlite', SQLiteDatabaseManager::class], + ['pgsql', PostgreSQLDatabaseManager::class], + ['pgsql', PostgreSQLSchemaManager::class], + ['sqlsrv', MicrosoftSQLDatabaseManager::class] +]); + +function createUsersTable() { - /** - * @test - * @dataProvider database_manager_provider - */ - public function databases_can_be_created_and_deleted($driver, $databaseManager) - { - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - - config()->set([ - "tenancy.database.managers.$driver" => $databaseManager, - ]); - - $name = 'db' . $this->randomString(); - - $manager = app($databaseManager); - $manager->setConnection($driver); - - $this->assertFalse($manager->databaseExists($name)); - - $tenant = Tenant::create([ - 'tenancy_db_name' => $name, - 'tenancy_db_connection' => $driver, - ]); - - $this->assertTrue($manager->databaseExists($name)); - $manager->deleteDatabase($tenant); - $this->assertFalse($manager->databaseExists($name)); - } - - /** @test */ - public function dbs_can_be_created_when_another_driver_is_used_for_the_central_db() - { - $this->assertSame('central', config('database.default')); - - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - - $database = 'db' . $this->randomString(); - - $mysqlmanager = app(MySQLDatabaseManager::class); - $mysqlmanager->setConnection('mysql'); - - $this->assertFalse($mysqlmanager->databaseExists($database)); - Tenant::create([ - 'tenancy_db_name' => $database, - 'tenancy_db_connection' => 'mysql', - ]); - - $this->assertTrue($mysqlmanager->databaseExists($database)); - - $postgresManager = app(PostgreSQLDatabaseManager::class); - $postgresManager->setConnection('pgsql'); - - $database = 'db' . $this->randomString(); - $this->assertFalse($postgresManager->databaseExists($database)); - - Tenant::create([ - 'tenancy_db_name' => $database, - 'tenancy_db_connection' => 'pgsql', - ]); - - $this->assertTrue($postgresManager->databaseExists($database)); - } - - public function database_manager_provider() - { - return [ - ['mysql', MySQLDatabaseManager::class], - ['mysql', PermissionControlledMySQLDatabaseManager::class], - ['sqlite', SQLiteDatabaseManager::class], - ['pgsql', PostgreSQLDatabaseManager::class], - ['pgsql', PostgreSQLSchemaManager::class], - ['sqlsrv', MicrosoftSQLDatabaseManager::class], - ]; - } - - /** @test */ - public function the_tenant_connection_is_fully_removed() - { - config([ - 'tenancy.boostrappers' => [ - DatabaseTenancyBootstrapper::class, - ], - ]); - - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); - - $tenant = Tenant::create(); - - $this->assertSame(['central'], array_keys(app('db')->getConnections())); - $this->assertArrayNotHasKey('tenant', config('database.connections')); - - tenancy()->initialize($tenant); - - $this->createUsersTable(); - - $this->assertSame(['central', 'tenant'], array_keys(app('db')->getConnections())); - $this->assertArrayHasKey('tenant', config('database.connections')); - - tenancy()->end(); - - $this->assertSame(['central'], array_keys(app('db')->getConnections())); - $this->assertNull(config('database.connections.tenant')); - } - - protected function createUsersTable() - { - Schema::create('users', function (Blueprint $table) { - $table->increments('id'); - $table->string('name'); - $table->string('email')->unique(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - } - - /** @test */ - public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() - { - if (file_exists(database_path('foodb'))) { - unlink(database_path('foodb')); // cleanup - } - config([ - 'database.connections.fooconn.driver' => 'sqlite', - ]); - - $tenant = Tenant::create([ - 'tenancy_db_name' => 'foodb', - 'tenancy_db_connection' => 'fooconn', - ]); - app(DatabaseManager::class)->createTenantConnection($tenant); - - $this->assertSame(config('database.connections.tenant.database'), database_path('foodb')); - } - - /** @test */ - public function schema_manager_uses_schema_to_separate_tenant_dbs() - { - config([ - 'tenancy.database.managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, - 'tenancy.boostrappers' => [ - DatabaseTenancyBootstrapper::class, - ], - ]); - - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - - $originalDatabaseName = config(['database.connections.pgsql.database']); - - $tenant = Tenant::create([ - 'tenancy_db_connection' => 'pgsql', - ]); - tenancy()->initialize($tenant); - - $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? - config('database.connections.' . config('database.default') . '.search_path') : - config('database.connections.' . config('database.default') . '.schema'); - - $this->assertSame($tenant->database()->getName(), $schemaConfig); - $this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database'])); - } - - /** @test */ - public function a_tenants_database_cannot_be_created_when_the_database_already_exists() - { - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - - $name = 'foo' . Str::random(8); - $tenant = Tenant::create([ - 'tenancy_db_name' => $name, - ]); - - $manager = $tenant->database()->manager(); - $this->assertTrue($manager->databaseExists($tenant->database()->getName())); - - $this->expectException(TenantDatabaseAlreadyExistsException::class); - $tenant2 = Tenant::create([ - 'tenancy_db_name' => $name, - ]); - } - - /** @test */ - public function tenant_database_can_be_created_on_a_foreign_server() - { - config([ - 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, - 'database.connections.mysql2' => [ - 'driver' => 'mysql', - 'host' => 'mysql2', // important line - 'port' => 3306, - 'database' => 'main', - 'username' => 'root', - 'password' => 'password', - 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], - ], - ]); - - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); - - $name = 'foo' . Str::random(8); - $tenant = Tenant::create([ - 'tenancy_db_name' => $name, - 'tenancy_db_connection' => 'mysql2', - ]); - - /** @var PermissionControlledMySQLDatabaseManager $manager */ - $manager = $tenant->database()->manager(); - - $manager->setConnection('mysql'); - $this->assertFalse($manager->databaseExists($name)); - - $manager->setConnection('mysql2'); - $this->assertTrue($manager->databaseExists($name)); - } - - /** @test */ - public function path_used_by_sqlite_manager_can_be_customized() - { - $this->markTestIncomplete(); - } + Schema::create('users', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); } diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index 2d46c233..346a208e 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Event; @@ -21,148 +19,127 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\UUIDGenerator; -class TenantModelTest extends TestCase -{ - /** @test */ - public function created_event_is_dispatched() - { - Event::fake([TenantCreated::class]); +test('created event is dispatched', function () { + Event::fake([TenantCreated::class]); - Event::assertNotDispatched(TenantCreated::class); + Event::assertNotDispatched(TenantCreated::class); - Tenant::create(); + Tenant::create(); - Event::assertDispatched(TenantCreated::class); - } + Event::assertDispatched(TenantCreated::class); +}); - /** @test */ - public function current_tenant_can_be_resolved_from_service_container_using_typehint() - { - $tenant = Tenant::create(); +test('current tenant can be resolved from service container using typehint', function () { + $tenant = Tenant::create(); - tenancy()->initialize($tenant); + tenancy()->initialize($tenant); - $this->assertSame($tenant->id, app(Contracts\Tenant::class)->id); + expect(app(Contracts\Tenant::class)->id)->toBe($tenant->id); - tenancy()->end(); + tenancy()->end(); - $this->assertSame(null, app(Contracts\Tenant::class)); - } + expect(app(Contracts\Tenant::class))->toBe(null); +}); - /** @test */ - public function id_is_generated_when_no_id_is_supplied() - { - config(['tenancy.id_generator' => UUIDGenerator::class]); +test('id is generated when no id is supplied', function () { + config(['tenancy.id_generator' => UUIDGenerator::class]); - $this->mock(UUIDGenerator::class, function ($mock) { - return $mock->shouldReceive('generate')->once(); - }); + $this->mock(UUIDGenerator::class, function ($mock) { + return $mock->shouldReceive('generate')->once(); + }); - $tenant = Tenant::create(); + $tenant = Tenant::create(); - $this->assertNotNull($tenant->id); - } + $this->assertNotNull($tenant->id); +}); - /** @test */ - public function autoincrement_ids_are_supported() - { - Schema::drop('domains'); - Schema::table('tenants', function (Blueprint $table) { - $table->bigIncrements('id')->change(); - }); +test('autoincrement ids are supported', function () { + Schema::drop('domains'); + Schema::table('tenants', function (Blueprint $table) { + $table->bigIncrements('id')->change(); + }); - unset(app()[UniqueIdentifierGenerator::class]); + unset(app()[UniqueIdentifierGenerator::class]); - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); - $this->assertSame(1, $tenant1->id); - $this->assertSame(2, $tenant2->id); - } + expect($tenant1->id)->toBe(1); + expect($tenant2->id)->toBe(2); +}); - /** @test */ - public function custom_tenant_model_can_be_used() - { - $tenant = MyTenant::create(); +test('custom tenant model can be used', function () { + $tenant = MyTenant::create(); - tenancy()->initialize($tenant); + tenancy()->initialize($tenant); - $this->assertTrue(tenant() instanceof MyTenant); - } + expect(tenant() instanceof MyTenant)->toBeTrue(); +}); - /** @test */ - public function custom_tenant_model_that_doesnt_extend_vendor_Tenant_model_can_be_used() - { - $tenant = AnotherTenant::create([ - 'id' => 'acme', +test('custom tenant model that doesnt extend vendor tenant model can be used', function () { + $tenant = AnotherTenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant); + + expect(tenant() instanceof AnotherTenant)->toBeTrue(); +}); + +test('tenant can be created even when we are in another tenants context', function () { + config(['tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class, + ]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function ($event) { + return $event->tenant; + })->toListener()); + + $tenant1 = Tenant::create([ + 'id' => 'foo', + 'tenancy_db_name' => 'db' . Str::random(16), + ]); + + tenancy()->initialize($tenant1); + + $tenant2 = Tenant::create([ + 'id' => 'bar', + 'tenancy_db_name' => 'db' . Str::random(16), + ]); + + tenancy()->end(); + + expect(Tenant::count())->toBe(2); +}); + +test('the model uses tenant collection', function () { + Tenant::create(); + Tenant::create(); + + expect(Tenant::count())->toBe(2); + expect(Tenant::all() instanceof TenantCollection)->toBeTrue(); +}); + +test('a command can be run on a collection of tenants', function () { + Tenant::create([ + 'id' => 't1', + 'foo' => 'bar', + ]); + Tenant::create([ + 'id' => 't2', + 'foo' => 'bar', + ]); + + Tenant::all()->runForEach(function ($tenant) { + $tenant->update([ + 'foo' => 'xyz', ]); + }); - tenancy()->initialize($tenant); - - $this->assertTrue(tenant() instanceof AnotherTenant); - } - - /** @test */ - public function tenant_can_be_created_even_when_we_are_in_another_tenants_context() - { - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function ($event) { - return $event->tenant; - })->toListener()); - - $tenant1 = Tenant::create([ - 'id' => 'foo', - 'tenancy_db_name' => 'db' . Str::random(16), - ]); - - tenancy()->initialize($tenant1); - - $tenant2 = Tenant::create([ - 'id' => 'bar', - 'tenancy_db_name' => 'db' . Str::random(16), - ]); - - tenancy()->end(); - - $this->assertSame(2, Tenant::count()); - } - - /** @test */ - public function the_model_uses_TenantCollection() - { - Tenant::create(); - Tenant::create(); - - $this->assertSame(2, Tenant::count()); - $this->assertTrue(Tenant::all() instanceof TenantCollection); - } - - /** @test */ - public function a_command_can_be_run_on_a_collection_of_tenants() - { - Tenant::create([ - 'id' => 't1', - 'foo' => 'bar', - ]); - Tenant::create([ - 'id' => 't2', - 'foo' => 'bar', - ]); - - Tenant::all()->runForEach(function ($tenant) { - $tenant->update([ - 'foo' => 'xyz', - ]); - }); - - $this->assertSame('xyz', Tenant::find('t1')->foo); - $this->assertSame('xyz', Tenant::find('t2')->foo); - } -} + expect(Tenant::find('t1')->foo)->toBe('xyz'); + expect(Tenant::find('t2')->foo)->toBe('xyz'); +}); class MyTenant extends Tenant { diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index b50db84b..bfbe0851 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -2,13 +2,9 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Carbon\Carbon; use Carbon\CarbonInterval; -use Closure; use Illuminate\Auth\SessionGuard; -use Illuminate\Foundation\Auth\User as Authenticable; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; @@ -26,249 +22,234 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Tests\Etc\Tenant; +use Illuminate\Foundation\Auth\User as Authenticable; -class TenantUserImpersonationTest extends TestCase +beforeEach(function () { + $this->artisan('migrate', [ + '--path' => __DIR__ . '/../assets/impersonation-migrations', + '--realpath' => true, + ])->assertExitCode(0); + + config([ + 'tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class, + ], + 'tenancy.features' => [ + UserImpersonation::class, + ], + ]); + + Event::listen( + TenantCreated::class, + JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + + config(['auth.providers.users.model' => ImpersonationUser::class]); +}); + +test('tenant user can be impersonated on a tenant domain', function () { + Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes()); + + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + migrateTenants(); + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + // We try to visit the dashboard directly, before impersonating the user. + $this->get('http://foo.localhost/dashboard') + ->assertRedirect('http://foo.localhost/login'); + + // We impersonate the user + $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); + $this->get('http://foo.localhost/impersonate/' . $token->token) + ->assertRedirect('http://foo.localhost/dashboard'); + + // Now we try to visit the dashboard directly, after impersonating the user. + $this->get('http://foo.localhost/dashboard') + ->assertSuccessful() + ->assertSee('You are logged in as Joe'); +}); + +test('tenant user can be impersonated on a tenant path', function () { + makeLoginRoute(); + + Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false)); + + $tenant = Tenant::create([ + 'id' => 'acme', + 'tenancy_db_name' => 'db' . Str::random(16), + ]); + migrateTenants(); + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + // We try to visit the dashboard directly, before impersonating the user. + $this->get('/acme/dashboard') + ->assertRedirect('/login'); + + // We impersonate the user + $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard'); + $this->get('/acme/impersonate/' . $token->token) + ->assertRedirect('/acme/dashboard'); + + // Now we try to visit the dashboard directly, after impersonating the user. + $this->get('/acme/dashboard') + ->assertSuccessful() + ->assertSee('You are logged in as Joe'); +}); + +test('tokens have a limited ttl', function () { + Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes()); + + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + migrateTenants(); + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + // We impersonate the user + $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); + $token->update([ + 'created_at' => Carbon::now()->subtract(CarbonInterval::make('100s')), + ]); + + $this->followingRedirects() + ->get('http://foo.localhost/impersonate/' . $token->token) + ->assertStatus(403); +}); + +test('tokens are deleted after use', function () { + Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes()); + + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + migrateTenants(); + $user = $tenant->run(function () { + return ImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + // We impersonate the user + $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); + + $this->assertNotNull(ImpersonationToken::find($token->token)); + + $this->followingRedirects() + ->get('http://foo.localhost/impersonate/' . $token->token) + ->assertSuccessful() + ->assertSee('You are logged in as Joe'); + + expect(ImpersonationToken::find($token->token))->toBeNull(); +}); + +test('impersonation works with multiple models and guards', function () { + config([ + 'auth.guards.another' => [ + 'driver' => 'session', + 'provider' => 'another_users', + ], + 'auth.providers.another_users' => [ + 'driver' => 'eloquent', + 'model' => AnotherImpersonationUser::class, + ], + ]); + + Auth::extend('another', function ($app, $name, array $config) { + return new SessionGuard($name, Auth::createUserProvider($config['provider']), session()); + }); + + Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes(true, 'another')); + + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'foo.localhost', + ]); + migrateTenants(); + $user = $tenant->run(function () { + return AnotherImpersonationUser::create([ + 'name' => 'Joe', + 'email' => 'joe@local', + 'password' => bcrypt('secret'), + ]); + }); + + // We try to visit the dashboard directly, before impersonating the user. + $this->get('http://foo.localhost/dashboard') + ->assertRedirect('http://foo.localhost/login'); + + // We impersonate the user + $token = tenancy()->impersonate($tenant, $user->id, '/dashboard', 'another'); + $this->get('http://foo.localhost/impersonate/' . $token->token) + ->assertRedirect('http://foo.localhost/dashboard'); + + // Now we try to visit the dashboard directly, after impersonating the user. + $this->get('http://foo.localhost/dashboard') + ->assertSuccessful() + ->assertSee('You are logged in as Joe'); + + Tenant::first()->run(function () { + expect(auth()->guard('another')->user()->name)->toBe('Joe'); + expect(auth()->guard('web')->user())->toBe(null); + }); +}); + +function migrateTenants() { - protected function migrateTenants() - { - $this->artisan('tenants:migrate')->assertExitCode(0); - } + test()->artisan('tenants:migrate')->assertExitCode(0); +} - public function setUp(): void - { - parent::setUp(); +function makeLoginRoute() +{ + Route::get('/login', function () { + return 'Please log in'; + })->name('login'); +} - $this->artisan('migrate', [ - '--path' => __DIR__ . '/../assets/impersonation-migrations', - '--realpath' => true, - ])->assertExitCode(0); +function getRoutes($loginRoute = true, $authGuard = 'web'): Closure +{ + return function () use ($loginRoute, $authGuard) { + if ($loginRoute) { + test()->makeLoginRoute(); + } - config([ - 'tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ], - 'tenancy.features' => [ - UserImpersonation::class, - ], - ]); + Route::get('/dashboard', function () use ($authGuard) { + return 'You are logged in as ' . auth()->guard($authGuard)->user()->name; + })->middleware('auth:' . $authGuard); - Event::listen( - TenantCreated::class, - JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); - - config(['auth.providers.users.model' => ImpersonationUser::class]); - } - - public function makeLoginRoute() - { - Route::get('/login', function () { - return 'Please log in'; - })->name('login'); - } - - public function getRoutes($loginRoute = true, $authGuard = 'web'): Closure - { - return function () use ($loginRoute, $authGuard) { - if ($loginRoute) { - $this->makeLoginRoute(); - } - - Route::get('/dashboard', function () use ($authGuard) { - return 'You are logged in as ' . auth()->guard($authGuard)->user()->name; - })->middleware('auth:' . $authGuard); - - Route::get('/impersonate/{token}', function ($token) { - return UserImpersonation::makeResponse($token); - }); - }; - } - - /** @test */ - public function tenant_user_can_be_impersonated_on_a_tenant_domain() - { - Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes()); - - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); - $this->migrateTenants(); - $user = $tenant->run(function () { - return ImpersonationUser::create([ - 'name' => 'Joe', - 'email' => 'joe@local', - 'password' => bcrypt('secret'), - ]); + Route::get('/impersonate/{token}', function ($token) { + return UserImpersonation::makeResponse($token); }); - - // We try to visit the dashboard directly, before impersonating the user. - $this->get('http://foo.localhost/dashboard') - ->assertRedirect('http://foo.localhost/login'); - - // We impersonate the user - $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); - $this->get('http://foo.localhost/impersonate/' . $token->token) - ->assertRedirect('http://foo.localhost/dashboard'); - - // Now we try to visit the dashboard directly, after impersonating the user. - $this->get('http://foo.localhost/dashboard') - ->assertSuccessful() - ->assertSee('You are logged in as Joe'); - } - - /** @test */ - public function tenant_user_can_be_impersonated_on_a_tenant_path() - { - $this->makeLoginRoute(); - - Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group($this->getRoutes(false)); - - $tenant = Tenant::create([ - 'id' => 'acme', - 'tenancy_db_name' => 'db' . Str::random(16), - ]); - $this->migrateTenants(); - $user = $tenant->run(function () { - return ImpersonationUser::create([ - 'name' => 'Joe', - 'email' => 'joe@local', - 'password' => bcrypt('secret'), - ]); - }); - - // We try to visit the dashboard directly, before impersonating the user. - $this->get('/acme/dashboard') - ->assertRedirect('/login'); - - // We impersonate the user - $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard'); - $this->get('/acme/impersonate/' . $token->token) - ->assertRedirect('/acme/dashboard'); - - // Now we try to visit the dashboard directly, after impersonating the user. - $this->get('/acme/dashboard') - ->assertSuccessful() - ->assertSee('You are logged in as Joe'); - } - - /** @test */ - public function tokens_have_a_limited_ttl() - { - Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes()); - - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); - $this->migrateTenants(); - $user = $tenant->run(function () { - return ImpersonationUser::create([ - 'name' => 'Joe', - 'email' => 'joe@local', - 'password' => bcrypt('secret'), - ]); - }); - - // We impersonate the user - $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); - $token->update([ - 'created_at' => Carbon::now()->subtract(CarbonInterval::make('100s')), - ]); - - $this->followingRedirects() - ->get('http://foo.localhost/impersonate/' . $token->token) - ->assertStatus(403); - } - - /** @test */ - public function tokens_are_deleted_after_use() - { - Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes()); - - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); - $this->migrateTenants(); - $user = $tenant->run(function () { - return ImpersonationUser::create([ - 'name' => 'Joe', - 'email' => 'joe@local', - 'password' => bcrypt('secret'), - ]); - }); - - // We impersonate the user - $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); - - $this->assertNotNull(ImpersonationToken::find($token->token)); - - $this->followingRedirects() - ->get('http://foo.localhost/impersonate/' . $token->token) - ->assertSuccessful() - ->assertSee('You are logged in as Joe'); - - $this->assertNull(ImpersonationToken::find($token->token)); - } - - /** @test */ - public function impersonation_works_with_multiple_models_and_guards() - { - config([ - 'auth.guards.another' => [ - 'driver' => 'session', - 'provider' => 'another_users', - ], - 'auth.providers.another_users' => [ - 'driver' => 'eloquent', - 'model' => AnotherImpersonationUser::class, - ], - ]); - - Auth::extend('another', function ($app, $name, array $config) { - return new SessionGuard($name, Auth::createUserProvider($config['provider']), session()); - }); - - Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes(true, 'another')); - - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'foo.localhost', - ]); - $this->migrateTenants(); - $user = $tenant->run(function () { - return AnotherImpersonationUser::create([ - 'name' => 'Joe', - 'email' => 'joe@local', - 'password' => bcrypt('secret'), - ]); - }); - - // We try to visit the dashboard directly, before impersonating the user. - $this->get('http://foo.localhost/dashboard') - ->assertRedirect('http://foo.localhost/login'); - - // We impersonate the user - $token = tenancy()->impersonate($tenant, $user->id, '/dashboard', 'another'); - $this->get('http://foo.localhost/impersonate/' . $token->token) - ->assertRedirect('http://foo.localhost/dashboard'); - - // Now we try to visit the dashboard directly, after impersonating the user. - $this->get('http://foo.localhost/dashboard') - ->assertSuccessful() - ->assertSee('You are logged in as Joe'); - - Tenant::first()->run(function () { - $this->assertSame('Joe', auth()->guard('another')->user()->name); - $this->assertSame(null, auth()->guard('web')->user()); - }); - } + }; } class ImpersonationUser extends Authenticable diff --git a/tests/UniversalRouteTest.php b/tests/UniversalRouteTest.php index c0852545..04d053cf 100644 --- a/tests/UniversalRouteTest.php +++ b/tests/UniversalRouteTest.php @@ -2,65 +2,76 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Features\UniversalRoutes; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Tests\Etc\Tenant; -class UniversalRouteTest extends TestCase -{ - public function tearDown(): void - { - InitializeTenancyByDomain::$onFail = null; +afterEach(function () { + InitializeTenancyByDomain::$onFail = null; +}); - parent::tearDown(); - } +test('a route can work in both central and tenant context', function () { + Route::middlewareGroup('universal', []); + config(['tenancy.features' => [UniversalRoutes::class]]); - /** @test */ - public function a_route_can_work_in_both_central_and_tenant_context() - { - Route::middlewareGroup('universal', []); - config(['tenancy.features' => [UniversalRoutes::class]]); + Route::get('/foo', function () { + return tenancy()->initialized + ? 'Tenancy is initialized.' + : 'Tenancy is not initialized.'; + })->middleware(['universal', InitializeTenancyByDomain::class]); - Route::get('/foo', function () { - return tenancy()->initialized - ? 'Tenancy is initialized.' - : 'Tenancy is not initialized.'; - })->middleware(['universal', InitializeTenancyByDomain::class]); + $this->get('http://localhost/foo') + ->assertSuccessful() + ->assertSee('Tenancy is not initialized.'); - $this->get('http://localhost/foo') - ->assertSuccessful() - ->assertSee('Tenancy is not initialized.'); + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + $tenant->domains()->create([ + 'domain' => 'acme.localhost', + ]); - $tenant = Tenant::create([ - 'id' => 'acme', - ]); - $tenant->domains()->create([ - 'domain' => 'acme.localhost', - ]); + $this->get('http://acme.localhost/foo') + ->assertSuccessful() + ->assertSee('Tenancy is initialized.'); +}); - $this->get('http://acme.localhost/foo') - ->assertSuccessful() - ->assertSee('Tenancy is initialized.'); - } +test('making one route universal doesnt make all routes universal', function () { + Route::get('/bar', function () { + return tenant('id'); + })->middleware(InitializeTenancyByDomain::class); - /** @test */ - public function making_one_route_universal_doesnt_make_all_routes_universal() - { - Route::get('/bar', function () { - return tenant('id'); - })->middleware(InitializeTenancyByDomain::class); + Route::middlewareGroup('universal', []); + config(['tenancy.features' => [UniversalRoutes::class]]); - $this->a_route_can_work_in_both_central_and_tenant_context(); - tenancy()->end(); + Route::get('/foo', function () { + return tenancy()->initialized + ? 'Tenancy is initialized.' + : 'Tenancy is not initialized.'; + })->middleware(['universal', InitializeTenancyByDomain::class]); - $this->get('http://localhost/bar') - ->assertStatus(500); + $this->get('http://localhost/foo') + ->assertSuccessful() + ->assertSee('Tenancy is not initialized.'); - $this->get('http://acme.localhost/bar') - ->assertSuccessful() - ->assertSee('acme'); - } -} + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + $tenant->domains()->create([ + 'domain' => 'acme.localhost', + ]); + + $this->get('http://acme.localhost/foo') + ->assertSuccessful() + ->assertSee('Tenancy is initialized.'); + + tenancy()->end(); + + $this->get('http://localhost/bar') + ->assertStatus(500); + + $this->get('http://acme.localhost/bar') + ->assertSuccessful() + ->assertSee('acme'); +}); From 2f3d4b99539e2965b3a344fb4d2e2fd3265bf059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 22 Jul 2022 19:48:56 +0200 Subject: [PATCH 47/51] Allow pest plugin --- composer.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3143175a..68f621bb 100644 --- a/composer.json +++ b/composer.json @@ -61,5 +61,10 @@ "test": "PHP_VERSION=8.0.11 ./test" }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + } } From 05f2a828a19dea5cfd3e74c826ed49393620964a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Fri, 22 Jul 2022 22:29:56 +0200 Subject: [PATCH 48/51] Better M1 DX --- CONTRIBUTING.md | 17 ++++------------- composer.json | 26 ++++++++++++++------------ docker-compose-m1.override.yml | 7 +++++++ 3 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 docker-compose-m1.override.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5a6ec3f..12e5e55b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,23 +2,14 @@ ## Code style -StyleCI will flag code style violations in your pull requests. +php-cs-fixer will fix code style violations in your pull requests. ## Running tests -Run `docker-compose up -d` to start the containers. Then run `./test` to run the tests. +Run `composer docker-up` to start the containers. Then run `composer test` to run the tests. -When you're done testing, run `docker-compose down` to shut down the containers. +When you're done testing, run `composer docker-down` to shut down the containers. ### Docker on M1 -You can add: -```yaml -services: - mysql: - platform: linux/amd64 - mysql2: - platform: linux/amd64 -``` - -to `docker-compose.override.yml` to make `docker-compose up-d` work on M1. +Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1. diff --git a/composer.json b/composer.json index 68f621bb..8aca9ded 100644 --- a/composer.json +++ b/composer.json @@ -11,21 +11,22 @@ "authors": [ { "name": "Samuel Å tancl", - "email": "samuel.stancl@gmail.com" + "email": "samuel@archte.ch" } ], "require": { + "php": "^8.1", "ext-json": "*", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0", + "illuminate/support": "^9.0", "facade/ignition-contracts": "^1.0", - "ramsey/uuid": "^3.7|^4.0", - "stancl/jobpipeline": "dev-master", - "stancl/virtualcolumn": "dev-master" + "ramsey/uuid": "^4.0", + "stancl/jobpipeline": "^1.6", + "stancl/virtualcolumn": "^1.2" }, "require-dev": { - "laravel/framework": "^6.0|^7.0|^8.0|^9.0", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", - "league/flysystem-aws-s3-v3": "^1.0|^3.0", + "laravel/framework": "^9.0", + "orchestra/testbench": "^7.0", + "league/flysystem-aws-s3-v3": "^3.0", "doctrine/dbal": "^2.10", "spatie/valuestore": "^1.2.5", "pestphp/pest": "^1.21" @@ -55,10 +56,11 @@ } }, "scripts": { - "docker-up": "PHP_VERSION=8.0.11 docker-compose up -d", - "docker-down": "PHP_VERSION=8.0.11 docker-compose down", - "docker-rebuild": "PHP_VERSION=8.0.11 docker-compose up -d --no-deps --build", - "test": "PHP_VERSION=8.0.11 ./test" + "docker-up": "PHP_VERSION=8.1 docker-compose up -d", + "docker-down": "PHP_VERSION=8.1 docker-compose down", + "docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build", + "docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml", + "test": "PHP_VERSION=8.1 ./test" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/docker-compose-m1.override.yml b/docker-compose-m1.override.yml new file mode 100644 index 00000000..32e163e6 --- /dev/null +++ b/docker-compose-m1.override.yml @@ -0,0 +1,7 @@ +services: + mysql: + platform: linux/amd64 + mysql2: + platform: linux/amd64 + mssql: + image: mcr.microsoft.com/azure-sql-edge From f9c9d8615f5c073502d31626929d3c2bfa22e7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 23 Jul 2022 01:16:50 +0200 Subject: [PATCH 49/51] Refactor tests to use pest() helper --- tests/AutomaticModeTest.php | 2 +- tests/BootstrapperTest.php | 4 +- tests/CacheManagerTest.php | 10 ++-- tests/CachedTenantResolverTest.php | 8 +-- ...edDomainAndSubdomainIdentificationTest.php | 4 +- tests/CommandsTest.php | 8 +-- tests/DatabaseUsersTest.php | 2 +- tests/DomainTest.php | 8 +-- tests/EventListenerTest.php | 8 +-- tests/Features/RedirectTest.php | 2 +- tests/Features/TenantConfigTest.php | 6 +-- tests/MaintenanceModeTest.php | 6 +-- tests/PathIdentificationTest.php | 14 +++--- tests/Pest.php | 9 +++- tests/QueueTest.php | 50 ++++++++----------- tests/ResourceSyncingTest.php | 12 ++--- tests/ScopeSessionsTest.php | 10 ++-- tests/SingleDatabaseTenancyTest.php | 6 +-- tests/SubdomainTest.php | 12 ++--- tests/TenantAssetTest.php | 6 +-- tests/TenantAwareCommandTest.php | 6 +-- tests/TenantDatabaseManagerTest.php | 16 +++--- tests/TenantModelTest.php | 2 +- tests/TenantUserImpersonationTest.php | 30 +++++------ tests/TestCase.php | 2 +- tests/UniversalRouteTest.php | 12 ++--- 26 files changed, 128 insertions(+), 127 deletions(-) diff --git a/tests/AutomaticModeTest.php b/tests/AutomaticModeTest.php index 7b5d5ded..ab484ccf 100644 --- a/tests/AutomaticModeTest.php +++ b/tests/AutomaticModeTest.php @@ -62,7 +62,7 @@ test('central helper runs callbacks in the central state', function () { test('central helper returns the value from the callback', function () { tenancy()->initialize(Tenant::create()); - $this->assertSame('foo', tenancy()->central(function () { + pest()->assertSame('foo', tenancy()->central(function () { return 'foo'; })); }); diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 929c4e47..8f5407bc 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -42,7 +42,7 @@ test('database data is separated', function () { $tenant1 = Tenant::create(); $tenant2 = Tenant::create(); - $this->artisan('tenants:migrate'); + pest()->artisan('tenants:migrate'); tenancy()->initialize($tenant1); @@ -175,7 +175,7 @@ test('filesystem data is separated', function () { // Check that disk prefixes respect the root_override logic expect(getDiskPrefix('local'))->toBe($expected_storage_path . '/app/'); expect(getDiskPrefix('public'))->toBe($expected_storage_path . '/app/public/'); - $this->assertSame('tenant' . tenant('id') . '/', getDiskPrefix('s3'), '/'); + pest()->assertSame('tenant' . tenant('id') . '/', getDiskPrefix('s3'), '/'); // Check suffixing logic $new_storage_path = storage_path(); diff --git a/tests/CacheManagerTest.php b/tests/CacheManagerTest.php index 7b34a7df..03580fe1 100644 --- a/tests/CacheManagerTest.php +++ b/tests/CacheManagerTest.php @@ -19,7 +19,7 @@ beforeEach(function () { test('default tag is automatically applied', function () { tenancy()->initialize(Tenant::create()); - $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); + pest()->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); }); test('tags are merged when array is passed', function () { @@ -39,14 +39,14 @@ test('tags are merged when string is passed', function () { test('exception is thrown when zero arguments are passed to tags method', function () { tenancy()->initialize(Tenant::create()); - $this->expectException(\Exception::class); + pest()->expectException(\Exception::class); cache()->tags(); }); test('exception is thrown when more than one argument is passed to tags method', function () { tenancy()->initialize(Tenant::create()); - $this->expectException(\Exception::class); + pest()->expectException(\Exception::class); cache()->tags(1, 2); }); @@ -60,7 +60,7 @@ test('tags separate cache well enough', function () { $tenant2 = Tenant::create(); tenancy()->initialize($tenant2); - $this->assertNotSame('bar', cache()->get('foo')); + pest()->assertNotSame('bar', cache()->get('foo')); cache()->put('foo', 'xyz', 1); expect(cache()->get('foo'))->toBe('xyz'); @@ -76,7 +76,7 @@ test('invoking the cache helper works', function () { $tenant2 = Tenant::create(); tenancy()->initialize($tenant2); - $this->assertNotSame('bar', cache('foo')); + pest()->assertNotSame('bar', cache('foo')); cache(['foo' => 'xyz'], 1); expect(cache('foo'))->toBe('xyz'); diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index dad0c010..d71375be 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -32,7 +32,7 @@ test('the underlying resolver is not touched when using the cached resolver', fu expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); DB::flushQueryLog(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); - $this->assertNotEmpty(DB::getQueryLog()); // not empty + pest()->assertNotEmpty(DB::getQueryLog()); // not empty DomainTenantResolver::$shouldCache = true; @@ -63,7 +63,7 @@ test('cache is invalidated when the tenant is updated', function () { DB::flushQueryLog(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); - $this->assertNotEmpty(DB::getQueryLog()); // not empty + pest()->assertNotEmpty(DB::getQueryLog()); // not empty }); test('cache is invalidated when a tenants domain is changed', function () { @@ -87,9 +87,9 @@ test('cache is invalidated when a tenants domain is changed', function () { DB::flushQueryLog(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue(); - $this->assertNotEmpty(DB::getQueryLog()); // not empty + pest()->assertNotEmpty(DB::getQueryLog()); // not empty DB::flushQueryLog(); expect($tenant->is(app(DomainTenantResolver::class)->resolve('bar')))->toBeTrue(); - $this->assertNotEmpty(DB::getQueryLog()); // not empty + pest()->assertNotEmpty(DB::getQueryLog()); // not empty }); diff --git a/tests/CombinedDomainAndSubdomainIdentificationTest.php b/tests/CombinedDomainAndSubdomainIdentificationTest.php index db01ef99..4e3c190b 100644 --- a/tests/CombinedDomainAndSubdomainIdentificationTest.php +++ b/tests/CombinedDomainAndSubdomainIdentificationTest.php @@ -32,7 +32,7 @@ test('tenant can be identified by subdomain', function () { expect(tenancy()->initialized)->toBeFalse(); - $this + pest() ->get('http://foo.localhost/foo/abc/xyz') ->assertSee('abc + xyz'); @@ -53,7 +53,7 @@ test('tenant can be identified by domain', function () { expect(tenancy()->initialized)->toBeFalse(); - $this + pest() ->get('http://foobar.localhost/foo/abc/xyz') ->assertSee('abc + xyz'); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 8ad67538..5172d752 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -46,7 +46,7 @@ test('migrate command doesnt change the db connection', function () { expect(Schema::hasTable('users'))->toBeFalse(); expect($new_connection_name)->toEqual($old_connection_name); - $this->assertNotEquals('tenant', $new_connection_name); + pest()->assertNotEquals('tenant', $new_connection_name); }); test('migrate command works without options', function () { @@ -140,7 +140,7 @@ test('install command works', function () { mkdir($dir, 0777, true); } - $this->artisan('tenancy:install'); + pest()->artisan('tenancy:install'); expect(base_path('routes/tenant.php'))->toBeFile(); expect(base_path('config/tenancy.php'))->toBeFile(); expect(app_path('Providers/TenancyServiceProvider.php'))->toBeFile(); @@ -174,7 +174,7 @@ test('run command with array of tenants works', function () { $tenantId2 = Tenant::create()->getTenantKey(); Artisan::call('tenants:migrate-fresh'); - $this->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'") + pest()->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'") ->expectsOutput('Tenant: ' . $tenantId1) ->expectsOutput('Tenant: ' . $tenantId2); }); @@ -186,7 +186,7 @@ function runCommandWorks(): void Artisan::call('tenants:migrate', ['--tenants' => [$id]]); - test()->artisan("tenants:run foo --tenants=$id --argument='a=foo' --option='b=bar' --option='c=xyz'") + pest()->artisan("tenants:run foo --tenants=$id --argument='a=foo' --option='b=bar' --option='c=xyz'") ->expectsOutput("User's name is Test command") ->expectsOutput('foo') ->expectsOutput('xyz'); diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php index 93ac7ec3..2635c167 100644 --- a/tests/DatabaseUsersTest.php +++ b/tests/DatabaseUsersTest.php @@ -56,7 +56,7 @@ test('a tenants database cannot be created when the user already exists', functi expect($manager->userExists($tenant->database()->getUsername()))->toBeTrue(); expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue(); - $this->expectException(TenantDatabaseUserAlreadyExistsException::class); + pest()->expectException(TenantDatabaseUserAlreadyExistsException::class); Event::fake([DatabaseCreated::class]); $tenant2 = Tenant::create([ diff --git a/tests/DomainTest.php b/tests/DomainTest.php index 907681ff..006faef9 100644 --- a/tests/DomainTest.php +++ b/tests/DomainTest.php @@ -47,14 +47,14 @@ test('a domain can belong to only one tenant', function () { $tenant2 = DomainTenant::create(); - $this->expectException(DomainOccupiedByOtherTenantException::class); + pest()->expectException(DomainOccupiedByOtherTenantException::class); $tenant2->domains()->create([ 'domain' => 'foo.localhost', ]); }); test('an exception is thrown if tenant cannot be identified', function () { - $this->expectException(TenantCouldNotBeIdentifiedOnDomainException::class); + pest()->expectException(TenantCouldNotBeIdentifiedOnDomainException::class); app(DomainTenantResolver::class)->resolve('foo.localhost'); }); @@ -70,7 +70,7 @@ test('tenant can be identified by domain', function () { expect(tenancy()->initialized)->toBeFalse(); - $this + pest() ->get('http://foo.localhost/foo/abc/xyz') ->assertSee('abc + xyz'); @@ -83,7 +83,7 @@ test('onfail logic can be customized', function () { return 'foo'; }; - $this + pest() ->get('http://foo.localhost/foo/abc/xyz') ->assertSee('foo'); }); diff --git a/tests/EventListenerTest.php b/tests/EventListenerTest.php index 66dcedc8..85203f07 100644 --- a/tests/EventListenerTest.php +++ b/tests/EventListenerTest.php @@ -81,7 +81,7 @@ test('ing events can be used to cancel db creation', function () { $tenant = Tenant::create(); dispatch_now(new CreateDatabase($tenant)); - $this->assertFalse($tenant->database()->manager()->databaseExists( + pest()->assertFalse($tenant->database()->manager()->databaseExists( $tenant->database()->getName() )); }); @@ -149,7 +149,7 @@ test('individual job pipelines can terminate while leaving others running', func Tenant::create(); - $this->assertSame([ + pest()->assertSame([ 'P1J1', 'P1J2', 'P2J1', // termminated after this @@ -163,7 +163,7 @@ test('database is not migrated if creation is disabled', function () { JobPipeline::make([ CreateDatabase::class, function () { - $this->fail("The job pipeline didn't exit."); + pest()->fail("The job pipeline didn't exit."); }, MigrateDatabase::class, ])->send(function (TenantCreated $event) { @@ -176,7 +176,7 @@ test('database is not migrated if creation is disabled', function () { 'tenancy_db_name' => 'already_created', ]); - expect($this->hasFailed())->toBeFalse(); + expect(pest()->hasFailed())->toBeFalse(); }); class FooListener extends QueueableListener diff --git a/tests/Features/RedirectTest.php b/tests/Features/RedirectTest.php index 7686867e..7aca2e92 100644 --- a/tests/Features/RedirectTest.php +++ b/tests/Features/RedirectTest.php @@ -22,7 +22,7 @@ test('tenant redirect macro replaces only the hostname', function () { $tenant = Tenant::create(); tenancy()->initialize($tenant); - $this->get('/redirect') + pest()->get('/redirect') ->assertRedirect('http://abcd/foobar'); }); diff --git a/tests/Features/TenantConfigTest.php b/tests/Features/TenantConfigTest.php index 21c92592..35df35ed 100644 --- a/tests/Features/TenantConfigTest.php +++ b/tests/Features/TenantConfigTest.php @@ -37,7 +37,7 @@ test('config is merged and removed', function () { expect(config('services.paypal'))->toBe(['public' => 'foo', 'private' => 'bar']); tenancy()->end(); - $this->assertSame([ + pest()->assertSame([ 'public' => null, 'private' => null, ], config('services.paypal')); @@ -66,14 +66,14 @@ test('the value can be set to multiple config keys', function () { ]); tenancy()->initialize($tenant); - $this->assertSame([ + pest()->assertSame([ 'public1' => 'foo', 'public2' => 'foo', 'private' => 'bar', ], config('services.paypal')); tenancy()->end(); - $this->assertSame([ + pest()->assertSame([ 'public1' => null, 'public2' => null, 'private' => null, diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index dace6c51..770dc5f2 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -19,15 +19,15 @@ test('tenant can be in maintenance mode', function () { 'domain' => 'acme.localhost', ]); - $this->get('http://acme.localhost/foo') + pest()->get('http://acme.localhost/foo') ->assertSuccessful(); tenancy()->end(); // flush stored tenant instance $tenant->putDownForMaintenance(); - $this->expectException(HttpException::class); - $this->withoutExceptionHandling() + pest()->expectException(HttpException::class); + pest()->withoutExceptionHandling() ->get('http://acme.localhost/foo'); }); diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index 4cd793d7..bda0cfcb 100644 --- a/tests/PathIdentificationTest.php +++ b/tests/PathIdentificationTest.php @@ -34,7 +34,7 @@ test('tenant can be identified by path', function () { expect(tenancy()->initialized)->toBeFalse(); - $this->get('/acme/foo/abc/xyz'); + pest()->get('/acme/foo/abc/xyz'); expect(tenancy()->initialized)->toBeTrue(); expect(tenant('id'))->toBe('acme'); @@ -47,7 +47,7 @@ test('route actions dont get the tenant id', function () { expect(tenancy()->initialized)->toBeFalse(); - $this + pest() ->get('/acme/foo/abc/xyz') ->assertContent('abc + xyz'); @@ -56,7 +56,7 @@ test('route actions dont get the tenant id', function () { }); test('exception is thrown when tenant cannot be identified by path', function () { - $this->expectException(TenantCouldNotBeIdentifiedByPathException::class); + pest()->expectException(TenantCouldNotBeIdentifiedByPathException::class); $this ->withoutExceptionHandling() @@ -70,7 +70,7 @@ test('onfail logic can be customized', function () { return 'foo'; }; - $this + pest() ->get('/acme/foo/abc/xyz') ->assertContent('foo'); }); @@ -89,7 +89,7 @@ test('an exception is thrown when the routes first parameter is not tenant', fun 'id' => 'acme', ]); - $this->expectException(RouteIsMissingTenantParameterException::class); + pest()->expectException(RouteIsMissingTenantParameterException::class); $this ->withoutExceptionHandling() @@ -112,12 +112,12 @@ test('tenant parameter name can be customized', function () { 'id' => 'acme', ]); - $this + pest() ->get('/acme/bar/abc/xyz') ->assertContent('abc + xyz'); // Parameter for resolver is changed, so the /{tenant}/foo route will no longer work. - $this->expectException(RouteIsMissingTenantParameterException::class); + pest()->expectException(RouteIsMissingTenantParameterException::class); $this ->withoutExceptionHandling() diff --git a/tests/Pest.php b/tests/Pest.php index 9325cf53..d7ca8c22 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,3 +1,10 @@ in(__DIR__); +use Stancl\Tenancy\Tests\TestCase; + +uses(TestCase::class)->in(__DIR__); + +function pest(): TestCase +{ + return Pest\TestSuite::getInstance()->test; +} diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 1e662645..938af39f 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -7,7 +7,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Str; use Spatie\Valuestore\Valuestore; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Tests\Etc\User; @@ -43,7 +42,7 @@ beforeEach(function () { }); afterEach(function () { - $this->valuestore->flush(); + pest()->valuestore->flush(); }); test('tenant id is passed to tenant queues', function () { @@ -55,7 +54,7 @@ test('tenant id is passed to tenant queues', function () { Event::fake([JobProcessing::class, JobProcessed::class]); - dispatch(new TestJob($this->valuestore)); + dispatch(new TestJob(pest()->valuestore)); Event::assertDispatched(JobProcessing::class, function ($event) { return $event->job->payload()['tenant_id'] === tenant('id'); @@ -74,7 +73,7 @@ test('tenant id is not passed to central queues', function () { 'central' => true, ]]); - dispatch(new TestJob($this->valuestore))->onConnection('central'); + dispatch(new TestJob(pest()->valuestore))->onConnection('central'); Event::assertDispatched(JobProcessing::class, function ($event) { return ! isset($event->job->payload()['tenant_id']); @@ -93,21 +92,21 @@ test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) { $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); - $this->valuestore->put('userName', 'Bar'); + pest()->valuestore->put('userName', 'Bar'); - dispatch(new TestJob($this->valuestore, $user)); + dispatch(new TestJob(pest()->valuestore, $user)); - expect($this->valuestore->has('tenant_id'))->toBeFalse(); + expect(pest()->valuestore->has('tenant_id'))->toBeFalse(); if ($shouldEndTenancy) { tenancy()->end(); } - $this->artisan('queue:work --once'); + pest()->artisan('queue:work --once'); expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0); - expect($this->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); + expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); $tenant->run(function () use ($user) { expect($user->fresh()->name)->toBe('Bar'); @@ -115,10 +114,6 @@ test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) { })->with([true, false]);; test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy) { - if (! Str::startsWith(app()->version(), '8')) { - $this->markTestSkipped('queue:retry tenancy is only supported in Laravel 8'); - } - withFailedJobs(); withTenantDatabases(); @@ -130,28 +125,28 @@ test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenan $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); - $this->valuestore->put('userName', 'Bar'); - $this->valuestore->put('shouldFail', true); + pest()->valuestore->put('userName', 'Bar'); + pest()->valuestore->put('shouldFail', true); - dispatch(new TestJob($this->valuestore, $user)); + dispatch(new TestJob(pest()->valuestore, $user)); - expect($this->valuestore->has('tenant_id'))->toBeFalse(); + expect(pest()->valuestore->has('tenant_id'))->toBeFalse(); if ($shouldEndTenancy) { tenancy()->end(); } - $this->artisan('queue:work --once'); + pest()->artisan('queue:work --once'); expect(DB::connection('central')->table('failed_jobs')->count())->toBe(1); - expect($this->valuestore->get('tenant_id'))->toBeNull(); // job failed + expect(pest()->valuestore->get('tenant_id'))->toBeNull(); // job failed - $this->artisan('queue:retry all'); - $this->artisan('queue:work --once'); + pest()->artisan('queue:retry all'); + pest()->artisan('queue:work --once'); expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0); - expect($this->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); // job succeeded + expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); // job succeeded $tenant->run(function () use ($user) { expect($user->fresh()->name)->toBe('Bar'); @@ -165,7 +160,7 @@ test('the tenant used by the job doesnt change when the current tenant changes', tenancy()->initialize($tenant1); - dispatch(new TestJob($this->valuestore)); + dispatch(new TestJob(pest()->valuestore)); $tenant2 = Tenant::create([ 'id' => 'foobar', @@ -173,10 +168,10 @@ test('the tenant used by the job doesnt change when the current tenant changes', tenancy()->initialize($tenant2); - expect($this->valuestore->has('tenant_id'))->toBeFalse(); - $this->artisan('queue:work --once'); + expect(pest()->valuestore->has('tenant_id'))->toBeFalse(); + pest()->artisan('queue:work --once'); - expect($this->valuestore->get('tenant_id'))->toBe('The current tenant id is: acme'); + expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: acme'); }); function createValueStore(): void @@ -192,7 +187,7 @@ function createValueStore(): void file_put_contents($valueStorePath, ''); } - test()->valuestore = Valuestore::make($valueStorePath)->flush(); + pest()->valuestore = Valuestore::make($valueStorePath)->flush(); } function withFailedJobs() @@ -262,4 +257,3 @@ class TestJob implements ShouldQueue } } } - diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php index 99b41f23..806e8706 100644 --- a/tests/ResourceSyncingTest.php +++ b/tests/ResourceSyncingTest.php @@ -47,7 +47,7 @@ beforeEach(function () { UpdateSyncedResource::$shouldQueue = false; // global state cleanup Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class); - test()->artisan('migrate', [ + pest()->artisan('migrate', [ '--path' => [ __DIR__ . '/Etc/synced_resource_migrations', __DIR__ . '/Etc/synced_resource_migrations/users', @@ -104,7 +104,7 @@ test('only the synced columns are updated in the central db', function () { ]); // Assert new values - $this->assertEquals([ + pest()->assertEquals([ 'id' => 1, 'global_id' => 'acme', 'name' => 'John Foo', @@ -116,7 +116,7 @@ test('only the synced columns are updated in the central db', function () { tenancy()->end(); // Assert changes bubbled up - $this->assertEquals([ + pest()->assertEquals([ 'id' => 1, 'global_id' => 'acme', 'name' => 'John Foo', // synced @@ -136,7 +136,7 @@ test('trying to update synced resources from central context using tenant models tenancy()->end(); expect(tenancy()->initialized)->toBeFalse(); - $this->expectException(ModelNotSyncMasterException::class); + pest()->expectException(ModelNotSyncMasterException::class); ResourceUser::first()->update(['role' => 'foobar']); }); @@ -338,7 +338,7 @@ test('global id is generated using id generator when its not supplied', function 'role' => 'employee', ]); - $this->assertNotNull($user->global_id); + pest()->assertNotNull($user->global_id); }); test('when the resource doesnt exist in the tenant db non synced columns will cascade too', function () { @@ -539,7 +539,7 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase() function migrateTenantsResource() { - test()->artisan('tenants:migrate', [ + pest()->artisan('tenants:migrate', [ '--path' => __DIR__ . '/Etc/synced_resource_migrations/users', '--realpath' => true, ])->assertExitCode(0); diff --git a/tests/ScopeSessionsTest.php b/tests/ScopeSessionsTest.php index b1b6a05e..27fa911f 100644 --- a/tests/ScopeSessionsTest.php +++ b/tests/ScopeSessionsTest.php @@ -35,7 +35,7 @@ test('tenant id is auto added to session if its missing', function () { 'id' => 'acme', ]); - $this->get('http://acme.localhost/foo') + pest()->get('http://acme.localhost/foo') ->assertSessionHas(ScopeSessions::$tenantIdKey, 'acme'); }); @@ -44,12 +44,12 @@ test('changing tenant id in session will abort the request', function () { 'id' => 'acme', ]); - $this->get('http://acme.localhost/foo') + pest()->get('http://acme.localhost/foo') ->assertSuccessful(); session()->put(ScopeSessions::$tenantIdKey, 'foobar'); - $this->get('http://acme.localhost/foo') + pest()->get('http://acme.localhost/foo') ->assertStatus(403); }); @@ -62,6 +62,6 @@ test('an exception is thrown when the middleware is executed before tenancy is i 'id' => 'acme', ]); - $this->expectException(TenancyNotInitializedException::class); - $this->withoutExceptionHandling()->get('http://acme.localhost/bar'); + pest()->expectException(TenancyNotInitializedException::class); + pest()->withoutExceptionHandling()->get('http://acme.localhost/bar'); }); diff --git a/tests/SingleDatabaseTenancyTest.php b/tests/SingleDatabaseTenancyTest.php index 83807d14..34b12383 100644 --- a/tests/SingleDatabaseTenancyTest.php +++ b/tests/SingleDatabaseTenancyTest.php @@ -138,7 +138,7 @@ test('tenant id and relationship is auto added when creating primary resources i }); test('tenant id is not auto added when creating primary resources in central context', function () { - $this->expectException(QueryException::class); + pest()->expectException(QueryException::class); Post::create(['text' => 'Foo']); }); @@ -212,8 +212,8 @@ test('the model returned by the tenant helper has unique and exists validation r ])->fails(); // Assert that 'unique' and 'exists' aren't scoped by default - // $this->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' - // $this->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' + // pest()->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' + // pest()->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists' $uniqueFails = Validator::make($data, [ 'slug' => tenant()->unique('posts'), diff --git a/tests/SubdomainTest.php b/tests/SubdomainTest.php index 6cbe1f05..00096d8c 100644 --- a/tests/SubdomainTest.php +++ b/tests/SubdomainTest.php @@ -34,7 +34,7 @@ test('tenant can be identified by subdomain', function () { expect(tenancy()->initialized)->toBeFalse(); - $this + pest() ->get('http://foo.localhost/foo/abc/xyz') ->assertSee('abc + xyz'); @@ -47,13 +47,13 @@ test('onfail logic can be customized', function () { return 'foo'; }; - $this + pest() ->get('http://foo.localhost/foo/abc/xyz') ->assertSee('foo'); }); test('localhost is not a valid subdomain', function () { - $this->expectException(NotASubdomainException::class); + pest()->expectException(NotASubdomainException::class); $this ->withoutExceptionHandling() @@ -61,7 +61,7 @@ test('localhost is not a valid subdomain', function () { }); test('ip address is not a valid subdomain', function () { - $this->expectException(NotASubdomainException::class); + pest()->expectException(NotASubdomainException::class); $this ->withoutExceptionHandling() @@ -99,7 +99,7 @@ test('we cant use a subdomain that doesnt belong to our central domains', functi 'domain' => 'foo', ]); - $this->expectException(NotASubdomainException::class); + pest()->expectException(NotASubdomainException::class); $this ->withoutExceptionHandling() @@ -119,7 +119,7 @@ test('central domain is not a subdomain', function () { 'domain' => 'acme', ]); - $this->expectException(NotASubdomainException::class); + pest()->expectException(NotASubdomainException::class); $this ->withoutExceptionHandling() diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 93a0d3b3..2c5000f1 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -32,14 +32,14 @@ test('asset can be accessed using the url returned by the tenant asset helper', $tenant = Tenant::create(); tenancy()->initialize($tenant); - $filename = 'testfile' . $this->randomString(10); + $filename = 'testfile' . pest()->randomString(10); Storage::disk('public')->put($filename, 'bar'); $path = storage_path("app/public/$filename"); // response()->file() returns BinaryFileResponse whose content is // inaccessible via getContent, so ->assertSee() can't be used expect($path)->toBeFile(); - $response = $this->get(tenant_asset($filename), [ + $response = pest()->get(tenant_asset($filename), [ 'X-Tenant' => $tenant->id, ]); @@ -99,7 +99,7 @@ function getEnvironmentSetUp($app) $app->booted(function () { if (file_exists(base_path('routes/tenant.php'))) { Route::middleware(['web']) - ->namespace(test()->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers') + ->namespace(pest()->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers') ->group(base_path('routes/tenant.php')); } }); diff --git a/tests/TenantAwareCommandTest.php b/tests/TenantAwareCommandTest.php index 1332ccfd..fe49685e 100644 --- a/tests/TenantAwareCommandTest.php +++ b/tests/TenantAwareCommandTest.php @@ -13,14 +13,14 @@ test('commands run globally are tenant aware and return valid exit code', functi '--tenants' => [$tenant1['id'], $tenant2['id']], ]); - $this->artisan('user:add') + pest()->artisan('user:add') ->assertExitCode(0); tenancy()->initialize($tenant1); - $this->assertNotEmpty(DB::table('users')->get()); + pest()->assertNotEmpty(DB::table('users')->get()); tenancy()->end(); tenancy()->initialize($tenant2); - $this->assertNotEmpty(DB::table('users')->get()); + pest()->assertNotEmpty(DB::table('users')->get()); tenancy()->end(); }); diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 5a470972..bc09e888 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -33,7 +33,7 @@ test('databases can be created and deleted', function ($driver, $databaseManager "tenancy.database.managers.$driver" => $databaseManager, ]); - $name = 'db' . $this->randomString(); + $name = 'db' . pest()->randomString(); $manager = app($databaseManager); $manager->setConnection($driver); @@ -57,7 +57,7 @@ test('dbs can be created when another driver is used for the central db', functi return $event->tenant; })->toListener()); - $database = 'db' . $this->randomString(); + $database = 'db' . pest()->randomString(); $mysqlmanager = app(MySQLDatabaseManager::class); $mysqlmanager->setConnection('mysql'); @@ -73,7 +73,7 @@ test('dbs can be created when another driver is used for the central db', functi $postgresManager = app(PostgreSQLDatabaseManager::class); $postgresManager->setConnection('pgsql'); - $database = 'db' . $this->randomString(); + $database = 'db' . pest()->randomString(); expect($postgresManager->databaseExists($database))->toBeFalse(); Tenant::create([ @@ -101,14 +101,14 @@ test('the tenant connection is fully removed', function () { $tenant = Tenant::create(); expect(array_keys(app('db')->getConnections()))->toBe(['central']); - $this->assertArrayNotHasKey('tenant', config('database.connections')); + pest()->assertArrayNotHasKey('tenant', config('database.connections')); tenancy()->initialize($tenant); createUsersTable(); expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant']); - $this->assertArrayHasKey('tenant', config('database.connections')); + pest()->assertArrayHasKey('tenant', config('database.connections')); tenancy()->end(); @@ -154,7 +154,7 @@ test('schema manager uses schema to separate tenant dbs', function () { ]); tenancy()->initialize($tenant); - $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? + $schemaConfig = version_compare(app()->version(), '9.0', '>=') ? config('database.connections.' . config('database.default') . '.search_path') : config('database.connections.' . config('database.default') . '.schema'); @@ -175,7 +175,7 @@ test('a tenants database cannot be created when the database already exists', fu $manager = $tenant->database()->manager(); expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue(); - $this->expectException(TenantDatabaseAlreadyExistsException::class); + pest()->expectException(TenantDatabaseAlreadyExistsException::class); $tenant2 = Tenant::create([ 'tenancy_db_name' => $name, ]); @@ -225,7 +225,7 @@ test('tenant database can be created on a foreign server', function () { }); test('path used by sqlite manager can be customized', function () { - $this->markTestIncomplete(); + pest()->markTestIncomplete(); }); // Datasets diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index 346a208e..d50c9b6b 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -50,7 +50,7 @@ test('id is generated when no id is supplied', function () { $tenant = Tenant::create(); - $this->assertNotNull($tenant->id); + pest()->assertNotNull($tenant->id); }); test('autoincrement ids are supported', function () { diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index bfbe0851..65aa380d 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -25,7 +25,7 @@ use Stancl\Tenancy\Tests\Etc\Tenant; use Illuminate\Foundation\Auth\User as Authenticable; beforeEach(function () { - $this->artisan('migrate', [ + pest()->artisan('migrate', [ '--path' => __DIR__ . '/../assets/impersonation-migrations', '--realpath' => true, ])->assertExitCode(0); @@ -69,16 +69,16 @@ test('tenant user can be impersonated on a tenant domain', function () { }); // We try to visit the dashboard directly, before impersonating the user. - $this->get('http://foo.localhost/dashboard') + pest()->get('http://foo.localhost/dashboard') ->assertRedirect('http://foo.localhost/login'); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); - $this->get('http://foo.localhost/impersonate/' . $token->token) + pest()->get('http://foo.localhost/impersonate/' . $token->token) ->assertRedirect('http://foo.localhost/dashboard'); // Now we try to visit the dashboard directly, after impersonating the user. - $this->get('http://foo.localhost/dashboard') + pest()->get('http://foo.localhost/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); }); @@ -102,16 +102,16 @@ test('tenant user can be impersonated on a tenant path', function () { }); // We try to visit the dashboard directly, before impersonating the user. - $this->get('/acme/dashboard') + pest()->get('/acme/dashboard') ->assertRedirect('/login'); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard'); - $this->get('/acme/impersonate/' . $token->token) + pest()->get('/acme/impersonate/' . $token->token) ->assertRedirect('/acme/dashboard'); // Now we try to visit the dashboard directly, after impersonating the user. - $this->get('/acme/dashboard') + pest()->get('/acme/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); }); @@ -138,7 +138,7 @@ test('tokens have a limited ttl', function () { 'created_at' => Carbon::now()->subtract(CarbonInterval::make('100s')), ]); - $this->followingRedirects() + pest()->followingRedirects() ->get('http://foo.localhost/impersonate/' . $token->token) ->assertStatus(403); }); @@ -162,9 +162,9 @@ test('tokens are deleted after use', function () { // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); - $this->assertNotNull(ImpersonationToken::find($token->token)); + pest()->assertNotNull(ImpersonationToken::find($token->token)); - $this->followingRedirects() + pest()->followingRedirects() ->get('http://foo.localhost/impersonate/' . $token->token) ->assertSuccessful() ->assertSee('You are logged in as Joe'); @@ -204,16 +204,16 @@ test('impersonation works with multiple models and guards', function () { }); // We try to visit the dashboard directly, before impersonating the user. - $this->get('http://foo.localhost/dashboard') + pest()->get('http://foo.localhost/dashboard') ->assertRedirect('http://foo.localhost/login'); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/dashboard', 'another'); - $this->get('http://foo.localhost/impersonate/' . $token->token) + pest()->get('http://foo.localhost/impersonate/' . $token->token) ->assertRedirect('http://foo.localhost/dashboard'); // Now we try to visit the dashboard directly, after impersonating the user. - $this->get('http://foo.localhost/dashboard') + pest()->get('http://foo.localhost/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); @@ -225,7 +225,7 @@ test('impersonation works with multiple models and guards', function () { function migrateTenants() { - test()->artisan('tenants:migrate')->assertExitCode(0); + pest()->artisan('tenants:migrate')->assertExitCode(0); } function makeLoginRoute() @@ -239,7 +239,7 @@ function getRoutes($loginRoute = true, $authGuard = 'web'): Closure { return function () use ($loginRoute, $authGuard) { if ($loginRoute) { - test()->makeLoginRoute(); + makeLoginRoute(); } Route::get('/dashboard', function () use ($authGuard) { diff --git a/tests/TestCase.php b/tests/TestCase.php index 75fe51fd..554aeb8d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,7 +23,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase Redis::connection('cache')->flushdb(); file_put_contents(database_path('central.sqlite'), ''); - $this->artisan('migrate:fresh', [ + pest()->artisan('migrate:fresh', [ '--force' => true, '--path' => __DIR__ . '/../assets/migrations', '--realpath' => true, diff --git a/tests/UniversalRouteTest.php b/tests/UniversalRouteTest.php index 04d053cf..20723cca 100644 --- a/tests/UniversalRouteTest.php +++ b/tests/UniversalRouteTest.php @@ -21,7 +21,7 @@ test('a route can work in both central and tenant context', function () { : 'Tenancy is not initialized.'; })->middleware(['universal', InitializeTenancyByDomain::class]); - $this->get('http://localhost/foo') + pest()->get('http://localhost/foo') ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); @@ -32,7 +32,7 @@ test('a route can work in both central and tenant context', function () { 'domain' => 'acme.localhost', ]); - $this->get('http://acme.localhost/foo') + pest()->get('http://acme.localhost/foo') ->assertSuccessful() ->assertSee('Tenancy is initialized.'); }); @@ -51,7 +51,7 @@ test('making one route universal doesnt make all routes universal', function () : 'Tenancy is not initialized.'; })->middleware(['universal', InitializeTenancyByDomain::class]); - $this->get('http://localhost/foo') + pest()->get('http://localhost/foo') ->assertSuccessful() ->assertSee('Tenancy is not initialized.'); @@ -62,16 +62,16 @@ test('making one route universal doesnt make all routes universal', function () 'domain' => 'acme.localhost', ]); - $this->get('http://acme.localhost/foo') + pest()->get('http://acme.localhost/foo') ->assertSuccessful() ->assertSee('Tenancy is initialized.'); tenancy()->end(); - $this->get('http://localhost/bar') + pest()->get('http://localhost/bar') ->assertStatus(500); - $this->get('http://acme.localhost/bar') + pest()->get('http://acme.localhost/bar') ->assertSuccessful() ->assertSee('acme'); }); From 233a1222bf72769c28edcb7558fef7bf3ef216a0 Mon Sep 17 00:00:00 2001 From: beezerk23 <84435010+beezerk23@users.noreply.github.com> Date: Mon, 25 Jul 2022 18:37:52 +0200 Subject: [PATCH 50/51] =?UTF-8?q?feat(UniversalRoutes):=20Stop=20overwriti?= =?UTF-8?q?ng=20the=20(maybe)=20customized=20onFail=E2=80=A6=20(#679)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(UniversalRoutes): Stop overwriting the (maybe) customized onFail method and just call it in case of an exception * throw correct exception when `$originalOnFail()` is null * Update DomainTest.php * convert test to pest and renamed * Update tests/DomainTest.php Co-authored-by: Samuel Å tancl Co-authored-by: Abrar Ahmad Co-authored-by: Samuel Å tancl --- src/Features/UniversalRoutes.php | 8 +++++++- tests/DomainTest.php | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Features/UniversalRoutes.php b/src/Features/UniversalRoutes.php index 6b729962..c73a5304 100644 --- a/src/Features/UniversalRoutes.php +++ b/src/Features/UniversalRoutes.php @@ -23,11 +23,17 @@ class UniversalRoutes implements Feature public function bootstrap(Tenancy $tenancy): void { foreach (static::$identificationMiddlewares as $middleware) { - $middleware::$onFail = function ($exception, $request, $next) { + $originalOnFail = $middleware::$onFail; + + $middleware::$onFail = function ($exception, $request, $next) use ($originalOnFail) { if (static::routeHasMiddleware($request->route(), static::$middlewareGroup)) { return $next($request); } + if ($originalOnFail) { + return $originalOnFail($exception, $request, $next); + } + throw $exception; }; } diff --git a/tests/DomainTest.php b/tests/DomainTest.php index 006faef9..594270e1 100644 --- a/tests/DomainTest.php +++ b/tests/DomainTest.php @@ -8,6 +8,7 @@ use Stancl\Tenancy\Database\Models; use Stancl\Tenancy\Database\Models\Domain; use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; +use Stancl\Tenancy\Features\UniversalRoutes; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Resolvers\DomainTenantResolver; @@ -88,6 +89,17 @@ test('onfail logic can be customized', function () { ->assertSee('foo'); }); +test('throw correct exception when onFail is null and universal routes are enabled', function () { + // un-define onFail logic + InitializeTenancyByDomain::$onFail = null; + + // Enable UniversalRoute feature + Route::middlewareGroup('universal', []); + config(['tenancy.features' => [UniversalRoutes::class]]); + + $this->withoutExceptionHandling()->get('http://foo.localhost/foo/abc/xyz'); +})->throws(TenantCouldNotBeIdentifiedOnDomainException::class);; + test('domains are always lowercase', function () { $tenant = DomainTenant::create(); From 29634dda846c41e7dc398197ed4302f4f883c45c Mon Sep 17 00:00:00 2001 From: Abrar Ahmad Date: Wed, 27 Jul 2022 04:35:55 +0500 Subject: [PATCH 51/51] added `$this->mockConsoleOutput` (#907) --- tests/BootstrapperTest.php | 2 ++ tests/QueueTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 8f5407bc..96afbc83 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -23,6 +23,8 @@ use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; beforeEach(function () { + $this->mockConsoleOutput = false; + Event::listen( TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 938af39f..c1fa24b8 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -27,6 +27,8 @@ use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; beforeEach(function () { + $this->mockConsoleOutput = false; + config([ 'tenancy.bootstrappers' => [ QueueTenancyBootstrapper::class,