diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2932450..9ece740 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: composer test diff --git a/composer.json b/composer.json index 013db51..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" : { @@ -63,13 +64,22 @@ "@clear", "@prepare", "@build", - "@php vendor/bin/phpunit" + "@php vendor/bin/phpunit --testsuite=Unit,Feature" + ], + "mutation" : [ + "@clear", + "@prepare", + "@build", + "@php vendor/bin/infection --threads=12" ] }, "extra" : { "laravel": { "providers": [ "Sprout\\SproutServiceProvider" + ], + "facades":[ + "Sprout\\Facades\\Sprout" ] } }, 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/phpunit.xml b/phpunit.xml index 36e7c26..7e3a02d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,8 +15,14 @@ testdox="true" > - - tests + + tests/_Original + + + ./tests/Feature + + + ./tests/Unit 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/Concerns/HandlesServiceOverrides.php b/src/Concerns/HandlesServiceOverrides.php index 3453467..ca973cf 100644 --- a/src/Concerns/HandlesServiceOverrides.php +++ b/src/Concerns/HandlesServiceOverrides.php @@ -54,27 +54,27 @@ 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; - ServiceOverrideRegistered::dispatch($overrideClass); + ServiceOverrideRegistered::dispatch($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; @@ -155,13 +155,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]); } /** @@ -170,13 +170,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; } /** @@ -236,13 +236,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/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; } 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/Facades/Sprout.php b/src/Facades/Sprout.php new file mode 100644 index 0000000..10a7091 --- /dev/null +++ b/src/Facades/Sprout.php @@ -0,0 +1,50 @@ + 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 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) + * @method static \Sprout\Managers\TenancyManager tenancies() + * @method static bool withinContext() + */ +final class Sprout extends Facade +{ + protected static function getFacadeAccessor(): string + { + return \Sprout\Sprout::class; + } +} 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/CookieIdentityResolver.php b/src/Http/Resolvers/CookieIdentityResolver.php index 2b76e58..3ead628 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 5832135..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; @@ -105,10 +105,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); } /** @@ -134,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 */ @@ -169,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 d01e79a..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; @@ -43,11 +43,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 = []) { @@ -81,6 +81,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 * @@ -90,7 +100,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; } @@ -113,10 +123,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); } /** @@ -128,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 */ @@ -163,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/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/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 1f69efb..deeec2d 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -146,18 +146,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 * @@ -228,7 +216,7 @@ public function markAsInContext(): self * * @return static */ - public function maskAsOutsideContext(): self + public function markAsOutsideContext(): self { $this->withinContext = false; @@ -272,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/BaseFactory.php b/src/Support/BaseFactory.php index 2d4035a..448ec2e 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'); @@ -173,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()); @@ -230,4 +250,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/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; } 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 { 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/Feature/.gitkeep b/tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/Unit/Attributes/CurrentTenantTest.php b/tests/Unit/Attributes/CurrentTenantTest.php new file mode 100644 index 0000000..631ddd8 --- /dev/null +++ b/tests/Unit/Attributes/CurrentTenantTest.php @@ -0,0 +1,82 @@ +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 = tenancy('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 = tenancy('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/Http/Resolvers/CookieIdentityResolverTest.php b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php new file mode 100644 index 0000000..ebf9a1b --- /dev/null +++ b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php @@ -0,0 +1,149 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + 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) { + $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()); + } + + #[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 new file mode 100644 index 0000000..df214de --- /dev/null +++ b/tests/Unit/Http/Resolvers/HeaderIdentityResolverTest.php @@ -0,0 +1,136 @@ +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); + } + + #[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 new file mode 100644 index 0000000..13ebf08 --- /dev/null +++ b/tests/Unit/Http/Resolvers/PathIdentityResolverTest.php @@ -0,0 +1,182 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + 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) { + $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()]); + } + + #[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 new file mode 100644 index 0000000..a656220 --- /dev/null +++ b/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php @@ -0,0 +1,116 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + }); + } + + 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) { + $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()); + } + + #[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 new file mode 100644 index 0000000..daa01da --- /dev/null +++ b/tests/Unit/Http/Resolvers/SubdomainIdentityResolverTest.php @@ -0,0 +1,164 @@ +set('multitenancy.providers.tenants.model', TenantModel::class); + $config->set('multitenancy.resolvers.subdomain.domain', 'localhost'); + }); + } + + 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) { + $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()]); + } + + #[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)); + } +} diff --git a/tests/Unit/Managers/IdentityResolverManagerTest.php b/tests/Unit/Managers/IdentityResolverManagerTest.php new file mode 100644 index 0000000..136bc71 --- /dev/null +++ b/tests/Unit/Managers/IdentityResolverManagerTest.php @@ -0,0 +1,191 @@ +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(); + + $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')); + } +} 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'); + } +} 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/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)); + } +} 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..f152494 --- /dev/null +++ b/tests/Unit/Support/DefaultTenancyTest.php @@ -0,0 +1,172 @@ +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..81cf122 --- /dev/null +++ b/tests/Unit/Support/ResolutionHelperTest.php @@ -0,0 +1,347 @@ +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 + { + [$resolverName, $tenancyName] = ResolutionHelper::parseOptions([]); + + $this->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); + } + + #[Test] + public function returnsFalseIfThereIsAlreadyATenant(): void + { + /** @var \Sprout\Contracts\Tenancy $tenancy */ + $tenancy = tenancy(); + + $tenancy->setTenant(TenantModel::factory()->createOne()); + + /** @var \Sprout\Contracts\IdentityResolver $resolver */ + $resolver = resolver('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 = tenancy(); + + /** @var \Sprout\Contracts\IdentityResolver $resolver */ + $resolver = resolver('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 = tenancy(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = resolver('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 = tenancy(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = resolver('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(NoTenantFoundException::class); + $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [' . $resolver->getName() . ']'); + + ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName()); + + $this->expectException(NoTenantFoundException::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 = tenancy(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = resolver('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 = tenancy(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = resolver('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 = tenancy(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = resolver('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(NoTenantFoundException::class); + $this->expectExceptionMessage('No valid tenant [' . $tenancy->getName() . '] found [' . $resolver->getName() . ']'); + + ResolutionHelper::handleResolution($fakeRequest, ResolutionHook::Routing, $resolver->getName(), $tenancy->getName()); + + $this->expectException(NoTenantFoundException::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 = tenancy(); + + /** @var \Sprout\Contracts\IdentityResolver&\Sprout\Contracts\IdentityResolverUsesParameters $resolver */ + $resolver = resolver('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()); + } +} diff --git a/tests/Unit/TenancyOptionsTest.php b/tests/Unit/TenancyOptionsTest.php new file mode 100644 index 0000000..9ee73df --- /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 = tenancy('tenants'); + $tenancy->removeOption(TenancyOptions::hydrateTenantRelation()); + + $this->assertFalse(TenancyOptions::shouldHydrateTenantRelation($tenancy)); + + $tenancy->addOption(TenancyOptions::hydrateTenantRelation()); + + $this->assertTrue(TenancyOptions::shouldHydrateTenantRelation($tenancy)); + + $tenancy = tenancy('backup'); + + $this->assertFalse(TenancyOptions::shouldHydrateTenantRelation($tenancy)); + } + + #[Test, DefineEnvironment('setupSecondTenancy')] + public function correctlyReportsThrowIfNotRelatedOptionPresence(): void + { + $tenancy = tenancy('tenants'); + $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); + + $this->assertFalse(TenancyOptions::shouldThrowIfNotRelated($tenancy)); + + $tenancy->addOption(TenancyOptions::throwIfNotRelated()); + + $this->assertTrue(TenancyOptions::shouldThrowIfNotRelated($tenancy)); + + $tenancy = tenancy('backup'); + + $this->assertFalse(TenancyOptions::shouldThrowIfNotRelated($tenancy)); + } +} diff --git a/tests/Unit/UnitTestCase.php b/tests/Unit/UnitTestCase.php new file mode 100644 index 0000000..049d89c --- /dev/null +++ b/tests/Unit/UnitTestCase.php @@ -0,0 +1,14 @@ +setCurrentTenancy(app(TenancyManager::class)->get()); - $this->expectException(TenantMissing::class); + $this->expectException(TenantMissingException::class); $this->expectExceptionMessage( 'There is no current tenant for tenancy [tenants]' ); @@ -206,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]' ); @@ -275,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/Database/Eloquent/BelongsToTenantTest.php b/tests/_Original/Database/Eloquent/BelongsToTenantTest.php similarity index 97% rename from tests/Database/Eloquent/BelongsToTenantTest.php rename to tests/_Original/Database/Eloquent/BelongsToTenantTest.php index 7bb857e..447f30d 100644 --- a/tests/Database/Eloquent/BelongsToTenantTest.php +++ b/tests/_Original/Database/Eloquent/BelongsToTenantTest.php @@ -1,7 +1,7 @@ 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/Database/Eloquent/TenantChildTest.php b/tests/_Original/Database/Eloquent/TenantChildTest.php similarity index 98% rename from tests/Database/Eloquent/TenantChildTest.php rename to tests/_Original/Database/Eloquent/TenantChildTest.php index f6d52f0..d725554 100644 --- a/tests/Database/Eloquent/TenantChildTest.php +++ b/tests/_Original/Database/Eloquent/TenantChildTest.php @@ -1,7 +1,7 @@ expectException(TenantMissing::class); + $this->expectException(TenantMissingException::class); $this->expectExceptionMessage('There is no current tenant for tenancy [tenants]'); Storage::disk('tenant'); diff --git a/tests/Providers/DatabaseTenantProviderTest.php b/tests/_Original/Providers/DatabaseTenantProviderTest.php similarity index 98% rename from tests/Providers/DatabaseTenantProviderTest.php rename to tests/_Original/Providers/DatabaseTenantProviderTest.php index 372dbf8..148954d 100644 --- a/tests/Providers/DatabaseTenantProviderTest.php +++ b/tests/_Original/Providers/DatabaseTenantProviderTest.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 @@ + */ + 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]); + } +}