From 66ee08776a4d1c6aaae0f21ec602287643f95ab9 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 11:14:58 +0000 Subject: [PATCH 01/31] chore: Temporarily migrate tests --- .../Database/Eloquent/BelongsToManyTenantsTest.php | 3 +-- .../Database/Eloquent/BelongsToTenantTest.php | 2 +- tests/{ => _Original}/Database/Eloquent/TenantChildTest.php | 3 +-- tests/{ => _Original}/Http/Resolvers/CookieResolverTest.php | 2 +- tests/{ => _Original}/Http/Resolvers/HeaderResolverTest.php | 2 +- tests/{ => _Original}/Http/Resolvers/PathResolverTest.php | 2 +- tests/{ => _Original}/Http/Resolvers/SessionResolverTest.php | 3 +-- .../{ => _Original}/Http/Resolvers/SubdomainResolverTest.php | 2 +- .../{ => _Original}/Listeners/SetCurrentTenantForJobTest.php | 4 +--- tests/{ => _Original}/Overrides/CookieOverrideTest.php | 5 +---- tests/{ => _Original}/Overrides/StorageOverrideTest.php | 2 +- .../{ => _Original}/Providers/DatabaseTenantProviderTest.php | 2 +- .../{ => _Original}/Providers/EloquentTenantProviderTest.php | 4 +--- tests/{ => _Original}/ServiceProviderTest.php | 4 ++-- tests/{ => _Original}/SproutTest.php | 3 +-- tests/{ => _Original}/TenancyOptionsTest.php | 2 +- 16 files changed, 17 insertions(+), 28 deletions(-) rename tests/{ => _Original}/Database/Eloquent/BelongsToManyTenantsTest.php (99%) rename tests/{ => _Original}/Database/Eloquent/BelongsToTenantTest.php (99%) rename tests/{ => _Original}/Database/Eloquent/TenantChildTest.php (98%) rename tests/{ => _Original}/Http/Resolvers/CookieResolverTest.php (97%) rename tests/{ => _Original}/Http/Resolvers/HeaderResolverTest.php (97%) rename tests/{ => _Original}/Http/Resolvers/PathResolverTest.php (98%) rename tests/{ => _Original}/Http/Resolvers/SessionResolverTest.php (98%) rename tests/{ => _Original}/Http/Resolvers/SubdomainResolverTest.php (99%) rename tests/{ => _Original}/Listeners/SetCurrentTenantForJobTest.php (96%) rename tests/{ => _Original}/Overrides/CookieOverrideTest.php (97%) rename tests/{ => _Original}/Overrides/StorageOverrideTest.php (99%) rename tests/{ => _Original}/Providers/DatabaseTenantProviderTest.php (98%) rename tests/{ => _Original}/Providers/EloquentTenantProviderTest.php (95%) rename tests/{ => _Original}/ServiceProviderTest.php (98%) rename tests/{ => _Original}/SproutTest.php (97%) rename tests/{ => _Original}/TenancyOptionsTest.php (98%) diff --git a/tests/Database/Eloquent/BelongsToManyTenantsTest.php b/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php similarity index 99% rename from tests/Database/Eloquent/BelongsToManyTenantsTest.php rename to tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php index 2c4891a..1dfda19 100644 --- a/tests/Database/Eloquent/BelongsToManyTenantsTest.php +++ b/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php @@ -1,7 +1,7 @@ assertArrayHasKey($key . '/../resources/config/multitenancy.php', $paths); $this->assertContains(config_path('multitenancy.php'), $paths); diff --git a/tests/SproutTest.php b/tests/_Original/SproutTest.php similarity index 97% rename from tests/SproutTest.php rename to tests/_Original/SproutTest.php index 43760f2..26d126b 100644 --- a/tests/SproutTest.php +++ b/tests/_Original/SproutTest.php @@ -1,7 +1,7 @@ Date: Mon, 18 Nov 2024 20:57:35 +0000 Subject: [PATCH 02/31] test: Separate out test suites --- phpunit.xml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 36e7c26..6bae6fd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,8 +15,14 @@ testdox="true" > - - tests + + tests/_Original + + + tests/Feature + + + tests/Unit From fb6376cf60aed144204eeaa3b4dc695553ad114e Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 20:57:47 +0000 Subject: [PATCH 03/31] refactor: Remove unused method --- src/Sprout.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Sprout.php b/src/Sprout.php index f8beb82..a2ecdff 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -118,18 +118,6 @@ public function getAllCurrentTenancies(): array return $this->tenancies; } - /** - * Should Sprout listen for the routing event - * - * @return bool - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - public function shouldListenForRouting(): bool - { - return (bool)$this->config('listen_for_routing', true); - } - /** * Get the identity resolver manager * From e44e72369433f437ba27da8b86d34c04a39d7d1c Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 20:57:58 +0000 Subject: [PATCH 04/31] fix: Correct naming of markOutsideContext method --- src/Sprout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sprout.php b/src/Sprout.php index a2ecdff..c853f13 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -188,7 +188,7 @@ public function markAsInContext(): self * * @return static */ - public function maskAsOutsideContext(): self + public function markAsOutsideContext(): self { $this->withinContext = false; From 9554346055330f4a56dd86aea209f4ac49fac245 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 20:58:09 +0000 Subject: [PATCH 05/31] test: Start separate unit tests --- tests/Unit/Attributes/CurrentTenantTest.php | 83 +++++++++ tests/Unit/SproutServiceProviderTest.php | 181 ++++++++++++++++++++ tests/Unit/SproutTest.php | 120 +++++++++++++ tests/Unit/Support/DefaultTenancyTest.php | 169 ++++++++++++++++++ tests/Unit/Support/ResolutionHelperTest.php | 48 ++++++ tests/Unit/TenancyOptionsTest.php | 73 ++++++++ tests/Unit/UnitTestCase.php | 13 ++ 7 files changed, 687 insertions(+) create mode 100644 tests/Unit/Attributes/CurrentTenantTest.php create mode 100644 tests/Unit/SproutServiceProviderTest.php create mode 100644 tests/Unit/SproutTest.php create mode 100644 tests/Unit/Support/DefaultTenancyTest.php create mode 100644 tests/Unit/Support/ResolutionHelperTest.php create mode 100644 tests/Unit/TenancyOptionsTest.php create mode 100644 tests/Unit/UnitTestCase.php diff --git a/tests/Unit/Attributes/CurrentTenantTest.php b/tests/Unit/Attributes/CurrentTenantTest.php new file mode 100644 index 0000000..2015310 --- /dev/null +++ b/tests/Unit/Attributes/CurrentTenantTest.php @@ -0,0 +1,83 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + protected function setupSecondTenancy($app): void + { + tap($app['config'], static function (Repository $config) { + $config->set('multitenancy.providers.backup', [ + 'driver' => 'database', + 'table' => 'tenants', + ]); + + $config->set('multitenancy.tenancies.backup', [ + 'provider' => 'backup', + ]); + }); + } + + #[Test] + public function resolvesCurrentTenant(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get('tenants'); + + sprout()->setCurrentTenancy($tenancy); + + $tenant = TenantModel::factory()->createOne(); + + $tenancy->setTenant($tenant); + + $callback = static function (#[CurrentTenant] TenantModel $tenant) { + return $tenant; + }; + + $currentTenant = $this->app->call($callback); + + $this->assertSame($tenant, $currentTenant); + $this->assertSame($tenancy->tenant(), $currentTenant); + } + + #[Test, DefineEnvironment('setupSecondTenancy')] + public function resolvesCurrentTenantForSpecificTenancy(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get('backup'); + + sprout()->setCurrentTenancy($tenancy); + + $tenant = new GenericTenant(TenantModel::factory()->createOne()->toArray()); + + $tenancy->setTenant($tenant); + + $callback = static function (#[CurrentTenant('backup')] GenericTenant $tenant) { + return $tenant; + }; + + $currentTenant = $this->app->call($callback); + + $this->assertSame($tenant, $currentTenant); + $this->assertSame($tenancy->tenant(), $currentTenant); + } +} diff --git a/tests/Unit/SproutServiceProviderTest.php b/tests/Unit/SproutServiceProviderTest.php new file mode 100644 index 0000000..7bf8914 --- /dev/null +++ b/tests/Unit/SproutServiceProviderTest.php @@ -0,0 +1,181 @@ +assertTrue(app()->providerIsLoaded(SproutServiceProvider::class)); + } + + #[Test] + public function serviceProviderIsDiscovered(): void + { + $manifest = app(PackageManifest::class); + + $this->assertContains(SproutServiceProvider::class, $manifest->providers()); + } + + #[Test] + public function sproutIsRegistered(): void + { + $this->assertTrue(app()->has(Sprout::class)); + $this->assertTrue(app()->has('sprout')); + $this->assertTrue(app()->isShared(Sprout::class)); + $this->assertFalse(app()->isShared('sprout')); + + $this->assertSame(app()->make(Sprout::class), app()->make(Sprout::class)); + $this->assertSame(app()->make('sprout'), app()->make('sprout')); + $this->assertSame(app()->make(Sprout::class), app()->make('sprout')); + $this->assertSame(app()->make('sprout'), app()->make(Sprout::class)); + $this->assertSame(sprout(), sprout()); + $this->assertSame(app()->make(Sprout::class), sprout()); + } + + #[Test] + public function providerManagerIsRegistered(): void + { + $this->assertTrue(app()->has(ProviderManager::class)); + $this->assertTrue(app()->has('sprout.providers')); + $this->assertTrue(app()->isShared(ProviderManager::class)); + $this->assertFalse(app()->isShared('sprout.providers')); + + $this->assertSame(app()->make(ProviderManager::class), app()->make(ProviderManager::class)); + $this->assertSame(app()->make('sprout.providers'), app()->make('sprout.providers')); + $this->assertSame(app()->make(ProviderManager::class), app()->make('sprout.providers')); + $this->assertSame(app()->make('sprout.providers'), app()->make(ProviderManager::class)); + $this->assertSame(app()->make(Sprout::class)->providers(), app()->make('sprout.providers')); + $this->assertSame(app()->make(Sprout::class)->providers(), app()->make(ProviderManager::class)); + $this->assertSame(sprout()->providers(), sprout()->providers()); + $this->assertSame(app()->make(Sprout::class)->providers(), sprout()->providers()); + } + + #[Test] + public function identityResolverManagerIsRegistered(): void + { + $this->assertTrue(app()->has(IdentityResolverManager::class)); + $this->assertTrue(app()->has('sprout.resolvers')); + $this->assertTrue(app()->isShared(IdentityResolverManager::class)); + $this->assertFalse(app()->isShared('sprout.resolvers')); + + $this->assertSame(app()->make(IdentityResolverManager::class), app()->make(IdentityResolverManager::class)); + $this->assertSame(app()->make('sprout.resolvers'), app()->make('sprout.resolvers')); + $this->assertSame(app()->make(IdentityResolverManager::class), app()->make('sprout.resolvers')); + $this->assertSame(app()->make('sprout.resolvers'), app()->make(IdentityResolverManager::class)); + $this->assertSame(app()->make(Sprout::class)->resolvers(), app()->make('sprout.resolvers')); + $this->assertSame(app()->make(Sprout::class)->resolvers(), app()->make(IdentityResolverManager::class)); + $this->assertSame(sprout()->resolvers(), sprout()->resolvers()); + $this->assertSame(app()->make(Sprout::class)->resolvers(), sprout()->resolvers()); + } + + #[Test] + public function tenancyManagerIsRegistered(): void + { + $this->assertTrue(app()->has(TenancyManager::class)); + $this->assertTrue(app()->has('sprout.tenancies')); + $this->assertTrue(app()->isShared(TenancyManager::class)); + $this->assertFalse(app()->isShared('sprout.tenancies')); + + $this->assertSame(app()->make(TenancyManager::class), app()->make(TenancyManager::class)); + $this->assertSame(app()->make('sprout.tenancies'), app()->make('sprout.tenancies')); + $this->assertSame(app()->make(TenancyManager::class), app()->make('sprout.tenancies')); + $this->assertSame(app()->make('sprout.tenancies'), app()->make(TenancyManager::class)); + $this->assertSame(app()->make(Sprout::class)->tenancies(), app()->make('sprout.tenancies')); + $this->assertSame(app()->make(Sprout::class)->tenancies(), app()->make(TenancyManager::class)); + $this->assertSame(sprout()->tenancies(), sprout()->tenancies()); + $this->assertSame(app()->make(Sprout::class)->tenancies(), sprout()->tenancies()); + } + + #[Test] + public function registersTenantRoutesMiddleware(): void + { + $router = $this->app->make(Router::class); + $middleware = $router->getMiddleware(); + + $this->assertTrue(isset($middleware[TenantRoutes::ALIAS])); + $this->assertSame(TenantRoutes::class, $middleware[TenantRoutes::ALIAS]); + $this->assertContains(TenantRoutes::class, $middleware); + } + + #[Test] + public function registersRouterMixinMethods(): void + { + $this->assertTrue(Router::hasMacro('tenanted')); + } + + #[Test] + public function publishesConfig(): void + { + $paths = ServiceProvider::pathsToPublish(SproutServiceProvider::class, 'config'); + + $key = realpath(__DIR__ . '/../../src'); + + $this->assertArrayHasKey($key . '/../resources/config/multitenancy.php', $paths); + $this->assertContains(config_path('multitenancy.php'), $paths); + } + + #[Test] + public function coreSproutConfigExists(): void + { + $this->assertTrue(app()['config']->has('sprout')); + $this->assertIsArray(app()['config']->get('sprout')); + $this->assertTrue(app()['config']->has('sprout.hooks')); + } + + #[Test] + public function registersServiceOverrides(): void + { + $overrides = config('sprout.services'); + + foreach ($overrides as $override) { + $this->assertTrue(sprout()->hasRegisteredOverride($override)); + } + } + + #[Test] + public function registersEventHandlers(): void + { + $dispatcher = app()->make(Dispatcher::class); + + $this->assertTrue($dispatcher->hasListeners(RouteMatched::class)); + + $listeners = $dispatcher->getRawListeners(); + + $this->assertContains(IdentifyTenantOnRouting::class, $listeners[RouteMatched::class]); + } + + #[Test] + public function registersTenancyBootstrappers(): void + { + $bootstrappers = config('sprout.bootstrappers'); + + $dispatcher = app()->make(Dispatcher::class); + + $this->assertTrue($dispatcher->hasListeners(RouteMatched::class)); + + $listeners = $dispatcher->getRawListeners(); + + foreach ($bootstrappers as $bootstrapper) { + $this->assertContains($bootstrapper, $listeners[CurrentTenantChanged::class]); + } + } +} diff --git a/tests/Unit/SproutTest.php b/tests/Unit/SproutTest.php new file mode 100644 index 0000000..a24499a --- /dev/null +++ b/tests/Unit/SproutTest.php @@ -0,0 +1,120 @@ +set('multitenancy.providers.backup', [ + 'driver' => 'database', + 'table' => 'tenants', + ]); + + $config->set('multitenancy.tenancies.backup', [ + 'provider' => 'backup', + ]); + }); + } + + #[Test] + public function allowsAccessToCoreConfig(): void + { + $this->assertSame(sprout()->config('hooks'), config('sprout.hooks')); + + config()->set('sprout.hooks', []); + + $this->assertSame(sprout()->config('hooks'), config('sprout.hooks')); + } + + #[Test] + public function hasNoCurrentTenancyByDefault(): void + { + $this->assertFalse(sprout()->hasCurrentTenancy()); + } + + #[Test] + public function isNotWithinMultitenantedContextByDefault(): void + { + $this->assertFalse(sprout()->withinContext()); + } + + #[Test] + public function setsCurrentTenancy(): void + { + $tenancy = sprout()->tenancies()->get(); + + $this->assertFalse(sprout()->hasCurrentTenancy()); + $this->assertNull(sprout()->getCurrentTenancy()); + $this->assertFalse(sprout()->withinContext()); + + sprout()->setCurrentTenancy($tenancy); + + $this->assertTrue(sprout()->hasCurrentTenancy()); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertTrue(sprout()->withinContext()); + } + + #[Test, DefineEnvironment('setupSecondTenancy')] + public function canStackCurrentTenancies(): void + { + $tenancy1 = sprout()->tenancies()->get(); + $tenancy2 = sprout()->tenancies()->get('backup'); + + $this->assertFalse(sprout()->hasCurrentTenancy()); + $this->assertNull(sprout()->getCurrentTenancy()); + $this->assertFalse(sprout()->withinContext()); + + sprout()->setCurrentTenancy($tenancy1); + + $this->assertTrue(sprout()->hasCurrentTenancy()); + $this->assertSame($tenancy1, sprout()->getCurrentTenancy()); + $this->assertTrue(sprout()->withinContext()); + + sprout()->setCurrentTenancy($tenancy2); + + $this->assertTrue(sprout()->hasCurrentTenancy()); + $this->assertSame($tenancy2, sprout()->getCurrentTenancy()); + $this->assertTrue(sprout()->withinContext()); + + $this->assertContains($tenancy1, sprout()->getAllCurrentTenancies()); + $this->assertContains($tenancy2, sprout()->getAllCurrentTenancies()); + } + + #[Test] + public function isAwareOfHooksToSupport(): void + { + $hooks = config('sprout.hooks'); + + foreach ($hooks as $hook) { + $this->assertTrue(sprout()->supportsHook($hook)); + } + + config()->set('sprout.hooks', []); + + foreach ($hooks as $hook) { + $this->assertFalse(sprout()->supportsHook($hook)); + } + } + + #[Test] + public function canManuallyMarkAsInOrOutOfContext(): void + { + $this->assertFalse(sprout()->withinContext()); + + sprout()->markAsInContext(); + + $this->assertTrue(sprout()->withinContext()); + + sprout()->markAsOutsideContext(); + + $this->assertFalse(sprout()->withinContext()); + } +} diff --git a/tests/Unit/Support/DefaultTenancyTest.php b/tests/Unit/Support/DefaultTenancyTest.php new file mode 100644 index 0000000..a0cfc51 --- /dev/null +++ b/tests/Unit/Support/DefaultTenancyTest.php @@ -0,0 +1,169 @@ +set('multitenancy.defaults.resolver', 'path'); + $config->set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + #[Test] + public function hasName(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $this->assertInstanceOf(DefaultTenancy::class, $tenancy); + $this->assertSame('tenants', $tenancy->getName()); + } + + #[Test] + public function hasNoCurrentTenantByDefault(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $this->assertFalse($tenancy->check()); + } + + #[Test] + public function storesCurrentTenantForAccess(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $this->assertFalse($tenancy->check()); + + $tenant = TenantModel::factory()->createOne(); + + $tenancy->setTenant($tenant); + + $this->assertTrue($tenancy->check()); + $this->assertSame($tenant, $tenancy->tenant()); + $this->assertSame($tenant->getTenantKey(), $tenancy->key()); + $this->assertSame($tenant->getTenantIdentifier(), $tenancy->identifier()); + } + + #[Test] + public function identifiesTenant(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $this->assertFalse($tenancy->check()); + + $tenant = TenantModel::factory()->createOne(); + + $this->assertFalse($tenancy->identify('non-existent')); + + Event::fake([TenantIdentified::class]); + + $this->assertTrue($tenancy->identify($tenant->getTenantIdentifier())); + + Event::assertDispatched(TenantIdentified::class); + } + + #[Test] + public function loadsTenant(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $this->assertFalse($tenancy->check()); + + $tenant = TenantModel::factory()->createOne(); + + $this->assertFalse($tenancy->load(-99999)); + + Event::fake([TenantLoaded::class]); + + $this->assertTrue($tenancy->load($tenant->getTenantKey())); + + Event::assertDispatched(TenantLoaded::class); + } + + #[Test] + public function hasATenantProvider(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $provider = $tenancy->provider(); + + $this->assertNotNull($provider); + $this->assertInstanceOf(EloquentTenantProvider::class, $provider); + } + + #[Test] + public function storesHowAndWhenTheTenantWasResolved(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $this->assertFalse($tenancy->wasResolved()); + $this->assertNull($tenancy->resolver()); + $this->assertNull($tenancy->hook()); + + $tenant = TenantModel::factory()->createOne(); + + $tenancy->setTenant($tenant); + + $this->assertFalse($tenancy->wasResolved()); + $this->assertNull($tenancy->resolver()); + $this->assertNull($tenancy->hook()); + + $tenancy->resolvedVia(sprout()->resolvers()->get()); + $tenancy->resolvedAt(ResolutionHook::Booting); + + $this->assertTrue($tenancy->wasResolved()); + $this->assertNotNull($tenancy->resolver()); + $this->assertSame(sprout()->resolvers()->get(), $tenancy->resolver()); + $this->assertNotNull($tenancy->hook()); + $this->assertSame(ResolutionHook::Booting, $tenancy->hook()); + } + + #[Test] + public function hasOptions(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = sprout()->tenancies()->get(); + + $this->assertSame(config('multitenancy.tenancies.tenants.options'), $tenancy->options()); + + $this->assertTrue($tenancy->hasOption(TenancyOptions::hydrateTenantRelation())); + $this->assertTrue($tenancy->hasOption(TenancyOptions::throwIfNotRelated())); + + $tenancy->removeOption(TenancyOptions::hydrateTenantRelation()); + + $this->assertFalse($tenancy->hasOption(TenancyOptions::hydrateTenantRelation())); + $this->assertTrue($tenancy->hasOption(TenancyOptions::throwIfNotRelated())); + + $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); + + $this->assertFalse($tenancy->hasOption(TenancyOptions::hydrateTenantRelation())); + $this->assertFalse($tenancy->hasOption(TenancyOptions::throwIfNotRelated())); + + $tenancy->addOption(TenancyOptions::hydrateTenantRelation()); + + $this->assertTrue($tenancy->hasOption(TenancyOptions::hydrateTenantRelation())); + $this->assertFalse($tenancy->hasOption(TenancyOptions::throwIfNotRelated())); + } +} diff --git a/tests/Unit/Support/ResolutionHelperTest.php b/tests/Unit/Support/ResolutionHelperTest.php new file mode 100644 index 0000000..81efd9c --- /dev/null +++ b/tests/Unit/Support/ResolutionHelperTest.php @@ -0,0 +1,48 @@ +assertNull($resolverName); + $this->assertNull($tenancyName); + + [$resolverName, $tenancyName] = ResolutionHelper::parseOptions(['test']); + + $this->assertNotNull($resolverName); + $this->assertSame('test', $resolverName); + $this->assertNull($tenancyName); + + [$resolverName, $tenancyName] = ResolutionHelper::parseOptions(['test', 'more']); + + $this->assertNotNull($resolverName); + $this->assertSame('test', $resolverName); + $this->assertNotNull($tenancyName); + $this->assertSame('more', $tenancyName); + } + + #[Test] + public function throwsExceptionWhenHandlingResolutionForUnsupportedHook(): void + { + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The resolution hook [Booting] is not supported'); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class); + + ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Booting); + } +} diff --git a/tests/Unit/TenancyOptionsTest.php b/tests/Unit/TenancyOptionsTest.php new file mode 100644 index 0000000..3f83d2f --- /dev/null +++ b/tests/Unit/TenancyOptionsTest.php @@ -0,0 +1,73 @@ +set('multitenancy.providers.backup', [ + 'driver' => 'database', + 'table' => 'tenants', + ]); + + $config->set('multitenancy.tenancies.backup', [ + 'provider' => 'backup', + ]); + }); + } + + #[Test] + public function hydrateTenantRelationOption(): void + { + $this->assertSame('tenant-relation.hydrate', TenancyOptions::hydrateTenantRelation()); + } + + #[Test] + public function throwIfNotRelatedOption(): void + { + $this->assertSame('tenant-relation.strict', TenancyOptions::throwIfNotRelated()); + } + + #[Test, DefineEnvironment('setupSecondTenancy')] + public function correctlyReportsHydrateTenantRelationOptionPresence(): void + { + $tenancy = app(TenancyManager::class)->get('tenants'); + $tenancy->removeOption(TenancyOptions::hydrateTenantRelation()); + + $this->assertFalse(TenancyOptions::shouldHydrateTenantRelation($tenancy)); + + $tenancy->addOption(TenancyOptions::hydrateTenantRelation()); + + $this->assertTrue(TenancyOptions::shouldHydrateTenantRelation($tenancy)); + + $tenancy = app(TenancyManager::class)->get('backup'); + + $this->assertFalse(TenancyOptions::shouldHydrateTenantRelation($tenancy)); + } + + #[Test, DefineEnvironment('setupSecondTenancy')] + public function correctlyReportsThrowIfNotRelatedOptionPresence(): void + { + $tenancy = app(TenancyManager::class)->get('tenants'); + $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); + + $this->assertFalse(TenancyOptions::shouldThrowIfNotRelated($tenancy)); + + $tenancy->addOption(TenancyOptions::throwIfNotRelated()); + + $this->assertTrue(TenancyOptions::shouldThrowIfNotRelated($tenancy)); + + $tenancy = app(TenancyManager::class)->get('backup'); + + $this->assertFalse(TenancyOptions::shouldThrowIfNotRelated($tenancy)); + } +} diff --git a/tests/Unit/UnitTestCase.php b/tests/Unit/UnitTestCase.php new file mode 100644 index 0000000..02e2b6a --- /dev/null +++ b/tests/Unit/UnitTestCase.php @@ -0,0 +1,13 @@ + Date: Mon, 18 Nov 2024 21:00:16 +0000 Subject: [PATCH 06/31] chore: Don't pass null to ltrim --- src/Concerns/OverridesCookieSettings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Concerns/OverridesCookieSettings.php b/src/Concerns/OverridesCookieSettings.php index 3ff4e86..b992b35 100644 --- a/src/Concerns/OverridesCookieSettings.php +++ b/src/Concerns/OverridesCookieSettings.php @@ -39,7 +39,7 @@ public static function setDomain(?string $domain): void */ public static function setPath(?string $path): void { - self::$settings['path'] = '/' . ltrim($path, '/'); + self::$settings['path'] = $path ? '/' . ltrim($path, '/') : null; } // @codeCoverageIgnoreStart From baea8681bcbdc1bc0df40a254394e9f4d579f0f8 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 21:01:09 +0000 Subject: [PATCH 07/31] build(github): Separate out testsuites for workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2932450..36f2581 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,7 +39,7 @@ jobs: run: vendor/bin/testbench vendor:publish --provider="Sprout\\SproutServiceProvider" - name: Execute tests - run: composer test + run: vendor/bin/phpunit --testsuite=Unit,Feature - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 From 294d12cc1fd1fab86916a76c8aa4ca162dd4a090 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 21:03:59 +0000 Subject: [PATCH 08/31] build(github): Remove unnecessary vendor:publish --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 36f2581..41b0136 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,9 +35,6 @@ jobs: - name: Prepare testbench run: composer clear && composer prepare && composer build - - name: Publish required assets - run: vendor/bin/testbench vendor:publish --provider="Sprout\\SproutServiceProvider" - - name: Execute tests run: vendor/bin/phpunit --testsuite=Unit,Feature From 297e7d02ab891d96f9323cac6bcbdd7fb2805e3c Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 21:06:04 +0000 Subject: [PATCH 09/31] build(github): Revert unit test command for workflow --- .github/workflows/tests.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 41b0136..9ece740 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: run: composer clear && composer prepare && composer build - name: Execute tests - run: vendor/bin/phpunit --testsuite=Unit,Feature + run: composer test - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4 diff --git a/composer.json b/composer.json index 013db51..0bd998d 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,7 @@ "@clear", "@prepare", "@build", - "@php vendor/bin/phpunit" + "@php vendor/bin/phpunit --testsuite=Unit,Feature" ] }, "extra" : { From c4a3a1b1a28d4a8c9993d22535d58cf9ce2bb4c9 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 21:08:46 +0000 Subject: [PATCH 10/31] chore: Fix paths for feature and unit testing --- phpunit.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 6bae6fd..7e3a02d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -19,10 +19,10 @@ tests/_Original - tests/Feature + ./tests/Feature - tests/Unit + ./tests/Unit From 55954507787f05f91c4b5e21dcb342345c3f0ba8 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 18 Nov 2024 21:11:12 +0000 Subject: [PATCH 11/31] chore: Ensure tests/Feature directory exists --- tests/Feature/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/Feature/.gitkeep diff --git a/tests/Feature/.gitkeep b/tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 From a00be22d107aa7241cd75fb3f0e6724b2c38d110 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Tue, 19 Nov 2024 12:00:17 +0000 Subject: [PATCH 12/31] chore: Reset resolver and hook when current tenant is nullified --- src/Support/DefaultTenancy.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Support/DefaultTenancy.php b/src/Support/DefaultTenancy.php index f88b381..c30dd8b 100644 --- a/src/Support/DefaultTenancy.php +++ b/src/Support/DefaultTenancy.php @@ -257,6 +257,11 @@ public function setTenant(?Tenant $tenant): static event(new CurrentTenantChanged($this, $previousTenant, $tenant)); } + if ($tenant === null) { + $this->resolver = null; + $this->hook = null; + } + return $this; } From 994871e43d5a32955166bf33ab7e2cab02de3ac0 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Tue, 19 Nov 2024 12:00:33 +0000 Subject: [PATCH 13/31] chore: Do not generate code coverage for the generic tenant class --- src/Support/GenericTenant.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Support/GenericTenant.php b/src/Support/GenericTenant.php index 305f510..9b627c1 100644 --- a/src/Support/GenericTenant.php +++ b/src/Support/GenericTenant.php @@ -13,6 +13,8 @@ * as the tenant entity. * * @pacakge Core + * + * @codeCoverageIgnore */ class GenericTenant implements Tenant { From 7d73645ed7a8ed0fe4ff8f145ce0ec64ce94b5f7 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Tue, 19 Nov 2024 12:00:45 +0000 Subject: [PATCH 14/31] test: Add unit tests for the resolution helper --- tests/Unit/Support/ResolutionHelperTest.php | 297 ++++++++++++++++++++ 1 file changed, 297 insertions(+) diff --git a/tests/Unit/Support/ResolutionHelperTest.php b/tests/Unit/Support/ResolutionHelperTest.php index 81efd9c..f7d55f1 100644 --- a/tests/Unit/Support/ResolutionHelperTest.php +++ b/tests/Unit/Support/ResolutionHelperTest.php @@ -4,14 +4,31 @@ namespace Sprout\Tests\Unit\Support; use Illuminate\Http\Request; +use Illuminate\Routing\Route; +use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; use Sprout\Exceptions\MisconfigurationException; +use Sprout\Exceptions\NoTenantFound; +use Sprout\Managers\IdentityResolverManager; +use Sprout\Managers\TenancyManager; use Sprout\Support\ResolutionHelper; use Sprout\Support\ResolutionHook; use Sprout\Tests\Unit\UnitTestCase; +use Workbench\App\Models\TenantModel; +use function Sprout\resolver; +use function Sprout\sprout; class ResolutionHelperTest extends UnitTestCase { + protected function defineEnvironment($app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.defaults.resolver', 'path'); + $config->set('multitenancy.providers.tenants.model', TenantModel::class); + $config->set('multitenancy.resolvers.subdomain.domain', 'localhost'); + }); + } + #[Test] public function parsesMiddlewareOptions(): void { @@ -45,4 +62,284 @@ public function throwsExceptionWhenHandlingResolutionForUnsupportedHook(): void ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Booting); } + + #[Test] + public function returnsFalseIfThereIsAlreadyATenant(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + $tenancy->setTenant(TenantModel::factory()->createOne()); + + /** @var \Sprout\Contracts\IdentityResolver $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class); + + $this->assertTrue($tenancy->check()); + $this->assertTrue($resolver->canResolve($fakeRequest, $tenancy, ResolutionHook::Routing)); + $this->assertFalse(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName())); + } + + #[Test] + public function returnsFalseIfTheResolverCannotResolve(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + /** @var \Sprout\Contracts\IdentityResolver $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + $tenancy->setTenant(TenantModel::factory()->createOne()) + ->resolvedVia($resolver) + ->resolvedAt(ResolutionHook::Routing); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class); + + $this->assertTrue($tenancy->check()); + $this->assertFalse($resolver->canResolve($fakeRequest, $tenancy, ResolutionHook::Routing)); + $this->assertFalse(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName())); + } + + #[Test] + public function resolvesTenantUsingRouteParameters(): void + { + $tenant = TenantModel::factory()->createOne(); + + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + /** @var \Illuminate\Routing\Route $fakeRoute */ + $fakeRoute = $this->mock(Route::class, function (MockInterface $mock) use ($tenant, $tenancy, $resolver) { + $parameterName = $resolver->getRouteParameterName($tenancy); + + $mock->shouldReceive('hasParameter') + ->with($parameterName) + ->andReturn(true); + + $mock->shouldReceive('parameter') + ->with($parameterName) + ->andReturn($tenant->getTenantIdentifier()); + + $mock->shouldReceive('forgetParameter') + ->with($parameterName); + }); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) use ($fakeRoute) { + $mock->shouldReceive('route')->andReturn($fakeRoute); + }); + + $this->assertTrue(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName())); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertTrue($tenancy->check()); + $this->assertTrue($tenant->is($tenancy->tenant())); + $this->assertTrue($tenancy->wasResolved()); + $this->assertSame($resolver, $tenancy->resolver()); + $this->assertSame(ResolutionHook::Routing, $tenancy->hook()); + + $tenancy->setTenant(null); + + $this->assertTrue(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing)); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertTrue($tenancy->check()); + $this->assertTrue($tenant->is($tenancy->tenant())); + $this->assertTrue($tenancy->wasResolved()); + $this->assertSame($resolver, $tenancy->resolver()); + $this->assertSame(ResolutionHook::Routing, $tenancy->hook()); + } + + #[Test] + public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRoute(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + /** @var \Illuminate\Routing\Route $fakeRoute */ + $fakeRoute = $this->mock(Route::class, function (MockInterface $mock) use ($tenancy, $resolver) { + $parameterName = $resolver->getRouteParameterName($tenancy); + + $mock->shouldReceive('hasParameter') + ->with($parameterName) + ->andReturn(true); + + $mock->shouldReceive('parameter') + ->with($parameterName) + ->andReturn('fake-identifier'); + + $mock->shouldReceive('forgetParameter') + ->with($parameterName); + }); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) use ($fakeRoute) { + $mock->shouldReceive('route')->andReturn($fakeRoute); + }); + + $this->expectException(NoTenantFound::class); + $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [' . $resolver->getName() . ']'); + + ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName()); + + $this->expectException(NoTenantFound::class); + $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [subdomain]'); + + ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing); + } + + #[Test] + public function returnsFalseWhenUnableToIdentifyATenantFromTheRouteAndToldNotToThrow(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + /** @var \Illuminate\Routing\Route $fakeRoute */ + $fakeRoute = $this->mock(Route::class, function (MockInterface $mock) use ($tenancy, $resolver) { + $parameterName = $resolver->getRouteParameterName($tenancy); + + $mock->shouldReceive('hasParameter') + ->with($parameterName) + ->andReturn(true); + + $mock->shouldReceive('parameter') + ->with($parameterName) + ->andReturn('fake-identifier'); + + $mock->shouldReceive('forgetParameter') + ->with($parameterName); + }); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) use ($fakeRoute) { + $mock->shouldReceive('route')->andReturn($fakeRoute); + }); + + $this->assertFalse(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName(), false)); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertFalse($tenancy->check()); + $this->assertFalse($tenancy->wasResolved()); + $this->assertNull($tenancy->resolver()); + $this->assertNull($tenancy->hook()); + + $this->assertFalse(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, throw: false)); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertFalse($tenancy->check()); + $this->assertFalse($tenancy->wasResolved()); + $this->assertNull($tenancy->resolver()); + $this->assertNull($tenancy->hook()); + } + + #[Test] + public function resolvesTenantWithoutRouteParameters(): void + { + $tenant = TenantModel::factory()->createOne(); + + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) use ($tenant) { + $mock->shouldReceive('route')->andReturnNull(); + + $mock->shouldReceive('segment') + ->with(1) + ->andReturn($tenant->getTenantIdentifier()); + }); + + $this->assertTrue(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName())); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertTrue($tenancy->check()); + $this->assertTrue($tenant->is($tenancy->tenant())); + $this->assertTrue($tenancy->wasResolved()); + $this->assertSame($resolver, $tenancy->resolver()); + $this->assertSame(ResolutionHook::Routing, $tenancy->hook()); + + $tenancy->setTenant(null); + + $this->assertTrue(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing)); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertTrue($tenancy->check()); + $this->assertTrue($tenant->is($tenancy->tenant())); + $this->assertTrue($tenancy->wasResolved()); + $this->assertSame($resolver, $tenancy->resolver()); + $this->assertSame(ResolutionHook::Routing, $tenancy->hook()); + } + + #[Test] + public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRequest(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) { + $mock->shouldReceive('route')->andReturnNull(); + + $mock->shouldReceive('segment') + ->with(1) + ->andReturn('fake-identifier'); + }); + + $this->expectException(NoTenantFound::class); + $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [' . $resolver->getName() . ']'); + + ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName()); + + $this->expectException(NoTenantFound::class); + $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [subdomain]'); + + ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing); + } + + #[Test] + public function returnsFalseWhenUnableToIdentifyATenantFromTheRequestAndToldNotToThrow(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = app(TenancyManager::class)->get(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = app(IdentityResolverManager::class)->get('path'); + + /** @var \Illuminate\Http\Request $fakeRequest */ + $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) { + $mock->shouldReceive('route')->andReturnNull(); + + $mock->shouldReceive('segment') + ->with(1) + ->andReturn('fake-identifier'); + }); + + $this->assertFalse(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName(), false)); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertFalse($tenancy->check()); + $this->assertFalse($tenancy->wasResolved()); + $this->assertNull($tenancy->resolver()); + $this->assertNull($tenancy->hook()); + + $tenancy->setTenant(null); + + $this->assertFalse(ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, throw: false)); + $this->assertSame($tenancy, sprout()->getCurrentTenancy()); + $this->assertFalse($tenancy->check()); + $this->assertFalse($tenancy->wasResolved()); + $this->assertNull($tenancy->resolver()); + $this->assertNull($tenancy->hook()); + } } From f6eaf0037626dce9d3cebc98a20d1f0d893276c4 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Tue, 19 Nov 2024 12:04:47 +0000 Subject: [PATCH 15/31] chore: Tidy up code to use helper functions --- tests/Unit/Attributes/CurrentTenantTest.php | 7 ++--- tests/Unit/Support/ResolutionHelperTest.php | 35 ++++++++++----------- tests/Unit/TenancyOptionsTest.php | 10 +++--- tests/Unit/UnitTestCase.php | 3 +- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/tests/Unit/Attributes/CurrentTenantTest.php b/tests/Unit/Attributes/CurrentTenantTest.php index 2015310..631ddd8 100644 --- a/tests/Unit/Attributes/CurrentTenantTest.php +++ b/tests/Unit/Attributes/CurrentTenantTest.php @@ -7,12 +7,11 @@ use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; use Sprout\Attributes\CurrentTenant; -use Sprout\Contracts\Tenant; -use Sprout\Managers\TenancyManager; use Sprout\Support\GenericTenant; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; use function Sprout\sprout; +use function Sprout\tenancy; class CurrentTenantTest extends UnitTestCase { @@ -41,7 +40,7 @@ protected function setupSecondTenancy($app): void public function resolvesCurrentTenant(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get('tenants'); + $tenancy = tenancy('tenants'); sprout()->setCurrentTenancy($tenancy); @@ -63,7 +62,7 @@ public function resolvesCurrentTenant(): void public function resolvesCurrentTenantForSpecificTenancy(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get('backup'); + $tenancy = tenancy('backup'); sprout()->setCurrentTenancy($tenancy); diff --git a/tests/Unit/Support/ResolutionHelperTest.php b/tests/Unit/Support/ResolutionHelperTest.php index f7d55f1..5385696 100644 --- a/tests/Unit/Support/ResolutionHelperTest.php +++ b/tests/Unit/Support/ResolutionHelperTest.php @@ -9,14 +9,13 @@ use PHPUnit\Framework\Attributes\Test; use Sprout\Exceptions\MisconfigurationException; use Sprout\Exceptions\NoTenantFound; -use Sprout\Managers\IdentityResolverManager; -use Sprout\Managers\TenancyManager; use Sprout\Support\ResolutionHelper; use Sprout\Support\ResolutionHook; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; use function Sprout\resolver; use function Sprout\sprout; +use function Sprout\tenancy; class ResolutionHelperTest extends UnitTestCase { @@ -67,12 +66,12 @@ public function throwsExceptionWhenHandlingResolutionForUnsupportedHook(): void public function returnsFalseIfThereIsAlreadyATenant(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); $tenancy->setTenant(TenantModel::factory()->createOne()); /** @var \Sprout\Contracts\IdentityResolver $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); /** @var \Illuminate\Http\Request $fakeRequest */ $fakeRequest = $this->mock(Request::class); @@ -86,10 +85,10 @@ public function returnsFalseIfThereIsAlreadyATenant(): void public function returnsFalseIfTheResolverCannotResolve(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); /** @var \Sprout\Contracts\IdentityResolver $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); $tenancy->setTenant(TenantModel::factory()->createOne()) ->resolvedVia($resolver) @@ -109,10 +108,10 @@ public function resolvesTenantUsingRouteParameters(): void $tenant = TenantModel::factory()->createOne(); /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); /** @var \Illuminate\Routing\Route $fakeRoute */ $fakeRoute = $this->mock(Route::class, function (MockInterface $mock) use ($tenant, $tenancy, $resolver) { @@ -158,10 +157,10 @@ public function resolvesTenantUsingRouteParameters(): void public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRoute(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); /** @var \Illuminate\Routing\Route $fakeRoute */ $fakeRoute = $this->mock(Route::class, function (MockInterface $mock) use ($tenancy, $resolver) { @@ -199,10 +198,10 @@ public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRoute(): void public function returnsFalseWhenUnableToIdentifyATenantFromTheRouteAndToldNotToThrow(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); /** @var \Illuminate\Routing\Route $fakeRoute */ $fakeRoute = $this->mock(Route::class, function (MockInterface $mock) use ($tenancy, $resolver) { @@ -246,10 +245,10 @@ public function resolvesTenantWithoutRouteParameters(): void $tenant = TenantModel::factory()->createOne(); /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); /** @var \Illuminate\Http\Request $fakeRequest */ $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) use ($tenant) { @@ -283,10 +282,10 @@ public function resolvesTenantWithoutRouteParameters(): void public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRequest(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); /** @var \Illuminate\Http\Request $fakeRequest */ $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) { @@ -312,10 +311,10 @@ public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRequest(): vo public function returnsFalseWhenUnableToIdentifyATenantFromTheRequestAndToldNotToThrow(): void { /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); + $tenancy = tenancy(); /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ - $resolver = app(IdentityResolverManager::class)->get('path'); + $resolver = resolver('path'); /** @var \Illuminate\Http\Request $fakeRequest */ $fakeRequest = $this->mock(Request::class, function (MockInterface $mock) { diff --git a/tests/Unit/TenancyOptionsTest.php b/tests/Unit/TenancyOptionsTest.php index 3f83d2f..9ee73df 100644 --- a/tests/Unit/TenancyOptionsTest.php +++ b/tests/Unit/TenancyOptionsTest.php @@ -6,8 +6,8 @@ use Illuminate\Config\Repository; use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; -use Sprout\Managers\TenancyManager; use Sprout\TenancyOptions; +use function Sprout\tenancy; class TenancyOptionsTest extends UnitTestCase { @@ -40,7 +40,7 @@ public function throwIfNotRelatedOption(): void #[Test, DefineEnvironment('setupSecondTenancy')] public function correctlyReportsHydrateTenantRelationOptionPresence(): void { - $tenancy = app(TenancyManager::class)->get('tenants'); + $tenancy = tenancy('tenants'); $tenancy->removeOption(TenancyOptions::hydrateTenantRelation()); $this->assertFalse(TenancyOptions::shouldHydrateTenantRelation($tenancy)); @@ -49,7 +49,7 @@ public function correctlyReportsHydrateTenantRelationOptionPresence(): void $this->assertTrue(TenancyOptions::shouldHydrateTenantRelation($tenancy)); - $tenancy = app(TenancyManager::class)->get('backup'); + $tenancy = tenancy('backup'); $this->assertFalse(TenancyOptions::shouldHydrateTenantRelation($tenancy)); } @@ -57,7 +57,7 @@ public function correctlyReportsHydrateTenantRelationOptionPresence(): void #[Test, DefineEnvironment('setupSecondTenancy')] public function correctlyReportsThrowIfNotRelatedOptionPresence(): void { - $tenancy = app(TenancyManager::class)->get('tenants'); + $tenancy = tenancy('tenants'); $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); $this->assertFalse(TenancyOptions::shouldThrowIfNotRelated($tenancy)); @@ -66,7 +66,7 @@ public function correctlyReportsThrowIfNotRelatedOptionPresence(): void $this->assertTrue(TenancyOptions::shouldThrowIfNotRelated($tenancy)); - $tenancy = app(TenancyManager::class)->get('backup'); + $tenancy = tenancy('backup'); $this->assertFalse(TenancyOptions::shouldThrowIfNotRelated($tenancy)); } diff --git a/tests/Unit/UnitTestCase.php b/tests/Unit/UnitTestCase.php index 02e2b6a..049d89c 100644 --- a/tests/Unit/UnitTestCase.php +++ b/tests/Unit/UnitTestCase.php @@ -4,8 +4,9 @@ namespace Sprout\Tests\Unit; use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; -abstract class UnitTestCase extends \Orchestra\Testbench\TestCase +abstract class UnitTestCase extends TestCase { use WithWorkbench; From 45d6c800a8dea389d9f3d0a334670aa64cd8da4d Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Tue, 19 Nov 2024 12:56:04 +0000 Subject: [PATCH 16/31] chore: Add RefreshDatabase trait to relevant tests --- tests/Unit/Support/DefaultTenancyTest.php | 3 +++ tests/Unit/Support/ResolutionHelperTest.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/Unit/Support/DefaultTenancyTest.php b/tests/Unit/Support/DefaultTenancyTest.php index a0cfc51..f152494 100644 --- a/tests/Unit/Support/DefaultTenancyTest.php +++ b/tests/Unit/Support/DefaultTenancyTest.php @@ -3,6 +3,7 @@ namespace Sprout\Tests\Unit\Support; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\Test; use Sprout\Events\TenantIdentified; @@ -17,6 +18,8 @@ class DefaultTenancyTest extends UnitTestCase { + use RefreshDatabase; + protected function defineEnvironment($app): void { tap($app['config'], static function ($config) { diff --git a/tests/Unit/Support/ResolutionHelperTest.php b/tests/Unit/Support/ResolutionHelperTest.php index 5385696..91875b5 100644 --- a/tests/Unit/Support/ResolutionHelperTest.php +++ b/tests/Unit/Support/ResolutionHelperTest.php @@ -3,6 +3,7 @@ namespace Sprout\Tests\Unit\Support; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Illuminate\Routing\Route; use Mockery\MockInterface; @@ -19,6 +20,8 @@ class ResolutionHelperTest extends UnitTestCase { + use RefreshDatabase; + protected function defineEnvironment($app): void { tap($app['config'], static function ($config) { From dc25c65e8417288114325f5412d5a594458c637b Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Tue, 19 Nov 2024 12:56:19 +0000 Subject: [PATCH 17/31] test: Add unit test for eloquent tenant provider --- tests/Unit/Providers/EloquentProviderTest.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/Unit/Providers/EloquentProviderTest.php diff --git a/tests/Unit/Providers/EloquentProviderTest.php b/tests/Unit/Providers/EloquentProviderTest.php new file mode 100644 index 0000000..0689aa8 --- /dev/null +++ b/tests/Unit/Providers/EloquentProviderTest.php @@ -0,0 +1,71 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + #[Test] + public function hasARegisteredName(): void + { + $provider = provider('tenants'); + + $this->assertInstanceOf(EloquentTenantProvider::class, $provider); + $this->assertSame('tenants', $provider->getName()); + } + + #[Test] + public function hasAModelClass(): void + { + $provider = provider('tenants'); + + $this->assertInstanceOf(EloquentTenantProvider::class, $provider); + $this->assertSame(TenantModel::class, $provider->getModelClass()); + } + + #[Test] + public function retrievesTenantsByTheirIdentifier(): void + { + $provider = provider('tenants'); + + $tenant = TenantModel::factory()->createOne(); + + $found = $provider->retrieveByIdentifier($tenant->getTenantIdentifier()); + + $this->assertNotNull($found); + $this->assertTrue($tenant->is($found)); + + $this->assertNull($provider->retrieveByIdentifier('fake-identifier')); + } + + #[Test] + public function retrievesTenantsByTheirKey(): void + { + $provider = provider('tenants'); + + $tenant = TenantModel::factory()->createOne(); + + $found = $provider->retrieveByKey($tenant->getTenantKey()); + + $this->assertNotNull($found); + $this->assertTrue($tenant->is($found)); + + $this->assertNull($provider->retrieveByKey(-999)); + } +} From a21189ad8077bba250625d4ca5ef87893f948e7c Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Tue, 19 Nov 2024 12:56:49 +0000 Subject: [PATCH 18/31] test: Add unit test for the database tenant provider --- tests/Unit/Providers/DatabaseProviderTest.php | 120 ++++++++++++++++ workbench/app/CustomTenantEntity.php | 131 ++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 tests/Unit/Providers/DatabaseProviderTest.php create mode 100644 workbench/app/CustomTenantEntity.php diff --git a/tests/Unit/Providers/DatabaseProviderTest.php b/tests/Unit/Providers/DatabaseProviderTest.php new file mode 100644 index 0000000..3ac615e --- /dev/null +++ b/tests/Unit/Providers/DatabaseProviderTest.php @@ -0,0 +1,120 @@ +set('multitenancy.providers.tenants.driver', 'database'); + $config->set('multitenancy.providers.tenants.table', 'tenants'); + }); + } + + protected function withCustomTenantEntity($app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.providers.tenants.entity', CustomTenantEntity::class); + }); + } + + #[Test] + public function hasARegisteredName(): void + { + $provider = provider('tenants'); + + $this->assertInstanceOf(DatabaseTenantProvider::class, $provider); + $this->assertSame('tenants', $provider->getName()); + } + + #[Test] + public function hasATable(): void + { + $provider = provider('tenants'); + + $this->assertInstanceOf(DatabaseTenantProvider::class, $provider); + $this->assertSame('tenants', $provider->getTable()); + } + + #[Test] + public function hasATenantEntity(): void + { + $provider = provider('tenants'); + + $this->assertInstanceOf(DatabaseTenantProvider::class, $provider); + $this->assertSame(GenericTenant::class, $provider->getEntityClass()); + } + + #[Test] + public function retrievesTenantsByTheirIdentifier(): void + { + $provider = provider('tenants'); + + $tenantData = [ + 'name' => 'Test Tenant', + 'identifier' => 'tenant-test', + 'active' => true, + ]; + + $tenantData['id'] = DB::table('tenants')->insertGetId($tenantData); + + $found = $provider->retrieveByIdentifier($tenantData['identifier']); + + $this->assertNotNull($found); + $this->assertInstanceOf(GenericTenant::class, $found); + $this->assertSame($tenantData['identifier'], $found->getTenantIdentifier()); + $this->assertSame($tenantData['id'], $found->getTenantKey()); + + $this->assertNull($provider->retrieveByIdentifier('fake-identifier')); + } + + #[Test] + public function retrievesTenantsByTheirKey(): void + { + $provider = provider('tenants'); + + $tenantData = [ + 'name' => 'Test Tenant', + 'identifier' => 'tenant-test', + 'active' => true, + ]; + + $tenantData['id'] = DB::table('tenants')->insertGetId($tenantData); + + $found = $provider->retrieveByKey($tenantData['id']); + + $this->assertNotNull($found); + $this->assertInstanceOf(GenericTenant::class, $found); + $this->assertSame($tenantData['identifier'], $found->getTenantIdentifier()); + $this->assertSame($tenantData['id'], $found->getTenantKey()); + + $this->assertNull($provider->retrieveByKey(-999)); + } + + #[Test, DefineEnvironment('withCustomTenantEntity')] + public function canHaveCustomTenantEntity(): void + { + // This is necessary as the provider has already been resolved + sprout()->providers()->flushResolved(); + + $provider = provider('tenants'); + + $this->assertInstanceOf(DatabaseTenantProvider::class, $provider); + $this->assertSame(CustomTenantEntity::class, $provider->getEntityClass()); + } +} diff --git a/workbench/app/CustomTenantEntity.php b/workbench/app/CustomTenantEntity.php new file mode 100644 index 0000000..a566c7a --- /dev/null +++ b/workbench/app/CustomTenantEntity.php @@ -0,0 +1,131 @@ + + */ + protected array $attributes; + + /** + * Create a new generic User object. + * + * @param array $attributes + * + * @return void + */ + public function __construct(array $attributes = []) + { + $this->attributes = $attributes; + } + + /** + * Get the tenant identifier + * + * Retrieve the identifier used to publicly identify the tenant. + * + * @return string + */ + public function getTenantIdentifier(): string + { + /** @phpstan-ignore-next-line */ + return $this->attributes[$this->getTenantIdentifierName()]; + } + + /** + * Get the name of the tenant identifier + * + * Retrieve the storage name for the tenant identifier, whether that's an + * attribute, column name, array key or something else. + * Used primarily by {@see \Sprout\Contracts\TenantProvider}. + * + * @return string + */ + public function getTenantIdentifierName(): string + { + return 'identifier'; + } + + /** + * Get the tenant key + * + * Retrieve the key used to identify a tenant internally. + * + * @return int|string + */ + public function getTenantKey(): int|string + { + /** @phpstan-ignore-next-line */ + return $this->attributes[$this->getTenantKeyName()]; + } + + /** + * Get the name of the tenant key + * + * Retrieve the storage name for the tenant key, whether that's an + * attribute, column name, array key or something else. + * Used primarily by {@see \Sprout\Contracts\TenantProvider}. + * + * @return string + */ + public function getTenantKeyName(): string + { + return 'id'; + } + + /** + * Dynamically access the tenant's attributes. + * + * @param string $key + * + * @return mixed + */ + public function __get(string $key): mixed + { + return $this->attributes[$key]; + } + + /** + * Dynamically set an attribute on the tenant. + * + * @param string $key + * @param mixed $value + * + * @return void + */ + public function __set(string $key, mixed $value): void + { + $this->attributes[$key] = $value; + } + + /** + * Dynamically check if a value is set on the tenant. + * + * @param string $key + * + * @return bool + */ + public function __isset(string $key): bool + { + return isset($this->attributes[$key]); + } + + /** + * Dynamically unset a value on the tenant. + * + * @param string $key + * + * @return void + */ + public function __unset(string $key): void + { + unset($this->attributes[$key]); + } +} From 6c118f2d0108c7a8888f0ccaa1668289e362d46b Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 23 Nov 2024 11:45:42 +0000 Subject: [PATCH 19/31] chore: Remove unnecessary service override from AuthOverride --- src/Overrides/AuthOverride.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Overrides/AuthOverride.php b/src/Overrides/AuthOverride.php index c7d41da..fcfdeee 100644 --- a/src/Overrides/AuthOverride.php +++ b/src/Overrides/AuthOverride.php @@ -21,7 +21,7 @@ * * @package Overrides */ -final class AuthOverride implements ServiceOverride, BootableServiceOverride, DeferrableServiceOverride +final class AuthOverride implements BootableServiceOverride, DeferrableServiceOverride { /** * @var \Illuminate\Auth\AuthManager From 98854efe31ec89249a1ba15dddc5d21c8241bddb Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 23 Nov 2024 11:47:43 +0000 Subject: [PATCH 20/31] refactor: Boot service override _AFTER_ the application has booted Remove the manual booting of service overrides from the SproutServiceProvider::boot() method and add to the Application::booted() lifecycle event --- src/SproutServiceProvider.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index f6c1604..a3e3368 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -31,6 +31,7 @@ public function register(): void $this->registerManagers(); $this->registerMiddleware(); $this->registerRouteMixin(); + $this->registerServiceOverrideBooting(); } private function registerSprout(): void @@ -78,13 +79,17 @@ protected function registerRouteMixin(): void Router::mixin(new RouterMethods()); } + protected function registerServiceOverrideBooting(): void + { + $this->app->booted($this->sprout->bootOverrides(...)); + } + public function boot(): void { $this->publishConfig(); $this->registerServiceOverrides(); $this->registerEventListeners(); $this->registerTenancyBootstrappers(); - $this->bootServiceOverrides(); } private function publishConfig(): void @@ -128,9 +133,4 @@ private function registerTenancyBootstrappers(): void $events->listen(CurrentTenantChanged::class, $bootstrapper); } } - - private function bootServiceOverrides(): void - { - $this->sprout->bootOverrides(); - } } From 346a6ee9f20412d17145db1fef01c1d2dc1a9bb1 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 25 Nov 2024 09:44:57 +0000 Subject: [PATCH 21/31] fix: Correct return type for the AddTenantHeaderToResponse middleware --- src/Http/Middleware/AddTenantHeaderToResponse.php | 10 +++++----- src/Http/Middleware/TenantRoutes.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Http/Middleware/AddTenantHeaderToResponse.php b/src/Http/Middleware/AddTenantHeaderToResponse.php index 057d56f..bbda4c2 100644 --- a/src/Http/Middleware/AddTenantHeaderToResponse.php +++ b/src/Http/Middleware/AddTenantHeaderToResponse.php @@ -5,11 +5,10 @@ use Closure; use Illuminate\Http\Request; -use Illuminate\Http\Response; use Sprout\Http\Resolvers\HeaderIdentityResolver; use Sprout\Sprout; use Sprout\Support\ResolutionHelper; -use Sprout\Support\ResolutionHook; +use Symfony\Component\HttpFoundation\Response; /** * Add Tenant Header to Response @@ -43,9 +42,10 @@ public function __construct(Sprout $sprout) * @param \Closure $next * @param string ...$options * - * @return \Illuminate\Http\Response + * @return \Symfony\Component\HttpFoundation\Response * - * @throws \Sprout\Exceptions\NoTenantFound + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Sprout\Exceptions\MisconfigurationException */ public function handle(Request $request, Closure $next, string ...$options): Response { @@ -68,7 +68,7 @@ public function handle(Request $request, Closure $next, string ...$options): Res } return $response->withHeaders([ - $resolver->getRequestHeaderName($tenancy) => $tenancy->identifier() + $resolver->getRequestHeaderName($tenancy) => $tenancy->identifier(), ]); } } diff --git a/src/Http/Middleware/TenantRoutes.php b/src/Http/Middleware/TenantRoutes.php index 941d501..1bc981f 100644 --- a/src/Http/Middleware/TenantRoutes.php +++ b/src/Http/Middleware/TenantRoutes.php @@ -48,7 +48,7 @@ public function __construct(Sprout $sprout) * @param \Closure $next * @param string ...$options * - * @return \Illuminate\Http\Response + * @return \Symfony\Component\HttpFoundation\Response * * @throws \Sprout\Exceptions\NoTenantFound * @throws \Illuminate\Contracts\Container\BindingResolutionException From a92444ddc05e4c6d906f0932489b09635a66dcff Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 15 Dec 2024 17:47:14 +0000 Subject: [PATCH 22/31] chore: Add Sprout facade --- composer.json | 3 +++ src/Facades/Sprout.php | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/Facades/Sprout.php diff --git a/composer.json b/composer.json index 0bd998d..a8895f1 100644 --- a/composer.json +++ b/composer.json @@ -70,6 +70,9 @@ "laravel": { "providers": [ "Sprout\\SproutServiceProvider" + ], + "facades":[ + "Sprout\\Facades\\Sprout" ] } }, diff --git a/src/Facades/Sprout.php b/src/Facades/Sprout.php new file mode 100644 index 0000000..97d1c46 --- /dev/null +++ b/src/Facades/Sprout.php @@ -0,0 +1,49 @@ + getAllCurrentTenancies() + * @method static array getCurrentOverrides(?Tenancy $tenancy = null) + * @method static Tenancy|null getCurrentTenancy() + * @method static array getOverrides() + * @method static array getRegisteredOverrides() + * @method static bool hasBootedOverride(string $class) + * @method static bool hasCurrentTenancy() + * @method static bool hasOverride(string $class) + * @method static bool hasRegisteredOverride(string $class) + * @method static bool hasSetupOverride(Tenancy $tenancy, string $class) + * @method static bool haveOverridesBooted() + * @method static bool isBootableOverride(string $class) + * @method static \Sprout\Sprout markAsInContext() + * @method static \Sprout\Sprout markAsOutsideContext() + * @method static ProviderManager providers() + * @method static \Sprout\Sprout registerOverride(string $class) + * @method static IdentityResolverManager resolvers() + * @method static void setCurrentTenancy(Tenancy $tenancy) + * @method static void setupOverrides(Tenancy $tenancy, Tenant $tenant) + * @method static bool supportsHook(ResolutionHook $hook) + * @method static \Sprout\Managers\TenancyManager tenancies() + * @method static bool withinContext() + */ +final class Sprout extends Facade +{ + protected static function getFacadeAccessor(): string + { + return \Sprout\Sprout::class; + } +} From 0e3a1ef405a38d6a45d5252623c820a42a5b17c2 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 15 Dec 2024 19:26:43 +0000 Subject: [PATCH 23/31] test: Initial rework of unit tests --- composer.json | 13 +- infection.json5 | 16 ++ src/Concerns/HandlesServiceOverrides.php | 34 +-- src/Http/Resolvers/CookieIdentityResolver.php | 10 + src/Http/Resolvers/PathIdentityResolver.php | 5 +- .../Resolvers/SubdomainIdentityResolver.php | 27 ++- src/Managers/IdentityResolverManager.php | 4 +- src/Managers/ProviderManager.php | 4 +- src/Managers/TenancyManager.php | 4 +- src/Support/BaseFactory.php | 35 +++- src/Support/BaseIdentityResolver.php | 10 + .../Resolvers/CookieIdentityResolverTest.php | 131 ++++++++++++ .../Resolvers/HeaderIdentityResolverTest.php | 124 +++++++++++ .../Resolvers/PathIdentityResolverTest.php | 163 +++++++++++++++ .../Resolvers/SessionIdentityResolverTest.php | 96 +++++++++ .../SubdomainIdentityResolverTest.php | 145 +++++++++++++ .../Managers/IdentityResolverManagerTest.php | 179 ++++++++++++++++ tests/Unit/Managers/ProviderManagerTest.php | 194 ++++++++++++++++++ tests/Unit/Managers/TenancyManager.php | 64 ++++++ 19 files changed, 1217 insertions(+), 41 deletions(-) create mode 100644 infection.json5 create mode 100644 tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php create mode 100644 tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php create mode 100644 tests/Unit/Http/Resolvers/PathIdentityResolverTest.php create mode 100644 tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php create mode 100644 tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php create mode 100644 tests/Unit/Managers/IdentityResolverManagerTest.php create mode 100644 tests/Unit/Managers/ProviderManagerTest.php create mode 100644 tests/Unit/Managers/TenancyManager.php diff --git a/composer.json b/composer.json index a8895f1..2cba323 100644 --- a/composer.json +++ b/composer.json @@ -3,14 +3,15 @@ "description" : "A flexible, seamless and easy to use multitenancy solution for Laravel", "type" : "library", "require" : { - "php" : "^8.2", - "laravel/framework": "^11.32", + "php" : "^8.2", + "laravel/framework" : "^11.32", "league/flysystem-path-prefixing": "^3.0" }, "require-dev" : { "phpunit/phpunit" : "^11.0.1", "orchestra/testbench": "^9.4", - "larastan/larastan" : "^2.9" + "larastan/larastan" : "^2.9", + "infection/infection": "^0.29.8" }, "license" : "MIT", "autoload" : { @@ -64,6 +65,12 @@ "@prepare", "@build", "@php vendor/bin/phpunit --testsuite=Unit,Feature" + ], + "mutation" : [ + "@clear", + "@prepare", + "@build", + "@php vendor/bin/infection --threads=12" ] }, "extra" : { diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..d7b22fd --- /dev/null +++ b/infection.json5 @@ -0,0 +1,16 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "build/infection.log", + "html": "build/infection.html" + }, + "mutators": { + "@default": true + }, + "testFrameworkOptions": "--testsuite=Unit,Feature" +} diff --git a/src/Concerns/HandlesServiceOverrides.php b/src/Concerns/HandlesServiceOverrides.php index fbcdc95..2859761 100644 --- a/src/Concerns/HandlesServiceOverrides.php +++ b/src/Concerns/HandlesServiceOverrides.php @@ -50,25 +50,25 @@ trait HandlesServiceOverrides /** * Register a service override * - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * @param class-string<\Sprout\Contracts\ServiceOverride> $class * * @return static * * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - public function registerOverride(string $overrideClass): static + public function registerOverride(string $class): static { - if (! is_subclass_of($overrideClass, ServiceOverride::class)) { - throw new InvalidArgumentException('Provided service override [' . $overrideClass . '] does not implement ' . ServiceOverride::class); + if (! is_subclass_of($class, ServiceOverride::class)) { + throw new InvalidArgumentException('Provided service override [' . $class . '] does not implement ' . ServiceOverride::class); } // Flag the service override as being registered - $this->registeredOverrides[] = $overrideClass; + $this->registeredOverrides[] = $class; - if (is_subclass_of($overrideClass, DeferrableServiceOverride::class)) { - $this->registerDeferrableOverride($overrideClass); + if (is_subclass_of($class, DeferrableServiceOverride::class)) { + $this->registerDeferrableOverride($class); } else { - $this->processOverride($overrideClass); + $this->processOverride($class); } return $this; @@ -145,13 +145,13 @@ protected function registerDeferrableOverride(string $overrideClass): static /** * Check if a service override is bootable * - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * @param class-string<\Sprout\Contracts\ServiceOverride> $class * * @return bool */ - public function isBootableOverride(string $overrideClass): bool + public function isBootableOverride(string $class): bool { - return isset($this->bootableOverrides[$overrideClass]); + return isset($this->bootableOverrides[$class]); } /** @@ -160,13 +160,13 @@ public function isBootableOverride(string $overrideClass): bool * This method returns true if the service override has been booted, or * false if either it hasn't, or it isn't bootable. * - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * @param class-string<\Sprout\Contracts\ServiceOverride> $class * * @return bool */ - public function hasBootedOverride(string $overrideClass): bool + public function hasBootedOverride(string $class): bool { - return $this->bootedOverrides[$overrideClass] ?? false; + return $this->bootedOverrides[$class] ?? false; } /** @@ -224,13 +224,13 @@ protected function bootOverride(string $overrideClass): void * Check if a service override has been set up * * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass + * @param class-string<\Sprout\Contracts\ServiceOverride> $class * * @return bool */ - public function hasSetupOverride(Tenancy $tenancy, string $overrideClass): bool + public function hasSetupOverride(Tenancy $tenancy, string $class): bool { - return $this->setupOverrides[$tenancy->getName()][$overrideClass] ?? false; + return $this->setupOverrides[$tenancy->getName()][$class] ?? false; } /** diff --git a/src/Http/Resolvers/CookieIdentityResolver.php b/src/Http/Resolvers/CookieIdentityResolver.php index 4e9831d..107ed3d 100644 --- a/src/Http/Resolvers/CookieIdentityResolver.php +++ b/src/Http/Resolvers/CookieIdentityResolver.php @@ -64,6 +64,16 @@ public function getCookieName(): string return $this->cookie; } + /** + * Get the extra cookie options + * + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + /** * Get the cookie name with replacements * diff --git a/src/Http/Resolvers/PathIdentityResolver.php b/src/Http/Resolvers/PathIdentityResolver.php index efc4670..86b5e37 100644 --- a/src/Http/Resolvers/PathIdentityResolver.php +++ b/src/Http/Resolvers/PathIdentityResolver.php @@ -103,10 +103,9 @@ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): { return $this->applyParameterPatternMapping( $router->middleware([TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName()]) - ->prefix($this->getRoutePrefix($tenancy)) - ->group($groupRoutes), + ->prefix($this->getRoutePrefix($tenancy)), $tenancy - ); + )->group($groupRoutes); } /** diff --git a/src/Http/Resolvers/SubdomainIdentityResolver.php b/src/Http/Resolvers/SubdomainIdentityResolver.php index 07b46ca..9856948 100644 --- a/src/Http/Resolvers/SubdomainIdentityResolver.php +++ b/src/Http/Resolvers/SubdomainIdentityResolver.php @@ -41,11 +41,11 @@ final class SubdomainIdentityResolver extends BaseIdentityResolver implements Id /** * Create a new instance * - * @param string $name - * @param string $domain - * @param string|null $pattern - * @param string|null $parameter - * @param array<\Sprout\Support\ResolutionHook> $hooks + * @param string $name + * @param string $domain + * @param string|null $pattern + * @param string|null $parameter + * @param array<\Sprout\Support\ResolutionHook> $hooks */ public function __construct(string $name, string $domain, ?string $pattern = null, ?string $parameter = null, array $hooks = []) { @@ -79,6 +79,16 @@ public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string return null; } + /** + * Get the domain the subdomains belong to + * + * @return string + */ + public function getDomain(): string + { + return $this->domain; + } + /** * Get the domain name with parameter for the route definition * @@ -88,7 +98,7 @@ public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string * * @return string */ - protected function getRouteDomain(Tenancy $tenancy): string + public function getRouteDomain(Tenancy $tenancy): string { return $this->getRouteParameter($tenancy) . '.' . $this->domain; } @@ -111,10 +121,9 @@ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): { return $this->applyParameterPatternMapping( $router->domain($this->getRouteDomain($tenancy)) - ->middleware([TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName()]) - ->group($groupRoutes), + ->middleware([TenantRoutes::ALIAS . ':' . $this->getName() . ',' . $tenancy->getName()]), $tenancy - ); + )->group($groupRoutes); } /** diff --git a/src/Managers/IdentityResolverManager.php b/src/Managers/IdentityResolverManager.php index 5035374..ae6a7d7 100644 --- a/src/Managers/IdentityResolverManager.php +++ b/src/Managers/IdentityResolverManager.php @@ -28,7 +28,7 @@ final class IdentityResolverManager extends BaseFactory * * @return string */ - protected function getFactoryName(): string + public function getFactoryName(): string { return 'resolver'; } @@ -40,7 +40,7 @@ protected function getFactoryName(): string * * @return string */ - protected function getConfigKey(string $name): string + public function getConfigKey(string $name): string { return 'multitenancy.resolvers.' . $name; } diff --git a/src/Managers/ProviderManager.php b/src/Managers/ProviderManager.php index 9bc152d..804a51f 100644 --- a/src/Managers/ProviderManager.php +++ b/src/Managers/ProviderManager.php @@ -28,7 +28,7 @@ final class ProviderManager extends BaseFactory * * @return string */ - protected function getFactoryName(): string + public function getFactoryName(): string { return 'provider'; } @@ -40,7 +40,7 @@ protected function getFactoryName(): string * * @return string */ - protected function getConfigKey(string $name): string + public function getConfigKey(string $name): string { return 'multitenancy.providers.' . $name; } diff --git a/src/Managers/TenancyManager.php b/src/Managers/TenancyManager.php index 46d2e23..a8462e3 100644 --- a/src/Managers/TenancyManager.php +++ b/src/Managers/TenancyManager.php @@ -40,7 +40,7 @@ public function __construct(Application $app, ProviderManager $providerManager) * * @return string */ - protected function getFactoryName(): string + public function getFactoryName(): string { return 'tenancy'; } @@ -52,7 +52,7 @@ protected function getFactoryName(): string * * @return string */ - protected function getConfigKey(string $name): string + public function getConfigKey(string $name): string { return 'multitenancy.tenancies.' . $name; } diff --git a/src/Support/BaseFactory.php b/src/Support/BaseFactory.php index 2d4035a..d2dd4e2 100644 --- a/src/Support/BaseFactory.php +++ b/src/Support/BaseFactory.php @@ -69,12 +69,25 @@ public function __construct(Application $app) $this->app = $app; } + /** + * Check if a factory has a driver + * + * @param string $name + * + * @return bool + */ + public function hasDriver(string $name): bool + { + return isset(static::$customCreators[$name]) + || method_exists($this, 'create' . ucfirst($name) . ucfirst($this->getFactoryName())); + } + /** * Get the name used by this factory * * @return string */ - abstract protected function getFactoryName(): string; + abstract public function getFactoryName(): string; /** * Get the config key for the given name @@ -83,7 +96,7 @@ abstract protected function getFactoryName(): string; * * @return string */ - abstract protected function getConfigKey(string $name): string; + abstract public function getConfigKey(string $name): string; /** * Get the default name @@ -92,7 +105,7 @@ abstract protected function getConfigKey(string $name): string; * * @throws \Sprout\Exceptions\MisconfigurationException */ - protected function getDefaultName(): string + public function getDefaultName(): string { /** @var \Illuminate\Config\Repository $config */ $config = app('config'); @@ -230,4 +243,20 @@ public function flushResolved(): static return $this; } + + /** + * Check if a driver has already been resolved + * + * @param string|null $name + * + * @return bool + */ + public function hasResolved(?string $name = null): bool + { + if ($name === null) { + return ! empty($this->objects); + } + + return isset($this->objects[$name]); + } } diff --git a/src/Support/BaseIdentityResolver.php b/src/Support/BaseIdentityResolver.php index a1e957d..ac7bf49 100644 --- a/src/Support/BaseIdentityResolver.php +++ b/src/Support/BaseIdentityResolver.php @@ -50,6 +50,16 @@ public function getName(): string return $this->name; } + /** + * Get the hooks this resolver uses + * + * @return array<\Sprout\Support\ResolutionHook> + */ + public function getHooks(): array + { + return $this->hooks; + } + /** * Perform setup actions for the tenant * diff --git a/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php new file mode 100644 index 0000000..6050ebf --- /dev/null +++ b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php @@ -0,0 +1,131 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + protected function withCustomCookieName(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.cookie.cookie', 'Custom-Cookie-Name'); + }); + } + + protected function withCustomOptions(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.cookie.options', [ + 'httpOnly' => true, + 'secure' => true, + 'sameSite' => true, + ]); + }); + } + + protected function withCustomCookieNamePattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.cookie.cookie', '{Tenancy}-{tenancy}-{Resolver}-{resolver}'); + }); + } + + #[Test] + public function isRegisteredAndCanBeAccessed(): void + { + $resolver = resolver('cookie'); + + $this->assertInstanceOf(CookieIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-Identifier', $resolver->getCookieName()); + $this->assertEmpty($resolver->getOptions()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test] + public function replacesPlaceholdersInCookieName(): void + { + $resolver = resolver('cookie'); + $tenancy = tenancy(); + + $this->assertInstanceOf(CookieIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-Identifier', $resolver->getCookieName()); + $this->assertSame(ucfirst($tenancy->getName()) . '-Identifier', $resolver->getRequestCookieName($tenancy)); + } + + #[Test, DefineEnvironment('withCustomCookieName')] + public function acceptsCustomCookieName(): void + { + $resolver = resolver('cookie'); + + $this->assertInstanceOf(CookieIdentityResolver::class, $resolver); + $this->assertNotSame('{Tenancy}-Identifier', $resolver->getCookieName()); + $this->assertSame('Custom-Cookie-Name', $resolver->getCookieName()); + $this->assertEmpty($resolver->getOptions()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomCookieName')] + public function replacesAllPlaceholders(): void + { + $resolver = resolver('cookie'); + + $this->assertInstanceOf(CookieIdentityResolver::class, $resolver); + $this->assertNotSame('{Tenancy}-Identifier', $resolver->getCookieName()); + $this->assertSame('Custom-Cookie-Name', $resolver->getCookieName()); + $this->assertEmpty($resolver->getOptions()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomOptions')] + public function acceptsCustomOptions(): void + { + $resolver = resolver('cookie'); + $options = $resolver->getOptions(); + + $this->assertInstanceOf(CookieIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-Identifier', $resolver->getCookieName()); + $this->assertNotEmpty($options); + $this->assertArrayHasKey('httpOnly', $options); + $this->assertArrayHasKey('secure', $options); + $this->assertArrayHasKey('sameSite', $options); + $this->assertTrue($options['httpOnly']); + $this->assertTrue($options['secure']); + $this->assertTrue($options['sameSite']); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomCookieNamePattern')] + public function replacesAllPlaceholdersInCookieName(): void + { + $resolver = resolver('cookie'); + $tenancy = tenancy(); + + $this->assertInstanceOf(CookieIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-{tenancy}-{Resolver}-{resolver}', $resolver->getCookieName()); + $this->assertSame( + ucfirst($tenancy->getName()) . '-' . $tenancy->getName() . '-' . ucfirst($resolver->getName()) . '-' . $resolver->getName(), + $resolver->getRequestCookieName($tenancy) + ); + $this->assertEmpty($resolver->getOptions()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } +} diff --git a/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php b/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php new file mode 100644 index 0000000..c1c8403 --- /dev/null +++ b/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php @@ -0,0 +1,124 @@ +set('multitenancy.resolvers.header.header', 'Custom-Header-Name'); + }); + } + + protected function defineRoutes($router) + { + $router->tenanted(function () { + Route::get('/tenant', function () { + })->name('tenant-route'); + }, 'header'); + } + + protected function withCustomOptions(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.header.options', [ + 'httpOnly' => true, + 'secure' => true, + 'sameSite' => true, + ]); + }); + } + + protected function withCustomHeaderNamePattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.header.header', '{Tenancy}-{tenancy}-{Resolver}-{resolver}'); + }); + } + + #[Test] + public function isRegisteredAndCanBeAccessed(): void + { + $resolver = resolver('header'); + + $this->assertInstanceOf(HeaderIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-Identifier', $resolver->getHeaderName()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test] + public function replacesPlaceholdersInHeaderName(): void + { + $resolver = resolver('header'); + $tenancy = tenancy(); + + $this->assertInstanceOf(HeaderIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-Identifier', $resolver->getHeaderName()); + $this->assertSame(ucfirst($tenancy->getName()) . '-Identifier', $resolver->getRequestHeaderName($tenancy)); + } + + #[Test, DefineEnvironment('withCustomHeaderName')] + public function acceptsCustomHeaderName(): void + { + $resolver = resolver('header'); + + $this->assertInstanceOf(HeaderIdentityResolver::class, $resolver); + $this->assertNotSame('{Tenancy}-Identifier', $resolver->getHeaderName()); + $this->assertSame('Custom-Header-Name', $resolver->getHeaderName()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomHeaderName')] + public function replacesAllPlaceholders(): void + { + $resolver = resolver('header'); + + $this->assertInstanceOf(HeaderIdentityResolver::class, $resolver); + $this->assertNotSame('{Tenancy}-Identifier', $resolver->getHeaderName()); + $this->assertSame('Custom-Header-Name', $resolver->getHeaderName()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomHeaderNamePattern')] + public function acceptsCustomOptions(): void + { + $resolver = resolver('header'); + $tenancy = tenancy(); + + $this->assertInstanceOf(HeaderIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-{tenancy}-{Resolver}-{resolver}', $resolver->getHeaderName()); + $this->assertSame( + ucfirst($tenancy->getName()) . '-' . $tenancy->getName() . '-' . ucfirst($resolver->getName()) . '-' . $resolver->getName(), + $resolver->getRequestHeaderName($tenancy) + ); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test] + public function addsTenantHeaderResponseMiddlewareToRoutes(): void + { + $resolver = resolver('header'); + $tenancy = tenancy(); + $routes = app(Router::class)->getRoutes(); + + $this->assertTrue($routes->hasNamedRoute('tenant-route')); + + $middleware = $routes->getByName('tenant-route')->middleware(); + + $this->assertContains(AddTenantHeaderToResponse::class . ':' . $resolver->getName() . ',' . $tenancy->getName(), $middleware); + } +} diff --git a/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php b/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php new file mode 100644 index 0000000..2680d1c --- /dev/null +++ b/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php @@ -0,0 +1,163 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + protected function withCustomSegment(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.path.segment', 2); + }); + } + + protected function withCustomParameterPattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.path.pattern', '.*'); + }); + } + + protected function withCustomParameterName(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.path.parameter', 'custom_parameter_name'); + }); + } + + protected function withCustomParameterNamePattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.path.parameter', '{resolver}_{tenancy}'); + }); + } + + protected function withCustomPathNamePattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.path.path', '{Tenancy}-{tenancy}-{Resolver}-{resolver}'); + }); + } + + protected function withTenantedRoute(Router $router): void + { + Route::tenanted(static function () { + Route::get('/tenant', function () { + })->name('tenant-route'); + }, 'path'); + } + + #[Test] + public function isRegisteredAndCanBeAccessed(): void + { + $resolver = resolver('path'); + + $this->assertInstanceOf(PathIdentityResolver::class, $resolver); + $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame(1, $resolver->getSegment()); + $this->assertNull($resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test] + public function replacesPlaceholdersInPathName(): void + { + $resolver = resolver('path'); + $tenancy = tenancy(); + + $this->assertInstanceOf(PathIdentityResolver::class, $resolver); + $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame(1, $resolver->getSegment()); + $this->assertSame($tenancy->getName() . '_' . $resolver->getName(), $resolver->getRouteParameterName($tenancy)); + $this->assertSame('{' . $tenancy->getName() . '_' . $resolver->getName() . '}', $resolver->getRoutePrefix($tenancy)); + $this->assertNull($resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomSegment')] + public function acceptsCustomSegment(): void + { + $resolver = resolver('path'); + + $this->assertInstanceOf(PathIdentityResolver::class, $resolver); + $this->assertSame(2, $resolver->getSegment()); + $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertNull($resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterPattern')] + public function acceptsCustomParameterPattern(): void + { + $resolver = resolver('path'); + + $this->assertInstanceOf(PathIdentityResolver::class, $resolver); + $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame(1, $resolver->getSegment()); + $this->assertSame('.*', $resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterName')] + public function acceptsCustomPathName(): void + { + $resolver = resolver('path'); + + $this->assertInstanceOf(PathIdentityResolver::class, $resolver); + $this->assertNotSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame('custom_parameter_name', $resolver->getParameter()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterNamePattern')] + public function replacesAllPlaceholdersInPathName(): void + { + $resolver = resolver('path'); + $tenancy = tenancy(); + + $this->assertInstanceOf(PathIdentityResolver::class, $resolver); + $this->assertSame('{resolver}_{tenancy}', $resolver->getParameter()); + $this->assertSame(1, $resolver->getSegment()); + $this->assertSame($resolver->getName() . '_' . $tenancy->getName(), $resolver->getRouteParameterName($tenancy)); + $this->assertNull($resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterPattern'), DefineRoute('withTenantedRoute')] + public function setsUpRouteProperly(): void + { + $resolver = resolver('path'); + $tenancy = tenancy(); + $routes = app(Router::class)->getRoutes(); + + $this->assertTrue($resolver->hasPattern()); + $this->assertTrue($routes->hasNamedRoute('tenant-route')); + + $route = $routes->getByName('tenant-route'); + + $this->assertContains($tenancy->getName() . '_' . $resolver->getName(), $route->parameterNames()); + $this->assertArrayHasKey($tenancy->getName() . '_' . $resolver->getName(), $route->wheres); + $this->assertSame('.*', $route->wheres[$tenancy->getName() . '_' . $resolver->getName()]); + } +} diff --git a/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php b/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php new file mode 100644 index 0000000..8bfb9f8 --- /dev/null +++ b/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php @@ -0,0 +1,96 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + protected function withCustomSessionName(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.session.session', 'Custom-Session-Name'); + }); + } + + protected function withCustomSessionNamePattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.session.session', '{Tenancy}-{tenancy}-{Resolver}-{resolver}'); + }); + } + + #[Test] + public function isRegisteredAndCanBeAccessed(): void + { + $resolver = resolver('session'); + + $this->assertInstanceOf(SessionIdentityResolver::class, $resolver); + $this->assertSame('multitenancy.{tenancy}', $resolver->getSessionName()); + $this->assertSame([ResolutionHook::Middleware], $resolver->getHooks()); + } + + #[Test] + public function replacesPlaceholdersInSessionName(): void + { + $resolver = resolver('session'); + $tenancy = tenancy(); + + $this->assertInstanceOf(SessionIdentityResolver::class, $resolver); + $this->assertSame('multitenancy.{tenancy}', $resolver->getSessionName()); + $this->assertSame('multitenancy.' . $tenancy->getName(), $resolver->getRequestSessionName($tenancy)); + } + + #[Test, DefineEnvironment('withCustomSessionName')] + public function acceptsCustomSessionName(): void + { + $resolver = resolver('session'); + + $this->assertInstanceOf(SessionIdentityResolver::class, $resolver); + $this->assertNotSame('multitenancy.{tenancy}', $resolver->getSessionName()); + $this->assertSame('Custom-Session-Name', $resolver->getSessionName()); + $this->assertSame([ResolutionHook::Middleware], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomSessionName')] + public function replacesAllPlaceholders(): void + { + $resolver = resolver('session'); + + $this->assertInstanceOf(SessionIdentityResolver::class, $resolver); + $this->assertNotSame('multitenancy.{tenancy}', $resolver->getSessionName()); + $this->assertSame('Custom-Session-Name', $resolver->getSessionName()); + $this->assertSame([ResolutionHook::Middleware], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomSessionNamePattern')] + public function replacesAllPlaceholdersInSessionName(): void + { + $resolver = resolver('session'); + $tenancy = tenancy(); + + $this->assertInstanceOf(SessionIdentityResolver::class, $resolver); + $this->assertSame('{Tenancy}-{tenancy}-{Resolver}-{resolver}', $resolver->getSessionName()); + $this->assertSame( + ucfirst($tenancy->getName()) . '-' . $tenancy->getName() . '-' . ucfirst($resolver->getName()) . '-' . $resolver->getName(), + $resolver->getRequestSessionName($tenancy) + ); + $this->assertSame([ResolutionHook::Middleware], $resolver->getHooks()); + } +} diff --git a/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php b/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php new file mode 100644 index 0000000..bdade30 --- /dev/null +++ b/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php @@ -0,0 +1,145 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + $config->set('multitenancy.resolvers.subdomain.domain', 'localhost'); + }); + } + + protected function withCustomParameterPattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.subdomain.pattern', '.*'); + }); + } + + protected function withCustomParameterName(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.subdomain.parameter', 'custom_parameter_name'); + }); + } + + protected function withCustomParameterNamePattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.subdomain.parameter', '{resolver}_{tenancy}'); + }); + } + + protected function withCustomSubdomainPattern(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('multitenancy.resolvers.subdomain.subdomain', '{Tenancy}-{tenancy}-{Resolver}-{resolver}'); + }); + } + + protected function withTenantedRoute(Router $router): void + { + Route::tenanted(static function () { + Route::get('/tenant', function () { + })->name('tenant-route'); + }, 'subdomain'); + } + + #[Test] + public function isRegisteredAndCanBeAccessed(): void + { + $resolver = resolver('subdomain'); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $resolver); + $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame('localhost', $resolver->getDomain()); + $this->assertSame('.*', $resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test] + public function replacesPlaceholdersInSubdomainName(): void + { + $resolver = resolver('subdomain'); + $tenancy = tenancy(); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $resolver); + $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame('localhost', $resolver->getDomain()); + $this->assertSame($tenancy->getName() . '_' . $resolver->getName(), $resolver->getRouteParameterName($tenancy)); + $this->assertSame('.*', $resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterPattern')] + public function acceptsCustomParameterPattern(): void + { + $resolver = resolver('subdomain'); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $resolver); + $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame('localhost', $resolver->getDomain()); + $this->assertSame('.*', $resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterName')] + public function acceptsCustomSubdomainName(): void + { + $resolver = resolver('subdomain'); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $resolver); + $this->assertNotSame('{tenancy}_{resolver}', $resolver->getParameter()); + $this->assertSame('custom_parameter_name', $resolver->getParameter()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterNamePattern')] + public function replacesAllPlaceholdersInSubdomainName(): void + { + $resolver = resolver('subdomain'); + $tenancy = tenancy(); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $resolver); + $this->assertSame('{resolver}_{tenancy}', $resolver->getParameter()); + $this->assertSame('localhost', $resolver->getDomain()); + $this->assertSame($resolver->getName() . '_' . $tenancy->getName(), $resolver->getRouteParameterName($tenancy)); + $this->assertSame('{' . $resolver->getName() . '_' . $tenancy->getName() . '}.localhost', $resolver->getRouteDomain($tenancy)); + $this->assertSame('.*', $resolver->getPattern()); + $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); + } + + #[Test, DefineEnvironment('withCustomParameterPattern'), DefineRoute('withTenantedRoute')] + public function setsUpRouteProperly(): void + { + $resolver = resolver('subdomain'); + $tenancy = tenancy(); + $routes = app(Router::class)->getRoutes(); + + $this->assertTrue($resolver->hasPattern()); + $this->assertTrue($routes->hasNamedRoute('tenant-route')); + + $route = $routes->getByName('tenant-route'); + + $this->assertContains($tenancy->getName() . '_' . $resolver->getName(), $route->parameterNames()); + $this->assertArrayHasKey($tenancy->getName() . '_' . $resolver->getName(), $route->wheres); + $this->assertSame('.*', $route->wheres[$tenancy->getName() . '_' . $resolver->getName()]); + } +} diff --git a/tests/Unit/Managers/IdentityResolverManagerTest.php b/tests/Unit/Managers/IdentityResolverManagerTest.php new file mode 100644 index 0000000..d7d0bfd --- /dev/null +++ b/tests/Unit/Managers/IdentityResolverManagerTest.php @@ -0,0 +1,179 @@ +set('multitenancy.resolvers.subdomain.domain', 'localhost'); + }); + } + + #[Test] + public function isNamedCorrectly(): void + { + $manager = sprout()->resolvers(); + + $this->assertSame('resolver', $manager->getFactoryName()); + } + + #[Test] + public function getsTheDefaultNameFromTheConfig(): void + { + $manager = sprout()->resolvers(); + + $this->assertSame('subdomain', $manager->getDefaultName()); + + config()->set('multitenancy.defaults.resolver', 'path'); + + $this->assertSame('path', $manager->getDefaultName()); + } + + #[Test] + public function generatesConfigKeys(): void + { + $manager = sprout()->resolvers(); + + $this->assertSame('multitenancy.resolvers.test-config', $manager->getConfigKey('test-config')); + } + + #[Test] + public function hasDefaultFirstPartyDrivers(): void + { + $manager = sprout()->resolvers(); + + $this->assertFalse($manager->hasResolved()); + + $this->assertTrue($manager->hasDriver('subdomain')); + $this->assertTrue($manager->hasDriver('path')); + $this->assertTrue($manager->hasDriver('header')); + $this->assertTrue($manager->hasDriver('cookie')); + $this->assertTrue($manager->hasDriver('session')); + + $this->assertFalse($manager->hasResolved()); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $manager->get('subdomain')); + $this->assertInstanceOf(PathIdentityResolver::class, $manager->get('path')); + $this->assertInstanceOf(HeaderIdentityResolver::class, $manager->get('header')); + $this->assertInstanceOf(CookieIdentityResolver::class, $manager->get('cookie')); + $this->assertInstanceOf(SessionIdentityResolver::class, $manager->get('session')); + + $this->assertTrue($manager->hasResolved('subdomain')); + $this->assertTrue($manager->hasResolved('path')); + $this->assertTrue($manager->hasResolved('header')); + $this->assertTrue($manager->hasResolved('cookie')); + $this->assertTrue($manager->hasResolved('session')); + } + + #[Test] + public function canFlushResolvedInstances(): void + { + $manager = sprout()->resolvers(); + + $this->assertFalse($manager->hasResolved()); + + $this->assertTrue($manager->hasDriver('subdomain')); + $this->assertTrue($manager->hasDriver('path')); + $this->assertTrue($manager->hasDriver('header')); + $this->assertTrue($manager->hasDriver('cookie')); + $this->assertTrue($manager->hasDriver('session')); + + $this->assertFalse($manager->hasResolved()); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $manager->get('subdomain')); + $this->assertInstanceOf(PathIdentityResolver::class, $manager->get('path')); + $this->assertInstanceOf(HeaderIdentityResolver::class, $manager->get('header')); + $this->assertInstanceOf(CookieIdentityResolver::class, $manager->get('cookie')); + $this->assertInstanceOf(SessionIdentityResolver::class, $manager->get('session')); + + $this->assertTrue($manager->hasResolved('subdomain')); + $this->assertTrue($manager->hasResolved('path')); + $this->assertTrue($manager->hasResolved('header')); + $this->assertTrue($manager->hasResolved('cookie')); + $this->assertTrue($manager->hasResolved('session')); + + $manager->flushResolved(); + + $this->assertFalse($manager->hasResolved('subdomain')); + $this->assertFalse($manager->hasResolved('path')); + $this->assertFalse($manager->hasResolved('header')); + $this->assertFalse($manager->hasResolved('cookie')); + $this->assertFalse($manager->hasResolved('session')); + } + + #[Test] + public function errorsIfTheresNoConfigCanBeFoundForADriver(): void + { + $manager = sprout()->resolvers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The config for [resolver::missing] could not be found'); + + $manager->get('missing'); + } + + #[Test] + public function errorsIfTheresNoCreatorForADriver(): void + { + $manager = sprout()->resolvers(); + + config()->set('multitenancy.resolvers.missing', []); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The creator for [resolver::missing] could not be found'); + + $manager->get('missing'); + } + + #[Test] + public function errorsIfNoSubdomainDomainWasProvided(): void + { + config()->set('multitenancy.resolvers.subdomain.domain', null); + + $manager = sprout()->resolvers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The resolver [subdomain] is missing a required value for \'domain\''); + + $manager->get('subdomain'); + } + + #[Test] + public function errorsIfPathSegmentIsInvalid(): void + { + config()->set('multitenancy.resolvers.path.segment', -7); + + $manager = sprout()->resolvers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The provided value for \'segment\' is not valid for resolver [path]'); + + $manager->get('path'); + } + + #[Test] + public function allowsCustomCreators(): void + { + config()->set('multitenancy.resolvers.path.driver', 'hello-there'); + + IdentityResolverManager::register('hello-there', static function () { + return new SubdomainIdentityResolver('hello-there', 'somedomain.local'); + }); + + $manager = sprout()->resolvers(); + } +} diff --git a/tests/Unit/Managers/ProviderManagerTest.php b/tests/Unit/Managers/ProviderManagerTest.php new file mode 100644 index 0000000..e9cd2c3 --- /dev/null +++ b/tests/Unit/Managers/ProviderManagerTest.php @@ -0,0 +1,194 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + $config->set('multitenancy.providers.backup', ['driver' => 'database', 'table' => 'tenants']); + }); + } + + #[Test] + public function isNamedCorrectly(): void + { + $manager = sprout()->providers(); + + $this->assertSame('provider', $manager->getFactoryName()); + } + + #[Test] + public function getsTheDefaultNameFromTheConfig(): void + { + $manager = sprout()->providers(); + + $this->assertSame('tenants', $manager->getDefaultName()); + + config()->set('multitenancy.defaults.provider', 'backup'); + + $this->assertSame('backup', $manager->getDefaultName()); + } + + #[Test] + public function generatesConfigKeys(): void + { + $manager = sprout()->providers(); + + $this->assertSame('multitenancy.providers.test-config', $manager->getConfigKey('test-config')); + } + + #[Test] + public function hasDefaultFirstPartyDrivers(): void + { + $manager = sprout()->providers(); + + $this->assertFalse($manager->hasResolved()); + + $this->assertTrue($manager->hasDriver('eloquent')); + $this->assertTrue($manager->hasDriver('database')); + + $this->assertFalse($manager->hasResolved()); + + $this->assertInstanceOf(EloquentTenantProvider::class, $manager->get('tenants')); + $this->assertInstanceOf(DatabaseTenantProvider::class, $manager->get('backup')); + + $this->assertTrue($manager->hasResolved('tenants')); + $this->assertTrue($manager->hasResolved('backup')); + } + + #[Test] + public function canFlushResolvedInstances(): void + { + $manager = sprout()->providers(); + + $this->assertFalse($manager->hasResolved()); + + $this->assertTrue($manager->hasDriver('eloquent')); + $this->assertTrue($manager->hasDriver('database')); + + $this->assertFalse($manager->hasResolved()); + + $this->assertInstanceOf(EloquentTenantProvider::class, $manager->get('tenants')); + $this->assertInstanceOf(DatabaseTenantProvider::class, $manager->get('backup')); + + $this->assertTrue($manager->hasResolved('tenants')); + $this->assertTrue($manager->hasResolved('backup')); + + $manager->flushResolved(); + + $this->assertFalse($manager->hasResolved('tenants')); + $this->assertFalse($manager->hasResolved('backup')); + } + + #[Test] + public function errorsIfTheresNoConfigCanBeFoundForADriver(): void + { + $manager = sprout()->providers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The config for [provider::missing] could not be found'); + + $manager->get('missing'); + } + + #[Test] + public function errorsIfTheresNoCreatorForADriver(): void + { + $manager = sprout()->providers(); + + config()->set('multitenancy.providers.missing', []); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The creator for [provider::missing] could not be found'); + + $manager->get('missing'); + } + + #[Test] + public function errorsIfNoEloquentModelIsProvided(): void + { + config()->set('multitenancy.providers.tenants.model', null); + + $manager = sprout()->providers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The provider [tenants] is missing a required value for \'model\''); + + $manager->get('tenants'); + } + + #[Test] + public function errorsIfTheEloquentModelConfigIsInvalid(): void + { + config()->set('multitenancy.providers.tenants.model', stdClass::class); + + $manager = sprout()->providers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The provided value for \'model\' is not valid for provider [tenants]'); + + $manager->get('tenants'); + } + + #[Test] + public function errorsIfTheDatabaseEntityConfigIsInvalid(): void + { + config()->set('multitenancy.providers.backup.entity', stdClass::class); + + $manager = sprout()->providers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The provided value for \'entity\' is not valid for provider [backup]'); + + $manager->get('backup'); + } + + #[Test] + public function errorsIfNoDatabaseTableIsProvided(): void + { + config()->set('multitenancy.providers.backup.table', null); + + $manager = sprout()->providers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The provider [backup] is missing a required value for \'table\''); + + $manager->get('backup'); + } + + #[Test] + public function errorsIfTheDatabaseTableIsNotAStringOrModel(): void + { + config()->set('multitenancy.providers.backup.table', stdClass::class); + + $manager = sprout()->providers(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The provided value for \'table\' is not valid for provider [backup]'); + + $manager->get('backup'); + } + + #[Test] + public function canUseAModelInsteadOfTableNameForDatabaseProviders(): void + { + config()->set('multitenancy.providers.backup.table', TenantModel::class); + + $manager = sprout()->providers(); + + $this->assertSame((new TenantModel())->getTable(), $manager->get('backup')->getTable()); + } +} diff --git a/tests/Unit/Managers/TenancyManager.php b/tests/Unit/Managers/TenancyManager.php new file mode 100644 index 0000000..7359410 --- /dev/null +++ b/tests/Unit/Managers/TenancyManager.php @@ -0,0 +1,64 @@ +tenancies(); + + $this->assertSame('tenancy', $manager->getFactoryName()); + } + + #[Test] + public function getsTheDefaultNameFromTheConfig(): void + { + $manager = sprout()->tenancies(); + + $this->assertSame('tenants', $manager->getDefaultName()); + + config()->set('multitenancy.defaults.tenancy', 'backup'); + + $this->assertSame('backup', $manager->getDefaultName()); + } + + #[Test] + public function generatesConfigKeys(): void + { + $manager = sprout()->tenancies(); + + $this->assertSame('multitenancy.tenancies.test-config', $manager->getConfigKey('test-config')); + } + + #[Test] + public function errorsIfTheresNoConfigCanBeFoundForADriver(): void + { + $manager = sprout()->tenancies(); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The config for [tenancy::missing] could not be found'); + + $manager->get('missing'); + } + + #[Test] + public function errorsIfTheresNoCreatorForADriver(): void + { + $manager = sprout()->tenancies(); + + config()->set('multitenancy.tenancies.missing', ['driver' => 'missing']); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The creator for [tenancy::missing] could not be found'); + + $manager->get('missing'); + } +} From 7f2a099d284a9ef40a12b71f37fd5383b7eac638 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 15 Dec 2024 19:46:07 +0000 Subject: [PATCH 24/31] chore: Tidy up docblocks and method for new route helper --- src/Concerns/FindsIdentityInRouteParameter.php | 13 +------------ src/Contracts/IdentityResolver.php | 2 -- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Concerns/FindsIdentityInRouteParameter.php b/src/Concerns/FindsIdentityInRouteParameter.php index 73c34b4..d0d45d5 100644 --- a/src/Concerns/FindsIdentityInRouteParameter.php +++ b/src/Concerns/FindsIdentityInRouteParameter.php @@ -9,7 +9,6 @@ use Illuminate\Support\Facades\URL; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Exceptions\TenantMissing; /** * Find Identity in Route Parameter @@ -269,22 +268,12 @@ public function setup(Tenancy $tenancy, ?Tenant $tenant): void * @param array $parameters * @param bool $absolute * - * @phpstan-param TenantClass|null $tenant + * @phpstan-param TenantClass $tenant * * @return string - * - * @throws \Sprout\Exceptions\TenantMissing */ public function route(string $name, Tenancy $tenancy, Tenant $tenant, array $parameters = [], bool $absolute = true): string { - if ($tenant === null) { - if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); - } - - $tenant = $tenancy->tenant(); - } - $parameter = $this->getRouteParameterName($tenancy); if (! isset($parameters[$parameter])) { diff --git a/src/Contracts/IdentityResolver.php b/src/Contracts/IdentityResolver.php index 78b3107..cfe0ada 100644 --- a/src/Contracts/IdentityResolver.php +++ b/src/Contracts/IdentityResolver.php @@ -113,8 +113,6 @@ public function canResolve(Request $request, Tenancy $tenancy, ResolutionHook $h * @phpstan-param TenantClass $tenant * * @return string - * - * @throws \Sprout\Exceptions\TenantMissing */ public function route(string $name, Tenancy $tenancy, Tenant $tenant, array $parameters = [], bool $absolute = true): string; } From b81814495a03a93bb738e0f6ae5b87974e89a6a2 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 15 Dec 2024 19:47:05 +0000 Subject: [PATCH 25/31] chore: Add Sprout\Sprout::route() method to sprout facade --- src/Facades/Sprout.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Facades/Sprout.php b/src/Facades/Sprout.php index 97d1c46..10a7091 100644 --- a/src/Facades/Sprout.php +++ b/src/Facades/Sprout.php @@ -34,6 +34,7 @@ * @method static ProviderManager providers() * @method static \Sprout\Sprout registerOverride(string $class) * @method static IdentityResolverManager resolvers() + * @method static string route(string $name, Tenant $tenant, string|null $resolver = null, string|null $tenancy = null, array $parameters = [], bool $absolute = true) * @method static void setCurrentTenancy(Tenancy $tenancy) * @method static void setupOverrides(Tenancy $tenancy, Tenant $tenant) * @method static bool supportsHook(ResolutionHook $hook) From 1b00334a6e27faf5c47437432933d22ea3d34734 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 16 Dec 2024 11:49:54 +0000 Subject: [PATCH 26/31] chore: Add core Sprout settings repository (#79) * chore: Add core Sprout settings repository * refactor: Replace cookie value overrides with usage of settings repository * refactor: Implement settings repository for overrides * refactor: Add helper methods for settings * fix: Fix database override setting --- src/Concerns/OverridesCookieSettings.php | 71 ------------------- src/Http/Resolvers/PathIdentityResolver.php | 5 +- .../Resolvers/SubdomainIdentityResolver.php | 5 +- src/Overrides/CookieOverride.php | 12 ++-- src/Overrides/SessionOverride.php | 53 +++++++------- src/Sprout.php | 37 +++++++++- src/SproutServiceProvider.php | 6 +- src/Support/Settings.php | 21 ++++++ src/Support/SettingsRepository.php | 71 +++++++++++++++++++ src/helpers.php | 13 +++- 10 files changed, 183 insertions(+), 111 deletions(-) delete mode 100644 src/Concerns/OverridesCookieSettings.php create mode 100644 src/Support/Settings.php create mode 100644 src/Support/SettingsRepository.php diff --git a/src/Concerns/OverridesCookieSettings.php b/src/Concerns/OverridesCookieSettings.php deleted file mode 100644 index 3ff4e86..0000000 --- a/src/Concerns/OverridesCookieSettings.php +++ /dev/null @@ -1,71 +0,0 @@ -parameterSetup($tenancy, $tenant); if ($tenant !== null) { - CookieOverride::setPath($this->getTenantRoutePrefix($tenancy)); - SessionOverride::setPath($this->getTenantRoutePrefix($tenancy)); + settings()->setUrlPath($this->getTenantRoutePrefix($tenancy)); } } } diff --git a/src/Http/Resolvers/SubdomainIdentityResolver.php b/src/Http/Resolvers/SubdomainIdentityResolver.php index 07b46ca..d01e79a 100644 --- a/src/Http/Resolvers/SubdomainIdentityResolver.php +++ b/src/Http/Resolvers/SubdomainIdentityResolver.php @@ -16,6 +16,8 @@ use Sprout\Overrides\CookieOverride; use Sprout\Overrides\SessionOverride; use Sprout\Support\BaseIdentityResolver; +use Sprout\Support\Settings; +use function Sprout\settings; /** * The Subdomain Identity Resolver @@ -172,8 +174,7 @@ public function setup(Tenancy $tenancy, ?Tenant $tenant): void $this->parameterSetup($tenancy, $tenant); if ($tenant !== null) { - CookieOverride::setDomain($this->getTenantRouteDomain($tenancy)); - SessionOverride::setDomain($this->getTenantRouteDomain($tenancy)); + settings()->setUrlDomain($this->getTenantRouteDomain($tenancy)); } } } diff --git a/src/Overrides/CookieOverride.php b/src/Overrides/CookieOverride.php index 798da88..f6c11c1 100644 --- a/src/Overrides/CookieOverride.php +++ b/src/Overrides/CookieOverride.php @@ -4,11 +4,11 @@ namespace Sprout\Overrides; use Illuminate\Cookie\CookieJar; -use Sprout\Concerns\OverridesCookieSettings; use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\ServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +use function Sprout\settings; /** * Cookie Override @@ -20,8 +20,6 @@ */ final class CookieOverride implements ServiceOverride, DeferrableServiceOverride { - use OverridesCookieSettings; - /** * Get the service to watch for before overriding * @@ -47,10 +45,10 @@ public static function service(): string public function setup(Tenancy $tenancy, Tenant $tenant): void { // Collect the values - $path = self::$settings['path'] ?? config('session.path') ?? '/'; - $domain = self::$settings['domain'] ?? config('session.domain'); - $secure = self::$settings['secure'] ?? config('session.secure', false); - $sameSite = self::$settings['same_site'] ?? config('session.same_site'); + $path = settings()->getUrlPath(config('session.path') ?? '/'); // @phpstan-ignore-line + $domain = settings()->getUrlDomain(config('session.domain')); // @phpstan-ignore-line + $secure = settings()->shouldCookieBeSecure(config('session.secure', false)); // @phpstan-ignore-line + $sameSite = settings()->shouldCookeBeSameSite(config('session.same_site')); // @phpstan-ignore-line /** * This is here to make PHPStan quiet down diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index 8c250ca..8d2b5ff 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -8,7 +8,7 @@ use Illuminate\Session\DatabaseSessionHandler as OriginalDatabaseSessionHandler; use Illuminate\Session\FileSessionHandler; use Illuminate\Session\SessionManager; -use Sprout\Concerns\OverridesCookieSettings; +use Illuminate\Support\Arr; use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\Tenancy; @@ -19,6 +19,8 @@ use Sprout\Exceptions\TenantMissing; use Sprout\Overrides\Session\TenantAwareDatabaseSessionHandler; use Sprout\Sprout; +use Sprout\Support\Settings; +use function Sprout\settings; use function Sprout\sprout; /** @@ -31,23 +33,6 @@ */ final class SessionOverride implements BootableServiceOverride, DeferrableServiceOverride { - use OverridesCookieSettings; - - /** - * @var bool - */ - private static bool $overrideDatabase = true; - - /** - * Prevent this override from overriding the database driver - * - * @return void - */ - public static function doNotOverrideDatabase(): void - { - self::$overrideDatabase = false; - } - /** * Get the service to watch for before overriding * @@ -80,7 +65,7 @@ public function boot(Application $app, Sprout $sprout): void $sessionManager->extend('file', $fileCreator); $sessionManager->extend('native', $fileCreator); - if (self::$overrideDatabase) { + if (settings()->shouldNotOverrideTheDatabase(false) === false) { $sessionManager->extend('database', self::createDatabaseDriver()); } } @@ -99,13 +84,33 @@ public function boot(Application $app, Sprout $sprout): void */ public function setup(Tenancy $tenancy, Tenant $tenant): void { - $settings = self::$settings; - /** @var \Illuminate\Contracts\Config\Repository $config */ - $config = config(); + $config = config(); + $settings = settings(); + + if (! $settings->has('original.session')) { + /** @var array $original */ + $original = $config->get('session'); + $settings->set( + 'original.session', + Arr::only($original, ['path', 'domain', 'secure', 'same_site']) + ); + } + + if ($settings->has(Settings::URL_PATH)) { + $config->set('session.path', $settings->getUrlPath()); + } + + if ($settings->has(Settings::URL_DOMAIN)) { + $config->set('session.domain', $settings->getUrlDomain()); + } + + if ($settings->has(Settings::COOKIE_SECURE)) { + $config->set('session.secure', $settings->shouldCookieBeSecure()); + } - foreach ($settings as $setting => $value) { - $config->set('session.' . $setting, $value); + if ($settings->has(Settings::COOKIE_SAME_SITE)) { + $config->set('session.same_site', $settings->shouldCookeBeSameSite()); } $config->set('session.cookie', $this->getCookieName($tenancy, $tenant)); diff --git a/src/Sprout.php b/src/Sprout.php index 2d85ec8..1f69efb 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -11,6 +11,7 @@ use Sprout\Managers\ProviderManager; use Sprout\Managers\TenancyManager; use Sprout\Support\ResolutionHook; +use Sprout\Support\SettingsRepository; /** * Sprout @@ -38,14 +39,21 @@ final class Sprout */ private bool $withinContext = false; + /** + * @var \Sprout\Support\SettingsRepository + */ + private SettingsRepository $settings; + /** * Create a new instance * * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Sprout\Support\SettingsRepository $settings */ - public function __construct(Application $app) + public function __construct(Application $app, SettingsRepository $settings) { - $this->app = $app; + $this->app = $app; + $this->settings = $settings; } /** @@ -63,6 +71,29 @@ public function config(string $key, mixed $default = null): mixed return $this->app->make('config')->get('sprout.' . $key, $default); } + /** + * Get a config item from the sprout config + * + * @param string $key + * @param mixed|null $default + * + * @return mixed + */ + public function setting(string $key, mixed $default = null): mixed + { + return $this->settings->get($key, $default); + } + + /** + * Get the Sprout settings repository + * + * @return \Sprout\Support\SettingsRepository + */ + public function settings(): SettingsRepository + { + return $this->settings; + } + /** * Set the current tenancy * @@ -222,7 +253,7 @@ public function withinContext(): bool * * If no tenancy name is provided, this method will use the current tenancy * or the default one. - * + * * If no resolver name is provided, this method will use the resolver * currently linked with the tenancy, or the default one. * diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index a3e3368..e9d61a6 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -15,6 +15,7 @@ use Sprout\Managers\ProviderManager; use Sprout\Managers\TenancyManager; use Sprout\Support\ResolutionHook; +use Sprout\Support\SettingsRepository; /** * Sprout Service Provider @@ -36,10 +37,13 @@ public function register(): void private function registerSprout(): void { - $this->sprout = new Sprout($this->app); + $this->sprout = new Sprout($this->app, new SettingsRepository()); $this->app->singleton(Sprout::class, fn () => $this->sprout); $this->app->alias(Sprout::class, 'sprout'); + + // Bind the settings repository too + $this->app->bind(SettingsRepository::class, fn () => $this->sprout->settings()); } private function registerManagers(): void diff --git a/src/Support/Settings.php b/src/Support/Settings.php new file mode 100644 index 0000000..c7dc2ea --- /dev/null +++ b/src/Support/Settings.php @@ -0,0 +1,21 @@ +set(Settings::URL_PATH, $path); + } + + public function getUrlPath(?string $default = null): ?string + { + /** @var string|null $path */ + $path = $this->get(Settings::URL_PATH, $default); + + return $path; + } + + public function setUrlDomain(?string $domain): void + { + $this->set(Settings::URL_DOMAIN, $domain); + } + + public function getUrlDomain(?string $default = null): ?string + { + /** @var string|null $domain */ + $domain = $this->get(Settings::URL_DOMAIN, $default); + + return $domain; + } + + public function setCookieSecure(bool $secure): void + { + $this->set(Settings::COOKIE_SECURE, $secure); + } + + public function shouldCookieBeSecure(?bool $default = null): bool + { + return $this->boolean(Settings::COOKIE_SECURE, $default); + } + + public function setCookieSameSite(bool $sameSite): void + { + $this->set(Settings::COOKIE_SAME_SITE, $sameSite); + } + + public function shouldCookeBeSameSite(?bool $default = null): bool + { + return $this->boolean(Settings::COOKIE_SAME_SITE, $default); + } + + public function doNotOverrideTheDatabase(): void + { + $this->set(Settings::NO_DATABASE_OVERRIDE, true); + } + + public function shouldNotOverrideTheDatabase(bool $default = null): bool + { + return $this->boolean(Settings::NO_DATABASE_OVERRIDE, $default); + } +} diff --git a/src/helpers.php b/src/helpers.php index c900d6f..941aea3 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -5,9 +5,10 @@ use Sprout\Contracts\IdentityResolver; use Sprout\Contracts\Tenancy; use Sprout\Contracts\TenantProvider; +use Sprout\Support\SettingsRepository; /** - * Get the core sprout class + * Get the core Sprout class * * @return \Sprout\Sprout */ @@ -16,6 +17,16 @@ function sprout(): Sprout return app(Sprout::class); } +/** + * Get the Sprout settings repository + * + * @return \Sprout\Support\SettingsRepository + */ +function settings(): SettingsRepository +{ + return sprout()->settings(); +} + /** * Get an identity resolver * From c269be6f74af8b1f37be38b18cd64a55964ae315 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 16 Dec 2024 11:53:32 +0000 Subject: [PATCH 27/31] chore: Skip test with no assertions --- tests/Unit/Managers/IdentityResolverManagerTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/Managers/IdentityResolverManagerTest.php b/tests/Unit/Managers/IdentityResolverManagerTest.php index d7d0bfd..bbf34a5 100644 --- a/tests/Unit/Managers/IdentityResolverManagerTest.php +++ b/tests/Unit/Managers/IdentityResolverManagerTest.php @@ -175,5 +175,7 @@ public function allowsCustomCreators(): void }); $manager = sprout()->resolvers(); + + $this->markTestSkipped(); } } From faf71b8faeccfe8a549c92ab14829a404e8c88a2 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 16 Dec 2024 12:00:34 +0000 Subject: [PATCH 28/31] fix: Fix bug with custom creators --- src/Support/BaseFactory.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Support/BaseFactory.php b/src/Support/BaseFactory.php index d2dd4e2..448ec2e 100644 --- a/src/Support/BaseFactory.php +++ b/src/Support/BaseFactory.php @@ -186,15 +186,22 @@ protected function resolve(string $name): object } // Ooo custom creation logic, let's use that - if (isset($this->customCreators[$name])) { + if (isset(static::$customCreators[$name])) { return $this->callCustomCreator($name, $config); } + /** @var string|null $driver */ + $driver = $config['driver'] ?? null; + // Is there a driver? - if (isset($config['driver'])) { + if ($driver !== null) { + // Is there a custom creator for the driver? + if (isset(static::$customCreators[$driver])) { + return $this->callCustomCreator($driver, $config); + } + // This has a driver, so we'll see if we can create based on that - /** @phpstan-ignore-next-line */ - $method = 'create' . ucfirst($config['driver']) . ucfirst($this->getFactoryName()); + $method = 'create' . ucfirst($driver) . ucfirst($this->getFactoryName()); } else { // There's no driver, so we'll see if there's a default available $method = 'createDefault' . ucfirst($this->getFactoryName()); From a0fca4daa8ee1ba398f4ed1449ef627043261e39 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 16 Dec 2024 12:00:49 +0000 Subject: [PATCH 29/31] test: Add tests for identity resolver custom creators --- tests/Unit/Managers/IdentityResolverManagerTest.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Managers/IdentityResolverManagerTest.php b/tests/Unit/Managers/IdentityResolverManagerTest.php index bbf34a5..136bc71 100644 --- a/tests/Unit/Managers/IdentityResolverManagerTest.php +++ b/tests/Unit/Managers/IdentityResolverManagerTest.php @@ -176,6 +176,16 @@ public function allowsCustomCreators(): void $manager = sprout()->resolvers(); - $this->markTestSkipped(); + $this->assertTrue($manager->hasDriver('hello-there')); + $this->assertFalse($manager->hasResolved('path')); + $this->assertFalse($manager->hasResolved('subdomain')); + + $resolver = $manager->get('path'); + + $this->assertInstanceOf(SubdomainIdentityResolver::class, $resolver); + $this->assertSame('hello-there', $resolver->getName()); + $this->assertSame('somedomain.local', $resolver->getDomain()); + $this->assertTrue($manager->hasResolved('path')); + $this->assertFalse($manager->hasResolved('subdomain')); } } From cd7ea804b970442e0134d74337cc6debb4a5acd3 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 16 Dec 2024 12:33:16 +0000 Subject: [PATCH 30/31] refactor: Add missing Exception suffix to custom exception classes --- .../BelongsToManyTenantsObserver.php | 20 +++++------ .../Observers/BelongsToTenantObserver.php | 20 +++++------ .../Scopes/BelongsToManyTenantsScope.php | 6 ++-- .../Eloquent/Scopes/BelongsToTenantScope.php | 6 ++-- ...ntFound.php => NoTenantFoundException.php} | 2 +- ...issing.php => TenancyMissingException.php} | 2 +- ...smatch.php => TenantMismatchException.php} | 2 +- ...Missing.php => TenantMissingException.php} | 2 +- src/Http/Middleware/TenantRoutes.php | 2 +- src/Http/Resolvers/PathIdentityResolver.php | 8 ++--- .../Resolvers/SubdomainIdentityResolver.php | 8 ++--- src/Listeners/IdentifyTenantOnRouting.php | 2 +- .../TenantAwareDatabaseTokenRepository.php | 36 +++++++++---------- src/Overrides/CacheOverride.php | 6 ++-- .../TenantAwareDatabaseSessionHandler.php | 20 +++++------ src/Overrides/SessionOverride.php | 8 ++--- src/Overrides/StorageOverride.php | 4 +-- src/Sprout.php | 2 +- src/Support/ResolutionHelper.php | 6 ++-- tests/Unit/Support/ResolutionHelperTest.php | 10 +++--- .../Eloquent/BelongsToManyTenantsTest.php | 10 +++--- .../Database/Eloquent/BelongsToTenantTest.php | 12 +++---- .../Overrides/StorageOverrideTest.php | 4 +-- 23 files changed, 99 insertions(+), 99 deletions(-) rename src/Exceptions/{NoTenantFound.php => NoTenantFoundException.php} (90%) rename src/Exceptions/{TenancyMissing.php => TenancyMissingException.php} (88%) rename src/Exceptions/{TenantMismatch.php => TenantMismatchException.php} (91%) rename src/Exceptions/{TenantMissing.php => TenantMissingException.php} (89%) diff --git a/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php b/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php index d65f4b0..fe83668 100644 --- a/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php +++ b/src/Database/Eloquent/Observers/BelongsToManyTenantsObserver.php @@ -7,8 +7,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Exceptions\TenantMismatch; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMismatchException; +use Sprout\Exceptions\TenantMissingException; use Sprout\TenancyOptions; use function Sprout\sprout; @@ -85,8 +85,8 @@ private function isTenantMismatched(Model $model, Tenant&Model $tenant, BelongsT * * @phpstan-param ChildModel $model * - * @throws \Sprout\Exceptions\TenantMismatch - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMismatchException + * @throws \Sprout\Exceptions\TenantMissingException */ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsToMany $relation, bool $succeedOnMatch = false): bool { @@ -100,7 +100,7 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsToMa // If we hit here then there's no tenant, and the model isn't // marked as tenant being optional, so we throw an exception - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } /** @@ -118,7 +118,7 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsToMa // So, the current foreign key value doesn't match the current // tenant, so we'll throw an exception...if we're allowed to if (TenancyOptions::shouldThrowIfNotRelated($tenancy)) { - throw TenantMismatch::make($model::class, $tenancy->getName()); + throw TenantMismatchException::make($model::class, $tenancy->getName()); } // If we hit here, we should continue without doing anything @@ -147,8 +147,8 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsToMa * * @phpstan-param ChildModel $model * - * @throws \Sprout\Exceptions\TenantMissing - * @throws \Sprout\Exceptions\TenantMismatch + * @throws \Sprout\Exceptions\TenantMissingException + * @throws \Sprout\Exceptions\TenantMismatchException */ public function created(Model $model): void { @@ -199,8 +199,8 @@ public function created(Model $model): void * * @phpstan-param ChildModel $model * - * @throws \Sprout\Exceptions\TenantMissing - * @throws \Sprout\Exceptions\TenantMismatch + * @throws \Sprout\Exceptions\TenantMissingException + * @throws \Sprout\Exceptions\TenantMismatchException */ public function retrieved(Model $model): void { diff --git a/src/Database/Eloquent/Observers/BelongsToTenantObserver.php b/src/Database/Eloquent/Observers/BelongsToTenantObserver.php index e8224ae..096883e 100644 --- a/src/Database/Eloquent/Observers/BelongsToTenantObserver.php +++ b/src/Database/Eloquent/Observers/BelongsToTenantObserver.php @@ -7,8 +7,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Exceptions\TenantMismatch; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMismatchException; +use Sprout\Exceptions\TenantMissingException; use Sprout\TenancyOptions; use function Sprout\sprout; @@ -67,8 +67,8 @@ private function isTenantMismatched(Model $model, Tenant&Model $tenant, BelongsT * * @phpstan-param ChildModel $model * - * @throws \Sprout\Exceptions\TenantMismatch - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMismatchException + * @throws \Sprout\Exceptions\TenantMissingException */ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsTo $relation, bool $succeedOnMatch = false): bool { @@ -82,7 +82,7 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsTo $ // If we hit here then there's no tenant, and the model isn't // marked as tenant being optional, so we throw an exception - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } /** @@ -100,7 +100,7 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsTo $ // So, the current foreign key value doesn't match the current // tenant, so we'll throw an exception...if we're allowed to if (TenancyOptions::shouldThrowIfNotRelated($tenancy)) { - throw TenantMismatch::make($model::class, $tenancy->getName()); + throw TenantMismatchException::make($model::class, $tenancy->getName()); } // If we hit here, we should continue without doing anything @@ -130,8 +130,8 @@ private function passesInitialChecks(Model $model, Tenancy $tenancy, BelongsTo $ * * @phpstan-param ChildModel $model * - * @throws \Sprout\Exceptions\TenantMissing - * @throws \Sprout\Exceptions\TenantMismatch + * @throws \Sprout\Exceptions\TenantMissingException + * @throws \Sprout\Exceptions\TenantMismatchException */ public function creating(Model $model): bool { @@ -175,8 +175,8 @@ public function creating(Model $model): bool * * @phpstan-param ChildModel $model * - * @throws \Sprout\Exceptions\TenantMissing - * @throws \Sprout\Exceptions\TenantMismatch + * @throws \Sprout\Exceptions\TenantMissingException + * @throws \Sprout\Exceptions\TenantMismatchException */ public function retrieved(Model $model): void { diff --git a/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php b/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php index 0422c43..4422475 100644 --- a/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php +++ b/src/Database/Eloquent/Scopes/BelongsToManyTenantsScope.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMissingException; use function Sprout\sprout; /** @@ -35,7 +35,7 @@ final class BelongsToManyTenantsScope extends TenantChildScope * * @return void * - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ public function apply(Builder $builder, Model $model): void { @@ -54,7 +54,7 @@ public function apply(Builder $builder, Model $model): void } // We should throw an exception because the tenant is missing - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } // Finally, add the clause so that all queries are scoped to the diff --git a/src/Database/Eloquent/Scopes/BelongsToTenantScope.php b/src/Database/Eloquent/Scopes/BelongsToTenantScope.php index b3ec890..b2491bc 100644 --- a/src/Database/Eloquent/Scopes/BelongsToTenantScope.php +++ b/src/Database/Eloquent/Scopes/BelongsToTenantScope.php @@ -5,7 +5,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMissingException; use function Sprout\sprout; /** @@ -35,7 +35,7 @@ final class BelongsToTenantScope extends TenantChildScope * * @return void * - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ public function apply(Builder $builder, Model $model): void { @@ -54,7 +54,7 @@ public function apply(Builder $builder, Model $model): void } // We should throw an exception because the tenant is missing - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } // Finally, add the clause so that all queries are scoped to the diff --git a/src/Exceptions/NoTenantFound.php b/src/Exceptions/NoTenantFoundException.php similarity index 90% rename from src/Exceptions/NoTenantFound.php rename to src/Exceptions/NoTenantFoundException.php index de95641..235908b 100644 --- a/src/Exceptions/NoTenantFound.php +++ b/src/Exceptions/NoTenantFoundException.php @@ -11,7 +11,7 @@ * * @package Core */ -final class NoTenantFound extends SproutException +final class NoTenantFoundException extends SproutException { /** * Create the exception diff --git a/src/Exceptions/TenancyMissing.php b/src/Exceptions/TenancyMissingException.php similarity index 88% rename from src/Exceptions/TenancyMissing.php rename to src/Exceptions/TenancyMissingException.php index dfe5347..03c7f5b 100644 --- a/src/Exceptions/TenancyMissing.php +++ b/src/Exceptions/TenancyMissingException.php @@ -13,7 +13,7 @@ * * @codeCoverageIgnore */ -final class TenancyMissing extends SproutException +final class TenancyMissingException extends SproutException { /** * Create the exception diff --git a/src/Exceptions/TenantMismatch.php b/src/Exceptions/TenantMismatchException.php similarity index 91% rename from src/Exceptions/TenantMismatch.php rename to src/Exceptions/TenantMismatchException.php index bf9a2e3..91bc724 100644 --- a/src/Exceptions/TenantMismatch.php +++ b/src/Exceptions/TenantMismatchException.php @@ -11,7 +11,7 @@ * * @package Core */ -final class TenantMismatch extends SproutException +final class TenantMismatchException extends SproutException { /** * Create the exception diff --git a/src/Exceptions/TenantMissing.php b/src/Exceptions/TenantMissingException.php similarity index 89% rename from src/Exceptions/TenantMissing.php rename to src/Exceptions/TenantMissingException.php index e632e76..5e4f85a 100644 --- a/src/Exceptions/TenantMissing.php +++ b/src/Exceptions/TenantMissingException.php @@ -11,7 +11,7 @@ * * @package Core */ -final class TenantMissing extends SproutException +final class TenantMissingException extends SproutException { /** * Create the exception diff --git a/src/Http/Middleware/TenantRoutes.php b/src/Http/Middleware/TenantRoutes.php index 1bc981f..0f47b6f 100644 --- a/src/Http/Middleware/TenantRoutes.php +++ b/src/Http/Middleware/TenantRoutes.php @@ -50,7 +50,7 @@ public function __construct(Sprout $sprout) * * @return \Symfony\Component\HttpFoundation\Response * - * @throws \Sprout\Exceptions\NoTenantFound + * @throws \Sprout\Exceptions\NoTenantFoundException * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Sprout\Exceptions\MisconfigurationException */ diff --git a/src/Http/Resolvers/PathIdentityResolver.php b/src/Http/Resolvers/PathIdentityResolver.php index ed640a8..f9989ee 100644 --- a/src/Http/Resolvers/PathIdentityResolver.php +++ b/src/Http/Resolvers/PathIdentityResolver.php @@ -11,7 +11,7 @@ use Sprout\Contracts\IdentityResolverUsesParameters; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMissingException; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Overrides\CookieOverride; use Sprout\Overrides\SessionOverride; @@ -133,12 +133,12 @@ public function getRoutePrefix(Tenancy $tenancy): string * * @return string * - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ public function getTenantRoutePrefix(Tenancy $tenancy): string { if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); // @codeCoverageIgnore + throw TenantMissingException::make($tenancy->getName()); // @codeCoverageIgnore } /** @var string $identifier */ @@ -168,7 +168,7 @@ public function getTenantRoutePrefix(Tenancy $tenancy): string * * @return void * - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ public function setup(Tenancy $tenancy, ?Tenant $tenant): void { diff --git a/src/Http/Resolvers/SubdomainIdentityResolver.php b/src/Http/Resolvers/SubdomainIdentityResolver.php index 1c55410..87ed7d3 100644 --- a/src/Http/Resolvers/SubdomainIdentityResolver.php +++ b/src/Http/Resolvers/SubdomainIdentityResolver.php @@ -11,7 +11,7 @@ use Sprout\Contracts\IdentityResolverUsesParameters; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMissingException; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Overrides\CookieOverride; use Sprout\Overrides\SessionOverride; @@ -137,12 +137,12 @@ public function routes(Router $router, Closure $groupRoutes, Tenancy $tenancy): * * @return string * - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ public function getTenantRouteDomain(Tenancy $tenancy): string { if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); // @codeCoverageIgnore + throw TenantMissingException::make($tenancy->getName()); // @codeCoverageIgnore } /** @var string $identifier */ @@ -172,7 +172,7 @@ public function getTenantRouteDomain(Tenancy $tenancy): string * * @return void * - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ public function setup(Tenancy $tenancy, ?Tenant $tenant): void { diff --git a/src/Listeners/IdentifyTenantOnRouting.php b/src/Listeners/IdentifyTenantOnRouting.php index f0b17a8..4aefb6b 100644 --- a/src/Listeners/IdentifyTenantOnRouting.php +++ b/src/Listeners/IdentifyTenantOnRouting.php @@ -28,7 +28,7 @@ final class IdentifyTenantOnRouting * * @return void * - * @throws \Sprout\Exceptions\NoTenantFound + * @throws \Sprout\Exceptions\NoTenantFoundException */ public function handle(RouteMatched $event): void { diff --git a/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php b/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php index 76debed..68c1baf 100644 --- a/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php +++ b/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php @@ -8,8 +8,8 @@ use Illuminate\Database\Query\Builder; use Illuminate\Support\Carbon; use SensitiveParameter; -use Sprout\Exceptions\TenancyMissing; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenancyMissingException; +use Sprout\Exceptions\TenantMissingException; use function Sprout\sprout; /** @@ -31,8 +31,8 @@ class TenantAwareDatabaseTokenRepository extends DatabaseTokenRepository * * @return array * - * @throws \Sprout\Exceptions\TenancyMissing - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException */ protected function getPayload($email, #[SensitiveParameter] $token): array { @@ -43,11 +43,11 @@ protected function getPayload($email, #[SensitiveParameter] $token): array $tenancy = sprout()->getCurrentTenancy(); if ($tenancy === null) { - throw TenancyMissing::make(); + throw TenancyMissingException::make(); } if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } return [ @@ -66,8 +66,8 @@ protected function getPayload($email, #[SensitiveParameter] $token): array * * @return \Illuminate\Database\Query\Builder * - * @throws \Sprout\Exceptions\TenancyMissing - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException */ protected function getTenantedQuery(string $email): Builder { @@ -78,11 +78,11 @@ protected function getTenantedQuery(string $email): Builder $tenancy = sprout()->getCurrentTenancy(); if ($tenancy === null) { - throw TenancyMissing::make(); + throw TenancyMissingException::make(); } if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } return $this->getTable() @@ -98,8 +98,8 @@ protected function getTenantedQuery(string $email): Builder * * @return object|null * - * @throws \Sprout\Exceptions\TenancyMissing - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException */ protected function getExistingTenantedRecord(CanResetPasswordContract $user): ?object { @@ -113,8 +113,8 @@ protected function getExistingTenantedRecord(CanResetPasswordContract $user): ?o * * @return int * - * @throws \Sprout\Exceptions\TenancyMissing - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException */ protected function deleteExisting(CanResetPasswordContract $user): int { @@ -129,8 +129,8 @@ protected function deleteExisting(CanResetPasswordContract $user): int * * @return bool * - * @throws \Sprout\Exceptions\TenancyMissing - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException */ public function exists(CanResetPasswordContract $user, #[SensitiveParameter] $token): bool { @@ -148,8 +148,8 @@ public function exists(CanResetPasswordContract $user, #[SensitiveParameter] $to * * @return bool * - * @throws \Sprout\Exceptions\TenancyMissing - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException */ public function recentlyCreatedToken(CanResetPasswordContract $user): bool { diff --git a/src/Overrides/CacheOverride.php b/src/Overrides/CacheOverride.php index 50f6b1d..8b4e0f3 100644 --- a/src/Overrides/CacheOverride.php +++ b/src/Overrides/CacheOverride.php @@ -18,7 +18,7 @@ use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMissingException; use Sprout\Sprout; /** @@ -69,14 +69,14 @@ public function boot(Application $app, Sprout $sprout): void /** * @param array $config * - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ function (Application $app, array $config) use ($sprout, $cacheManager) { $tenancy = $sprout->tenancies()->get($config['tenancy'] ?? null); // If there's no tenant, error out if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } $tenant = $tenancy->tenant(); diff --git a/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php b/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php index 91669c9..84f7b55 100644 --- a/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php +++ b/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php @@ -5,8 +5,8 @@ use Illuminate\Database\Query\Builder; use Illuminate\Session\DatabaseSessionHandler; -use Sprout\Exceptions\TenancyMissing; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenancyMissingException; +use Sprout\Exceptions\TenantMissingException; use function Sprout\sprout; /** @@ -25,19 +25,19 @@ class TenantAwareDatabaseSessionHandler extends DatabaseSessionHandler * * @return \Illuminate\Database\Query\Builder * - * @throws \Sprout\Exceptions\TenantMissing - * @throws \Sprout\Exceptions\TenancyMissing + * @throws \Sprout\Exceptions\TenantMissingException + * @throws \Sprout\Exceptions\TenancyMissingException */ protected function getQuery(): Builder { $tenancy = sprout()->getCurrentTenancy(); if ($tenancy === null) { - throw TenancyMissing::make(); + throw TenancyMissingException::make(); } if ($tenancy->check() === false) { - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } return parent::getQuery() @@ -53,19 +53,19 @@ protected function getQuery(): Builder * * @return bool|null * - * @throws \Sprout\Exceptions\TenancyMissing - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException */ protected function performInsert($sessionId, $payload): ?bool { $tenancy = sprout()->getCurrentTenancy(); if ($tenancy === null) { - throw TenancyMissing::make(); + throw TenancyMissingException::make(); } if ($tenancy->check() === false) { - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } $payload['tenancy'] = $tenancy->getName(); diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index 8d2b5ff..89deb9f 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -15,8 +15,8 @@ use Sprout\Contracts\Tenant; use Sprout\Contracts\TenantHasResources; use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenancyMissing; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenancyMissingException; +use Sprout\Exceptions\TenantMissingException; use Sprout\Overrides\Session\TenantAwareDatabaseSessionHandler; use Sprout\Sprout; use Sprout\Support\Settings; @@ -168,12 +168,12 @@ private static function createFilesDriver(): Closure $tenancy = sprout()->getCurrentTenancy(); if ($tenancy === null) { - throw TenancyMissing::make(); + throw TenancyMissingException::make(); } // If there's no tenant, error out if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } $tenant = $tenancy->tenant(); diff --git a/src/Overrides/StorageOverride.php b/src/Overrides/StorageOverride.php index 61a8cfa..aea63bb 100644 --- a/src/Overrides/StorageOverride.php +++ b/src/Overrides/StorageOverride.php @@ -14,7 +14,7 @@ use Sprout\Contracts\Tenant; use Sprout\Contracts\TenantHasResources; use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMissingException; use Sprout\Sprout; /** @@ -121,7 +121,7 @@ private static function creator(Sprout $sprout, FilesystemManager $manager): Clo // If there's no tenant, error out if (! $tenancy->check()) { - throw TenantMissing::make($tenancy->getName()); + throw TenantMissingException::make($tenancy->getName()); } $tenant = $tenancy->tenant(); diff --git a/src/Sprout.php b/src/Sprout.php index fe2d96b..deeec2d 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -260,7 +260,7 @@ public function withinContext(): bool * * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Sprout\Exceptions\MisconfigurationException - * @throws \Sprout\Exceptions\TenantMissing + * @throws \Sprout\Exceptions\TenantMissingException */ public function route(string $name, Tenant $tenant, ?string $resolver = null, ?string $tenancy = null, array $parameters = [], bool $absolute = true): string { diff --git a/src/Support/ResolutionHelper.php b/src/Support/ResolutionHelper.php index d53af3a..f951185 100644 --- a/src/Support/ResolutionHelper.php +++ b/src/Support/ResolutionHelper.php @@ -6,7 +6,7 @@ use Illuminate\Http\Request; use Sprout\Contracts\IdentityResolverUsesParameters; use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\NoTenantFound; +use Sprout\Exceptions\NoTenantFoundException; use Sprout\Sprout; class ResolutionHelper @@ -41,7 +41,7 @@ public static function parseOptions(array $options): array * * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Sprout\Exceptions\MisconfigurationException - * @throws \Sprout\Exceptions\NoTenantFound + * @throws \Sprout\Exceptions\NoTenantFoundException */ public static function handleResolution(Request $request, ResolutionHook $hook, ?string $resolverName = null, ?string $tenancyName = null, bool $throw = true): bool { @@ -91,7 +91,7 @@ public static function handleResolution(Request $request, ResolutionHook $hook, if ($identity === null || $tenancy->identify($identity) === false) { if ($throw) { - throw NoTenantFound::make($resolver->getName(), $tenancy->getName()); + throw NoTenantFoundException::make($resolver->getName(), $tenancy->getName()); } return false; diff --git a/tests/Unit/Support/ResolutionHelperTest.php b/tests/Unit/Support/ResolutionHelperTest.php index 91875b5..81cf122 100644 --- a/tests/Unit/Support/ResolutionHelperTest.php +++ b/tests/Unit/Support/ResolutionHelperTest.php @@ -9,7 +9,7 @@ use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\NoTenantFound; +use Sprout\Exceptions\NoTenantFoundException; use Sprout\Support\ResolutionHelper; use Sprout\Support\ResolutionHook; use Sprout\Tests\Unit\UnitTestCase; @@ -186,12 +186,12 @@ public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRoute(): void $mock->shouldReceive('route')->andReturn($fakeRoute); }); - $this->expectException(NoTenantFound::class); + $this->expectException(NoTenantFoundException::class); $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [' . $resolver->getName() . ']'); ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName()); - $this->expectException(NoTenantFound::class); + $this->expectException(NoTenantFoundException::class); $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [subdomain]'); ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing); @@ -299,12 +299,12 @@ public function throwsAnExceptionWhenUnableToIdentifyATenantFromTheRequest(): vo ->andReturn('fake-identifier'); }); - $this->expectException(NoTenantFound::class); + $this->expectException(NoTenantFoundException::class); $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [' . $resolver->getName() . ']'); ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName()); - $this->expectException(NoTenantFound::class); + $this->expectException(NoTenantFoundException::class); $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [subdomain]'); ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing); diff --git a/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php b/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php index 1dfda19..cac1a08 100644 --- a/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php +++ b/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php @@ -15,8 +15,8 @@ use Sprout\Database\Eloquent\Contracts\OptionalTenant; use Sprout\Database\Eloquent\Observers\BelongsToManyTenantsObserver; use Sprout\Database\Eloquent\Scopes\BelongsToManyTenantsScope; -use Sprout\Exceptions\TenantMismatch; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMismatchException; +use Sprout\Exceptions\TenantMissingException; use Sprout\Managers\TenancyManager; use Sprout\TenancyOptions; use Workbench\App\Models\TenantChildren; @@ -101,7 +101,7 @@ public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCr { sprout()->setCurrentTenancy(app(TenancyManager::class)->get()); - $this->expectException(TenantMissing::class); + $this->expectException(TenantMissingException::class); $this->expectExceptionMessage( 'There is no current tenant for tenancy [tenants]' ); @@ -205,7 +205,7 @@ public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenHy $tenancy->setTenant(null); - $this->expectException(TenantMissing::class); + $this->expectException(TenantMissingException::class); $this->expectExceptionMessage( 'There is no current tenant for tenancy [tenants]' ); @@ -274,7 +274,7 @@ public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDiffere $tenancy->setTenant(TenantModel::factory()->create()); - $this->expectException(TenantMismatch::class); + $this->expectException(TenantMismatchException::class); $this->expectExceptionMessage( 'Model [' . TenantChildren::class diff --git a/tests/_Original/Database/Eloquent/BelongsToTenantTest.php b/tests/_Original/Database/Eloquent/BelongsToTenantTest.php index f8b21c4..447f30d 100644 --- a/tests/_Original/Database/Eloquent/BelongsToTenantTest.php +++ b/tests/_Original/Database/Eloquent/BelongsToTenantTest.php @@ -14,8 +14,8 @@ use Sprout\Database\Eloquent\Contracts\OptionalTenant; use Sprout\Database\Eloquent\Observers\BelongsToTenantObserver; use Sprout\Database\Eloquent\Scopes\BelongsToTenantScope; -use Sprout\Exceptions\TenantMismatch; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMismatchException; +use Sprout\Exceptions\TenantMissingException; use Sprout\Managers\TenancyManager; use Sprout\TenancyOptions; use Workbench\App\Models\TenantChild; @@ -100,7 +100,7 @@ public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCr { sprout()->setCurrentTenancy(app(TenancyManager::class)->get()); - $this->expectException(TenantMissing::class); + $this->expectException(TenantMissingException::class); $this->expectExceptionMessage( 'There is no current tenant for tenancy [tenants]' ); @@ -171,7 +171,7 @@ public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDiffere $tenancy->addOption(TenancyOptions::throwIfNotRelated()); - $this->expectException(TenantMismatch::class); + $this->expectException(TenantMismatchException::class); $this->expectExceptionMessage( 'Model [' . TenantChild::class @@ -259,7 +259,7 @@ public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenHy $tenancy->setTenant(null); - $this->expectException(TenantMissing::class); + $this->expectException(TenantMissingException::class); $this->expectExceptionMessage( 'There is no current tenant for tenancy [tenants]' ); @@ -328,7 +328,7 @@ public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDiffere $tenancy->setTenant(TenantModel::factory()->create()); - $this->expectException(TenantMismatch::class); + $this->expectException(TenantMismatchException::class); $this->expectExceptionMessage( 'Model [' . TenantChild::class diff --git a/tests/_Original/Overrides/StorageOverrideTest.php b/tests/_Original/Overrides/StorageOverrideTest.php index 829bf63..2657b2b 100644 --- a/tests/_Original/Overrides/StorageOverrideTest.php +++ b/tests/_Original/Overrides/StorageOverrideTest.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenantMissing; +use Sprout\Exceptions\TenantMissingException; use Sprout\Managers\TenancyManager; use Sprout\Overrides\AuthOverride; use Sprout\Overrides\CacheOverride; @@ -108,7 +108,7 @@ public function canCreateScopedTenantFilesystemDiskWithCustomConfig(): void #[Test, DefineEnvironment('createTenantDisk')] public function throwsExceptionIfThereIsNoTenant(): void { - $this->expectException(TenantMissing::class); + $this->expectException(TenantMissingException::class); $this->expectExceptionMessage('There is no current tenant for tenancy [tenants]'); Storage::disk('tenant'); From d320e3d444815734059ccc088c48f625bd01c828 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Mon, 16 Dec 2024 15:45:23 +0000 Subject: [PATCH 31/31] test: Add tests for the new route helper method on identity resolvers --- .../Resolvers/CookieIdentityResolverTest.php | 20 ++++++++++++++++++- .../Resolvers/HeaderIdentityResolverTest.php | 12 +++++++++++ .../Resolvers/PathIdentityResolverTest.php | 19 ++++++++++++++++++ .../Resolvers/SessionIdentityResolverTest.php | 20 +++++++++++++++++++ .../SubdomainIdentityResolverTest.php | 19 ++++++++++++++++++ 5 files changed, 89 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php index 6050ebf..ebf9a1b 100644 --- a/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php +++ b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php @@ -4,7 +4,6 @@ namespace Sprout\Tests\Unit\Http\Resolvers; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Routing\Router; use Illuminate\Support\Facades\Route; use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; @@ -24,6 +23,14 @@ protected function defineEnvironment($app): void }); } + protected function defineRoutes($router): void + { + $router->tenanted(function () { + Route::get('/tenant', function () { + })->name('tenant-route'); + }, 'cookie'); + } + protected function withCustomCookieName(Application $app): void { tap($app['config'], static function ($config) { @@ -128,4 +135,15 @@ public function replacesAllPlaceholdersInCookieName(): void $this->assertEmpty($resolver->getOptions()); $this->assertSame([ResolutionHook::Routing], $resolver->getHooks()); } + + #[Test] + public function canGenerateRoutesForATenant(): void + { + $resolver = resolver('cookie'); + $tenancy = tenancy(); + $tenant = TenantModel::factory()->createOne(); + + $this->assertSame('http://localhost/tenant', $resolver->route('tenant-route', $tenancy, $tenant)); + $this->assertSame('/tenant', $resolver->route('tenant-route', $tenancy, $tenant, absolute: false)); + } } diff --git a/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php b/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php index c1c8403..df214de 100644 --- a/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php +++ b/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php @@ -12,6 +12,7 @@ use Sprout\Http\Resolvers\HeaderIdentityResolver; use Sprout\Support\ResolutionHook; use Sprout\Tests\Unit\UnitTestCase; +use Workbench\App\Models\TenantModel; use function Sprout\resolver; use function Sprout\tenancy; @@ -121,4 +122,15 @@ public function addsTenantHeaderResponseMiddlewareToRoutes(): void $this->assertContains(AddTenantHeaderToResponse::class . ':' . $resolver->getName() . ',' . $tenancy->getName(), $middleware); } + + #[Test] + public function canGenerateRoutesForATenant(): void + { + $resolver = resolver('header'); + $tenancy = tenancy(); + $tenant = TenantModel::factory()->createOne(); + + $this->assertSame('http://localhost/tenant', $resolver->route('tenant-route', $tenancy, $tenant)); + $this->assertSame('/tenant', $resolver->route('tenant-route', $tenancy, $tenant, absolute: false)); + } } diff --git a/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php b/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php index 2680d1c..13ebf08 100644 --- a/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php +++ b/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php @@ -25,6 +25,14 @@ protected function defineEnvironment($app): void }); } + protected function defineRoutes($router): void + { + $router->tenanted(function () { + Route::get('/tenant', function () { + })->name('tenant-route'); + }, 'path'); + } + protected function withCustomSegment(Application $app): void { tap($app['config'], static function ($config) { @@ -160,4 +168,15 @@ public function setsUpRouteProperly(): void $this->assertArrayHasKey($tenancy->getName() . '_' . $resolver->getName(), $route->wheres); $this->assertSame('.*', $route->wheres[$tenancy->getName() . '_' . $resolver->getName()]); } + + #[Test] + public function canGenerateRoutesForATenant(): void + { + $resolver = resolver('path'); + $tenancy = tenancy(); + $tenant = TenantModel::factory()->createOne(); + + $this->assertSame('http://localhost/' . $tenant->getTenantIdentifier() . '/tenant', $resolver->route('tenant-route', $tenancy, $tenant)); + $this->assertSame('/' . $tenant->getTenantIdentifier() . '/tenant', $resolver->route('tenant-route', $tenancy, $tenant, absolute: false)); + } } diff --git a/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php b/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php index 8bfb9f8..a656220 100644 --- a/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php +++ b/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php @@ -4,6 +4,7 @@ namespace Sprout\Tests\Unit\Http\Resolvers; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Support\Facades\Route; use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; use Sprout\Http\Resolvers\SessionIdentityResolver; @@ -22,6 +23,14 @@ protected function defineEnvironment($app): void }); } + protected function defineRoutes($router): void + { + $router->tenanted(function () { + Route::get('/tenant', function () { + })->name('tenant-route'); + }, 'session'); + } + protected function withCustomSessionName(Application $app): void { tap($app['config'], static function ($config) { @@ -93,4 +102,15 @@ public function replacesAllPlaceholdersInSessionName(): void ); $this->assertSame([ResolutionHook::Middleware], $resolver->getHooks()); } + + #[Test] + public function canGenerateRoutesForATenant(): void + { + $resolver = resolver('session'); + $tenancy = tenancy(); + $tenant = TenantModel::factory()->createOne(); + + $this->assertSame('http://localhost/tenant', $resolver->route('tenant-route', $tenancy, $tenant)); + $this->assertSame('/tenant', $resolver->route('tenant-route', $tenancy, $tenant, absolute: false)); + } } diff --git a/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php b/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php index bdade30..daa01da 100644 --- a/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php +++ b/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php @@ -26,6 +26,14 @@ protected function defineEnvironment($app): void }); } + protected function defineRoutes($router): void + { + $router->tenanted(function () { + Route::get('/tenant', function () { + })->name('tenant-route'); + }, 'subdomain'); + } + protected function withCustomParameterPattern(Application $app): void { tap($app['config'], static function ($config) { @@ -142,4 +150,15 @@ public function setsUpRouteProperly(): void $this->assertArrayHasKey($tenancy->getName() . '_' . $resolver->getName(), $route->wheres); $this->assertSame('.*', $route->wheres[$tenancy->getName() . '_' . $resolver->getName()]); } + + #[Test] + public function canGenerateRoutesForATenant(): void + { + $resolver = resolver('subdomain'); + $tenancy = tenancy(); + $tenant = TenantModel::factory()->createOne(); + + $this->assertSame('http://' . $tenant->getTenantIdentifier() . '.localhost/tenant', $resolver->route('tenant-route', $tenancy, $tenant)); + $this->assertSame('/tenant', $resolver->route('tenant-route', $tenancy, $tenant, absolute: false)); + } }