diff --git a/tests/AutomaticModeTest.php b/tests/AutomaticModeTest.php index 714092c3..c34a5dbd 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,124 +10,104 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\Tenant; -class AutomaticModeTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +beforeEach(function () { + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); +}); + +test('context is switched when tenancy is initialized', function () { + config(['tenancy.bootstrappers' => [ + MyBootstrapper::class, + ]]); + + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant); + + $this->assertSame('acme', app('tenancy_initialized_for_tenant')); +}); + +test('context is reverted when tenancy is ended', function () { + $this->context_is_switched_when_tenancy_is_initialized(); + + tenancy()->end(); + + $this->assertSame(true, app('tenancy_ended')); +}); + +test('context is switched when tenancy is reinitialized', function () { + 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('central helper runs callbacks in the central state', function () { + tenancy()->initialize($tenant = Tenant::create()); + + tenancy()->central(function () { + $this->assertSame(null, tenant()); + }); + + $this->assertSame($tenant, 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 () { + // + }); + + $this->assertSame($tenant, tenant()); +}); + +test('central helper doesnt change tenancy state when called in central context', function () { + $this->assertFalse(tenancy()->initialized); + $this->assertNull(tenant()); + + tenancy()->central(function () { + // + }); + + $this->assertFalse(tenancy()->initialized); + $this->assertNull(tenant()); +}); + +// Helpers +function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant) { - public function setUp(): void - { - parent::setUp(); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - Event::listen(TenancyEnded::class, RevertToCentralContext::class); - } - - /** @test */ - public function context_is_switched_when_tenancy_is_initialized() - { - config(['tenancy.bootstrappers' => [ - MyBootstrapper::class, - ]]); - - $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()); - } + app()->instance('tenancy_initialized_for_tenant', $tenant->getTenantKey()); } -class MyBootstrapper implements TenancyBootstrapper +function revert() { - public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant) - { - app()->instance('tenancy_initialized_for_tenant', $tenant->getTenantKey()); - } - - public function revert() - { - app()->instance('tenancy_ended', true); - } + app()->instance('tenancy_ended', true); } diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php index 588fadd8..f0420614 100644 --- a/tests/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Filesystem\FilesystemAdapter; use ReflectionObject; use ReflectionProperty; @@ -26,202 +24,187 @@ use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; -class BootstrapperTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +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']); + $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('cache data is separated', function () { + 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('redis data is separated', function () { + 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('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'); + $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/', getDiskPrefix('local')); + $this->assertSame($expected_storage_path . '/app/public/', getDiskPrefix('public')); + $this->assertSame('tenant' . tenant('id') . '/', getDiskPrefix('s3'), '/'); + + // Check suffixing logic + $new_storage_path = storage_path(); + $this->assertEquals($expected_storage_path, $new_storage_path); +}); + +// Helpers +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..563fa6c9 100644 --- a/tests/CacheManagerTest.php +++ b/tests/CacheManagerTest.php @@ -2,136 +2,112 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - config(['tenancy.bootstrappers' => [ - CacheTenancyBootstrapper::class, - ]]); +beforeEach(function () { + config(['tenancy.bootstrappers' => [ + CacheTenancyBootstrapper::class, + ]]); - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - } + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); +}); - /** @test */ - public function default_tag_is_automatically_applied() - { - tenancy()->initialize(Tenant::create()); +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()); - } + $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); +}); - /** @test */ - public function tags_are_merged_when_array_is_passed() - { - tenancy()->initialize(Tenant::create()); +test('tags are merged when array is passed', function () { + tenancy()->initialize(Tenant::create()); - $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; - $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); - } + $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; + $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); +}); - /** @test */ - public function tags_are_merged_when_string_is_passed() - { - tenancy()->initialize(Tenant::create()); +test('tags are merged when string is passed', function () { + tenancy()->initialize(Tenant::create()); - $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo']; - $this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames()); - } + $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo']; + $this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames()); +}); - /** @test */ - public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method() - { - tenancy()->initialize(Tenant::create()); +test('exception is thrown when zero arguments are passed to tags method', function () { + tenancy()->initialize(Tenant::create()); - $this->expectException(\Exception::class); - cache()->tags(); - } + $this->expectException(\Exception::class); + cache()->tags(); +}); - /** @test */ - public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method() - { - tenancy()->initialize(Tenant::create()); +test('exception is thrown when more than one argument is passed to tags method', function () { + tenancy()->initialize(Tenant::create()); - $this->expectException(\Exception::class); - cache()->tags(1, 2); - } + $this->expectException(\Exception::class); + cache()->tags(1, 2); +}); - /** @test */ - public function tags_separate_cache_well_enough() - { - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); +test('tags separate cache well enough', function () { + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); - cache()->put('foo', 'bar', 1); - $this->assertSame('bar', cache()->get('foo')); + 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); - $this->assertSame('xyz', cache()->get('foo')); - } + cache()->put('foo', 'xyz', 1); + $this->assertSame('xyz', cache()->get('foo')); +}); - /** @test */ - public function invoking_the_cache_helper_works() - { - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); +test('invoking the cache helper works', function () { + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); - cache(['foo' => 'bar'], 1); - $this->assertSame('bar', cache('foo')); + 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); - $this->assertSame('xyz', cache('foo')); - } + cache(['foo' => 'xyz'], 1); + $this->assertSame('xyz', cache('foo')); +}); - /** @test */ - public function cache_is_persisted() - { - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); +test('cache is persisted', function () { + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); - cache(['foo' => 'bar'], 10); - $this->assertSame('bar', cache('foo')); + cache(['foo' => 'bar'], 10); + $this->assertSame('bar', cache('foo')); - tenancy()->end(); + tenancy()->end(); - tenancy()->initialize($tenant1); - $this->assertSame('bar', cache('foo')); - } + tenancy()->initialize($tenant1); + $this->assertSame('bar', cache('foo')); +}); - /** @test */ - public function cache_is_persisted_when_reidentification_is_used() - { - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - tenancy()->initialize($tenant1); +test('cache is persisted when reidentification is used', function () { + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant1); - cache(['foo' => 'bar'], 10); - $this->assertSame('bar', cache('foo')); + 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); + $this->assertSame('bar', cache('foo')); +}); diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index e7eb52d3..8ac93657 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -2,111 +2,97 @@ 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; +uses(Stancl\Tenancy\Tests\TestCase::class); - parent::tearDown(); - } +afterEach(function () { + DomainTenantResolver::$shouldCache = false; +}); - /** @test */ - public function tenants_can_be_resolved_using_the_cached_resolver() - { - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'acme', - ]); +test('tenants can be resolved using the cached resolver', function () { + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'acme', + ]); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); - } + $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); + $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); +}); - /** @test */ - public function the_underlying_resolver_is_not_touched_when_using_the_cached_resolver() - { - $tenant = Tenant::create(); - $tenant->domains()->create([ - 'domain' => 'acme', - ]); +test('the underlying resolver is not touched when using the cached resolver', function () { + $tenant = Tenant::create(); + $tenant->domains()->create([ + 'domain' => 'acme', + ]); - DB::enableQueryLog(); + DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = false; + DomainTenantResolver::$shouldCache = false; - $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 + $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; - $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 - } + $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 */ - public function cache_is_invalidated_when_the_tenant_is_updated() - { - $tenant = Tenant::create(); - $tenant->createDomain([ - 'domain' => 'acme', - ]); +test('cache is invalidated when the tenant is updated', function () { + $tenant = Tenant::create(); + $tenant->createDomain([ + 'domain' => 'acme', + ]); - DB::enableQueryLog(); + DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = true; + DomainTenantResolver::$shouldCache = true; - $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 + $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(); - $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('acme'))); + $this->assertNotEmpty(DB::getQueryLog()); // not empty +}); - /** @test */ - public function cache_is_invalidated_when_a_tenants_domain_is_changed() - { - $tenant = Tenant::create(); - $tenant->createDomain([ - 'domain' => 'acme', - ]); +test('cache is invalidated when a tenants domain is changed', function () { + $tenant = Tenant::create(); + $tenant->createDomain([ + 'domain' => 'acme', + ]); - DB::enableQueryLog(); + DB::enableQueryLog(); - DomainTenantResolver::$shouldCache = true; + DomainTenantResolver::$shouldCache = true; - $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 + $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(); - $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('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(); + $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('bar'))); + $this->assertNotEmpty(DB::getQueryLog()); // not empty +}); diff --git a/tests/CombinedDomainAndSubdomainIdentificationTest.php b/tests/CombinedDomainAndSubdomainIdentificationTest.php index 6712458c..9fea8649 100644 --- a/tests/CombinedDomainAndSubdomainIdentificationTest.php +++ b/tests/CombinedDomainAndSubdomainIdentificationTest.php @@ -2,78 +2,63 @@ 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; -class CombinedDomainAndSubdomainIdentificationTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +uses(Stancl\Tenancy\Tests\TestCase::class); - 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); + $this->assertFalse(tenancy()->initialized); - $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')); - } + $this->assertTrue(tenancy()->initialized); + $this->assertSame('acme', tenant('id')); +}); - /** @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); + $this->assertFalse(tenancy()->initialized); - $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')); - } -} - -class CombinedTenant extends Models\Tenant -{ - use HasDomains; -} + $this->assertTrue(tenancy()->initialized); + $this->assertSame('acme', tenant('id')); +}); diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 145a93c5..cf722e52 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,186 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Tests\Etc\ExampleSeeder; use Stancl\Tenancy\Tests\Etc\Tenant; -class CommandsTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +uses(Stancl\Tenancy\Tests\TestCase::class); - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); +beforeEach(function () { + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); + config(['tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::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); +}); + +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 () { + $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('migrate command works without options', function () { + $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('migrate command works with tenants option', function () { + $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('migrate command loads schema state', function () { + $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('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"'); + $this->assertFileExists('tests/Etc/tenant-schema-test.dump'); +}); + +test('rollback command works', function () { + $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('seed command works', function () { + $this->markTestIncomplete(); +}); + +test('database connection is switched to default', function () { + $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('database connection is switched to default when tenancy has been initialized', function () { + tenancy()->initialize(Tenant::create()); + + $this->database_connection_is_switched_to_default(); +}); + +test('run commands works', function () { + $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('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); } - public function tearDown(): void - { - parent::tearDown(); + $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')); +}); - // Cleanup tenancy config cache - if (file_exists(base_path('config/tenancy.php'))) { - unlink(base_path('config/tenancy.php')); - } - } +test('migrate fresh command works', function () { + $tenant = Tenant::create(); - /** @test */ - public function migrate_command_doesnt_change_the_db_connection() - { - $this->assertFalse(Schema::hasTable('users')); + $this->assertFalse(Schema::hasTable('users')); + Artisan::call('tenants:migrate-fresh'); + $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(); + tenancy()->initialize($tenant); - $this->assertFalse(Schema::hasTable('users')); - $this->assertEquals($old_connection_name, $new_connection_name); - $this->assertNotEquals('tenant', $new_connection_name); - } + $this->assertTrue(Schema::hasTable('users')); - /** @test */ - public function migrate_command_works_without_options() - { - $tenant = Tenant::create(); + $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()); - $this->assertFalse(Schema::hasTable('users')); - Artisan::call('tenants:migrate'); - $this->assertFalse(Schema::hasTable('users')); + // test that db is wiped + Artisan::call('tenants:migrate-fresh'); + $this->assertFalse(DB::table('users')->exists()); +}); - tenancy()->initialize($tenant); +test('run command with array of tenants works', function () { + $tenantId1 = Tenant::create()->getTenantKey(); + $tenantId2 = Tenant::create()->getTenantKey(); + Artisan::call('tenants:migrate-fresh'); - $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); - } -} + $this->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'") + ->expectsOutput('Tenant: ' . $tenantId1) + ->expectsOutput('Tenant: ' . $tenantId2); +}); diff --git a/tests/DatabasePreparationTest.php b/tests/DatabasePreparationTest.php index 12d30059..ada0899f 100644 --- a/tests/DatabasePreparationTest.php +++ b/tests/DatabasePreparationTest.php @@ -2,8 +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; @@ -17,123 +15,102 @@ use Stancl\Tenancy\Jobs\SeedDatabase; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\Tests\Etc\Tenant; -class DatabasePreparationTest extends TestCase -{ - /** @test */ - public function database_can_be_created_after_tenant_creation() - { - config(['tenancy.database.template_tenant_connection' => 'mysql']); +uses(Stancl\Tenancy\Tests\TestCase::class); - Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); +test('database can be created after tenant creation', function () { + config(['tenancy.database.template_tenant_connection' => 'mysql']); - $tenant = Tenant::create(); + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - $manager = app(MySQLDatabaseManager::class); - $manager->setConnection('mysql'); + $tenant = Tenant::create(); - $this->assertTrue($manager->databaseExists($tenant->database()->getName())); - } + $manager = app(MySQLDatabaseManager::class); + $manager->setConnection('mysql'); - /** @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()); + $this->assertTrue($manager->databaseExists($tenant->database()->getName())); +}); - $tenant = Tenant::create(); +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->run(function () { - $this->assertTrue(Schema::hasTable('users')); - }); - } + $tenant = Tenant::create(); - /** @test */ - public function database_can_be_seeded_after_tenant_creation() - { - config(['tenancy.seeder_parameters' => [ - '--class' => TestSeeder::class, - ]]); + $tenant->run(function () { + $this->assertTrue(Schema::hasTable('users')); + }); +}); - Event::listen(TenantCreated::class, JobPipeline::make([ - CreateDatabase::class, - MigrateDatabase::class, - SeedDatabase::class, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); +test('database can be seeded after tenant creation', function () { + config(['tenancy.seeder_parameters' => [ + '--class' => TestSeeder::class, + ]]); - $tenant = Tenant::create(); + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + SeedDatabase::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - $tenant->run(function () { - $this->assertSame('Seeded User', User::first()->name); - }); - } + $tenant = Tenant::create(); - /** @test */ - public function custom_job_can_be_added_to_the_pipeline() - { - config(['tenancy.seeder_parameters' => [ - '--class' => TestSeeder::class, - ]]); + $tenant->run(function () { + $this->assertSame('Seeded User', User::first()->name); + }); +}); - Event::listen(TenantCreated::class, JobPipeline::make([ - CreateDatabase::class, - MigrateDatabase::class, - SeedDatabase::class, - CreateSuperuser::class, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener()); +test('custom job can be added to the pipeline', function () { + config(['tenancy.seeder_parameters' => [ + '--class' => TestSeeder::class, + ]]); - $tenant = Tenant::create(); + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + SeedDatabase::class, + CreateSuperuser::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); - $tenant->run(function () { - $this->assertSame('Foo', User::all()[1]->name); - }); - } -} + $tenant = Tenant::create(); -class User extends Authenticable -{ - protected $guarded = []; -} + $tenant->run(function () { + $this->assertSame('Foo', User::all()[1]->name); + }); +}); -class TestSeeder extends Seeder -{ - /** +// Helpers +/** * Run the database seeds. * * @return void */ - public function run() - { - DB::table('users')->insert([ - 'name' => 'Seeded User', - 'email' => 'seeded@user', - 'password' => bcrypt('password'), - ]); - } -} - -class CreateSuperuser +function run() { - protected $tenant; - - public function __construct(Tenant $tenant) - { - $this->tenant = $tenant; - } - - public function handle() - { - $this->tenant->run(function () { - User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); - }); - } + DB::table('users')->insert([ + 'name' => 'Seeded User', + 'email' => 'seeded@user', + 'password' => bcrypt('password'), + ]); +} + +function __construct(Tenant $tenant) +{ + test()->tenant = $tenant; +} + +function handle() +{ + test()->tenant->run(function () { + User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + }); } diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php index 344239d1..7c400af9 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,99 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - config([ - 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class, - 'tenancy.database.suffix' => '', - 'tenancy.database.template_tenant_connection' => 'mysql', - ]); +beforeEach(function () { + 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 */ - public function users_are_created_when_permission_controlled_mysql_manager_is_used() - { - $tenant = new Tenant([ - 'id' => 'foo' . Str::random(10), - ]); - $tenant->database()->makeCredentials(); +test('users are created when permission controlled mysql manager is used', function () { + $tenant = new Tenant([ + 'id' => 'foo' . Str::random(10), + ]); + $tenant->database()->makeCredentials(); - /** @var ManagesDatabaseUsers $manager */ - $manager = $tenant->database()->manager(); - $this->assertFalse($manager->userExists($tenant->database()->getUsername())); + /** @var ManagesDatabaseUsers $manager */ + $manager = $tenant->database()->manager(); + $this->assertFalse($manager->userExists($tenant->database()->getUsername())); - $tenant->save(); + $tenant->save(); - $this->assertTrue($manager->userExists($tenant->database()->getUsername())); - } + $this->assertTrue($manager->userExists($tenant->database()->getUsername())); +}); - /** @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, - ]); +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, + ]); - /** @var ManagesDatabaseUsers $manager */ - $manager = $tenant->database()->manager(); - $this->assertTrue($manager->userExists($tenant->database()->getUsername())); - $this->assertTrue($manager->databaseExists($tenant->database()->getName())); + /** @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 - $this->assertFalse($manager2->databaseExists($tenant2->database()->getName())); - Event::assertNotDispatched(DatabaseCreated::class); - } + // database was not created because of DB transaction + $this->assertFalse($manager2->databaseExists($tenant2->database()->getName())); + Event::assertNotDispatched(DatabaseCreated::class); +}); - /** @test */ - public function correct_grants_are_given_to_users() - { - PermissionControlledMySQLDatabaseManager::$grants = [ - 'ALTER', 'ALTER ROUTINE', 'CREATE', - ]; +test('correct grants are given to users', function () { + 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]; - $this->assertStringStartsWith('GRANT CREATE, ALTER, ALTER ROUTINE ON', $query->{"Grants for {$user}@%"}); // @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 */ - 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, - ], - ]); +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, + ], + ]); - 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), + ]); - $this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager); + $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')); - } -} + $this->assertTrue($tenant->database()->manager() instanceof PermissionControlledMySQLDatabaseManager); + $this->assertSame('root', config('database.connections.tenant.username')); +}); diff --git a/tests/DomainTest.php b/tests/DomainTest.php index 9c1bac28..cc201b0a 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,112 +11,91 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - 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()); - } + $this->assertSame($id, $resolvedTenant->id); + $this->assertSame(['foo.localhost'], $resolvedTenant->domains->pluck('domain')->toArray()); +}); - /** @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); + $this->assertFalse(tenancy()->initialized); - $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')); - } + $this->assertTrue(tenancy()->initialized); + $this->assertSame('acme', tenant('id')); +}); - /** @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); - } -} - -class DomainTenant extends Models\Tenant -{ - use HasDomains; -} + $this->assertSame('capitals', Domain::first()->domain); +}); diff --git a/tests/EventListenerTest.php b/tests/EventListenerTest.php index 4a45205c..db293330 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; @@ -23,190 +21,169 @@ use Stancl\Tenancy\Listeners\QueueableListener; use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tests\Etc\Tenant; -class EventListenerTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +test('listeners can be synchronous', function () { + Queue::fake(); + Event::listen(TenantCreated::class, FooListener::class); + + Tenant::create(); + + Queue::assertNothingPushed(); + + $this->assertSame('bar', app('foo')); +}); + +test('listeners can be queued by setting a static property', function () { + Queue::fake(); + + Event::listen(TenantCreated::class, FooListener::class); + FooListener::$shouldQueue = true; + + Tenant::create(); + + Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) { + return $job->class === FooListener::class; + }); + + $this->assertFalse(app()->bound('foo')); +}); + +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()); +}); + +test('ing events can be used to cancel domain model actions', function () { + $tenant = Tenant::create(); + + Event::listen(UpdatingDomain::class, function () { + return false; + }); + + $domain = $tenant->domains()->create([ + 'domain' => 'acme', + ]); + + $domain->update([ + 'domain' => 'foo', + ]); + + $this->assertSame('acme', $domain->refresh()->domain); +}); + +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)); + + $this->assertFalse($tenant->database()->manager()->databaseExists( + $tenant->database()->getName() + )); +}); + +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(TenancyInitialized::class, BootstrapTenancy::class); + + Event::listen(BootstrappingTenancy::class, function (BootstrappingTenancy $event) { + $event->tenancy->getBootstrappersUsing = function () { + return [DatabaseTenancyBootstrapper::class]; + }; + }); + + tenancy()->initialize(Tenant::create()); + + $this->assertSame([DatabaseTenancyBootstrapper::class], array_map('get_class', tenancy()->getBootstrappers())); +}); + +test('individual job pipelines can terminate while leaving others running', function () { + $executed = []; + + Event::listen( + TenantCreated::class, + JobPipeline::make([ + function () use (&$executed) { + $executed[] = 'P1J1'; + }, + + function () use (&$executed) { + $executed[] = 'P1J2'; + }, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); + + Event::listen( + TenantCreated::class, + JobPipeline::make([ + function () use (&$executed) { + $executed[] = 'P2J1'; + + return false; + }, + + function () use (&$executed) { + $executed[] = 'P2J2'; + }, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); + + Tenant::create(); + + $this->assertSame([ + 'P1J1', + 'P1J2', + 'P2J1', // termminated after this + // P2J2 was not reached + ], $executed); +}); + +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', + ]); + + $this->assertFalse($this->hasFailed()); +}); + +// Helpers +function handle() { - /** @test */ - public function listeners_can_be_synchronous() - { - Queue::fake(); - Event::listen(TenantCreated::class, FooListener::class); - - Tenant::create(); - - Queue::assertNothingPushed(); - - $this->assertSame('bar', app('foo')); - } - - /** @test */ - public function listeners_can_be_queued_by_setting_a_static_property() - { - Queue::fake(); - - Event::listen(TenantCreated::class, FooListener::class); - FooListener::$shouldQueue = true; - - Tenant::create(); - - Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) { - return $job->class === FooListener::class; - }); - - $this->assertFalse(app()->bound('foo')); - } - - /** @test */ - public function ing_events_can_be_used_to_cancel_tenant_model_actions() - { - Event::listen(CreatingTenant::class, function () { - return false; - }); - - $this->assertSame(false, Tenant::create()->exists); - $this->assertSame(0, Tenant::count()); - } - - /** @test */ - public function ing_events_can_be_used_to_cancel_domain_model_actions() - { - $tenant = Tenant::create(); - - Event::listen(UpdatingDomain::class, function () { - return false; - }); - - $domain = $tenant->domains()->create([ - 'domain' => 'acme', - ]); - - $domain->update([ - 'domain' => 'foo', - ]); - - $this->assertSame('acme', $domain->refresh()->domain); - } - - /** @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); - }); - - $tenant = Tenant::create(); - dispatch_now(new CreateDatabase($tenant)); - - $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, - ]]); - - Event::listen( - TenantCreated::class, - JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - - Event::listen(BootstrappingTenancy::class, function (BootstrappingTenancy $event) { - $event->tenancy->getBootstrappersUsing = function () { - return [DatabaseTenancyBootstrapper::class]; - }; - }); - - tenancy()->initialize(Tenant::create()); - - $this->assertSame([DatabaseTenancyBootstrapper::class], array_map('get_class', tenancy()->getBootstrappers())); - } - - /** @test */ - public function individual_job_pipelines_can_terminate_while_leaving_others_running() - { - $executed = []; - - Event::listen( - TenantCreated::class, - JobPipeline::make([ - function () use (&$executed) { - $executed[] = 'P1J1'; - }, - - function () use (&$executed) { - $executed[] = 'P1J2'; - }, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); - - Event::listen( - TenantCreated::class, - JobPipeline::make([ - function () use (&$executed) { - $executed[] = 'P2J1'; - - return false; - }, - - function () use (&$executed) { - $executed[] = 'P2J2'; - }, - ])->send(function (TenantCreated $event) { - return $event->tenant; - })->toListener() - ); - - Tenant::create(); - - $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() - ); - - Tenant::create([ - 'tenancy_create_database' => false, - 'tenancy_db_name' => 'already_created', - ]); - - $this->assertFalse($this->hasFailed()); - } -} - -class FooListener extends QueueableListener -{ - public static $shouldQueue = false; - - public function handle() - { - app()->instance('foo', 'bar'); - } + app()->instance('foo', 'bar'); } diff --git a/tests/Features/RedirectTest.php b/tests/Features/RedirectTest.php index 4f7f77a1..9c5f6191 100644 --- a/tests/Features/RedirectTest.php +++ b/tests/Features/RedirectTest.php @@ -2,45 +2,38 @@ 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], - ]); +uses(TestCase::class); - Route::get('/foobar', function () { - return 'Foo'; - })->name('home'); +test('tenant redirect macro replaces only the hostname', function () { + config([ + 'tenancy.features' => [CrossDomainRedirect::class], + ]); - Route::get('/redirect', function () { - return redirect()->route('home')->domain('abcd'); - }); + Route::get('/foobar', function () { + return 'Foo'; + })->name('home'); - $tenant = Tenant::create(); - tenancy()->initialize($tenant); + Route::get('/redirect', function () { + return redirect()->route('home')->domain('abcd'); + }); - $this->get('/redirect') - ->assertRedirect('http://abcd/foobar'); - } + $tenant = Tenant::create(); + tenancy()->initialize($tenant); - /** @test */ - public function tenant_route_helper_generates_correct_url() - { - Route::get('/abcdef/{a?}/{b?}', function () { - return 'Foo'; - })->name('foo'); + $this->get('/redirect') + ->assertRedirect('http://abcd/foobar'); +}); - $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', [])); - } -} +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', [])); +}); diff --git a/tests/Features/TenantConfigTest.php b/tests/Features/TenantConfigTest.php index 37e26198..105a48fb 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; @@ -13,82 +11,74 @@ 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 = []; +uses(TestCase::class); - parent::tearDown(); - } +afterEach(function () { + TenantConfig::$storageToConfigMap = []; +}); - /** @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); +test('config is merged and removed', function () { + $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); - $this->assertSame(['public' => 'foo', 'private' => 'bar'], config('services.paypal')); + 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 */ - 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); +test('the value can be set to multiple config keys', function () { + $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..d02c9449 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,43 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - config(['tenancy.bootstrappers' => [ - CacheTenancyBootstrapper::class, - ]]); +beforeEach(function () { + 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 */ - 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')); +test('global cache manager stores data in global cache', function () { + $this->assertSame(null, cache('foo')); + GlobalCache::put(['foo' => 'bar'], 1); + $this->assertSame('bar', GlobalCache::get('foo')); - $tenant1 = Tenant::create(); - tenancy()->initialize($tenant1); - $this->assertSame('bar', GlobalCache::get('foo')); + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); + $this->assertSame('bar', GlobalCache::get('foo')); - GlobalCache::put(['abc' => 'xyz'], 1); - cache(['def' => 'ghi'], 10); - $this->assertSame('ghi', cache('def')); + GlobalCache::put(['abc' => 'xyz'], 1); + cache(['def' => 'ghi'], 10); + $this->assertSame('ghi', cache('def')); - tenancy()->end(); - $this->assertSame('xyz', GlobalCache::get('abc')); - $this->assertSame('bar', GlobalCache::get('foo')); - $this->assertSame(null, cache('def')); + 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); - $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')); + $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); - $this->assertSame('ghi', cache('def')); - } -} + tenancy()->initialize($tenant1); + $this->assertSame('ghi', cache('def')); +}); diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index 4a8d8d0c..6103b905 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -2,8 +2,6 @@ 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; @@ -13,34 +11,26 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Tests\Etc\Tenant; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; -class MaintenanceModeTest extends TestCase -{ - /** @test */ - public function tenant_can_be_in_maintenance_mode() - { - Route::get('/foo', function () { - return 'bar'; - })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]); +uses(Stancl\Tenancy\Tests\TestCase::class); - $tenant = MaintenanceTenant::create(); - $tenant->domains()->create([ - 'domain' => 'acme.localhost', - ]); +test('tenant can be in maintenance mode', function () { + Route::get('/foo', function () { + return 'bar'; + })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]); - $this->get('http://acme.localhost/foo') - ->assertSuccessful(); + $tenant = MaintenanceTenant::create(); + $tenant->domains()->create([ + 'domain' => 'acme.localhost', + ]); - tenancy()->end(); // flush stored tenant instance + $this->get('http://acme.localhost/foo') + ->assertSuccessful(); - $tenant->putDownForMaintenance(); + tenancy()->end(); // flush stored tenant instance - $this->expectException(HttpException::class); - $this->withoutExceptionHandling() - ->get('http://acme.localhost/foo'); - } -} + $tenant->putDownForMaintenance(); -class MaintenanceTenant extends Tenant -{ - use MaintenanceMode; -} + $this->expectException(HttpException::class); + $this->withoutExceptionHandling() + ->get('http://acme.localhost/foo'); +}); diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php index 7a408ed0..65532bad 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,119 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - PathTenantResolver::$tenantParameterName = 'tenant'; +beforeEach(function () { + 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', - ]); + $this->assertFalse(tenancy()->initialized); - $this->assertFalse(tenancy()->initialized); + $this->get('/acme/foo/abc/xyz'); - $this->get('/acme/foo/abc/xyz'); + $this->assertTrue(tenancy()->initialized); + $this->assertSame('acme', tenant('id')); +}); - $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', - ]); + $this->assertFalse(tenancy()->initialized); - $this->assertFalse(tenancy()->initialized); + $this + ->get('/acme/foo/abc/xyz') + ->assertContent('abc + xyz'); - $this - ->get('/acme/foo/abc/xyz') - ->assertContent('abc + xyz'); + $this->assertTrue(tenancy()->initialized); + $this->assertSame('acme', tenant('id')); +}); - $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'); + $this->assertFalse(tenancy()->initialized); +}); - $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/QueueTest.php b/tests/QueueTest.php index a3df9cd7..96307306 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Closure; use Exception; use Illuminate\Support\Str; @@ -32,270 +30,240 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; -class QueueTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +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']); + }); +}); + +/** + * + * @testWith [true] + * [false] + */ +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)); + + $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); + }); +}); + +/** + * + * @testWith [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)); + + $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('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); + + $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')); +}); + +// Helpers +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(); - }); - } - - 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() - { - 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')); - } + test()->valuestore = Valuestore::make($valueStorePath)->flush(); } -class TestJob implements ShouldQueue +function withFailedJobs() { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + 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(); + }); +} - /** @var Valuestore */ - protected $valuestore; +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(); + }); +} - /** @var User|null */ - protected $user; +function withTenantDatabases() +{ + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); +} - public function __construct(Valuestore $valuestore, User $user = null) - { - $this->valuestore = $valuestore; - $this->user = $user; +function __construct(Valuestore $valuestore, User $user = null) +{ + test()->valuestore = $valuestore; + test()->user = $user; +} + +function handle() +{ + if (test()->valuestore->get('shouldFail')) { + test()->valuestore->put('shouldFail', false); + + throw new Exception('failing'); } - public function handle() - { - if ($this->valuestore->get('shouldFail')) { - $this->valuestore->put('shouldFail', false); + if (test()->user) { + assert(test()->user->getConnectionName() === 'tenant'); + } - throw new Exception('failing'); - } + test()->valuestore->put('tenant_id', 'The current tenant id is: ' . tenant('id')); - 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]); - } + if ($userName = test()->valuestore->get('userName')) { + test()->user->update(['name' => $userName]); } } diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php index 52a502f9..46b880b4 100644 --- a/tests/RequestDataIdentificationTest.php +++ b/tests/RequestDataIdentificationTest.php @@ -2,64 +2,51 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - config([ - 'tenancy.central_domains' => [ - 'localhost', - ], - ]); +beforeEach(function () { + 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'); + }); +}); - public function tearDown(): void - { - InitializeTenancyByRequestData::$header = 'X-Tenant'; - InitializeTenancyByRequestData::$queryParameter = 'tenant'; +afterEach(function () { + InitializeTenancyByRequestData::$header = 'X-Tenant'; + InitializeTenancyByRequestData::$queryParameter = 'tenant'; +}); - parent::tearDown(); - } +test('header identification works', function () { + InitializeTenancyByRequestData::$header = 'X-Tenant'; + $tenant = Tenant::create(); + $tenant2 = Tenant::create(); - /** @test */ - public function header_identification_works() - { - InitializeTenancyByRequestData::$header = 'X-Tenant'; - $tenant = Tenant::create(); - $tenant2 = Tenant::create(); + $this + ->withoutExceptionHandling() + ->get('test', [ + 'X-Tenant' => $tenant->id, + ]) + ->assertSee($tenant->id); +}); - $this - ->withoutExceptionHandling() - ->get('test', [ - 'X-Tenant' => $tenant->id, - ]) - ->assertSee($tenant->id); - } +test('query parameter identification works', function () { + InitializeTenancyByRequestData::$header = null; + InitializeTenancyByRequestData::$queryParameter = 'tenant'; - /** @test */ - public function query_parameter_identification_works() - { - InitializeTenancyByRequestData::$header = null; - InitializeTenancyByRequestData::$queryParameter = 'tenant'; + $tenant = Tenant::create(); + $tenant2 = Tenant::create(); - $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 570448d1..6064f83d 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,633 +28,557 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - config(['tenancy.bootstrappers' => [ - DatabaseTenancyBootstrapper::class, - ]]); +beforeEach(function () { + 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); - $this->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); +}); - protected function migrateTenants() - { - $this->artisan('tenants:migrate', [ - '--path' => __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]); - /** @test */ - public function an_event_is_triggered_when_a_synced_resource_is_changed() - { - Event::fake([SyncedResourceSaved::class]); + $user = ResourceUser::create([ + 'name' => 'Foo', + 'email' => 'foo@email.com', + 'password' => 'secret', + 'global_id' => 'foo', + 'role' => 'foo', + ]); - $user = ResourceUser::create([ - 'name' => 'Foo', - 'email' => 'foo@email.com', - 'password' => 'secret', - 'global_id' => 'foo', - 'role' => 'foo', - ]); + Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) { + return $event->model === $user; + }); +}); - Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) { - return $event->model === $user; - }); - } +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 + ]); - /** @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 - ]); + $tenant = ResourceTenant::create(); + migrateTenants(); - $tenant = ResourceTenant::create(); - $this->migrateTenants(); + tenancy()->initialize($tenant); - tenancy()->initialize($tenant); + // Create the same user in tenant DB + $user = ResourceUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // 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 - ]); + // Update user in tenant DB + $user->update([ + 'name' => 'John Foo', // synced + 'email' => 'john@foreignhost', // synced + 'role' => 'admin', // unsynced + ]); - // Update user in tenant DB - $user->update([ - 'name' => 'John Foo', // synced - 'email' => 'john@foreignhost', // synced - 'role' => 'admin', // unsynced - ]); + // Assert new values + $this->assertEquals([ + 'id' => 1, + 'global_id' => 'acme', + 'name' => 'John Foo', + 'email' => 'john@foreignhost', + 'password' => 'secret', + 'role' => 'admin', + ], $user->getAttributes()); - // Assert new values - $this->assertEquals([ - 'id' => 1, - 'global_id' => 'acme', - 'name' => 'John Foo', - 'email' => 'john@foreignhost', - 'password' => 'secret', - 'role' => 'admin', - ], $user->getAttributes()); + tenancy()->end(); - tenancy()->end(); + // 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()); +}); - // 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('creating the resource in tenant database creates it in central database and creates the mapping', function () { + // Assert no user in central DB + $this->assertCount(0, ResourceUser::all()); - /** @test */ - public function creating_the_resource_in_tenant_database_creates_it_in_central_database_and_creates_the_mapping() - { - // Assert no user in central DB + $tenant = ResourceTenant::create(); + migrateTenants(); + + tenancy()->initialize($tenant); + + // Create the same user in tenant DB + ResourceUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); + + tenancy()->end(); + + // Asset user was created + $this->assertSame('acme', CentralUser::first()->global_id); + $this->assertSame('commenter', CentralUser::first()->role); + + // Assert mapping was created + $this->assertCount(1, CentralUser::first()->tenants); + + // Assert role change doesn't cascade + CentralUser::first()->update(['role' => 'central superadmin']); + tenancy()->initialize($tenant); + $this->assertSame('commenter', ResourceUser::first()->role); +}); + +test('trying to update synced resources from central context using tenant models results in an exception', function () { + $this->creating_the_resource_in_tenant_database_creates_it_in_central_database_and_creates_the_mapping(); + + tenancy()->end(); + $this->assertFalse(tenancy()->initialized); + + $this->expectException(ModelNotSyncMasterException::class); + ResourceUser::first()->update(['role' => 'foobar']); +}); + +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 + ]); + + $tenant = ResourceTenant::create([ + 'id' => 't1', + ]); + migrateTenants(); + + $tenant->run(function () { $this->assertCount(0, ResourceUser::all()); + }); - $tenant = ResourceTenant::create(); - $this->migrateTenants(); + $centralUser->tenants()->attach('t1'); - tenancy()->initialize($tenant); + $tenant->run(function () { + $this->assertCount(1, ResourceUser::all()); + }); +}); - // Create the same user in tenant DB +test('attaching users to tenants d o e s n o t d o a n y t h i n g', function () { + $centralUser = CentralUser::create([ + 'global_id' => 'acme', + 'name' => 'John Doe', + 'email' => 'john@localhost', + 'password' => 'secret', + 'role' => 'commenter', // unsynced + ]); + + $tenant = ResourceTenant::create([ + 'id' => 't1', + ]); + 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('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', + ]); + 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('when a resource exists in other tenant dbs but is c r e a t e d 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', + ]); + 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 Doe', - 'email' => 'john@localhost', + 'name' => 'John Foo', // changed + 'email' => 'john@foo', // changed 'password' => 'secret', - 'role' => 'commenter', // unsynced + '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('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', + ]); + 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 ]); - tenancy()->end(); + $this->assertSame('employee', ResourceUser::first()->role); + }); - // 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); + $this->assertSame('John 3', $user->name); // synced + $this->assertSame('commenter', $user->role); // 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 + $this->assertSame(1, CentralUser::count()); + $centralUser = CentralUser::first(); + $this->assertSame('John 3', $centralUser->name); // synced + $this->assertSame('commenter', $centralUser->role); // 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(); + migrateTenants(); - $tenant->run(function () { - $this->assertCount(0, ResourceUser::all()); - }); + $centralUser->tenants()->attach('t1'); - $centralUser->tenants()->attach('t1'); + $t1->run(function () { + $this->assertSame('employee', ResourceUser::first()->role); + }); +}); - $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 - ]); + migrateTenants(); - $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); - } + $this->assertSame('employee', CentralUser::first()->role); +}); - /** @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', + ]); + + migrateTenants(); + + 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', + ]); + 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->migrateTenants(); + $this->assertSame('employee', ResourceUser::first()->role); + }); - $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; - - $t1 = ResourceTenant::create([ - 'id' => 't1', - ]); - - $this->migrateTenants(); - - Queue::assertNothingPushed(); - - $t1->run(function () { - ResourceUser::create([ - 'name' => 'John Doe', - 'email' => 'john@doe', - 'password' => 'secret', - 'role' => 'employee', - ]); - }); - - Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) { - return $job->class === UpdateSyncedResource::class; - }); - } - - /** @test */ - public function an_event_is_fired_for_all_touched_resources() - { - 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', - ]); - $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; - }); - } -} - -class ResourceTenant extends Tenant +// Helpers +function migrateTenants() { - public function users() - { - return $this->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id') - ->using(TenantPivot::class); - } + test()->artisan('tenants:migrate', [ + '--path' => __DIR__ . '/Etc/synced_resource_migrations/users', + '--realpath' => true, + ])->assertExitCode(0); } -class CentralUser extends Model implements SyncMaster +function users() { - use ResourceSyncing, CentralConnection; - - protected $guarded = []; - public $timestamps = false; - public $table = 'users'; - - public function tenants(): BelongsToMany - { - return $this->belongsToMany(ResourceTenant::class, 'tenant_users', 'global_user_id', 'tenant_id', 'global_id') - ->using(TenantPivot::class); - } - - public function getTenantModelName(): string - { - return ResourceUser::class; - } - - public function getGlobalIdentifierKey() - { - return $this->getAttribute($this->getGlobalIdentifierKeyName()); - } - - public function getGlobalIdentifierKeyName(): string - { - return 'global_id'; - } - - public function getCentralModelName(): string - { - return static::class; - } - - public function getSyncedAttributeNames(): array - { - return [ - 'name', - 'password', - 'email', - ]; - } + return test()->belongsToMany(CentralUser::class, 'tenant_users', 'tenant_id', 'global_user_id', 'id', 'global_id') + ->using(TenantPivot::class); } -class ResourceUser extends Model implements Syncable +function tenants(): BelongsToMany { - use ResourceSyncing; - - protected $table = 'users'; - protected $guarded = []; - public $timestamps = false; - - public function getGlobalIdentifierKey() - { - return $this->getAttribute($this->getGlobalIdentifierKeyName()); - } - - public function getGlobalIdentifierKeyName(): string - { - return 'global_id'; - } - - public function getCentralModelName(): string - { - return CentralUser::class; - } - - public function getSyncedAttributeNames(): array - { - return [ - 'name', - 'password', - 'email', - ]; - } + return test()->belongsToMany(ResourceTenant::class, 'tenant_users', 'global_user_id', 'tenant_id', 'global_id') + ->using(TenantPivot::class); +} + +function getTenantModelName(): string +{ + return ResourceUser::class; +} + +function getGlobalIdentifierKey() +{ + return test()->getAttribute(test()->getGlobalIdentifierKeyName()); +} + +function getGlobalIdentifierKeyName(): string +{ + return 'global_id'; +} + +function getCentralModelName(): string +{ + return CentralUser::class; +} + +function getSyncedAttributeNames(): array +{ + return [ + 'name', + 'password', + 'email', + ]; } diff --git a/tests/ScopeSessionsTest.php b/tests/ScopeSessionsTest.php index b5fb962a..9e8eedb0 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,59 @@ 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(); +uses(Stancl\Tenancy\Tests\TestCase::class); - 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 b64478cc..70860507 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; @@ -12,361 +10,304 @@ use Illuminate\Support\Facades\Validator; use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel; use Stancl\Tenancy\Database\Concerns\BelongsToTenant; use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules; + +uses(Stancl\Tenancy\Tests\TestCase::class); use Stancl\Tenancy\Tests\Etc\Tenant as TestTenant; -class SingleDatabaseTenancyTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +beforeEach(function () { + BelongsToTenant::$tenantIdColumn = 'tenant_id'; - BelongsToTenant::$tenantIdColumn = 'tenant_id'; + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); - Schema::create('posts', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); + $table->string('tenant_id'); - $table->string('tenant_id'); + $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + }); - $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); - }); + Schema::create('comments', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); - Schema::create('comments', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); + $table->unsignedInteger('post_id'); - $table->unsignedInteger('post_id'); + $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade'); + }); - $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade'); - }); + config(['tenancy.tenant_model' => Tenant::class]); +}); - config(['tenancy.tenant_model' => Tenant::class]); - } +test('primary models are scoped to the current tenant', function () { + // acme context + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); - /** @test */ - public function primary_models_are_scoped_to_the_current_tenant() - { - // acme context - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); + $post = Post::create(['text' => 'Foo']); + $this->assertSame('acme', $post->tenant_id); + $this->assertSame('acme', $post->tenant->id); + + $post = Post::first(); + + $this->assertSame('acme', $post->tenant_id); + $this->assertSame('acme', $post->tenant->id); + + // ====================================== + // foobar context + tenancy()->initialize($foobar = Tenant::create([ + 'id' => 'foobar', + ])); + + $post = Post::create(['text' => 'Bar']); + + $this->assertSame('foobar', $post->tenant_id); + $this->assertSame('foobar', $post->tenant->id); + + $post = Post::first(); + + $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('primary models are not scoped in the central context', function () { + $this->primary_models_are_scoped_to_the_current_tenant(); + + tenancy()->end(); + + $this->assertSame(2, Post::count()); +}); + +test('secondary models are scoped to the current tenant when accessed via primary model', function () { + // 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('secondary models are n o t scoped to the current tenant when accessed directly', function () { + $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('secondary models a r e scoped to the current tenant when accessed directly a n d p a r e n t r e l a t i o n s h i p t r a i t i s u s e d', function () { + $acme = Tenant::create([ + 'id' => 'acme', + ]); + + $acme->run(function () { $post = Post::create(['text' => 'Foo']); + $post->scoped_comments()->create(['text' => 'Comment Text']); - $this->assertSame('acme', $post->tenant_id); - $this->assertSame('acme', $post->tenant->id); + $this->assertSame(1, Post::count()); + $this->assertSame(1, ScopedComment::count()); + }); - $post = Post::first(); + $foobar = Tenant::create([ + 'id' => 'foobar', + ]); - $this->assertSame('acme', $post->tenant_id); - $this->assertSame('acme', $post->tenant->id); - - // ====================================== - // foobar context - tenancy()->initialize($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('foobar', $post->tenant_id); - $this->assertSame('foobar', $post->tenant->id); - - $post = Post::first(); - - $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()); - } + $this->assertSame(1, ScopedComment::count()); + }); - /** @test */ - public function primary_models_are_not_scoped_in_the_central_context() - { - $this->primary_models_are_scoped_to_the_current_tenant(); + // Global context + $this->assertSame(2, ScopedComment::count()); +}); - tenancy()->end(); +test('secondary models are n o t scoped in the central context', function () { + $this->secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model(); - $this->assertSame(2, Post::count()); - } + tenancy()->end(); - /** @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', - ])); + $this->assertSame(2, Comment::count()); +}); - $post = Post::create(['text' => 'Foo']); - $post->comments()->create(['text' => 'Comment text']); +test('global models are not scoped at all', function () { + Schema::create('global_resources', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); + }); - // ================ - // foobar context - tenancy()->initialize($foobar = Tenant::create([ - 'id' => 'foobar', - ])); + GlobalResource::create(['text' => 'First']); + GlobalResource::create(['text' => 'Second']); - $post = Post::create(['text' => 'Bar']); - $post->comments()->create(['text' => 'Comment text 2']); + $acme = Tenant::create([ + 'id' => 'acme', + ]); - // ================ - // acme context again - tenancy()->initialize($acme); - $this->assertSame(1, Post::count()); - $this->assertSame(1, Post::first()->comments->count()); - } + $acme->run(function () { + $this->assertSame(2, GlobalResource::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(); + GlobalResource::create(['text' => 'Third']); + GlobalResource::create(['text' => 'Fourth']); + }); - // We're in acme context - $this->assertSame('acme', tenant('id')); + $this->assertSame(4, GlobalResource::count()); +}); - $this->assertSame(2, Comment::count()); - } +test('tenant id and relationship is auto added when creating primary resources in tenant context', function () { + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); - /** @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', - ]); + $post = Post::create(['text' => 'Foo']); - $acme->run(function () { - $post = Post::create(['text' => 'Foo']); - $post->scoped_comments()->create(['text' => 'Comment Text']); + $this->assertSame('acme', $post->tenant_id); + $this->assertTrue($post->relationLoaded('tenant')); + $this->assertSame($acme, $post->tenant); + $this->assertSame(tenant(), $post->tenant); +}); - $this->assertSame(1, Post::count()); - $this->assertSame(1, ScopedComment::count()); - }); +test('tenant id is not auto added when creating primary resources in central context', function () { + $this->expectException(QueryException::class); - $foobar = Tenant::create([ - 'id' => 'foobar', - ]); + Post::create(['text' => 'Foo']); +}); - $foobar->run(function () { - $this->assertSame(0, Post::count()); - $this->assertSame(0, ScopedComment::count()); +test('tenant id column name can be customized', function () { + BelongsToTenant::$tenantIdColumn = 'team_id'; - $post = Post::create(['text' => 'Bar']); - $post->scoped_comments()->create(['text' => 'Comment Text 2']); + Schema::drop('comments'); + Schema::drop('posts'); + Schema::create('posts', function (Blueprint $table) { + $table->increments('id'); + $table->string('text'); - $this->assertSame(1, Post::count()); - $this->assertSame(1, ScopedComment::count()); - }); + $table->string('team_id'); - // Global context - $this->assertSame(2, ScopedComment::count()); - } + $table->foreign('team_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + }); - /** @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()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); - tenancy()->end(); + $post = Post::create(['text' => 'Foo']); - $this->assertSame(2, Comment::count()); - } + $this->assertSame('acme', $post->team_id); - /** @test */ - public function global_models_are_not_scoped_at_all() - { - Schema::create('global_resources', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); - }); + // ====================================== + // foobar context + tenancy()->initialize($foobar = Tenant::create([ + 'id' => 'foobar', + ])); - GlobalResource::create(['text' => 'First']); - GlobalResource::create(['text' => 'Second']); + $post = Post::create(['text' => 'Bar']); - $acme = Tenant::create([ - 'id' => 'acme', - ]); + $this->assertSame('foobar', $post->team_id); - $acme->run(function () { - $this->assertSame(2, GlobalResource::count()); + $post = Post::first(); - GlobalResource::create(['text' => 'Third']); - GlobalResource::create(['text' => 'Fourth']); - }); + $this->assertSame('foobar', $post->team_id); - $this->assertSame(4, GlobalResource::count()); - } + // ====================================== + // acme context again - /** @test */ - public function tenant_id_and_relationship_is_auto_added_when_creating_primary_resources_in_tenant_context() - { - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); + tenancy()->initialize($acme); - $post = Post::create(['text' => 'Foo']); + $post = Post::first(); + $this->assertSame('acme', $post->team_id); - $this->assertSame('acme', $post->tenant_id); - $this->assertTrue($post->relationLoaded('tenant')); - $this->assertSame($acme, $post->tenant); - $this->assertSame(tenant(), $post->tenant); - } + // Assert foobar models are inaccessible in acme context + $this->assertSame(1, Post::count()); +}); - /** @test */ - public function tenant_id_is_not_auto_added_when_creating_primary_resources_in_central_context() - { - $this->expectException(QueryException::class); +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']); + }); - Post::create(['text' => 'Foo']); - } + tenancy()->initialize($acme = Tenant::create([ + 'id' => 'acme', + ])); - /** @test */ - public function tenant_id_column_name_can_be_customized() - { - BelongsToTenant::$tenantIdColumn = 'team_id'; + Post::create(['text' => 'Foo', 'slug' => 'foo']); + $data = ['text' => 'Foo 2', 'slug' => 'foo']; - Schema::drop('comments'); - Schema::drop('posts'); - Schema::create('posts', function (Blueprint $table) { - $table->increments('id'); - $table->string('text'); + $uniqueFails = Validator::make($data, [ + 'slug' => 'unique:posts', + ])->fails(); + $existsFails = Validator::make($data, [ + 'slug' => 'exists:posts', + ])->fails(); - $table->string('team_id'); + // 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' - $table->foreign('team_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); - }); + $uniqueFails = Validator::make($data, [ + 'slug' => tenant()->unique('posts'), + ])->fails(); + $existsFails = Validator::make($data, [ + 'slug' => tenant()->exists('posts'), + ])->fails(); - tenancy()->initialize($acme = Tenant::create([ - 'id' => 'acme', - ])); + // Assert that tenant()->unique() and tenant()->exists() are scoped + $this->assertTrue($uniqueFails); + $this->assertFalse($existsFails); +}); - $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); - } -} - -class Tenant extends TestTenant +// Helpers +function comments() { - use HasScopedValidationRules; + return test()->hasMany(Comment::class); } -class Post extends Model +function scoped_comments() { - use BelongsToTenant; - - protected $guarded = []; - public $timestamps = false; - - public function comments() - { - return $this->hasMany(Comment::class); - } - - public function scoped_comments() - { - return $this->hasMany(Comment::class); - } + return test()->hasMany(Comment::class); } -class Comment extends Model +function post() { - protected $guarded = []; - public $timestamps = false; - - public function post() - { - return $this->belongsTo(Post::class); - } + return test()->belongsTo(Post::class); } -class ScopedComment extends Comment +function getRelationshipToPrimaryModel(): string { - use BelongsToPrimaryModel; - - protected $table = 'comments'; - - public function getRelationshipToPrimaryModel(): string - { - return 'post'; - } -} - -class GlobalResource extends Model -{ - protected $guarded = []; - public $timestamps = false; + return 'post'; } diff --git a/tests/SubdomainTest.php b/tests/SubdomainTest.php index 17fbc1b3..dfb6bf0f 100644 --- a/tests/SubdomainTest.php +++ b/tests/SubdomainTest.php @@ -2,153 +2,128 @@ 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; -class SubdomainTest extends TestCase -{ - public function setUp(): void - { - parent::setUp(); +uses(Stancl\Tenancy\Tests\TestCase::class); - // Global state cleanup after some tests - InitializeTenancyBySubdomain::$onFail = null; +beforeEach(function () { + // 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); + $this->assertFalse(tenancy()->initialized); - $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')); - } + $this->assertTrue(tenancy()->initialized); + $this->assertSame('acme', tenant('id')); +}); - /** @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'); - } -} - -class SubdomainTenant extends Models\Tenant -{ - use HasDomains; -} + $this + ->withoutExceptionHandling() + ->get('http://localhost/foo/abc/xyz'); +}); diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 77a130b4..3b132aec 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,99 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Tests\Etc\Tenant; -class TenantAssetTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +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 + $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('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); + + $this->assertSame(route('stancl.tenancy.asset', ['path' => 'foo']), asset('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); + + $this->assertSame("https://an-s3-bucket/tenant{$tenant->id}/foo", asset('foo')); +}); + +test('global asset helper returns the same url regardless of tenancy initialization', function () { + $original = global_asset('foobar'); + $this->assertSame(asset('foobar'), global_asset('foobar')); + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + $this->assertSame($original, global_asset('foobar')); +}); + +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); + + $this->assertSame($original, asset('foo')); +}); + +// Helpers +function getEnvironmentSetUp($app) { - public function getEnvironmentSetUp($app) - { - parent::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..06d31616 100644 --- a/tests/TenantAwareCommandTest.php +++ b/tests/TenantAwareCommandTest.php @@ -2,32 +2,27 @@ 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']], - ]); +uses(Stancl\Tenancy\Tests\TestCase::class); - $this->artisan('user:add') - ->assertExitCode(0); +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']], + ]); - tenancy()->initialize($tenant1); - $this->assertNotEmpty(DB::table('users')->get()); - tenancy()->end(); + $this->artisan('user:add') + ->assertExitCode(0); - tenancy()->initialize($tenant2); - $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(); +}); diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 0e1464c0..0d7f4e56 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; @@ -27,252 +25,231 @@ use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager; use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager; use Stancl\Tenancy\Tests\Etc\Tenant; -class TenantDatabaseManagerTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +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); + + $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)); +})->with('database_manager_provider'); + +test('dbs can be created when another driver is used for the central db', function () { + $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)); +}); + +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(); + + $this->assertSame(['central'], array_keys(app('db')->getConnections())); + $this->assertArrayNotHasKey('tenant', config('database.connections')); + + tenancy()->initialize($tenant); + + 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')); +}); + +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); + + $this->assertSame(config('database.connections.tenant.database'), database_path('foodb')); +}); + +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'); + + $this->assertSame($tenant->database()->getName(), $schemaConfig); + $this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database'])); +}); + +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(); + $this->assertTrue($manager->databaseExists($tenant->database()->getName())); + + $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'); + $this->assertFalse($manager->databaseExists($name)); + + $manager->setConnection('mysql2'); + $this->assertTrue($manager->databaseExists($name)); +}); + +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] +]); + +// Helpers +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 46dc6a00..8dc2a9ac 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,181 +19,152 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\UUIDGenerator; -class TenantModelTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +test('created event is dispatched', function () { + Event::fake([TenantCreated::class]); + + Event::assertNotDispatched(TenantCreated::class); + + Tenant::create(); + + Event::assertDispatched(TenantCreated::class); +}); + +test('current tenant can be resolved from service container using typehint', function () { + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + $this->assertSame($tenant->id, app(Contracts\Tenant::class)->id); + + tenancy()->end(); + + $this->assertSame(null, app(Contracts\Tenant::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(); + }); + + $tenant = Tenant::create(); + + $this->assertNotNull($tenant->id); +}); + +test('autoincrement ids are supported', function () { + Schema::drop('domains'); + Schema::table('tenants', function (Blueprint $table) { + $table->bigIncrements('id')->change(); + }); + + unset(app()[UniqueIdentifierGenerator::class]); + + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + + $this->assertSame(1, $tenant1->id); + $this->assertSame(2, $tenant2->id); +}); + +test('custom tenant model can be used', function () { + $tenant = MyTenant::create(); + + tenancy()->initialize($tenant); + + $this->assertTrue(tenant() instanceof MyTenant); +}); + +test('custom tenant model that doesnt extend vendor tenant model can be used', function () { + $tenant = AnotherTenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant); + + $this->assertTrue(tenant() instanceof AnotherTenant); +}); + +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(); + + $this->assertSame(2, Tenant::count()); +}); + +test('the model uses tenant collection', function () { + Tenant::create(); + Tenant::create(); + + $this->assertSame(2, Tenant::count()); + $this->assertTrue(Tenant::all() instanceof TenantCollection); +}); + +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', + ]); + }); + + $this->assertSame('xyz', Tenant::find('t1')->foo); + $this->assertSame('xyz', Tenant::find('t2')->foo); +}); + +// Helpers +function getTenantKeyName(): string { - /** @test */ - public function created_event_is_dispatched() - { - Event::fake([TenantCreated::class]); - - Event::assertNotDispatched(TenantCreated::class); - - Tenant::create(); - - Event::assertDispatched(TenantCreated::class); - } - - /** @test */ - public function current_tenant_can_be_resolved_from_service_container_using_typehint() - { - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - $this->assertSame($tenant->id, app(Contracts\Tenant::class)->id); - - tenancy()->end(); - - $this->assertSame(null, app(Contracts\Tenant::class)); - } - - /** @test */ - public function id_is_generated_when_no_id_is_supplied() - { - config(['tenancy.id_generator' => UUIDGenerator::class]); - - $this->mock(UUIDGenerator::class, function ($mock) { - return $mock->shouldReceive('generate')->once(); - }); - - $tenant = Tenant::create(); - - $this->assertNotNull($tenant->id); - } - - /** @test */ - public function autoincrement_ids_are_supported() - { - Schema::drop('domains'); - Schema::table('tenants', function (Blueprint $table) { - $table->bigIncrements('id')->change(); - }); - - unset(app()[UniqueIdentifierGenerator::class]); - - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); - - $this->assertSame(1, $tenant1->id); - $this->assertSame(2, $tenant2->id); - } - - /** @test */ - public function custom_tenant_model_can_be_used() - { - $tenant = MyTenant::create(); - - tenancy()->initialize($tenant); - - $this->assertTrue(tenant() instanceof MyTenant); - } - - /** @test */ - public function custom_tenant_model_that_doesnt_extend_vendor_Tenant_model_can_be_used() - { - $tenant = AnotherTenant::create([ - 'id' => 'acme', - ]); - - 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); - } + return 'id'; } -class MyTenant extends Tenant +function getTenantKey() { - protected $table = 'tenants'; + return test()->getAttribute('id'); } -class AnotherTenant extends Model implements Contracts\Tenant +function run(callable $callback) { - protected $guarded = []; - protected $table = 'tenants'; - - public function getTenantKeyName(): string - { - return 'id'; - } - - public function getTenantKey() - { - return $this->getAttribute('id'); - } - - public function run(callable $callback) - { - $callback(); - } - - public function getInternal(string $key) - { - return $this->$key; - } - - public function setInternal(string $key, $value) - { - $this->$key = $value; - } + $callback(); +} + +function getInternal(string $key) +{ + return test()->$key; +} + +function setInternal(string $key, $value) +{ + test()->$key = $value; } diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php index c5e83853..9fd6dc8c 100644 --- a/tests/TenantUserImpersonationTest.php +++ b/tests/TenantUserImpersonationTest.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; - use Carbon\Carbon; use Carbon\CarbonInterval; use Closure; @@ -27,258 +25,233 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Tests\Etc\Tenant; -class TenantUserImpersonationTest extends TestCase +uses(Stancl\Tenancy\Tests\TestCase::class); + +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'); + + $this->assertNull(ImpersonationToken::find($token->token)); +}); + +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 () { + $this->assertSame('Joe', auth()->guard('another')->user()->name); + $this->assertSame(null, auth()->guard('web')->user()); + }); +}); + +// Helpers +function migrateTenants() { - protected function migrateTenants() - { - $this->artisan('tenants:migrate')->assertExitCode(0); - } - - public function setUp(): void - { - parent::setUp(); - - $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]); - } - - 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'), - ]); - }); - - // 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()); - }); - } + test()->artisan('tenants:migrate')->assertExitCode(0); } -class ImpersonationUser extends Authenticable +function makeLoginRoute() { - protected $guarded = []; - protected $table = 'users'; + Route::get('/login', function () { + return 'Please log in'; + })->name('login'); } -class AnotherImpersonationUser extends Authenticable +function getRoutes($loginRoute = true, $authGuard = 'web'): Closure { - protected $guarded = []; - protected $table = 'users'; + return function () use ($loginRoute, $authGuard) { + if ($loginRoute) { + test()->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); + }); + }; } diff --git a/tests/UniversalRouteTest.php b/tests/UniversalRouteTest.php index c0852545..4460f0d9 100644 --- a/tests/UniversalRouteTest.php +++ b/tests/UniversalRouteTest.php @@ -2,65 +2,55 @@ 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; +uses(Stancl\Tenancy\Tests\TestCase::class); - parent::tearDown(); - } +afterEach(function () { + InitializeTenancyByDomain::$onFail = null; +}); - /** @test */ - public function a_route_can_work_in_both_central_and_tenant_context() - { - Route::middlewareGroup('universal', []); - config(['tenancy.features' => [UniversalRoutes::class]]); +test('a route can work in both central and tenant context', function () { + 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 */ - public function making_one_route_universal_doesnt_make_all_routes_universal() - { - Route::get('/bar', function () { - return tenant('id'); - })->middleware(InitializeTenancyByDomain::class); +test('making one route universal doesnt make all routes universal', function () { + Route::get('/bar', function () { + return tenant('id'); + })->middleware(InitializeTenancyByDomain::class); - $this->a_route_can_work_in_both_central_and_tenant_context(); - tenancy()->end(); + $this->a_route_can_work_in_both_central_and_tenant_context(); + tenancy()->end(); - $this->get('http://localhost/bar') - ->assertStatus(500); + $this->get('http://localhost/bar') + ->assertStatus(500); - $this->get('http://acme.localhost/bar') - ->assertSuccessful() - ->assertSee('acme'); - } -} + $this->get('http://acme.localhost/bar') + ->assertSuccessful() + ->assertSee('acme'); +});