From b79425f6dfba85ec32485a0e3a23060b1bc1d429 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:06:00 +0000 Subject: [PATCH 01/48] chore: Add new overrides config file --- resources/config/overrides.php | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 resources/config/overrides.php diff --git a/resources/config/overrides.php b/resources/config/overrides.php new file mode 100644 index 0000000..1e2da40 --- /dev/null +++ b/resources/config/overrides.php @@ -0,0 +1,46 @@ + [ + 'driver' => \Sprout\Overrides\StorageOverride::class, + ], + + 'job' => [ + 'driver' => \Sprout\Overrides\JobOverride::class, + ], + + 'cache' => [ + 'driver' => \Sprout\Overrides\CacheOverride::class, + ], + + 'auth' => [ + 'driver' => \Sprout\Overrides\AuthOverride::class, + ], + + 'cookie' => [ + 'driver' => \Sprout\Overrides\CookieOverride::class, + ], + + 'session' => [ + 'driver' => \Sprout\Overrides\SessionOverride::class, + 'database' => false, + ], +]; From b6e120d19b9c0e0c719f4eae3b8a906ca7c22e29 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:06:57 +0000 Subject: [PATCH 02/48] feat(tenancies): Introduce tenancy option config --- src/Contracts/Tenancy.php | 18 ++++++++++++ src/Support/DefaultTenancy.php | 51 +++++++++++++++++++++++++++++++--- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/Contracts/Tenancy.php b/src/Contracts/Tenancy.php index 9d8dfb8..433c01e 100644 --- a/src/Contracts/Tenancy.php +++ b/src/Contracts/Tenancy.php @@ -177,6 +177,15 @@ public function options(): array; */ public function hasOption(string $option): bool; + /** + * Check if a tenancy has an option with config + * + * @param string $option + * + * @return bool + */ + public function hasOptionConfig(string $option): bool; + /** * Add an option to the tenancy * @@ -194,4 +203,13 @@ public function addOption(string $option): static; * @return static */ public function removeOption(string $option): static; + + /** + * Get a tenancy options config + * + * @param string $option + * + * @return array|null + */ + public function optionConfig(string $option): ?array; } diff --git a/src/Support/DefaultTenancy.php b/src/Support/DefaultTenancy.php index c30dd8b..dc16139 100644 --- a/src/Support/DefaultTenancy.php +++ b/src/Support/DefaultTenancy.php @@ -50,6 +50,11 @@ final class DefaultTenancy implements Tenancy */ private array $options; + /** + * @var array> + */ + private array $optionConfig = []; + /** * @var \Sprout\Support\ResolutionHook|null */ @@ -58,15 +63,25 @@ final class DefaultTenancy implements Tenancy /** * Create a new instance * - * @param string $name - * @param \Sprout\Contracts\TenantProvider $provider - * @param list $options + * @param string $name + * @param \Sprout\Contracts\TenantProvider $provider + * @param list>> $options */ public function __construct(string $name, TenantProvider $provider, array $options) { $this->name = $name; $this->provider = $provider; - $this->options = $options; + + /** @var string|array> $value */ + foreach ($options as $value) { + if (is_array($value)) { + $option = array_keys($value)[0]; + $this->options[] = $option; + $this->optionConfig[$option] = $value[$option]; + } else if (is_string($value)) { + $this->options[] = $value; + } + } } /** @@ -287,6 +302,18 @@ public function hasOption(string $option): bool return in_array($option, $this->options(), true); } + /** + * Check if a tenancy has an option with config + * + * @param string $option + * + * @return bool + */ + public function hasOptionConfig(string $option): bool + { + return isset($this->optionConfig[$option]); + } + /** * Add an option to the tenancy * @@ -319,6 +346,22 @@ public function removeOption(string $option): static return $this; } + /** + * Get a tenancy options config + * + * @param string $option + * + * @return array|null + */ + public function optionConfig(string $option): ?array + { + if (! $this->hasOptionConfig($option)) { + return null; + } + + return $this->optionConfig[$option]; + } + /** * Set the hook where the tenant was resolved * From b0cd9b429fc12fdf8181ec6a1738f66ad96ed924 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:07:17 +0000 Subject: [PATCH 03/48] chore: Add overrides tenancy option with sensible default --- resources/config/multitenancy.php | 8 ++++++++ src/TenancyOptions.php | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/resources/config/multitenancy.php b/resources/config/multitenancy.php index 3cd377a..1e84174 100644 --- a/resources/config/multitenancy.php +++ b/resources/config/multitenancy.php @@ -50,6 +50,14 @@ 'options' => [ TenancyOptions::hydrateTenantRelation(), TenancyOptions::throwIfNotRelated(), + TenancyOptions::overrides([ + 'filesystem', + 'job', + 'cache', + 'auth', + 'cookie', + 'session', + ]), ], ], diff --git a/src/TenancyOptions.php b/src/TenancyOptions.php index 63103da..8dd36f4 100644 --- a/src/TenancyOptions.php +++ b/src/TenancyOptions.php @@ -34,6 +34,18 @@ public static function throwIfNotRelated(): string return 'tenant-relation.strict'; } + /** + * @param list $overrides + * + * @return array> + */ + public static function overrides(array $overrides): array + { + return [ + 'overrides' => $overrides, + ]; + } + /** * @param \Sprout\Contracts\Tenancy<*> $tenancy * @@ -53,4 +65,14 @@ public static function shouldThrowIfNotRelated(Tenancy $tenancy): bool { return $tenancy->hasOption(static::throwIfNotRelated()); } + + /** + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * + * @return list|null + */ + public static function enabledOverrides(Tenancy $tenancy): array|null + { + return $tenancy->optionConfig('overrides'); + } } From 084c9e8cdd900f1af64fa9a97ada8c5fb8524958 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:07:57 +0000 Subject: [PATCH 04/48] chore: Add generics to setup and cleanup on ServiceOverride --- src/Contracts/ServiceOverride.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Contracts/ServiceOverride.php b/src/Contracts/ServiceOverride.php index 86c0758..5e1626a 100644 --- a/src/Contracts/ServiceOverride.php +++ b/src/Contracts/ServiceOverride.php @@ -19,8 +19,12 @@ interface ServiceOverride * override. * It is called when a new tenant is marked as the current tenant. * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant * * @return void */ @@ -37,8 +41,12 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void; * It will be called before {@see self::setup()}, but only if the previous * tenant was not null. * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant * * @return void */ From 801ee12f85d2c7b3bb748e2f4d411fc77c728413 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:08:12 +0000 Subject: [PATCH 05/48] refactor: Add a constructor to the ServiceOverride interface --- src/Contracts/ServiceOverride.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Contracts/ServiceOverride.php b/src/Contracts/ServiceOverride.php index 5e1626a..40b0a2f 100644 --- a/src/Contracts/ServiceOverride.php +++ b/src/Contracts/ServiceOverride.php @@ -12,6 +12,14 @@ */ interface ServiceOverride { + /** + * Create a new instance of the service override + * + * @param string $service + * @param array $config + */ + public function __construct(string $service, array $config); + /** * Set up the service override * From 054230223225018070ea5b7a4c7d281ed54d705c Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:08:40 +0000 Subject: [PATCH 06/48] chore: Add config publishing for new overrides config file --- src/SproutServiceProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index 1bf3228..968eebc 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -102,6 +102,7 @@ private function publishConfig(): void $this->publishes([ __DIR__ . '/../resources/config/sprout.php' => config_path('sprout.php'), __DIR__ . '/../resources/config/multitenancy.php' => config_path('multitenancy.php'), + __DIR__ . '/../resources/config/overrides.php' => config_path('sprout/overrides.php'), ], ['config', 'sprout-config']); } From 8b76886d9d221945a9184b7aef5e9f4cbf4c8d17 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:09:15 +0000 Subject: [PATCH 07/48] feat(overrides): Add new service override manager with base functionality --- src/Events/ServiceOverrideBooted.php | 30 +- src/Events/ServiceOverrideEvent.php | 33 +++ src/Events/ServiceOverrideProcessed.php | 43 --- src/Events/ServiceOverrideProcessing.php | 38 --- src/Events/ServiceOverrideRegistered.php | 27 +- src/Exceptions/ServiceOverrideException.php | 23 +- src/Listeners/CleanupServiceOverrides.php | 16 +- src/Listeners/SetupServiceOverrides.php | 14 +- src/Managers/ServiceOverrideManager.php | 309 ++++++++++++++++++++ src/Sprout.php | 15 +- src/SproutServiceProvider.php | 22 +- 11 files changed, 398 insertions(+), 172 deletions(-) delete mode 100644 src/Events/ServiceOverrideProcessed.php delete mode 100644 src/Events/ServiceOverrideProcessing.php create mode 100644 src/Managers/ServiceOverrideManager.php diff --git a/src/Events/ServiceOverrideBooted.php b/src/Events/ServiceOverrideBooted.php index 530a696..9abee3a 100644 --- a/src/Events/ServiceOverrideBooted.php +++ b/src/Events/ServiceOverrideBooted.php @@ -10,36 +10,12 @@ * * This event is dispatched after a service override has been booted. * - * @template ServiceOverrideClass of \Sprout\Contracts\ServiceOverride + * @template OverrideClass of \Sprout\Contracts\ServiceOverride * - * @package Overrides + * @extends \Sprout\Events\ServiceOverrideEvent * - * @method static self dispatch(string $service, object $override) - * @method static self dispatchIf(bool $boolean, string $service, object $override) - * @method static self dispatchUnless(bool $boolean, string $service, object $override) + * @package Overrides */ final class ServiceOverrideBooted extends ServiceOverrideEvent { - /** - * @var string - */ - public readonly string $service; - - /** - * @var object<\Sprout\Contracts\ServiceOverride> - * @phpstan-var ServiceOverrideClass - */ - public readonly object $override; - - /** - * @param string $service - * @param object<\Sprout\Contracts\ServiceOverride> $override - * - * @phpstan-param ServiceOverrideClass $override - */ - public function __construct(string $service, object $override) - { - $this->service = $service; - $this->override = $override; - } } diff --git a/src/Events/ServiceOverrideEvent.php b/src/Events/ServiceOverrideEvent.php index 52753f0..63dd06d 100644 --- a/src/Events/ServiceOverrideEvent.php +++ b/src/Events/ServiceOverrideEvent.php @@ -4,15 +4,48 @@ namespace Sprout\Events; use Illuminate\Foundation\Events\Dispatchable; +use Sprout\Contracts\ServiceOverride; /** * Service Override Event * * This is a base event class for the service override events. * + * @template OverrideClass of \Sprout\Contracts\ServiceOverride + * + * @package Overrides + * + * @method static self dispatch(string $service, ServiceOverride $override) + * @method static self dispatchIf(bool $boolean, string $service, ServiceOverride $override) + * @method static self dispatchUnless(bool $boolean, string $service, ServiceOverride $override) + * * @package Overrides */ abstract class ServiceOverrideEvent { use Dispatchable; + + /** + * @var string + */ + public readonly string $service; + + /** + * @var \Sprout\Contracts\ServiceOverride + * + * @phpstam-var OverrideClass + */ + public readonly ServiceOverride $override; + + /** + * @param string $service + * @param \Sprout\Contracts\ServiceOverride $override + * + * @phpstan-param OverrideClass $override + */ + public function __construct(string $service, ServiceOverride $override) + { + $this->service = $service; + $this->override = $override; + } } diff --git a/src/Events/ServiceOverrideProcessed.php b/src/Events/ServiceOverrideProcessed.php deleted file mode 100644 index 72138cc..0000000 --- a/src/Events/ServiceOverrideProcessed.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @phpstan-var ServiceOverrideClass - */ - public readonly object $override; - - /** - * @param string $service - * @param object<\Sprout\Contracts\ServiceOverride> $override - * - * @phpstan-param ServiceOverrideClass $override - */ - public function __construct(string $service, object $override) - { - $this->service = $service; - $this->override = $override; - } -} diff --git a/src/Events/ServiceOverrideProcessing.php b/src/Events/ServiceOverrideProcessing.php deleted file mode 100644 index 19861d3..0000000 --- a/src/Events/ServiceOverrideProcessing.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ - public readonly string $override; - - /** - * @param string $service - * @param class-string<\Sprout\Contracts\ServiceOverride> $override - */ - public function __construct(string $service, string $override) - { - $this->service = $service; - $this->override = $override; - } -} diff --git a/src/Events/ServiceOverrideRegistered.php b/src/Events/ServiceOverrideRegistered.php index 4b96d1b..da2663f 100644 --- a/src/Events/ServiceOverrideRegistered.php +++ b/src/Events/ServiceOverrideRegistered.php @@ -9,31 +9,12 @@ * This event is dispatched when a service override is registered with * Sprout. * - * @package Overrides + * @template OverrideClass of \Sprout\Contracts\ServiceOverride + * + * @extends \Sprout\Events\ServiceOverrideEvent * - * @method static self dispatch(string $service, string $override) - * @method static self dispatchIf(bool $boolean, string $service, string $override) - * @method static self dispatchUnless(bool $boolean, string $service, string $override) + * @package Overrides */ final class ServiceOverrideRegistered extends ServiceOverrideEvent { - /** - * @var string - */ - public readonly string $service; - - /** - * @var class-string<\Sprout\Contracts\ServiceOverride> - */ - public readonly string $override; - - /** - * @param string $service - * @param class-string<\Sprout\Contracts\ServiceOverride> $override - */ - public function __construct(string $service, string $override) - { - $this->service = $service; - $this->override = $override; - } } diff --git a/src/Exceptions/ServiceOverrideException.php b/src/Exceptions/ServiceOverrideException.php index 2bb9037..4ceb7f8 100644 --- a/src/Exceptions/ServiceOverrideException.php +++ b/src/Exceptions/ServiceOverrideException.php @@ -3,34 +3,31 @@ namespace Sprout\Exceptions; -use Sprout\Contracts\ServiceOverride; - final class ServiceOverrideException extends SproutException { /** - * Create an exception when a provided service override class is invalid + * Create an exception for when attempting to boot a non-bootable service override * - * @param class-string $class + * @param string $service * * @return self */ - public static function invalidClass(string $class): self + public static function notBootable(string $service): self { - return new self('The provided service override [' . $class . '] does not implement the ' . ServiceOverride::class . ' interface'); + return new self('The service override [' . $service . '] is not bootable'); } /** - * Create an exception when attempting to replace a service override that has already been processed + * Create an exception for when a service override has been set up, but isn't + * enabled for the tenancy * - * @param string $service - * @param class-string<\Sprout\Contracts\ServiceOverride> $class + * @param string $service + * @param string $tenancy * * @return self */ - public static function alreadyProcessed(string $service, string $class): self + public static function setupButNotEnabled(string $service, string $tenancy): self { - return new self( - 'The service [' . $service . '] already has an override registered [' . $class . '] which has already been processed' - ); + return new self('The service override [' . $service . '] has been set up for the tenancy [' . $tenancy . '] but it is not enabled for that tenancy'); } } diff --git a/src/Listeners/CleanupServiceOverrides.php b/src/Listeners/CleanupServiceOverrides.php index afb3512..560b42d 100644 --- a/src/Listeners/CleanupServiceOverrides.php +++ b/src/Listeners/CleanupServiceOverrides.php @@ -4,7 +4,7 @@ namespace Sprout\Listeners; use Sprout\Events\CurrentTenantChanged; -use Sprout\Sprout; +use Sprout\Managers\ServiceOverrideManager; /** * Clean-up Service Overrides @@ -17,18 +17,18 @@ final class CleanupServiceOverrides { /** - * @var \Sprout\Sprout + * @var \Sprout\Managers\ServiceOverrideManager */ - private Sprout $sprout; + private ServiceOverrideManager $overrides; /** * Create a new instance * - * @param \Sprout\Sprout $sprout + * @param \Sprout\Managers\ServiceOverrideManager $overrides */ - public function __construct(Sprout $sprout) + public function __construct(ServiceOverrideManager $overrides) { - $this->sprout = $sprout; + $this->overrides = $overrides; } /** @@ -39,6 +39,8 @@ public function __construct(Sprout $sprout) * @param \Sprout\Events\CurrentTenantChanged $event * * @return void + * + * @throws \Sprout\Exceptions\ServiceOverrideException */ public function handle(CurrentTenantChanged $event): void { @@ -47,6 +49,6 @@ public function handle(CurrentTenantChanged $event): void return; } - $this->sprout->cleanupOverrides($event->tenancy, $event->previous); + $this->overrides->cleanupOverrides($event->tenancy, $event->previous); } } diff --git a/src/Listeners/SetupServiceOverrides.php b/src/Listeners/SetupServiceOverrides.php index 1f9a314..1ea1f89 100644 --- a/src/Listeners/SetupServiceOverrides.php +++ b/src/Listeners/SetupServiceOverrides.php @@ -4,7 +4,7 @@ namespace Sprout\Listeners; use Sprout\Events\CurrentTenantChanged; -use Sprout\Sprout; +use Sprout\Managers\ServiceOverrideManager; /** * Setup Service Overrides @@ -17,18 +17,18 @@ final class SetupServiceOverrides { /** - * @var \Sprout\Sprout + * @var \Sprout\Managers\ServiceOverrideManager */ - private Sprout $sprout; + private ServiceOverrideManager $overrides; /** * Create a new instance * - * @param \Sprout\Sprout $sprout + * @param \Sprout\Managers\ServiceOverrideManager $overrides */ - public function __construct(Sprout $sprout) + public function __construct(ServiceOverrideManager $overrides) { - $this->sprout = $sprout; + $this->overrides = $overrides; } /** @@ -47,6 +47,6 @@ public function handle(CurrentTenantChanged $event): void return; } - $this->sprout->setupOverrides($event->tenancy, $event->current); + $this->overrides->setupOverrides($event->tenancy, $event->current); } } diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php new file mode 100644 index 0000000..d05c2b5 --- /dev/null +++ b/src/Managers/ServiceOverrideManager.php @@ -0,0 +1,309 @@ + + */ + protected array $overrides = []; + + /** + * @var array> + */ + protected array $overrideClasses = []; + + /** + * @var list + */ + protected array $bootableOverrides = []; + + protected bool $overridesBooted = false; + + /** + * @var array, string>> + */ + protected array $setupOverrides = []; + + /** + * Create a new factory instance + * + * @param \Illuminate\Contracts\Foundation\Application $app + */ + public function __construct(Application $app) + { + $this->app = $app; + } + + /** + * @param string $service + * + * @return array|null + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + protected function getServiceConfig(string $service): ?array + { + /** @var array|null $config */ + $config = $this->app->make('config')->get('sprout.overrides.' . $service); + + return $config; + } + + public function hasOverride(string $service): bool + { + return isset($this->overrides[$service]); + } + + public function haveOverridesBooted(): bool + { + return $this->overridesBooted; + } + + /** + * Get all services whose overrides have been set up for a tenancy + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * + * @return array, string> + */ + public function getSetupOverrides(Tenancy $tenancy): array + { + return $this->setupOverrides[$tenancy->getName()] ?? []; + } + + /** + * Get an override for a service + * + * @param string $service + * + * @return \Sprout\Contracts\ServiceOverride|null + */ + public function get(string $service): ?ServiceOverride + { + if ($this->hasOverride($service)) { + return $this->overrides[$service]; + } + + return null; + } + + /** + * Register all the configured service overrides + * + * @return void + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\ServiceOverrideException + */ + public function registerOverrides(): void + { + /** @var array> $services */ + $services = $this->app->make('config')->get('sprout.overrides', []); + + foreach ($services as $service => $config) { + $this->register($service); + } + } + + /** + * Boot all the registered service overrides + * + * @return void + * + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\ServiceOverrideException + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function bootOverrides(): void + { + foreach ($this->bootableOverrides as $service) { + $this->boot($service); + } + } + + /** + * Setup all the registered and enabled service overrides + * + * @template TenantClass of Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + public function setupOverrides(Tenancy $tenancy, Tenant $tenant): void + { + // Get the overrides enabled for this tenancy + $enabled = TenancyOptions::enabledOverrides($tenancy) ?? []; + + // Loop through all registered overrides + foreach ($this->overrides as $service => $override) { + // If the override is enabled + if (in_array($service, $enabled, true)) { + // Perform the setup action + $override->setup($tenancy, $tenant); + // Keep track of the fact the override was set up + $this->setupOverrides[$tenancy->getName()][$this->overrideClasses[$service]] = $service; + } + } + } + + /** + * Clean-up all registered and setup service overrides + * + * @template TenantClass of Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + * + * @throws \Sprout\Exceptions\ServiceOverrideException + */ + public function cleanupOverrides(Tenancy $tenancy, Tenant $tenant): void + { + // Get the overrides enabled for this tenancy + $enabled = TenancyOptions::enabledOverrides($tenancy) ?? []; + $setupOverrides = $this->getSetupOverrides($tenancy); + + // Loop through all registered overrides + foreach ($setupOverrides as $driver => $service) { + // If the override is enabled + if (in_array($service, $enabled, true)) { + // Perform the setup action + $this->overrides[$service]->cleanup($tenancy, $tenant); + + unset($this->setupOverrides[$tenancy->getName()][$driver]); + } else { + throw ServiceOverrideException::setupButNotEnabled($service, $tenancy->getName()); + } + } + } + + /** + * Register a service override + * + * @param string $service + * + * @return static + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\ServiceOverrideException + */ + protected function register(string $service): self + { + // If the override already exists, we'll error out, because it should + // we'd just load the same config again + if ($this->hasOverride($service)) { + return $this; + } + + // Get the config for this service override + $config = $this->getServiceConfig($service); + + // If there isn't config, there's an issue + if ($config === null) { + throw MisconfigurationException::notFound('service override', $service); + } + + // If there is config, but it's missing a driver, that's also an issue + if (! isset($config['driver'])) { + throw MisconfigurationException::missingConfig('driver', 'service override', $service); + } + + /** @var array{driver:string} $config */ + + // If there is a driver, but it doesn't implement the correct interface, + // that's also an issue + if (! is_subclass_of($config['driver'], ServiceOverride::class)) { + throw MisconfigurationException::invalidConfig('driver', 'service override', $service); + } + + /** @var class-string<\Sprout\Contracts\ServiceOverride> $driver */ + $driver = $config['driver']; + + // Create a new instance of the service override with the service name + // and config, as we know the constructor signature + $override = $this->app->make($driver, compact('service', 'config')); + + // Store the override + $this->overrides[$service] = $override; + + // And map it to its driver, rather than the final class + $this->overrideClasses[$service] = $driver; + + ServiceOverrideRegistered::dispatch($service, $override); + + // If the service override is bootable, we'll keep track of that too + if ($override instanceof BootableServiceOverride) { + $this->bootableOverrides[] = $service; + + // If the overrides have already booted, we'll boot it + if ($this->haveOverridesBooted()) { + $this->boot($service); + } + } + + return $this; + } + + /** + * Boot a service override + * + * @param string $service + * + * @return static + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\ServiceOverrideException + */ + protected function boot(string $service): self + { + // If the override doesn't exist, that's an issue + if ($this->hasOverride($service)) { + throw MisconfigurationException::notFound('service override', $service); + } + + $override = $this->overrides[$service]; + + // If the override exists, but isn't bootable, that's also an issue + if (! ($override instanceof BootableServiceOverride)) { + throw ServiceOverrideException::notBootable($service); + } + + $override->boot($this->app, $this->app->make(Sprout::class)); + + ServiceOverrideBooted::dispatch($service, $override); + + return $this; + } +} diff --git a/src/Sprout.php b/src/Sprout.php index 371a615..844ac34 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -8,8 +8,9 @@ use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Managers\IdentityResolverManager; -use Sprout\Managers\TenantProviderManager; +use Sprout\Managers\ServiceOverrideManager; use Sprout\Managers\TenancyManager; +use Sprout\Managers\TenantProviderManager; use Sprout\Support\ResolutionHook; use Sprout\Support\SettingsRepository; @@ -182,6 +183,18 @@ public function tenancies(): TenancyManager return $this->app->make(TenancyManager::class); } + /** + * Get the service override manager + * + * @return \Sprout\Managers\ServiceOverrideManager + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function overrides(): ServiceOverrideManager + { + return $this->app->make(ServiceOverrideManager::class); + } + /** * Check if a resolution hook is enabled * diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index 968eebc..b6ea881 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -7,14 +7,14 @@ use Illuminate\Routing\Events\RouteMatched; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; -use RuntimeException; use Sprout\Events\CurrentTenantChanged; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Http\RouterMethods; use Sprout\Listeners\IdentifyTenantOnRouting; use Sprout\Managers\IdentityResolverManager; -use Sprout\Managers\TenantProviderManager; +use Sprout\Managers\ServiceOverrideManager; use Sprout\Managers\TenancyManager; +use Sprout\Managers\TenantProviderManager; use Sprout\Support\ResolutionHook; use Sprout\Support\SettingsRepository; @@ -64,10 +64,15 @@ private function registerManagers(): void return new TenancyManager($app, $app->make(TenantProviderManager::class)); }); + $this->app->singleton(ServiceOverrideManager::class, function ($app) { + return new ServiceOverrideManager($app); + }); + // Alias the managers with simple names $this->app->alias(TenantProviderManager::class, 'sprout.providers'); $this->app->alias(IdentityResolverManager::class, 'sprout.resolvers'); $this->app->alias(TenancyManager::class, 'sprout.tenancies'); + $this->app->alias(ServiceOverrideManager::class, 'sprout.overrides'); } private function registerMiddleware(): void @@ -86,7 +91,7 @@ protected function registerRouteMixin(): void protected function registerServiceOverrideBooting(): void { - $this->app->booted($this->sprout->bootOverrides(...)); + $this->app->booted($this->sprout->overrides()->bootOverrides(...)); } public function boot(): void @@ -108,16 +113,7 @@ private function publishConfig(): void private function registerServiceOverrides(): void { - /** @var array> $overrides */ - $overrides = config('sprout.services', []); - - foreach ($overrides as $service => $overrideClass) { - if (! is_string($service)) { - throw new RuntimeException('Service overrides must be registered against a "service"'); // @codeCoverageIgnore - } - - $this->sprout->registerOverride($service, $overrideClass); - } + $this->sprout->overrides()->registerOverrides(); } private function registerEventListeners(): void From a449b36bfc7793797ef0150e941d22369d162f4a Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 00:15:47 +0000 Subject: [PATCH 08/48] chore: Add override.all tenancy option --- src/Managers/ServiceOverrideManager.php | 8 +++++--- src/TenancyOptions.php | 24 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index d05c2b5..6b1440b 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -159,12 +159,13 @@ public function bootOverrides(): void public function setupOverrides(Tenancy $tenancy, Tenant $tenant): void { // Get the overrides enabled for this tenancy - $enabled = TenancyOptions::enabledOverrides($tenancy) ?? []; + $enabled = TenancyOptions::enabledOverrides($tenancy) ?? []; + $allEnabled = TenancyOptions::shouldEnableAllOverrides($tenancy); // Loop through all registered overrides foreach ($this->overrides as $service => $override) { // If the override is enabled - if (in_array($service, $enabled, true)) { + if ($allEnabled || in_array($service, $enabled, true)) { // Perform the setup action $override->setup($tenancy, $tenant); // Keep track of the fact the override was set up @@ -191,12 +192,13 @@ public function cleanupOverrides(Tenancy $tenancy, Tenant $tenant): void { // Get the overrides enabled for this tenancy $enabled = TenancyOptions::enabledOverrides($tenancy) ?? []; + $allEnabled = TenancyOptions::shouldEnableAllOverrides($tenancy); $setupOverrides = $this->getSetupOverrides($tenancy); // Loop through all registered overrides foreach ($setupOverrides as $driver => $service) { // If the override is enabled - if (in_array($service, $enabled, true)) { + if ($allEnabled || in_array($service, $enabled, true)) { // Perform the setup action $this->overrides[$service]->cleanup($tenancy, $tenant); diff --git a/src/TenancyOptions.php b/src/TenancyOptions.php index 8dd36f4..432991f 100644 --- a/src/TenancyOptions.php +++ b/src/TenancyOptions.php @@ -35,6 +35,8 @@ public static function throwIfNotRelated(): string } /** + * Only enable these overrides for the tenancy + * * @param list $overrides * * @return array> @@ -46,6 +48,16 @@ public static function overrides(array $overrides): array ]; } + /** + * Enable all overrides for the tenancy + * + * @return string + */ + public static function allOverrides(): string + { + return 'overrides.all'; + } + /** * @param \Sprout\Contracts\Tenancy<*> $tenancy * @@ -73,6 +85,16 @@ public static function shouldThrowIfNotRelated(Tenancy $tenancy): bool */ public static function enabledOverrides(Tenancy $tenancy): array|null { - return $tenancy->optionConfig('overrides'); + return $tenancy->optionConfig('overrides'); // @phpstan-ignore-line + } + + /** + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * + * @return bool + */ + public static function shouldEnableAllOverrides(Tenancy $tenancy): bool + { + return $tenancy->hasOption(static::allOverrides()); } } From 556183492ef0a2e04452e47fd17b3bb1463064b1 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:15:26 +0000 Subject: [PATCH 09/48] chore: Remove services config from sprout config --- resources/config/sprout.php | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/resources/config/sprout.php b/resources/config/sprout.php index 98321cf..29d5c4f 100644 --- a/resources/config/sprout.php +++ b/resources/config/sprout.php @@ -42,35 +42,4 @@ \Sprout\Listeners\SetupServiceOverrides::class, ], - /* - |-------------------------------------------------------------------------- - | Service Overrides - |-------------------------------------------------------------------------- - | - | This is an array of service override classes. - | These classes will be instantiated and automatically run when relevant. - | - */ - - 'services' => [ - // This will override the storage by introducing a 'sprout' driver - // that wraps any other storage drive in a tenant resource subdirectory. - Services::STORAGE => \Sprout\Overrides\StorageOverride::class, - // This will hydrate tenants when running jobs, based on the current - // context. - Services::JOB => \Sprout\Overrides\JobOverride::class, - // This will override the cache by introducing a 'sprout' driver - // that adds a prefix to cache stores for the current tenant. - Services::CACHE => \Sprout\Overrides\CacheOverride::class, - // This is a simple override that removes all currently resolved - // guards to prevent user auth leaking. - Services::AUTH => \Sprout\Overrides\AuthOverride::class, - // This will override the cookie settings so that all created cookies - // are specific to the tenant. - Services::COOKIE => \Sprout\Overrides\CookieOverride::class, - // This will override the session by introducing a 'sprout' driver - // that wraps any other session store. - Services::SESSION => \Sprout\Overrides\SessionOverride::class, - ], - ]; From 75c399aa4835b6ae304feb22f4789022b27ba760 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:15:47 +0000 Subject: [PATCH 10/48] chore: Add manager to option to override filesystem manager to overrides config --- resources/config/overrides.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/config/overrides.php b/resources/config/overrides.php index 1e2da40..53ab72d 100644 --- a/resources/config/overrides.php +++ b/resources/config/overrides.php @@ -20,7 +20,11 @@ return [ 'filesystem' => [ - 'driver' => \Sprout\Overrides\StorageOverride::class, + 'driver' => \Sprout\Overrides\FilesystemOverride::class, + // This config option defines whether the filesystem override will + // override the filesystem manager with a Sprout version. + // The default value is 'true' + 'manager' => true, ], 'job' => [ From 4973ed390ad960fc7b367303bd72e523689be303 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:16:54 +0000 Subject: [PATCH 11/48] chore: Updated compatibility check on session and cookie resolvers --- src/Http/Resolvers/CookieIdentityResolver.php | 4 ++-- src/Http/Resolvers/SessionIdentityResolver.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Http/Resolvers/CookieIdentityResolver.php b/src/Http/Resolvers/CookieIdentityResolver.php index 9de0e07..bd87aca 100644 --- a/src/Http/Resolvers/CookieIdentityResolver.php +++ b/src/Http/Resolvers/CookieIdentityResolver.php @@ -120,8 +120,8 @@ public function getRequestCookieName(Tenancy $tenancy): string */ public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string { - if (sprout()->hasOverride(CookieOverride::class)) { - throw CompatibilityException::make('resolver', $this->getName(), 'override', CookieOverride::class); + if (sprout()->overrides()->hasOverride('cookie')) { + throw CompatibilityException::make('resolver', $this->getName(), 'service override', 'cookie'); } /** diff --git a/src/Http/Resolvers/SessionIdentityResolver.php b/src/Http/Resolvers/SessionIdentityResolver.php index 1c14322..6909b0c 100644 --- a/src/Http/Resolvers/SessionIdentityResolver.php +++ b/src/Http/Resolvers/SessionIdentityResolver.php @@ -99,8 +99,8 @@ public function getRequestSessionName(Tenancy $tenancy): string */ public function resolveFromRequest(Request $request, Tenancy $tenancy): ?string { - if (sprout()->hasOverride(SessionOverride::class)) { - throw CompatibilityException::make('resolver', $this->getName(), 'override', SessionOverride::class); + if (sprout()->overrides()->hasOverride('session')) { + throw CompatibilityException::make('resolver', $this->getName(), 'service override', 'session'); } /** From afe56af11895d28ff2c6c21fa7e08ea1483dc80f Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:17:36 +0000 Subject: [PATCH 12/48] refactor: Update service overrides to use new approach --- src/Overrides/AuthOverride.php | 2 +- src/Overrides/BaseOverride.php | 77 +++++++ .../Cache/SproutCacheDriverCreator.php | 130 +++++++++++ src/Overrides/CacheOverride.php | 206 ++--------------- src/Overrides/CookieOverride.php | 35 +-- .../SproutFilesystemDriverCreator.php | 177 +++++++++++++++ .../Filesystem/SproutFilesystemManager.php | 68 ++++++ src/Overrides/FilesystemOverride.php | 123 ++++++++++ src/Overrides/JobOverride.php | 44 +--- ...r.php => SproutDatabaseSessionHandler.php} | 4 +- .../SproutSessionDatabaseDriverCreator.php | 108 +++++++++ .../SproutSessionFileDriverCreator.php | 100 ++++++++ src/Overrides/SessionOverride.php | 137 +++-------- src/Overrides/StorageOverride.php | 213 ------------------ 14 files changed, 841 insertions(+), 583 deletions(-) create mode 100644 src/Overrides/BaseOverride.php create mode 100644 src/Overrides/Cache/SproutCacheDriverCreator.php create mode 100644 src/Overrides/Filesystem/SproutFilesystemDriverCreator.php create mode 100644 src/Overrides/Filesystem/SproutFilesystemManager.php create mode 100644 src/Overrides/FilesystemOverride.php rename src/Overrides/Session/{TenantAwareDatabaseSessionHandler.php => SproutDatabaseSessionHandler.php} (94%) create mode 100644 src/Overrides/Session/SproutSessionDatabaseDriverCreator.php create mode 100644 src/Overrides/Session/SproutSessionFileDriverCreator.php delete mode 100644 src/Overrides/StorageOverride.php diff --git a/src/Overrides/AuthOverride.php b/src/Overrides/AuthOverride.php index 10b5e1d..6041ef4 100644 --- a/src/Overrides/AuthOverride.php +++ b/src/Overrides/AuthOverride.php @@ -24,7 +24,7 @@ * * @package Overrides */ -final class AuthOverride implements BootableServiceOverride +final class AuthOverride extends BaseOverride implements BootableServiceOverride { /** * Boot a service override diff --git a/src/Overrides/BaseOverride.php b/src/Overrides/BaseOverride.php new file mode 100644 index 0000000..fdf047a --- /dev/null +++ b/src/Overrides/BaseOverride.php @@ -0,0 +1,77 @@ + + */ + protected array $config; + + /** + * Create a new instance of the service override + * + * @param string $service + * @param array $config + */ + public function __construct(string $service, array $config) + { + $this->config = $config; + $this->service = $service; + } + + /** + * Set up the service override + * + * This method should perform any necessary setup actions for the service + * override. + * It is called when a new tenant is marked as the current tenant. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + public function setup(Tenancy $tenancy, Tenant $tenant): void + { + // I'm intentionally empty + } + + /** + * Clean up the service override + * + * This method should perform any necessary setup actions for the service + * override. + * It is called when the current tenant is unset, either to be replaced + * by another tenant, or none. + * + * It will be called before {@see self::setup()}, but only if the previous + * tenant was not null. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + public function cleanup(Tenancy $tenancy, Tenant $tenant): void + { + // I'm intentionally empty + } + +} diff --git a/src/Overrides/Cache/SproutCacheDriverCreator.php b/src/Overrides/Cache/SproutCacheDriverCreator.php new file mode 100644 index 0000000..1b7754d --- /dev/null +++ b/src/Overrides/Cache/SproutCacheDriverCreator.php @@ -0,0 +1,130 @@ + + */ + private array $config; + + /** + * @var \Sprout\Sprout + */ + private Sprout $sprout; + + /** + * Create a new instance + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Illuminate\Cache\CacheManager $manager + * @param array $config + * @param \Sprout\Sprout $sprout + */ + public function __construct(Application $app, CacheManager $manager, array $config, Sprout $sprout) + { + $this->app = $app; + $this->config = $config; + $this->manager = $manager; + $this->sprout = $sprout; + } + + /** + * Create the Sprout cache driver + * + * @return \Illuminate\Contracts\Cache\Repository + * + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + */ + public function __invoke(): Repository + { + // If we're not within a multitenanted context, we need to error + // out, as this driver shouldn't be hit without one + if (! $this->sprout->withinContext()) { + // TODO: Create a better exception + throw TenancyMissingException::make(); + } + + // Get the current active tenancy + $tenancy = $this->sprout->getCurrentTenancy(); + + // If there isn't one, that's an issue as we need a tenancy + if ($tenancy === null) { + throw TenancyMissingException::make(); + } + + // If there is a tenancy, but it doesn't have a tenant, that's also + // an issue + if ($tenancy->check() === false) { + throw TenantMissingException::make($tenancy->getName()); + } + + /** @var \Sprout\Contracts\Tenant $tenant */ + $tenant = $tenancy->tenant(); + + // We need to know which store we're overriding to make tenanted + if (! isset($this->config['override'])) { + throw MisconfigurationException::missingConfig('override', self::class, 'service override'); + } + + // We need to get the config for that store + /** @var array $storeConfig */ + $storeConfig = config('cache.stores.' . $this->config['override']); + + if (empty($storeConfig)) { + throw new InvalidArgumentException('Cache store [' . $this->config['override'] . '] is not defined.'); + } + + // Get the prefix for the tenanted store based on the store config, + // the tenancy and its current tenant + $storeConfig['prefix'] = $this->getStorePrefix($storeConfig, $tenancy, $tenant); + + return $this->manager->build($storeConfig); + } + + /** + * Get the prefix for the store + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param array $config + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return string + */ + protected function getStorePrefix(array $config, Tenancy $tenancy, Tenant $tenant): string + { + return (isset($config['prefix']) ? $config['prefix'] . '_' : '') + . $tenancy->getName() + . '_' + . $tenant->getTenantKey(); + } +} diff --git a/src/Overrides/CacheOverride.php b/src/Overrides/CacheOverride.php index 8b4e0f3..2b7e725 100644 --- a/src/Overrides/CacheOverride.php +++ b/src/Overrides/CacheOverride.php @@ -3,50 +3,21 @@ namespace Sprout\Overrides; -use Illuminate\Cache\ApcStore; -use Illuminate\Cache\ApcWrapper; -use Illuminate\Cache\ArrayStore; use Illuminate\Cache\CacheManager; -use Illuminate\Cache\DatabaseStore; -use Illuminate\Cache\FileStore; -use Illuminate\Cache\MemcachedStore; -use Illuminate\Cache\NullStore; -use Illuminate\Cache\RedisStore; +use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Foundation\Application; use Sprout\Contracts\BootableServiceOverride; -use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenantMissingException; +use Sprout\Overrides\Cache\SproutCacheDriverCreator; use Sprout\Sprout; -/** - * Cache Override - * - * This class provides the override/multitenancy extension/features for Laravels - * cache service. - * - * @package Overrides - */ -final class CacheOverride implements BootableServiceOverride, DeferrableServiceOverride +final class CacheOverride extends BaseOverride implements BootableServiceOverride { /** - * Cache stores that can be purged - * * @var list */ - private static array $purgableStores = []; - - /** - * Get the service to watch for before overriding - * - * @return string - */ - public static function service(): string - { - return CacheManager::class; - } + protected array $drivers = []; /** * Boot a service override @@ -58,155 +29,20 @@ public static function service(): string * @param \Sprout\Sprout $sprout * * @return void - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException */ public function boot(Application $app, Sprout $sprout): void { - $cacheManager = app(CacheManager::class); - - $cacheManager->extend('sprout', - /** - * @param array $config - * - * @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 TenantMissingException::make($tenancy->getName()); - } - - $tenant = $tenancy->tenant(); - - if (! isset($config['override'])) { - throw MisconfigurationException::missingConfig('override', self::class, 'override'); - } - - /** @var array $storeConfig */ - $storeConfig = config('caches.store.' . $config['override']); - $prefix = ( - isset($storeConfig['prefix']) - ? $storeConfig['prefix'] . '_' - : '' - ) - . $tenancy->getName() - . '_' - . $tenant->getTenantKey(); - - /** @var array{driver:string,serialize?:bool,path:string,permission?:int|null,lock_path?:string|null} $storeConfig */ - - /** @var string $storeName */ - $storeName = config('store'); - - if (! in_array($storeName, self::$purgableStores, true)) { - self::$purgableStores[] = $storeName; - } - - return $cacheManager->repository(match ($storeConfig['driver']) { - 'apc' => new ApcStore(new ApcWrapper(), $prefix), - 'array' => new ArrayStore($storeConfig['serialize'] ?? false), - 'file' => (new FileStore(app('files'), $storeConfig['path'], $storeConfig['permission'] ?? null)) - ->setLockDirectory($storeConfig['lock_path'] ?? null), - 'null' => new NullStore(), - 'memcached' => $this->createTenantedMemcachedStore($prefix, $storeConfig), - 'redis' => $this->createTenantedRedisStore($prefix, $storeConfig), - 'database' => $this->createTenantedDatabaseStore($prefix, $storeConfig), - default => throw MisconfigurationException::invalidConfig('driver', 'override', CacheOverride::class) - }, array_merge($config, $storeConfig)); - - } - ); - } - - /** - * Create a memcache cache store that's tenanted - * - * @param string $prefix - * @param array $config - * - * @return \Illuminate\Cache\MemcachedStore - */ - private function createTenantedMemcachedStore(string $prefix, array $config): MemcachedStore - { - /** @var array{servers:array,persistent_id?:string|null, options?:array|null,sasl?:array|null} $config */ - - $memcached = app('memcached.connector')->connect( - $config['servers'], - $config['persistent_id'] ?? null, - $config['options'] ?? [], - array_filter($config['sasl'] ?? []) - ); - - return new MemcachedStore($memcached, $prefix); - } - - /** - * Create a Redis cache store that's tenanted - * - * @param string $prefix - * @param array $config - * - * @return \Illuminate\Cache\RedisStore - */ - private function createTenantedRedisStore(string $prefix, array $config): RedisStore - { - /** @var array{connection?:string|null, lock_connection?:string|null} $config */ - $redis = app('redis'); - - $connection = $config['connection'] ?? 'default'; - - return (new RedisStore($redis, $prefix, $connection))->setLockConnection($config['lock_connection'] ?? $connection); - } - - /** - * Create a database cache store that's tenanted - * - * @param string $prefix - * @param array $config - * - * @return \Illuminate\Cache\DatabaseStore - */ - private function createTenantedDatabaseStore(string $prefix, array $config): DatabaseStore - { - /** @var array{table:string,lock_table?:string|null,lock_lottery?:array|null,lock_timeout?:int|null,connection?:string|null, lock_connection?:string|null} $config */ - $connection = app('db')->connection($config['connection'] ?? null); - - $store = new DatabaseStore( - $connection, - $config['table'], - $prefix, - $config['lock_table'] ?? 'cache_locks', - $config['lock_lottery'] ?? [2, 100], - $config['lock_timeout'] ?? 86400, - ); - - if (isset($config['lock_connection'])) { - $store->setLockConnection(app('db')->connection($config['lock_connection'])); - } else { - $store->setLockConnection($connection); - } - - return $store; - } - - /** - * Set up the service override - * - * This method should perform any necessary setup actions for the service - * override. - * It is called when a new tenant is marked as the current tenant. - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @return void - */ - public function setup(Tenancy $tenancy, Tenant $tenant): void - { - // This is intentionally empty, nothing to do here + // We only want to add the driver if the filesystem service is + // resolved at some point + $app->afterResolving('cache', function (CacheManager $manager) use ($sprout) { + $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout): Repository { + // The cache manager adds the store name to the config, so we'll + // _STORE_ that ;) + $this->drivers[] = $config['store']; + + return (new SproutCacheDriverCreator($app, $manager, $config, $sprout))(); + }); + }); } /** @@ -220,13 +56,19 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void * It will be called before {@see self::setup()}, but only if the previous * tenant was not null. * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant * * @return void */ public function cleanup(Tenancy $tenancy, Tenant $tenant): void { - app(CacheManager::class)->forgetDriver(self::$purgableStores); + if (! empty($this->drivers)) { + app(CacheManager::class)->forgetDriver($this->drivers); + } } } diff --git a/src/Overrides/CookieOverride.php b/src/Overrides/CookieOverride.php index 2f35768..e267ce6 100644 --- a/src/Overrides/CookieOverride.php +++ b/src/Overrides/CookieOverride.php @@ -18,18 +18,8 @@ * * @package Overrides */ -final class CookieOverride implements ServiceOverride, DeferrableServiceOverride +final class CookieOverride extends BaseOverride { - /** - * Get the service to watch for before overriding - * - * @return string - */ - public static function service(): string - { - return 'cookie'; - } - /** * Set up the service override * @@ -62,27 +52,4 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void // Set the default values on the cookiejar app(CookieJar::class)->setDefaultPathAndDomain($path, $domain, $secure, $sameSite); } - - /** - * Clean up the service override - * - * This method should perform any necessary setup actions for the service - * override. - * It is called when the current tenant is unset, either to be replaced - * by another tenant, or none. - * - * It will be called before {@see self::setup()}, but only if the previous - * tenant was not null. - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @return void - * - * @codeCoverageIgnore - */ - public function cleanup(Tenancy $tenancy, Tenant $tenant): void - { - // This is intentionally empty - } } diff --git a/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php new file mode 100644 index 0000000..b0ca52b --- /dev/null +++ b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php @@ -0,0 +1,177 @@ + + */ + private array $config; + + /** + * @var \Sprout\Sprout + */ + private Sprout $sprout; + + /** + * Create a new instance + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Illuminate\Filesystem\FilesystemManager $manager + * @param array $config + * @param \Sprout\Sprout $sprout + */ + public function __construct(Application $app, FilesystemManager $manager, array $config, Sprout $sprout) + { + $this->app = $app; + $this->config = $config; + $this->manager = $manager; + $this->sprout = $sprout; + } + + /** + * Create the sprout filesystem driver + * + * @return \Illuminate\Contracts\Filesystem\Filesystem + * + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + */ + public function __invoke(): Filesystem + { + // If we're not within a multitenanted context, we need to error + // out, as this driver shouldn't be hit without one + if (! $this->sprout->withinContext()) { + // TODO: Create a better exception + throw TenancyMissingException::make(); + } + + // Get the current active tenancy + $tenancy = $this->sprout->getCurrentTenancy(); + + // If there isn't one, that's an issue as we need a tenancy + if ($tenancy === null) { + throw TenancyMissingException::make(); + } + + // If there is a tenancy, but it doesn't have a tenant, that's also + // an issue + if ($tenancy->check() === false) { + throw TenantMissingException::make($tenancy->getName()); + } + + $tenant = $tenancy->tenant(); + + // If the tenant isn't configured for resources, this is another issue + if (! ($tenant instanceof TenantHasResources)) { + throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); + } + + // Get a tenant-specific version of the store config + $tenantConfig = $this->getTenantSpecificDiskConfig($tenancy, $tenant); + + // Create a scoped driver for the new path + return $this->manager->createScopedDriver($tenantConfig); + } + + /** + * Make the disk config tenant-specific + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * @param \Sprout\Contracts\TenantHasResources $tenant + * + * @return array + */ + protected function getTenantSpecificDiskConfig(Tenancy $tenancy, TenantHasResources $tenant): array + { + /** @var string $pathPrefix */ + $pathPrefix = $this->config['path'] ?? ('{tenancy}' . DIRECTORY_SEPARATOR . '{tenant}'); + + // Create the empty tenant config + $tenantConfig = []; + + // Build up the path prefix with the tenant resource key + $tenantConfig['prefix'] = $this->createTenantedPrefix($tenancy, $tenant, $pathPrefix); + + // Set the disk config on the newly created tenant config, so that the + // filesystem manager uses this, rather gets it straight from the config + $tenantConfig['disk'] = $this->getTrueDiskConfig(); + + return $tenantConfig; + } + + /** + * Create a storage prefix using the current tenant + * + * @param \Sprout\Contracts\Tenancy<*> $tenancy + * @param \Sprout\Contracts\TenantHasResources $tenant + * @param string $pathPrefix + * + * @return string + */ + protected function createTenantedPrefix(Tenancy $tenancy, TenantHasResources $tenant, string $pathPrefix): string + { + return PlaceholderHelper::replace( + $pathPrefix, + [ + 'tenancy' => $tenancy->getName(), + 'tenant' => $tenant->getTenantResourceKey(), + ] + ); + } + + /** + * Get the true disk config + * + * @return array + */ + protected function getTrueDiskConfig(): array + { + if (is_array($this->config['disk'])) { + $diskConfig = $this->config['disk']; + } else { + /** @var string $diskName */ + $diskName = $this->config['disk'] ?? config('filesystems.default'); + $diskConfig = config('filesystems.disks.' . $diskName); + } + + /** @var array $diskConfig */ + + // This is where we'd do anything like load config overrides for + // the tenant, like say they have their own S3 setup, etc. + + return $diskConfig; + } +} diff --git a/src/Overrides/Filesystem/SproutFilesystemManager.php b/src/Overrides/Filesystem/SproutFilesystemManager.php new file mode 100644 index 0000000..bdc8efe --- /dev/null +++ b/src/Overrides/Filesystem/SproutFilesystemManager.php @@ -0,0 +1,68 @@ +syncOriginal($original); + } + } + + /** + * Sync the original manager in case things have been registered + * + * @param \Illuminate\Filesystem\FilesystemManager $original + * + * @return void + */ + private function syncOriginal(FilesystemManager $original): void + { + $this->disks = array_merge($original->disks, $this->disks); + $this->customCreators = array_merge($original->customCreators, $this->customCreators); + } + + /** + * Resolve the given disk. + * + * @param string $name + * @param array|null $config + * + * @return \Illuminate\Contracts\Filesystem\Filesystem + * + * @throws \InvalidArgumentException + */ + protected function resolve($name, $config = null): Filesystem + { + $config ??= $this->getConfig($name); + + if (empty($config['driver'])) { + throw new InvalidArgumentException("Disk [{$name}] does not have a configured driver."); + } + + $config['name'] = $name; + + $driver = $config['driver']; + + if (isset($this->customCreators[$driver])) { + return $this->callCustomCreator($config); + } + + $driverMethod = 'create' . ucfirst($driver) . 'Driver'; + + if (! method_exists($this, $driverMethod)) { + throw new InvalidArgumentException("Driver [{$driver}] is not supported."); + } + + return $this->{$driverMethod}($config, $name); + } +} diff --git a/src/Overrides/FilesystemOverride.php b/src/Overrides/FilesystemOverride.php new file mode 100644 index 0000000..506dc3c --- /dev/null +++ b/src/Overrides/FilesystemOverride.php @@ -0,0 +1,123 @@ + + */ + protected array $drivers = []; + + /** + * Should the manager be overridden? + * + * @return bool + */ + protected function shouldOverrideManager(): bool + { + return $this->config['manager'] ?? true; // @phpstan-ignore-line + } + + /** + * Boot a service override + * + * This method should perform any initial steps required for the service + * override that take place during the booting of the framework. + * + * @param \Illuminate\Contracts\Foundation\Application $app + * @param \Sprout\Sprout $sprout + * + * @return void + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function boot(Application $app, Sprout $sprout): void + { + // If we're overriding the filesystem manager + if ($this->shouldOverrideManager()) { + $original = null; + + // If the filesystem has already been resolved + if ($app->resolved('filesystem')) { + // We'll grab the manager + $original = $app->make('filesystem'); + // and then tell the container to forget it + $app->forgetInstance('filesystem'); + } + + // Bind a replacement filesystem manager to enable Sprout features + $app->singleton('filesystem', fn ($app) => new SproutFilesystemManager($app, $original)); + } + + // We only want to add the driver if the filesystem service is + // resolved at some point + $app->afterResolving('filesystem', function (FilesystemManager $manager) use ($sprout) { + $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout): Filesystem { + // If the config contains the disk name + if (isset($config['name'])) { + // Track it + $this->drivers[] = $config['name']; + } + + return (new SproutFilesystemDriverCreator($app, $manager, $config, $sprout))(); + }); + }); + } + + /** + * Clean up the service override + * + * This method should perform any necessary setup actions for the service + * override. + * It is called when the current tenant is unset, either to be replaced + * by another tenant, or none. + * + * It will be called before {@see self::setup()}, but only if the previous + * tenant was not null. + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant + * + * @return void + */ + public function cleanup(Tenancy $tenancy, Tenant $tenant): void + { + /** @var array> $diskConfig */ + $diskConfig = config('filesystems.disks', []); + + /** @var \Illuminate\Filesystem\FilesystemManager $filesystemManager */ + $filesystemManager = app(FilesystemManager::class); + + // If it's our custom filesystem manager, we know that we have the names + // of the created disks + if ($filesystemManager instanceof SproutFilesystemManager) { + if (! empty($this->drivers)) { + $filesystemManager->forgetDisk($this->drivers); + } + } else { + // But if we don't, we have to cycle through the config and pick out + // any that have the 'sprout' driver + foreach ($diskConfig as $disk => $config) { + if (($config['driver'] ?? null) === 'sprout') { + $filesystemManager->forgetDisk($disk); + } + } + } + } +} diff --git a/src/Overrides/JobOverride.php b/src/Overrides/JobOverride.php index be05142..45def56 100644 --- a/src/Overrides/JobOverride.php +++ b/src/Overrides/JobOverride.php @@ -7,8 +7,6 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Queue\Events\JobProcessing; use Sprout\Contracts\BootableServiceOverride; -use Sprout\Contracts\Tenancy; -use Sprout\Contracts\Tenant; use Sprout\Listeners\SetCurrentTenantForJob; use Sprout\Sprout; @@ -20,7 +18,7 @@ * * @package Overrides */ -final class JobOverride implements BootableServiceOverride +final class JobOverride extends BaseOverride implements BootableServiceOverride { /** * Boot a service override @@ -38,44 +36,8 @@ public function boot(Application $app, Sprout $sprout): void /** @var \Illuminate\Contracts\Events\Dispatcher $events */ $events = app(Dispatcher::class); + // This override simply adds a listener to make sure that tenancies + // and their tenants are accessible to jobs $events->listen(JobProcessing::class, SetCurrentTenantForJob::class); } - - /** - * Set up the service override - * - * This method should perform any necessary setup actions for the service - * override. - * It is called when a new tenant is marked as the current tenant. - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @return void - */ - public function setup(Tenancy $tenancy, Tenant $tenant): void - { - // I am intentionally empty - } - - /** - * Clean up the service override - * - * This method should perform any necessary setup actions for the service - * override. - * It is called when the current tenant is unset, either to be replaced - * by another tenant, or none. - * - * It will be called before {@see self::setup()}, but only if the previous - * tenant was not null. - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @return void - */ - public function cleanup(Tenancy $tenancy, Tenant $tenant): void - { - // I am intentionally empty - } } diff --git a/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php b/src/Overrides/Session/SproutDatabaseSessionHandler.php similarity index 94% rename from src/Overrides/Session/TenantAwareDatabaseSessionHandler.php rename to src/Overrides/Session/SproutDatabaseSessionHandler.php index 84f7b55..e1d4457 100644 --- a/src/Overrides/Session/TenantAwareDatabaseSessionHandler.php +++ b/src/Overrides/Session/SproutDatabaseSessionHandler.php @@ -10,7 +10,7 @@ use function Sprout\sprout; /** - * Tenant Aware Database Session Handler + * Sprout Database Session Handler * * This is a database session driver that wraps the default * {@see \Illuminate\Session\DatabaseSessionHandler} and adds a where clause @@ -18,7 +18,7 @@ * * @package Overrides */ -class TenantAwareDatabaseSessionHandler extends DatabaseSessionHandler +class SproutDatabaseSessionHandler extends DatabaseSessionHandler { /** * Get a fresh query builder instance for the table. diff --git a/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php b/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php new file mode 100644 index 0000000..67c2832 --- /dev/null +++ b/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php @@ -0,0 +1,108 @@ +app = $app; + $this->manager = $manager; + $this->sprout = $sprout; + } + + /** + * Create the tenant-aware session database driver + * + * @return \Illuminate\Session\DatabaseSessionHandler + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + */ + public function __invoke(): DatabaseSessionHandler + { + $table = config('session.table'); + $lifetime = config('session.lifetime'); + $connection = config('session.connection'); + + /** + * @var string|null $connection + * @var string $table + * @var int $lifetime + */ + + // This driver is unlike many of the others, where if we aren't in + // multitenanted context, we don't do anything + if ($this->sprout->withinContext()) { + // Get the current active tenancy + $tenancy = $this->sprout->getCurrentTenancy(); + + // If there isn't one, that's an issue as we need a tenancy + if ($tenancy === null) { + throw TenancyMissingException::make(); + } + + // If there is a tenancy, but it doesn't have a tenant, that's also + // an issue + if ($tenancy->check() === false) { + throw TenantMissingException::make($tenancy->getName()); + } + + $tenant = $tenancy->tenant(); + + // If the tenant isn't configured for resources, this is another issue + if (! ($tenant instanceof TenantHasResources)) { + throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); + } + + return new SproutDatabaseSessionHandler( + $this->app->make('db')->connection($connection), + $table, + $lifetime, + $this->app + ); + } + + return new OriginalDatabaseSessionHandler( + $this->app->make('db')->connection($connection), + $table, + $lifetime, + $this->app + ); + } +} diff --git a/src/Overrides/Session/SproutSessionFileDriverCreator.php b/src/Overrides/Session/SproutSessionFileDriverCreator.php new file mode 100644 index 0000000..f5dfe37 --- /dev/null +++ b/src/Overrides/Session/SproutSessionFileDriverCreator.php @@ -0,0 +1,100 @@ +app = $app; + $this->manager = $manager; + $this->sprout = $sprout; + } + + /** + * Create the tenant-aware session file driver + * + * @return \Illuminate\Session\FileSessionHandler + * + * @throws \Sprout\Exceptions\MisconfigurationException + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function __invoke(): FileSessionHandler + { + /** @var string $originalPath */ + $originalPath = config('session.files'); + $path = rtrim($originalPath, '/') . DIRECTORY_SEPARATOR; + + // This driver is unlike many of the others, where if we aren't in + // multitenanted context, we don't do anything + if ($this->sprout->withinContext()) { + // Get the current active tenancy + $tenancy = $this->sprout->getCurrentTenancy(); + + // If there isn't one, that's an issue as we need a tenancy + if ($tenancy === null) { + throw TenancyMissingException::make(); + } + + // If there is a tenancy, but it doesn't have a tenant, that's also + // an issue + if ($tenancy->check() === false) { + throw TenantMissingException::make($tenancy->getName()); + } + + $tenant = $tenancy->tenant(); + + // If the tenant isn't configured for resources, this is another issue + if (! ($tenant instanceof TenantHasResources)) { + throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); + } + + $path = rtrim($path, DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR + . $tenant->getTenantResourceKey(); + } + + /** @var int $lifetime */ + $lifetime = config('session.lifetime'); + + return new FileSessionHandler( + $this->app->make('files'), + $path, + $lifetime, + ); + } +} diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index a221d77..d66cec1 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -3,25 +3,17 @@ namespace Sprout\Overrides; -use Closure; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Session\DatabaseSessionHandler as OriginalDatabaseSessionHandler; -use Illuminate\Session\FileSessionHandler; use Illuminate\Session\SessionManager; use Illuminate\Support\Arr; use Sprout\Contracts\BootableServiceOverride; -use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Contracts\TenantHasResources; -use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenancyMissingException; -use Sprout\Exceptions\TenantMissingException; -use Sprout\Overrides\Session\TenantAwareDatabaseSessionHandler; +use Sprout\Overrides\Session\SproutSessionDatabaseDriverCreator; +use Sprout\Overrides\Session\SproutSessionFileDriverCreator; use Sprout\Sprout; use Sprout\Support\Settings; use function Sprout\settings; -use function Sprout\sprout; /** * Session Override @@ -31,18 +23,8 @@ * * @package Overrides */ -final class SessionOverride implements BootableServiceOverride, DeferrableServiceOverride +final class SessionOverride extends BaseOverride implements BootableServiceOverride { - /** - * Get the service to watch for before overriding - * - * @return string - */ - public static function service(): string - { - return 'session'; - } - /** * Boot a service override * @@ -56,18 +38,21 @@ public static function service(): string */ public function boot(Application $app, Sprout $sprout): void { - $sessionManager = app(SessionManager::class); + app()->afterResolving('session', function (SessionManager $manager) use ($app, $sprout) { + $creator = new SproutSessionFileDriverCreator($app, $manager, $sprout); - // The native driver proxies the call to the createFileDriver method, - // so we have to override that too. - $fileCreator = self::createFilesDriver(); + $manager->extend('file', $creator(...)); + $manager->extend('native', $creator(...)); - $sessionManager->extend('file', $fileCreator); - $sessionManager->extend('native', $fileCreator); + /** @var bool $overrideDatabase */ + $overrideDatabase = $this->config['database'] ?? true; - if (settings()->shouldNotOverrideTheDatabase(false) === false) { - $sessionManager->extend('database', self::createDatabaseDriver()); - } + if (settings()->shouldNotOverrideTheDatabase($overrideDatabase) === false) { + $manager->extend('database', fn () => (new SproutSessionDatabaseDriverCreator( + $app, $manager, $sprout + ))()); + } + }); } /** @@ -77,8 +62,12 @@ public function boot(Application $app, Sprout $sprout): void * override. * It is called when a new tenant is marked as the current tenant. * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant * * @return void */ @@ -130,8 +119,12 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void * It will be called before {@see self::setup()}, but only if the previous * tenant was not null. * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy $tenancy + * @param \Sprout\Contracts\Tenant $tenant + * + * @phpstan-param TenantClass $tenant * * @return void */ @@ -151,80 +144,4 @@ private function getCookieName(Tenancy $tenancy, Tenant $tenant): string { return $tenancy->getName() . '_' . $tenant->getTenantIdentifier() . '_session'; } - - /** - * Get a creator for a tenant scoped file session handler - * - * @return \Closure - */ - private static function createFilesDriver(): Closure - { - return static function (): FileSessionHandler { - /** @var string $originalPath */ - $originalPath = config('session.files'); - $path = rtrim($originalPath, '/') . DIRECTORY_SEPARATOR; - - if (sprout()->withinContext()) { - $tenancy = sprout()->getCurrentTenancy(); - - if ($tenancy === null) { - throw TenancyMissingException::make(); - } - - // If there's no tenant, error out - if (! $tenancy->check()) { - throw TenantMissingException::make($tenancy->getName()); - } - - $tenant = $tenancy->tenant(); - - // If the tenant isn't configured for resources, also error out - if (! ($tenant instanceof TenantHasResources)) { - throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); - } - - $path .= $tenant->getTenantResourceKey(); - } - - /** @var int $lifetime */ - $lifetime = config('session.lifetime'); - - return new FileSessionHandler( - app()->make('files'), - $path, - $lifetime, - ); - }; - } - - private static function createDatabaseDriver(): Closure - { - return static function (): OriginalDatabaseSessionHandler { - $table = config('session.table'); - $lifetime = config('session.lifetime'); - $connection = config('session.connection'); - - /** - * @var string|null $connection - * @var string $table - * @var int $lifetime - */ - - if (sprout()->withinContext()) { - return new TenantAwareDatabaseSessionHandler( - app()->make('db')->connection($connection), - $table, - $lifetime, - app() - ); - } - - return new OriginalDatabaseSessionHandler( - app()->make('db')->connection($connection), - $table, - $lifetime, - app() - ); - }; - } } diff --git a/src/Overrides/StorageOverride.php b/src/Overrides/StorageOverride.php deleted file mode 100644 index 8092c3e..0000000 --- a/src/Overrides/StorageOverride.php +++ /dev/null @@ -1,213 +0,0 @@ -make(FilesystemManager::class); - $filesystemManager->extend('sprout', self::creator($sprout, $filesystemManager)); - } - - /** - * Set up the service override - * - * This method should perform any necessary setup actions for the service - * override. - * It is called when a new tenant is marked as the current tenant. - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @return void - */ - public function setup(Tenancy $tenancy, Tenant $tenant): void - { - // This is intentionally empty, nothing to do here - } - - /** - * Clean up the service override - * - * This method should perform any necessary setup actions for the service - * override. - * It is called when the current tenant is unset, either to be replaced - * by another tenant, or none. - * - * It will be called before {@see self::setup()}, but only if the previous - * tenant was not null. - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @return void - */ - public function cleanup(Tenancy $tenancy, Tenant $tenant): void - { - /** @var array> $diskConfig */ - $diskConfig = config('filesystems.disks', []); - - /** @var \Illuminate\Filesystem\FilesystemManager $filesystemManager */ - $filesystemManager = app(FilesystemManager::class); - - // If any of the disks have the 'sprout' driver, we need to purge them - // if they exist, so we don't end up leaking tenant information - foreach ($diskConfig as $disk => $config) { - if (($config['driver'] ?? null) === 'sprout') { - $filesystemManager->forgetDisk($disk); - } - } - } - - /** - * Create a driver creator - * - * @param \Sprout\Sprout $sprout - * @param \Illuminate\Filesystem\FilesystemManager $manager - * - * @return \Closure - */ - private static function creator(Sprout $sprout, FilesystemManager $manager): Closure - { - return static function (Application $app, array $config) use ($sprout, $manager): Filesystem { - $tenancy = $sprout->tenancies()->get($config['tenancy'] ?? null); - - // If there's no tenant, error out - if (! $tenancy->check()) { - throw TenantMissingException::make($tenancy->getName()); - } - - $tenant = $tenancy->tenant(); - - // If the tenant isn't configured for resources, also error out - if (! ($tenant instanceof TenantHasResources)) { - throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); - } - - $tenantConfig = self::getTenantStorageConfig($manager, $tenancy, $tenant, $config); - - // Create a scoped driver for the new path - return $manager->createScopedDriver($tenantConfig); - }; - } - - /** - * Tenantise the storage config - * - * @param \Illuminate\Filesystem\FilesystemManager $manager - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\TenantHasResources $tenant - * @param array $config - * - * @return array - */ - private static function getTenantStorageConfig(FilesystemManager $manager, Tenancy $tenancy, TenantHasResources $tenant, array $config): array - { - /** @var string $pathPrefix */ - $pathPrefix = $config['path'] ?? ('{tenancy}' . DIRECTORY_SEPARATOR . '{tenant}'); - - // Create the empty tenant config - $tenantConfig = []; - - // Build up the path prefix with the tenant resource key - $tenantConfig['prefix'] = self::createTenantedPrefix($tenancy, $tenant, $pathPrefix); - - // Set the disk config on the newly created tenant config, so that the - // filesystem manager uses this, rather gets it straight from the config - $tenantConfig['disk'] = self::getDiskConfig($config); - - return $tenantConfig; - } - - /** - * Create a storage prefix using the current tenant - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param \Sprout\Contracts\TenantHasResources $tenant - * @param string $pathPrefix - * - * @return string - */ - private static function createTenantedPrefix(Tenancy $tenancy, TenantHasResources $tenant, string $pathPrefix): string - { - return PlaceholderHelper::replace( - $pathPrefix, - [ - 'tenancy' => $tenancy->getName(), - 'tenant' => $tenant->getTenantResourceKey(), - ] - ); - } - - /** - * Get the config of the disk being tenantised - * - * @param array $config - * - * @return array - */ - private static function getDiskConfig(array $config): array - { - if (is_array($config['disk'])) { - $diskConfig = $config['disk']; - } else { - /** @var string $diskName */ - $diskName = $config['disk'] ?? config('filesystems.default'); - $diskConfig = config('filesystems.disks.' . $diskName); - } - - /** @var array $diskConfig */ - - // This is where we'd do anything like load config overrides for - // the tenant, like say they have their own S3 setup, etc. - - return $diskConfig; - } -} From 0e253beab6432dc5a5d1531ba9f7b77eda6f9e35 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:18:03 +0000 Subject: [PATCH 13/48] chore: Remove HandlesServiceOverrides and DeferrableServiceOverride --- src/Concerns/HandlesServiceOverrides.php | 476 -------------------- src/Contracts/DeferrableServiceOverride.php | 20 - src/Sprout.php | 3 - 3 files changed, 499 deletions(-) delete mode 100644 src/Concerns/HandlesServiceOverrides.php delete mode 100644 src/Contracts/DeferrableServiceOverride.php diff --git a/src/Concerns/HandlesServiceOverrides.php b/src/Concerns/HandlesServiceOverrides.php deleted file mode 100644 index 72f2bdb..0000000 --- a/src/Concerns/HandlesServiceOverrides.php +++ /dev/null @@ -1,476 +0,0 @@ -> - */ - private array $registeredOverrides = []; - - /** - * @var array, \Sprout\Contracts\ServiceOverride> - */ - private array $overrides = []; - - /** - * @var array, string|class-string> - */ - private array $deferredOverrides = []; - - /** - * @var array, BootableServiceOverride> - */ - private array $bootableOverrides = []; - - /** - * @var array, bool> - */ - private array $bootedOverrides = []; - - /** - * @var array, bool>> - */ - private array $setupOverrides = []; - - /** - * @var array, string> - */ - private array $serviceOverrideMapping = []; - - /** - * @var bool - */ - private bool $hasBooted = false; - - /** - * Register a service override - * - * @param string $service - * @param class-string<\Sprout\Contracts\ServiceOverride> $class - * - * @return static - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - * @throws \Sprout\Exceptions\ServiceOverrideException - */ - public function registerOverride(string $service, string $class): static - { - if (! is_subclass_of($class, ServiceOverride::class)) { - throw ServiceOverrideException::invalidClass($class); - } - - if ($this->isServiceBeingOverridden($service)) { - $originalClass = $this->registeredOverrides[$service]; - - if ($this->hasBootedOverride($originalClass) || $this->hasOverrideBeenSetup($originalClass)) { - throw ServiceOverrideException::alreadyProcessed($service, $this->registeredOverrides[$service]); - } - } - - // Flag the service override as being registered - $this->registeredOverrides[$service] = $class; - - // Map the override class to the service it's overriding - $this->serviceOverrideMapping[$class] = $service; - - ServiceOverrideRegistered::dispatch($service, $class); - - if (is_subclass_of($class, DeferrableServiceOverride::class)) { - $this->registerDeferrableOverride($class); - } else { - $this->processOverride($class); - } - - return $this; - } - - /** - * Get the service an override is overriding - * - * @param string $class - * - * @return string|null - */ - public function getServiceForOverride(string $class): ?string - { - return $this->serviceOverrideMapping[$class] ?? null; - } - - /** - * Process the registration of a service override - * - * This method is an abstraction of the service override registration - * processing, which exists entirely to make deferrable overrides easier. - * - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass - * - * @return static - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - protected function processOverride(string $overrideClass): static - { - /** @phpstan-ignore-next-line */ - ServiceOverrideProcessing::dispatch($this->getServiceForOverride($overrideClass), $overrideClass); - - // Create a new instance of the override - $override = $this->app->make($overrideClass); - - // Register the instance - $this->overrides[$overrideClass] = $override; - - // The override is bootable - if ($override instanceof BootableServiceOverride) { - /** @var class-string<\Sprout\Contracts\BootableServiceOverride> $overrideClass */ - // So register it as one - $this->bootableOverrides[$overrideClass] = $override; - $this->bootedOverrides[$overrideClass] = false; - - // If the boot phase has already happened, we'll boot it now - if ($this->haveOverridesBooted()) { - $this->bootOverride($overrideClass); - } - } - - /** @phpstan-ignore-next-line */ - ServiceOverrideProcessed::dispatch($this->getServiceForOverride($overrideClass), $override); - - return $this; - } - - /** - * Register a deferrable service override - * - * @param class-string<\Sprout\Contracts\DeferrableServiceOverride> $overrideClass - * - * @return static - * - * @throws \Illuminate\Contracts\Container\BindingResolutionException - */ - protected function registerDeferrableOverride(string $overrideClass): static - { - // Register the deferred override and its service - $this->deferredOverrides[$overrideClass] = $overrideClass::service(); - - if ($this->app->resolved($overrideClass::service())) { - $this->processOverride($overrideClass); - } - - $this->app->afterResolving($overrideClass::service(), function () use ($overrideClass) { - $this->processOverride($overrideClass); - - // Get the current tenancy - $tenancy = $this->getCurrentTenancy(); - - // If there's a current tenancy WITH a tenant, we can set up the - // override - if ($tenancy !== null && $tenancy->check()) { - $this->setupOverride($overrideClass, $tenancy, $tenancy->tenant()); - } - }); - - return $this; - } - - /** - * Check if a service override is bootable - * - * @param class-string<\Sprout\Contracts\ServiceOverride> $class - * - * @return bool - */ - public function isBootableOverride(string $class): bool - { - return isset($this->bootableOverrides[$class]); - } - - /** - * Check if a service override is deferred - * - * @param class-string<\Sprout\Contracts\ServiceOverride> $class - * - * @return bool - */ - public function isDeferrableOverride(string $class): bool - { - return isset($this->deferredOverrides[$class]); - } - - /** - * Check if a service override has been booted - * - * 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> $class - * - * @return bool - */ - public function hasBootedOverride(string $class): bool - { - return $this->bootedOverrides[$class] ?? false; - } - - /** - * Check if the boot phase has already happened - * - * @return bool - */ - public function haveOverridesBooted(): bool - { - return $this->hasBooted; - } - - /** - * Boot all bootable overrides - * - * @return void - */ - public function bootOverrides(): void - { - // If the boot phase for the override has already happened, skip it - if ($this->haveOverridesBooted()) { - return; - } - - // @codeCoverageIgnoreStart - foreach ($this->bootableOverrides as $overrideClass => $override) { - // It's possible this is being called a second time, so we don't - // want to do it again - if (! $this->hasBootedOverride($overrideClass)) { - // Boot the override - $this->bootOverride($overrideClass); - } - } - // @codeCoverageIgnoreEnd - - // Mark the override boot phase as having completed - $this->hasBooted = true; - } - - /** - * Boot a service override - * - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass - * - * @return void - */ - protected function bootOverride(string $overrideClass): void - { - /** @var \Sprout\Contracts\BootableServiceOverride $override */ - $override = $this->overrides[$overrideClass]; - - $override->boot($this->app, $this); - $this->bootedOverrides[$overrideClass] = true; - - /** @phpstan-ignore-next-line */ - ServiceOverrideBooted::dispatch($this->getServiceForOverride($overrideClass), $override); - } - - /** - * Check if a service override has been set up - * - * @param \Sprout\Contracts\Tenancy<*> $tenancy - * @param class-string<\Sprout\Contracts\ServiceOverride> $class - * - * @return bool - */ - public function hasSetupOverride(Tenancy $tenancy, string $class): bool - { - return $this->setupOverrides[$tenancy->getName()][$class] ?? false; - } - - /** - * Check if a service override has been set up for any tenancy - * - * @param class-string<\Sprout\Contracts\ServiceOverride> $class - * - * @return bool - */ - public function hasOverrideBeenSetup(string $class): bool - { - foreach ($this->setupOverrides as $overrides) { - if (isset($overrides[$class])) { - return true; - } - } - - return false; - } - - /** - * Set-up all available service overrides - * - * @template TenantClass of \Sprout\Contracts\Tenant - * - * @param \Sprout\Contracts\Tenancy $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @phpstan-param TenantClass $tenant - * - * @return void - */ - public function setupOverrides(Tenancy $tenancy, Tenant $tenant): void - { - foreach ($this->overrides as $overrideClass => $override) { - $this->setupOverride($overrideClass, $tenancy, $tenant); - } - } - - /** - * Set up a service override - * - * @template TenantClass of \Sprout\Contracts\Tenant - * - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass - * @param \Sprout\Contracts\Tenancy $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @phpstan-param TenantClass $tenant - * - * @return void - */ - protected function setupOverride(string $overrideClass, Tenancy $tenancy, Tenant $tenant): void - { - if (! $this->hasSetupOverride($tenancy, $overrideClass)) { - $this->overrides[$overrideClass]->setup($tenancy, $tenant); - $this->setupOverrides[$tenancy->getName()][$overrideClass] = true; - } - } - - /** - * Clean-up all service overrides - * - * @template TenantClass of \Sprout\Contracts\Tenant - * - * @param \Sprout\Contracts\Tenancy $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @phpstan-param TenantClass $tenant - * - * @return void - */ - public function cleanupOverrides(Tenancy $tenancy, Tenant $tenant): void - { - $overrides = $this->setupOverrides[$tenancy->getName()] ?? []; - - foreach ($overrides as $overrideClass => $status) { - if ($status === true) { - $this->cleanupOverride($overrideClass, $tenancy, $tenant); - } - } - } - - /** - * Clean-up a service override - * - * @template TenantClass of \Sprout\Contracts\Tenant - * - * @param class-string<\Sprout\Contracts\ServiceOverride> $overrideClass - * @param \Sprout\Contracts\Tenancy $tenancy - * @param \Sprout\Contracts\Tenant $tenant - * - * @phpstan-param TenantClass $tenant - * - * @return void - */ - protected function cleanupOverride(string $overrideClass, Tenancy $tenancy, Tenant $tenant): void - { - $this->overrides[$overrideClass]->cleanup($tenancy, $tenant); - unset($this->setupOverrides[$tenancy->getName()][$overrideClass]); - } - - /** - * Get all service overrides - * - * @return array, \Sprout\Contracts\ServiceOverride> - */ - public function getOverrides(): array - { - return $this->overrides; - } - - /** - * Get all registered service overrides - * - * @return array> - */ - public function getRegisteredOverrides(): array - { - return $this->registeredOverrides; - } - - /** - * Check if a service override is present - * - * @param string $class - * - * @return bool - */ - public function hasOverride(string $class): bool - { - return isset($this->overrides[$class]); - } - - /** - * Check if a service override has been registered - * - * @param string $class - * - * @return bool - */ - public function hasRegisteredOverride(string $class): bool - { - return in_array($class, $this->registeredOverrides, true); - } - - /** - * Check if a particular service is being overridden - * - * @param string $service - * - * @return bool - */ - public function isServiceBeingOverridden(string $service): bool - { - return isset($this->registeredOverrides[$service]); - } - - /** - * Get all service overrides for a tenancy - * - * @param \Sprout\Contracts\Tenancy<*>|null $tenancy - * - * @return array<\Sprout\Contracts\ServiceOverride> - */ - public function getCurrentOverrides(?Tenancy $tenancy = null): array - { - $tenancy ??= $this->getCurrentTenancy(); - - if ($tenancy !== null) { - return array_filter( - $this->overrides, - function (string $overrideClass) use ($tenancy) { - return $this->hasSetupOverride($tenancy, $overrideClass); - }, - ARRAY_FILTER_USE_KEY - ); - } - - return []; - } -} diff --git a/src/Contracts/DeferrableServiceOverride.php b/src/Contracts/DeferrableServiceOverride.php deleted file mode 100644 index 03dabff..0000000 --- a/src/Contracts/DeferrableServiceOverride.php +++ /dev/null @@ -1,20 +0,0 @@ - Date: Sat, 11 Jan 2025 12:20:55 +0000 Subject: [PATCH 14/48] chore: Tidy up service override manager --- src/Managers/ServiceOverrideManager.php | 87 ++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index 6b1440b..c599d06 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -12,9 +12,19 @@ use Sprout\Events\ServiceOverrideRegistered; use Sprout\Exceptions\MisconfigurationException; use Sprout\Exceptions\ServiceOverrideException; +use Sprout\Exceptions\TenancyMissingException; use Sprout\Sprout; use Sprout\TenancyOptions; +/** + * Service Override Manager + * + * This manager is responsible for managing service overrides, from calling + * register and booting them, to integrating them into tenancy lifecycle + * events. + * + * @package Overrides + */ final class ServiceOverrideManager { /** @@ -39,6 +49,9 @@ final class ServiceOverrideManager */ protected array $bootableOverrides = []; + /** + * @var bool + */ protected bool $overridesBooted = false; /** @@ -57,6 +70,8 @@ public function __construct(Application $app) } /** + * Get the config for a service + * * @param string $service * * @return array|null @@ -71,16 +86,86 @@ protected function getServiceConfig(string $service): ?array return $config; } + /** + * Check if a service has an override + * + * @param string $service + * + * @return bool + */ public function hasOverride(string $service): bool { return isset($this->overrides[$service]); } + /** + * Check if a services' override has been booted + * + * @param string $service + * + * @return bool + */ + public function hasOverrideBooted(string $service): bool + { + return $this->haveOverridesBooted() && $this->isOverrideBootable($service); + } + + /** + * Check if a service override has been set up for a tenancy + * + * @param string $service + * @param \Sprout\Contracts\Tenancy<*>|null $tenancy + * + * @return bool + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Sprout\Exceptions\TenancyMissingException + */ + public function hasOverrideBeenSetUp(string $service, ?Tenancy $tenancy = null): bool + { + $tenancy ??= $this->app->make(Sprout::class)->getCurrentTenancy(); + + if ($tenancy === null) { + throw TenancyMissingException::make(); + } + + return in_array($service, $this->getSetupOverrides($tenancy), true); + } + + /** + * Check if a services' override is bootable + * + * @param string $service + * + * @return bool + */ + public function isOverrideBootable(string $service): bool + { + return in_array($service, $this->bootableOverrides, true); + } + + /** + * Check if the service override boot stage has passed + * + * @return bool + */ public function haveOverridesBooted(): bool { return $this->overridesBooted; } + /** + * Get the driver class for a service + * + * @param string $service + * + * @return class-string<\Sprout\Contracts\ServiceOverride>|null + */ + public function getOverrideClass(string $service): ?string + { + return $this->overrideClasses[$service] ?? null; + } + /** * Get all services whose overrides have been set up for a tenancy * @@ -222,7 +307,7 @@ public function cleanupOverrides(Tenancy $tenancy, Tenant $tenant): void */ protected function register(string $service): self { - // If the override already exists, we'll error out, because it should + // If the override already exists, we'll error out, because it should, // we'd just load the same config again if ($this->hasOverride($service)) { return $this; From f8895266ccb2874872b1027b69e6a8c55883f2de Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:21:52 +0000 Subject: [PATCH 15/48] chore: Remove old unused tests --- .gitignore | 1 + phpunit.xml | 3 - .../Eloquent/BelongsToManyTenantsTest.php | 363 --------------- .../Database/Eloquent/BelongsToTenantTest.php | 417 ------------------ .../Database/Eloquent/TenantChildTest.php | 169 ------- .../Http/Resolvers/CookieResolverTest.php | 75 ---- .../Http/Resolvers/HeaderResolverTest.php | 79 ---- .../Http/Resolvers/PathResolverTest.php | 150 ------- .../Http/Resolvers/SessionResolverTest.php | 123 ------ .../Http/Resolvers/SubdomainResolverTest.php | 173 -------- .../Listeners/SetCurrentTenantForJobTest.php | 91 ---- .../Overrides/CookieOverrideTest.php | 165 ------- .../Overrides/StorageOverrideTest.php | 184 -------- .../Providers/DatabaseTenantProviderTest.php | 106 ----- .../Providers/EloquentTenantProviderTest.php | 94 ---- tests/_Original/ServiceProviderTest.php | 135 ------ tests/_Original/SproutTest.php | 67 --- tests/_Original/TenancyOptionsTest.php | 67 --- 18 files changed, 1 insertion(+), 2461 deletions(-) delete mode 100644 tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php delete mode 100644 tests/_Original/Database/Eloquent/BelongsToTenantTest.php delete mode 100644 tests/_Original/Database/Eloquent/TenantChildTest.php delete mode 100644 tests/_Original/Http/Resolvers/CookieResolverTest.php delete mode 100644 tests/_Original/Http/Resolvers/HeaderResolverTest.php delete mode 100644 tests/_Original/Http/Resolvers/PathResolverTest.php delete mode 100644 tests/_Original/Http/Resolvers/SessionResolverTest.php delete mode 100644 tests/_Original/Http/Resolvers/SubdomainResolverTest.php delete mode 100644 tests/_Original/Listeners/SetCurrentTenantForJobTest.php delete mode 100644 tests/_Original/Overrides/CookieOverrideTest.php delete mode 100644 tests/_Original/Overrides/StorageOverrideTest.php delete mode 100644 tests/_Original/Providers/DatabaseTenantProviderTest.php delete mode 100644 tests/_Original/Providers/EloquentTenantProviderTest.php delete mode 100644 tests/_Original/ServiceProviderTest.php delete mode 100644 tests/_Original/SproutTest.php delete mode 100644 tests/_Original/TenancyOptionsTest.php diff --git a/.gitignore b/.gitignore index 779d009..cb238fe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock /wiki/ /build/ .phpunit.result.cache +/tests/_Original diff --git a/phpunit.xml b/phpunit.xml index 7e3a02d..ec40e30 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -15,9 +15,6 @@ testdox="true" > - - tests/_Original - ./tests/Feature diff --git a/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php b/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php deleted file mode 100644 index cac1a08..0000000 --- a/tests/_Original/Database/Eloquent/BelongsToManyTenantsTest.php +++ /dev/null @@ -1,363 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - #[Test] - public function addsGlobalScope(): void - { - $model = new TenantChildren(); - - $this->assertContains(BelongsToManyTenants::class, class_uses_recursive($model)); - $this->assertArrayHasKey(BelongsToManyTenantsScope::class, $model->getGlobalScopes()); - } - - #[Test] - public function addsObservers(): void - { - $model = new TenantChildren(); - $dispatcher = TenantChildren::getEventDispatcher(); - - $this->assertContains(BelongsToManyTenants::class, class_uses_recursive($model)); - - if ($dispatcher instanceof Dispatcher) { - $this->assertTrue($dispatcher->hasListeners('eloquent.retrieved: ' . TenantChildren::class)); - $this->assertTrue($dispatcher->hasListeners('eloquent.created: ' . TenantChildren::class)); - - $listeners = $dispatcher->getRawListeners(); - - $this->assertContains(BelongsToManyTenantsObserver::class . '@retrieved', $listeners['eloquent.retrieved: ' . TenantChildren::class]); - $this->assertContains(BelongsToManyTenantsObserver::class . '@created', $listeners['eloquent.created: ' . TenantChildren::class]); - } else { - $this->markTestIncomplete('Cannot complete the test because a custom dispatcher is in place'); - } - } - - #[Test] - public function automaticallyAssociatesWithTenantWhenCreating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $child = TenantChildren::factory()->create(); - - $this->assertTrue($child->exists); - $this->assertTrue($child->relationLoaded('tenants')); - $this->assertNotNull($child->tenants->first(fn (Model $model) => $model->is($tenant))); - } - - #[Test] - public function doesNotAutomaticallyAssociateWithTenantWhenCreatingWhenOutsideMultitenantedContext(): void - { - $child = TenantChildren::factory()->create(); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenants')); - $this->assertTrue($child->tenants->isEmpty()); - } - - #[Test] - public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreating(): void - { - sprout()->setCurrentTenancy(app(TenancyManager::class)->get()); - - $this->expectException(TenantMissingException::class); - $this->expectExceptionMessage( - 'There is no current tenant for tenancy [tenants]' - ); - - TenantChildren::factory()->create(); - } - - #[Test] - public function doesNotThrowAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreatingWhenOutsideMultitenantedContext(): void - { - $model = TenantChildren::factory()->create(); - - $this->assertNotNull($model); - $this->assertTrue($model->exists); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithInterfaceWhenCreating(): void - { - $child = TenantChildrenOptional::factory()->create(); - - $this->assertInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertEmpty($child->tenants); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithOverrideWhenCreating(): void - { - TenantChildren::ignoreTenantRestrictions(); - - $child = TenantChildren::factory()->create(); - - TenantChildren::resetTenantRestrictions(); - - $this->assertNotInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertEmpty($child->tenants); - } - - #[Test] - public function doesNothingIfTheTenantIsAlreadySetOnTheModelWhenCreating(): void - { - $this->markTestSkipped('This test cannot be performed with a belongs to many relation'); - } - - #[Test] - public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDifferentWhenCreating(): void - { - $this->markTestSkipped('This test cannot be performed with a belongs to many relation'); - } - - #[Test] - public function doesNotThrowAnExceptionForTenantMismatchIfNotSetToWhenCreating(): void - { - $this->markTestSkipped('This test cannot be performed with a belongs to many relation'); - } - - #[Test] - public function automaticallyPopulateTheTenantRelationWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $child = TenantChildren::query()->find(TenantChildren::factory()->create()->getKey()); - - $this->assertTrue($child->exists); - $this->assertTrue($child->relationLoaded('tenants')); - $this->assertNotNull($child->getRelation('tenants')->first(fn (Model $model) => $model->is($tenant))); - } - - #[Test] - public function doesNotAutomaticallyPopulateTheTenantRelationWhenHydratingWhenOutsideMultitenantedContext(): void - { - - $child = TenantChildren::query()->find(TenantChildren::factory()->create()->getKey()); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenants')); - } - - #[Test] - public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $child = TenantChildren::factory()->create(); - - $tenancy->setTenant(null); - - $this->expectException(TenantMissingException::class); - $this->expectExceptionMessage( - 'There is no current tenant for tenancy [tenants]' - ); - - TenantChildren::query()->find($child->getKey()); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithInterfaceWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - $tenancy->setTenant($tenant); - - $child = TenantChildrenOptional::factory()->create(); - - $tenancy->setTenant(null); - - $child = TenantChildrenOptional::query()->find($child->getKey()); - - $this->assertInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenants')); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithOverrideWhenHydrating(): void - { - TenantChildren:: ignoreTenantRestrictions(); - - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - $tenancy->setTenant($tenant); - - $child = TenantChildren::factory()->create(); - - $tenancy->setTenant(null); - - $child = TenantChildren::query()->find($child->getKey()); - - TenantChildren::resetTenantRestrictions(); - - $this->assertNotInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenants')); - } - - #[Test] - public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDifferentWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $tenancy->addOption(TenancyOptions::throwIfNotRelated()); - - $child = TenantChildren::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $this->expectException(TenantMismatchException::class); - $this->expectExceptionMessage( - 'Model [' - . TenantChildren::class - . '] already has a tenant, but it is not the current tenant for the tenancy' - . ' [tenants]' - ); - - TenantChildren::query()->withoutTenants()->find($child->getKey()); - } - - #[Test] - public function doesNotThrowAnExceptionForTenantMismatchIfNotSetToWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); - - $child = TenantChildren::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $child = TenantChildren::query()->withoutTenants()->find($child->getKey()); - - $this->assertTrue($child->exists); - $this->assertTrue($child->relationLoaded('tenants')); - $this->assertNotNull($child->tenants->first(fn (Model $model) => $model->is($tenant))); - $this->assertNull($child->tenants->first(fn (Model $model) => $model->is($tenancy->tenant()))); - } - - #[Test] - public function onlyReturnsModelsForTheCurrentTenant(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $original = TenantChildren::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $child = TenantChildren::query()->find($original->getKey()); - - $this->assertNull($child); - - $tenancy->setTenant($tenant); - - $child = TenantChildren::query()->find($original->getKey()); - - $this->assertNotNull($child); - } - - #[Test] - public function ignoresTenantClauseWithBuilderMacro(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - $tenancy->setTenant($tenant); - $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); - - $original = TenantChildren::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $child = TenantChildren::query()->withoutTenants()->find($original->getKey()); - - $this->assertNotNull($child); - - $tenancy->setTenant($tenant); - - $child = TenantChildren::query()->withoutTenants()->find($original->getKey()); - - $this->assertNotNull($child); - } -} diff --git a/tests/_Original/Database/Eloquent/BelongsToTenantTest.php b/tests/_Original/Database/Eloquent/BelongsToTenantTest.php deleted file mode 100644 index 447f30d..0000000 --- a/tests/_Original/Database/Eloquent/BelongsToTenantTest.php +++ /dev/null @@ -1,417 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - #[Test] - public function addsGlobalScope(): void - { - $model = new TenantChild(); - - $this->assertContains(BelongsToTenant::class, class_uses_recursive($model)); - $this->assertArrayHasKey(BelongsToTenantScope::class, $model->getGlobalScopes()); - } - - #[Test] - public function addsObservers(): void - { - $model = new TenantChild(); - $dispatcher = TenantChild::getEventDispatcher(); - - $this->assertContains(BelongsToTenant::class, class_uses_recursive($model)); - - if ($dispatcher instanceof Dispatcher) { - $this->assertTrue($dispatcher->hasListeners('eloquent.retrieved: ' . TenantChild::class)); - $this->assertTrue($dispatcher->hasListeners('eloquent.creating: ' . TenantChild::class)); - - $listeners = $dispatcher->getRawListeners(); - - $this->assertContains(BelongsToTenantObserver::class . '@retrieved', $listeners['eloquent.retrieved: ' . TenantChild::class]); - $this->assertContains(BelongsToTenantObserver::class . '@creating', $listeners['eloquent.creating: ' . TenantChild::class]); - } else { - $this->markTestIncomplete('Cannot complete the test because a custom dispatcher is in place'); - } - } - - #[Test] - public function automaticallyAssociatesWithTenantWhenCreating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $child = TenantChild::factory()->create(); - - $this->assertTrue($child->exists); - $this->assertTrue($child->relationLoaded('tenant')); - $this->assertTrue($child->tenant->is($tenant)); - } - - #[Test] - public function doesNotAutomaticallyAssociateWithTenantWhenCreatingWhenOutsideMultitenantedContext(): void - { - $child = TenantChild::factory()->create(); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertNull($child->tenant); - } - - #[Test] - public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreating(): void - { - sprout()->setCurrentTenancy(app(TenancyManager::class)->get()); - - $this->expectException(TenantMissingException::class); - $this->expectExceptionMessage( - 'There is no current tenant for tenancy [tenants]' - ); - - TenantChild::factory()->create(); - } - - #[Test] - public function doesNotThrowAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenCreatingWhenOutsideMultitenantedContext(): void - { - $child = TenantChild::factory()->create(); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertNull($child->tenant); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithInterfaceWhenCreating(): void - { - $child = TenantChildOptional::factory()->create(); - - $this->assertInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertNull($child->tenant); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithOverrideWhenCreating(): void - { - TenantChild::ignoreTenantRestrictions(); - $child = TenantChild::factory()->create(); - TenantChild::resetTenantRestrictions(); - - $this->assertNotInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertNull($child->tenant); - } - - #[Test] - public function doesNothingIfTheTenantIsAlreadySetOnTheModelWhenCreating(): void - { - $tenant = TenantModel::factory()->create(); - - app(TenancyManager::class)->get()->setTenant($tenant); - - $child = TenantChild::factory()->afterMaking(function (TenantChild $model) use ($tenant) { - $model->tenant()->associate($tenant); - })->create(); - - $this->assertTrue($child->exists); - $this->assertTrue($child->relationLoaded('tenant')); - $this->assertTrue($child->tenant->is($tenant)); - } - - #[Test] - public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDifferentWhenCreating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $tenancy->addOption(TenancyOptions::throwIfNotRelated()); - - $this->expectException(TenantMismatchException::class); - $this->expectExceptionMessage( - 'Model [' - . TenantChild::class - . '] already has a tenant, but it is not the current tenant for the tenancy' - . ' [tenants]' - ); - - TenantChild::factory()->for(TenantModel::factory(), 'tenant')->create(); - } - - #[Test] - public function doesNotThrowAnExceptionForTenantMismatchIfNotSetToWhenCreating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - $tenancy->setTenant($tenant); - $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); - - $child = TenantChild::factory()->for(TenantModel::factory(), 'tenant')->create(); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertFalse($child->tenant->is($tenant)); - } - - #[Test] - public function automaticallyPopulateTheTenantRelationWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $child = TenantChild::query()->find(TenantChild::factory()->create()->getKey()); - - $this->assertTrue($child->exists); - $this->assertTrue($child->relationLoaded('tenant')); - $this->assertNotNull($child->getRelation('tenant')); - $this->assertTrue($child->getRelation('tenant')->is($tenant)); - } - - #[Test] - public function doesNotAutomaticallyPopulateTheTenantRelationWhenHydratingWhenOutsideMultitenantedCContext(): void - { - $child = TenantChild::query()->find(TenantChild::factory()->create()->getKey()); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - } - - #[Test] - public function doNotHydrateWhenHydrateTenantRelationIsMissing(): void - { - /** @var \Sprout\Contracts\Tenancy $tenancy */ - $tenancy = app(TenancyManager::class)->get(); - $tenancy->removeOption(TenancyOptions::hydrateTenantRelation()); - - $tenant = TenantModel::factory()->create(); - - $tenancy->setTenant($tenant); - - $child = TenantChild::query()->find(TenantChild::factory()->create()->getKey()); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - } - - #[Test] - public function throwsAnExceptionIfTheresNoTenantAndTheTenantIsNotOptionalWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $child = TenantChild::factory()->create(); - - $tenancy->setTenant(null); - - $this->expectException(TenantMissingException::class); - $this->expectExceptionMessage( - 'There is no current tenant for tenancy [tenants]' - ); - - TenantChild::query()->find($child->getKey()); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithInterfaceWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - $tenancy->setTenant($tenant); - - $child = TenantChildOptional::factory()->create(); - - $tenancy->setTenant(null); - - $child = TenantChildOptional::query()->find($child->getKey()); - - $this->assertInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - } - - #[Test] - public function doesNothingIfTheresNoTenantAndTheTenantIsOptionalWithOverrideWhenHydrating(): void - { - TenantChild::ignoreTenantRestrictions(); - - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - $tenancy->setTenant($tenant); - - $child = TenantChild::factory()->create(); - - $tenancy->setTenant(null); - - $child = TenantChild::query()->find($child->getKey()); - - TenantChild::resetTenantRestrictions(); - - $this->assertNotInstanceOf(OptionalTenant::class, $child); - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - } - - #[Test] - public function throwsAnExceptionIfTheTenantIsAlreadySetOnTheModelAndItIsDifferentWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $tenancy->addOption(TenancyOptions::throwIfNotRelated()); - - $child = TenantChild::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $this->expectException(TenantMismatchException::class); - $this->expectExceptionMessage( - 'Model [' - . TenantChild::class - . '] already has a tenant, but it is not the current tenant for the tenancy' - . ' [tenants]' - ); - - TenantChild::query()->withoutTenants()->find($child->getKey()); - } - - #[Test] - public function doesNotThrowAnExceptionForTenantMismatchIfNotSetToWhenHydrating(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); - - $child = TenantChild::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $child = TenantChild::query()->withoutTenants()->find($child->getKey()); - - $this->assertTrue($child->exists); - $this->assertFalse($child->relationLoaded('tenant')); - $this->assertTrue($child->tenant->is($tenant)); - $this->assertFalse($child->tenant->is($tenancy->tenant())); - } - - #[Test] - public function onlyReturnsModelsForTheCurrentTenant(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - $original = TenantChild::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $child = TenantChild::query()->find($original->getKey()); - - $this->assertNull($child); - - $tenancy->setTenant($tenant); - - $child = TenantChild::query()->find($original->getKey()); - - $this->assertNotNull($child); - } - - #[Test] - public function ignoresTenantClauseWithBuilderMacro(): void - { - $tenant = TenantModel::factory()->create(); - - $tenancy = app(TenancyManager::class)->get(); - - $tenancy->setTenant($tenant); - $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); - - $original = TenantChild::factory()->create(); - - $tenancy->setTenant(TenantModel::factory()->create()); - - $child = TenantChild::query()->withoutTenants()->find($original->getKey()); - - $this->assertNotNull($child); - - $tenancy->setTenant($tenant); - - $child = TenantChild::query()->withoutTenants()->find($original->getKey()); - - $this->assertNotNull($child); - } -} diff --git a/tests/_Original/Database/Eloquent/TenantChildTest.php b/tests/_Original/Database/Eloquent/TenantChildTest.php deleted file mode 100644 index d725554..0000000 --- a/tests/_Original/Database/Eloquent/TenantChildTest.php +++ /dev/null @@ -1,169 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - #[Test] - public function canFindTenantRelationUsingAttribute(): void - { - $model = new TenantChild(); - - $this->assertContains(IsTenantChild::class, class_uses_recursive($model)); - $this->assertSame('tenant', $model->getTenantRelationName()); - } - - #[Test] - public function canManuallyProvideTenantRelationName(): void - { - $model = new TenantChildren(); - - $this->assertContains(IsTenantChild::class, class_uses_recursive($model)); - $this->assertSame('tenants', $model->getTenantRelationName()); - } - - #[Test] - public function throwsAnExceptionIfItCantFindTheTenantRelation(): void - { - $model = new NoTenantRelationModel(); - - $this->expectException(TenantRelationException::class); - $this->expectExceptionMessage('Cannot find tenant relation for model [' . NoTenantRelationModel::class . ']'); - - $model->getTenantRelationName(); - } - - #[Test] - public function throwsAnExceptionIfThereAreMultipleTenantRelations(): void - { - $model = new TooManyTenantRelationModel(); - - $this->expectException(TenantRelationException::class); - $this->expectExceptionMessage('Expected one tenant relation, found 2 in model [' . TooManyTenantRelationModel::class . ']'); - - $model->getTenantRelationName(); - } - - #[Test] - public function canRetrieveTenantRelationCorrectly(): void - { - $model1 = new TenantChild(); - $model2 = new TenantChildren(); - - $relation1 = $model1->getTenantRelation(); - $relation2 = $model2->getTenantRelation(); - - $this->assertContains(IsTenantChild::class, class_uses_recursive($model1)); - $this->assertContains(IsTenantChild::class, class_uses_recursive($model2)); - $this->assertInstanceOf(BelongsTo::class, $relation1); - $this->assertInstanceOf(BelongsToMany::class, $relation2); - $this->assertSame('tenant', $relation1->getRelationName()); - $this->assertSame('tenants', $relation2->getRelationName()); - } - - #[Test] - public function hasNullTenancyNameByDefault(): void - { - $model = new TenantChild(); - - $this->assertContains(IsTenantChild::class, class_uses_recursive($model)); - $this->assertNull($model->getTenancyName()); - } - - #[Test] - public function canManuallyProvideTheTenancyName(): void - { - $model = new TenantChildren(); - - $this->assertContains(IsTenantChild::class, class_uses_recursive($model)); - $this->assertSame('tenants', $model->getTenancyName()); - } - - #[Test] - public function canRetrieveTenancyCorrectly(): void - { - $model1 = new TenantChild(); - $model2 = new TenantChildren(); - - $tenancy1 = $model1->getTenancy(); - $tenancy2 = $model2->getTenancy(); - - $this->assertSame('tenants', $tenancy1->getName()); - $this->assertSame('tenants', $tenancy2->getName()); - } - - #[Test] - public function hasManualTenantRestrictionOverride(): void - { - $this->assertFalse(TenantChild::isTenantOptional()); - $this->assertFalse(TenantChildren::isTenantOptional()); - - TenantChild::ignoreTenantRestrictions(); - - $this->assertTrue(TenantChild::isTenantOptional()); - $this->assertFalse(TenantChildren::isTenantOptional()); - - TenantChildren::ignoreTenantRestrictions(); - - $this->assertTrue(TenantChild::isTenantOptional()); - $this->assertTrue(TenantChildren::isTenantOptional()); - - TenantChild::resetTenantRestrictions(); - - $this->assertFalse(TenantChild::isTenantOptional()); - $this->assertTrue(TenantChildren::isTenantOptional()); - - TenantChildren::resetTenantRestrictions(); - - $this->assertFalse(TenantChild::isTenantOptional()); - $this->assertFalse(TenantChildren::isTenantOptional()); - } - - #[Test] - public function hasManualTenantRestrictionTemporaryOverride(): void - { - $this->assertFalse(TenantChild::isTenantOptional()); - $this->assertFalse(TenantChildren::isTenantOptional()); - - TenantChild::withoutTenantRestrictions(function () { - $this->assertTrue(TenantChild::isTenantOptional()); - $this->assertFalse(TenantChildren::isTenantOptional()); - }); - - $this->assertFalse(TenantChild::isTenantOptional()); - $this->assertFalse(TenantChildren::isTenantOptional()); - - TenantChildren::withoutTenantRestrictions(function () { - $this->assertFalse(TenantChild::isTenantOptional()); - $this->assertTrue(TenantChildren::isTenantOptional()); - }); - - $this->assertFalse(TenantChild::isTenantOptional()); - $this->assertFalse(TenantChildren::isTenantOptional()); - } -} diff --git a/tests/_Original/Http/Resolvers/CookieResolverTest.php b/tests/_Original/Http/Resolvers/CookieResolverTest.php deleted file mode 100644 index 80063a5..0000000 --- a/tests/_Original/Http/Resolvers/CookieResolverTest.php +++ /dev/null @@ -1,75 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - $config->set('multitenancy.defaults.resolver', 'cookie'); - $config->set('multitenancy.resolvers.cookie', [ - 'driver' => 'cookie', - 'cookie' => '{Tenancy}-Identifier', - ]); - }); - } - - protected function defineRoutes($router) - { - $router->get('/', function () { - return 'no'; - }); - - $router->tenanted(function (Router $router) { - $router->get('/cookie-route', function (#[CurrentTenant] Tenant $tenant) { - return $tenant->getTenantKey(); - })->name('cookie.route'); - }, 'cookie', 'tenants'); - } - - #[Test] - public function resolvesFromRoute(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->withUnencryptedCookie('Tenants-Identifier', $tenant->getTenantIdentifier())->get(route('cookie.route')); - - $result->assertOk(); - $result->assertContent((string)$tenant->getTenantKey()); - $result->cookie('Tenants-Identifier', $tenant->getTenantIdentifier()); - } - - #[Test] - public function throwsExceptionForInvalidTenant(): void - { - $result = $this->withCookie('Tenants-Identifier', 'i-am-not-real')->get(route('cookie.route')); - $result->assertInternalServerError(); - } - - #[Test] - public function throwsExceptionWithoutHeader(): void - { - $result = $this->get(route('cookie.route')); - - $result->assertInternalServerError(); - } -} diff --git a/tests/_Original/Http/Resolvers/HeaderResolverTest.php b/tests/_Original/Http/Resolvers/HeaderResolverTest.php deleted file mode 100644 index 6f98b42..0000000 --- a/tests/_Original/Http/Resolvers/HeaderResolverTest.php +++ /dev/null @@ -1,79 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - $config->set('multitenancy.defaults.resolver', 'header'); - }); - } - - protected function defineRoutes($router) - { - $router->get('/', function () { - return 'no'; - }); - - $router->tenanted(function (Router $router) { - $router->get('/header-route', function (#[CurrentTenant] Tenant $tenant) { - return $tenant->getTenantKey(); - })->name('header.route'); - }, 'header', 'tenants'); - } - - #[Test] - public function resolvesFromRoute(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get(route('header.route'), ['Tenants-Identifier' => $tenant->getTenantIdentifier()]); - - $result->assertOk(); - $result->assertContent((string)$tenant->getTenantKey()); - } - - #[Test] - public function throwsExceptionForInvalidTenant(): void - { - $result = $this->get(route('header.route'), ['Tenants-Identifier' => 'i-am-not-real']); - - $result->assertInternalServerError(); - } - - #[Test] - public function throwsExceptionWithoutHeader(): void - { - $result = $this->get(route('header.route')); - - $result->assertInternalServerError(); - } - - #[Test] - public function addTenantHeaderQueueingMiddleware(): void - { - $route = app(Router::class)->getRoutes()->getByName('header.route'); - - $this->assertNotNull($route); - $this->assertContains(AddTenantHeaderToResponse::class . ':header,tenants', $route->middleware()); - } -} diff --git a/tests/_Original/Http/Resolvers/PathResolverTest.php b/tests/_Original/Http/Resolvers/PathResolverTest.php deleted file mode 100644 index a767cff..0000000 --- a/tests/_Original/Http/Resolvers/PathResolverTest.php +++ /dev/null @@ -1,150 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - $config->set('multitenancy.defaults.resolver', 'path'); - }); - } - - protected function withManualParameterName($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.path.parameter', 'custom-parameter'); - }); - } - - protected function withParameterPatternName($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.path.pattern', '.*'); - }); - } - - protected function withoutManualParameterName($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.path.parameter', null); - }); - } - - protected function withoutParameterPattern($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.path.pattern', null); - }); - } - - protected function defineRoutes($router): void - { - $router->get('/', function () { - return 'no'; - }); - - $router->tenanted(function (Router $router) { - $router->get('/path-route', function (#[CurrentTenant] Tenant $tenant) { - return $tenant->getTenantKey(); - })->name('path.route'); - }, 'path', 'tenants'); - - $router->get('/{identifier}/path-request', function (#[CurrentTenant] Tenant $tenant) { - return $tenant->getTenantKey(); - })->middleware('sprout.tenanted')->name('path.request'); - } - - #[Test] - public function resolvesFromParameter(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get(route('path.route', ['tenants_path' => $tenant->getTenantIdentifier()])); - - $result->assertOk(); - $result->assertContent((string)$tenant->getTenantKey()); - } - - #[Test] - public function resolvesWithoutParameter(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get('/' . $tenant->getTenantIdentifier() . '/path-request'); - - $result->assertOk(); - $result->assertContent((string)$tenant->getTenantKey()); - } - - #[Test] - public function throwsExceptionForInvalidTenantWithParameter(): void - { - $result = $this->get(route('path.route', ['tenants_path' => 'i-am-not-real'])); - - $result->assertInternalServerError(); - } - - #[Test] - public function throwsExceptionForInvalidTenantWithoutParameter(): void - { - $result = $this->get('/i-am-not-real/path-request'); - - $result->assertInternalServerError(); - } - - #[Test, DefineEnvironment('withoutParameterPattern')] - public function hasNoParameterPatternByDefault(): void - { - /** @var \Sprout\Http\Resolvers\SubdomainIdentityResolver $resolver */ - $resolver = resolver('path'); - - $this->assertNull($resolver->getPattern()); - } - - #[Test, DefineEnvironment('withoutManualParameterName')] - public function hasDefaultParameterNameByDefault(): void - { - /** @var \Sprout\Http\Resolvers\SubdomainIdentityResolver $resolver */ - $resolver = resolver('path'); - - $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); - } - - #[Test, DefineEnvironment('withManualParameterName')] - public function allowsForCustomParameterName(): void - { - /** @var \Sprout\Http\Resolvers\PathIdentityResolver $resolver */ - $resolver = resolver('path'); - - $this->assertSame('custom-parameter', $resolver->getParameter()); - } - - #[Test, DefineEnvironment('withParameterPatternName')] - public function allowsForCustomParameterPattern(): void - { - /** @var \Sprout\Http\Resolvers\PathIdentityResolver $resolver */ - $resolver = resolver('path'); - - $this->assertSame('.*', $resolver->getPattern()); - } -} diff --git a/tests/_Original/Http/Resolvers/SessionResolverTest.php b/tests/_Original/Http/Resolvers/SessionResolverTest.php deleted file mode 100644 index 2564e6d..0000000 --- a/tests/_Original/Http/Resolvers/SessionResolverTest.php +++ /dev/null @@ -1,123 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - $config->set('multitenancy.defaults.resolver', 'session'); - $config->set('multitenancy.resolvers.session', [ - 'driver' => 'session', - 'session' => 'multitenancy.{tenancy}', - ]); - $config->set('sprout.services', [ - StorageOverride::class, - JobOverride::class, - CacheOverride::class, - AuthOverride::class, - CookieOverride::class, - ]); - }); - } - - protected function withSessionOverride($app):void - { - tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', [ - StorageOverride::class, - JobOverride::class, - CacheOverride::class, - AuthOverride::class, - CookieOverride::class, - SessionOverride::class, - ]); - }); - } - - protected function defineRoutes($router): void - { - $router->middleware(StartSession::class)->group(function (Router $router) { - $router->get('/', function () { - return 'no'; - }); - - $router->tenanted(function (Router $router) { - $router->get('/session-route', function (#[CurrentTenant] Tenant $tenant) { - return $tenant->getTenantKey(); - })->name('session.route'); - }, 'session', 'tenants'); - }); - } - - #[Test] - public function resolvesFromRoute(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->withSession(['multitenancy' => ['tenants' => $tenant->getTenantIdentifier()]])->get(route('session.route')); - - $result->assertOk(); - $result->assertContent((string)$tenant->getTenantKey()); - $result->assertSessionHas('multitenancy.tenants', $tenant->getTenantIdentifier()); - } - - #[Test] - public function throwsExceptionForInvalidTenant(): void - { - $result = $this->withSession(['multitenancy' => ['tenants' => 'i-am-not-real']])->get(route('session.route')); - $result->assertInternalServerError(); - } - - #[Test] - public function throwsExceptionWithoutHeader(): void - { - $result = $this->get(route('session.route')); - - $result->assertInternalServerError(); - } - - #[Test] - public function throwsExceptionIfSessionOverrideIsEnabled(): void - { - sprout()->registerOverride(Services::SESSION, SessionOverride::class); - $tenant = TenantModel::factory()->createOne(); - - $result = $this->withSession(['multitenancy' => ['tenants' => $tenant->getTenantIdentifier()]])->get(route('session.route')); - $result->assertInternalServerError(); - $this->assertInstanceOf(CompatibilityException::class, $result->exception); - $this->assertSame( - 'Cannot use resolver [session] with override [' . SessionOverride::class . ']', - $result->exception->getMessage() - ); - } -} diff --git a/tests/_Original/Http/Resolvers/SubdomainResolverTest.php b/tests/_Original/Http/Resolvers/SubdomainResolverTest.php deleted file mode 100644 index ed9152c..0000000 --- a/tests/_Original/Http/Resolvers/SubdomainResolverTest.php +++ /dev/null @@ -1,173 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - $config->set('multitenancy.resolvers.subdomain.domain', 'localhost'); - $config->set('session.driver', 'database'); - }); - } - - protected function withManualParameterName($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.subdomain.parameter', 'custom-parameter'); - }); - } - - protected function withParameterPatternName($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.subdomain.pattern', '.*'); - }); - } - - protected function withoutManualParameterName($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.subdomain.parameter', null); - }); - } - - protected function withoutParameterPattern($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('multitenancy.resolvers.subdomain.pattern', null); - }); - } - - protected function defineRoutes($router) - { - $router->middleware('web')->group(function (Router $router) { - $router->get('/', function () { - return 'no'; - }); - - $router->tenanted(function (Router $router) { - $router->get('/subdomain-route', function (#[CurrentTenant] Tenant $tenant) { - return $tenant->getTenantKey(); - })->name('subdomain.route'); - }, 'subdomain', 'tenants'); - - $router->get('/subdomain-request', function (#[CurrentTenant] Tenant $tenant) { - return $tenant->getTenantKey(); - })->middleware('sprout.tenanted')->name('subdomain.request'); - }); - } - - #[Test] - public function resolvesFromParameter(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get(route('subdomain.route', ['tenants_subdomain' => $tenant->getTenantIdentifier()])); - - $result->assertOk(); - $result->assertContent((string)$tenant->getTenantKey()); - $this->assertTrue(sprout()->withinContext()); - } - - #[Test] - public function canAccessNonTenantedRoutesSuccessfully(): void - { - $result = $this->get('/'); - - $result->assertOk(); - $result->assertContent('no'); - $this->assertFalse(sprout()->withinContext()); - } - - #[Test] - public function resolvesWithoutParameter(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get('http://' . $tenant->getTenantIdentifier() . '.localhost/subdomain-request'); - - $result->assertOk(); - $result->assertContent((string)$tenant->getTenantKey()); - } - - #[Test] - public function throwsExceptionForInvalidTenantWithParameter(): void - { - $result = $this->get(route('subdomain.route', ['tenants_subdomain' => 'i-am-not-real'])); - - $result->assertInternalServerError(); - } - - #[Test] - public function throwsExceptionForInvalidTenantWithoutParameter(): void - { - $result = $this->get('http://i-am-not-real.localhost/subdomain-request'); - - $result->assertInternalServerError(); - } - - #[Test] - public function throwsExceptionForInvalidTenantWithJustMultitenantedDomain(): void - { - $result = $this->get('http://localhost/subdomain-request'); - - $result->assertInternalServerError(); - } - - #[Test, DefineEnvironment('withoutParameterPattern')] - public function hasNoParameterPatternByDefault(): void - { - /** @var \Sprout\Http\Resolvers\SubdomainIdentityResolver $resolver */ - $resolver = resolver('subdomain'); - - $this->assertNull($resolver->getPattern()); - } - - #[Test, DefineEnvironment('withoutManualParameterName')] - public function hasDefaultParameterNameByDefault(): void - { - /** @var \Sprout\Http\Resolvers\SubdomainIdentityResolver $resolver */ - $resolver = resolver('subdomain'); - - $this->assertSame('{tenancy}_{resolver}', $resolver->getParameter()); - } - - #[Test, DefineEnvironment('withManualParameterName')] - public function allowsForCustomParameterName(): void - { - /** @var \Sprout\Http\Resolvers\SubdomainIdentityResolver $resolver */ - $resolver = resolver('subdomain'); - - $this->assertSame('custom-parameter', $resolver->getParameter()); - } - - #[Test, DefineEnvironment('withParameterPatternName')] - public function allowsForCustomParameterPattern(): void - { - /** @var \Sprout\Http\Resolvers\SubdomainIdentityResolver $resolver */ - $resolver = resolver('subdomain'); - - $this->assertSame('.*', $resolver->getPattern()); - } -} diff --git a/tests/_Original/Listeners/SetCurrentTenantForJobTest.php b/tests/_Original/Listeners/SetCurrentTenantForJobTest.php deleted file mode 100644 index 4cf82db..0000000 --- a/tests/_Original/Listeners/SetCurrentTenantForJobTest.php +++ /dev/null @@ -1,91 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - protected function noJobOverride($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', [ - StorageOverride::class, - CacheOverride::class, - AuthOverride::class, - CookieOverride::class, - SessionOverride::class, - ]); - }); - } - - #[Test, DefineEnvironment('noJobOverride')] - public function doesNotSetCurrentTenantForJobWithoutOption(): void - { - /** @var \Sprout\Contracts\Tenancy<*> $tenancy */ - $tenancy = app(TenancyManager::class)->get(); - - $this->assertFalse($tenancy->check()); - - $tenant = TenantModel::factory()->createOne(); - - Context::add('sprout.tenants', [$tenancy->getName() => $tenant->getKey()]); - - $this->assertTrue(Context::has('sprout.tenants')); - $this->assertSame([$tenancy->getName() => $tenant->getKey()], Context::get('sprout.tenants')); - - TestTenantJob::dispatchSync(); - - $this->assertFalse($tenancy->check()); - } - - #[Test] - public function setsCurrentTenantForJobWithOption(): void - { - /** @var \Sprout\Contracts\Tenancy<*> $tenancy */ - $tenancy = app(TenancyManager::class)->get(); - - $this->assertFalse($tenancy->check()); - - $tenant = TenantModel::factory()->createOne(); - - Context::add('sprout.tenants', [$tenancy->getName() => $tenant->getKey()]); - - $this->assertTrue(Context::has('sprout.tenants')); - $this->assertSame([$tenancy->getName() => $tenant->getKey()], Context::get('sprout.tenants')); - - TestTenantJob::dispatchSync(); - - $this->assertTrue($tenancy->check()); - $this->assertSame($tenant->getKey(), $tenancy->key()); - $this->assertTrue($tenant->is($tenancy->tenant())); - } -} diff --git a/tests/_Original/Overrides/CookieOverrideTest.php b/tests/_Original/Overrides/CookieOverrideTest.php deleted file mode 100644 index a55afcf..0000000 --- a/tests/_Original/Overrides/CookieOverrideTest.php +++ /dev/null @@ -1,165 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - $config->set('multitenancy.resolvers.subdomain.domain', 'localhost'); - }); - } - - protected function noCookieOverride($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', [ - StorageOverride::class, - JobOverride::class, - CacheOverride::class, - AuthOverride::class, - ]); - }); - } - - protected function defineRoutes($router): void - { - $router->get('/', function () { - return response('No tenancy')->cookie(Cookie::make('no_tenancy_cookie', 'foo')); - })->middleware('web')->name('home'); - - $router->tenanted(function (Router $router) { - $router->get('/subdomain-route', function (#[CurrentTenant] Tenant $tenant) { - return response($tenant->getTenantIdentifier())->cookie( - Cookie::make('yes_tenancy_cookie', $tenant->getTenantKey()) - ); - })->name('subdomain.route')->middleware('web'); - }, 'subdomain', 'tenants'); - - $router->tenanted(function (Router $router) { - $router->get('/path-route', function (#[CurrentTenant] Tenant $tenant) { - return response($tenant->getTenantIdentifier())->cookie( - Cookie::make('yes_tenancy_cookie', $tenant->getTenantKey()) - ); - })->name('path.route')->middleware('web'); - }, 'path', 'tenants'); - } - - #[Test] - public function doesNotAffectNonTenantedCookies(): void - { - $result = $this->get(route('home')); - - $result->assertOk()->assertCookie('no_tenancy_cookie'); - - /** @var \Symfony\Component\HttpFoundation\Cookie $cookie */ - $cookie = $result->getCookie('no_tenancy_cookie'); - - $this->assertSame(config('session.domain'), $cookie->getDomain()); - $this->assertSame(config('session.path'), $cookie->getPath()); - $this->assertSame((bool)config('session.secure'), $cookie->isSecure()); - $this->assertSame(config('session.same_site'), $cookie->getSameSite()); - $this->assertSame('foo', $cookie->getValue()); - } - - #[Test] - public function setsTheCookieDomainWhenUsingTheSubdomainIdentityResolver(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get(route('subdomain.route', [$tenant->getTenantIdentifier()])); - - $result->assertOk()->assertCookie('yes_tenancy_cookie'); - - /** @var \Symfony\Component\HttpFoundation\Cookie $cookie */ - $cookie = $result->getCookie('yes_tenancy_cookie'); - - $this->assertSame($tenant->getTenantIdentifier() . '.localhost', $cookie->getDomain()); - $this->assertSame(config('session.path'), $cookie->getPath()); - $this->assertSame((bool)config('session.secure'), $cookie->isSecure()); - $this->assertSame(config('session.same_site'), $cookie->getSameSite()); - $this->assertSame((string)$tenant->getTenantKey(), $cookie->getValue()); - } - - #[Test] - public function setsTheCookiePathWhenUsingThePathIdentityResolver(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get(route('path.route', [$tenant->getTenantIdentifier()])); - - $result->assertOk()->assertCookie('yes_tenancy_cookie'); - - /** @var \Symfony\Component\HttpFoundation\Cookie $cookie */ - $cookie = $result->getCookie('yes_tenancy_cookie'); - - $this->assertSame(config('session.domain'), $cookie->getDomain()); - $this->assertSame($tenant->getTenantIdentifier(), $cookie->getPath()); - $this->assertSame((bool)config('session.secure'), $cookie->isSecure()); - $this->assertSame(config('session.same_site'), $cookie->getSameSite()); - $this->assertSame((string)$tenant->getTenantKey(), $cookie->getValue()); - } - - #[Test, DefineEnvironment('noCookieOverride')] - public function doesNotSetTheCookieDomainWhenUsingTheSubdomainIdentityResolverIfDisabled(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get(route('subdomain.route', [$tenant->getTenantIdentifier()])); - - $result->assertOk()->assertCookie('yes_tenancy_cookie'); - - /** @var \Symfony\Component\HttpFoundation\Cookie $cookie */ - $cookie = $result->getCookie('yes_tenancy_cookie'); - - $this->assertSame(config('session.domain'), $cookie->getDomain()); - $this->assertSame(config('session.path'), $cookie->getPath()); - $this->assertSame((bool)config('session.secure'), $cookie->isSecure()); - $this->assertSame(config('session.same_site'), $cookie->getSameSite()); - $this->assertSame((string)$tenant->getTenantKey(), $cookie->getValue()); - } - - #[Test, DefineEnvironment('noCookieOverride')] - public function doesNotSetTheCookiePathWhenUsingThePathIdentityResolverIfDisabled(): void - { - $tenant = TenantModel::factory()->createOne(); - - $result = $this->get(route('path.route', [$tenant->getTenantIdentifier()])); - - $result->assertOk()->assertCookie('yes_tenancy_cookie'); - - /** @var \Symfony\Component\HttpFoundation\Cookie $cookie */ - $cookie = $result->getCookie('yes_tenancy_cookie'); - - $this->assertSame(config('session.domain'), $cookie->getDomain()); - $this->assertSame(config('session.path'), $cookie->getPath()); - $this->assertSame((bool)config('session.secure'), $cookie->isSecure()); - $this->assertSame(config('session.same_site'), $cookie->getSameSite()); - $this->assertSame((string)$tenant->getTenantKey(), $cookie->getValue()); - } -} diff --git a/tests/_Original/Overrides/StorageOverrideTest.php b/tests/_Original/Overrides/StorageOverrideTest.php deleted file mode 100644 index 2657b2b..0000000 --- a/tests/_Original/Overrides/StorageOverrideTest.php +++ /dev/null @@ -1,184 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - protected function createTenantDisk($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('filesystems.disks.tenant', [ - 'driver' => 'sprout', - 'disk' => 'local', - 'tenancy' => 'tenants', - ]); - }); - } - - protected function noStorageOverride($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', [ - JobOverride::class, - CacheOverride::class, - AuthOverride::class, - CookieOverride::class, - SessionOverride::class, - ]); - }); - } - - protected function yesStorageOverride($app): void - { - tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', [ - JobOverride::class, - CacheOverride::class, - AuthOverride::class, - CookieOverride::class, - SessionOverride::class, - StorageOverride::class - ]); - }); - } - - #[Test, DefineEnvironment('createTenantDisk')] - public function canCreateScopedTenantFilesystemDisk(): void - { - $tenant = TenantModel::factory()->createOne(); - - app(TenancyManager::class)->get()->setTenant($tenant); - - $disk = Storage::disk('tenant'); - - $this->assertNotNull($disk); - $this->assertSame($tenant->getTenantResourceKey(), basename($disk->path(''))); - } - - #[Test, DefineEnvironment('createTenantDisk')] - public function canCreateScopedTenantFilesystemDiskWithCustomConfig(): void - { - config()->set('filesystems.disks.tenant.disk', config('filesystems.disks.local')); - - $tenant = TenantModel::factory()->createOne(); - - app(TenancyManager::class)->get()->setTenant($tenant); - - $disk = Storage::disk('tenant'); - - $this->assertNotNull($disk); - $this->assertSame($tenant->getTenantResourceKey(), basename($disk->path(''))); - } - - #[Test, DefineEnvironment('createTenantDisk')] - public function throwsExceptionIfThereIsNoTenant(): void - { - $this->expectException(TenantMissingException::class); - $this->expectExceptionMessage('There is no current tenant for tenancy [tenants]'); - - Storage::disk('tenant'); - } - - #[Test, DefineEnvironment('createTenantDisk')] - public function throwsExceptionIfTheTenantDoesNotHaveResources(): void - { - $this->expectException(MisconfigurationException::class); - $this->expectExceptionMessage('The current tenant [' . NoResourcesTenantModel::class . '] is not configured correctly for resources'); - - config()->set('multitenancy.providers.tenants.model', NoResourcesTenantModel::class); - - app(TenancyManager::class)->get()->setTenant(NoResourcesTenantModel::factory()->createOne()); - - Storage::disk('tenant'); - } - - #[Test, DefineEnvironment('createTenantDisk')] - public function cleansUpStorageDiskAfterTenantChange(): void - { - $tenant = TenantModel::factory()->createOne(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant); - - Storage::disk('tenant'); - - Storage::shouldReceive('forgetDisk')->withArgs(['tenant'])->once(); - - app(TenancyManager::class)->get()->setTenant(null); - } - - #[Test, DefineEnvironment('createTenantDisk')] - public function recreatesStorageDiskPerTenant(): void - { - $tenant1 = TenantModel::factory()->createOne(); - - $tenancy = app(TenancyManager::class)->get(); - - sprout()->setCurrentTenancy($tenancy); - - $tenancy->setTenant($tenant1); - - $disk = Storage::disk('tenant'); - - $this->assertNotNull($disk); - $this->assertSame($tenant1->getTenantResourceKey(), basename($disk->path(''))); - - $tenant2 = TenantModel::factory()->createOne(); - - app(TenancyManager::class)->get()->setTenant($tenant2); - - $disk = Storage::disk('tenant'); - - $this->assertNotNull($disk); - $this->assertSame($tenant2->getTenantResourceKey(), basename($disk->path(''))); - } - - #[Test, DefineEnvironment('noStorageOverride')] - public function doesNotOverrideStorageIfDisabled(): void - { - app(TenancyManager::class)->get()->setTenant(TenantModel::factory()->createOne()); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Disk [tenant] does not have a configured driver.'); - - Storage::disk('tenant'); - } -} diff --git a/tests/_Original/Providers/DatabaseTenantProviderTest.php b/tests/_Original/Providers/DatabaseTenantProviderTest.php deleted file mode 100644 index 90c24b9..0000000 --- a/tests/_Original/Providers/DatabaseTenantProviderTest.php +++ /dev/null @@ -1,106 +0,0 @@ -set('multitenancy.providers.backup', [ - 'driver' => 'database', - 'table' => 'tenants', - ]); - }); - } - - #[Test] - public function isRegisteredCorrectly(): void - { - $manager = app(TenantProviderManager::class); - $provider = $manager->get('backup'); - - $this->assertNotNull($provider); - $this->assertInstanceOf(DatabaseTenantProvider::class, $provider); - $this->assertSame('backup', $provider->getName()); - $this->assertSame('tenants', $provider->getTable()); - $this->assertSame(GenericTenant::class, $provider->getEntityClass()); - } - - #[Test] - public function canRetrieveByIdentifier(): void - { - $id = DB::table('tenants')->insertGetId( - [ - 'name' => 'Test Tenant', - 'identifier' => 'the-big-test-boy', - 'resource_key' => Str::uuid()->toString(), - 'active' => true, - ] - ); - - $provider = app(Sprout::class)->providers()->get('backup'); - $tenant = $provider->retrieveByIdentifier('the-big-test-boy'); - - $this->assertNotNull($tenant); - $this->assertInstanceOf(GenericTenant::class, $tenant); - $this->assertSame('the-big-test-boy', $tenant->getTenantIdentifier()); - $this->assertSame($id, $tenant->getTenantKey()); - } - - #[Test] - public function failsSilentlyWithInvalidIdentifier(): void - { - $provider = app(Sprout::class)->providers()->get('backup'); - $tenant = $provider->retrieveByIdentifier('i-do-not-exists-and-never-will'); - - $this->assertNull($tenant); - } - - #[Test] - public function canRetrieveByKey(): void - { - $id = DB::table('tenants')->insertGetId( - [ - 'name' => 'Test Tenant', - 'identifier' => 'the-big-test-boy2', - 'resource_key' => Str::uuid()->toString(), - 'active' => true, - ] - ); - - $provider = app(Sprout::class)->providers()->get('backup'); - $tenant = $provider->retrieveByKey($id); - - $this->assertNotNull($tenant); - $this->assertInstanceOf(GenericTenant::class, $tenant); - $this->assertSame('the-big-test-boy2', $tenant->getTenantIdentifier()); - $this->assertSame($id, $tenant->getTenantKey()); - } - - #[Test] - public function failsSilentlyWithInvalidKey(): void - { - $provider = app(Sprout::class)->providers()->get('backup'); - $tenant = $provider->retrieveByKey(889907); - - $this->assertNull($tenant); - } -} diff --git a/tests/_Original/Providers/EloquentTenantProviderTest.php b/tests/_Original/Providers/EloquentTenantProviderTest.php deleted file mode 100644 index 4b4c146..0000000 --- a/tests/_Original/Providers/EloquentTenantProviderTest.php +++ /dev/null @@ -1,94 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - #[Test] - public function isRegisteredCorrectly(): void - { - $manager = app(TenantProviderManager::class); - $provider = $manager->get('tenants'); - - $this->assertNotNull($provider); - $this->assertInstanceOf(EloquentTenantProvider::class, $provider); - $this->assertSame('tenants', $provider->getName()); - $this->assertSame(TenantModel::class, $provider->getModelClass()); - } - - #[Test] - public function canRetrieveByIdentifier(): void - { - $newTenant = TenantModel::factory()->createOne( - [ - 'name' => 'Test Tenant', - 'identifier' => 'the-big-test-boy', - 'active' => true, - ] - ); - - $provider = app(Sprout::class)->providers()->get('tenants'); - $tenant = $provider->retrieveByIdentifier($newTenant->getTenantIdentifier()); - - $this->assertNotNull($tenant); - $this->assertTrue($newTenant->is($tenant)); - } - - #[Test] - public function failsSilentlyWithInvalidIdentifier(): void - { - $provider = app(Sprout::class)->providers()->get('tenants'); - $tenant = $provider->retrieveByIdentifier('i-do-not-exists-and-never-will'); - - $this->assertNull($tenant); - } - - #[Test] - public function canRetrieveByKey(): void - { - $newTenant = TenantModel::factory()->createOne( - [ - 'name' => 'Test Tenant', - 'identifier' => 'the-big-test-boy2', - 'active' => true, - ] - ); - - $provider = app(Sprout::class)->providers()->get('tenants'); - $tenant = $provider->retrieveByKey($newTenant->getTenantKey()); - - $this->assertNotNull($tenant); - $this->assertTrue($newTenant->is($tenant)); - } - - #[Test] - public function failsSilentlyWithInvalidKey(): void - { - $provider = app(Sprout::class)->providers()->get('tenants'); - $tenant = $provider->retrieveByKey(889907); - - $this->assertNull($tenant); - } -} diff --git a/tests/_Original/ServiceProviderTest.php b/tests/_Original/ServiceProviderTest.php deleted file mode 100644 index 1982463..0000000 --- a/tests/_Original/ServiceProviderTest.php +++ /dev/null @@ -1,135 +0,0 @@ -assertTrue(app()->providerIsLoaded(SproutServiceProvider::class)); - } - - #[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)); - } - - #[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 providerManagerIsRegistered(): void - { - $this->assertTrue(app()->has(TenantProviderManager::class)); - $this->assertTrue(app()->has('sprout.providers')); - $this->assertTrue(app()->isShared(TenantProviderManager::class)); - $this->assertFalse(app()->isShared('sprout.providers')); - - $this->assertSame(app()->make(TenantProviderManager::class), app()->make(TenantProviderManager::class)); - $this->assertSame(app()->make('sprout.providers'), app()->make('sprout.providers')); - $this->assertSame(app()->make(TenantProviderManager::class), app()->make('sprout.providers')); - $this->assertSame(app()->make('sprout.providers'), app()->make(TenantProviderManager::class)); - $this->assertSame(app()->make(Sprout::class)->providers(), app()->make('sprout.providers')); - $this->assertSame(app()->make(Sprout::class)->providers(), app()->make(TenantProviderManager::class)); - } - - #[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)); - } - - #[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)); - } - - #[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 registersEventHandlers(): void - { - $dispatcher = app()->make(Dispatcher::class); - - $this->assertTrue($dispatcher->hasListeners(RouteMatched::class)); - $this->assertTrue($dispatcher->hasListeners(CurrentTenantChanged::class)); - $this->assertTrue($dispatcher->hasListeners(JobProcessing::class)); - - $listeners = $dispatcher->getRawListeners(); - - $this->assertContains(IdentifyTenantOnRouting::class, $listeners[RouteMatched::class]); - $this->assertContains(SetCurrentTenantContext::class, $listeners[CurrentTenantChanged::class]); - $this->assertContains(PerformIdentityResolverSetup::class, $listeners[CurrentTenantChanged::class]); - $this->assertContains(SetCurrentTenantForJob::class, $listeners[JobProcessing::class]); - } -} diff --git a/tests/_Original/SproutTest.php b/tests/_Original/SproutTest.php deleted file mode 100644 index 26d126b..0000000 --- a/tests/_Original/SproutTest.php +++ /dev/null @@ -1,67 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - #[Test] - public function makesCoreConfigAccessible(): void - { - $sprout = app()->make(Sprout::class); - - $this->assertNotNull($sprout->config('hooks')); - $this->assertNotNull(config('sprout.hooks')); - $this->assertSame($sprout->config('hooks'), config('sprout.hooks')); - - app()['config']->set('sprout.hooks', null); - - $this->assertNull($sprout->config('hooks')); - $this->assertNull(config('sprout.hooks')); - $this->assertSame($sprout->config('hooks'), config('sprout.hooks')); - } - - #[Test] - public function keepsTrackOfCurrentTenancies(): void - { - $sprout = app()->make(Sprout::class); - - $this->assertFalse($sprout->hasCurrentTenancy()); - $this->assertNull($sprout->getCurrentTenancy()); - $this->assertEmpty($sprout->getAllCurrentTenancies()); - - $tenancy = $sprout->tenancies()->get('tenants'); - $sprout->setCurrentTenancy($tenancy); - - $this->assertTrue($sprout->hasCurrentTenancy()); - $this->assertNotNull($sprout->getCurrentTenancy()); - $this->assertSame($tenancy, $sprout->getCurrentTenancy()); - $this->assertNotEmpty($sprout->getAllCurrentTenancies()); - $this->assertCount(1, $sprout->getAllCurrentTenancies()); - - $sprout->setCurrentTenancy($tenancy); - - $this->assertCount(1, $sprout->getAllCurrentTenancies()); - } -} diff --git a/tests/_Original/TenancyOptionsTest.php b/tests/_Original/TenancyOptionsTest.php deleted file mode 100644 index fd60e7f..0000000 --- a/tests/_Original/TenancyOptionsTest.php +++ /dev/null @@ -1,67 +0,0 @@ -set('multitenancy.providers.tenants.model', TenantModel::class); - }); - } - - #[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] - public function correctlyReportsHydrateTenantRelationOptionPresence(): void - { - $tenancy = app(TenancyManager::class)->get('tenants'); - $tenancy->removeOption(TenancyOptions::hydrateTenantRelation()); - - $this->assertFalse(TenancyOptions::shouldHydrateTenantRelation($tenancy)); - - $tenancy->addOption(TenancyOptions::hydrateTenantRelation()); - - $this->assertTrue(TenancyOptions::shouldHydrateTenantRelation($tenancy)); - } - - #[Test] - public function correctlyReportsThrowIfNotRelatedOptionPresence(): void - { - $tenancy = app(TenancyManager::class)->get('tenants'); - $tenancy->removeOption(TenancyOptions::throwIfNotRelated()); - - $this->assertFalse(TenancyOptions::shouldThrowIfNotRelated($tenancy)); - - $tenancy->addOption(TenancyOptions::throwIfNotRelated()); - - $this->assertTrue(TenancyOptions::shouldThrowIfNotRelated($tenancy)); - } -} From 89c4fdc338df78a98807c6a260be325ed4033753 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:49:12 +0000 Subject: [PATCH 16/48] chore: Default tenancy to have all overrides --- resources/config/multitenancy.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/resources/config/multitenancy.php b/resources/config/multitenancy.php index 1e84174..4df0332 100644 --- a/resources/config/multitenancy.php +++ b/resources/config/multitenancy.php @@ -50,14 +50,7 @@ 'options' => [ TenancyOptions::hydrateTenantRelation(), TenancyOptions::throwIfNotRelated(), - TenancyOptions::overrides([ - 'filesystem', - 'job', - 'cache', - 'auth', - 'cookie', - 'session', - ]), + TenancyOptions::allOverrides(), ], ], From 1a95763b49bef353d2582cdf79095057404a63c7 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:49:26 +0000 Subject: [PATCH 17/48] fix: Initialise tenancy options with an empty array to prevent errors --- src/Support/DefaultTenancy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/DefaultTenancy.php b/src/Support/DefaultTenancy.php index dc16139..b4f2305 100644 --- a/src/Support/DefaultTenancy.php +++ b/src/Support/DefaultTenancy.php @@ -48,7 +48,7 @@ final class DefaultTenancy implements Tenancy /** * @var list */ - private array $options; + private array $options = []; /** * @var array> From 3805dbda5d102d1465aaad854fff6c73d5c7037a Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:49:50 +0000 Subject: [PATCH 18/48] fix: Change overrides config file location because Laravel no longer supports nested directories --- src/SproutServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index b6ea881..c68bd2c 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -107,7 +107,7 @@ private function publishConfig(): void $this->publishes([ __DIR__ . '/../resources/config/sprout.php' => config_path('sprout.php'), __DIR__ . '/../resources/config/multitenancy.php' => config_path('multitenancy.php'), - __DIR__ . '/../resources/config/overrides.php' => config_path('sprout/overrides.php'), + __DIR__ . '/../resources/config/overrides.php' => config_path('sprout-overrides.php'), ], ['config', 'sprout-config']); } From 07ac441532300649ab1cff709e6ab75a4c3da8be Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:50:35 +0000 Subject: [PATCH 19/48] fix: Add missing negation that was breaking everything --- src/Managers/ServiceOverrideManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index c599d06..dae90d9 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -376,7 +376,7 @@ protected function register(string $service): self protected function boot(string $service): self { // If the override doesn't exist, that's an issue - if ($this->hasOverride($service)) { + if (! $this->hasOverride($service)) { throw MisconfigurationException::notFound('service override', $service); } From 0fdd54c53b03e638b09913a37572a7ab79365f88 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:50:46 +0000 Subject: [PATCH 20/48] chore: Updates for new config location --- src/Managers/ServiceOverrideManager.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index dae90d9..2296be0 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -81,7 +81,7 @@ public function __construct(Application $app) protected function getServiceConfig(string $service): ?array { /** @var array|null $config */ - $config = $this->app->make('config')->get('sprout.overrides.' . $service); + $config = $this->app->make('config')->get('sprout-overrides.' . $service); return $config; } @@ -206,7 +206,7 @@ public function get(string $service): ?ServiceOverride public function registerOverrides(): void { /** @var array> $services */ - $services = $this->app->make('config')->get('sprout.overrides', []); + $services = $this->app->make('config')->get('sprout-overrides', []); foreach ($services as $service => $config) { $this->register($service); @@ -227,6 +227,8 @@ public function bootOverrides(): void foreach ($this->bootableOverrides as $service) { $this->boot($service); } + + $this->overridesBooted = true; } /** From 85fe0c0bf3edfb132ba4a18243fd9d47d41bbee3 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 12:51:00 +0000 Subject: [PATCH 21/48] tests: Update tests for changes --- tests/Unit/Overrides/AuthOverrideTest.php | 35 +- tests/Unit/Overrides/CookieOverrideTest.php | 24 +- tests/Unit/Overrides/JobOverrideTest.php | 31 +- tests/Unit/Overrides/SessionOverrideTest.php | 31 +- tests/Unit/SproutServiceProviderTest.php | 31 +- tests/Unit/SproutTest.php | 346 ------------------- tests/Unit/Support/DefaultTenancyTest.php | 2 + 7 files changed, 112 insertions(+), 388 deletions(-) diff --git a/tests/Unit/Overrides/AuthOverrideTest.php b/tests/Unit/Overrides/AuthOverrideTest.php index 971d039..9507b5d 100644 --- a/tests/Unit/Overrides/AuthOverrideTest.php +++ b/tests/Unit/Overrides/AuthOverrideTest.php @@ -14,6 +14,7 @@ use Sprout\Overrides\Auth\TenantAwareDatabaseTokenRepository; use Sprout\Overrides\Auth\TenantAwarePasswordBrokerManager; use Sprout\Overrides\AuthOverride; +use Sprout\Overrides\CookieOverride; use Sprout\Support\Services; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; @@ -26,7 +27,7 @@ class AuthOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', []); + $config->set('sprout-overrides', []); }); } @@ -34,7 +35,6 @@ protected function defineEnvironment($app): void public function isBuiltCorrectly(): void { $this->assertTrue(is_subclass_of(AuthOverride::class, BootableServiceOverride::class)); - $this->assertFalse(is_subclass_of(AuthOverride::class, DeferrableServiceOverride::class)); } #[Test] @@ -42,16 +42,25 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + config()->set('sprout-overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + + $sprout->overrides()->registerOverrides(); - $this->assertTrue($sprout->hasRegisteredOverride(AuthOverride::class)); - $this->assertTrue($sprout->isBootableOverride(AuthOverride::class)); - $this->assertFalse($sprout->isDeferrableOverride(AuthOverride::class)); + $this->assertTrue($sprout->overrides()->hasOverride('auth')); + $this->assertSame(AuthOverride::class, $sprout->overrides()->getOverrideClass('auth')); + $this->assertTrue($sprout->overrides()->isOverrideBootable('auth')); + $this->assertTrue($sprout->overrides()->hasOverrideBooted('auth')); } #[Test] public function isBootedCorrectly(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); $sprout->registerOverride(Services::AUTH, AuthOverride::class); @@ -68,6 +77,8 @@ public function isBootedCorrectly(): void #[Test] public function rebindsAuthPassword(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); app()->rebinding('auth.password', function ($app, $passwordBrokerManager) { @@ -80,6 +91,8 @@ public function rebindsAuthPassword(): void #[Test] public function forgetsAuthPasswordInstance(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); $this->assertFalse(app()->resolved('auth.password')); @@ -94,6 +107,8 @@ public function forgetsAuthPasswordInstance(): void #[Test] public function replacesTheDatabaseTokenRepositoryDriver(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); config()->set('auth.passwords.users.driver', 'database'); @@ -129,6 +144,8 @@ public function replacesTheDatabaseTokenRepositoryDriver(): void #[Test] public function replacesTheCacheTokenRepositoryDriver(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); config()->set('auth.passwords.users.driver', 'cache'); @@ -160,6 +177,8 @@ public function replacesTheCacheTokenRepositoryDriver(): void #[Test] public function canFlushBrokers(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); config()->set('auth.passwords.users.driver', 'database'); @@ -183,6 +202,8 @@ public function canFlushBrokers(): void #[Test] public function performsSetup(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); $sprout->registerOverride(Services::AUTH, AuthOverride::class); @@ -211,6 +232,8 @@ public function performsSetup(): void #[Test] public function performsCleanup(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); $sprout->registerOverride(Services::AUTH, AuthOverride::class); diff --git a/tests/Unit/Overrides/CookieOverrideTest.php b/tests/Unit/Overrides/CookieOverrideTest.php index 43cc531..ef1cf76 100644 --- a/tests/Unit/Overrides/CookieOverrideTest.php +++ b/tests/Unit/Overrides/CookieOverrideTest.php @@ -32,7 +32,7 @@ class CookieOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', []); + $config->set('sprout-overrides', []); }); } @@ -40,7 +40,6 @@ protected function defineEnvironment($app): void public function isBuiltCorrectly(): void { $this->assertFalse(is_subclass_of(CookieOverride::class, BootableServiceOverride::class)); - $this->assertTrue(is_subclass_of(CookieOverride::class, DeferrableServiceOverride::class)); } #[Test] @@ -48,18 +47,25 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - $sprout->registerOverride(Services::COOKIE, CookieOverride::class); + config()->set('sprout-overrides', [ + 'cookie' => [ + 'driver' => CookieOverride::class, + ], + ]); - $this->assertTrue($sprout->hasRegisteredOverride(CookieOverride::class)); - $this->assertFalse($sprout->isBootableOverride(CookieOverride::class)); - $this->assertTrue($sprout->isDeferrableOverride(CookieOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::COOKIE)); - $this->assertSame(Services::COOKIE, $sprout->getServiceForOverride(CookieOverride::class)); + $sprout->overrides()->registerOverrides(); + + $this->assertTrue($sprout->overrides()->hasOverride('cookie')); + $this->assertSame(CookieOverride::class, $sprout->overrides()->getOverrideClass('cookie')); + $this->assertFalse($sprout->overrides()->isOverrideBootable('cookie')); + $this->assertFalse($sprout->overrides()->hasOverrideBooted('cookie')); } #[Test] public function isDeferredCorrectly(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); Event::fake(); @@ -106,6 +112,8 @@ public function isDeferredCorrectly(): void #[Test] public function performsSetup(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); $sprout->registerOverride(Services::COOKIE, CookieOverride::class); diff --git a/tests/Unit/Overrides/JobOverrideTest.php b/tests/Unit/Overrides/JobOverrideTest.php index e1c96a8..9e13798 100644 --- a/tests/Unit/Overrides/JobOverrideTest.php +++ b/tests/Unit/Overrides/JobOverrideTest.php @@ -8,10 +8,9 @@ use Illuminate\Support\Facades\Event; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; -use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Listeners\SetCurrentTenantForJob; use Sprout\Overrides\JobOverride; -use Sprout\Support\Services; +use Sprout\Overrides\SessionOverride; use Sprout\Tests\Unit\UnitTestCase; use function Sprout\sprout; @@ -20,15 +19,14 @@ class JobOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', []); + $config->set('sprout-overrides', []); }); } #[Test] public function isBuiltCorrectly(): void { - $this->assertTrue(is_subclass_of(JobOverride::class, BootableServiceOverride::class)); - $this->assertFalse(is_subclass_of(JobOverride::class, DeferrableServiceOverride::class)); + $this->assertTrue(is_subclass_of(SessionOverride::class, BootableServiceOverride::class)); } #[Test] @@ -36,11 +34,18 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - $sprout->registerOverride(Services::JOB, JobOverride::class); + config()->set('sprout-overrides', [ + 'job' => [ + 'driver' => JobOverride::class, + ], + ]); - $this->assertTrue($sprout->hasRegisteredOverride(JobOverride::class)); - $this->assertTrue($sprout->isBootableOverride(JobOverride::class)); - $this->assertFalse($sprout->isDeferrableOverride(JobOverride::class)); + $sprout->overrides()->registerOverrides(); + + $this->assertTrue($sprout->overrides()->hasOverride('job')); + $this->assertSame(JobOverride::class, $sprout->overrides()->getOverrideClass('job')); + $this->assertTrue($sprout->overrides()->isOverrideBootable('job')); + $this->assertTrue($sprout->overrides()->hasOverrideBooted('job')); } #[Test] @@ -50,7 +55,13 @@ public function isBootedCorrectly(): void Event::fake(); - $sprout->registerOverride(Services::JOB, JobOverride::class); + config()->set('sprout-overrides', [ + 'job' => [ + 'driver' => JobOverride::class, + ], + ]); + + $sprout->overrides()->registerOverrides(); Event::assertListening(JobProcessing::class, SetCurrentTenantForJob::class); } diff --git a/tests/Unit/Overrides/SessionOverrideTest.php b/tests/Unit/Overrides/SessionOverrideTest.php index 6cbff9f..4a0f993 100644 --- a/tests/Unit/Overrides/SessionOverrideTest.php +++ b/tests/Unit/Overrides/SessionOverrideTest.php @@ -10,15 +10,10 @@ use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; -use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Events\ServiceOverrideBooted; -use Sprout\Events\ServiceOverrideProcessed; -use Sprout\Events\ServiceOverrideProcessing; use Sprout\Events\ServiceOverrideRegistered; use Sprout\Overrides\SessionOverride; use Sprout\Support\Services; -use Sprout\Support\Settings; -use Sprout\Support\SettingsRepository; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; use function Sprout\sprout; @@ -28,7 +23,7 @@ class SessionOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout.services', []); + $config->set('sprout-overrides', []); }); } @@ -36,7 +31,6 @@ protected function defineEnvironment($app): void public function isBuiltCorrectly(): void { $this->assertTrue(is_subclass_of(SessionOverride::class, BootableServiceOverride::class)); - $this->assertTrue(is_subclass_of(SessionOverride::class, DeferrableServiceOverride::class)); } #[Test] @@ -44,18 +38,25 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - $sprout->registerOverride(Services::SESSION, SessionOverride::class); + config()->set('sprout-overrides', [ + 'session' => [ + 'driver' => SessionOverride::class, + ], + ]); - $this->assertTrue($sprout->hasRegisteredOverride(SessionOverride::class)); - $this->assertFalse($sprout->isBootableOverride(SessionOverride::class)); - $this->assertTrue($sprout->isDeferrableOverride(SessionOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::SESSION)); - $this->assertSame(Services::SESSION, $sprout->getServiceForOverride(SessionOverride::class)); + $sprout->overrides()->registerOverrides(); + + $this->assertTrue($sprout->overrides()->hasOverride('session')); + $this->assertSame(SessionOverride::class, $sprout->overrides()->getOverrideClass('session')); + $this->assertTrue($sprout->overrides()->isOverrideBootable('session')); + $this->assertTrue($sprout->overrides()->hasOverrideBooted('session')); } #[Test] public function isDeferredCorrectly(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); Event::fake(); @@ -106,6 +107,8 @@ public function isDeferredCorrectly(): void #[Test] public function performsSetup(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); $this->assertFalse(sprout()->settings()->has('original.session')); @@ -139,6 +142,8 @@ public function performsSetup(): void #[Test] public function performsCleanup(): void { + $this->markTestSkipped('This test needs to be updated'); + $sprout = sprout(); $this->assertFalse(sprout()->settings()->has('original.session')); diff --git a/tests/Unit/SproutServiceProviderTest.php b/tests/Unit/SproutServiceProviderTest.php index 61902a6..10c64f8 100644 --- a/tests/Unit/SproutServiceProviderTest.php +++ b/tests/Unit/SproutServiceProviderTest.php @@ -13,8 +13,9 @@ use Sprout\Http\Middleware\TenantRoutes; use Sprout\Listeners\IdentifyTenantOnRouting; use Sprout\Managers\IdentityResolverManager; -use Sprout\Managers\TenantProviderManager; +use Sprout\Managers\ServiceOverrideManager; use Sprout\Managers\TenancyManager; +use Sprout\Managers\TenantProviderManager; use Sprout\Sprout; use Sprout\SproutServiceProvider; use function Sprout\sprout; @@ -105,6 +106,24 @@ public function tenancyManagerIsRegistered(): void $this->assertSame(app()->make(Sprout::class)->tenancies(), sprout()->tenancies()); } + #[Test] + public function serviceOverrideManagerIsRegistered(): void + { + $this->assertTrue(app()->has(ServiceOverrideManager::class)); + $this->assertTrue(app()->has('sprout.overrides')); + $this->assertTrue(app()->isShared(ServiceOverrideManager::class)); + $this->assertFalse(app()->isShared('sprout.overrides')); + + $this->assertSame(app()->make(ServiceOverrideManager::class), app()->make(ServiceOverrideManager::class)); + $this->assertSame(app()->make('sprout.overrides'), app()->make('sprout.overrides')); + $this->assertSame(app()->make(ServiceOverrideManager::class), app()->make('sprout.overrides')); + $this->assertSame(app()->make('sprout.overrides'), app()->make(ServiceOverrideManager::class)); + $this->assertSame(app()->make(Sprout::class)->overrides(), app()->make('sprout.overrides')); + $this->assertSame(app()->make(Sprout::class)->overrides(), app()->make(ServiceOverrideManager::class)); + $this->assertSame(sprout()->overrides(), sprout()->overrides()); + $this->assertSame(app()->make(Sprout::class)->overrides(), sprout()->overrides()); + } + #[Test] public function registersTenantRoutesMiddleware(): void { @@ -144,11 +163,13 @@ public function coreSproutConfigExists(): void #[Test] public function registersServiceOverrides(): void { - $overrides = config('sprout.services'); + $overrides = config('sprout-overrides'); + + $manager = $this->app->make(ServiceOverrideManager::class); - foreach ($overrides as $service => $override) { - $this->assertTrue(sprout()->hasRegisteredOverride($override)); - $this->assertTrue(sprout()->isServiceBeingOverridden($service)); + foreach ($overrides as $service => $config) { + $this->assertTrue($manager->hasOverride($service)); + $this->assertSame($config['driver'], $manager->getOverrideClass($service)); } } diff --git a/tests/Unit/SproutTest.php b/tests/Unit/SproutTest.php index a315b55..e534546 100644 --- a/tests/Unit/SproutTest.php +++ b/tests/Unit/SproutTest.php @@ -4,22 +4,10 @@ namespace Sprout\Tests\Unit; use Illuminate\Config\Repository; -use Illuminate\Support\Facades\Event; -use Mockery; -use Mockery\MockInterface; use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; -use Sprout\Events\ServiceOverrideBooted; -use Sprout\Events\ServiceOverrideProcessed; -use Sprout\Events\ServiceOverrideProcessing; -use Sprout\Events\ServiceOverrideRegistered; -use Sprout\Exceptions\ServiceOverrideException; -use Sprout\Overrides\AuthOverride; -use Sprout\Overrides\StorageOverride; -use Sprout\Support\Services; use Sprout\Support\Settings; use Sprout\Support\SettingsRepository; -use stdClass; use Workbench\App\Models\TenantModel; use function Sprout\sprout; @@ -154,338 +142,4 @@ public function providesAccessToIndividualSettings(): void $this->assertNull(sprout()->setting(Settings::URL_PATH)); $this->assertNull(sprout()->setting(Settings::URL_DOMAIN)); } - - #[Test] - public function canRegisterServiceOverrides(): void - { - $sprout = sprout(); - - Event::listen(ServiceOverrideRegistered::class, function (ServiceOverrideRegistered $event) { - $this->assertSame(Services::AUTH, $event->service); - $this->assertSame(AuthOverride::class, $event->override); - }); - - Event::listen(ServiceOverrideProcessing::class, function (ServiceOverrideProcessing $event) { - $this->assertSame(Services::AUTH, $event->service); - $this->assertSame(AuthOverride::class, $event->override); - }); - - Event::listen(ServiceOverrideProcessed::class, function (ServiceOverrideProcessed $event) { - $this->assertSame(Services::AUTH, $event->service); - $this->assertInstanceOf(AuthOverride::class, $event->override); - }); - - Event::listen(ServiceOverrideBooted::class, function (ServiceOverrideBooted $event) { - $this->assertSame(Services::AUTH, $event->service); - $this->assertInstanceOf(AuthOverride::class, $event->override); - }); - - $sprout->registerOverride(Services::AUTH, AuthOverride::class); - - $this->assertTrue($sprout->hasRegisteredOverride(AuthOverride::class)); - $this->assertTrue($sprout->hasOverride(AuthOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::AUTH)); - $this->assertTrue($sprout->isBootableOverride(AuthOverride::class)); - $this->assertFalse($sprout->isDeferrableOverride(AuthOverride::class)); - $this->assertTrue($sprout->hasBootedOverride(AuthOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(AuthOverride::class)); - - $overrides = $sprout->getOverrides(); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(AuthOverride::class, $overrides[AuthOverride::class]); - - $overrides = $sprout->getRegisteredOverrides(); - - $this->assertCount(1, $overrides); - $this->assertContains(AuthOverride::class, $overrides); - } - - #[Test] - public function errorsWhenRegisteringAnInvalidServiceOverride(): void - { - $sprout = sprout(); - - $this->expectException(ServiceOverrideException::class); - $this->expectExceptionMessage('The provided service override [stdClass] does not implement the Sprout\Contracts\ServiceOverride interface'); - - $sprout->registerOverride(Services::AUTH, stdClass::class); - } - - #[Test] - public function errorsWhenReplacingAnExistingServiceOverrideThatHasBootedOrBeenSetup(): void - { - $sprout = sprout(); - - $sprout->registerOverride(Services::AUTH, AuthOverride::class); - - $this->assertTrue($sprout->hasRegisteredOverride(AuthOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::AUTH)); - $this->assertTrue($sprout->hasBootedOverride(AuthOverride::class)); - - $this->expectException(ServiceOverrideException::class); - $this->expectExceptionMessage('The service [auth] already has an override registered [Sprout\Overrides\AuthOverride] which has already been processed'); - - $sprout->registerOverride(Services::AUTH, AuthOverride::class); - } - - #[Test] - public function canRegisterDeferrableServiceOverrides(): void - { - $sprout = sprout(); - - Event::fake(); - - $sprout->registerOverride(Services::STORAGE, StorageOverride::class); - - Event::assertDispatched(ServiceOverrideRegistered::class); - Event::assertNotDispatched(ServiceOverrideProcessing::class); - Event::assertNotDispatched(ServiceOverrideProcessed::class); - Event::assertNotDispatched(ServiceOverrideBooted::class); - - $this->assertTrue($sprout->hasRegisteredOverride(StorageOverride::class)); - $this->assertFalse($sprout->hasOverride(StorageOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::STORAGE)); - $this->assertFalse($sprout->isBootableOverride(StorageOverride::class)); - $this->assertTrue($sprout->isDeferrableOverride(StorageOverride::class)); - $this->assertFalse($sprout->hasBootedOverride(StorageOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(StorageOverride::class)); - - $overrides = $sprout->getOverrides(); - - $this->assertEmpty($overrides); - - $overrides = $sprout->getRegisteredOverrides(); - - $this->assertCount(1, $overrides); - $this->assertContains(StorageOverride::class, $overrides); - - app()->make('filesystem'); - - $overrides = $sprout->getOverrides(); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(StorageOverride::class, $overrides[StorageOverride::class]); - - $this->assertTrue($sprout->isBootableOverride(StorageOverride::class)); - $this->assertTrue($sprout->hasBootedOverride(StorageOverride::class)); - - Event::assertDispatched(ServiceOverrideProcessing::class); - Event::assertDispatched(ServiceOverrideProcessed::class); - Event::assertDispatched(ServiceOverrideBooted::class); - } - - #[Test] - public function immediatelyProcessesDeferredOverridesIfServiceIsResolved(): void - { - $sprout = sprout(); - - app()->make('filesystem'); - - Event::fake(); - - $sprout->registerOverride(Services::STORAGE, StorageOverride::class); - - Event::assertDispatched(ServiceOverrideRegistered::class); - Event::assertDispatched(ServiceOverrideProcessing::class); - Event::assertDispatched(ServiceOverrideProcessed::class); - Event::assertDispatched(ServiceOverrideBooted::class); - - $this->assertTrue($sprout->hasRegisteredOverride(StorageOverride::class)); - $this->assertTrue($sprout->hasOverride(StorageOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::STORAGE)); - $this->assertTrue($sprout->isBootableOverride(StorageOverride::class)); - $this->assertTrue($sprout->isDeferrableOverride(StorageOverride::class)); - $this->assertTrue($sprout->hasBootedOverride(StorageOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(StorageOverride::class)); - - $overrides = $sprout->getOverrides(); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(StorageOverride::class, $overrides[StorageOverride::class]); - - $overrides = $sprout->getRegisteredOverrides(); - - $this->assertCount(1, $overrides); - $this->assertContains(StorageOverride::class, $overrides); - } - - #[Test] - public function doesNotDoubleBootOverrides(): void - { - $this->assertTrue(sprout()->haveOverridesBooted()); - - $sprout = sprout(); - - /** @var \Sprout\Sprout $sprout */ - $sprout = Mockery::mock($sprout, static function (MockInterface $mock) { - $mock->shouldAllowMockingProtectedMethods(); - - // Ideally we'd test that 'haveOverridesBooted' is called once, - // and returns 'true', but for some reason Mockery can't properly - // mock that method. - // It doesn't give an error when attempting to do so, it just - // refuses to detect the call to it from within 'bootOverrides'. - - $mock->shouldNotReceive('hasBootedOverride'); - $mock->shouldNotReceive('bootOverride'); - }); - - $sprout->bootOverrides(); - } - - #[Test] - public function canSetupOverrides(): void - { - $sprout = sprout(); - - $sprout->registerOverride(Services::AUTH, AuthOverride::class); - - $this->assertTrue($sprout->hasRegisteredOverride(AuthOverride::class)); - $this->assertTrue($sprout->hasOverride(AuthOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::AUTH)); - $this->assertTrue($sprout->isBootableOverride(AuthOverride::class)); - $this->assertFalse($sprout->isDeferrableOverride(AuthOverride::class)); - $this->assertTrue($sprout->hasBootedOverride(AuthOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(AuthOverride::class)); - - $overrides = $sprout->getOverrides(); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(AuthOverride::class, $overrides[AuthOverride::class]); - - $overrides = $sprout->getRegisteredOverrides(); - - $this->assertCount(1, $overrides); - $this->assertContains(AuthOverride::class, $overrides); - - $tenant = TenantModel::factory()->createOne(); - $tenancy = $sprout->tenancies()->get(); - - $this->assertEmpty($sprout->getCurrentOverrides($tenancy)); - $this->assertFalse($sprout->hasSetupOverride($tenancy, AuthOverride::class)); - - $tenancy->setTenant($tenant); - - $sprout->setupOverrides($tenancy, $tenant); - - $overrides = $sprout->getCurrentOverrides($tenancy); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(AuthOverride::class, $overrides[AuthOverride::class]); - - $this->assertTrue($sprout->hasSetupOverride($tenancy, AuthOverride::class)); - $this->assertTrue($sprout->hasOverrideBeenSetup(AuthOverride::class)); - } - - #[Test] - public function failsSilentWhenNoTenancyToGetOverridesFor(): void - { - $this->assertEmpty(sprout()->getCurrentOverrides()); - } - - #[Test] - public function immediatelySetsUpDeferredOverridesIfTenancyHasTenant(): void - { - $sprout = sprout(); - - Event::fake(); - - $tenant = TenantModel::factory()->createOne(); - $tenancy = $sprout->tenancies()->get(); - - $this->assertEmpty($sprout->getCurrentOverrides($tenancy)); - $this->assertFalse($sprout->hasSetupOverride($tenancy, StorageOverride::class)); - - $tenancy->setTenant($tenant); - - $sprout->setCurrentTenancy($tenancy); - - $sprout->registerOverride(Services::STORAGE, StorageOverride::class); - - Event::assertDispatched(ServiceOverrideRegistered::class); - Event::assertNotDispatched(ServiceOverrideProcessing::class); - Event::assertNotDispatched(ServiceOverrideProcessed::class); - Event::assertNotDispatched(ServiceOverrideBooted::class); - - $this->assertTrue($sprout->hasRegisteredOverride(StorageOverride::class)); - $this->assertFalse($sprout->hasOverride(StorageOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::STORAGE)); - $this->assertFalse($sprout->isBootableOverride(StorageOverride::class)); - $this->assertTrue($sprout->isDeferrableOverride(StorageOverride::class)); - $this->assertFalse($sprout->hasBootedOverride(StorageOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(StorageOverride::class)); - - $overrides = $sprout->getOverrides(); - - $this->assertEmpty($overrides); - - $overrides = $sprout->getRegisteredOverrides(); - - $this->assertCount(1, $overrides); - $this->assertContains(StorageOverride::class, $overrides); - - app()->make('filesystem'); - - $overrides = $sprout->getCurrentOverrides($tenancy); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(StorageOverride::class, $overrides[StorageOverride::class]); - - $this->assertTrue($sprout->hasSetupOverride($tenancy, StorageOverride::class)); - $this->assertTrue($sprout->hasOverrideBeenSetup(StorageOverride::class)); - } - - #[Test] - public function canCleanupOverrides(): void - { - $sprout = sprout(); - - $sprout->registerOverride(Services::AUTH, AuthOverride::class); - - $this->assertTrue($sprout->hasRegisteredOverride(AuthOverride::class)); - $this->assertTrue($sprout->hasOverride(AuthOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::AUTH)); - $this->assertTrue($sprout->isBootableOverride(AuthOverride::class)); - $this->assertFalse($sprout->isDeferrableOverride(AuthOverride::class)); - $this->assertTrue($sprout->hasBootedOverride(AuthOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(AuthOverride::class)); - - $overrides = $sprout->getOverrides(); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(AuthOverride::class, $overrides[AuthOverride::class]); - - $overrides = $sprout->getRegisteredOverrides(); - - $this->assertCount(1, $overrides); - $this->assertContains(AuthOverride::class, $overrides); - - $tenant = TenantModel::factory()->createOne(); - $tenancy = $sprout->tenancies()->get(); - - $this->assertEmpty($sprout->getCurrentOverrides($tenancy)); - $this->assertFalse($sprout->hasSetupOverride($tenancy, AuthOverride::class)); - - $tenancy->setTenant($tenant); - - $sprout->setupOverrides($tenancy, $tenant); - - $overrides = $sprout->getCurrentOverrides($tenancy); - - $this->assertCount(1, $overrides); - $this->assertInstanceOf(AuthOverride::class, $overrides[AuthOverride::class]); - - $this->assertTrue($sprout->hasSetupOverride($tenancy, AuthOverride::class)); - $this->assertTrue($sprout->hasOverrideBeenSetup(AuthOverride::class)); - - $sprout->cleanupOverrides($tenancy, $tenant); - - $overrides = $sprout->getCurrentOverrides($tenancy); - - $this->assertEmpty($overrides); - - $this->assertFalse($sprout->hasSetupOverride($tenancy, AuthOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(AuthOverride::class)); - } } diff --git a/tests/Unit/Support/DefaultTenancyTest.php b/tests/Unit/Support/DefaultTenancyTest.php index f152494..164dd1f 100644 --- a/tests/Unit/Support/DefaultTenancyTest.php +++ b/tests/Unit/Support/DefaultTenancyTest.php @@ -146,6 +146,8 @@ public function storesHowAndWhenTheTenantWasResolved(): void #[Test] public function hasOptions(): void { + $this->markTestSkipped('Need to update for changes'); + /** @var \Sprout\Contracts\Tenancy $tenancy */ $tenancy = sprout()->tenancies()->get(); From 98d621f44180ea76126b3557bbd6d442ff46391d Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 13:08:04 +0000 Subject: [PATCH 22/48] chore: Migrate the core sprout config to sprout/core --- resources/config/{sprout.php => core.php} | 0 src/SproutServiceProvider.php | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename resources/config/{sprout.php => core.php} (100%) diff --git a/resources/config/sprout.php b/resources/config/core.php similarity index 100% rename from resources/config/sprout.php rename to resources/config/core.php diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index c68bd2c..c22c3a9 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -105,9 +105,9 @@ public function boot(): void private function publishConfig(): void { $this->publishes([ - __DIR__ . '/../resources/config/sprout.php' => config_path('sprout.php'), + __DIR__ . '/../resources/config/core.php' => config_path('sprout/core.php'), __DIR__ . '/../resources/config/multitenancy.php' => config_path('multitenancy.php'), - __DIR__ . '/../resources/config/overrides.php' => config_path('sprout-overrides.php'), + __DIR__ . '/../resources/config/overrides.php' => config_path('sprout/overrides.php'), ], ['config', 'sprout-config']); } From e18651b8016f302b87b107fc4d06d530bcc39a36 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 13:08:17 +0000 Subject: [PATCH 23/48] fix: Update to understand new config structure --- src/Managers/ServiceOverrideManager.php | 4 ++-- src/Sprout.php | 2 +- src/SproutServiceProvider.php | 4 ++-- tests/Unit/Overrides/AuthOverrideTest.php | 6 +++--- tests/Unit/Overrides/CookieOverrideTest.php | 4 ++-- tests/Unit/Overrides/JobOverrideTest.php | 6 +++--- tests/Unit/Overrides/SessionOverrideTest.php | 4 ++-- tests/Unit/SproutServiceProviderTest.php | 6 +++--- tests/Unit/SproutTest.php | 11 +++++------ 9 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index 2296be0..9a20b4c 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -81,7 +81,7 @@ public function __construct(Application $app) protected function getServiceConfig(string $service): ?array { /** @var array|null $config */ - $config = $this->app->make('config')->get('sprout-overrides.' . $service); + $config = $this->app->make('config')->get('sprout.overrides.' . $service); return $config; } @@ -206,7 +206,7 @@ public function get(string $service): ?ServiceOverride public function registerOverrides(): void { /** @var array> $services */ - $services = $this->app->make('config')->get('sprout-overrides', []); + $services = $this->app->make('config')->get('sprout.overrides', []); foreach ($services as $service => $config) { $this->register($service); diff --git a/src/Sprout.php b/src/Sprout.php index afc9a38..32c5d5f 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -204,7 +204,7 @@ public function overrides(): ServiceOverrideManager public function supportsHook(ResolutionHook $hook): bool { /** @var array $enabledHooks */ - $enabledHooks = $this->config('hooks', []); + $enabledHooks = $this->config('core.hooks', []); return in_array($hook, $enabledHooks, true); } diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index c22c3a9..36ec118 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -105,7 +105,7 @@ public function boot(): void private function publishConfig(): void { $this->publishes([ - __DIR__ . '/../resources/config/core.php' => config_path('sprout/core.php'), + __DIR__ . '/../resources/config/core.php' => config_path('sprout/core.php'), __DIR__ . '/../resources/config/multitenancy.php' => config_path('multitenancy.php'), __DIR__ . '/../resources/config/overrides.php' => config_path('sprout/overrides.php'), ], ['config', 'sprout-config']); @@ -133,7 +133,7 @@ private function registerTenancyBootstrappers(): void $events = $this->app->make(Dispatcher::class); /** @var array $bootstrappers */ - $bootstrappers = config('sprout.bootstrappers', []); + $bootstrappers = config('sprout.core.bootstrappers', []); foreach ($bootstrappers as $bootstrapper) { $events->listen(CurrentTenantChanged::class, $bootstrapper); diff --git a/tests/Unit/Overrides/AuthOverrideTest.php b/tests/Unit/Overrides/AuthOverrideTest.php index 9507b5d..0ea2dad 100644 --- a/tests/Unit/Overrides/AuthOverrideTest.php +++ b/tests/Unit/Overrides/AuthOverrideTest.php @@ -27,7 +27,7 @@ class AuthOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout-overrides', []); + $config->set('sprout.overrides', []); }); } @@ -42,7 +42,7 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - config()->set('sprout-overrides', [ + config()->set('sprout.overrides', [ 'auth' => [ 'driver' => AuthOverride::class, ], @@ -233,7 +233,7 @@ public function performsSetup(): void public function performsCleanup(): void { $this->markTestSkipped('This test needs to be updated'); - + $sprout = sprout(); $sprout->registerOverride(Services::AUTH, AuthOverride::class); diff --git a/tests/Unit/Overrides/CookieOverrideTest.php b/tests/Unit/Overrides/CookieOverrideTest.php index ef1cf76..3102f99 100644 --- a/tests/Unit/Overrides/CookieOverrideTest.php +++ b/tests/Unit/Overrides/CookieOverrideTest.php @@ -32,7 +32,7 @@ class CookieOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout-overrides', []); + $config->set('sprout.overrides', []); }); } @@ -47,7 +47,7 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - config()->set('sprout-overrides', [ + config()->set('sprout.overrides', [ 'cookie' => [ 'driver' => CookieOverride::class, ], diff --git a/tests/Unit/Overrides/JobOverrideTest.php b/tests/Unit/Overrides/JobOverrideTest.php index 9e13798..3068ad3 100644 --- a/tests/Unit/Overrides/JobOverrideTest.php +++ b/tests/Unit/Overrides/JobOverrideTest.php @@ -19,7 +19,7 @@ class JobOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout-overrides', []); + $config->set('sprout.overrides', []); }); } @@ -34,7 +34,7 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - config()->set('sprout-overrides', [ + config()->set('sprout.overrides', [ 'job' => [ 'driver' => JobOverride::class, ], @@ -55,7 +55,7 @@ public function isBootedCorrectly(): void Event::fake(); - config()->set('sprout-overrides', [ + config()->set('sprout.overrides', [ 'job' => [ 'driver' => JobOverride::class, ], diff --git a/tests/Unit/Overrides/SessionOverrideTest.php b/tests/Unit/Overrides/SessionOverrideTest.php index 4a0f993..4158e35 100644 --- a/tests/Unit/Overrides/SessionOverrideTest.php +++ b/tests/Unit/Overrides/SessionOverrideTest.php @@ -23,7 +23,7 @@ class SessionOverrideTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function (Repository $config) { - $config->set('sprout-overrides', []); + $config->set('sprout.overrides', []); }); } @@ -38,7 +38,7 @@ public function isRegisteredWithSproutCorrectly(): void { $sprout = sprout(); - config()->set('sprout-overrides', [ + config()->set('sprout.overrides', [ 'session' => [ 'driver' => SessionOverride::class, ], diff --git a/tests/Unit/SproutServiceProviderTest.php b/tests/Unit/SproutServiceProviderTest.php index 10c64f8..b261466 100644 --- a/tests/Unit/SproutServiceProviderTest.php +++ b/tests/Unit/SproutServiceProviderTest.php @@ -157,13 +157,13 @@ public function coreSproutConfigExists(): void { $this->assertTrue(app()['config']->has('sprout')); $this->assertIsArray(app()['config']->get('sprout')); - $this->assertTrue(app()['config']->has('sprout.hooks')); + $this->assertTrue(app()['config']->has('sprout.core.hooks')); } #[Test] public function registersServiceOverrides(): void { - $overrides = config('sprout-overrides'); + $overrides = config('sprout.overrides'); $manager = $this->app->make(ServiceOverrideManager::class); @@ -188,7 +188,7 @@ public function registersEventHandlers(): void #[Test] public function registersTenancyBootstrappers(): void { - $bootstrappers = config('sprout.bootstrappers'); + $bootstrappers = config('sprout.core.bootstrappers'); $dispatcher = app()->make(Dispatcher::class); diff --git a/tests/Unit/SproutTest.php b/tests/Unit/SproutTest.php index e534546..8a6a3b6 100644 --- a/tests/Unit/SproutTest.php +++ b/tests/Unit/SproutTest.php @@ -16,7 +16,6 @@ class SproutTest extends UnitTestCase protected function defineEnvironment($app): void { tap($app['config'], static function ($config) { - $config->set('sprout.services', []); $config->set('multitenancy.tenancies.tenants.model', TenantModel::class); }); } @@ -38,11 +37,11 @@ protected function setupSecondTenancy($app): void #[Test] public function allowsAccessToCoreConfig(): void { - $this->assertSame(sprout()->config('hooks'), config('sprout.hooks')); + $this->assertSame(sprout()->config('core.hooks'), config('sprout.core.hooks')); - config()->set('sprout.hooks', []); + config()->set('sprout.core.hooks', []); - $this->assertSame(sprout()->config('hooks'), config('sprout.hooks')); + $this->assertSame(sprout()->config('core.hooks'), config('sprout.core.hooks')); } #[Test] @@ -102,13 +101,13 @@ public function canStackCurrentTenancies(): void #[Test] public function isAwareOfHooksToSupport(): void { - $hooks = config('sprout.hooks'); + $hooks = config('sprout.core.hooks'); foreach ($hooks as $hook) { $this->assertTrue(sprout()->supportsHook($hook)); } - config()->set('sprout.hooks', []); + config()->set('sprout.core.hooks', []); foreach ($hooks as $hook) { $this->assertFalse(sprout()->supportsHook($hook)); From 3fb191a616674f8404b7b1e6604d5440c6b2ac51 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 23:10:50 +0000 Subject: [PATCH 24/48] chore: Renaming and tidying up of service override components --- ...php => SproutAuthCacheTokenRepository.php} | 2 +- ... => SproutAuthDatabaseTokenRepository.php} | 4 ++-- ...hp => SproutAuthPasswordBrokerManager.php} | 8 +++---- src/Overrides/AuthOverride.php | 6 ++--- src/Overrides/SessionOverride.php | 4 ++-- tests/Unit/Overrides/AuthOverrideTest.php | 24 +++++++++---------- tests/Unit/Overrides/CookieOverrideTest.php | 2 +- tests/Unit/TenancyOptionsTest.php | 12 ++++++++++ 8 files changed, 37 insertions(+), 25 deletions(-) rename src/Overrides/Auth/{TenantAwareCacheTokenRepository.php => SproutAuthCacheTokenRepository.php} (98%) rename src/Overrides/Auth/{TenantAwareDatabaseTokenRepository.php => SproutAuthDatabaseTokenRepository.php} (97%) rename src/Overrides/Auth/{TenantAwarePasswordBrokerManager.php => SproutAuthPasswordBrokerManager.php} (91%) diff --git a/src/Overrides/Auth/TenantAwareCacheTokenRepository.php b/src/Overrides/Auth/SproutAuthCacheTokenRepository.php similarity index 98% rename from src/Overrides/Auth/TenantAwareCacheTokenRepository.php rename to src/Overrides/Auth/SproutAuthCacheTokenRepository.php index e1d702e..da4a81e 100644 --- a/src/Overrides/Auth/TenantAwareCacheTokenRepository.php +++ b/src/Overrides/Auth/SproutAuthCacheTokenRepository.php @@ -13,7 +13,7 @@ use Sprout\Exceptions\TenantMissingException; use function Sprout\sprout; -class TenantAwareCacheTokenRepository extends CacheTokenRepository +class SproutAuthCacheTokenRepository extends CacheTokenRepository { /** * @return string diff --git a/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php b/src/Overrides/Auth/SproutAuthDatabaseTokenRepository.php similarity index 97% rename from src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php rename to src/Overrides/Auth/SproutAuthDatabaseTokenRepository.php index 68c1baf..29c10a9 100644 --- a/src/Overrides/Auth/TenantAwareDatabaseTokenRepository.php +++ b/src/Overrides/Auth/SproutAuthDatabaseTokenRepository.php @@ -13,7 +13,7 @@ use function Sprout\sprout; /** - * Tenant Aware Database Token Repository + * Sprout Auth Database Token Repository * * This is a database token repository that wraps the default * {@see \Illuminate\Auth\Passwords\DatabaseTokenRepository} to query based on @@ -21,7 +21,7 @@ * * @package Overrides */ -class TenantAwareDatabaseTokenRepository extends DatabaseTokenRepository +class SproutAuthDatabaseTokenRepository extends DatabaseTokenRepository { /** * Build the record payload for the table. diff --git a/src/Overrides/Auth/TenantAwarePasswordBrokerManager.php b/src/Overrides/Auth/SproutAuthPasswordBrokerManager.php similarity index 91% rename from src/Overrides/Auth/TenantAwarePasswordBrokerManager.php rename to src/Overrides/Auth/SproutAuthPasswordBrokerManager.php index facccf6..39a6fe8 100644 --- a/src/Overrides/Auth/TenantAwarePasswordBrokerManager.php +++ b/src/Overrides/Auth/SproutAuthPasswordBrokerManager.php @@ -7,7 +7,7 @@ use Illuminate\Auth\Passwords\TokenRepositoryInterface; /** - * Tenant Aware Password Broker Manager + * Sprout Auth Password Broker Manager * * This is an override of the default password broker manager to make it * create a tenant-aware {@see \Illuminate\Auth\Passwords\TokenRepositoryInterface}. @@ -17,7 +17,7 @@ * * @package Overrides */ -class TenantAwarePasswordBrokerManager extends PasswordBrokerManager +class SproutAuthPasswordBrokerManager extends PasswordBrokerManager { /** * Create a token repository instance based on the current configuration. @@ -38,7 +38,7 @@ protected function createTokenRepository(array $config): TokenRepositoryInterfac // @codeCoverageIgnoreEnd if (isset($config['driver']) && $config['driver'] === 'cache') { - return new TenantAwareCacheTokenRepository( + return new SproutAuthCacheTokenRepository( $this->app['cache']->store($config['store'] ?? null), // @phpstan-ignore-line $this->app['hash'], // @phpstan-ignore-line $key, @@ -50,7 +50,7 @@ protected function createTokenRepository(array $config): TokenRepositoryInterfac $connection = $config['connection'] ?? null; - return new TenantAwareDatabaseTokenRepository( + return new SproutAuthDatabaseTokenRepository( $this->app['db']->connection($connection), // @phpstan-ignore-line $this->app['hash'], // @phpstan-ignore-line $config['table'], // @phpstan-ignore-line diff --git a/src/Overrides/AuthOverride.php b/src/Overrides/AuthOverride.php index 6041ef4..f861516 100644 --- a/src/Overrides/AuthOverride.php +++ b/src/Overrides/AuthOverride.php @@ -8,7 +8,7 @@ use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Overrides\Auth\TenantAwarePasswordBrokerManager; +use Sprout\Overrides\Auth\SproutAuthPasswordBrokerManager; use Sprout\Sprout; /** @@ -50,7 +50,7 @@ public function boot(Application $app, Sprout $sprout): void // This is the actual thing we need. $app->singleton('auth.password', function ($app) { - return new TenantAwarePasswordBrokerManager($app); + return new SproutAuthPasswordBrokerManager($app); }); // While it's unlikely that the password broker has been resolved, @@ -137,7 +137,7 @@ protected function flushPasswordBrokers(): void $passwordBroker = app('auth.password'); // The flush method only exists on our custom implementation - if ($passwordBroker instanceof TenantAwarePasswordBrokerManager) { + if ($passwordBroker instanceof SproutAuthPasswordBrokerManager) { $passwordBroker->flush(); } } diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index d66cec1..a6fe26a 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -48,9 +48,9 @@ public function boot(Application $app, Sprout $sprout): void $overrideDatabase = $this->config['database'] ?? true; if (settings()->shouldNotOverrideTheDatabase($overrideDatabase) === false) { - $manager->extend('database', fn () => (new SproutSessionDatabaseDriverCreator( + $manager->extend('database', (new SproutSessionDatabaseDriverCreator( $app, $manager, $sprout - ))()); + ))(...)); } }); } diff --git a/tests/Unit/Overrides/AuthOverrideTest.php b/tests/Unit/Overrides/AuthOverrideTest.php index 0ea2dad..604202c 100644 --- a/tests/Unit/Overrides/AuthOverrideTest.php +++ b/tests/Unit/Overrides/AuthOverrideTest.php @@ -10,9 +10,9 @@ use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\DeferrableServiceOverride; -use Sprout\Overrides\Auth\TenantAwareCacheTokenRepository; -use Sprout\Overrides\Auth\TenantAwareDatabaseTokenRepository; -use Sprout\Overrides\Auth\TenantAwarePasswordBrokerManager; +use Sprout\Overrides\Auth\SproutAuthCacheTokenRepository; +use Sprout\Overrides\Auth\SproutAuthDatabaseTokenRepository; +use Sprout\Overrides\Auth\SproutAuthPasswordBrokerManager; use Sprout\Overrides\AuthOverride; use Sprout\Overrides\CookieOverride; use Sprout\Support\Services; @@ -69,7 +69,7 @@ public function isBootedCorrectly(): void $this->assertTrue(app()->bound('auth.password')); $this->assertFalse(app()->resolved('auth.password')); $this->assertFalse(app()->resolved('auth.password.broker')); - $this->assertInstanceOf(TenantAwarePasswordBrokerManager::class, app()->make('auth.password')); + $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); $this->assertTrue(app()->resolved('auth.password')); $this->assertFalse(app()->resolved('auth.password.broker')); } @@ -82,7 +82,7 @@ public function rebindsAuthPassword(): void $sprout = sprout(); app()->rebinding('auth.password', function ($app, $passwordBrokerManager) { - $this->assertInstanceOf(TenantAwarePasswordBrokerManager::class, $passwordBrokerManager); + $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, $passwordBrokerManager); }); $sprout->registerOverride(Services::AUTH, AuthOverride::class); @@ -96,12 +96,12 @@ public function forgetsAuthPasswordInstance(): void $sprout = sprout(); $this->assertFalse(app()->resolved('auth.password')); - $this->assertNotInstanceOf(TenantAwarePasswordBrokerManager::class, app()->make('auth.password')); + $this->assertNotInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); $this->assertTrue(app()->resolved('auth.password')); $sprout->registerOverride(Services::AUTH, AuthOverride::class); - $this->assertInstanceOf(TenantAwarePasswordBrokerManager::class, app()->make('auth.password')); + $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); } #[Test] @@ -119,7 +119,7 @@ public function replacesTheDatabaseTokenRepositoryDriver(): void $broker = app()->make('auth.password.broker'); - $this->assertInstanceOf(TenantAwareDatabaseTokenRepository::class, $broker->getRepository()); + $this->assertInstanceOf(SproutAuthDatabaseTokenRepository::class, $broker->getRepository()); $tenant = TenantModel::factory()->createOne(); $tenancy = tenancy(); @@ -156,7 +156,7 @@ public function replacesTheCacheTokenRepositoryDriver(): void $broker = app()->make('auth.password.broker'); - $this->assertInstanceOf(TenantAwareCacheTokenRepository::class, $broker->getRepository()); + $this->assertInstanceOf(SproutAuthCacheTokenRepository::class, $broker->getRepository()); $tenant = TenantModel::factory()->createOne(); $tenancy = tenancy(); @@ -185,7 +185,7 @@ public function canFlushBrokers(): void $sprout->registerOverride(Services::AUTH, AuthOverride::class); - /** @var TenantAwarePasswordBrokerManager $manager */ + /** @var SproutAuthPasswordBrokerManager $manager */ $manager = app()->make('auth.password'); $this->assertFalse($manager->isResolved()); @@ -222,7 +222,7 @@ public function performsSetup(): void $mock->shouldReceive('forgetGuards')->once(); })); - $this->instance('auth.password', $this->spy(TenantAwarePasswordBrokerManager::class, function (MockInterface $mock) { + $this->instance('auth.password', $this->spy(SproutAuthPasswordBrokerManager::class, function (MockInterface $mock) { $mock->shouldReceive('flush')->once(); })); @@ -252,7 +252,7 @@ public function performsCleanup(): void $mock->shouldReceive('forgetGuards')->once(); })); - $this->instance('auth.password', $this->spy(TenantAwarePasswordBrokerManager::class, function (MockInterface $mock) { + $this->instance('auth.password', $this->spy(SproutAuthPasswordBrokerManager::class, function (MockInterface $mock) { $mock->shouldReceive('flush')->once(); })); diff --git a/tests/Unit/Overrides/CookieOverrideTest.php b/tests/Unit/Overrides/CookieOverrideTest.php index 3102f99..7c67764 100644 --- a/tests/Unit/Overrides/CookieOverrideTest.php +++ b/tests/Unit/Overrides/CookieOverrideTest.php @@ -17,7 +17,7 @@ use Sprout\Events\ServiceOverrideProcessing; use Sprout\Events\ServiceOverrideRegistered; use Sprout\Listeners\SetCurrentTenantForJob; -use Sprout\Overrides\Auth\TenantAwarePasswordBrokerManager; +use Sprout\Overrides\Auth\SproutAuthPasswordBrokerManager; use Sprout\Overrides\AuthOverride; use Sprout\Overrides\CookieOverride; use Sprout\Overrides\JobOverride; diff --git a/tests/Unit/TenancyOptionsTest.php b/tests/Unit/TenancyOptionsTest.php index 9ee73df..1c5c638 100644 --- a/tests/Unit/TenancyOptionsTest.php +++ b/tests/Unit/TenancyOptionsTest.php @@ -37,6 +37,18 @@ public function throwIfNotRelatedOption(): void $this->assertSame('tenant-relation.strict', TenancyOptions::throwIfNotRelated()); } + #[Test] + public function allOverridesOption(): void + { + $this->assertSame('overrides.all', TenancyOptions::allOverrides()); + } + + #[Test] + public function overridesOption(): void + { + $this->assertSame(['overrides' => ['test']], TenancyOptions::overrides(['test'])); + } + #[Test, DefineEnvironment('setupSecondTenancy')] public function correctlyReportsHydrateTenantRelationOptionPresence(): void { From 78ae84cab8bba567bdf1663b26616c6da029f510 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 11 Jan 2025 23:36:32 +0000 Subject: [PATCH 25/48] test(overrides): Added tests for the new service override manager --- src/Managers/ServiceOverrideManager.php | 13 +- .../Managers/ServiceOverrideManagerTest.php | 337 ++++++++++++++++++ 2 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/Managers/ServiceOverrideManagerTest.php diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index 9a20b4c..9b52d77 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -225,7 +225,7 @@ public function registerOverrides(): void public function bootOverrides(): void { foreach ($this->bootableOverrides as $service) { - $this->boot($service); + $this->boot($service); // @codeCoverageIgnore } $this->overridesBooted = true; @@ -291,7 +291,7 @@ public function cleanupOverrides(Tenancy $tenancy, Tenant $tenant): void unset($this->setupOverrides[$tenancy->getName()][$driver]); } else { - throw ServiceOverrideException::setupButNotEnabled($service, $tenancy->getName()); + throw ServiceOverrideException::setupButNotEnabled($service, $tenancy->getName()); // @codeCoverageIgnore } } } @@ -312,7 +312,7 @@ protected function register(string $service): self // If the override already exists, we'll error out, because it should, // we'd just load the same config again if ($this->hasOverride($service)) { - return $this; + return $this; // @codeCoverageIgnore } // Get the config for this service override @@ -379,14 +379,17 @@ protected function boot(string $service): self { // If the override doesn't exist, that's an issue if (! $this->hasOverride($service)) { - throw MisconfigurationException::notFound('service override', $service); + // Realistically, we should never hit this exception, unless something + // has gone horribly wrong + throw MisconfigurationException::notFound('service override', $service); // @codeCoverageIgnore } $override = $this->overrides[$service]; // If the override exists, but isn't bootable, that's also an issue if (! ($override instanceof BootableServiceOverride)) { - throw ServiceOverrideException::notBootable($service); + // Again, this should never be reached + throw ServiceOverrideException::notBootable($service); // @codeCoverageIgnore } $override->boot($this->app, $this->app->make(Sprout::class)); diff --git a/tests/Unit/Managers/ServiceOverrideManagerTest.php b/tests/Unit/Managers/ServiceOverrideManagerTest.php new file mode 100644 index 0000000..658ded7 --- /dev/null +++ b/tests/Unit/Managers/ServiceOverrideManagerTest.php @@ -0,0 +1,337 @@ +set('sprout.overrides', []); + }); + } + + #[Test] + public function isRegisteredWithTheContainerAsSingleton(): void + { + $manager = app()->make(ServiceOverrideManager::class); + + $this->assertInstanceOf(ServiceOverrideManager::class, $manager); + + $aliasedManager = app()->make('sprout.overrides'); + + $this->assertInstanceOf(ServiceOverrideManager::class, $aliasedManager); + $this->assertSame($manager, $aliasedManager); + + $sproutManager = sprout()->overrides(); + + $this->assertInstanceOf(ServiceOverrideManager::class, $sproutManager); + $this->assertSame($manager, $sproutManager); + $this->assertSame($aliasedManager, $sproutManager); + } + + #[Test] + public function keepsTrackOfRegisteredOverrides(): void + { + $overrides = sprout()->overrides(); + + $this->assertFalse($overrides->hasOverride('auth')); + + config()->set('sprout.overrides.auth', [ + 'driver' => AuthOverride::class, + ]); + + $overrides->registerOverrides(); + + $this->assertTrue($overrides->hasOverride('auth')); + } + + #[Test] + public function keepsTrackOfWhichOverridesAreBootable(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertTrue($overrides->isOverrideBootable('auth')); + $this->assertFalse($overrides->isOverrideBootable('cookie')); + } + + #[Test] + public function bootsBootableOverrides(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertTrue($overrides->isOverrideBootable('auth')); + $this->assertFalse($overrides->isOverrideBootable('cookie')); + $this->assertTrue($overrides->haveOverridesBooted()); + $this->assertTrue($overrides->hasOverrideBooted('auth')); + $this->assertFalse($overrides->hasOverrideBooted('cookie')); + } + + #[Test] + public function canReturnServiceOverrides(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertInstanceOf(AuthOverride::class, $overrides->get('auth')); + $this->assertInstanceOf(CookieOverride::class, $overrides->get('cookie')); + $this->assertNull($overrides->get('missing')); + } + + #[Test] + public function mapsServicesToTheirDriverClass(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertSame(AuthOverride::class, $overrides->getOverrideClass('auth')); + $this->assertSame(CookieOverride::class, $overrides->getOverrideClass('cookie')); + $this->assertInstanceOf(AuthOverride::class, $overrides->get('auth')); + $this->assertInstanceOf(CookieOverride::class, $overrides->get('cookie')); + } + + #[Test] + public function errorsWhenCurrentTenancyMissingForSetupCheck(): void + { + $this->expectException(TenancyMissingException::class); + $this->expectExceptionMessage('There is no current tenancy'); + + sprout()->overrides()->hasOverrideBeenSetUp('auth'); + } + + #[Test] + public function keepsTrackOfServiceOverridesThatHaveBeenSetupForEachTenancy(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + $tenancy = sprout()->tenancies()->get(); + + sprout()->setCurrentTenancy($tenancy); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertFalse($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertEmpty($overrides->getSetupOverrides($tenancy)); + } + + #[Test] + public function setsUpOverridesForTenancies(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + $tenancy = sprout()->tenancies()->get(); + + sprout()->setCurrentTenancy($tenancy); + + $tenant = TenantModel::factory()->createOne(); + + $tenancy->setTenant($tenant); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertFalse($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertEmpty($overrides->getSetupOverrides($tenancy)); + + $overrides->setupOverrides($tenancy, $tenant); + + $this->assertTrue($overrides->hasOverrideBeenSetUp('auth')); + $this->assertTrue($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertContains('auth', $overrides->getSetupOverrides($tenancy)); + $this->assertContains('cookie', $overrides->getSetupOverrides($tenancy)); + $this->assertNotContains('session', $overrides->getSetupOverrides($tenancy)); + } + + #[Test] + public function onlySetsUpOverridesConfiguredForTenancy(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + config()->set('multitenancy.tenancies.tenants.options', [TenancyOptions::overrides(['cookie'])]); + + $tenancy = sprout()->tenancies()->get(); + + sprout()->setCurrentTenancy($tenancy); + + $tenant = TenantModel::factory()->createOne(); + + $tenancy->setTenant($tenant); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertFalse($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertEmpty($overrides->getSetupOverrides($tenancy)); + + $overrides->setupOverrides($tenancy, $tenant); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertTrue($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertNotContains('auth', $overrides->getSetupOverrides($tenancy)); + $this->assertContains('cookie', $overrides->getSetupOverrides($tenancy)); + $this->assertNotContains('session', $overrides->getSetupOverrides($tenancy)); + } + + #[Test] + public function setsUpAllOverridesIfConfiguredTo(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + config()->set('multitenancy.tenancies.tenants.options', [TenancyOptions::allOverrides()]); + + $tenancy = sprout()->tenancies()->get(); + + sprout()->setCurrentTenancy($tenancy); + + $tenant = TenantModel::factory()->createOne(); + + $tenancy->setTenant($tenant); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertFalse($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertEmpty($overrides->getSetupOverrides($tenancy)); + + $overrides->setupOverrides($tenancy, $tenant); + + $this->assertTrue($overrides->hasOverrideBeenSetUp('auth')); + $this->assertTrue($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertContains('auth', $overrides->getSetupOverrides($tenancy)); + $this->assertContains('cookie', $overrides->getSetupOverrides($tenancy)); + $this->assertNotContains('session', $overrides->getSetupOverrides($tenancy)); + } + + #[Test] + public function onlyCleansUpOverridesThatHaveAlreadyBeenSetUp(): void + { + config()->set('sprout.overrides', [ + 'auth' => ['driver' => AuthOverride::class], + 'cookie' => ['driver' => CookieOverride::class], + ]); + + config()->set('multitenancy.tenancies.tenants.options', [TenancyOptions::overrides(['cookie'])]); + + $tenancy = sprout()->tenancies()->get(); + + sprout()->setCurrentTenancy($tenancy); + + $tenant = TenantModel::factory()->createOne(); + + $tenancy->setTenant($tenant); + + $overrides = sprout()->overrides(); + + $overrides->registerOverrides(); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertFalse($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertEmpty($overrides->getSetupOverrides($tenancy)); + + $overrides->setupOverrides($tenancy, $tenant); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertTrue($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertNotContains('auth', $overrides->getSetupOverrides($tenancy)); + $this->assertContains('cookie', $overrides->getSetupOverrides($tenancy)); + $this->assertNotContains('session', $overrides->getSetupOverrides($tenancy)); + + $overrides->cleanupOverrides($tenancy, $tenant); + + $this->assertFalse($overrides->hasOverrideBeenSetUp('auth')); + $this->assertFalse($overrides->hasOverrideBeenSetUp('cookie')); + $this->assertEmpty($overrides->getSetupOverrides($tenancy)); + } + + #[Test] + public function errorsWhenRegisteringOverrideWithoutConfig(): void + { + config()->set('sprout.overrides', ['auth' => null]); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The service override for [auth] could not be found'); + + sprout()->overrides()->registerOverrides(); + } + + #[Test] + public function errorsWhenRegisteringOverrideWithoutDriver(): void + { + config()->set('sprout.overrides', ['auth' => []]); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The service override [auth] is missing a required value for \'driver\''); + + sprout()->overrides()->registerOverrides(); + } + + #[Test] + public function errorsWhenRegisteringOverrideWithInvalidDriver(): void + { + config()->set('sprout.overrides', ['auth' => ['driver' => \stdClass::class]]); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The provided value for \'driver\' is not valid for service override [auth]'); + + sprout()->overrides()->registerOverrides(); + } +} From e1b84150ec592fb4d21f74073afd1edc7c6dff2c Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 12 Jan 2025 00:02:01 +0000 Subject: [PATCH 26/48] test(resolvers): Add compatibility exception cases to cookie and session identity resolver tests --- .../Resolvers/CookieIdentityResolverTest.php | 25 ++++++++++++++++++ .../Resolvers/SessionIdentityResolverTest.php | 26 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php index b4be0ad..63b2019 100644 --- a/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php +++ b/tests/Unit/Http/Resolvers/CookieIdentityResolverTest.php @@ -4,10 +4,13 @@ namespace Sprout\Tests\Unit\Http\Resolvers; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; +use Sprout\Exceptions\CompatibilityException; use Sprout\Http\Resolvers\CookieIdentityResolver; +use Sprout\Overrides\CookieOverride; use Sprout\Support\ResolutionHook; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; @@ -58,6 +61,13 @@ protected function withCustomCookieNamePattern(Application $app): void }); } + protected function withCookieServiceOverride(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('sprout.overrides.cookie', ['driver' => CookieOverride::class]); + }); + } + #[Test] public function isRegisteredAndCanBeAccessed(): void { @@ -150,4 +160,19 @@ public function canGenerateRoutesForATenant(): void $this->assertSame('http://localhost/tenant', sprout()->route('tenant-route', $tenant, $resolver->getName(), $tenancy->getName())); $this->assertSame('http://localhost/tenant', sprout()->route('tenant-route', $tenant)); } + + #[Test, DefineEnvironment('withCookieServiceOverride')] + public function errorsOutIfTheCookieServiceHasAnOverride(): void + { + /** @var \Illuminate\Http\Request $request */ + $request = app()->make(Request::class); + + $tenancy = tenancy(); + $resolver = resolver('cookie'); + + $this->expectException(CompatibilityException::class); + $this->expectExceptionMessage('Cannot use resolver [cookie] with service override [cookie]'); + + $resolver->resolveFromRequest($request, $tenancy); + } } diff --git a/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php b/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php index 46f28fb..6d960ba 100644 --- a/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php +++ b/tests/Unit/Http/Resolvers/SessionIdentityResolverTest.php @@ -4,10 +4,14 @@ namespace Sprout\Tests\Unit\Http\Resolvers; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Orchestra\Testbench\Attributes\DefineEnvironment; use PHPUnit\Framework\Attributes\Test; +use Sprout\Exceptions\CompatibilityException; use Sprout\Http\Resolvers\SessionIdentityResolver; +use Sprout\Overrides\CookieOverride; +use Sprout\Overrides\SessionOverride; use Sprout\Support\ResolutionHook; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; @@ -47,6 +51,13 @@ protected function withCustomSessionNamePattern(Application $app): void }); } + protected function withSessionServiceOverride(Application $app): void + { + tap($app['config'], static function ($config) { + $config->set('sprout.overrides.session', ['driver' => SessionOverride::class]); + }); + } + #[Test] public function isRegisteredAndCanBeAccessed(): void { @@ -117,4 +128,19 @@ public function canGenerateRoutesForATenant(): void $this->assertSame('http://localhost/tenant', sprout()->route('tenant-route', $tenant, $resolver->getName(), $tenancy->getName())); $this->assertSame('http://localhost/tenant', sprout()->route('tenant-route', $tenant)); } + + #[Test, DefineEnvironment('withSessionServiceOverride')] + public function errorsOutIfTheCookieServiceHasAnOverride(): void + { + /** @var \Illuminate\Http\Request $request */ + $request = app()->make(Request::class); + + $tenancy = tenancy(); + $resolver = resolver('session'); + + $this->expectException(CompatibilityException::class); + $this->expectExceptionMessage('Cannot use resolver [session] with service override [session]'); + + $resolver->resolveFromRequest($request, $tenancy); + } } From dc781def5d6da850089458f09eb4a5324878c49e Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 12 Jan 2025 00:02:21 +0000 Subject: [PATCH 27/48] test(overrides): Fix skipped tests for auth and session service overrides --- tests/Unit/Overrides/AuthOverrideTest.php | 94 ++++++++------- tests/Unit/Overrides/SessionOverrideTest.php | 118 +++++++++---------- 2 files changed, 112 insertions(+), 100 deletions(-) diff --git a/tests/Unit/Overrides/AuthOverrideTest.php b/tests/Unit/Overrides/AuthOverrideTest.php index 604202c..6b10fa9 100644 --- a/tests/Unit/Overrides/AuthOverrideTest.php +++ b/tests/Unit/Overrides/AuthOverrideTest.php @@ -48,6 +48,8 @@ public function isRegisteredWithSproutCorrectly(): void ], ]); + $this->assertFalse($sprout->overrides()->hasOverride('auth')); + $sprout->overrides()->registerOverrides(); $this->assertTrue($sprout->overrides()->hasOverride('auth')); @@ -56,50 +58,40 @@ public function isRegisteredWithSproutCorrectly(): void $this->assertTrue($sprout->overrides()->hasOverrideBooted('auth')); } - #[Test] - public function isBootedCorrectly(): void - { - $this->markTestSkipped('This test needs to be updated'); - - $sprout = sprout(); - - $sprout->registerOverride(Services::AUTH, AuthOverride::class); - - $this->assertFalse(app()->isDeferredService('auth.password')); - $this->assertTrue(app()->bound('auth.password')); - $this->assertFalse(app()->resolved('auth.password')); - $this->assertFalse(app()->resolved('auth.password.broker')); - $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); - $this->assertTrue(app()->resolved('auth.password')); - $this->assertFalse(app()->resolved('auth.password.broker')); - } - #[Test] public function rebindsAuthPassword(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); + config()->set('sprout.overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + app()->rebinding('auth.password', function ($app, $passwordBrokerManager) { $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, $passwordBrokerManager); }); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + $sprout->overrides()->registerOverrides(); } #[Test] public function forgetsAuthPasswordInstance(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); + config()->set('sprout.overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + $this->assertFalse(app()->resolved('auth.password')); $this->assertNotInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); $this->assertTrue(app()->resolved('auth.password')); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + $sprout->overrides()->registerOverrides(); $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); } @@ -107,15 +99,19 @@ public function forgetsAuthPasswordInstance(): void #[Test] public function replacesTheDatabaseTokenRepositoryDriver(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); + config()->set('sprout.overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + config()->set('auth.passwords.users.driver', 'database'); config()->set('auth.passwords.users.table', 'password_reset_tokens'); config()->set('multitenancy.providers.eloquent.model', TenantModel::class); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + $sprout->overrides()->registerOverrides(); $broker = app()->make('auth.password.broker'); @@ -144,15 +140,19 @@ public function replacesTheDatabaseTokenRepositoryDriver(): void #[Test] public function replacesTheCacheTokenRepositoryDriver(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); + config()->set('sprout.overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + config()->set('auth.passwords.users.driver', 'cache'); config()->set('auth.passwords.users.store', 'array'); config()->set('multitenancy.providers.eloquent.model', TenantModel::class); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + $sprout->overrides()->registerOverrides(); $broker = app()->make('auth.password.broker'); @@ -177,13 +177,17 @@ public function replacesTheCacheTokenRepositoryDriver(): void #[Test] public function canFlushBrokers(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); + config()->set('sprout.overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + config()->set('auth.passwords.users.driver', 'database'); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + $sprout->overrides()->registerOverrides(); /** @var SproutAuthPasswordBrokerManager $manager */ $manager = app()->make('auth.password'); @@ -202,13 +206,17 @@ public function canFlushBrokers(): void #[Test] public function performsSetup(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + config()->set('sprout.overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + + $sprout->overrides()->registerOverrides(); - $override = $sprout->getOverrides()[AuthOverride::class]; + $override = $sprout->overrides()->get('auth'); $this->assertInstanceOf(AuthOverride::class, $override); @@ -232,13 +240,17 @@ public function performsSetup(): void #[Test] public function performsCleanup(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); - $sprout->registerOverride(Services::AUTH, AuthOverride::class); + config()->set('sprout.overrides', [ + 'auth' => [ + 'driver' => AuthOverride::class, + ], + ]); + + $sprout->overrides()->registerOverrides(); - $override = $sprout->getOverrides()[AuthOverride::class]; + $override = $sprout->overrides()->get('auth'); $this->assertInstanceOf(AuthOverride::class, $override); diff --git a/tests/Unit/Overrides/SessionOverrideTest.php b/tests/Unit/Overrides/SessionOverrideTest.php index 4158e35..f23efb4 100644 --- a/tests/Unit/Overrides/SessionOverrideTest.php +++ b/tests/Unit/Overrides/SessionOverrideTest.php @@ -16,6 +16,7 @@ use Sprout\Support\Services; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; +use function Sprout\settings; use function Sprout\sprout; class SessionOverrideTest extends UnitTestCase @@ -53,64 +54,55 @@ public function isRegisteredWithSproutCorrectly(): void } #[Test] - public function isDeferredCorrectly(): void + public function performsSetup(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); - Event::fake(); - - $sprout->registerOverride(Services::SESSION, SessionOverride::class); - - Event::assertDispatched(ServiceOverrideRegistered::class); - Event::assertNotDispatched(ServiceOverrideProcessing::class); - Event::assertNotDispatched(ServiceOverrideProcessed::class); - Event::assertNotDispatched(ServiceOverrideBooted::class); + config()->set('sprout.overrides', [ + 'session' => [ + 'driver' => SessionOverride::class, + ], + ]); - $this->assertTrue($sprout->hasRegisteredOverride(SessionOverride::class)); - $this->assertFalse($sprout->hasOverride(SessionOverride::class)); - $this->assertTrue($sprout->isServiceBeingOverridden(Services::SESSION)); - $this->assertFalse($sprout->isBootableOverride(SessionOverride::class)); - $this->assertTrue($sprout->isDeferrableOverride(SessionOverride::class)); - $this->assertFalse($sprout->hasBootedOverride(SessionOverride::class)); - $this->assertFalse($sprout->hasOverrideBeenSetup(SessionOverride::class)); + $this->assertFalse(sprout()->settings()->has('original.session')); - $overrides = $sprout->getOverrides(); + $this->instance(SessionManager::class, Mockery::mock(SessionManager::class, function (MockInterface $mock) { + $mock->makePartial(); + $mock->shouldReceive('forgetDrivers')->once(); + })); - $this->assertEmpty($overrides); + $this->assertNull(sprout()->settings()->getUrlPath()); + $this->assertNull(sprout()->settings()->getUrlDomain()); + $this->assertNull(sprout()->settings()->shouldCookieBeSecure()); + $this->assertNull(sprout()->settings()->getCookieSameSite()); - $overrides = $sprout->getRegisteredOverrides(); + $tenant = TenantModel::factory()->createOne(); + $tenancy = $sprout->tenancies()->get(); - $this->assertCount(1, $overrides); - $this->assertContains(SessionOverride::class, $overrides); + $tenancy->setTenant($tenant); - $this->instance(SessionManager::class, Mockery::mock(SessionManager::class, function (MockInterface $mock) { - $mock->shouldReceive('extend')->times(3); - })); + $sprout->overrides()->registerOverrides(); app()->make('session'); - $overrides = $sprout->getOverrides(); + $override = $sprout->overrides()->get('session'); - $this->assertCount(1, $overrides); - $this->assertInstanceOf(SessionOverride::class, $overrides[SessionOverride::class]); - - $this->assertTrue($sprout->isBootableOverride(SessionOverride::class)); - $this->assertTrue($sprout->hasBootedOverride(SessionOverride::class)); + $this->assertInstanceOf(SessionOverride::class, $override); - Event::assertDispatched(ServiceOverrideProcessing::class); - Event::assertDispatched(ServiceOverrideProcessed::class); - Event::assertDispatched(ServiceOverrideBooted::class); + $override->setup($tenancy, $tenant); } #[Test] - public function performsSetup(): void + public function performsCleanup(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); + config()->set('sprout.overrides', [ + 'session' => [ + 'driver' => SessionOverride::class, + ], + ]); + $this->assertFalse(sprout()->settings()->has('original.session')); $this->instance(SessionManager::class, Mockery::mock(SessionManager::class, function (MockInterface $mock) { @@ -118,54 +110,62 @@ public function performsSetup(): void $mock->shouldReceive('forgetDrivers')->once(); })); - $this->assertNull(sprout()->settings()->getUrlPath()); - $this->assertNull(sprout()->settings()->getUrlDomain()); - $this->assertNull(sprout()->settings()->shouldCookieBeSecure()); - $this->assertNull(sprout()->settings()->getCookieSameSite()); - $tenant = TenantModel::factory()->createOne(); $tenancy = $sprout->tenancies()->get(); $tenancy->setTenant($tenant); - $sprout->registerOverride(Services::SESSION, SessionOverride::class); + $sprout->overrides()->registerOverrides(); app()->make('session'); - $override = $sprout->getOverrides()[SessionOverride::class]; + $override = $sprout->overrides()->get('session'); $this->assertInstanceOf(SessionOverride::class, $override); - $override->setup($tenancy, $tenant); + $override->cleanup($tenancy, $tenant); } #[Test] - public function performsCleanup(): void + public function setSessionConfigFromSproutSettings(): void { - $this->markTestSkipped('This test needs to be updated'); - $sprout = sprout(); - $this->assertFalse(sprout()->settings()->has('original.session')); + config()->set('sprout.overrides', [ + 'session' => [ + 'driver' => SessionOverride::class, + ], + ]); - $this->instance(SessionManager::class, Mockery::mock(SessionManager::class, function (MockInterface $mock) { - $mock->makePartial(); - $mock->shouldReceive('forgetDrivers')->once(); - })); + config()->set('session.path', '/test-path'); + config()->set('session.domain', 'test-domain.localhost'); + config()->set('session.secure', false); + config()->set('session.same_site', 'lax'); $tenant = TenantModel::factory()->createOne(); $tenancy = $sprout->tenancies()->get(); $tenancy->setTenant($tenant); - $sprout->registerOverride(Services::SESSION, SessionOverride::class); + $sprout->overrides()->registerOverrides(); - app()->make('session'); + $this->assertSame('/test-path', config('session.path')); + $this->assertSame('test-domain.localhost', config('session.domain')); + $this->assertFalse(config('session.secure')); + $this->assertSame('lax', config('session.same_site')); - $override = $sprout->getOverrides()[SessionOverride::class]; + $override = $sprout->overrides()->get('session'); - $this->assertInstanceOf(SessionOverride::class, $override); + settings()->setUrlPath('/test-path2'); + settings()->setUrlDomain('test-domain2.localhost'); + settings()->setCookieSecure(true); + settings()->setCookieSameSite('strict'); - $override->cleanup($tenancy, $tenant); + $override->setup($tenancy, $tenant); + + $this->assertSame('/test-path2', config('session.path')); + $this->assertSame('test-domain2.localhost', config('session.domain')); + $this->assertTrue(config('session.secure')); + $this->assertSame('strict', config('session.same_site')); } } From 90d19518e0ad5182d8189a80c265e482cbc6fa90 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 12 Jan 2025 00:05:02 +0000 Subject: [PATCH 28/48] chore: Ignore service override exceptions from code coverage --- src/Exceptions/ServiceOverrideException.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Exceptions/ServiceOverrideException.php b/src/Exceptions/ServiceOverrideException.php index 4ceb7f8..024a111 100644 --- a/src/Exceptions/ServiceOverrideException.php +++ b/src/Exceptions/ServiceOverrideException.php @@ -14,7 +14,7 @@ final class ServiceOverrideException extends SproutException */ public static function notBootable(string $service): self { - return new self('The service override [' . $service . '] is not bootable'); + return new self('The service override [' . $service . '] is not bootable'); // @codeCoverageIgnore } /** @@ -28,6 +28,6 @@ public static function notBootable(string $service): self */ public static function setupButNotEnabled(string $service, string $tenancy): self { - return new self('The service override [' . $service . '] has been set up for the tenancy [' . $tenancy . '] but it is not enabled for that tenancy'); + return new self('The service override [' . $service . '] has been set up for the tenancy [' . $tenancy . '] but it is not enabled for that tenancy'); // @codeCoverageIgnore } } From b84b386d06d18df796162711c6d908a1445525f7 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 12 Jan 2025 12:19:51 +0000 Subject: [PATCH 29/48] feat: Add method for checking whether a tenancy has had their service overrides setup --- src/Managers/ServiceOverrideManager.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index 9b52d77..7e65975 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -132,6 +132,27 @@ public function hasOverrideBeenSetUp(string $service, ?Tenancy $tenancy = null): return in_array($service, $this->getSetupOverrides($tenancy), true); } + /** + * Check if a tenancy has been set up + * + * @param \Sprout\Contracts\Tenancy<*>|null $tenancy + * + * @return bool + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Sprout\Exceptions\TenancyMissingException + */ + public function hasTenancyBeenSetup(?Tenancy $tenancy = null): bool + { + $tenancy ??= $this->app->make(Sprout::class)->getCurrentTenancy(); + + if ($tenancy === null) { + throw TenancyMissingException::make(); + } + + return array_key_exists($tenancy->getName(), $this->setupOverrides); + } + /** * Check if a services' override is bootable * @@ -249,6 +270,8 @@ public function setupOverrides(Tenancy $tenancy, Tenant $tenant): void $enabled = TenancyOptions::enabledOverrides($tenancy) ?? []; $allEnabled = TenancyOptions::shouldEnableAllOverrides($tenancy); + $this->setupOverrides[$tenancy->getName()] = []; + // Loop through all registered overrides foreach ($this->overrides as $service => $override) { // If the override is enabled @@ -294,6 +317,8 @@ public function cleanupOverrides(Tenancy $tenancy, Tenant $tenant): void throw ServiceOverrideException::setupButNotEnabled($service, $tenancy->getName()); // @codeCoverageIgnore } } + + unset($this->setupOverrides[$tenancy->getName()]); } /** From 4be33e1cf122ab73153c3df9a0cfb7abf977ec93 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 12 Jan 2025 13:02:42 +0000 Subject: [PATCH 30/48] fix: Fix two issues with the cache override The sprout driver for the cache manager wasn't added if the cache manager had already been resolved, and because the driver creation method was bound to the manager, it wasn't able to access $this->drivers --- src/Overrides/CacheOverride.php | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Overrides/CacheOverride.php b/src/Overrides/CacheOverride.php index 2b7e725..ae07996 100644 --- a/src/Overrides/CacheOverride.php +++ b/src/Overrides/CacheOverride.php @@ -3,6 +3,7 @@ namespace Sprout\Overrides; +use Closure; use Illuminate\Cache\CacheManager; use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Foundation\Application; @@ -32,16 +33,27 @@ final class CacheOverride extends BaseOverride implements BootableServiceOverrid */ public function boot(Application $app, Sprout $sprout): void { - // We only want to add the driver if the filesystem service is - // resolved at some point - $app->afterResolving('cache', function (CacheManager $manager) use ($sprout) { - $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout): Repository { - // The cache manager adds the store name to the config, so we'll - // _STORE_ that ;) - $this->drivers[] = $config['store']; + $tracker = fn(string $store) => $this->drivers[] = $store; - return (new SproutCacheDriverCreator($app, $manager, $config, $sprout))(); + // If the cache manager has been resolved, we can add the driver + if ($app->resolved('cache')) { + $this->addDriver($app->make('cache'), $sprout, $tracker); + } else { + // But if it hasn't, we'll add it once it is + $app->afterResolving('cache', function (CacheManager $manager) use ($sprout, $tracker) { + $this->addDriver($manager, $sprout, $tracker); }); + } + } + + protected function addDriver(CacheManager $manager, Sprout $sprout, Closure $tracker): void + { + $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout, $tracker): Repository { + // The cache manager adds the store name to the config, so we'll + // _STORE_ that ;) + $tracker($config['store']); + + return (new SproutCacheDriverCreator($app, $manager, $config, $sprout))(); }); } @@ -68,7 +80,9 @@ public function boot(Application $app, Sprout $sprout): void public function cleanup(Tenancy $tenancy, Tenant $tenant): void { if (! empty($this->drivers)) { - app(CacheManager::class)->forgetDriver($this->drivers); + app('cache')->forgetDriver($this->drivers); + + $this->drivers = []; } } } From ee1d04e9fa7b853c8e39e3848c9ad5adbea6b435 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 12 Jan 2025 13:02:58 +0000 Subject: [PATCH 31/48] fix: Fix two issues with the filesystem override The sprout driver for the cache manager wasn't added if the cache manager had already been resolved, and because the driver creation method was bound to the manager, it wasn't able to access $this->drivers --- src/Overrides/FilesystemOverride.php | 35 ++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/Overrides/FilesystemOverride.php b/src/Overrides/FilesystemOverride.php index 506dc3c..eeb4f58 100644 --- a/src/Overrides/FilesystemOverride.php +++ b/src/Overrides/FilesystemOverride.php @@ -3,12 +3,16 @@ namespace Sprout\Overrides; +use Closure; +use Illuminate\Cache\CacheManager; +use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Foundation\Application; use Illuminate\Filesystem\FilesystemManager; use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +use Sprout\Overrides\Cache\SproutCacheDriverCreator; use Sprout\Overrides\Filesystem\SproutFilesystemDriverCreator; use Sprout\Overrides\Filesystem\SproutFilesystemManager; use Sprout\Sprout; @@ -61,18 +65,29 @@ public function boot(Application $app, Sprout $sprout): void $app->singleton('filesystem', fn ($app) => new SproutFilesystemManager($app, $original)); } - // We only want to add the driver if the filesystem service is - // resolved at some point - $app->afterResolving('filesystem', function (FilesystemManager $manager) use ($sprout) { - $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout): Filesystem { - // If the config contains the disk name - if (isset($config['name'])) { - // Track it - $this->drivers[] = $config['name']; - } + $tracker = fn(string $store) => $this->drivers[] = $store; - return (new SproutFilesystemDriverCreator($app, $manager, $config, $sprout))(); + // If the filesystem manager has been resolved, we can add the driver + if ($app->resolved('filesystem')) { + $this->addDriver($app->make('filesystem'), $sprout, $tracker); + } else { + // But if it hasn't, we'll add it once it is + $app->afterResolving('filesystem', function (FilesystemManager $manager) use ($sprout, $tracker) { + $this->addDriver($manager, $sprout, $tracker); }); + } + } + + protected function addDriver(FilesystemManager $manager, Sprout $sprout, Closure $tracker): void + { + $manager->extend('sprout', function (Application $app, array $config) use ($manager, $sprout, $tracker): Filesystem { + // If the config contains the disk name + if (isset($config['name'])) { + // Track it + $tracker($config['name']); + } + + return (new SproutFilesystemDriverCreator($app, $manager, $config, $sprout))(); }); } From 3ac2257eb3dc682e2bd7fac89b9f91774a8cc030 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sun, 12 Jan 2025 13:04:00 +0000 Subject: [PATCH 32/48] test(overrides): Add cache override test --- tests/Unit/Overrides/CacheOverrideTest.php | 128 +++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 tests/Unit/Overrides/CacheOverrideTest.php diff --git a/tests/Unit/Overrides/CacheOverrideTest.php b/tests/Unit/Overrides/CacheOverrideTest.php new file mode 100644 index 0000000..fa63831 --- /dev/null +++ b/tests/Unit/Overrides/CacheOverrideTest.php @@ -0,0 +1,128 @@ +set('sprout.overrides', []); + }); + } + + #[Test] + public function isBuiltCorrectly(): void + { + $this->assertTrue(is_subclass_of(CacheOverride::class, BootableServiceOverride::class)); + } + + #[Test] + public function isRegisteredWithSproutCorrectly(): void + { + $sprout = sprout(); + + config()->set('sprout.overrides', [ + 'cache' => [ + 'driver' => CacheOverride::class, + ], + ]); + + $this->assertFalse($sprout->overrides()->hasOverride('cache')); + + $sprout->overrides()->registerOverrides(); + + $this->assertTrue($sprout->overrides()->hasOverride('cache')); + $this->assertSame(CacheOverride::class, $sprout->overrides()->getOverrideClass('cache')); + $this->assertTrue($sprout->overrides()->isOverrideBootable('cache')); + $this->assertTrue($sprout->overrides()->hasOverrideBooted('cache')); + } + + #[Test] + public function addsSproutDriverToCacheManager(): void + { + $sprout = sprout(); + + config()->set('sprout.overrides', [ + 'cache' => [ + 'driver' => CacheOverride::class, + ], + ]); + + config()->set('cache.stores.null', [ + 'driver' => 'null', + ]); + + $sprout->overrides()->registerOverrides(); + + $tenant = TenantModel::factory()->createOne(); + $tenancy = tenancy(); + + $tenancy->setTenant($tenant); + sprout()->setCurrentTenancy($tenancy); + + $manager = $this->app->make('cache'); + + $disk = $manager->build([ + 'driver' => 'sprout', + 'override' => 'null', + ]); + + $this->assertInstanceOf(\Illuminate\Contracts\Cache\Repository::class, $disk); + } + + #[Test] + public function performsCleanup(): void + { + $sprout = sprout(); + + config()->set('sprout.overrides', [ + 'cache' => [ + 'driver' => CacheOverride::class, + ], + ]); + + config()->set('cache.stores.null', [ + 'driver' => 'null', + ]); + + config()->set('cache.stores.sprout', [ + 'driver' => 'sprout', + 'override' => 'null', + ]); + + $this->app->forgetInstance('cache'); + + $sprout->overrides()->registerOverrides(); + + $override = $sprout->overrides()->get('cache'); + + $this->assertInstanceOf(CacheOverride::class, $override); + + $tenant = TenantModel::factory()->createOne(); + $tenancy = tenancy(); + + $tenancy->setTenant($tenant); + sprout()->setCurrentTenancy($tenancy); + + $this->app->make('cache')->store('sprout'); + + $this->instance('cache', $this->spy(CacheManager::class, function (MockInterface $mock) { + $mock->shouldReceive('forgetDriver')->once()->withArgs([['sprout']]); + })); + + $override->cleanup($tenancy, $tenant); + } +} From 479c530bb264530326050d3e2275b69722b8e90e Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Wed, 22 Jan 2025 23:25:13 +0000 Subject: [PATCH 33/48] chore: Add tenant aware contract and default trait implementation --- src/Concerns/AwareOfTenant.php | 86 ++++++++++++++++++++++++++++++++++ src/Contracts/TenantAware.php | 56 ++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/Concerns/AwareOfTenant.php create mode 100644 src/Contracts/TenantAware.php diff --git a/src/Concerns/AwareOfTenant.php b/src/Concerns/AwareOfTenant.php new file mode 100644 index 0000000..91c57c6 --- /dev/null +++ b/src/Concerns/AwareOfTenant.php @@ -0,0 +1,86 @@ +tenant ?? null; + } + + /** + * Check if there is a tenant + * + * @return bool + */ + public function hasTenant(): bool + { + return $this->getTenant() !== null; + } + + /** + * Set the tenant + * + * @param \Sprout\Contracts\Tenant|null $tenant + * + * @return static + */ + public function setTenant(?Tenant $tenant): static + { + $this->tenant = $tenant; + + return $this; + } + + /** + * Get the tenancy if there is one + * + * @return \Sprout\Contracts\Tenancy<*>|null + */ + public function getTenancy(): ?Tenancy + { + return $this->tenancy ?? null; + } + + /** + * Check if there is a tenancy + * + * @return bool + */ + public function hasTenancy(): bool + { + return $this->getTenancy() !== null; + } + + /** + * Set the tenancy + * + * @template TenantClass of \Sprout\Contracts\Tenant + * @param \Sprout\Contracts\Tenancy|null $tenancy + * + * @return static + */ + public function setTenancy(?Tenancy $tenancy): static + { + $this->tenancy = $tenancy; + + return $this; + } +} diff --git a/src/Contracts/TenantAware.php b/src/Contracts/TenantAware.php new file mode 100644 index 0000000..a9b537a --- /dev/null +++ b/src/Contracts/TenantAware.php @@ -0,0 +1,56 @@ +|null + */ + public function getTenancy(): ?Tenancy; + + /** + * Check if there is a tenancy + * + * @return bool + */ + public function hasTenancy(): bool; + + /** + * Set the tenancy + * + * @template TenantClass of \Sprout\Contracts\Tenant + * @param \Sprout\Contracts\Tenancy|null $tenancy + * + * @return static + */ + public function setTenancy(?Tenancy $tenancy): static; +} From a39c053b7365d25a4b706455474171a2f79531a7 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Wed, 22 Jan 2025 23:26:37 +0000 Subject: [PATCH 34/48] chore: Add functionality to refresh tenant-aware dependencies when the tenancy and/or tenant changes --- resources/config/core.php | 2 + src/Concerns/AwareOfTenant.php | 10 ++++ src/Contracts/TenantAware.php | 7 +++ .../RefreshTenantAwareDependencies.php | 47 +++++++++++++++++++ src/Sprout.php | 4 ++ src/SproutServiceProvider.php | 17 +++++++ 6 files changed, 87 insertions(+) create mode 100644 src/Listeners/RefreshTenantAwareDependencies.php diff --git a/resources/config/core.php b/resources/config/core.php index 29d5c4f..7c6d8ce 100644 --- a/resources/config/core.php +++ b/resources/config/core.php @@ -40,6 +40,8 @@ \Sprout\Listeners\CleanupServiceOverrides::class, // Sets up service overrides for the current tenancy \Sprout\Listeners\SetupServiceOverrides::class, + // Refresh anything that's tenant-aware + \Sprout\Listeners\RefreshTenantAwareDependencies::class, ], ]; diff --git a/src/Concerns/AwareOfTenant.php b/src/Concerns/AwareOfTenant.php index 91c57c6..19c41f3 100644 --- a/src/Concerns/AwareOfTenant.php +++ b/src/Concerns/AwareOfTenant.php @@ -15,6 +15,16 @@ trait AwareOfTenant private ?Tenancy $tenancy; + /** + * Should the tenancy and tenant be refreshed when they change? + * + * @return bool + */ + public function shouldBeRefreshed(): bool + { + return true; + } + /** * Get the tenant if there is one * diff --git a/src/Contracts/TenantAware.php b/src/Contracts/TenantAware.php index a9b537a..2cfce7f 100644 --- a/src/Contracts/TenantAware.php +++ b/src/Contracts/TenantAware.php @@ -7,6 +7,13 @@ */ interface TenantAware { + /** + * Should the tenancy and tenant be refreshed when they change? + * + * @return bool + */ + public function shouldBeRefreshed(): bool; + /** * Get the tenant if there is one * diff --git a/src/Listeners/RefreshTenantAwareDependencies.php b/src/Listeners/RefreshTenantAwareDependencies.php new file mode 100644 index 0000000..1da58c6 --- /dev/null +++ b/src/Listeners/RefreshTenantAwareDependencies.php @@ -0,0 +1,47 @@ +app = $app; + } + + /** + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Events\CurrentTenantChanged $event + * + * @return void + */ + public function handle(CurrentTenantChanged $event): void + { + if ($event->current !== null) { + $this->app->forgetExtenders(Tenant::class); + $this->app->extend(Tenant::class, fn(?Tenant $tenant) => $tenant); + } + } +} diff --git a/src/Sprout.php b/src/Sprout.php index 32c5d5f..65c7a50 100644 --- a/src/Sprout.php +++ b/src/Sprout.php @@ -105,6 +105,10 @@ public function setCurrentTenancy(Tenancy $tenancy): void { if ($this->getCurrentTenancy() !== $tenancy) { $this->tenancies[] = $tenancy; + + // This is a bit of a cheat to enable the refreshing of the Tenancy + $this->app->forgetExtenders(Tenancy::class); + $this->app->extend(Tenancy::class, fn(?Tenancy $tenancy) => $tenancy); } $this->markAsInContext(); diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index 36ec118..18c2ba1 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -7,6 +7,9 @@ use Illuminate\Routing\Events\RouteMatched; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; +use Sprout\Contracts\Tenancy; +use Sprout\Contracts\Tenant; +use Sprout\Contracts\TenantAware; use Sprout\Events\CurrentTenantChanged; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Http\RouterMethods; @@ -34,6 +37,7 @@ public function register(): void $this->registerMiddleware(); $this->registerRouteMixin(); $this->registerServiceOverrideBooting(); + $this->registerTenantAwareHandling(); } private function registerSprout(): void @@ -94,6 +98,19 @@ protected function registerServiceOverrideBooting(): void $this->app->booted($this->sprout->overrides()->bootOverrides(...)); } + protected function registerTenantAwareHandling(): void + { + // If something is resolved, that is aware of tenants... + $this->app->afterResolving(TenantAware::class, function (TenantAware $tenantAware) { + // And it wants to be refreshed... + if ($tenantAware->shouldBeRefreshed()) { + // Make sure it's notified when the tenant or tenancy change + $this->app->refresh(Tenant::class, $tenantAware, 'setTenant'); + $this->app->refresh(Tenancy::class, $tenantAware, 'setTenancy'); + } + }); + } + public function boot(): void { $this->publishConfig(); From 1623606cbb77006c4353861303d5d09d9c70ee85 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Wed, 22 Jan 2025 23:27:12 +0000 Subject: [PATCH 35/48] refactor: Update session handler overrides to be persistent and tenant-aware --- .../Session/SproutDatabaseSessionHandler.php | 45 ++++---- .../Session/SproutFileSessionHandler.php | 107 ++++++++++++++++++ .../SproutSessionDatabaseDriverCreator.php | 63 +++-------- .../SproutSessionFileDriverCreator.php | 61 +++------- src/Overrides/SessionOverride.php | 82 +++++++++++--- tests/Unit/Overrides/SessionOverrideTest.php | 34 ++++-- 6 files changed, 247 insertions(+), 145 deletions(-) create mode 100644 src/Overrides/Session/SproutFileSessionHandler.php diff --git a/src/Overrides/Session/SproutDatabaseSessionHandler.php b/src/Overrides/Session/SproutDatabaseSessionHandler.php index e1d4457..a3d5942 100644 --- a/src/Overrides/Session/SproutDatabaseSessionHandler.php +++ b/src/Overrides/Session/SproutDatabaseSessionHandler.php @@ -5,6 +5,8 @@ use Illuminate\Database\Query\Builder; use Illuminate\Session\DatabaseSessionHandler; +use Sprout\Concerns\AwareOfTenant; +use Sprout\Contracts\TenantAware; use Sprout\Exceptions\TenancyMissingException; use Sprout\Exceptions\TenantMissingException; use function Sprout\sprout; @@ -18,8 +20,10 @@ * * @package Overrides */ -class SproutDatabaseSessionHandler extends DatabaseSessionHandler +class SproutDatabaseSessionHandler extends DatabaseSessionHandler implements TenantAware { + use AwareOfTenant; + /** * Get a fresh query builder instance for the table. * @@ -30,19 +34,21 @@ class SproutDatabaseSessionHandler extends DatabaseSessionHandler */ protected function getQuery(): Builder { - $tenancy = sprout()->getCurrentTenancy(); - - if ($tenancy === null) { - throw TenancyMissingException::make(); + if (! $this->hasTenant()) { + return parent::getQuery(); } - if ($tenancy->check() === false) { - throw TenantMissingException::make($tenancy->getName()); - } + $tenancy = $this->getTenancy(); + $tenant = $this->getTenant(); + + /** + * @var \Sprout\Contracts\Tenancy<*> $tenancy + * @var \Sprout\Contracts\Tenant $tenant + */ return parent::getQuery() ->where('tenancy', '=', $tenancy->getName()) - ->where('tenant_id', '=', $tenancy->key()); + ->where('tenant_id', '=', $tenant->getTenantKey()); } /** @@ -52,24 +58,23 @@ protected function getQuery(): Builder * @param array $payload * * @return bool|null - * - * @throws \Sprout\Exceptions\TenancyMissingException - * @throws \Sprout\Exceptions\TenantMissingException */ protected function performInsert($sessionId, $payload): ?bool { - $tenancy = sprout()->getCurrentTenancy(); - - if ($tenancy === null) { - throw TenancyMissingException::make(); + if (! $this->hasTenant()) { + return parent::performInsert($sessionId, $payload); } - if ($tenancy->check() === false) { - throw TenantMissingException::make($tenancy->getName()); - } + $tenancy = $this->getTenancy(); + $tenant = $this->getTenant(); + + /** + * @var \Sprout\Contracts\Tenancy<*> $tenancy + * @var \Sprout\Contracts\Tenant $tenant + */ $payload['tenancy'] = $tenancy->getName(); - $payload['tenant_id'] = $tenancy->key(); + $payload['tenant_id'] = $tenant->getTenantKey(); return parent::performInsert($sessionId, $payload); } diff --git a/src/Overrides/Session/SproutFileSessionHandler.php b/src/Overrides/Session/SproutFileSessionHandler.php new file mode 100644 index 0000000..3fb9ff3 --- /dev/null +++ b/src/Overrides/Session/SproutFileSessionHandler.php @@ -0,0 +1,107 @@ +hasTenant()) { + return $this->path; + } + + /** @var \Sprout\Contracts\Tenant&\Sprout\Contracts\TenantHasResources $tenant */ + $tenant = $this->getTenant(); + + return rtrim($this->path, DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR + . $tenant->getTenantResourceKey(); + } + + /** + * {@inheritdoc} + * + * @param $sessionId + * + * @return string|false + * + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + */ + public function read($sessionId): string|false + { + if ($this->files->isFile($path = $this->getPath() . '/' . $sessionId) && + $this->files->lastModified($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) { + return $this->files->sharedGet($path); + } + + return ''; + } + + /** + * {@inheritdoc} + * + * @return bool + * + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + */ + public function write($sessionId, $data): bool + { + $this->files->put($this->getPath() . '/' . $sessionId, $data, true); + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function destroy($sessionId): bool + { + $this->files->delete($this->getPath() . '/' . $sessionId); + + return true; + } + + /** + * {@inheritdoc} + * + * @return int + */ + public function gc($lifetime): int + { + $files = Finder::create() + ->in($this->getPath()) + ->files() + ->ignoreDotFiles(true) + ->date('<= now - ' . $lifetime . ' seconds'); + + $deletedSessions = 0; + + foreach ($files as $file) { + $this->files->delete($file->getRealPath()); + $deletedSessions++; + } + + return $deletedSessions; + } +} diff --git a/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php b/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php index 67c2832..b254e5f 100644 --- a/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php +++ b/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php @@ -5,12 +5,6 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Session\DatabaseSessionHandler; -use Illuminate\Session\DatabaseSessionHandler as OriginalDatabaseSessionHandler; -use Illuminate\Session\SessionManager; -use Sprout\Contracts\TenantHasResources; -use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenancyMissingException; -use Sprout\Exceptions\TenantMissingException; use Sprout\Sprout; final class SproutSessionDatabaseDriverCreator @@ -20,11 +14,6 @@ final class SproutSessionDatabaseDriverCreator */ private Application $app; - /** - * @var \Illuminate\Session\SessionManager - */ - private SessionManager $manager; // @phpstan-ignore-line - /** * @var \Sprout\Sprout */ @@ -34,14 +23,11 @@ final class SproutSessionDatabaseDriverCreator * Create a new instance * * @param \Illuminate\Contracts\Foundation\Application $app - * @param \Illuminate\Session\SessionManager $manager - * @param \Sprout\Sprout $sprout */ - public function __construct(Application $app, SessionManager $manager, Sprout $sprout) + public function __construct(Application $app, Sprout $sprout) { - $this->app = $app; - $this->manager = $manager; - $this->sprout = $sprout; + $this->app = $app; + $this->sprout = $sprout; } /** @@ -66,43 +52,20 @@ public function __invoke(): DatabaseSessionHandler * @var int $lifetime */ - // This driver is unlike many of the others, where if we aren't in - // multitenanted context, we don't do anything - if ($this->sprout->withinContext()) { - // Get the current active tenancy - $tenancy = $this->sprout->getCurrentTenancy(); - - // If there isn't one, that's an issue as we need a tenancy - if ($tenancy === null) { - throw TenancyMissingException::make(); - } - - // If there is a tenancy, but it doesn't have a tenant, that's also - // an issue - if ($tenancy->check() === false) { - throw TenantMissingException::make($tenancy->getName()); - } - - $tenant = $tenancy->tenant(); - - // If the tenant isn't configured for resources, this is another issue - if (! ($tenant instanceof TenantHasResources)) { - throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); - } - - return new SproutDatabaseSessionHandler( - $this->app->make('db')->connection($connection), - $table, - $lifetime, - $this->app - ); - } - - return new OriginalDatabaseSessionHandler( + $handler = new SproutDatabaseSessionHandler( $this->app->make('db')->connection($connection), $table, $lifetime, $this->app ); + + if ($this->sprout->withinContext()) { + $tenancy = $this->sprout->getCurrentTenancy(); + + $handler->setTenancy($tenancy) + ->setTenant($tenancy?->tenant()); + } + + return $handler; } } diff --git a/src/Overrides/Session/SproutSessionFileDriverCreator.php b/src/Overrides/Session/SproutSessionFileDriverCreator.php index f5dfe37..72f5338 100644 --- a/src/Overrides/Session/SproutSessionFileDriverCreator.php +++ b/src/Overrides/Session/SproutSessionFileDriverCreator.php @@ -5,11 +5,6 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Session\FileSessionHandler; -use Illuminate\Session\SessionManager; -use Sprout\Contracts\TenantHasResources; -use Sprout\Exceptions\MisconfigurationException; -use Sprout\Exceptions\TenancyMissingException; -use Sprout\Exceptions\TenantMissingException; use Sprout\Sprout; final class SproutSessionFileDriverCreator @@ -19,11 +14,6 @@ final class SproutSessionFileDriverCreator */ private Application $app; - /** - * @var \Illuminate\Session\SessionManager - */ - private SessionManager $manager; // @phpstan-ignore-line - /** * @var \Sprout\Sprout */ @@ -33,14 +23,11 @@ final class SproutSessionFileDriverCreator * Create a new instance * * @param \Illuminate\Contracts\Foundation\Application $app - * @param \Illuminate\Session\SessionManager $manager - * @param \Sprout\Sprout $sprout */ - public function __construct(Application $app, SessionManager $manager, Sprout $sprout) + public function __construct(Application $app, Sprout $sprout) { - $this->app = $app; - $this->manager = $manager; - $this->sprout = $sprout; + $this->app = $app; + $this->sprout = $sprout; } /** @@ -59,42 +46,22 @@ public function __invoke(): FileSessionHandler $originalPath = config('session.files'); $path = rtrim($originalPath, '/') . DIRECTORY_SEPARATOR; - // This driver is unlike many of the others, where if we aren't in - // multitenanted context, we don't do anything - if ($this->sprout->withinContext()) { - // Get the current active tenancy - $tenancy = $this->sprout->getCurrentTenancy(); - - // If there isn't one, that's an issue as we need a tenancy - if ($tenancy === null) { - throw TenancyMissingException::make(); - } - - // If there is a tenancy, but it doesn't have a tenant, that's also - // an issue - if ($tenancy->check() === false) { - throw TenantMissingException::make($tenancy->getName()); - } - - $tenant = $tenancy->tenant(); - - // If the tenant isn't configured for resources, this is another issue - if (! ($tenant instanceof TenantHasResources)) { - throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); - } - - $path = rtrim($path, DIRECTORY_SEPARATOR) - . DIRECTORY_SEPARATOR - . $tenant->getTenantResourceKey(); - } - /** @var int $lifetime */ $lifetime = config('session.lifetime'); - return new FileSessionHandler( + $handler = new SproutFileSessionHandler( $this->app->make('files'), $path, - $lifetime, + $lifetime ); + + if ($this->sprout->withinContext()) { + $tenancy = $this->sprout->getCurrentTenancy(); + + $handler->setTenancy($tenancy) + ->setTenant($tenancy?->tenant()); + } + + return $handler; } } diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index a6fe26a..1f9d140 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -9,6 +9,7 @@ use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +use Sprout\Contracts\TenantAware; use Sprout\Overrides\Session\SproutSessionDatabaseDriverCreator; use Sprout\Overrides\Session\SproutSessionFileDriverCreator; use Sprout\Sprout; @@ -38,21 +39,32 @@ final class SessionOverride extends BaseOverride implements BootableServiceOverr */ public function boot(Application $app, Sprout $sprout): void { - app()->afterResolving('session', function (SessionManager $manager) use ($app, $sprout) { - $creator = new SproutSessionFileDriverCreator($app, $manager, $sprout); + // If the session manager has been resolved, we can add the driver + if ($app->resolved('session')) { + $manager = $app->make('session'); + $this->addDriver($manager, $app, $sprout); + $manager->forgetDrivers(); + } else { + // But if it hasn't, we'll add it once it is + $app->afterResolving('session', function (SessionManager $manager) use ($app, $sprout) { + $this->addDriver($manager, $app, $sprout); + }); + } + } + + protected function addDriver(SessionManager $manager, Application $app, Sprout $sprout): void + { + $creator = new SproutSessionFileDriverCreator($app, $sprout); - $manager->extend('file', $creator(...)); - $manager->extend('native', $creator(...)); + $manager->extend('file', $creator(...)); + $manager->extend('native', $creator(...)); - /** @var bool $overrideDatabase */ - $overrideDatabase = $this->config['database'] ?? true; + /** @var bool $overrideDatabase */ + $overrideDatabase = $this->config['database'] ?? true; - if (settings()->shouldNotOverrideTheDatabase($overrideDatabase) === false) { - $manager->extend('database', (new SproutSessionDatabaseDriverCreator( - $app, $manager, $sprout - ))(...)); - } - }); + if (settings()->shouldNotOverrideTheDatabase($overrideDatabase) === false) { + $manager->extend('database', (new SproutSessionDatabaseDriverCreator($app, $sprout))(...)); + } } /** @@ -104,8 +116,7 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void $config->set('session.cookie', $this->getCookieName($tenancy, $tenant)); - // Reset all the drivers - app(SessionManager::class)->forgetDrivers(); + $this->refreshSessionStore($tenancy, $tenant); } /** @@ -130,8 +141,7 @@ public function setup(Tenancy $tenancy, Tenant $tenant): void */ public function cleanup(Tenancy $tenancy, Tenant $tenant): void { - // Reset all the drivers - app(SessionManager::class)->forgetDrivers(); + $this->refreshSessionStore(); } /** @@ -144,4 +154,44 @@ private function getCookieName(Tenancy $tenancy, Tenant $tenant): string { return $tenancy->getName() . '_' . $tenant->getTenantIdentifier() . '_session'; } + + /** + * Set the tenant details and refresh the session + * + * @template TenantClass of \Sprout\Contracts\Tenant + * + * @param \Sprout\Contracts\Tenancy|null $tenancy + * @param \Sprout\Contracts\Tenant|null $tenant + * + * @phpstan-param TenantClass|null $tenant + * + * @return void + */ + private function refreshSessionStore(?Tenancy $tenancy = null, ?Tenant $tenant = null): void + { + // We only want to touch this if the session manager has actually been + // loaded, and is therefore most likely being used + if (app()->resolved('session')) { + $manager = app('session'); + + // If there are no loaded drivers, we can exit early + if (empty($manager->getDrivers())) { + return; + } + + /** @var \Illuminate\Session\Store $driver */ + $driver = $manager->driver(); + $handler = $driver->getHandler(); + + if ($handler instanceof TenantAware) { + // If the handler is one of our tenant-aware boyos, we'll set + // the tenancy and tenant + $handler->setTenancy($tenancy)->setTenant($tenant); + + // Unfortunately, we can't call 'loadSession', so we have to settle + // for start + $driver->start(); + } + } + } } diff --git a/tests/Unit/Overrides/SessionOverrideTest.php b/tests/Unit/Overrides/SessionOverrideTest.php index f23efb4..ca36b06 100644 --- a/tests/Unit/Overrides/SessionOverrideTest.php +++ b/tests/Unit/Overrides/SessionOverrideTest.php @@ -10,6 +10,7 @@ use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; +use Sprout\Contracts\TenantAware; use Sprout\Events\ServiceOverrideBooted; use Sprout\Events\ServiceOverrideRegistered; use Sprout\Overrides\SessionOverride; @@ -58,6 +59,7 @@ public function performsSetup(): void { $sprout = sprout(); + config()->set('session.driver', 'file'); config()->set('sprout.overrides', [ 'session' => [ 'driver' => SessionOverride::class, @@ -66,27 +68,32 @@ public function performsSetup(): void $this->assertFalse(sprout()->settings()->has('original.session')); - $this->instance(SessionManager::class, Mockery::mock(SessionManager::class, function (MockInterface $mock) { - $mock->makePartial(); - $mock->shouldReceive('forgetDrivers')->once(); - })); - $this->assertNull(sprout()->settings()->getUrlPath()); $this->assertNull(sprout()->settings()->getUrlDomain()); $this->assertNull(sprout()->settings()->shouldCookieBeSecure()); $this->assertNull(sprout()->settings()->getCookieSameSite()); + $session = app()->make('session'); + + $this->assertEmpty($session->getDrivers()); + $tenant = TenantModel::factory()->createOne(); $tenancy = $sprout->tenancies()->get(); - $tenancy->setTenant($tenant); - $sprout->overrides()->registerOverrides(); - app()->make('session'); + $sprout->setCurrentTenancy($tenancy); + + $tenancy->setTenant($tenant); $override = $sprout->overrides()->get('session'); + $driver = $session->driver(); + + $this->assertNotEmpty($session->getDrivers()); + $this->assertInstanceOf(TenantAware::class, $driver->getHandler()); + $this->assertTrue($driver->getHandler()->hasTenant()); + $this->assertTrue($driver->getHandler()->hasTenancy()); $this->assertInstanceOf(SessionOverride::class, $override); $override->setup($tenancy, $tenant); @@ -97,6 +104,7 @@ public function performsCleanup(): void { $sprout = sprout(); + config()->set('session.driver', 'file'); config()->set('sprout.overrides', [ 'session' => [ 'driver' => SessionOverride::class, @@ -105,10 +113,7 @@ public function performsCleanup(): void $this->assertFalse(sprout()->settings()->has('original.session')); - $this->instance(SessionManager::class, Mockery::mock(SessionManager::class, function (MockInterface $mock) { - $mock->makePartial(); - $mock->shouldReceive('forgetDrivers')->once(); - })); + $session = app()->make('session'); $tenant = TenantModel::factory()->createOne(); $tenancy = $sprout->tenancies()->get(); @@ -120,7 +125,12 @@ public function performsCleanup(): void app()->make('session'); $override = $sprout->overrides()->get('session'); + $driver = $session->driver(); + $this->assertNotEmpty($session->getDrivers()); + $this->assertInstanceOf(TenantAware::class, $driver->getHandler()); + $this->assertFalse($driver->getHandler()->hasTenant()); + $this->assertFalse($driver->getHandler()->hasTenancy()); $this->assertInstanceOf(SessionOverride::class, $override); $override->cleanup($tenancy, $tenant); From 20d5bccbd6eca91b8441d390d3008d1f20cf6bdd Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Wed, 22 Jan 2025 23:28:03 +0000 Subject: [PATCH 36/48] chore: Make override filesystem manager aware of whether it override an existing original --- .../Filesystem/SproutFilesystemManager.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Overrides/Filesystem/SproutFilesystemManager.php b/src/Overrides/Filesystem/SproutFilesystemManager.php index bdc8efe..967ad7b 100644 --- a/src/Overrides/Filesystem/SproutFilesystemManager.php +++ b/src/Overrides/Filesystem/SproutFilesystemManager.php @@ -9,6 +9,8 @@ final class SproutFilesystemManager extends FilesystemManager { + protected bool $syncedFromOriginal = false; + public function __construct($app, ?FilesystemManager $original = null) { parent::__construct($app); @@ -18,6 +20,16 @@ public function __construct($app, ?FilesystemManager $original = null) } } + /** + * Check if this manager override was synced from the original + * + * @return bool + */ + public function wasSyncedFromOriginal(): bool + { + return $this->syncedFromOriginal; + } + /** * Sync the original manager in case things have been registered * @@ -27,8 +39,9 @@ public function __construct($app, ?FilesystemManager $original = null) */ private function syncOriginal(FilesystemManager $original): void { - $this->disks = array_merge($original->disks, $this->disks); - $this->customCreators = array_merge($original->customCreators, $this->customCreators); + $this->disks = array_merge($original->disks, $this->disks); + $this->customCreators = array_merge($original->customCreators, $this->customCreators); + $this->syncedFromOriginal = true; } /** From b1ea583c4615864881a5668781e83ccf2a404ee5 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Wed, 22 Jan 2025 23:29:06 +0000 Subject: [PATCH 37/48] chore: Single for tenancy generic with phpstan and tenant-aware implementation --- src/Concerns/AwareOfTenant.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Concerns/AwareOfTenant.php b/src/Concerns/AwareOfTenant.php index 19c41f3..1f9d1c4 100644 --- a/src/Concerns/AwareOfTenant.php +++ b/src/Concerns/AwareOfTenant.php @@ -11,8 +11,14 @@ */ trait AwareOfTenant { + /** + * @var \Sprout\Contracts\Tenant|null + */ private ?Tenant $tenant; + /** + * @var \Sprout\Contracts\Tenancy<*>|null + */ private ?Tenancy $tenancy; /** From 7ad7dbfd37224a6ed7df2ac72b5a2dfd27244389 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Thu, 23 Jan 2025 20:32:30 +0000 Subject: [PATCH 38/48] test(overrides): Completed the tests of the session service override --- .../Session/SproutDatabaseSessionHandler.php | 62 +++-- ...> SproutDatabaseSessionHandlerCreator.php} | 10 +- .../Session/SproutFileSessionHandler.php | 2 + ...hp => SproutFileSessionHandlerCreator.php} | 12 +- src/Overrides/SessionOverride.php | 10 +- ...proutDatabaseSessionHandlerCreatorTest.php | 79 ++++++ .../SproutDatabaseSessionHandlerTest.php | 262 ++++++++++++++++++ .../SproutFileSessionHandlerCreatorTest.php | 77 +++++ .../Session/SproutFileSessionHandlerTest.php | 132 +++++++++ tests/Unit/Overrides/SessionOverrideTest.php | 125 ++++++++- 10 files changed, 726 insertions(+), 45 deletions(-) rename src/Overrides/Session/{SproutSessionDatabaseDriverCreator.php => SproutDatabaseSessionHandlerCreator.php} (79%) rename src/Overrides/Session/{SproutSessionFileDriverCreator.php => SproutFileSessionHandlerCreator.php} (74%) create mode 100644 tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php create mode 100644 tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php create mode 100644 tests/Unit/Overrides/Session/SproutFileSessionHandlerCreatorTest.php create mode 100644 tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php diff --git a/src/Overrides/Session/SproutDatabaseSessionHandler.php b/src/Overrides/Session/SproutDatabaseSessionHandler.php index a3d5942..65d766b 100644 --- a/src/Overrides/Session/SproutDatabaseSessionHandler.php +++ b/src/Overrides/Session/SproutDatabaseSessionHandler.php @@ -4,12 +4,11 @@ namespace Sprout\Overrides\Session; use Illuminate\Database\Query\Builder; +use Illuminate\Database\QueryException; use Illuminate\Session\DatabaseSessionHandler; +use Illuminate\Support\Arr; use Sprout\Concerns\AwareOfTenant; use Sprout\Contracts\TenantAware; -use Sprout\Exceptions\TenancyMissingException; -use Sprout\Exceptions\TenantMissingException; -use function Sprout\sprout; /** * Sprout Database Session Handler @@ -32,23 +31,28 @@ class SproutDatabaseSessionHandler extends DatabaseSessionHandler implements Ten * @throws \Sprout\Exceptions\TenantMissingException * @throws \Sprout\Exceptions\TenancyMissingException */ - protected function getQuery(): Builder + protected function getQuery(?bool $write = false): Builder { if (! $this->hasTenant()) { return parent::getQuery(); } $tenancy = $this->getTenancy(); - $tenant = $this->getTenant(); + $tenant = $this->getTenant(); /** * @var \Sprout\Contracts\Tenancy<*> $tenancy * @var \Sprout\Contracts\Tenant $tenant */ - return parent::getQuery() - ->where('tenancy', '=', $tenancy->getName()) - ->where('tenant_id', '=', $tenant->getTenantKey()); + $query = parent::getQuery(); + + if ($write === false) { + return $query->where('tenancy', '=', $tenancy->getName()) + ->where('tenant_id', '=', $tenant->getTenantKey()); + } + + return $query; } /** @@ -61,21 +65,39 @@ protected function getQuery(): Builder */ protected function performInsert($sessionId, $payload): ?bool { - if (! $this->hasTenant()) { - return parent::performInsert($sessionId, $payload); - } + if ($this->hasTenant()) { + $tenancy = $this->getTenancy(); + $tenant = $this->getTenant(); - $tenancy = $this->getTenancy(); - $tenant = $this->getTenant(); + /** + * @var \Sprout\Contracts\Tenancy<*> $tenancy + * @var \Sprout\Contracts\Tenant $tenant + */ - /** - * @var \Sprout\Contracts\Tenancy<*> $tenancy - * @var \Sprout\Contracts\Tenant $tenant - */ + $payload['tenancy'] = $tenancy->getName(); + $payload['tenant_id'] = $tenant->getTenantKey(); + } - $payload['tenancy'] = $tenancy->getName(); - $payload['tenant_id'] = $tenant->getTenantKey(); + try { + return $this->getQuery(true)->insert(Arr::set($payload, 'id', $sessionId)); + } catch (QueryException) { // @codeCoverageIgnore + $this->performUpdate($sessionId, $payload); // @codeCoverageIgnore + } + } - return parent::performInsert($sessionId, $payload); + /** + * Perform an update operation on the session ID. + * + * @param string $sessionId + * @param array $payload + * + * @return int + * + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + */ + protected function performUpdate($sessionId, $payload): int + { + return $this->getQuery(true)->where('id', $sessionId)->update($payload); } } diff --git a/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php b/src/Overrides/Session/SproutDatabaseSessionHandlerCreator.php similarity index 79% rename from src/Overrides/Session/SproutSessionDatabaseDriverCreator.php rename to src/Overrides/Session/SproutDatabaseSessionHandlerCreator.php index b254e5f..5bc5c1a 100644 --- a/src/Overrides/Session/SproutSessionDatabaseDriverCreator.php +++ b/src/Overrides/Session/SproutDatabaseSessionHandlerCreator.php @@ -4,10 +4,9 @@ namespace Sprout\Overrides\Session; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Session\DatabaseSessionHandler; use Sprout\Sprout; -final class SproutSessionDatabaseDriverCreator +final class SproutDatabaseSessionHandlerCreator { /** * @var \Illuminate\Contracts\Foundation\Application @@ -33,14 +32,11 @@ public function __construct(Application $app, Sprout $sprout) /** * Create the tenant-aware session database driver * - * @return \Illuminate\Session\DatabaseSessionHandler + * @return \Sprout\Overrides\Session\SproutDatabaseSessionHandler * * @throws \Illuminate\Contracts\Container\BindingResolutionException - * @throws \Sprout\Exceptions\MisconfigurationException - * @throws \Sprout\Exceptions\TenancyMissingException - * @throws \Sprout\Exceptions\TenantMissingException */ - public function __invoke(): DatabaseSessionHandler + public function __invoke(): SproutDatabaseSessionHandler { $table = config('session.table'); $lifetime = config('session.lifetime'); diff --git a/src/Overrides/Session/SproutFileSessionHandler.php b/src/Overrides/Session/SproutFileSessionHandler.php index 3fb9ff3..1e73440 100644 --- a/src/Overrides/Session/SproutFileSessionHandler.php +++ b/src/Overrides/Session/SproutFileSessionHandler.php @@ -89,6 +89,7 @@ public function destroy($sessionId): bool */ public function gc($lifetime): int { + // @codeCoverageIgnoreStart $files = Finder::create() ->in($this->getPath()) ->files() @@ -103,5 +104,6 @@ public function gc($lifetime): int } return $deletedSessions; + // @codeCoverageIgnoreEnd } } diff --git a/src/Overrides/Session/SproutSessionFileDriverCreator.php b/src/Overrides/Session/SproutFileSessionHandlerCreator.php similarity index 74% rename from src/Overrides/Session/SproutSessionFileDriverCreator.php rename to src/Overrides/Session/SproutFileSessionHandlerCreator.php index 72f5338..82748b5 100644 --- a/src/Overrides/Session/SproutSessionFileDriverCreator.php +++ b/src/Overrides/Session/SproutFileSessionHandlerCreator.php @@ -4,10 +4,9 @@ namespace Sprout\Overrides\Session; use Illuminate\Contracts\Foundation\Application; -use Illuminate\Session\FileSessionHandler; use Sprout\Sprout; -final class SproutSessionFileDriverCreator +final class SproutFileSessionHandlerCreator { /** * @var \Illuminate\Contracts\Foundation\Application @@ -33,18 +32,15 @@ public function __construct(Application $app, Sprout $sprout) /** * Create the tenant-aware session file driver * - * @return \Illuminate\Session\FileSessionHandler + * @return \Sprout\Overrides\Session\SproutFileSessionHandler * - * @throws \Sprout\Exceptions\MisconfigurationException - * @throws \Sprout\Exceptions\TenancyMissingException - * @throws \Sprout\Exceptions\TenantMissingException * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - public function __invoke(): FileSessionHandler + public function __invoke(): SproutFileSessionHandler { /** @var string $originalPath */ $originalPath = config('session.files'); - $path = rtrim($originalPath, '/') . DIRECTORY_SEPARATOR; + $path = rtrim($originalPath, '/'); /** @var int $lifetime */ $lifetime = config('session.lifetime'); diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index 1f9d140..701a285 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -10,8 +10,8 @@ use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Contracts\TenantAware; -use Sprout\Overrides\Session\SproutSessionDatabaseDriverCreator; -use Sprout\Overrides\Session\SproutSessionFileDriverCreator; +use Sprout\Overrides\Session\SproutDatabaseSessionHandlerCreator; +use Sprout\Overrides\Session\SproutFileSessionHandlerCreator; use Sprout\Sprout; use Sprout\Support\Settings; use function Sprout\settings; @@ -54,7 +54,7 @@ public function boot(Application $app, Sprout $sprout): void protected function addDriver(SessionManager $manager, Application $app, Sprout $sprout): void { - $creator = new SproutSessionFileDriverCreator($app, $sprout); + $creator = new SproutFileSessionHandlerCreator($app, $sprout); $manager->extend('file', $creator(...)); $manager->extend('native', $creator(...)); @@ -62,8 +62,8 @@ protected function addDriver(SessionManager $manager, Application $app, Sprout $ /** @var bool $overrideDatabase */ $overrideDatabase = $this->config['database'] ?? true; - if (settings()->shouldNotOverrideTheDatabase($overrideDatabase) === false) { - $manager->extend('database', (new SproutSessionDatabaseDriverCreator($app, $sprout))(...)); + if ($sprout->settings()->shouldNotOverrideTheDatabase($overrideDatabase) === false) { + $manager->extend('database', (new SproutDatabaseSessionHandlerCreator($app, $sprout))(...)); } } diff --git a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php new file mode 100644 index 0000000..766cd75 --- /dev/null +++ b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php @@ -0,0 +1,79 @@ +set('sprout.overrides', []); + }); + } + + #[Test] + public function canCreateTheDatabaseHandler(): void + { + $connection = Mockery::mock(Connection::class); + $this->swap('db', Mockery::mock(DatabaseManager::class, static function (Mockery\MockInterface $mock) use ($connection) { + $mock->shouldReceive('connection')->with(null)->andReturn($connection)->once(); + })); + + $creator = new SproutDatabaseSessionHandlerCreator( + $this->app, + $this->app->make(Sprout::class) + ); + + $handler = $creator(); + + $this->assertInstanceOf(SproutDatabaseSessionHandler::class, $handler); + $this->assertFalse($handler->hasTenancy()); + $this->assertFalse($handler->hasTenant()); + } + + #[Test] + public function canCreateTheFileHandlerWithTenantContext(): void + { + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class)->makePartial(); + $sprout = sprout(); + + $tenancy->shouldReceive('tenant')->andReturn($tenant)->once(); + + $sprout->setCurrentTenancy($tenancy); + + $connection = Mockery::mock(Connection::class); + $this->swap('db', Mockery::mock(DatabaseManager::class, static function (Mockery\MockInterface $mock) use ($connection) { + $mock->shouldReceive('connection')->with(null)->andReturn($connection)->once(); + })); + + $creator = new SproutDatabaseSessionHandlerCreator( + $this->app, + $this->app->make(Sprout::class) + ); + + $handler = $creator(); + + $this->assertInstanceOf(SproutDatabaseSessionHandler::class, $handler); + $this->assertTrue($handler->hasTenancy()); + $this->assertTrue($handler->hasTenant()); + $this->assertSame($tenancy, $handler->getTenancy()); + $this->assertSame($tenant, $handler->getTenant()); + } +} diff --git a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php new file mode 100644 index 0000000..36d1cea --- /dev/null +++ b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php @@ -0,0 +1,262 @@ +set('sprout.overrides', []); + }); + } + + protected function createHandler(?Tenancy $tenancy = null, ?Tenant $tenant = null, ?Builder $builder = null): SproutDatabaseSessionHandler + { + $lifetime = config('session.lifetime'); + $connection = Mockery::mock(Connection::class, static function (Mockery\MockInterface $mock) use ($builder) { + if ($builder !== null) { + $mock->shouldReceive('table')->withArgs(['my_tenant_table'])->andReturn($builder)->atLeast()->once(); + } + }); + + $handler = new SproutDatabaseSessionHandler( + $connection, + 'my_tenant_table', + $lifetime + ); + + if ($tenancy && $tenant) { + $handler->setTenancy($tenancy)->setTenant($tenant); + } + + return $handler; + } + + protected function mockTenancyAndTenant(?Tenancy $tenancy = null, ?Tenant $tenant = null): void + { + if ($tenancy !== null && $tenant !== null) { + /** + * @var \Mockery\MockInterface|Tenancy $tenancy + * @var \Mockery\MockInterface|Tenant $tenant + */ + $tenancy->shouldReceive('getName')->atLeast()->once()->andReturn('my-tenancy'); + $tenant->shouldReceive('getTenantKey')->atLeast()->once()->andReturn(777); + } + } + + private function mockQuery($sessionId, ?Tenancy $tenancy, ?Tenant $tenant, $returnValue, bool $find = true): Mockery\MockInterface&Builder + { + return Mockery::mock(Builder::class, static function (Mockery\MockInterface $mock) use ($sessionId, $tenancy, $tenant, $returnValue, $find) { + if ($tenancy !== null) { + $mock->shouldReceive('where')->withArgs(['tenancy', '=', $tenancy->getName()])->andReturnSelf()->once(); + $mock->shouldReceive('where')->withArgs(['tenant_id', '=', $tenant->getTenantKey()])->andReturnSelf()->once(); + } + + if ($find) { + $mock->shouldReceive('find')->withArgs([$sessionId])->andReturn($returnValue)->once(); + } + }); + } + + #[Test] + public function hasTheCorrectTenancyState(): void + { + $handler = $this->createHandler(); + + $this->assertInstanceOf(SproutDatabaseSessionHandler::class, $handler); + $this->assertFalse($handler->hasTenancy()); + $this->assertFalse($handler->hasTenant()); + } + + #[Test] + public function canCreateTheDatabaseHandlerWithTenantContext(): void + { + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class)->makePartial(); + + $handler = $this->createHandler($tenancy, $tenant); + + $this->assertInstanceOf(SproutDatabaseSessionHandler::class, $handler); + $this->assertTrue($handler->hasTenancy()); + $this->assertTrue($handler->hasTenant()); + $this->assertSame($tenancy, $handler->getTenancy()); + $this->assertSame($tenant, $handler->getTenant()); + } + + #[Test, DataProvider('databaseSessionDataProvider')] + public function canReadFromValidSession(?Tenancy $tenancy, ?Tenant $tenant): void + { + $sessionId = 'my-session-id'; + + $this->mockTenancyAndTenant($tenancy, $tenant); + + $handler = $this->createHandler( + $tenancy, + $tenant, + $this->mockQuery($sessionId, $tenancy, $tenant, (object)['payload' => base64_encode('my-session-data')]) + )->setTenancy($tenancy)->setTenant($tenant); + + $this->assertSame('my-session-data', $handler->read($sessionId)); + } + + #[Test, DataProvider('databaseSessionDataProvider')] + public function doesNotReadFromFilesystemWhenSessionIsInvalid(?Tenancy $tenancy, ?Tenant $tenant): void + { + $sessionId = 'my-session-id'; + + $this->mockTenancyAndTenant($tenancy, $tenant); + + $handler = $this->createHandler( + $tenancy, + $tenant, + $this->mockQuery($sessionId, $tenancy, $tenant, null) + )->setTenancy($tenancy)->setTenant($tenant); + + $this->assertEmpty($handler->read($sessionId)); + } + + #[Test, DataProvider('databaseSessionDataProvider')] + public function doesNotReadFromFilesystemWhenSessionHasExpired(?Tenancy $tenancy, ?Tenant $tenant): void + { + $sessionId = 'my-session-id'; + + $this->mockTenancyAndTenant($tenancy, $tenant); + + $handler = $this->createHandler( + $tenancy, + $tenant, + $this->mockQuery($sessionId, $tenancy, $tenant, (object)[ + 'payload' => base64_encode('my-session-data'), + 'last_activity' => Carbon::now()->subHours(10)->getTimestamp(), + ]) + )->setTenancy($tenancy)->setTenant($tenant); + + $this->assertEmpty($handler->read($sessionId)); + } + + #[Test, DataProvider('databaseSessionDataProvider')] + public function canWriteNewSessionData(?Tenancy $tenancy, ?Tenant $tenant): void + { + $sessionId = 'my-session-id'; + + $this->mockTenancyAndTenant($tenancy, $tenant); + + $query = $this->mockQuery($sessionId, $tenancy, $tenant, null); + + if ($tenancy !== null) { + $query->shouldReceive('insert')->withArgs([ + [ + 'id' => $sessionId, + 'payload' => base64_encode('my-session-data'), + 'last_activity' => Carbon::now()->getTimestamp(), + 'tenancy' => 'my-tenancy', + 'tenant_id' => 777, + ], + ])->once(); + } else { + $query->shouldReceive('insert')->withArgs([ + [ + 'id' => $sessionId, + 'payload' => base64_encode('my-session-data'), + 'last_activity' => Carbon::now()->getTimestamp(), + ], + ])->once(); + } + + $handler = $this->createHandler( + $tenancy, + $tenant, + $query + )->setTenancy($tenancy)->setTenant($tenant); + + $handler->write($sessionId, 'my-session-data'); + } + + #[Test, DataProvider('databaseSessionDataProvider')] + public function canWriteExistingSessionData(?Tenancy $tenancy, ?Tenant $tenant): void + { + $sessionId = 'my-session-id'; + + $this->mockTenancyAndTenant($tenancy, $tenant); + + $query = $this->mockQuery($sessionId, $tenancy, $tenant, (object)['payload' => base64_encode('my-session-data')]); + + $query->shouldReceive('where')->withArgs(['id', $sessionId])->andReturnSelf()->once(); + + if ($tenancy !== null) { + $query->shouldReceive('update')->withArgs([ + [ + 'payload' => base64_encode('my-session-data'), + 'last_activity' => Carbon::now()->getTimestamp(), + ], + ])->andReturn(1)->once(); + } else { + $query->shouldReceive('update')->withArgs([ + [ + 'payload' => base64_encode('my-session-data'), + 'last_activity' => Carbon::now()->getTimestamp(), + ], + ])->andReturn(1)->once(); + } + + $handler = $this->createHandler( + $tenancy, + $tenant, + $query + )->setTenancy($tenancy)->setTenant($tenant); + + $handler->write($sessionId, 'my-session-data'); + } + + #[Test, DataProvider('databaseSessionDataProvider')] + public function canDestroySessionData($tenancy, $tenant): void + { + $sessionId = 'my-session-id'; + + $this->mockTenancyAndTenant($tenancy, $tenant); + + $query = $this->mockQuery($sessionId, $tenancy, $tenant, (object)['payload' => base64_encode('my-session-data')], false); + + $query->shouldNotReceive('find'); + $query->shouldReceive('where')->withArgs(['id', $sessionId])->andReturnSelf()->once(); + + if ($tenancy !== null) { + $query->shouldReceive('delete')->andReturn(1)->once(); + } else { + $query->shouldReceive('delete')->andReturn(1)->once(); + } + + $handler = $this->createHandler( + $tenancy, + $tenant, + $query + )->setTenancy($tenancy)->setTenant($tenant); + + $handler->destroy($sessionId); + } + + public static function databaseSessionDataProvider(): array + { + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class)->makePartial(); + + return [ + [null, null], + [$tenancy, $tenant], + ]; + } +} diff --git a/tests/Unit/Overrides/Session/SproutFileSessionHandlerCreatorTest.php b/tests/Unit/Overrides/Session/SproutFileSessionHandlerCreatorTest.php new file mode 100644 index 0000000..d0a13f6 --- /dev/null +++ b/tests/Unit/Overrides/Session/SproutFileSessionHandlerCreatorTest.php @@ -0,0 +1,77 @@ +set('sprout.overrides', []); + }); + } + + #[Test] + public function canCreateTheFileHandler(): void + { + $creator = new SproutFileSessionHandlerCreator( + $this->app, + $this->app->make(Sprout::class) + ); + + $handler = $creator(); + + $this->assertInstanceOf(SproutFileSessionHandler::class, $handler); + $this->assertFalse($handler->hasTenancy()); + $this->assertFalse($handler->hasTenant()); + + // Assert that the path is the default value + $defaultPath = rtrim(config('session.files'), '/'); + $this->assertEquals($defaultPath, $handler->getPath()); + } + + #[Test] + public function canCreateTheFileHandlerWithTenantContext(): void + { + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class)->makePartial(); + $sprout = sprout(); + + $tenancy->shouldReceive('tenant')->andReturn($tenant)->once(); + $tenant->shouldReceive('getTenantResourceKey')->andReturn('tenant-resource-key')->once(); + + $sprout->setCurrentTenancy($tenancy); + + $creator = new SproutFileSessionHandlerCreator( + $this->app, + $this->app->make(Sprout::class) + ); + + $handler = $creator(); + + $this->assertInstanceOf(SproutFileSessionHandler::class, $handler); + $this->assertTrue($handler->hasTenancy()); + $this->assertTrue($handler->hasTenant()); + $this->assertSame($tenancy, $handler->getTenancy()); + $this->assertSame($tenant, $handler->getTenant()); + + // Assert that the path is not the default value + $defaultPath = rtrim(config('session.files'), '/'); + $handlerPath = $handler->getPath(); + + $this->assertNotEquals($defaultPath, $handlerPath); + $this->assertEquals($defaultPath . DIRECTORY_SEPARATOR . 'tenant-resource-key', $handlerPath); + } +} diff --git a/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php new file mode 100644 index 0000000..2171022 --- /dev/null +++ b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php @@ -0,0 +1,132 @@ +set('sprout.overrides', []); + }); + } + + protected function createHandler(?Tenancy $tenancy = null, ?Tenant $tenant = null, ?Filesystem $files = null): SproutFileSessionHandler + { + $defaultPath = '/default/path'; + $lifetime = config('session.lifetime'); + $files ??= Mockery::mock(Filesystem::class); + + $handler = new SproutFileSessionHandler($files, $defaultPath, $lifetime); + + if ($tenancy && $tenant) { + $handler->setTenancy($tenancy)->setTenant($tenant); + } + + return $handler; + } + + #[Test] + public function hasTheCorrectPathAndTenancyState(): void + { + $handler = $this->createHandler(); + + $this->assertInstanceOf(SproutFileSessionHandler::class, $handler); + $this->assertFalse($handler->hasTenancy()); + $this->assertFalse($handler->hasTenant()); + $this->assertEquals('/default/path', $handler->getPath()); + } + + #[Test] + public function canCreateTheFileHandlerWithTenantContext(): void + { + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class)->makePartial(); + + $tenant->shouldReceive('getTenantResourceKey')->andReturn('tenant-resource-key')->once(); + + $handler = $this->createHandler($tenancy, $tenant); + + $this->assertInstanceOf(SproutFileSessionHandler::class, $handler); + $this->assertTrue($handler->hasTenancy()); + $this->assertTrue($handler->hasTenant()); + $this->assertSame($tenancy, $handler->getTenancy()); + $this->assertSame($tenant, $handler->getTenant()); + $this->assertEquals('/default/path' . DIRECTORY_SEPARATOR . 'tenant-resource-key', $handler->getPath()); + } + + #[Test, DataProvider('fileSessionDataProvider')] + public function canReadFromValidSession(?Tenancy $tenancy, ?Tenant $tenant, string $expectedPath): void + { + $sessionId = 'my-session-id'; + + $handler = $this->createHandler($tenancy, $tenant, Mockery::mock(Filesystem::class, function (Mockery\MockInterface $mock) use ($expectedPath, $sessionId) { + $mock->shouldReceive('isFile')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId])->andReturn(true)->once(); + $mock->shouldReceive('lastModified')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId])->andReturn(Carbon::now()->subHour()->timestamp)->once(); + $mock->shouldReceive('sharedGet')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId])->andReturn('my-session-data')->once(); + }))->setTenancy($tenancy)->setTenant($tenant); + + $this->assertSame('my-session-data', $handler->read($sessionId)); + } + + #[Test, DataProvider('fileSessionDataProvider')] + public function doesNotReadFromFilesystemWhenSessionIsInvalidOrTooOld(?Tenancy $tenancy, ?Tenant $tenant, string $expectedPath): void + { + $sessionId = 'my-session-id'; + + $handler = $this->createHandler($tenancy, $tenant, Mockery::mock(Filesystem::class, function (Mockery\MockInterface $mock) use ($expectedPath, $sessionId) { + $mock->shouldReceive('isFile')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId])->andReturn(false)->once(); + }))->setTenancy($tenancy)->setTenant($tenant); + + $this->assertEmpty($handler->read($sessionId)); + } + + #[Test, DataProvider('fileSessionDataProvider')] + public function canWriteSessionData(?Tenancy $tenancy, ?Tenant $tenant, string $expectedPath): void + { + $sessionId = 'my-session-id'; + + $handler = $this->createHandler($tenancy, $tenant, Mockery::mock(Filesystem::class, function (Mockery\MockInterface $mock) use ($expectedPath, $sessionId) { + $mock->shouldReceive('put')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId, 'my-session-data', true])->once(); + }))->setTenancy($tenancy)->setTenant($tenant); + + $handler->write($sessionId, 'my-session-data'); + } + + #[Test, DataProvider('fileSessionDataProvider')] + public function canDestroySessionData($tenancy, $tenant, $expectedPath): void + { + $sessionId = 'my-session-id'; + + $handler = $this->createHandler($tenancy, $tenant, Mockery::mock(Filesystem::class, function (Mockery\MockInterface $mock) use ($expectedPath, $sessionId) { + $mock->shouldReceive('delete')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId])->once(); + }))->setTenancy($tenancy)->setTenant($tenant); + + $handler->destroy($sessionId); + } + + public static function fileSessionDataProvider(): array + { + $defaultPath = '/default/path'; + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class)->makePartial(); + $tenant->shouldReceive('getTenantResourceKey')->andReturn('tenant-resource-key'); + + return [ + [null, null, $defaultPath], + [$tenancy, $tenant, $defaultPath . DIRECTORY_SEPARATOR . 'tenant-resource-key'], + ]; + } +} diff --git a/tests/Unit/Overrides/SessionOverrideTest.php b/tests/Unit/Overrides/SessionOverrideTest.php index ca36b06..c5ed631 100644 --- a/tests/Unit/Overrides/SessionOverrideTest.php +++ b/tests/Unit/Overrides/SessionOverrideTest.php @@ -3,18 +3,20 @@ namespace Sprout\Tests\Unit\Overrides; +use Closure; use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; use Illuminate\Session\SessionManager; -use Illuminate\Support\Facades\Event; use Mockery; use Mockery\MockInterface; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\TenantAware; -use Sprout\Events\ServiceOverrideBooted; -use Sprout\Events\ServiceOverrideRegistered; use Sprout\Overrides\SessionOverride; -use Sprout\Support\Services; +use Sprout\Sprout; +use Sprout\Support\Settings; +use Sprout\Support\SettingsRepository; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; use function Sprout\settings; @@ -29,6 +31,45 @@ protected function defineEnvironment($app): void }); } + protected function mockSessionManager(bool $database, bool $forget = true): SessionManager&MockInterface + { + return Mockery::mock(SessionManager::class, static function (MockInterface $mock) use ($database, $forget) { + $mock->shouldReceive('extend') + ->withArgs([ + Mockery::on(static function ($arg) { + return $arg === 'file' || $arg === 'native'; + }), + Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + }), + ]) + ->twice(); + + if ($database) { + $mock->shouldNotReceive('extend') + ->withArgs([ + 'database', + Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + }), + ]); + } else { + $mock->shouldReceive('extend') + ->withArgs([ + 'database', + Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + }), + ]) + ->once(); + } + + if ($forget) { + $mock->shouldReceive('forgetDrivers')->once(); + } + }); + } + #[Test] public function isBuiltCorrectly(): void { @@ -125,7 +166,7 @@ public function performsCleanup(): void app()->make('session'); $override = $sprout->overrides()->get('session'); - $driver = $session->driver(); + $driver = $session->driver(); $this->assertNotEmpty($session->getDrivers()); $this->assertInstanceOf(TenantAware::class, $driver->getHandler()); @@ -178,4 +219,78 @@ public function setSessionConfigFromSproutSettings(): void $this->assertTrue(config('session.secure')); $this->assertSame('strict', config('session.same_site')); } + + #[Test, DataProvider('overrideDatabaseSetting')] + public function bootsCorrectlyWhenSessionManagerHasNotBeenResolved(bool $database): void + { + $override = new SessionOverride('session', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + $mock->shouldReceive('resolved')->withArgs(['session'])->andReturnFalse()->once(); + $mock->shouldReceive('afterResolving') + ->withArgs([ + 'session', + Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + }), + ]) + ->once(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $override->boot($app, $sprout); + } + + #[Test, DataProvider('overrideDatabaseSetting')] + public function bootsCorrectlyWhenSessionManagerHasBeenResolved(bool $database): void + { + $override = new SessionOverride('session', []); + + $app = Mockery::mock(Application::class, function (MockInterface $mock) use ($database) { + $mock->shouldReceive('resolved')->withArgs(['session'])->andReturnTrue()->once(); + $mock->shouldReceive('make') + ->withArgs(['session']) + ->andReturn($this->mockSessionManager($database)) + ->once(); + }); + + $sprout = new Sprout($app, new SettingsRepository([ + Settings::NO_DATABASE_OVERRIDE => $database, + ])); + + $override->boot($app, $sprout); + } + + #[Test, DataProvider('overrideDatabaseSetting')] + public function addsDriverSessionManagerHasBeenResolved(bool $database): void + { + $override = new SessionOverride('session', []); + + $app = Mockery::mock(Application::class, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $app->singleton('session', function () use ($database) { + return $this->mockSessionManager($database, false); + }); + + $sprout = new Sprout($app, new SettingsRepository([ + Settings::NO_DATABASE_OVERRIDE => $database, + ])); + + $override->boot($app, $sprout); + + $app->make('session'); + } + + public static function overrideDatabaseSetting(): array + { + return [ + [true], + [false], + ]; + } } From 1ae9a62dcf3c2a2fe5d6f3e328ceaef186bf81cf Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Thu, 23 Jan 2025 22:16:28 +0000 Subject: [PATCH 39/48] test(overrides): A few QOL fixes for the session override tests --- .../Overrides/Session/SproutDatabaseSessionHandlerTest.php | 4 ++-- tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php | 4 ++-- tests/Unit/Overrides/SessionOverrideTest.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php index 36d1cea..6d5fe08 100644 --- a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php +++ b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php @@ -255,8 +255,8 @@ public static function databaseSessionDataProvider(): array $tenant = Mockery::mock(Tenant::class)->makePartial(); return [ - [null, null], - [$tenancy, $tenant], + 'outside of tenant context' => [null, null], + 'inside of tenant context' => [$tenancy, $tenant], ]; } } diff --git a/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php index 2171022..d344974 100644 --- a/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php +++ b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php @@ -125,8 +125,8 @@ public static function fileSessionDataProvider(): array $tenant->shouldReceive('getTenantResourceKey')->andReturn('tenant-resource-key'); return [ - [null, null, $defaultPath], - [$tenancy, $tenant, $defaultPath . DIRECTORY_SEPARATOR . 'tenant-resource-key'], + 'outside of tenant context' => [null, null, $defaultPath], + 'inside of tenant context' => [$tenancy, $tenant, $defaultPath . DIRECTORY_SEPARATOR . 'tenant-resource-key'], ]; } } diff --git a/tests/Unit/Overrides/SessionOverrideTest.php b/tests/Unit/Overrides/SessionOverrideTest.php index c5ed631..aef1dd3 100644 --- a/tests/Unit/Overrides/SessionOverrideTest.php +++ b/tests/Unit/Overrides/SessionOverrideTest.php @@ -289,8 +289,8 @@ public function addsDriverSessionManagerHasBeenResolved(bool $database): void public static function overrideDatabaseSetting(): array { return [ - [true], - [false], + 'do not override the database' => [true], + 'override the database' => [false], ]; } } From 0a3f0bb50a3eb7f75a41076ce09fc0e8b3871b57 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Thu, 23 Jan 2025 22:20:20 +0000 Subject: [PATCH 40/48] test(overrides): Fix database session handler test to account for updated Laravel core --- .../Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php index 6d5fe08..5d48b20 100644 --- a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php +++ b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerTest.php @@ -69,6 +69,8 @@ private function mockQuery($sessionId, ?Tenancy $tenancy, ?Tenant $tenant, $retu if ($find) { $mock->shouldReceive('find')->withArgs([$sessionId])->andReturn($returnValue)->once(); } + + $mock->shouldReceive('useWritePdo')->andReturnSelf(); }); } From 6df3056f8225573035bd567ebddde508c0b37c43 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Thu, 23 Jan 2025 22:21:58 +0000 Subject: [PATCH 41/48] chore(overrides): Fix static analysis issue with overridden DatabaseSessionHandler::performInsert --- src/Overrides/Session/SproutDatabaseSessionHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Overrides/Session/SproutDatabaseSessionHandler.php b/src/Overrides/Session/SproutDatabaseSessionHandler.php index 65d766b..48b50be 100644 --- a/src/Overrides/Session/SproutDatabaseSessionHandler.php +++ b/src/Overrides/Session/SproutDatabaseSessionHandler.php @@ -81,7 +81,7 @@ protected function performInsert($sessionId, $payload): ?bool try { return $this->getQuery(true)->insert(Arr::set($payload, 'id', $sessionId)); } catch (QueryException) { // @codeCoverageIgnore - $this->performUpdate($sessionId, $payload); // @codeCoverageIgnore + return $this->performUpdate($sessionId, $payload) > 0; // @codeCoverageIgnore } } From f3761506f61412e6fd194a739bdc9df70ac5f691 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Fri, 24 Jan 2025 13:27:40 +0000 Subject: [PATCH 42/48] test(overrides): Further tests for auth and session overrides --- infection.json5 | 13 +- .../Auth/SproutAuthCacheTokenRepository.php | 46 +- .../SproutAuthDatabaseTokenRepository.php | 79 ++- .../Auth/SproutAuthPasswordBrokerManager.php | 36 +- src/Overrides/AuthOverride.php | 16 +- src/Overrides/BaseOverride.php | 28 + .../SproutFileSessionHandlerCreator.php | 4 +- .../SproutAuthCacheTokenRepositoryTest.php | 558 ++++++++++++++++++ .../SproutAuthDatabaseTokenRepositoryTest.php | 547 +++++++++++++++++ .../SproutAuthPasswordBrokerManagerTest.php | 206 +++++++ tests/Unit/Overrides/AuthOverrideTest.php | 380 ++++++------ ...proutDatabaseSessionHandlerCreatorTest.php | 58 +- .../Session/SproutFileSessionHandlerTest.php | 4 +- 13 files changed, 1743 insertions(+), 232 deletions(-) create mode 100644 tests/Unit/Overrides/Auth/SproutAuthCacheTokenRepositoryTest.php create mode 100644 tests/Unit/Overrides/Auth/SproutAuthDatabaseTokenRepositoryTest.php create mode 100644 tests/Unit/Overrides/Auth/SproutAuthPasswordBrokerManagerTest.php diff --git a/infection.json5 b/infection.json5 index c8b7518..938097b 100644 --- a/infection.json5 +++ b/infection.json5 @@ -13,10 +13,15 @@ "@default" : true, "ProtectedVisibility": { "ignore": [ - "Sprout\\Concerns\\FindsIdentityRouteParameters::initialiseRouteParameter", - "Sprout\\Concerns\\FindsIdentityRouteParameters::getParameterPatternMapping", - "Sprout\\Concerns\\FindsIdentityRouteParameters::applyParameterMapping", - "Sprout\\Support\\BaseFactory::callCustomCreator" + "Sprout\\Concerns\\FindsIdentityRouteParameters::*", + "Sprout\\Support\\BaseFactory::callCustomCreator", + "Sprout\\Overrides\\Auth\\SproutAuthCacheTokenRepository::getTenantedPrefix", + "Sprout\\Overrides\\Auth\\SproutAuthDatabaseTokenRepository::*" + ] + }, + "PublicVisibility": { + "ignore": [ + "Sprout\\Concerns\\FindsIdentityRouteParameters::*" ] }, "UnwrapUcFirst" : { diff --git a/src/Overrides/Auth/SproutAuthCacheTokenRepository.php b/src/Overrides/Auth/SproutAuthCacheTokenRepository.php index da4a81e..c335aac 100644 --- a/src/Overrides/Auth/SproutAuthCacheTokenRepository.php +++ b/src/Overrides/Auth/SproutAuthCacheTokenRepository.php @@ -4,17 +4,50 @@ namespace Sprout\Overrides\Auth; use Illuminate\Auth\Passwords\CacheTokenRepository; +use Illuminate\Cache\Repository; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; +use Illuminate\Contracts\Hashing\Hasher as HasherContract; use Illuminate\Support\Carbon; use Illuminate\Support\Str; use SensitiveParameter; use Sprout\Contracts\TenantHasResources; use Sprout\Exceptions\TenancyMissingException; use Sprout\Exceptions\TenantMissingException; +use Sprout\Sprout; use function Sprout\sprout; class SproutAuthCacheTokenRepository extends CacheTokenRepository { + /** + * @var \Sprout\Sprout + */ + private Sprout $sprout; + + /** @infection-ignore-all */ + public function __construct( + Sprout $sprout, + Repository $cache, + HasherContract $hasher, + string $hashKey, + int $expires = 3600, + int $throttle = 60, + string $prefix = '' + ) + { + parent::__construct($cache, $hasher, $hashKey, $expires, $throttle, $prefix); + $this->sprout = $sprout; + } + + public function getExpires(): int + { + return $this->expires; + } + + public function getThrottle(): int + { + return $this->throttle; + } + /** * @return string * @@ -25,6 +58,10 @@ public function getPrefix(): string { $prefix = $this->getTenantedPrefix(); + if (empty($prefix)) { + return $this->prefix; + } + if (! empty($this->prefix)) { $prefix = $this->prefix . '.' . $prefix; } @@ -40,11 +77,11 @@ public function getPrefix(): string */ protected function getTenantedPrefix(): string { - if (! sprout()->withinContext()) { + if (! $this->sprout->withinContext()) { return ''; } - $tenancy = sprout()->getCurrentTenancy(); + $tenancy = $this->sprout->getCurrentTenancy(); if ($tenancy === null) { throw TenancyMissingException::make(); @@ -57,7 +94,7 @@ protected function getTenantedPrefix(): string /** @var \Sprout\Contracts\Tenant $tenant */ $tenant = $tenancy->tenant(); - return $tenancy->getName() . '.' . ($tenant instanceof TenantHasResources ? $tenant->getTenantResourceKey() : $tenant->getTenantKey()) . '.'; + return $tenancy->getName() . '.' . ($tenant instanceof TenantHasResources ? $tenant->getTenantResourceKey() : $tenant->getTenantKey()); } /** @@ -74,11 +111,12 @@ public function create(CanResetPasswordContract $user): string { $this->delete($user); + /** @infection-ignore-all */ $token = hash_hmac('sha256', Str::random(40), $this->hashKey); $this->cache->put( $this->getPrefix() . $user->getEmailForPasswordReset(), - [$token, Carbon::now()->format($this->format)], + [$this->hasher->make($token), Carbon::now()->format($this->format)], $this->expires, ); diff --git a/src/Overrides/Auth/SproutAuthDatabaseTokenRepository.php b/src/Overrides/Auth/SproutAuthDatabaseTokenRepository.php index 29c10a9..d0d7149 100644 --- a/src/Overrides/Auth/SproutAuthDatabaseTokenRepository.php +++ b/src/Overrides/Auth/SproutAuthDatabaseTokenRepository.php @@ -5,11 +5,15 @@ use Illuminate\Auth\Passwords\DatabaseTokenRepository; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; +use Illuminate\Contracts\Hashing\Hasher as HasherContract; +use Illuminate\Database\ConnectionInterface; use Illuminate\Database\Query\Builder; use Illuminate\Support\Carbon; use SensitiveParameter; +use Sprout\Contracts\Tenancy; use Sprout\Exceptions\TenancyMissingException; use Sprout\Exceptions\TenantMissingException; +use Sprout\Sprout; use function Sprout\sprout; /** @@ -23,6 +27,57 @@ */ class SproutAuthDatabaseTokenRepository extends DatabaseTokenRepository { + /** + * @var \Sprout\Sprout + */ + private Sprout $sprout; + + /** @infection-ignore-all */ + public function __construct( + Sprout $sprout, + ConnectionInterface $connection, + HasherContract $hasher, + $table, + $hashKey, + $expires = 60, + $throttle = 60 + ) + { + parent::__construct($connection, $hasher, $table, $hashKey, $expires, $throttle); + $this->sprout = $sprout; + } + + public function getExpires(): int + { + return $this->expires; + } + + public function getThrottle(): int + { + return $this->throttle; + } + + /** + * @return \Sprout\Contracts\Tenancy<*> + * + * @throws \Sprout\Exceptions\TenancyMissingException + * @throws \Sprout\Exceptions\TenantMissingException + */ + protected function getTenancy(): Tenancy + { + $tenancy = $this->sprout->getCurrentTenancy(); + + if ($tenancy === null) { + throw TenancyMissingException::make(); + } + + if (! $tenancy->check()) { + throw TenantMissingException::make($tenancy->getName()); + } + + return $tenancy; + } + /** * Build the record payload for the table. * @@ -36,19 +91,11 @@ class SproutAuthDatabaseTokenRepository extends DatabaseTokenRepository */ protected function getPayload($email, #[SensitiveParameter] $token): array { - if (! sprout()->withinContext()) { + if (! $this->sprout->withinContext()) { return parent::getPayload($email, $token); } - $tenancy = sprout()->getCurrentTenancy(); - - if ($tenancy === null) { - throw TenancyMissingException::make(); - } - - if (! $tenancy->check()) { - throw TenantMissingException::make($tenancy->getName()); - } + $tenancy = $this->getTenancy(); return [ 'tenancy' => $tenancy->getName(), @@ -71,19 +118,11 @@ protected function getPayload($email, #[SensitiveParameter] $token): array */ protected function getTenantedQuery(string $email): Builder { - if (! sprout()->withinContext()) { + if (! $this->sprout->withinContext()) { return $this->getTable()->where('email', $email); } - $tenancy = sprout()->getCurrentTenancy(); - - if ($tenancy === null) { - throw TenancyMissingException::make(); - } - - if (! $tenancy->check()) { - throw TenantMissingException::make($tenancy->getName()); - } + $tenancy = $this->getTenancy(); return $this->getTable() ->where('tenancy', $tenancy->getName()) diff --git a/src/Overrides/Auth/SproutAuthPasswordBrokerManager.php b/src/Overrides/Auth/SproutAuthPasswordBrokerManager.php index 39a6fe8..63e5ef0 100644 --- a/src/Overrides/Auth/SproutAuthPasswordBrokerManager.php +++ b/src/Overrides/Auth/SproutAuthPasswordBrokerManager.php @@ -5,6 +5,8 @@ use Illuminate\Auth\Passwords\PasswordBrokerManager; use Illuminate\Auth\Passwords\TokenRepositoryInterface; +use Illuminate\Foundation\Application; +use Sprout\Sprout; /** * Sprout Auth Password Broker Manager @@ -19,28 +21,43 @@ */ class SproutAuthPasswordBrokerManager extends PasswordBrokerManager { + /** + * @var \Sprout\Sprout + */ + private Sprout $sprout; + + public function __construct(Application $app, Sprout $sprout) + { + parent::__construct($app); + + $this->sprout = $sprout; + } + /** * Create a token repository instance based on the current configuration. * * @param array $config * * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ protected function createTokenRepository(array $config): TokenRepositoryInterface { - // @phpstan-ignore-next-line - $key = $this->app['config']['app.key']; + /** @var string $key */ + $key = $this->app->make('config')->get('app.key'); // @codeCoverageIgnoreStart - if (str_starts_with($key, 'base64:')) { - $key = base64_decode(substr($key, 7)); + if (str_starts_with($key, 'base64:')) { // @infection-ignore-all + $key = base64_decode(substr($key, 7)); // @infection-ignore-all } // @codeCoverageIgnoreEnd if (isset($config['driver']) && $config['driver'] === 'cache') { return new SproutAuthCacheTokenRepository( - $this->app['cache']->store($config['store'] ?? null), // @phpstan-ignore-line - $this->app['hash'], // @phpstan-ignore-line + $this->sprout, + $this->app->make('cache')->store($config['store'] ?? null), // @phpstan-ignore-line + $this->app->make('hash'), $key, ($config['expire'] ?? 60) * 60, $config['throttle'] ?? 0, // @phpstan-ignore-line @@ -51,9 +68,10 @@ protected function createTokenRepository(array $config): TokenRepositoryInterfac $connection = $config['connection'] ?? null; return new SproutAuthDatabaseTokenRepository( - $this->app['db']->connection($connection), // @phpstan-ignore-line - $this->app['hash'], // @phpstan-ignore-line - $config['table'], // @phpstan-ignore-line + $this->sprout, + $this->app->make('db')->connection($connection), // @phpstan-ignore-line + $this->app->make('hash'), + $config['table'], // @phpstan-ignore-line $key, $config['expire'],// @phpstan-ignore-line $config['throttle'] ?? 0// @phpstan-ignore-line diff --git a/src/Overrides/AuthOverride.php b/src/Overrides/AuthOverride.php index f861516..cd7c1fa 100644 --- a/src/Overrides/AuthOverride.php +++ b/src/Overrides/AuthOverride.php @@ -39,6 +39,10 @@ final class AuthOverride extends BaseOverride implements BootableServiceOverride */ public function boot(Application $app, Sprout $sprout): void { + // Set the app and sprout instances, so we don't have to mess + // around with global helpers + $this->setApp($app)->setSprout($sprout); + // Although this isn't strictly necessary, this is here to tidy up // the list of deferred services, just in case there's some weird gotcha // somewhere that causes the provider to be loaded anyway. @@ -49,8 +53,8 @@ public function boot(Application $app, Sprout $sprout): void // 'auth.password.broker' as it will proxy to our new 'auth.password'. // This is the actual thing we need. - $app->singleton('auth.password', function ($app) { - return new SproutAuthPasswordBrokerManager($app); + $app->singleton('auth.password', function ($app) use($sprout) { + return new SproutAuthPasswordBrokerManager($app, $sprout); }); // While it's unlikely that the password broker has been resolved, @@ -113,9 +117,9 @@ protected function forgetGuards(): void // Since this class isn't deferred because it has to rely on // multiple services, we only want to actually run this code if // the auth manager has been resolved. - if (app()->resolved('auth')) { + if ($this->getApp()->resolved('auth')) { /** @var \Illuminate\Auth\AuthManager $authManager */ - $authManager = app(AuthManager::class); + $authManager = $this->getApp()->make(AuthManager::class); if ($authManager->hasResolvedGuards()) { $authManager->forgetGuards(); @@ -132,9 +136,9 @@ protected function flushPasswordBrokers(): void { // Same as with 'auth' above, we only want to run this code if the // password broker has been resolved already. - if (app()->resolved('auth.password')) { + if ($this->getApp()->resolved('auth.password')) { /** @var \Illuminate\Auth\Passwords\PasswordBrokerManager $passwordBroker */ - $passwordBroker = app('auth.password'); + $passwordBroker = $this->getApp()->make('auth.password'); // The flush method only exists on our custom implementation if ($passwordBroker instanceof SproutAuthPasswordBrokerManager) { diff --git a/src/Overrides/BaseOverride.php b/src/Overrides/BaseOverride.php index fdf047a..879de0a 100644 --- a/src/Overrides/BaseOverride.php +++ b/src/Overrides/BaseOverride.php @@ -3,9 +3,11 @@ namespace Sprout\Overrides; +use Illuminate\Foundation\Application; use Sprout\Contracts\ServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; +use Sprout\Sprout; abstract class BaseOverride implements ServiceOverride { @@ -16,6 +18,10 @@ abstract class BaseOverride implements ServiceOverride */ protected array $config; + protected Application $app; + + protected Sprout $sprout; + /** * Create a new instance of the service override * @@ -28,6 +34,28 @@ public function __construct(string $service, array $config) $this->service = $service; } + public function getApp(): Application + { + return $this->app; + } + + public function setApp(Application $app): BaseOverride + { + $this->app = $app; + return $this; + } + + public function getSprout(): Sprout + { + return $this->sprout; + } + + public function setSprout(Sprout $sprout): BaseOverride + { + $this->sprout = $sprout; + return $this; + } + /** * Set up the service override * diff --git a/src/Overrides/Session/SproutFileSessionHandlerCreator.php b/src/Overrides/Session/SproutFileSessionHandlerCreator.php index 82748b5..39dc80a 100644 --- a/src/Overrides/Session/SproutFileSessionHandlerCreator.php +++ b/src/Overrides/Session/SproutFileSessionHandlerCreator.php @@ -54,8 +54,8 @@ public function __invoke(): SproutFileSessionHandler if ($this->sprout->withinContext()) { $tenancy = $this->sprout->getCurrentTenancy(); - $handler->setTenancy($tenancy) - ->setTenant($tenancy?->tenant()); + $handler->setTenancy($tenancy); + $handler->setTenant($tenancy?->tenant()); } return $handler; diff --git a/tests/Unit/Overrides/Auth/SproutAuthCacheTokenRepositoryTest.php b/tests/Unit/Overrides/Auth/SproutAuthCacheTokenRepositoryTest.php new file mode 100644 index 0000000..e5cf861 --- /dev/null +++ b/tests/Unit/Overrides/Auth/SproutAuthCacheTokenRepositoryTest.php @@ -0,0 +1,558 @@ +setCurrentTenancy($tenancy); + + /** + * @var \Sprout\Contracts\Tenancy&\Mockery\MockInterface $tenancy + * @var \Sprout\Contracts\Tenancy&\Mockery\MockInterface $tenant + */ + + $tenancy->shouldReceive('check')->andReturn(true)->times($callCount); + $tenancy->shouldReceive('tenant')->andReturn($tenant)->times($callCount); + $tenancy->shouldReceive('getName')->andReturn('my-tenancy')->times($callCount); + $tenant->shouldReceive('getTenantKey')->andReturn(7)->times($callCount); + } + + return $sprout; + } + + private function mockUser(int $callCount = 1): CanResetPassword&MockInterface + { + return Mockery::mock(CanResetPassword::class, static function (MockInterface $mock) use ($callCount) { + $mock->shouldReceive('getEmailForPasswordReset')->andReturn('test@email.com')->times($callCount); + }); + } + + private function mockStoreGet(?Tenancy $tenancy, string $token, $hasher, bool $return = true, ?\Closure $expiryAdjuster = null): Repository&MockInterface + { + $storedToken = $return ? $hasher->make($token) : null; + + return Mockery::mock(Repository::class, static function (MockInterface $mock) use ($expiryAdjuster, $storedToken, $tenancy) { + $mock->shouldReceive('get') + ->withArgs([ + 'my-prefix' . ($tenancy !== null ? '.my-tenancy.7' : '') . 'test@email.com', + ]) + ->andReturn($storedToken ? [ + $storedToken, + ($expiryAdjuster ? $expiryAdjuster(Carbon::now()) : Carbon::now()->subMinute())->format('Y-m-d H:i:s'), + ] : null) + ->once(); + }); + } + + #[Test, DataProvider('multitenancyContextDataProvider')] + public function returnsTheCorrectPrefix(bool $inContext): void + { + $store = Mockery::mock(Repository::class, static function (MockInterface $mock) { + + }); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + if ($inContext) { + $sprout->markAsInContext(); + } + + if ($inContext) { + $sprout->setCurrentTenancy(Mockery::mock(Tenancy::class, static function (MockInterface $mock) use ($inContext) { + $mock->shouldReceive('check')->andReturn($inContext)->once(); + $mock->shouldReceive('tenant')->andReturn(Mockery::mock(Tenant::class, static function (MockInterface $mock) { + $mock->shouldReceive('getTenantKey')->andReturn(7)->once(); + $mock->shouldNotReceive('getTenantResourceKey'); + }))->once(); + + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + })); + } + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $this->app->make('hash'), + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $prefix = 'my-prefix' . ($inContext ? '.my-tenancy.7' : ''); + + $this->assertEquals($prefix, $repository->getPrefix()); + } + + #[Test, DataProvider('multitenancyContextDataProvider')] + public function returnsTheCorrectPrefixForTenantsWithResources(bool $inContext): void + { + $store = Mockery::mock(Repository::class, static function (MockInterface $mock) { + + }); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + if ($inContext) { + $sprout->markAsInContext(); + } + + if ($inContext) { + $sprout->setCurrentTenancy(Mockery::mock(Tenancy::class, static function (MockInterface $mock) use ($inContext) { + $mock->shouldReceive('check')->andReturn($inContext)->once(); + $mock->shouldReceive('tenant')->andReturn(Mockery::mock(Tenant::class, TenantHasResources::class, static function (MockInterface $mock) { + $mock->shouldNotReceive('getTenantKey'); + $mock->shouldReceive('getTenantResourceKey')->andReturn('the-key')->once(); + }))->once(); + + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + })); + } + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $this->app->make('hash'), + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $prefix = 'my-prefix' . ($inContext ? '.my-tenancy.the-key' : ''); + + $this->assertEquals($prefix, $repository->getPrefix()); + } + + #[Test] + public function throwsExceptionWhenInContextWithoutTenancy(): void + { + $store = Mockery::mock(Repository::class, static function (MockInterface $mock) { + + }); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $sprout->markAsInContext(); + + $this->expectException(TenancyMissingException::class); + $this->expectExceptionMessage('There is no current tenancy'); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $this->app->make('hash'), + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $repository->getPrefix(); + } + + #[Test] + public function throwsExceptionWhenInContextWithTenancyButNoTenant(): void + { + $store = Mockery::mock(Repository::class, static function (MockInterface $mock) { + + }); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $sprout->markAsInContext(); + + $tenancy = Mockery::mock(Tenancy::class, static function (MockInterface $mock) { + $mock->shouldReceive('check')->andReturn(false)->once(); + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + }); + + $sprout->setCurrentTenancy($tenancy); + + $this->expectException(TenantMissingException::class); + $this->expectExceptionMessage('There is no current tenant for tenancy [my-tenancy]'); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $this->app->make('hash'), + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $repository->getPrefix(); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCreateTokens(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant, 2); + $user = $this->mockUser(2); + $hash = null; + + $store = Mockery::mock(Repository::class, static function (MockInterface $mock) use ($tenancy, &$hash) { + $mock->shouldReceive('forget') + ->withArgs([ + 'my-prefix' . ($tenancy !== null ? '.my-tenancy.7' : '') . 'test@email.com', + ]) + ->once(); + $mock->shouldReceive('put') + ->withArgs([ + 'my-prefix' . ($tenancy !== null ? '.my-tenancy.7' : '') . 'test@email.com', + Mockery::on(static function ($arg) use (&$hash) { + if (is_array($arg) + && is_string($arg[0]) + && is_string($arg[1])) { + $hash = $arg[0]; + + return true; + } + + return false; + }), + 3600, + ]) + ->once(); + }); + + $hasher = $this->app->make('hash'); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $token = $repository->create($user); + $this->assertTrue($hasher->check($token, $hash)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExistingTokensExist(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $store = $this->mockStoreGet($tenancy, $token, $hasher); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $this->assertTrue($repository->exists($user, $token)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExpiredExistingTokensExist(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $store = $this->mockStoreGet( + $tenancy, + $token, + $hasher, + true, + fn(Carbon $carbon) => $carbon->subMinutes(3700) + ); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $this->assertFalse($repository->exists($user, $token)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfNonExistingTokensExist(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $store = $this->mockStoreGet( + $tenancy, + $token, + $hasher, + false + ); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $this->assertFalse($repository->exists($user, $token)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExistingTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $store = $this->mockStoreGet( + $tenancy, + $token, + $hasher, + true, + fn(Carbon $carbon) => $carbon->subSeconds(10) + ); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $this->assertTrue($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExistingOldTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $store = $this->mockStoreGet( + $tenancy, + $token, + $hasher, + true, + fn(Carbon $carbon) => $carbon->subMinute() + ); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $this->assertFalse($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExpiredExistingTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $store = $this->mockStoreGet( + $tenancy, + $token, + $hasher, + true, + fn(Carbon $carbon) => $carbon->subMinutes(3700) + ); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $this->assertFalse($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfNonExistingTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $store = $this->mockStoreGet( + $tenancy, + $token, + $hasher, + false + ); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $hasher, + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $this->assertFalse($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canDeleteTokens(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $store = Mockery::mock(Repository::class, static function (MockInterface $mock) use ($tenancy) { + $mock->shouldReceive('forget') + ->withArgs([ + 'my-prefix' . ($tenancy !== null ? '.my-tenancy.7' : '') . 'test@email.com', + ]) + ->once(); + }); + + $repository = new SproutAuthCacheTokenRepository( + $sprout, + $store, + $this->app->make('hash'), + 'hash-key', + 3600, + 60, + 'my-prefix' + ); + + $repository->delete($user); + } + + public static function multitenancyContextDataProvider(): array + { + return [ + 'not in context' => [false], + 'in context' => [true], + ]; + } + + public static function tenantContextDataProvider(): array + { + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class); + + return [ + 'no tenant context' => [null, null], + 'tenant context' => [$tenancy, $tenant], + ]; + } +} diff --git a/tests/Unit/Overrides/Auth/SproutAuthDatabaseTokenRepositoryTest.php b/tests/Unit/Overrides/Auth/SproutAuthDatabaseTokenRepositoryTest.php new file mode 100644 index 0000000..2ba1b8b --- /dev/null +++ b/tests/Unit/Overrides/Auth/SproutAuthDatabaseTokenRepositoryTest.php @@ -0,0 +1,547 @@ +setCurrentTenancy($tenancy); + + /** + * @var \Sprout\Contracts\Tenancy&\Mockery\MockInterface $tenancy + * @var \Sprout\Contracts\Tenancy&\Mockery\MockInterface $tenant + */ + + $tenancy->shouldReceive('check')->andReturn(true)->times($callCount); + $tenancy->shouldReceive('key')->andReturn(7)->times($callCount); + $tenancy->shouldReceive('getName')->andReturn('my-tenancy')->times($callCount); + } + + return $sprout; + } + + private function mockUser(int $callCount = 1): CanResetPassword&MockInterface + { + return Mockery::mock(CanResetPassword::class, static function (MockInterface $mock) use ($callCount) { + $mock->shouldReceive('getEmailForPasswordReset')->andReturn('test@email.com')->times($callCount); + }); + } + + private function mockConnectionGet(?Tenancy $tenancy, string $token, $hasher, bool $return = true, ?\Closure $expiryAdjuster = null): Connection&MockInterface + { + $storedToken = $return ? $hasher->make($token) : null; + + return Mockery::mock(Connection::class, static function (MockInterface $mock) use ($expiryAdjuster, $storedToken, $tenancy) { + $builder = Mockery::mock(Builder::class, static function (MockInterface $mock) use ($storedToken, $expiryAdjuster, $tenancy) { + $mock->shouldReceive('where') + ->withArgs([ + 'email', + 'test@email.com', + ]) + ->andReturnSelf() + ->once(); + + if ($tenancy !== null) { + $mock->shouldReceive('where') + ->withArgs([ + 'tenancy', + 'my-tenancy', + ]) + ->andReturnSelf() + ->once(); + $mock->shouldReceive('where') + ->withArgs([ + 'tenant_id', + '7', + ]) + ->andReturnSelf() + ->once(); + } + + $mock->shouldReceive('first') + ->andReturn($storedToken ? (object)[ + 'token' => $storedToken, + 'created_at' => ($expiryAdjuster ? $expiryAdjuster(Carbon::now()) : Carbon::now()->subMinute())->format('Y-m-d H:i:s'), + ] : null) + ->once(); + }); + + $mock->shouldReceive('table') + ->withArgs(['my_passwords']) + ->andReturn($builder) + ->once(); + }); + } + + #[Test] + public function throwsExceptionWhenInContextWithoutTenancy(): void + { + $connection = Mockery::mock(Connection::class); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $sprout->markAsInContext(); + + $this->expectException(TenancyMissingException::class); + $this->expectExceptionMessage('There is no current tenancy'); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $this->app->make('hash'), + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $repository->create($this->mockUser(2)); + } + + #[Test] + public function throwsExceptionWhenInContextWithTenancyButNoTenant(): void + { + $connection = Mockery::mock(Connection::class); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $sprout->markAsInContext(); + + $tenancy = Mockery::mock(Tenancy::class, static function (MockInterface $mock) { + $mock->shouldReceive('check')->andReturn(false)->once(); + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + }); + + $sprout->setCurrentTenancy($tenancy); + + $this->expectException(TenantMissingException::class); + $this->expectExceptionMessage('There is no current tenant for tenancy [my-tenancy]'); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $this->app->make('hash'), + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $repository->create($this->mockUser(2)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCreateTokens(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant, 2); + $user = $this->mockUser(2); + + $connection = Mockery::mock(Connection::class, static function (MockInterface $mock) use ($tenancy) { + $builder = Mockery::mock(Builder::class, static function (MockInterface $mock) use ($tenancy) { + $mock->shouldReceive('where') + ->withArgs([ + 'email', + 'test@email.com', + ]) + ->andReturnSelf() + ->once(); + + if ($tenancy !== null) { + $mock->shouldReceive('where') + ->withArgs([ + 'tenancy', + 'my-tenancy', + ]) + ->andReturnSelf() + ->once(); + $mock->shouldReceive('where') + ->withArgs([ + 'tenant_id', + '7', + ]) + ->andReturnSelf() + ->once(); + } + + $mock->shouldReceive('delete')->andReturn(0)->once(); + + $mock->shouldReceive('insert') + ->with(Mockery::on(static function ($arg) use ($tenancy) { + $check = is_array($arg) + && (isset($arg['email']) && $arg['email'] === 'test@email.com') + && (isset($arg['token']) && is_string($arg['token']) && strlen($arg['token']) === 60) + && (isset($arg['created_at']) && $arg['created_at'] instanceof \Illuminate\Support\Carbon); + + if ($tenancy !== null) { + return $check + && count($arg) === 5 + && (isset($arg['tenancy']) && $arg['tenancy'] === 'my-tenancy') + && (isset($arg['tenant_id']) && $arg['tenant_id'] === 7); + } + + return $check + && count($arg) === 3; + })) + ->once(); + }); + + $mock->shouldReceive('table') + ->withArgs(['my_passwords']) + ->andReturn($builder) + ->twice(); + }); + + $hasher = $this->app->make('hash'); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $repository->create($user); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExistingTokensExist(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $connection = $this->mockConnectionGet($tenancy, $token, $hasher); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $this->assertTrue($repository->exists($user, $token)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExpiredExistingTokensExist(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $connection = $this->mockConnectionGet( + $tenancy, + $token, + $hasher, + true, + fn (Carbon $carbon) => $carbon->subMinutes(3700) + ); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $this->assertFalse($repository->exists($user, $token)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfNonExistingTokensExist(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $connection = $this->mockConnectionGet( + $tenancy, + $token, + $hasher, + false + ); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $this->assertFalse($repository->exists($user, $token)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExistingTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $connection = $this->mockConnectionGet( + $tenancy, + $token, + $hasher, + true, + fn (Carbon $carbon) => $carbon->subSeconds(10) + ); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $this->assertTrue($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExistingOldTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $connection = $this->mockConnectionGet( + $tenancy, + $token, + $hasher, + true, + fn (Carbon $carbon) => $carbon->subMinute() + ); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $this->assertFalse($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfExpiredExistingTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $connection = $this->mockConnectionGet( + $tenancy, + $token, + $hasher, + true, + fn (Carbon $carbon) => $carbon->subMinutes(3700) + ); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $this->assertFalse($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canCheckIfNonExistingTokensWereRecentlyCreated(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + $hasher = $this->app->make('hash'); + $token = hash_hmac('sha256', Str::random(40), 'hash-key'); + $connection = $this->mockConnectionGet( + $tenancy, + $token, + $hasher, + false + ); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + $connection, + $hasher, + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $this->assertFalse($repository->recentlyCreatedToken($user)); + } + + #[Test, DataProvider('tenantContextDataProvider')] + public function canDeleteTokens(?Tenancy $tenancy, ?Tenant $tenant): void + { + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = $this->getSprout($app, $tenancy, $tenant); + $user = $this->mockUser(); + + $repository = new SproutAuthDatabaseTokenRepository( + $sprout, + Mockery::mock(Connection::class, static function (MockInterface $mock) use ($tenancy) { + $mock->shouldReceive('table') + ->withArgs(['my_passwords']) + ->andReturn(Mockery::mock(Builder::class, static function (MockInterface $mock) use ($tenancy) { + $mock->shouldReceive('where') + ->withArgs([ + 'email', + 'test@email.com', + ]) + ->andReturnSelf() + ->once(); + + if ($tenancy !== null) { + $mock->shouldReceive('where') + ->withArgs([ + 'tenancy', + 'my-tenancy', + ]) + ->andReturnSelf() + ->once(); + $mock->shouldReceive('where') + ->withArgs([ + 'tenant_id', + '7', + ]) + ->andReturnSelf() + ->once(); + } + + $mock->shouldReceive('delete') + ->andReturn(1) + ->once(); + })) + ->once(); + }), + $this->app->make('hash'), + 'my_passwords', + 'hash-key', + 3600, + 60 + ); + + $repository->delete($user); + } + + public static function multitenancyContextDataProvider(): array + { + return [ + 'not in context' => [false], + 'in context' => [true], + ]; + } + + public static function tenantContextDataProvider(): array + { + $tenancy = Mockery::mock(Tenancy::class); + $tenant = Mockery::mock(Tenant::class); + + return [ + 'no tenant context' => [null, null], + 'tenant context' => [$tenancy, $tenant], + ]; + } +} diff --git a/tests/Unit/Overrides/Auth/SproutAuthPasswordBrokerManagerTest.php b/tests/Unit/Overrides/Auth/SproutAuthPasswordBrokerManagerTest.php new file mode 100644 index 0000000..9c5ed67 --- /dev/null +++ b/tests/Unit/Overrides/Auth/SproutAuthPasswordBrokerManagerTest.php @@ -0,0 +1,206 @@ + $driver, + 'provider' => 'users', + 'expire' => $expire, + 'throttle' => $throttle, + ]; + + if ($driver === 'database') { + $config['table'] = 'password_resets'; + } + + if ($driver === 'cache') { + $config['store'] = 'my-store'; + $config['prefix'] = 'my-prefix'; + } + + /** @var Application&\Mockery\MockInterface $app */ + $app = Mockery::mock($this->app, static function (Mockery\MockInterface $mock) use ($config) { + $mock->makePartial(); + + $repository = Mockery::mock(Repository::class, static function (Mockery\MockInterface $mock) use ($config) { + $mock->shouldReceive('get') + ->withArgs(['app.key']) + ->andReturn('base64:' . base64_encode('fake-key')) + ->once(); + + $mock->shouldReceive('offsetGet') + ->withArgs(['auth.defaults.passwords']) + ->andReturn('users') + ->atLeast() + ->once(); + + $mock->shouldReceive('offsetGet') + ->withArgs(['auth.passwords.my-passwords']) + ->once() + ->andReturn($config); + }); + + $mock->shouldReceive('offsetGet') + ->withArgs(['config']) + ->atLeast() + ->once() + ->andReturn($repository); + + $mock->shouldReceive('make') + ->withArgs(['config']) + ->once() + ->andReturn($repository); + + $mock->shouldReceive('make') + ->withArgs(['hash']) + ->andReturn(Mockery::mock(Hasher::class)) + ->once(); + }); + + return $app; + } + + #[Test, DataProvider('expireAndThrottleDataProvider')] + public function createsSproutDatabaseTokenRepository(?int $expire = null, ?int $throttle = null): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = $this->mockApp('database', $expire, $throttle); + + $app->shouldReceive('make') + ->withArgs(['db']) + ->andReturn( + Mockery::mock(DatabaseManager::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('connection') + ->andReturn(Mockery::mock(Connection::class)) + ->once(); + }) + ) + ->once(); + + $sprout = new Sprout($app, new SettingsRepository()); + $manager = new SproutAuthPasswordBrokerManager($app, $sprout); + + $this->assertFalse($manager->isResolved()); + + $repository = $manager->broker('my-passwords')->getRepository(); + + $this->assertInstanceOf(SproutAuthDatabaseTokenRepository::class, $repository); + $this->assertTrue($manager->isResolved('my-passwords')); + $this->assertFalse($manager->isResolved()); + $this->assertSame(($expire ?? 0) * 60, $repository->getExpires()); + $this->assertSame($throttle ?? 0, $repository->getThrottle()); + } + + #[Test, DataProvider('expireAndThrottleDataProvider')] + public function createsSproutCacheTokenRepository(?int $expire = null, ?int $throttle = null): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = $this->mockApp('cache', $expire, $throttle); + + $app->shouldReceive('make') + ->withArgs(['cache']) + ->andReturn( + Mockery::mock(CacheRepository::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('store') + ->withArgs(['my-store']) + ->andReturn(Mockery::mock(CacheRepository::class)) + ->once(); + }) + ) + ->once(); + + $sprout = new Sprout($app, new SettingsRepository()); + + $manager = new SproutAuthPasswordBrokerManager($app, $sprout); + + $this->assertFalse($manager->isResolved()); + + $repository = $manager->broker('my-passwords')->getRepository(); + + $this->assertInstanceOf(SproutAuthCacheTokenRepository::class, $repository); + $this->assertTrue($manager->isResolved('my-passwords')); + $this->assertFalse($manager->isResolved()); + $this->assertSame(($expire ?? 60) * 60, $repository->getExpires()); + $this->assertSame($throttle ?? 0, $repository->getThrottle()); + $this->assertSame('my-prefix', $repository->getPrefix()); + } + + #[Test] + public function fallsBackToDatabaseIfDriverIsSomethingElse(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock($this->app, static function (Mockery\MockInterface $mock) { + $mock->makePartial(); + }); + + $app->make('config')->set('auth.passwords.users.driver', 'this-is-a-fake-driver'); + + $sprout = new Sprout($app, new SettingsRepository()); + + $manager = new SproutAuthPasswordBrokerManager($app, $sprout); + + $this->assertFalse($manager->isResolved()); + + $repository = $manager->broker()->getRepository(); + + $this->assertNotInstanceOf(SproutAuthCacheTokenRepository::class, $repository); + $this->assertInstanceOf(SproutAuthDatabaseTokenRepository::class, $repository); + + $this->assertTrue($manager->isResolved()); + } + + #[Test] + public function canBeFlushed(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock($this->app, static function (Mockery\MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $manager = new SproutAuthPasswordBrokerManager($app, $sprout); + + $this->assertFalse($manager->isResolved()); + + $manager->broker(); + + $this->assertTrue($manager->isResolved()); + + $manager->flush(); + + $this->assertFalse($manager->isResolved()); + } + + public static function expireAndThrottleDataProvider(): array + { + return [ + [15, 20], + [], + [60, 60], + [null, 11], + [26, null], + ]; + } +} diff --git a/tests/Unit/Overrides/AuthOverrideTest.php b/tests/Unit/Overrides/AuthOverrideTest.php index 6b10fa9..5478ede 100644 --- a/tests/Unit/Overrides/AuthOverrideTest.php +++ b/tests/Unit/Overrides/AuthOverrideTest.php @@ -3,19 +3,24 @@ namespace Sprout\Tests\Unit\Overrides; +use Closure; use Illuminate\Auth\AuthManager; +use Illuminate\Auth\Passwords\PasswordBrokerManager; use Illuminate\Config\Repository; use Mockery; use Mockery\MockInterface; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\DeferrableServiceOverride; +use Sprout\Contracts\Tenancy; +use Sprout\Contracts\Tenant; use Sprout\Overrides\Auth\SproutAuthCacheTokenRepository; use Sprout\Overrides\Auth\SproutAuthDatabaseTokenRepository; use Sprout\Overrides\Auth\SproutAuthPasswordBrokerManager; use Sprout\Overrides\AuthOverride; -use Sprout\Overrides\CookieOverride; -use Sprout\Support\Services; +use Sprout\Sprout; +use Sprout\Support\SettingsRepository; use Sprout\Tests\Unit\UnitTestCase; use Workbench\App\Models\TenantModel; use Workbench\App\Models\User; @@ -58,216 +63,235 @@ public function isRegisteredWithSproutCorrectly(): void $this->assertTrue($sprout->overrides()->hasOverrideBooted('auth')); } - #[Test] - public function rebindsAuthPassword(): void + #[Test, DataProvider('authPasswordResolvedDataProvider')] + public function bootsCorrectly(bool $return): void { - $sprout = sprout(); - - config()->set('sprout.overrides', [ - 'auth' => [ - 'driver' => AuthOverride::class, - ], - ]); - - app()->rebinding('auth.password', function ($app, $passwordBrokerManager) { - $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, $passwordBrokerManager); + $override = new AuthOverride('auth', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) use ($return) { + $mock->makePartial(); + $mock->shouldReceive('removeDeferredServices') + ->withArgs([['auth.password']]) + ->once(); + + $mock->shouldReceive('singleton') + ->withArgs([ + 'auth.password', + Mockery::on(static function ($closure) { + return is_callable($closure) && $closure instanceof Closure; + }), + ]) + ->once(); + + $mock->shouldReceive('resolved')->withArgs(['auth.password'])->once()->andReturn($return); + + if ($return) { + $mock->shouldReceive('forgetInstance')->withArgs(['auth.password'])->once(); + } }); - $sprout->overrides()->registerOverrides(); - } - - #[Test] - public function forgetsAuthPasswordInstance(): void - { - $sprout = sprout(); - - config()->set('sprout.overrides', [ - 'auth' => [ - 'driver' => AuthOverride::class, - ], - ]); - - $this->assertFalse(app()->resolved('auth.password')); - $this->assertNotInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); - $this->assertTrue(app()->resolved('auth.password')); - - $sprout->overrides()->registerOverrides(); + $sprout = new Sprout($app, new SettingsRepository()); - $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, app()->make('auth.password')); + $override->boot($app, $sprout); } #[Test] - public function replacesTheDatabaseTokenRepositoryDriver(): void + public function overridesThePasswordBrokerManager(): void { - $sprout = sprout(); + $override = new AuthOverride('auth', []); - config()->set('sprout.overrides', [ - 'auth' => [ - 'driver' => AuthOverride::class, - ], - ]); - - config()->set('auth.passwords.users.driver', 'database'); - config()->set('auth.passwords.users.table', 'password_reset_tokens'); - config()->set('multitenancy.providers.eloquent.model', TenantModel::class); - - $sprout->overrides()->registerOverrides(); - - $broker = app()->make('auth.password.broker'); - - $this->assertInstanceOf(SproutAuthDatabaseTokenRepository::class, $broker->getRepository()); - - $tenant = TenantModel::factory()->createOne(); - $tenancy = tenancy(); + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); - $tenancy->setTenant($tenant); - sprout()->setCurrentTenancy($tenancy); + $sprout = new Sprout($app, new SettingsRepository()); - $user = User::factory()->createOne(); - $token = $broker->createToken($user); - $database = app()->make('db.connection'); + $override->boot($app, $sprout); - $dbEntry = $database->table('password_reset_tokens') - ->where('email', '=', $user->getAttribute('email')) - ->where('tenancy', '=', $tenancy->getName()) - ->where('tenant_id', '=', $tenant->getTenantKey()) - ->first(); + $manager = $app->make('auth.password'); - $this->assertNotNull($dbEntry); - $this->assertTrue(app()->make('hash')->check($token, $dbEntry->token)); + $this->assertInstanceOf(SproutAuthPasswordBrokerManager::class, $manager); } - #[Test] - public function replacesTheCacheTokenRepositoryDriver(): void + #[Test, DataProvider('authResolvedDataProvider')] + public function setsUpForTheTenancy(bool $authReturn, bool $authPasswordReturn): void { - $sprout = sprout(); - - config()->set('sprout.overrides', [ - 'auth' => [ - 'driver' => AuthOverride::class, - ], - ]); - - config()->set('auth.passwords.users.driver', 'cache'); - config()->set('auth.passwords.users.store', 'array'); - config()->set('multitenancy.providers.eloquent.model', TenantModel::class); - - $sprout->overrides()->registerOverrides(); - - $broker = app()->make('auth.password.broker'); - - $this->assertInstanceOf(SproutAuthCacheTokenRepository::class, $broker->getRepository()); - - $tenant = TenantModel::factory()->createOne(); - $tenancy = tenancy(); - - $tenancy->setTenant($tenant); - sprout()->setCurrentTenancy($tenancy); + $override = new AuthOverride('auth', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) use ($authReturn, $authPasswordReturn) { + $mock->makePartial(); + + $mock->shouldReceive('resolved') + ->withArgs(['auth']) + ->andReturn($authReturn) + ->once(); + + if ($authReturn) { + $authManager = Mockery::mock(AuthManager::class, static function (MockInterface $mock) { + $mock->shouldReceive('hasResolvedGuards')->once()->andReturn(true); + $mock->shouldReceive('forgetGuards')->once(); + }); + + $mock->shouldReceive('make') + ->withArgs([AuthManager::class]) + ->andReturn($authManager) + ->once(); + } + + $mock->shouldReceive('resolved') + ->withArgs(['auth.password']) + ->andReturn($authPasswordReturn) + ->atLeast() + ->once(); + + if ($authPasswordReturn) { + $passwordManager = Mockery::mock(SproutAuthPasswordBrokerManager::class, static function (MockInterface $mock) { + $mock->shouldReceive('flush')->once(); + }); + + $mock->shouldReceive('make') + ->withArgs(['auth.password']) + ->andReturn($passwordManager) + ->once(); + } + }); - $user = User::factory()->createOne(); - $token = $broker->createToken($user); - $cache = app()->make('cache')->store('array'); + $sprout = new Sprout($app, new SettingsRepository()); - $cacheKey = $tenancy->getName() . '.' . $tenant->getTenantResourceKey() . '.' . $user->getAttribute('email'); + $override->boot($app, $sprout); - $this->assertTrue($cache->has($cacheKey)); - $this->assertSame($token, $cache->get($cacheKey)[0]); + $override->setup( + Mockery::mock(Tenancy::class), + Mockery::mock(Tenant::class) + ); } - #[Test] - public function canFlushBrokers(): void + #[Test, DataProvider('authResolvedDataProvider')] + public function cleansUpAfterTheTenancy(bool $authReturn, bool $authPasswordReturn): void { - $sprout = sprout(); - - config()->set('sprout.overrides', [ - 'auth' => [ - 'driver' => AuthOverride::class, - ], - ]); - - config()->set('auth.passwords.users.driver', 'database'); - - $sprout->overrides()->registerOverrides(); - - /** @var SproutAuthPasswordBrokerManager $manager */ - $manager = app()->make('auth.password'); - - $this->assertFalse($manager->isResolved()); - - $manager->broker(); + $override = new AuthOverride('auth', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) use ($authReturn, $authPasswordReturn) { + $mock->makePartial(); + + $mock->shouldReceive('resolved') + ->withArgs(['auth']) + ->andReturn($authReturn) + ->once(); + + if ($authReturn) { + $authManager = Mockery::mock(AuthManager::class, static function (MockInterface $mock) { + $mock->shouldReceive('hasResolvedGuards')->once()->andReturn(true); + $mock->shouldReceive('forgetGuards')->once(); + }); + + $mock->shouldReceive('make') + ->withArgs([AuthManager::class]) + ->andReturn($authManager) + ->once(); + } + + $mock->shouldReceive('resolved') + ->withArgs(['auth.password']) + ->andReturn($authPasswordReturn) + ->atLeast() + ->once(); + + if ($authPasswordReturn) { + $passwordManager = Mockery::mock(SproutAuthPasswordBrokerManager::class, static function (MockInterface $mock) { + $mock->shouldReceive('flush')->once(); + }); + + $mock->shouldReceive('make') + ->withArgs(['auth.password']) + ->andReturn($passwordManager) + ->once(); + } + }); - $this->assertTrue($manager->isResolved()); + $sprout = new Sprout($app, new SettingsRepository()); - $manager->flush(); + $override->boot($app, $sprout); - $this->assertFalse($manager->isResolved()); + $override->cleanup( + Mockery::mock(Tenancy::class), + Mockery::mock(Tenant::class) + ); } - #[Test] - public function performsSetup(): void + #[Test, DataProvider('authResolvedDataProvider')] + public function cleansUpAfterTheTenancyWithoutOverriddenBrokerManager(bool $authReturn, bool $authPasswordReturn): void { - $sprout = sprout(); - - config()->set('sprout.overrides', [ - 'auth' => [ - 'driver' => AuthOverride::class, - ], - ]); - - $sprout->overrides()->registerOverrides(); - - $override = $sprout->overrides()->get('auth'); - - $this->assertInstanceOf(AuthOverride::class, $override); - - $tenant = TenantModel::factory()->createOne(); - $tenancy = $sprout->tenancies()->get(); - - $tenancy->setTenant($tenant); + $override = new AuthOverride('auth', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) use ($authReturn, $authPasswordReturn) { + $mock->makePartial(); + + $mock->shouldReceive('resolved') + ->withArgs(['auth']) + ->andReturn($authReturn) + ->once(); + + if ($authReturn) { + $authManager = Mockery::mock(AuthManager::class, static function (MockInterface $mock) { + $mock->shouldReceive('hasResolvedGuards')->once()->andReturn(true); + $mock->shouldReceive('forgetGuards')->once(); + }); + + $mock->shouldReceive('make') + ->withArgs([AuthManager::class]) + ->andReturn($authManager) + ->once(); + } + + $mock->shouldReceive('resolved') + ->withArgs(['auth.password']) + ->andReturn($authPasswordReturn) + ->atLeast() + ->once(); + + if ($authPasswordReturn) { + $passwordManager = Mockery::mock(PasswordBrokerManager::class, static function (MockInterface $mock) { + $mock->shouldNotReceive('flush'); + }); + + $mock->shouldReceive('make') + ->withArgs(['auth.password']) + ->andReturn($passwordManager) + ->once(); + } + }); - $this->instance('auth', $this->spy(AuthManager::class, function (MockInterface $mock) { - $mock->shouldReceive('hasResolvedGuards')->once()->andReturn(true); - $mock->shouldReceive('forgetGuards')->once(); - })); + $sprout = new Sprout($app, new SettingsRepository()); - $this->instance('auth.password', $this->spy(SproutAuthPasswordBrokerManager::class, function (MockInterface $mock) { - $mock->shouldReceive('flush')->once(); - })); + $override->boot($app, $sprout); - $override->setup($tenancy, $tenant); + $override->cleanup( + Mockery::mock(Tenancy::class), + Mockery::mock(Tenant::class) + ); } - #[Test] - public function performsCleanup(): void + public static function authPasswordResolvedDataProvider(): array { - $sprout = sprout(); - - config()->set('sprout.overrides', [ - 'auth' => [ - 'driver' => AuthOverride::class, - ], - ]); - - $sprout->overrides()->registerOverrides(); - - $override = $sprout->overrides()->get('auth'); - - $this->assertInstanceOf(AuthOverride::class, $override); - - $tenant = TenantModel::factory()->createOne(); - $tenancy = $sprout->tenancies()->get(); - - $tenancy->setTenant($tenant); - - $this->instance('auth', $this->spy(AuthManager::class, function (MockInterface $mock) { - $mock->shouldReceive('hasResolvedGuards')->once()->andReturn(true); - $mock->shouldReceive('forgetGuards')->once(); - })); - - $this->instance('auth.password', $this->spy(SproutAuthPasswordBrokerManager::class, function (MockInterface $mock) { - $mock->shouldReceive('flush')->once(); - })); + return [ + 'auth.password resolved' => [true], + 'auth.password not resolved' => [false], + ]; + } - $override->cleanup($tenancy, $tenant); + public static function authResolvedDataProvider(): array + { + return [ + 'auth resolved auth.password not resolved' => [true, false], + 'auth resolved auth.password resolved' => [true, true], + 'auth not resolved auth.password not resolved' => [false, false], + 'auth not resolved auth.password resolved' => [false, true], + ]; } } diff --git a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php index 766cd75..8a49339 100644 --- a/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php +++ b/tests/Unit/Overrides/Session/SproutDatabaseSessionHandlerCreatorTest.php @@ -6,17 +6,17 @@ use Illuminate\Config\Repository; use Illuminate\Database\Connection; use Illuminate\Database\DatabaseManager; +use Illuminate\Foundation\Application; use Mockery; +use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; use Sprout\Overrides\Session\SproutDatabaseSessionHandler; -use Sprout\Overrides\Session\SproutFileSessionHandler; use Sprout\Overrides\Session\SproutDatabaseSessionHandlerCreator; -use Sprout\Overrides\Session\SproutFileSessionHandlerCreator; use Sprout\Sprout; +use Sprout\Support\SettingsRepository; use Sprout\Tests\Unit\UnitTestCase; -use function Sprout\sprout; class SproutDatabaseSessionHandlerCreatorTest extends UnitTestCase { @@ -35,9 +35,16 @@ public function canCreateTheDatabaseHandler(): void $mock->shouldReceive('connection')->with(null)->andReturn($connection)->once(); })); + /** @var Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + $creator = new SproutDatabaseSessionHandlerCreator( $this->app, - $this->app->make(Sprout::class) + $sprout ); $handler = $creator(); @@ -48,11 +55,17 @@ public function canCreateTheDatabaseHandler(): void } #[Test] - public function canCreateTheFileHandlerWithTenantContext(): void + public function canCreateTheDatabaseHandlerWithTenantContext(): void { $tenancy = Mockery::mock(Tenancy::class); $tenant = Mockery::mock(Tenant::class)->makePartial(); - $sprout = sprout(); + + /** @var Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); $tenancy->shouldReceive('tenant')->andReturn($tenant)->once(); @@ -65,7 +78,7 @@ public function canCreateTheFileHandlerWithTenantContext(): void $creator = new SproutDatabaseSessionHandlerCreator( $this->app, - $this->app->make(Sprout::class) + $sprout ); $handler = $creator(); @@ -76,4 +89,35 @@ public function canCreateTheFileHandlerWithTenantContext(): void $this->assertSame($tenancy, $handler->getTenancy()); $this->assertSame($tenant, $handler->getTenant()); } + + #[Test] + public function canCreateTheDatabaseHandlerWithTenantContextButNoTenancy(): void + { + $connection = Mockery::mock(Connection::class); + $this->swap('db', Mockery::mock(DatabaseManager::class, static function (Mockery\MockInterface $mock) use ($connection) { + $mock->shouldReceive('connection')->with(null)->andReturn($connection)->once(); + })); + + /** @var Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $sprout->markAsInContext(); + + $this->assertFalse($sprout->hasCurrentTenancy()); + + $creator = new SproutDatabaseSessionHandlerCreator( + $this->app, + $sprout + ); + + $handler = $creator(); + + $this->assertInstanceOf(SproutDatabaseSessionHandler::class, $handler); + $this->assertFalse($handler->hasTenancy()); + $this->assertFalse($handler->hasTenant()); + } } diff --git a/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php index d344974..29601c4 100644 --- a/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php +++ b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php @@ -102,7 +102,7 @@ public function canWriteSessionData(?Tenancy $tenancy, ?Tenant $tenant, string $ $mock->shouldReceive('put')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId, 'my-session-data', true])->once(); }))->setTenancy($tenancy)->setTenant($tenant); - $handler->write($sessionId, 'my-session-data'); + $this->assertTrue($handler->write($sessionId, 'my-session-data')); } #[Test, DataProvider('fileSessionDataProvider')] @@ -114,7 +114,7 @@ public function canDestroySessionData($tenancy, $tenant, $expectedPath): void $mock->shouldReceive('delete')->withArgs([$expectedPath . DIRECTORY_SEPARATOR . $sessionId])->once(); }))->setTenancy($tenancy)->setTenant($tenant); - $handler->destroy($sessionId); + $this->assertTrue($handler->destroy($sessionId)); } public static function fileSessionDataProvider(): array From e60762c66848d19c9b4ecf8af695eb6e9739be22 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Fri, 24 Jan 2025 23:35:16 +0000 Subject: [PATCH 43/48] test(overrides): Add more tests for auth and adjust existing ones --- composer.json | 3 +- infection.json5 | 3 +- src/Concerns/AwareOfTenant.php | 2 +- src/Overrides/BaseOverride.php | 2 +- .../Cache/SproutCacheDriverCreator.php | 6 +- src/Overrides/CacheOverride.php | 16 +- .../SproutFilesystemDriverCreator.php | 5 +- src/Overrides/FilesystemOverride.php | 39 ++- src/Overrides/SessionOverride.php | 8 +- src/SproutServiceProvider.php | 8 + tests/Unit/Overrides/AuthOverrideTest.php | 12 +- .../Cache/SproutCacheDriverCreatorTest.php | 216 +++++++++++++ tests/Unit/Overrides/CacheOverrideTest.php | 215 +++++++++--- .../Unit/Overrides/FilesystemOverrideTest.php | 305 ++++++++++++++++++ .../Session/SproutFileSessionHandlerTest.php | 4 +- tests/Unit/Overrides/SessionOverrideTest.php | 5 + tests/Unit/SproutServiceProviderTest.php | 22 ++ 17 files changed, 789 insertions(+), 82 deletions(-) create mode 100644 tests/Unit/Overrides/Cache/SproutCacheDriverCreatorTest.php create mode 100644 tests/Unit/Overrides/FilesystemOverrideTest.php diff --git a/composer.json b/composer.json index 2cba323..0ce07db 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "phpunit/phpunit" : "^11.0.1", "orchestra/testbench": "^9.4", "larastan/larastan" : "^2.9", - "infection/infection": "^0.29.8" + "infection/infection": "^0.29.8", + "brianium/paratest": "^7.7" }, "license" : "MIT", "autoload" : { diff --git a/infection.json5 b/infection.json5 index 938097b..6602671 100644 --- a/infection.json5 +++ b/infection.json5 @@ -16,7 +16,8 @@ "Sprout\\Concerns\\FindsIdentityRouteParameters::*", "Sprout\\Support\\BaseFactory::callCustomCreator", "Sprout\\Overrides\\Auth\\SproutAuthCacheTokenRepository::getTenantedPrefix", - "Sprout\\Overrides\\Auth\\SproutAuthDatabaseTokenRepository::*" + "Sprout\\Overrides\\Auth\\SproutAuthDatabaseTokenRepository::*", + "Sprout\\Overrides\\Cache\\SproutCacheDriverCreator::*" ] }, "PublicVisibility": { diff --git a/src/Concerns/AwareOfTenant.php b/src/Concerns/AwareOfTenant.php index 1f9d1c4..e0509aa 100644 --- a/src/Concerns/AwareOfTenant.php +++ b/src/Concerns/AwareOfTenant.php @@ -28,7 +28,7 @@ trait AwareOfTenant */ public function shouldBeRefreshed(): bool { - return true; + return true; // @codeCoverageIgnore } /** diff --git a/src/Overrides/BaseOverride.php b/src/Overrides/BaseOverride.php index 879de0a..65bc533 100644 --- a/src/Overrides/BaseOverride.php +++ b/src/Overrides/BaseOverride.php @@ -3,7 +3,7 @@ namespace Sprout\Overrides; -use Illuminate\Foundation\Application; +use Illuminate\Contracts\Foundation\Application; use Sprout\Contracts\ServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; diff --git a/src/Overrides/Cache/SproutCacheDriverCreator.php b/src/Overrides/Cache/SproutCacheDriverCreator.php index 1b7754d..178445d 100644 --- a/src/Overrides/Cache/SproutCacheDriverCreator.php +++ b/src/Overrides/Cache/SproutCacheDriverCreator.php @@ -89,15 +89,15 @@ public function __invoke(): Repository // We need to know which store we're overriding to make tenanted if (! isset($this->config['override'])) { - throw MisconfigurationException::missingConfig('override', self::class, 'service override'); + throw MisconfigurationException::missingConfig('override', 'service override', 'cache'); } // We need to get the config for that store /** @var array $storeConfig */ - $storeConfig = config('cache.stores.' . $this->config['override']); + $storeConfig = $this->app->make('config')->get('cache.stores.' . $this->config['override']); if (empty($storeConfig)) { - throw new InvalidArgumentException('Cache store [' . $this->config['override'] . '] is not defined.'); + throw new InvalidArgumentException('Cache store [' . $this->config['override'] . '] is not defined'); } // Get the prefix for the tenanted store based on the store config, diff --git a/src/Overrides/CacheOverride.php b/src/Overrides/CacheOverride.php index ae07996..a86ec94 100644 --- a/src/Overrides/CacheOverride.php +++ b/src/Overrides/CacheOverride.php @@ -33,7 +33,9 @@ final class CacheOverride extends BaseOverride implements BootableServiceOverrid */ public function boot(Application $app, Sprout $sprout): void { - $tracker = fn(string $store) => $this->drivers[] = $store; + $this->setApp($app)->setSprout($sprout); + + $tracker = fn (string $store) => $this->drivers[] = $store; // If the cache manager has been resolved, we can add the driver if ($app->resolved('cache')) { @@ -57,6 +59,16 @@ protected function addDriver(CacheManager $manager, Sprout $sprout, Closure $tra }); } + /** + * Get the drivers that have been resolved + * + * @return array + */ + public function getDrivers(): array + { + return $this->drivers; + } + /** * Clean up the service override * @@ -80,7 +92,7 @@ protected function addDriver(CacheManager $manager, Sprout $sprout, Closure $tra public function cleanup(Tenancy $tenancy, Tenant $tenant): void { if (! empty($this->drivers)) { - app('cache')->forgetDriver($this->drivers); + $this->getApp()->make('cache')->forgetDriver($this->drivers); $this->drivers = []; } diff --git a/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php index b0ca52b..fc5343d 100644 --- a/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php +++ b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php @@ -162,9 +162,10 @@ protected function getTrueDiskConfig(): array if (is_array($this->config['disk'])) { $diskConfig = $this->config['disk']; } else { + $config = $this->app->make('config'); /** @var string $diskName */ - $diskName = $this->config['disk'] ?? config('filesystems.default'); - $diskConfig = config('filesystems.disks.' . $diskName); + $diskName = $this->config['disk'] ?? $config->get('filesystems.default'); + $diskConfig = $config->get('filesystems.disks.' . $diskName); } /** @var array $diskConfig */ diff --git a/src/Overrides/FilesystemOverride.php b/src/Overrides/FilesystemOverride.php index eeb4f58..913611f 100644 --- a/src/Overrides/FilesystemOverride.php +++ b/src/Overrides/FilesystemOverride.php @@ -4,15 +4,12 @@ namespace Sprout\Overrides; use Closure; -use Illuminate\Cache\CacheManager; -use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Foundation\Application; use Illuminate\Filesystem\FilesystemManager; use Sprout\Contracts\BootableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Overrides\Cache\SproutCacheDriverCreator; use Sprout\Overrides\Filesystem\SproutFilesystemDriverCreator; use Sprout\Overrides\Filesystem\SproutFilesystemManager; use Sprout\Sprout; @@ -34,6 +31,16 @@ protected function shouldOverrideManager(): bool return $this->config['manager'] ?? true; // @phpstan-ignore-line } + /** + * Get the drivers that have been resolved + * + * @return array + */ + public function getDrivers(): array + { + return $this->drivers; + } + /** * Boot a service override * @@ -49,6 +56,8 @@ protected function shouldOverrideManager(): bool */ public function boot(Application $app, Sprout $sprout): void { + $this->setApp($app)->setSprout($sprout); + // If we're overriding the filesystem manager if ($this->shouldOverrideManager()) { $original = null; @@ -65,7 +74,7 @@ public function boot(Application $app, Sprout $sprout): void $app->singleton('filesystem', fn ($app) => new SproutFilesystemManager($app, $original)); } - $tracker = fn(string $store) => $this->drivers[] = $store; + $tracker = fn (string $store) => $this->drivers[] = $store; // If the filesystem manager has been resolved, we can add the driver if ($app->resolved('filesystem')) { @@ -113,19 +122,19 @@ protected function addDriver(FilesystemManager $manager, Sprout $sprout, Closure */ public function cleanup(Tenancy $tenancy, Tenant $tenant): void { - /** @var array> $diskConfig */ - $diskConfig = config('filesystems.disks', []); - - /** @var \Illuminate\Filesystem\FilesystemManager $filesystemManager */ - $filesystemManager = app(FilesystemManager::class); + if ($this->getApp()->resolved('filesystem')) { + /** @var \Illuminate\Filesystem\FilesystemManager $filesystemManager */ + $filesystemManager = $this->getApp()->make(FilesystemManager::class); - // If it's our custom filesystem manager, we know that we have the names - // of the created disks - if ($filesystemManager instanceof SproutFilesystemManager) { - if (! empty($this->drivers)) { - $filesystemManager->forgetDisk($this->drivers); + // If we're tracking some drivers we can simply forget those + if (! empty($this->getDrivers())) { + $filesystemManager->forgetDisk($this->getDrivers()); + $this->drivers = []; } - } else { + + /** @var array> $diskConfig */ + $diskConfig = $this->getApp()->make('config')->get('filesystems.disks', []); + // But if we don't, we have to cycle through the config and pick out // any that have the 'sprout' driver foreach ($diskConfig as $disk => $config) { diff --git a/src/Overrides/SessionOverride.php b/src/Overrides/SessionOverride.php index 701a285..969f236 100644 --- a/src/Overrides/SessionOverride.php +++ b/src/Overrides/SessionOverride.php @@ -39,6 +39,8 @@ final class SessionOverride extends BaseOverride implements BootableServiceOverr */ public function boot(Application $app, Sprout $sprout): void { + $this->setApp($app)->setSprout($sprout); + // If the session manager has been resolved, we can add the driver if ($app->resolved('session')) { $manager = $app->make('session'); @@ -86,8 +88,8 @@ protected function addDriver(SessionManager $manager, Application $app, Sprout $ public function setup(Tenancy $tenancy, Tenant $tenant): void { /** @var \Illuminate\Contracts\Config\Repository $config */ - $config = config(); - $settings = settings(); + $config = $this->app->make('config'); + $settings = $this->sprout->settings(); if (! $settings->has('original.session')) { /** @var array $original */ @@ -171,7 +173,7 @@ private function refreshSessionStore(?Tenancy $tenancy = null, ?Tenant $tenant = { // We only want to touch this if the session manager has actually been // loaded, and is therefore most likely being used - if (app()->resolved('session')) { + if ($this->app->resolved('session')) { $manager = app('session'); // If there are no loaded drivers, we can exit early diff --git a/src/SproutServiceProvider.php b/src/SproutServiceProvider.php index 18c2ba1..a22c83a 100644 --- a/src/SproutServiceProvider.php +++ b/src/SproutServiceProvider.php @@ -33,6 +33,7 @@ class SproutServiceProvider extends ServiceProvider public function register(): void { $this->registerSprout(); + $this->registerDefaultBindings(); $this->registerManagers(); $this->registerMiddleware(); $this->registerRouteMixin(); @@ -51,6 +52,13 @@ private function registerSprout(): void $this->app->bind(SettingsRepository::class, fn () => $this->sprout->settings()); } + private function registerDefaultBindings(): void + { + // Bind the tenancy and tenant contracts + $this->app->bind(Tenancy::class, fn () => $this->sprout->getCurrentTenancy()); + $this->app->bind(Tenant::class, fn () => $this->sprout->getCurrentTenancy()?->tenant()); + } + private function registerManagers(): void { // Register the tenant provider manager diff --git a/tests/Unit/Overrides/AuthOverrideTest.php b/tests/Unit/Overrides/AuthOverrideTest.php index 5478ede..cefa4a0 100644 --- a/tests/Unit/Overrides/AuthOverrideTest.php +++ b/tests/Unit/Overrides/AuthOverrideTest.php @@ -7,25 +7,20 @@ use Illuminate\Auth\AuthManager; use Illuminate\Auth\Passwords\PasswordBrokerManager; use Illuminate\Config\Repository; +use Illuminate\Contracts\Foundation\Application; use Mockery; use Mockery\MockInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; -use Sprout\Contracts\DeferrableServiceOverride; use Sprout\Contracts\Tenancy; use Sprout\Contracts\Tenant; -use Sprout\Overrides\Auth\SproutAuthCacheTokenRepository; -use Sprout\Overrides\Auth\SproutAuthDatabaseTokenRepository; use Sprout\Overrides\Auth\SproutAuthPasswordBrokerManager; use Sprout\Overrides\AuthOverride; use Sprout\Sprout; use Sprout\Support\SettingsRepository; use Sprout\Tests\Unit\UnitTestCase; -use Workbench\App\Models\TenantModel; -use Workbench\App\Models\User; use function Sprout\sprout; -use function Sprout\tenancy; class AuthOverrideTest extends UnitTestCase { @@ -94,6 +89,11 @@ public function bootsCorrectly(bool $return): void $sprout = new Sprout($app, new SettingsRepository()); $override->boot($app, $sprout); + + // These are only here because there would be errors if their + // corresponding setters were not called + $this->assertInstanceOf(Application::class, $override->getApp()); + $this->assertInstanceOf(Sprout::class, $override->getSprout()); } #[Test] diff --git a/tests/Unit/Overrides/Cache/SproutCacheDriverCreatorTest.php b/tests/Unit/Overrides/Cache/SproutCacheDriverCreatorTest.php new file mode 100644 index 0000000..389d460 --- /dev/null +++ b/tests/Unit/Overrides/Cache/SproutCacheDriverCreatorTest.php @@ -0,0 +1,216 @@ +shouldReceive('getTenantKey')->andReturn(7777777)->once(); + } + }); + + return Mockery::mock(Tenancy::class, static function (Mockery\MockInterface $mock) use ($getsKey, $withTenant, $tenant) { + $mock->shouldReceive('check')->andReturn($withTenant)->once(); + + if ($getsKey) { + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + } + + if ($withTenant) { + $mock->shouldReceive('tenant')->andReturn($tenant)->once(); + } + }); + } + + #[Test] + public function canCreateTheDriver(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock($this->app, static function (Mockery\MockInterface $mock) { + $mock->makePartial(); + + $mock->shouldReceive('make') + ->with('config') + ->andReturn(Mockery::mock(Repository::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('get') + ->with('cache.stores.my-fake-store') + ->andReturn([ + 'driver' => 'null', + 'prefix' => 'hello-there', + ]) + ->once(); + })); + }); + $cache = Mockery::mock(CacheManager::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('build') + ->andReturn(Mockery::mock(CacheRepository::class)) + ->with(Mockery::on(static function ($arg) { + return is_array($arg) + && isset($arg['driver']) + && $arg['driver'] === 'null' + && isset($arg['prefix']) + && $arg['prefix'] === 'hello-there_my-tenancy_7777777'; + })) + ->once(); + }); + $sprout = new Sprout($app, new SettingsRepository()); + $creator = new SproutCacheDriverCreator( + $app, + $cache, + ['override' => 'my-fake-store',], + $sprout + ); + + $tenancy = $this->mockTenancy(); + + $sprout->setCurrentTenancy($tenancy); + + $store = $creator(); + + $this->assertInstanceOf(CacheRepository::class, $store); + } + + #[Test] + public function throwsAnExceptionWhenOutsideMultitenantedContext(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock(Application::class); + $cache = Mockery::mock(CacheManager::class); + $sprout = new Sprout($app, new SettingsRepository()); + $creator = new SproutCacheDriverCreator( + $app, + $cache, + ['override' => 'my-fake-store',], + $sprout + ); + + $this->expectException(TenancyMissingException::class); + $this->expectExceptionMessage('There is no current tenancy'); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenThereIsNoTenancy(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock(Application::class); + $cache = Mockery::mock(CacheManager::class); + $sprout = new Sprout($app, new SettingsRepository()); + $creator = new SproutCacheDriverCreator( + $app, + $cache, + ['override' => 'my-fake-store',], + $sprout + ); + + $sprout->markAsInContext(); + + $this->expectException(TenancyMissingException::class); + $this->expectExceptionMessage('There is no current tenancy'); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenThereIsNoTenant(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock(Application::class, static function (Mockery\MockInterface $mock) { + $mock->shouldIgnoreMissing(); + }); + $cache = Mockery::mock(CacheManager::class); + $sprout = new Sprout($app, new SettingsRepository()); + $creator = new SproutCacheDriverCreator( + $app, + $cache, + ['override' => 'my-fake-store',], + $sprout + ); + + $sprout->setCurrentTenancy($this->mockTenancy(false)); + + $this->expectException(TenantMissingException::class); + $this->expectExceptionMessage('There is no current tenant for tenancy [my-tenancy]'); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenThereIsNoOverride(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock(Application::class, static function (Mockery\MockInterface $mock) { + $mock->shouldIgnoreMissing(); + }); + $cache = Mockery::mock(CacheManager::class); + $sprout = new Sprout($app, new SettingsRepository()); + $creator = new SproutCacheDriverCreator( + $app, + $cache, + [], + $sprout + ); + + $sprout->setCurrentTenancy($this->mockTenancy(true, false)); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The service override [cache] is missing a required value for \'override\''); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenTheOverrideIsNotConfigured(): void + { + /** @var \Illuminate\Foundation\Application&\Mockery\MockInterface $app */ + $app = Mockery::mock(Application::class, static function (Mockery\MockInterface $mock) { + $mock->shouldIgnoreMissing(); + + $mock->shouldReceive('make') + ->with('config') + ->andReturn(Mockery::mock(Repository::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('get') + ->with('cache.stores.my-fake-store') + ->andReturnNull() + ->once(); + })); + }); + $cache = Mockery::mock(CacheManager::class); + $sprout = new Sprout($app, new SettingsRepository()); + $creator = new SproutCacheDriverCreator( + $app, + $cache, + ['override' => 'my-fake-store'], + $sprout + ); + + $sprout->setCurrentTenancy($this->mockTenancy(true, false)); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cache store [my-fake-store] is not defined'); + + $creator(); + } +} diff --git a/tests/Unit/Overrides/CacheOverrideTest.php b/tests/Unit/Overrides/CacheOverrideTest.php index fa63831..2973b53 100644 --- a/tests/Unit/Overrides/CacheOverrideTest.php +++ b/tests/Unit/Overrides/CacheOverrideTest.php @@ -3,16 +3,22 @@ namespace Sprout\Tests\Unit\Overrides; +use Closure; use Illuminate\Cache\CacheManager; use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; +use Mockery; use Mockery\MockInterface; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Sprout\Contracts\BootableServiceOverride; +use Sprout\Contracts\Tenancy; +use Sprout\Contracts\Tenant; use Sprout\Overrides\CacheOverride; +use Sprout\Sprout; +use Sprout\Support\SettingsRepository; use Sprout\Tests\Unit\UnitTestCase; -use Workbench\App\Models\TenantModel; use function Sprout\sprout; -use function Sprout\tenancy; class CacheOverrideTest extends UnitTestCase { @@ -23,6 +29,23 @@ protected function defineEnvironment($app): void }); } + private function mockCacheManager(): CacheManager&MockInterface + { + /** @var CacheManager&MockInterface $app */ + $app = Mockery::mock(CacheManager::class, static function (MockInterface $mock) { + $mock->shouldReceive('extend') + ->withArgs([ + 'sprout', + Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + }), + ]) + ->once(); + }); + + return $app; + } + #[Test] public function isBuiltCorrectly(): void { @@ -50,79 +73,181 @@ public function isRegisteredWithSproutCorrectly(): void $this->assertTrue($sprout->overrides()->hasOverrideBooted('cache')); } + #[Test, DataProvider('cacheResolvedDataProvider')] + public function bootsCorrectly(bool $return): void + { + $override = new CacheOverride('cache', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, function (MockInterface $mock) use ($return) { + $mock->makePartial(); + $mock->shouldReceive('resolved')->withArgs(['cache'])->andReturn($return)->once(); + + if ($return) { + $mock->shouldReceive('make') + ->with('cache') + ->andReturn($this->mockCacheManager()) + ->once(); + } else { + $mock->shouldReceive('afterResolving') + ->withArgs([ + 'cache', + Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + }), + ]) + ->once(); + } + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $override->boot($app, $sprout); + + // These are only here because there would be errors if their + // corresponding setters were not called + $this->assertInstanceOf(\Illuminate\Contracts\Foundation\Application::class, $override->getApp()); + $this->assertInstanceOf(Sprout::class, $override->getSprout()); + } + #[Test] - public function addsSproutDriverToCacheManager(): void + public function addsDriverCacheManagerHasBeenResolved(): void { - $sprout = sprout(); + $override = new CacheOverride('cache', []); - config()->set('sprout.overrides', [ - 'cache' => [ - 'driver' => CacheOverride::class, - ], - ]); + $app = Mockery::mock(Application::class, static function (MockInterface $mock) { + $mock->makePartial(); + }); - config()->set('cache.stores.null', [ - 'driver' => 'null', - ]); + $app->singleton('cache', function () { + return $this->mockCacheManager(); + }); - $sprout->overrides()->registerOverrides(); + $sprout = new Sprout($app, new SettingsRepository()); + + $override->boot($app, $sprout); + + $app->make('cache'); + } + + #[Test] + public function keepsTrackOfResolvedSproutDrivers(): void + { + $override = new CacheOverride('cache', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + $tenant = Mockery::mock(Tenant::class, static function (MockInterface $mock) { + $mock->shouldReceive('getTenantKey')->andReturn(7777)->once(); + }); + $tenancy = Mockery::mock(Tenancy::class, static function (MockInterface $mock) use ($tenant) { + $mock->shouldReceive('check')->andReturnTrue()->once(); + $mock->shouldReceive('tenant')->andReturn($tenant)->once(); + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + }); - $tenant = TenantModel::factory()->createOne(); - $tenancy = tenancy(); + $sprout->setCurrentTenancy($tenancy); - $tenancy->setTenant($tenant); - sprout()->setCurrentTenancy($tenancy); + $override->boot($app, $sprout); - $manager = $this->app->make('cache'); + $cache = $app->make('cache'); - $disk = $manager->build([ + $cache->build([ 'driver' => 'sprout', - 'override' => 'null', + 'override' => 'array', ]); - $this->assertInstanceOf(\Illuminate\Contracts\Cache\Repository::class, $disk); + $this->assertNotEmpty($override->getDrivers()); + $this->assertContains('ondemand', $override->getDrivers()); } #[Test] - public function performsCleanup(): void + public function cleansUpResolvedDrivers(): void { - $sprout = sprout(); + $override = new CacheOverride('cache', []); + $cache = Mockery::mock($this->app->make('cache'), static function (MockInterface $mock) { + $mock->makePartial(); + $mock->shouldReceive('forgetDriver')->once(); + }); + $this->app->forgetInstance('cache'); - config()->set('sprout.overrides', [ - 'cache' => [ - 'driver' => CacheOverride::class, - ], - ]); + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) use ($cache) { + $mock->makePartial(); - config()->set('cache.stores.null', [ - 'driver' => 'null', - ]); + $mock->shouldReceive('make') + ->with('cache') + ->andReturn($cache); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + $tenant = Mockery::mock(Tenant::class, static function (MockInterface $mock) { + $mock->shouldReceive('getTenantKey')->andReturn(7777)->once(); + }); + $tenancy = Mockery::mock(Tenancy::class, static function (MockInterface $mock) use ($tenant) { + $mock->shouldReceive('check')->andReturnTrue()->once(); + $mock->shouldReceive('tenant')->andReturn($tenant)->once(); + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + }); + + $sprout->setCurrentTenancy($tenancy); - config()->set('cache.stores.sprout', [ + $override->boot($app, $sprout); + + $cache->build([ 'driver' => 'sprout', - 'override' => 'null', + 'override' => 'array', ]); - $this->app->forgetInstance('cache'); + $this->assertNotEmpty($override->getDrivers()); + $this->assertContains('ondemand', $override->getDrivers()); - $sprout->overrides()->registerOverrides(); + $override->cleanup($tenancy, $tenant); - $override = $sprout->overrides()->get('cache'); + $this->assertEmpty($override->getDrivers()); + } - $this->assertInstanceOf(CacheOverride::class, $override); + #[Test] + public function cleansUpNothingWithoutResolvedDrivers(): void + { + $override = new CacheOverride('cache', []); + $cache = Mockery::mock($this->app->make('cache'), static function (MockInterface $mock) { + $mock->makePartial(); + $mock->shouldNotReceive('forgetDriver'); + }); + $this->app->forgetInstance('cache'); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) use ($cache) { + $mock->makePartial(); + + $mock->shouldReceive('make') + ->with('cache') + ->andReturn($cache); + }); - $tenant = TenantModel::factory()->createOne(); - $tenancy = tenancy(); + $sprout = new Sprout($app, new SettingsRepository()); + $tenant = Mockery::mock(Tenant::class); + $tenancy = Mockery::mock(Tenancy::class); - $tenancy->setTenant($tenant); - sprout()->setCurrentTenancy($tenancy); + $sprout->setCurrentTenancy($tenancy); - $this->app->make('cache')->store('sprout'); + $override->boot($app, $sprout); - $this->instance('cache', $this->spy(CacheManager::class, function (MockInterface $mock) { - $mock->shouldReceive('forgetDriver')->once()->withArgs([['sprout']]); - })); + $this->assertEmpty($override->getDrivers()); $override->cleanup($tenancy, $tenant); } + + public static function cacheResolvedDataProvider(): array + { + return [ + 'cache resolved' => [true], + 'cache not resolved' => [false], + ]; + } } diff --git a/tests/Unit/Overrides/FilesystemOverrideTest.php b/tests/Unit/Overrides/FilesystemOverrideTest.php new file mode 100644 index 0000000..a63ae7c --- /dev/null +++ b/tests/Unit/Overrides/FilesystemOverrideTest.php @@ -0,0 +1,305 @@ +set('sprout.overrides', []); + }); + } + + private function mockFilesystemManager(): FilesystemManager&MockInterface + { + return Mockery::mock(FilesystemManager::class, static function (MockInterface $mock) { + $mock->shouldReceive('extend') + ->with('sprout', Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + })) + ->once(); + }); + } + + #[Test] + public function isBuiltCorrectly(): void + { + $this->assertTrue(is_subclass_of(FilesystemOverride::class, BootableServiceOverride::class)); + } + + #[Test] + public function isRegisteredWithSproutCorrectly(): void + { + $sprout = sprout(); + + config()->set('sprout.overrides', [ + 'filesystem' => [ + 'driver' => FilesystemOverride::class, + ], + ]); + + $this->assertFalse($sprout->overrides()->hasOverride('filesystem')); + + $sprout->overrides()->registerOverrides(); + + $this->assertTrue($sprout->overrides()->hasOverride('filesystem')); + $this->assertSame(FilesystemOverride::class, $sprout->overrides()->getOverrideClass('filesystem')); + $this->assertTrue($sprout->overrides()->isOverrideBootable('filesystem')); + $this->assertTrue($sprout->overrides()->hasOverrideBooted('filesystem')); + } + + #[Test, DataProvider('filesystemResolvedDataProvider')] + public function bootsCorrectly(bool $return, bool $overrideManager): void + { + $override = new FilesystemOverride('filesystem', ['manager' => $overrideManager]); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, function (MockInterface $mock) use ($return, $overrideManager) { + $mock->makePartial(); + + if ($overrideManager) { + if ($return) { + $mock->shouldReceive('forgetInstance')->with('filesystem')->once(); + } + + $mock->shouldReceive('singleton') + ->with('filesystem', Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + })) + ->once(); + } + + $mock->shouldReceive('resolved')->withArgs(['filesystem'])->andReturn($return)->times($overrideManager ? 2 : 1); + + if ($return) { + $mock->shouldReceive('make') + ->with('filesystem') + ->andReturn($this->mockFilesystemManager()) + ->times($overrideManager ? 2 : 1); + } else { + $mock->shouldReceive('afterResolving') + ->withArgs([ + 'filesystem', + Mockery::on(static function ($arg) { + return is_callable($arg) && $arg instanceof Closure; + }), + ]) + ->once(); + } + }); + + $sprout = new Sprout($app, new SettingsRepository()); + + $override->boot($app, $sprout); + + // These are only here because there would be errors if their + // corresponding setters were not called + $this->assertInstanceOf(Application::class, $override->getApp()); + $this->assertInstanceOf(Sprout::class, $override->getSprout()); + } + + #[Test] + public function addsDriverCacheManagerHasBeenResolved(): void + { + $override = new FilesystemOverride('filesystem', [ + 'manager' => false, + ]); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $app->singleton('filesystem', fn () => $this->mockFilesystemManager()); + + $sprout = new Sprout($app, new SettingsRepository()); + + $override->boot($app, $sprout); + + $app->make('filesystem'); + } + + #[Test] + public function keepsTrackOfResolvedSproutDrivers(): void + { + $override = new FilesystemOverride('filesystem', []); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + $tenant = Mockery::mock(Tenant::class, TenantHasResources::class, static function (MockInterface $mock) { + $mock->shouldReceive('getTenantResourceKey')->andReturn('my-resource-key')->once(); + }); + $tenancy = Mockery::mock(Tenancy::class, static function (MockInterface $mock) use ($tenant) { + $mock->shouldReceive('check')->andReturnTrue()->once(); + $mock->shouldReceive('tenant')->andReturn($tenant)->once(); + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + }); + + $sprout->setCurrentTenancy($tenancy); + + $override->boot($app, $sprout); + + $filesystem = $app->make('filesystem'); + + $filesystem->build([ + 'driver' => 'sprout', + 'disk' => 'local', + ]); + + $this->assertNotEmpty($override->getDrivers()); + $this->assertContains('ondemand', $override->getDrivers()); + } + + #[Test] + public function cleansUpResolvedDrivers(): void + { + $override = new FilesystemOverride('filesystem', []); + + $this->app->forgetInstance('filesystem'); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + $tenant = Mockery::mock(Tenant::class, TenantHasResources::class, static function (MockInterface $mock) { + $mock->shouldReceive('getTenantResourceKey')->andReturn('my-resource-key')->once(); + }); + $tenancy = Mockery::mock(Tenancy::class, static function (MockInterface $mock) use ($tenant) { + $mock->shouldReceive('check')->andReturnTrue()->once(); + $mock->shouldReceive('tenant')->andReturn($tenant)->once(); + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + }); + + $sprout->setCurrentTenancy($tenancy); + + $override->boot($app, $sprout); + + $this->assertEmpty($override->getDrivers()); + + $filesystem = $app->make('filesystem'); + + $filesystem->build([ + 'driver' => 'sprout', + 'disk' => 'local', + ]); + + $this->assertNotEmpty($override->getDrivers()); + $this->assertContains('ondemand', $override->getDrivers()); + + $override->cleanup($tenancy, $tenant); + + $this->assertEmpty($override->getDrivers()); + } + + #[Test] + public function cleansUpResolvedDriversFromPreconfiguredDisks(): void + { + $override = new FilesystemOverride('filesystem', []); + + $this->app->forgetInstance('filesystem'); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $app->make('config')->set('filesystems.disks.my-disk', [ + 'driver' => 'sprout', + 'disk' => 'local' + ]); + + $sprout = new Sprout($app, new SettingsRepository()); + $tenant = Mockery::mock(Tenant::class, TenantHasResources::class, static function (MockInterface $mock) { + $mock->shouldReceive('getTenantResourceKey')->andReturn('my-resource-key')->once(); + }); + $tenancy = Mockery::mock(Tenancy::class, static function (MockInterface $mock) use ($tenant) { + $mock->shouldReceive('check')->andReturnTrue()->once(); + $mock->shouldReceive('tenant')->andReturn($tenant)->once(); + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + }); + + $sprout->setCurrentTenancy($tenancy); + + $override->boot($app, $sprout); + + $this->assertEmpty($override->getDrivers()); + + $filesystem = $app->make('filesystem'); + + $filesystem->disk('my-disk'); + + $this->assertNotEmpty($override->getDrivers()); + $this->assertContains('my-disk', $override->getDrivers()); + + $override->cleanup($tenancy, $tenant); + + $this->assertEmpty($override->getDrivers()); + } + + #[Test] + public function cleansUpNothingWithoutResolvedDrivers(): void + { + $override = new FilesystemOverride('filesystem', []); + + $this->app->forgetInstance('filesystem'); + + /** @var \Illuminate\Foundation\Application&MockInterface $app */ + $app = Mockery::mock($this->app, static function (MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new Sprout($app, new SettingsRepository()); + $tenant = Mockery::mock(Tenant::class, TenantHasResources::class); + $tenancy = Mockery::mock(Tenancy::class); + + $sprout->setCurrentTenancy($tenancy); + + $override->boot($app, $sprout); + + $this->assertEmpty($override->getDrivers()); + + $app->make('filesystem'); + + $this->assertEmpty($override->getDrivers()); + + $override->cleanup($tenancy, $tenant); + + $this->assertEmpty($override->getDrivers()); + } + + public static function filesystemResolvedDataProvider(): array + { + return [ + 'cache resolved no manager override' => [true, false], + 'cache not resolved no manager override' => [false, false], + 'cache resolved manager override' => [true, true], + 'cache not resolved manager override' => [false, true], + ]; + } +} diff --git a/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php index 29601c4..cfa37be 100644 --- a/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php +++ b/tests/Unit/Overrides/Session/SproutFileSessionHandlerTest.php @@ -25,7 +25,7 @@ protected function defineEnvironment($app): void protected function createHandler(?Tenancy $tenancy = null, ?Tenant $tenant = null, ?Filesystem $files = null): SproutFileSessionHandler { - $defaultPath = '/default/path'; + $defaultPath = '/default/path' . ($tenancy !== null ? '/' : ''); $lifetime = config('session.lifetime'); $files ??= Mockery::mock(Filesystem::class); @@ -126,7 +126,7 @@ public static function fileSessionDataProvider(): array return [ 'outside of tenant context' => [null, null, $defaultPath], - 'inside of tenant context' => [$tenancy, $tenant, $defaultPath . DIRECTORY_SEPARATOR . 'tenant-resource-key'], + 'inside of tenant context' => [$tenancy, $tenant, $defaultPath . DIRECTORY_SEPARATOR . 'tenant-resource-key'], ]; } } diff --git a/tests/Unit/Overrides/SessionOverrideTest.php b/tests/Unit/Overrides/SessionOverrideTest.php index aef1dd3..ff45b27 100644 --- a/tests/Unit/Overrides/SessionOverrideTest.php +++ b/tests/Unit/Overrides/SessionOverrideTest.php @@ -242,6 +242,11 @@ public function bootsCorrectlyWhenSessionManagerHasNotBeenResolved(bool $databas $sprout = new Sprout($app, new SettingsRepository()); $override->boot($app, $sprout); + + // These are only here because there would be errors if their + // corresponding setters were not called + $this->assertInstanceOf(\Illuminate\Contracts\Foundation\Application::class, $override->getApp()); + $this->assertInstanceOf(Sprout::class, $override->getSprout()); } #[Test, DataProvider('overrideDatabaseSetting')] diff --git a/tests/Unit/SproutServiceProviderTest.php b/tests/Unit/SproutServiceProviderTest.php index b261466..f892f08 100644 --- a/tests/Unit/SproutServiceProviderTest.php +++ b/tests/Unit/SproutServiceProviderTest.php @@ -8,7 +8,12 @@ use Illuminate\Routing\Events\RouteMatched; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; +use Mockery; +use Mockery\MockInterface; use PHPUnit\Framework\Attributes\Test; +use Sprout\Contracts\Tenancy; +use Sprout\Contracts\Tenant; +use Sprout\Contracts\TenantAware; use Sprout\Events\CurrentTenantChanged; use Sprout\Http\Middleware\TenantRoutes; use Sprout\Listeners\IdentifyTenantOnRouting; @@ -141,6 +146,23 @@ public function registersRouterMixinMethods(): void $this->assertTrue(Router::hasMacro('tenanted')); } + #[Test] + public function registersTenantAwareHandling(): void + { + $tenantAware = Mockery::mock(TenantAware::class, static function (MockInterface $mock) { + $mock->shouldReceive('shouldBeRefreshed')->andReturn(true)->once(); + $mock->shouldReceive('setTenant')->once(); + $mock->shouldReceive('setTenancy')->once(); + }); + + $this->app->singleton(TenantAware::class, fn() => $tenantAware); + + $this->app->make(TenantAware::class); + + $this->app->extend(Tenancy::class, fn(?Tenancy $tenancy) => $tenancy); + $this->app->extend(Tenant::class, fn(?Tenant $tenant) => $tenant); + } + #[Test] public function publishesConfig(): void { From 291c1539a9eefa41e25990642bbaf03de2e830be Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Fri, 24 Jan 2025 23:37:11 +0000 Subject: [PATCH 44/48] chore: Remove unneeded phpstan error silencing --- src/Overrides/Cache/SproutCacheDriverCreator.php | 2 +- src/Overrides/Filesystem/SproutFilesystemDriverCreator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Overrides/Cache/SproutCacheDriverCreator.php b/src/Overrides/Cache/SproutCacheDriverCreator.php index 178445d..b42e3bf 100644 --- a/src/Overrides/Cache/SproutCacheDriverCreator.php +++ b/src/Overrides/Cache/SproutCacheDriverCreator.php @@ -19,7 +19,7 @@ final class SproutCacheDriverCreator /** * @var \Illuminate\Contracts\Foundation\Application */ - private Application $app; // @phpstan-ignore-line + private Application $app; /** * @var \Illuminate\Cache\CacheManager diff --git a/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php index fc5343d..7df0155 100644 --- a/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php +++ b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php @@ -27,7 +27,7 @@ /** * @var \Illuminate\Contracts\Foundation\Application */ - private Application $app; // @phpstan-ignore-line + private Application $app; /** * @var \Illuminate\Filesystem\FilesystemManager From b3dda9c3eb037a54d847323529659303e88f94bd Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 25 Jan 2025 11:00:14 +0000 Subject: [PATCH 45/48] test(overrides): Added a test for the filesystem manager --- .../Overrides/SproutFilesystemManagerTest.php | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 tests/Unit/Overrides/SproutFilesystemManagerTest.php diff --git a/tests/Unit/Overrides/SproutFilesystemManagerTest.php b/tests/Unit/Overrides/SproutFilesystemManagerTest.php new file mode 100644 index 0000000..7fa1994 --- /dev/null +++ b/tests/Unit/Overrides/SproutFilesystemManagerTest.php @@ -0,0 +1,142 @@ +assertTrue($sprout->wasSyncedFromOriginal()); + } + + #[Test] + public function addsNameWhenResolvingDriver(): void + { + $app = Mockery::mock(Application::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('offsetGet') + ->with('config') + ->andReturn( + Mockery::mock(Repository::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('offsetGet') + ->with('filesystems.disks.fake-disk') + ->andReturn([ + 'driver' => 'fake', + ]) + ->once(); + }) + ) + ->once(); + }); + + $sprout = new SproutFilesystemManager($app); + + $sprout->extend('fake', function (Application $app, array $config) { + $this->assertArrayHasKey('name', $config); + $this->assertSame('fake-disk', $config['name']); + + return Mockery::mock(Filesystem::class); + }); + + $sprout->disk('fake-disk'); + } + + #[Test] + public function throwsAnExceptionWhenTheresNoDriverInTheConfig(): void + { + $app = Mockery::mock(Application::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('offsetGet') + ->with('config') + ->andReturn( + Mockery::mock(Repository::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('offsetGet') + ->with('filesystems.disks.fake-disk') + ->andReturn([ + 'name' => 'hi', + ]) + ->once(); + }) + ) + ->once(); + }); + + $sprout = new SproutFilesystemManager($app); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Disk [fake-disk] does not have a configured driver.'); + + $sprout->disk('fake-disk'); + } + + #[Test] + public function throwsAnExceptionWhenTheresNoConfig(): void + { + $app = Mockery::mock(Application::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('offsetGet') + ->with('config') + ->andReturn( + Mockery::mock(Repository::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('offsetGet') + ->with('filesystems.disks.fake-disk') + ->andReturn(null) + ->once(); + }) + ) + ->once(); + }); + + $sprout = new SproutFilesystemManager($app); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Disk [fake-disk] does not have a configured driver.'); + + $sprout->disk('fake-disk'); + } + + #[Test] + public function canCreateDisksNormally(): void + { + $app = Mockery::mock($this->app, static function (Mockery\MockInterface $mock) { + $mock->makePartial(); + }); + + $sprout = new SproutFilesystemManager($app); + + $this->assertInstanceOf(LocalFilesystemAdapter::class, $sprout->disk('local')); + } + + #[Test] + public function throwsAnExceptionForDriversThatDoNotExist(): void + { + $app = Mockery::mock($this->app, static function (Mockery\MockInterface $mock) { + $mock->makePartial(); + }); + + $app['config']['filesystems.disks.local.driver'] = 'fake'; + + $sprout = new SproutFilesystemManager($app); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Driver [fake] is not supported.'); + + $sprout->disk('local'); + } +} From 2561469a88b9eb35f2532961a760d6e7348996e0 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 25 Jan 2025 11:46:38 +0000 Subject: [PATCH 46/48] test(overrides): Complete the service override tests --- .../SproutFilesystemDriverCreator.php | 5 +- .../SproutFilesystemDriverCreatorTest.php | 243 ++++++++++++++++++ .../SproutFilesystemManagerTest.php | 2 +- 3 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/Overrides/Filesystem/SproutFilesystemDriverCreatorTest.php rename tests/Unit/Overrides/{ => Filesystem}/SproutFilesystemManagerTest.php (98%) diff --git a/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php index 7df0155..50b62e5 100644 --- a/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php +++ b/src/Overrides/Filesystem/SproutFilesystemDriverCreator.php @@ -74,7 +74,6 @@ public function __invoke(): Filesystem // If we're not within a multitenanted context, we need to error // out, as this driver shouldn't be hit without one if (! $this->sprout->withinContext()) { - // TODO: Create a better exception throw TenancyMissingException::make(); } @@ -96,7 +95,7 @@ public function __invoke(): Filesystem // If the tenant isn't configured for resources, this is another issue if (! ($tenant instanceof TenantHasResources)) { - throw MisconfigurationException::misconfigured('tenant', $tenant::class, 'resources'); + throw MisconfigurationException::misconfigured('tenant', $tenancy->getName(), 'resources'); } // Get a tenant-specific version of the store config @@ -159,7 +158,7 @@ protected function createTenantedPrefix(Tenancy $tenancy, TenantHasResources $te */ protected function getTrueDiskConfig(): array { - if (is_array($this->config['disk'])) { + if (isset($this->config['disk']) && is_array($this->config['disk'])) { $diskConfig = $this->config['disk']; } else { $config = $this->app->make('config'); diff --git a/tests/Unit/Overrides/Filesystem/SproutFilesystemDriverCreatorTest.php b/tests/Unit/Overrides/Filesystem/SproutFilesystemDriverCreatorTest.php new file mode 100644 index 0000000..83e625e --- /dev/null +++ b/tests/Unit/Overrides/Filesystem/SproutFilesystemDriverCreatorTest.php @@ -0,0 +1,243 @@ +shouldReceive('make')->with('config')->andReturn( + Mockery::mock(Repository::class, static function (Mockery\MockInterface $mock) use ($config, $configKey, $default) { + if ($default) { + $mock->shouldReceive('get')->with('filesystems.default')->andReturn('local')->once(); + } + + $mock->shouldReceive('get')->with($configKey)->andReturn($config)->once(); + }) + )->once(); + } + + $mock->shouldIgnoreMissing(); + }); + } + + private function mockManager(?string $driver = null): FilesystemManager&Mockery\MockInterface + { + return Mockery::mock(FilesystemManager::class, static function (Mockery\MockInterface $mock) use ($driver) { + if ($driver) { + $mock->shouldReceive('createScopedDriver') + ->with(Mockery::on(function ($arg) use ($driver) { + return is_array($arg) + && (isset($arg['prefix']) && $arg['prefix'] === 'my-tenancy/my-resource-key') + && (isset($arg['disk']) && is_array($arg['disk'])) + && (isset($arg['disk']['driver']) && $arg['disk']['driver'] === $driver); + })) + ->andReturn(Mockery::mock(Filesystem::class)) + ->once(); + } + }); + } + + private function getSprout(Application $app, bool $withTenancy = true, bool $withTenant = true, bool $withResources = true): Sprout + { + $sprout = new Sprout($app, new SettingsRepository()); + + if ($withTenant) { + if ($withResources) { + $tenant = Mockery::mock(Tenant::class, TenantHasResources::class, static function (Mockery\MockInterface $mock) { + $mock->shouldReceive('getTenantResourceKey')->andReturn('my-resource-key')->once(); + }); + } else { + $tenant = Mockery::mock(Tenant::class); + } + + + } else { + $tenant = null; + } + + if ($withTenancy) { + $sprout->setCurrentTenancy(Mockery::mock(Tenancy::class, static function (Mockery\MockInterface $mock) use ($tenant, $withTenant) { + $mock->shouldReceive('check')->andReturn($withTenant)->once(); + + if ($withTenant) { + $mock->shouldReceive('tenant')->andReturn($tenant)->once(); + } + + $mock->shouldReceive('getName')->andReturn('my-tenancy')->once(); + })); + } + + return $sprout; + } + + #[Test] + public function canCreateTheDriver(): void + { + $app = $this->mockApplication('filesystems.disks.fake-disk', ['driver' => 'fake-driver']); + $manager = $this->mockManager('fake-driver'); + $config = ['disk' => 'fake-disk']; + $sprout = $this->getSprout($app); + + $creator = new SproutFilesystemDriverCreator( + $app, + $manager, + $config, + $sprout + ); + + $creator(); + } + + #[Test] + public function fallsBackToDefaultDiskWhenCreatingDriver(): void + { + $app = $this->mockApplication('filesystems.disks.local', ['driver' => 'local'], true); + $manager = $this->mockManager('local'); + $config = []; + $sprout = $this->getSprout($app); + + $creator = new SproutFilesystemDriverCreator( + $app, + $manager, + $config, + $sprout + ); + + $creator(); + } + + #[Test] + public function canUseOnDemandDiskConfig(): void + { + $app = $this->mockApplication(); + $manager = $this->mockManager('fake-driver'); + $config = ['disk' => ['driver' => 'fake-driver']]; + $sprout = $this->getSprout($app); + + $creator = new SproutFilesystemDriverCreator( + $app, + $manager, + $config, + $sprout + ); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenOutsideOfContext(): void + { + $app = $this->mockApplication(); + $manager = $this->mockManager(); + $config = []; + $sprout = $this->getSprout($app, false, false); + + $sprout->markAsOutsideContext(); + + $this->assertFalse($sprout->withinContext()); + + $creator = new SproutFilesystemDriverCreator( + $app, + $manager, + $config, + $sprout + ); + + $this->expectException(TenancyMissingException::class); + $this->expectExceptionMessage('There is no current tenancy'); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenThereIsNoTenancy(): void + { + $app = $this->mockApplication(); + $manager = $this->mockManager(); + $config = []; + $sprout = $this->getSprout($app, false, false); + + $sprout->markAsInContext(); + + $this->assertTrue($sprout->withinContext()); + + $creator = new SproutFilesystemDriverCreator( + $app, + $manager, + $config, + $sprout + ); + + $this->expectException(TenancyMissingException::class); + $this->expectExceptionMessage('There is no current tenancy'); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenThereIsNoTenant(): void + { + $app = $this->mockApplication(); + $manager = $this->mockManager(); + $config = []; + $sprout = $this->getSprout($app, true, false); + + $this->assertTrue($sprout->withinContext()); + + $creator = new SproutFilesystemDriverCreator( + $app, + $manager, + $config, + $sprout + ); + + $this->expectException(TenantMissingException::class); + $this->expectExceptionMessage('There is no current tenant for tenancy [my-tenancy]'); + + $creator(); + } + + #[Test] + public function throwsAnExceptionWhenTheTenantIsNotConfiguredForResources(): void + { + $app = $this->mockApplication(); + $manager = $this->mockManager(); + $config = []; + $sprout = $this->getSprout($app, true, true, false); + + $this->assertTrue($sprout->withinContext()); + + $creator = new SproutFilesystemDriverCreator( + $app, + $manager, + $config, + $sprout + ); + + $this->expectException(MisconfigurationException::class); + $this->expectExceptionMessage('The current tenant [my-tenancy] is not configured correctly for resources'); + + $creator(); + } +} diff --git a/tests/Unit/Overrides/SproutFilesystemManagerTest.php b/tests/Unit/Overrides/Filesystem/SproutFilesystemManagerTest.php similarity index 98% rename from tests/Unit/Overrides/SproutFilesystemManagerTest.php rename to tests/Unit/Overrides/Filesystem/SproutFilesystemManagerTest.php index 7fa1994..6ecc916 100644 --- a/tests/Unit/Overrides/SproutFilesystemManagerTest.php +++ b/tests/Unit/Overrides/Filesystem/SproutFilesystemManagerTest.php @@ -1,7 +1,7 @@ Date: Sat, 25 Jan 2025 11:46:48 +0000 Subject: [PATCH 47/48] chore: Remove Laravel pint config --- composer.json | 4 ++-- pint.json | 11 ----------- 2 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 pint.json diff --git a/composer.json b/composer.json index 0ce07db..a79a2c2 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "orchestra/testbench": "^9.4", "larastan/larastan" : "^2.9", "infection/infection": "^0.29.8", - "brianium/paratest": "^7.7" + "brianium/paratest" : "^7.7" }, "license" : "MIT", "autoload" : { @@ -79,7 +79,7 @@ "providers": [ "Sprout\\SproutServiceProvider" ], - "facades":[ + "facades" : [ "Sprout\\Facades\\Sprout" ] } diff --git a/pint.json b/pint.json deleted file mode 100644 index ff4270d..0000000 --- a/pint.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "preset": "laravel", - "rules" : { - "declare_strict_types" : true, - "no_superfluous_phpdoc_tags" : false, - "concat_space" : false, - "Laravel/laravel_phpdoc_alignment": false, - "phpdoc_separation" : false, - "binary_operator_spaces" : false - } -} From 72ecd6517fc5c7b1b85fb442b3e46a40ef4ac886 Mon Sep 17 00:00:00 2001 From: Ollie Read Date: Sat, 25 Jan 2025 11:51:10 +0000 Subject: [PATCH 48/48] chore: Ignore ServiceOverrideManager::hasTenancyBeenSetup for code coverage --- src/Managers/ServiceOverrideManager.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Managers/ServiceOverrideManager.php b/src/Managers/ServiceOverrideManager.php index 7e65975..dd95d8e 100644 --- a/src/Managers/ServiceOverrideManager.php +++ b/src/Managers/ServiceOverrideManager.php @@ -141,6 +141,8 @@ public function hasOverrideBeenSetUp(string $service, ?Tenancy $tenancy = null): * * @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Sprout\Exceptions\TenancyMissingException + * + * @codeCoverageIgnore */ public function hasTenancyBeenSetup(?Tenancy $tenancy = null): bool {