diff --git a/assets/config.php b/assets/config.php index a90e849b..e5712767 100644 --- a/assets/config.php +++ b/assets/config.php @@ -8,7 +8,7 @@ use Stancl\Tenancy\Database\Models\Tenant; return [ 'tenant_model' => Tenant::class, 'domain_model' => Domain::class, - 'internal_column_prefix' => 'tenancy_', + 'internal_prefix' => 'tenancy_', 'central_connection' => 'central', 'template_tenant_connection' => null, @@ -46,13 +46,6 @@ return [ */ 'tenant_route_namespace' => 'App\Http\Controllers', - /** - * Central domains (hostnames), e.g. domains which host landing pages, sign up pages, etc. - */ - 'exempt_domains' => [ - // 'localhost', - ], - /** * Tenancy bootstrappers are executed when tenancy is initialized. * Their responsibility is making Laravel features tenant-aware. @@ -201,7 +194,7 @@ return [ * See the documentation page for each class to * understand which ones you want to enable. */ - 'features' => [ + 'features' => [ // todo test features // Stancl\Tenancy\Features\Timestamps::class, // https://tenancy.samuelstancl.me/docs/v2/features/timestamps/ // Stancl\Tenancy\Features\TenantConfig::class, // https://tenancy.samuelstancl.me/docs/v2/features/tenant-config/ // Stancl\Tenancy\Features\TelescopeTags::class, // https://tenancy.samuelstancl.me/docs/v2/telescope/ @@ -211,32 +204,15 @@ return [ /** * The URL to which users will be redirected when they try to acceess a central route on a tenant domain. */ - 'home_url' => '/app', - - /** - * Should tenant databases be created asynchronously in a queued job. - */ - 'queue_database_creation' => false, // todo make this a static property + 'home_url' => '/app', // todo move this to static 'migration_parameters' => [ '--force' => true, // Set this to true to be able to run migrations in production - // '--path' => [database_path('migrations/tenant')], // If you need to customize paths to tenant migrations + '--path' => [database_path('migrations/tenant')], ], 'seeder_parameters' => [ '--class' => 'DatabaseSeeder', // root seeder class, e.g.: 'DatabaseSeeder' // '--force' => true, ], - - /** - * Should tenant databases be deleted asynchronously in a queued job. - */ - 'queue_database_deletion' => false, - - /** - * Middleware pushed to the global middleware stack. - */ - 'global_middleware' => [ // todo get rid of this - // Stancl\Tenancy\Middleware\InitializeTenancy::class, - ], ]; diff --git a/src/Commands/CreateTenant.php b/src/Commands/CreateTenant.php deleted file mode 100644 index 1c97da07..00000000 --- a/src/Commands/CreateTenant.php +++ /dev/null @@ -1,47 +0,0 @@ -withDomains($this->getDomains()) - ->withData($this->getData()) - ->save(); - - $this->info($tenant->id); - } - - public function getDomains(): array - { - return $this->option('domain'); - } - - public function getData(): array - { - return array_reduce($this->argument('data'), function ($data, $pair) { - [$key, $value] = explode('=', $pair, 2); - $data[$key] = $value; - - return $data; - }, []); - } -} diff --git a/src/Commands/Install.php b/src/Commands/Install.php index 224d1ded..c108ce31 100644 --- a/src/Commands/Install.php +++ b/src/Commands/Install.php @@ -36,17 +36,6 @@ class Install extends Command ]); $this->info('✔️ Created config/tenancy.php'); - $newKernel = $this->setMiddlewarePriority(); - - $newKernel = str_replace("'web' => [", "'web' => [ - \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel); - - $newKernel = str_replace("'api' => [", "'api' => [ - \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel); - - file_put_contents(app_path('Http/Kernel.php'), $newKernel); - $this->info('✔️ Set middleware priority'); - if (! file_exists(base_path('routes/tenant.php'))) { file_put_contents(base_path('routes/tenant.php'), file_get_contents(__DIR__ . '/../../assets/tenant_routes.php.stub')); $this->info('✔️ Created routes/tenant.php'); @@ -54,15 +43,13 @@ class Install extends Command $this->info('Found routes/tenant.php.'); } - $this->line(''); - $this->line('This package lets you store data about tenants either in Redis or in a relational database like MySQL. To store data about tenants in a relational database, you need a few database tables.'); - if ($this->confirm('Do you wish to publish the migrations that create these tables?', true)) { - $this->callSilent('vendor:publish', [ - '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', - '--tag' => 'migrations', - ]); - $this->info('✔️ Created migrations. Remember to run [php artisan migrate]!'); - } + // todo tenancy SP stub + + $this->callSilent('vendor:publish', [ + '--provider' => 'Stancl\Tenancy\TenancyServiceProvider', + '--tag' => 'migrations', + ]); + $this->info('✔️ Created migrations. Remember to run [php artisan migrate]!'); if (! is_dir(database_path('migrations/tenant'))) { mkdir(database_path('migrations/tenant')); @@ -71,34 +58,4 @@ class Install extends Command $this->comment('✨️ stancl/tenancy installed successfully.'); } - - protected function setMiddlewarePriority(): string - { - if (app()->version()[0] === '6') { - return str_replace( - 'protected $middlewarePriority = [', - "protected \$middlewarePriority = [ - \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class, - \Stancl\Tenancy\Middleware\InitializeTenancy::class,", - file_get_contents(app_path('Http/Kernel.php')) - ); - } else { - return str_replace( - "];\n}", - "];\n\n protected \$middlewarePriority = [ - \Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class, - \Stancl\Tenancy\Middleware\InitializeTenancy::class, - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class, - \Illuminate\Routing\Middleware\ThrottleRequests::class, - \Illuminate\Session\Middleware\AuthenticateSession::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \Illuminate\Auth\Middleware\Authorize::class, - ]; -}", - file_get_contents(app_path('Http/Kernel.php')) - ); - } - } } diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 18e082fe..bbe3dc22 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -57,20 +57,13 @@ class Migrate extends MigrateCommand return; } - tenancy() - ->query() - ->when($this->option('tenants'), function ($query) { - $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); - }) - ->each(function (TenantWithDatabase $tenant) { - $this->line("Tenant: {$tenant['id']}"); + tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { + $this->line("Tenant: {$tenant['id']}"); + + // Migrate + parent::handle(); - $tenant->run(function () { - // Migrate - parent::handle(); - }); - - event(new DatabaseMigrated($tenant)); - }); + event(new DatabaseMigrated($tenant)); + }); } } diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index eb4b0f16..dec6c415 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\Traits\DealsWithMigrations; use Stancl\Tenancy\Traits\HasATenantsOption; @@ -33,22 +34,18 @@ final class MigrateFresh extends Command */ public function handle() { - tenancy()->all($this->option('tenants'))->each(function ($tenant) { - $this->line("Tenant: {$tenant->id}"); + tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { + $this->info('Dropping tables.'); + $this->call('db:wipe', array_filter([ + '--database' => 'tenant', + '--force' => true, + ])); - $tenant->run(function ($tenant) { - $this->info('Dropping tables.'); - $this->call('db:wipe', array_filter([ - '--database' => 'tenant', - '--force' => true, - ])); - - $this->info('Migrating.'); - $this->callSilent('tenants:migrate', [ - '--tenants' => [$tenant->id], - '--force' => true, - ]); - }); + $this->info('Migrating.'); + $this->callSilent('tenants:migrate', [ + '--tenants' => [$tenant->id], + '--force' => true, + ]); }); $this->info('Done.'); diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php index 0a85a10e..c614fa4a 100644 --- a/src/Commands/Rollback.php +++ b/src/Commands/Rollback.php @@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Migrations\Migrator; +use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\DatabaseManager; use Stancl\Tenancy\Traits\DealsWithMigrations; use Stancl\Tenancy\Traits\HasATenantsOption; @@ -55,13 +56,13 @@ class Rollback extends RollbackCommand return; } - tenancy()->all($this->option('tenants'))->each(function ($tenant) { + tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { $this->line("Tenant: {$tenant['id']}"); - $tenant->run(function () { - // Rollback - parent::handle(); - }); + // Rollback + parent::handle(); + + // todo DatabaseRolledBack event }); } } diff --git a/src/Commands/Run.php b/src/Commands/Run.php index 874b1f86..dc3da651 100644 --- a/src/Commands/Run.php +++ b/src/Commands/Run.php @@ -32,8 +32,7 @@ class Run extends Command */ public function handle() { - $originalTenant = tenancy()->getTenant(); - tenancy()->all($this->option('tenants'))->each(function ($tenant) { + tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { $this->line("Tenant: {$tenant['id']}"); tenancy()->initialize($tenant); @@ -54,12 +53,6 @@ class Run extends Command // Run command $this->call($this->argument('commandname'), array_merge($arguments, $options)); - - tenancy()->endTenancy(); }); - - if ($originalTenant) { - tenancy()->initialize($originalTenant); - } } } diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index a31d0045..81d61d6f 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Commands; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\Seeds\SeedCommand; +use Stancl\Tenancy\Contracts\TenantWithDatabase; use Stancl\Tenancy\DatabaseManager; use Stancl\Tenancy\Events\DatabaseSeeded; use Stancl\Tenancy\Traits\HasATenantsOption; @@ -54,13 +55,11 @@ class Seed extends SeedCommand return; } - tenancy()->all($this->option('tenants'))->each(function ($tenant) { + tenancy()->runForMultiple($this->option('tenants'), function ($tenant) { $this->line("Tenant: {$tenant['id']}"); - $tenant->run(function () { - // Seed - parent::handle(); - }); + // Seed + parent::handle(); event(new DatabaseSeeded($tenant)); }); diff --git a/src/Commands/TenantList.php b/src/Commands/TenantList.php index 3d57df22..f706a07b 100644 --- a/src/Commands/TenantList.php +++ b/src/Commands/TenantList.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Console\Command; +use Stancl\Tenancy\Contracts\Tenant; class TenantList extends Command { @@ -30,8 +31,14 @@ class TenantList extends Command public function handle() { $this->info('Listing all tenants.'); - tenancy()->all()->each(function ($tenant) { - $this->line("[Tenant] id: {$tenant['id']} @ " . implode('; ', $tenant->domains)); + tenancy() + ->query() + ->when($this->option('tenants'), function ($query) { + $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); + }) + ->cursor() + ->each(function (Tenant $tenant) { + $this->line("[Tenant] id: {$tenant['id']} @ " . implode('; ', $tenant->domains ?? [])); }); } } diff --git a/src/Contracts/Feature.php b/src/Contracts/Feature.php index 0d51c4aa..02c3f931 100644 --- a/src/Contracts/Feature.php +++ b/src/Contracts/Feature.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Stancl\Tenancy\Contracts; -use Stancl\Tenancy\TenantManager; +use Stancl\Tenancy\Facades\Tenancy; /** Additional features, like Telescope tags and tenant redirects. */ interface Feature { - public function bootstrap(TenantManager $tenantManager): void; + public function bootstrap(Tenancy $tenancy): void; } diff --git a/src/Contracts/ModifiesDatabaseNameForConnection.php b/src/Contracts/ModifiesDatabaseNameForConnection.php deleted file mode 100644 index 713aa3be..00000000 --- a/src/Contracts/ModifiesDatabaseNameForConnection.php +++ /dev/null @@ -1,13 +0,0 @@ -middleware('tenancy'); + $this->middleware(static::$tenancyMiddleware); } public function asset($path) diff --git a/src/Database/Models/Concerns/HasADataColumn.php b/src/Database/Models/Concerns/HasADataColumn.php index 19f6c53c..a7b3eb75 100644 --- a/src/Database/Models/Concerns/HasADataColumn.php +++ b/src/Database/Models/Concerns/HasADataColumn.php @@ -6,9 +6,24 @@ namespace Stancl\Tenancy\Database\Models\Concerns; // todo rename trait HasADataColumn { + public static $priorityListeners = []; + + /** + * We need this property, because both created & saved event listeners + * decode the data (to take precedence before other created & saved) + * listeners, but we don't want the dadta to be decoded twice. + * + * @var string + */ + public $dataEncodingStatus = 'decoded'; // todo write tests for this + public static function bootHasADataColumn() { $encode = function (self $model) { + if ($model->dataEncodingStatus === 'encoded') { + return; + } + foreach ($model->getAttributes() as $key => $value) { if (! in_array($key, static::getCustomColums())) { $current = $model->getAttribute(static::getDataColumn()) ?? []; @@ -20,19 +35,64 @@ trait HasADataColumn unset($model->attributes[$key]); } } + + $model->dataEncodingStatus = 'encoded'; }; $decode = function (self $model) { + if ($model->dataEncodingStatus === 'decoded') { + return; + } + foreach ($model->getAttribute(static::getDataColumn()) ?? [] as $key => $value) { $model->setAttribute($key, $value); } $model->setAttribute(static::getDataColumn(), null); + + $model->dataEncodingStatus = 'decoded'; }; - static::saving($encode); - static::saved($decode); - static::retrieved($decode); + static::registerPriorityListener('retrieved', $decode); + + static::registerPriorityListener('saving', $encode); + static::registerPriorityListener('creating', $encode); + static::registerPriorityListener('updating', $encode); + static::registerPriorityListener('saved', $decode); + static::registerPriorityListener('created', $decode); + static::registerPriorityListener('updated', $decode); + } + + protected function fireModelEvent($event, $halt = true) + { + $this->runPriorityListeners($event, $halt); + + return parent::fireModelEvent($event, $halt); + } + + public function runPriorityListeners($event, $halt = true) + { + $listeners = static::$priorityListeners[$event] ?? []; + + if (! $event) { + return; + } + + foreach ($listeners as $listener) { + if (is_string($listener)) { + $listener = app($listener); + $handle = [$listener, 'handle']; + } else { + $handle = $listener; + } + + $handle($this); + } + } + + public static function registerPriorityListener(string $event, callable $callback) + { + static::$priorityListeners[$event][] = $callback; } public function getCasts() diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php index 1aa70002..4792b30d 100644 --- a/src/Database/Models/Tenant.php +++ b/src/Database/Models/Tenant.php @@ -10,7 +10,7 @@ use Stancl\Tenancy\Contracts; // todo @property class Tenant extends Model implements Contracts\TenantWithDatabase { - use Concerns\CentralConnection, Concerns\HasADataColumn, Concerns\GeneratesIds, Concerns\HasADataColumn { + use Concerns\CentralConnection, Concerns\HasADataColumn, Concerns\GeneratesIds, Concerns\HasADataColumn { Concerns\HasADataColumn::getCasts as dataColumnCasts; } @@ -41,14 +41,11 @@ class Tenant extends Model implements Contracts\TenantWithDatabase public static function internalPrefix(): string { - return config('tenancy.internal_column_prefix'); + return config('tenancy.internal_prefix'); } /** * Get an internal key. - * - * @param string $key - * @return mixed */ public function getInternal(string $key) { @@ -57,14 +54,10 @@ class Tenant extends Model implements Contracts\TenantWithDatabase /** * Set internal key. - * - * @param string $key - * @param mixed $value - * @return $this */ public function setInternal(string $key, $value) { - $this->setAttribute($key, $value); + $this->setAttribute(static::internalPrefix() . $key, $value); return $this; } diff --git a/src/DatabaseConfig.php b/src/DatabaseConfig.php index bc5af906..ea581b2f 100644 --- a/src/DatabaseConfig.php +++ b/src/DatabaseConfig.php @@ -102,19 +102,11 @@ class DatabaseConfig public function connection(): array { $template = $this->getTemplateConnectionName(); - $templateConnection = config("database.connections.{$template}"); - // todo move a lot of this logic to the tenant DB manager so that we dont have to deal with the separators & modifying DB names here - $databaseName = $this->getName(); - if (($manager = $this->manager()) instanceof ModifiesDatabaseNameForConnection) { - /** @var ModifiesDatabaseNameForConnection $manager */ - $databaseName = $manager->getDatabaseNameForConnection($databaseName); - } - - return array_merge($templateConnection, $this->tenantConfig(), [ - $this->manager()->getSeparator() => $databaseName, - ]); + return $this->manager()->makeConnectionConfig( + array_merge($templateConnection, $this->tenantConfig()), $this->getName() + ); } /** diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index a76a53fa..795ab107 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -83,84 +83,10 @@ class DatabaseManager * @throws DatabaseManagerNotRegisteredException * @throws TenantDatabaseAlreadyExistsException */ - public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void + public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void // todo do we need this? { if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) { throw new TenantDatabaseAlreadyExistsException($database); } } - - /** - * Create a database for a tenant. - * - * @param Tenant $tenant - * @param ShouldQueue[]|callable[] $afterCreating - * @return void - * @throws DatabaseManagerNotRegisteredException - */ - public function createDatabase(TenantWithDatabase $tenant, array $afterCreating = []) - { - // todo get rid of aftercreating logic - $afterCreating = array_merge( - $afterCreating, - $this->tenancy->event('database.creating', $tenant->database()->getName(), $tenant) - ); - - if ($this->app['config']['tenancy.queue_database_creation'] ?? false) { - $this->createDatabaseAsynchronously($tenant, $afterCreating); - } else { - $this->createDatabaseSynchronously($tenant, $afterCreating); - } - - $this->tenancy->event('database.created', $tenant->database()->getName(), $tenant); - } - - protected function createDatabaseAsynchronously(Tenant $tenant, array $afterCreating) - { - $chain = []; - foreach ($afterCreating as $item) { - if (is_string($item) && class_exists($item)) { - $chain[] = new $item($tenant); // Classes are instantiated and given $tenant - } elseif ($item instanceof ShouldQueue) { - $chain[] = $item; - } - } - - QueuedTenantDatabaseCreator::withChain($chain)->dispatch($tenant->database()->manager(), $tenant); - } - - protected function createDatabaseSynchronously(Tenant $tenant, array $afterCreating) - { - $manager = $tenant->database()->manager(); - $manager->createDatabase($tenant); - - foreach ($afterCreating as $item) { - if (is_object($item) && ! $item instanceof Closure) { - $item->handle($tenant); - } else { - $item($tenant); - } - } - } - - /** - * Delete a tenant's database. - * - * @throws DatabaseManagerNotRegisteredException - */ - public function deleteDatabase(TenantWithDatabase $tenant) - { - $database = $tenant->database()->getName(); - $manager = $tenant->database()->manager(); - - $this->tenancy->event('database.deleting', $database, $tenant); - - if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) { - QueuedTenantDatabaseDeleter::dispatch($manager, $tenant); - } else { - $manager->deleteDatabase($tenant); - } - - $this->tenancy->event('database.deleted', $database, $tenant); - } } diff --git a/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php b/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php new file mode 100644 index 00000000..685c6ef0 --- /dev/null +++ b/src/Exceptions/TenantCouldNotBeIdentifiedByRequestDataException.php @@ -0,0 +1,27 @@ +setSolutionDescription('Did you forget to create a tenant with this id?') + ->setDocumentationLinks([ + 'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v2/creating-tenants/', // todo update link for v3 + ]); + } +} diff --git a/src/Features/CrossDomainRedirect.php b/src/Features/CrossDomainRedirect.php index 8eecaeee..6ebd15fc 100644 --- a/src/Features/CrossDomainRedirect.php +++ b/src/Features/CrossDomainRedirect.php @@ -6,13 +6,15 @@ namespace Stancl\Tenancy\Features; use Illuminate\Http\RedirectResponse; use Stancl\Tenancy\Contracts\Feature; -use Stancl\Tenancy\TenantManager; +use Stancl\Tenancy\Tenancy; class CrossDomainRedirect implements Feature { - public function bootstrap(TenantManager $tenantManager): void + public function bootstrap(Tenancy $tenancy): void { RedirectResponse::macro('domain', function (string $domain) { + /** @var RedirectResponse $this */ + // replace first occurance of hostname fragment with $domain $url = $this->getTargetUrl(); $hostname = parse_url($url, PHP_URL_HOST); diff --git a/src/Features/TelescopeTags.php b/src/Features/TelescopeTags.php index 7978214b..cd87160d 100644 --- a/src/Features/TelescopeTags.php +++ b/src/Features/TelescopeTags.php @@ -8,8 +8,9 @@ use Laravel\Telescope\IncomingEntry; use Laravel\Telescope\Telescope; use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains; -use Stancl\Tenancy\TenantManager; +use Stancl\Tenancy\Tenancy; +// todo rewrite this class TelescopeTags implements Feature { /** @var callable User-specific callback that returns tags. */ @@ -22,7 +23,7 @@ class TelescopeTags implements Feature }; } - public function bootstrap(TenantManager $tenantManager): void + public function bootstrap(Tenancy $tenancy): void { if (! class_exists(Telescope::class)) { return; @@ -35,6 +36,7 @@ class TelescopeTags implements Feature return $tags; } + // todo lines below $tenantRoute = PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'tenancy') || PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'universal'); diff --git a/src/Features/TenantConfig.php b/src/Features/TenantConfig.php index a8f45150..f181bb6c 100644 --- a/src/Features/TenantConfig.php +++ b/src/Features/TenantConfig.php @@ -6,9 +6,11 @@ namespace Stancl\Tenancy\Features; use Illuminate\Contracts\Config\Repository; use Stancl\Tenancy\Contracts\Feature; +use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tenant; use Stancl\Tenancy\TenantManager; +// todo rewrite this class TenantConfig implements Feature { /** @var Repository */ @@ -30,7 +32,7 @@ class TenantConfig implements Feature } } - public function bootstrap(TenantManager $tenantManager): void + public function bootstrap(Tenancy $tenancy): void { $tenantManager->eventListener('bootstrapped', function (TenantManager $manager) { $this->setTenantConfig($manager->getTenant()); diff --git a/src/Features/Timestamps.php b/src/Features/Timestamps.php index 63dc9c85..b40542ef 100644 --- a/src/Features/Timestamps.php +++ b/src/Features/Timestamps.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Tenant; use Stancl\Tenancy\TenantManager; +// todo rewrite this class Timestamps implements Feature { /** @var Repository */ diff --git a/src/Jobs/CreateDatabase.php b/src/Jobs/CreateDatabase.php index 5cad079c..68d9cc09 100644 --- a/src/Jobs/CreateDatabase.php +++ b/src/Jobs/CreateDatabase.php @@ -27,7 +27,8 @@ class CreateDatabase implements ShouldQueue public function handle() { - if ($this->tenant->getAttribute('_tenancy_create_database') !== false) { + if ($this->tenant->getInternal('create_database') !== false) { + $this->tenant->database()->makeCredentials(); $this->tenant->database()->manager()->createDatabase($this->tenant); } } diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php index 33e17a09..dad2c5ef 100644 --- a/src/Middleware/InitializeTenancyByRequestData.php +++ b/src/Middleware/InitializeTenancyByRequestData.php @@ -6,26 +6,28 @@ namespace Stancl\Tenancy\Middleware; use Closure; use Illuminate\Http\Request; -use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; +use Stancl\Tenancy\Resolvers\RequestDataTenantResolver; +use Stancl\Tenancy\Tenancy; -class InitializeTenancyByRequestData +// todo write tests for this +class InitializeTenancyByRequestData extends IdentificationMiddleware { /** @var string|null */ - protected $header; + public static $header = 'X-Tenant'; /** @var string|null */ - protected $queryParameter; + public static $queryParameter = 'tenant'; - /** @var callable */ - protected $onFail; + /** @var Tenancy */ + protected $tenancy; - public function __construct(?string $header = 'X-Tenant', ?string $queryParameter = 'tenant', callable $onFail = null) + /** @var TenantResolver */ + protected $resolver; + + public function __construct(Tenancy $tenancy, RequestDataTenantResolver $resolver) { - $this->header = $header; - $this->queryParameter = $queryParameter; - $this->onFail = $onFail ?? function ($e) { - throw $e; - }; + $this->tenancy = $tenancy; + $this->resolver = $resolver; } /** @@ -38,33 +40,21 @@ class InitializeTenancyByRequestData public function handle($request, Closure $next) { if ($request->method() !== 'OPTIONS') { - try { - $this->initializeTenancy($request); - } catch (TenantCouldNotBeIdentifiedException $e) { - return ($this->onFail)($e, $request, $next); - } + return $this->initializeTenancy($request, $next, $this->getPayload($request)); } return $next($request); } - protected function initializeTenancy(Request $request) + protected function getPayload(Request $request): ?string { - if (tenancy()->initialized) { - return; - } - $tenant = null; - if ($this->header && $request->hasHeader($this->header)) { - $tenant = $request->header($this->header); - } elseif ($this->queryParameter && $request->has($this->queryParameter)) { - $tenant = $request->get($this->queryParameter); + if (static::$header && $request->hasHeader(static::$header)) { + $tenant = $request->header(static::$header); + } elseif (static::$queryParameter && $request->has(static::$queryParameter)) { + $tenant = $request->get(static::$queryParameter); } - if (! $tenant) { - throw new TenantCouldNotBeIdentifiedException($request->getHost()); - } - - tenancy()->initialize(tenancy()->find($tenant)); + return $tenant; } } diff --git a/src/Resolvers/PathTenantResolver.php b/src/Resolvers/PathTenantResolver.php index 4a7c4187..da9d840f 100644 --- a/src/Resolvers/PathTenantResolver.php +++ b/src/Resolvers/PathTenantResolver.php @@ -19,7 +19,7 @@ class PathTenantResolver implements TenantResolver if ($id = $route->parameter(static::$tenantParameterName)) { $route->forgetParameter(static::$tenantParameterName); - if ($tenant = config('tenancy.tenant_model')::find($id)) { + if ($tenant = tenancy()->find($id)) { return $tenant; } } diff --git a/src/Resolvers/RequestDataTenantResolver.php b/src/Resolvers/RequestDataTenantResolver.php new file mode 100644 index 00000000..d9befd5c --- /dev/null +++ b/src/Resolvers/RequestDataTenantResolver.php @@ -0,0 +1,21 @@ +find($payload)) { + return $tenant; + } + + throw new TenantCouldNotBeIdentifiedByRequestDataException($payload); + } +} diff --git a/src/StorageDrivers/Database/CachedTenantResolver.php b/src/StorageDrivers/Database/CachedTenantResolver.php deleted file mode 100644 index db4d5a46..00000000 --- a/src/StorageDrivers/Database/CachedTenantResolver.php +++ /dev/null @@ -1,68 +0,0 @@ -cache = $cacheManager->store($config->get('tenancy.storage_drivers.db.cache_store')); - $this->config = $config; - } - - protected function ttl(): int - { - return $this->config->get('tenancy.storage_drivers.db.cache_ttl'); - } - - public function getTenantIdByDomain(string $domain, Closure $query): ?string - { - return $this->cache->remember('_tenancy_domain_to_id:' . $domain, $this->ttl(), $query); - } - - public function getDataById(string $id, Closure $dataQuery): ?array - { - return $this->cache->remember('_tenancy_id_to_data:' . $id, $this->ttl(), $dataQuery); - } - - public function getDomainsById(string $id, Closure $domainsQuery): ?array - { - return $this->cache->remember('_tenancy_id_to_domains:' . $id, $this->ttl(), $domainsQuery); - } - - public function invalidateTenant(string $id): void - { - $this->invalidateTenantData($id); - $this->invalidateTenantDomains($id); - } - - public function invalidateTenantData(string $id): void - { - $this->cache->forget('_tenancy_id_to_data:' . $id); - } - - public function invalidateTenantDomains(string $id): void - { - $this->cache->forget('_tenancy_id_to_domains:' . $id); - } - - public function invalidateDomainToIdMapping(array $domains): void - { - foreach ($domains as $domain) { - $this->cache->forget('_tenancy_domain_to_id:' . $domain); - } - } -} diff --git a/src/StorageDrivers/Database/CentralConnection.php b/src/StorageDrivers/Database/CentralConnection.php deleted file mode 100644 index 3c697783..00000000 --- a/src/StorageDrivers/Database/CentralConnection.php +++ /dev/null @@ -1,13 +0,0 @@ -app = $app; - $this->cache = $cache; - $this->centralDatabase = $this->getCentralConnection(); - $this->tenants = new TenantRepository($config); - $this->domains = new DomainRepository($config); - } - - /** - * Get the central database connection. - * - * @return Connection - */ - public static function getCentralConnection(): Connection - { - return DB::connection(static::getCentralConnectionName()); - } - - public static function getCentralConnectionName(): string - { - return config('tenancy.storage_drivers.db.connection') ?? DatabaseManager::$originalDefaultConnectionName; - } - - public function findByDomain(string $domain): Tenant - { - $query = function () use ($domain) { - return $this->domains->getTenantIdByDomain($domain); - }; - - if ($this->usesCache()) { - $id = $this->cache->getTenantIdByDomain($domain, $query); - } else { - $id = $query(); - } - - if (! $id) { - throw new TenantCouldNotBeIdentifiedException($domain); - } - - return $this->findById($id); - } - - public function findById(string $id): Tenant - { - $dataQuery = function () use ($id) { - $data = $this->tenants->find($id); - - return $data ? $this->tenants->decodeData($data) : null; - }; - $domainsQuery = function () use ($id) { - return $this->domains->getTenantDomains($id); - }; - - if ($this->usesCache()) { - $data = $this->cache->getDataById($id, $dataQuery); - $domains = $this->cache->getDomainsById($id, $domainsQuery); - } else { - $data = $dataQuery(); - $domains = $domainsQuery(); - } - - if (! $data) { - throw new TenantDoesNotExistException($id); - } - - return Tenant::fromStorage($data) - ->withDomains($domains); - } - - /** - * Find a tenant using an arbitrary key. - * - * @param string $key - * @param mixed $value - * @return Tenant - * @throws TenantDoesNotExistException - */ - public function findBy(string $key, $value): Tenant - { - $tenant = $this->tenants->findBy($key, $value); - - if (! $tenant) { - throw new TenantDoesNotExistException($value, $key); - } - - return Tenant::fromStorage($this->tenants->decodeData($tenant)) - ->withDomains($this->domains->getTenantDomains($tenant['id'])); - } - - public function ensureTenantCanBeCreated(Tenant $tenant): void - { - if ($this->tenants->exists($tenant)) { - throw new TenantWithThisIdAlreadyExistsException($tenant->id); - } - - if ($this->domains->occupied($tenant->domains)) { - throw new DomainsOccupiedByOtherTenantException; - } - } - - public function withDefaultTenant(Tenant $tenant): self - { - $this->tenant = $tenant; - - return $this; - } - - public function createTenant(Tenant $tenant): void - { - $this->centralDatabase->transaction(function () use ($tenant) { - $this->tenants->insert($tenant); - $this->domains->insertTenantDomains($tenant); - }); - } - - public function updateTenant(Tenant $tenant): void - { - $originalDomains = $this->domains->getTenantDomains($tenant); - - $this->centralDatabase->transaction(function () use ($tenant, $originalDomains) { - $this->tenants->updateTenant($tenant); - - $this->domains->updateTenantDomains($tenant, $originalDomains); - }); - - if ($this->usesCache()) { - $this->cache->invalidateTenant($tenant->id); - $this->cache->invalidateDomainToIdMapping($originalDomains); - } - } - - public function deleteTenant(Tenant $tenant): void - { - $originalDomains = $this->domains->getTenantDomains($tenant); - - $this->centralDatabase->transaction(function () use ($tenant) { - $this->tenants->where('id', $tenant->id)->delete(); - $this->domains->where('tenant_id', $tenant->id)->delete(); - }); - - if ($this->usesCache()) { - $this->cache->invalidateTenant($tenant->id); - $this->cache->invalidateDomainToIdMapping($originalDomains); - } - } - - /** - * Get all tenants. - * - * @param string[] $ids - * @return Tenant[] - */ - public function all(array $ids = []): array - { - return $this->tenants->all($ids)->map(function ($data) { - return Tenant::fromStorage($data) - ->withDomains($this->domains->getTenantDomains($data['id'])); - })->toArray(); - } - - /** - * Get the current tenant. - * - * @return Tenant - */ - protected function currentTenant() - { - return $this->tenant ?? $this->app[Tenant::class]; - } - - public function get(string $key, Tenant $tenant = null) - { - return $this->tenants->get($key, $tenant ?? $this->currentTenant()); - } - - public function getMany(array $keys, Tenant $tenant = null): array - { - return $this->tenants->getMany($keys, $tenant ?? $this->currentTenant()); - } - - public function put(string $key, $value, Tenant $tenant = null): void - { - $tenant = $tenant ?? $this->currentTenant(); - $this->tenants->put($key, $value, $tenant); - - if ($this->usesCache()) { - $this->cache->invalidateTenantData($tenant->id); - } - } - - public function putMany(array $kvPairs, Tenant $tenant = null): void - { - $tenant = $tenant ?? $this->currentTenant(); - $this->tenants->putMany($kvPairs, $tenant); - - if ($this->usesCache()) { - $this->cache->invalidateTenantData($tenant->id); - } - } - - public function deleteMany(array $keys, Tenant $tenant = null): void - { - $tenant = $tenant ?? $this->currentTenant(); - $this->tenants->deleteMany($keys, $tenant); - - if ($this->usesCache()) { - $this->cache->invalidateTenantData($tenant->id); - } - } - - public function usesCache(): bool - { - return $this->app['config']['tenancy.storage_drivers.db.cache_store'] !== null; - } -} diff --git a/src/StorageDrivers/Database/DomainRepository.php b/src/StorageDrivers/Database/DomainRepository.php deleted file mode 100644 index 3e77396f..00000000 --- a/src/StorageDrivers/Database/DomainRepository.php +++ /dev/null @@ -1,55 +0,0 @@ -where('domain', $domain)->first()->tenant_id ?? null; - } - - public function occupied(array $domains): bool - { - return $this->whereIn('domain', $domains)->exists(); - } - - public function getTenantDomains($tenant) - { - $id = $tenant instanceof Tenant ? $tenant->id : $tenant; - - return $this->where('tenant_id', $id)->get('domain')->pluck('domain')->all(); - } - - public function insertTenantDomains(Tenant $tenant) - { - $this->insert(array_map(function ($domain) use ($tenant) { - return ['domain' => $domain, 'tenant_id' => $tenant->id]; - }, $tenant->domains)); - } - - public function updateTenantDomains(Tenant $tenant, array $originalDomains) - { - $deletedDomains = array_diff($originalDomains, $tenant->domains); - $newDomains = array_diff($tenant->domains, $originalDomains); - - $this->whereIn('domain', $deletedDomains)->delete(); - - foreach ($newDomains as $domain) { - $this->insert([ - 'tenant_id' => $tenant->id, - 'domain' => $domain, - ]); - } - } - - public function getTable(ConfigRepository $config) - { - return $config->get('tenancy.storage_drivers.db.table_names.domains') ?? 'domains'; - } -} diff --git a/src/StorageDrivers/Database/Repository.php b/src/StorageDrivers/Database/Repository.php deleted file mode 100644 index e3cce981..00000000 --- a/src/StorageDrivers/Database/Repository.php +++ /dev/null @@ -1,41 +0,0 @@ -database = DatabaseStorageDriver::getCentralConnection(); - $this->tableName = $this->getTable($config); - $this->table = $this->database->table($this->tableName); - } - - public function table() - { - return $this->table->newQuery()->from($this->tableName); - } - - abstract public function getTable(ConfigRepository $config); - - public function __call($method, $parameters) - { - return $this->table()->$method(...$parameters); - } -} diff --git a/src/StorageDrivers/Database/TenantRepository.php b/src/StorageDrivers/Database/TenantRepository.php deleted file mode 100644 index adaf054a..00000000 --- a/src/StorageDrivers/Database/TenantRepository.php +++ /dev/null @@ -1,186 +0,0 @@ -whereIn('id', $ids)->get(); - } else { - $data = $this->table()->get(); - } - - return $data->map(function (stdClass $obj) { - return $this->decodeData((array) $obj); - }); - } - - public function find($tenant) - { - return (array) $this->table()->find( - $tenant instanceof Tenant ? $tenant->id : $tenant - ); - } - - public function findBy(string $key, $value) - { - if (in_array($key, static::customColumns())) { - return (array) $this->table()->where($key, $value)->first(); - } - - return (array) $this->table()->where( - static::dataColumn() . '->' . $key, - $value - )->first(); - } - - public function updateTenant(Tenant $tenant) - { - $this->putMany($tenant->data, $tenant); - } - - public function exists(Tenant $tenant) - { - return $this->where('id', $tenant->id)->exists(); - } - - public function get(string $key, Tenant $tenant) - { - return $this->decodeFreshDataForTenant($tenant)[$key] ?? null; - } - - public function getMany(array $keys, Tenant $tenant) - { - $decodedData = $this->decodeFreshDataForTenant($tenant); - - $result = []; - - foreach ($keys as $key) { - $result[$key] = $decodedData[$key] ?? null; - } - - return $result; - } - - public function put(string $key, $value, Tenant $tenant) - { - $record = $this->where('id', $tenant->id); - - if (in_array($key, static::customColumns())) { - $record->update([$key => $value]); - } else { - $data = json_decode($record->first()->{static::dataColumn()}, true); - $data[$key] = $value; - - $record->update([static::dataColumn() => $data]); - } - } - - public function putMany(array $kvPairs, Tenant $tenant) - { - $record = $this->where('id', $tenant->id); - - $data = []; - $jsonData = json_decode($record->first()->{static::dataColumn()}, true); - foreach ($kvPairs as $key => $value) { - if (in_array($key, static::customColumns())) { - $data[$key] = $value; - continue; - } else { - $jsonData[$key] = $value; - } - } - - $data[static::dataColumn()] = json_encode($jsonData); - - $record->update($data); - } - - public function deleteMany(array $keys, Tenant $tenant) - { - $record = $this->where('id', $tenant->id); - - $data = []; - $jsonData = json_decode($record->first(static::dataColumn())->data, true); - foreach ($keys as $key) { - if (in_array($key, static::customColumns())) { - $data[$key] = null; - - continue; - } else { - unset($jsonData[$key]); - } - } - - $data[static::dataColumn()] = json_encode($jsonData); - - $record->update($data); - } - - public function decodeFreshDataForTenant(Tenant $tenant): array - { - return $this->decodeData( - (array) $this->table()->where('id', $tenant->id)->first() - ); - } - - public static function decodeData(array $columns): array - { - $dataColumn = static::dataColumn(); - $decoded = json_decode($columns[$dataColumn], true); - $columns = array_merge($columns, $decoded); - - // If $columns[$dataColumn] has been overriden by a value, don't delete the key. - if (! array_key_exists($dataColumn, $decoded)) { - unset($columns[$dataColumn]); - } - - return $columns; - } - - public function insert(Tenant $tenant) - { - $this->table()->insert(array_merge( - $this->encodeData($tenant->data), - ['id' => $tenant->id] - )); - } - - public static function encodeData(array $data): array - { - $result = []; - foreach (array_intersect(static::customColumns(), array_keys($data)) as $customColumn) { - $result[$customColumn] = $data[$customColumn]; - unset($data[$customColumn]); - } - - $result = array_merge($result, [static::dataColumn() => json_encode($data)]); - - return $result; - } - - public static function customColumns(): array - { - return config('tenancy.storage_drivers.db.custom_columns', []); - } - - public static function dataColumn(): string - { - return config('tenancy.storage_drivers.db.data_column', 'data'); - } - - public function getTable(ConfigRepository $config) - { - return $config->get('tenancy.storage_drivers.db.table_names.TenantModel') // legacy - ?? $config->get('tenancy.storage_drivers.db.table_names.tenants') - ?? 'tenants'; - } -} diff --git a/src/Tenancy.php b/src/Tenancy.php index 63730ec1..2ef8dac8 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -77,15 +77,25 @@ class Tenancy * Run a callback for multiple tenants. * More performant than running $tenant->run() one by one. * - * @param Tenant[]|\Illuminate\Support\Collection $tenants + * @param Tenant[]|\Traversable|string[]|null $tenants * @param callable $callback * @return void */ public function runForMultiple($tenants, callable $callback) { + // Wrap string in array + $tenants = is_string($tenants) ? [$tenants] : $tenants; + + // Use all tenants if $tenants is falsy + $tenants = $tenants ?: $this->model()->cursor(); + $originalTenant = $this->tenant; foreach ($tenants as $tenant) { + if (is_string($tenant)) { + $tenant = $this->find($tenant); + } + $this->initialize($tenant); $callback($tenant); } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index 0eb2627f..450493c0 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -24,6 +24,13 @@ class TenancyServiceProvider extends ServiceProvider $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.id_generator']); $this->app->singleton(DatabaseManager::class); $this->app->singleton(Tenancy::class); + $this->app->extend(Tenancy::class, function (Tenancy $tenancy) { + foreach ($this->app['config']['tenancy.features'] as $feature) { + $this->app[$feature]->bootstrap($tenancy); + } + + return $tenancy; + }); $this->app->bind(Tenant::class, function ($app) { return $app[Tenancy::class]->tenant; }); @@ -63,7 +70,6 @@ class TenancyServiceProvider extends ServiceProvider Commands\Migrate::class, Commands\Rollback::class, Commands\TenantList::class, - Commands\CreateTenant::class, Commands\MigrateFresh::class, ]); diff --git a/src/TenantDatabaseManagers/MySQLDatabaseManager.php b/src/TenantDatabaseManagers/MySQLDatabaseManager.php index 2685b527..ea32be63 100644 --- a/src/TenantDatabaseManagers/MySQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/MySQLDatabaseManager.php @@ -9,7 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Contracts\TenantWithDatabase; class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { @@ -21,11 +21,6 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection $this->connection = $config->get('tenancy.database_manager_connections.mysql'); } - public function getSeparator(): string - { - return 'database'; - } - protected function database(): Connection { return DB::connection($this->connection); @@ -36,7 +31,7 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection $this->connection = $connection; } - public function createDatabase(Tenant $tenant): bool + public function createDatabase(TenantWithDatabase $tenant): bool { $database = $tenant->database()->getName(); $charset = $this->database()->getConfig('charset'); @@ -45,7 +40,7 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection return $this->database()->statement("CREATE DATABASE `{$database}` CHARACTER SET `$charset` COLLATE `$collation`"); } - public function deleteDatabase(Tenant $tenant): bool + public function deleteDatabase(TenantWithDatabase $tenant): bool { return $this->database()->statement("DROP DATABASE `{$tenant->database()->getName()}`"); } @@ -54,4 +49,11 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { return (bool) $this->database()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); } + + public function makeConnectionConfig(array $baseConfig, string $databaseName): array + { + $baseConfig['database'] = $databaseName; + + return $baseConfig; + } } diff --git a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php index 3ce51568..7a247c6c 100644 --- a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php @@ -59,4 +59,11 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl { return (bool) $this->database()->select("SELECT count(*) FROM mysql.user WHERE user = '$username'")[0]->{'count(*)'}; } + + public function makeConnectionConfig(array $baseConfig, string $databaseName): array + { + $baseConfig['database'] = $databaseName; + + return $baseConfig; + } } diff --git a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php index f189e232..e17aae9b 100644 --- a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php @@ -9,7 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Contracts\TenantWithDatabase; class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { @@ -21,11 +21,6 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti $this->connection = $config->get('tenancy.database_manager_connections.pgsql'); } - public function getSeparator(): string - { - return 'database'; - } - protected function database(): Connection { return DB::connection($this->connection); @@ -36,12 +31,12 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti $this->connection = $connection; } - public function createDatabase(Tenant $tenant): bool + public function createDatabase(TenantWithDatabase $tenant): bool { return $this->database()->statement("CREATE DATABASE \"{$tenant->database()->getName()}\" WITH TEMPLATE=template0"); } - public function deleteDatabase(Tenant $tenant): bool + public function deleteDatabase(TenantWithDatabase $tenant): bool { return $this->database()->statement("DROP DATABASE \"{$tenant->database()->getName()}\""); } @@ -50,4 +45,11 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti { return (bool) $this->database()->select("SELECT datname FROM pg_database WHERE datname = '$name'"); } + + public function makeConnectionConfig(array $baseConfig, string $databaseName): array + { + $baseConfig['database'] = $databaseName; + + return $baseConfig; + } } diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php index 819daf09..6077b6dc 100644 --- a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -9,7 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Contracts\TenantWithDatabase; class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection { @@ -21,11 +21,6 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection $this->connection = $config->get('tenancy.database_manager_connections.pgsql'); } - public function getSeparator(): string - { - return 'schema'; - } - protected function database(): Connection { return DB::connection($this->connection); @@ -36,12 +31,12 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection $this->connection = $connection; } - public function createDatabase(Tenant $tenant): bool + public function createDatabase(TenantWithDatabase $tenant): bool { return $this->database()->statement("CREATE SCHEMA \"{$tenant->database()->getName()}\""); } - public function deleteDatabase(Tenant $tenant): bool + public function deleteDatabase(TenantWithDatabase $tenant): bool { return $this->database()->statement("DROP SCHEMA \"{$tenant->database()->getName()}\""); } @@ -50,4 +45,11 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection { return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'"); } + + public function makeConnectionConfig(array $baseConfig, string $databaseName): array + { + $baseConfig['schema'] = $databaseName; + + return $baseConfig; + } } diff --git a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php index f3c110d5..562a54be 100644 --- a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php +++ b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php @@ -4,17 +4,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\TenantDatabaseManagers; -use Stancl\Tenancy\Contracts\ModifiesDatabaseNameForConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; use Stancl\Tenancy\Contracts\TenantWithDatabase; -class SQLiteDatabaseManager implements TenantDatabaseManager, ModifiesDatabaseNameForConnection +class SQLiteDatabaseManager implements TenantDatabaseManager { - public function getSeparator(): string - { - return 'database'; - } - public function createDatabase(TenantWithDatabase $tenant): bool { try { @@ -38,8 +32,10 @@ class SQLiteDatabaseManager implements TenantDatabaseManager, ModifiesDatabaseNa return file_exists(database_path($name)); } - public function getDatabaseNameForConnection(string $original): string + public function makeConnectionConfig(array $baseConfig, string $databaseName): array { - return database_path($original); + $baseConfig['database'] = database_path($databaseName); + + return $baseConfig; } } diff --git a/src/TenantRouteServiceProvider.php b/src/TenantRouteServiceProvider.php index 1f36b35b..531fe153 100644 --- a/src/TenantRouteServiceProvider.php +++ b/src/TenantRouteServiceProvider.php @@ -13,7 +13,7 @@ class TenantRouteServiceProvider extends RouteServiceProvider { $this->app->booted(function () { if (file_exists(base_path('routes/tenant.php'))) { - Route::middleware(['web', 'tenancy']) + Route::middleware(['web']) ->namespace($this->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers') ->group(base_path('routes/tenant.php')); } diff --git a/src/Traits/CreatesDatabaseUsers.php b/src/Traits/CreatesDatabaseUsers.php index 40d97e30..694bee13 100644 --- a/src/Traits/CreatesDatabaseUsers.php +++ b/src/Traits/CreatesDatabaseUsers.php @@ -4,11 +4,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\Traits; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Contracts\TenantWithDatabase; trait CreatesDatabaseUsers { - public function createDatabase(Tenant $tenant): bool + public function createDatabase(TenantWithDatabase $tenant): bool { return $this->database()->transaction(function () use ($tenant) { parent::createDatabase($tenant); @@ -17,7 +17,7 @@ trait CreatesDatabaseUsers }); } - public function deleteDatabase(Tenant $tenant): bool + public function deleteDatabase(TenantWithDatabase $tenant): bool { return $this->database()->transaction(function () use ($tenant) { parent::deleteDatabase($tenant); diff --git a/src/Traits/HasATenantsOption.php b/src/Traits/HasATenantsOption.php index 103a3b91..e7c24784 100644 --- a/src/Traits/HasATenantsOption.php +++ b/src/Traits/HasATenantsOption.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Traits; +use Illuminate\Support\LazyCollection; use Symfony\Component\Console\Input\InputOption; trait HasATenantsOption @@ -15,9 +16,14 @@ trait HasATenantsOption ], parent::getOptions()); } - protected function getTenants(): array + protected function getTenants(): LazyCollection { - return tenancy()->all($this->option('tenants'))->all(); + return tenancy() + ->query() + ->when($this->option('tenants'), function ($query) { + $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); + }) + ->cursor(); } public function __construct() diff --git a/src/Traits/HasArrayAccess.php b/src/Traits/HasArrayAccess.php deleted file mode 100644 index 58d46f26..00000000 --- a/src/Traits/HasArrayAccess.php +++ /dev/null @@ -1,28 +0,0 @@ -$offset; - } - - public function offsetSet($offset, $value): void - { - $this->$offset = $value; - } - - public function offsetUnset($offset): void - { - unset($this->$offset); - } -} diff --git a/src/Traits/TenantAwareCommand.php b/src/Traits/TenantAwareCommand.php index fb11df9f..57690356 100644 --- a/src/Traits/TenantAwareCommand.php +++ b/src/Traits/TenantAwareCommand.php @@ -32,7 +32,7 @@ trait TenantAwareCommand /** * Get an array of tenants for which the command should be executed. * - * @return Tenant[] + * @return Tenant[]|mixed */ - abstract protected function getTenants(): array; + abstract protected function getTenants(); } diff --git a/src/routes.php b/src/routes.php index 093f5de4..54bbb0b2 100644 --- a/src/routes.php +++ b/src/routes.php @@ -2,8 +2,8 @@ declare(strict_types=1); -Route::middleware(['tenancy'])->group(function () { - Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset') - ->where('path', '(.*)') - ->name('stancl.tenancy.asset'); -}); +use Illuminate\Support\Facades\Route; + +Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset') + ->where('path', '(.*)') + ->name('stancl.tenancy.asset'); \ No newline at end of file diff --git a/tests/v3/AutomaticModeTest.php b/tests/AutomaticModeTest.php similarity index 100% rename from tests/v3/AutomaticModeTest.php rename to tests/AutomaticModeTest.php diff --git a/tests/v3/BootstrapperTest.php b/tests/BootstrapperTest.php similarity index 82% rename from tests/v3/BootstrapperTest.php rename to tests/BootstrapperTest.php index 71bdf997..94b4c7d9 100644 --- a/tests/v3/BootstrapperTest.php +++ b/tests/BootstrapperTest.php @@ -154,6 +154,12 @@ class BootstrapperTest extends TestCase FilesystemTenancyBootstrapper::class ]]); + $old_storage_path = storage_path(); + $old_storage_facade_roots = []; + foreach (config('tenancy.filesystem.disks') as $disk) { + $old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root"); + } + $tenant1 = Tenant::create(); $tenant2 = Tenant::create(); @@ -176,6 +182,27 @@ class BootstrapperTest extends TestCase tenancy()->initialize($tenant3); $this->assertFalse(Storage::disk('public')->exists('foo')); $this->assertFalse(Storage::disk('public')->exists('abc')); + + // Check suffixing logic + $new_storage_path = storage_path(); + $this->assertEquals($old_storage_path . '/' . config('tenancy.filesystem.suffix_base') . tenant('id'), $new_storage_path); + + foreach (config('tenancy.filesystem.disks') as $disk) { + $suffix = config('tenancy.filesystem.suffix_base') . tenant('id'); + $current_path_prefix = Storage::disk($disk)->getAdapter()->getPathPrefix(); + + if ($override = config("tenancy.filesystem.root_override.{$disk}")) { + $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override); + } else { + if ($base = $old_storage_facade_roots[$disk]) { + $correct_path_prefix = $base . "/$suffix/"; + } else { + $correct_path_prefix = "$suffix/"; + } + } + + $this->assertSame($correct_path_prefix, $current_path_prefix); + } } // for queues see QueueTest diff --git a/tests/CacheManagerTest.php b/tests/CacheManagerTest.php index 13e88916..54d9b033 100644 --- a/tests/CacheManagerTest.php +++ b/tests/CacheManagerTest.php @@ -2,25 +2,41 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\v3; -use Stancl\Tenancy\Tenant; +use Illuminate\Support\Facades\Event; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Events\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper; +use Stancl\Tenancy\Tests\TestCase; class CacheManagerTest extends TestCase { + public function setUp(): void + { + parent::setUp(); + + config(['tenancy.bootstrappers' => [ + CacheTenancyBootstrapper::class, + ]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + } + /** @test */ public function default_tag_is_automatically_applied() { - $this->createTenant(); - $this->initTenancy(); + tenancy()->initialize(Tenant::create()); + $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); } /** @test */ public function tags_are_merged_when_array_is_passed() { - $this->createTenant(); - $this->initTenancy(); + tenancy()->initialize(Tenant::create()); + $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); } @@ -28,8 +44,8 @@ class CacheManagerTest extends TestCase /** @test */ public function tags_are_merged_when_string_is_passed() { - $this->createTenant(); - $this->initTenancy(); + tenancy()->initialize(Tenant::create()); + $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo']; $this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames()); } @@ -37,8 +53,8 @@ class CacheManagerTest extends TestCase /** @test */ public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method() { - $this->createTenant(); - $this->initTenancy(); + tenancy()->initialize(Tenant::create()); + $this->expectException(\Exception::class); cache()->tags(); } @@ -46,8 +62,8 @@ class CacheManagerTest extends TestCase /** @test */ public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method() { - $this->createTenant(); - $this->initTenancy(); + tenancy()->initialize(Tenant::create()); + $this->expectException(\Exception::class); cache()->tags(1, 2); } @@ -55,14 +71,14 @@ class CacheManagerTest extends TestCase /** @test */ public function tags_separate_cache_well_enough() { - Tenant::new()->withDomains(['foo.localhost'])->save(); - tenancy()->init('foo.localhost'); + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); cache()->put('foo', 'bar', 1); $this->assertSame('bar', cache()->get('foo')); - Tenant::new()->withDomains(['bar.localhost'])->save(); - tenancy()->init('bar.localhost'); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant2); $this->assertNotSame('bar', cache()->get('foo')); @@ -73,14 +89,14 @@ class CacheManagerTest extends TestCase /** @test */ public function invoking_the_cache_helper_works() { - Tenant::new()->withDomains(['foo.localhost'])->save(); - tenancy()->init('foo.localhost'); + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); cache(['foo' => 'bar'], 1); $this->assertSame('bar', cache('foo')); - Tenant::new()->withDomains(['bar.localhost'])->save(); - tenancy()->init('bar.localhost'); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant2); $this->assertNotSame('bar', cache('foo')); @@ -91,32 +107,32 @@ class CacheManagerTest extends TestCase /** @test */ public function cache_is_persisted() { - Tenant::new()->withDomains(['foo.localhost'])->save(); - tenancy()->init('foo.localhost'); + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); cache(['foo' => 'bar'], 10); $this->assertSame('bar', cache('foo')); - tenancy()->endTenancy(); + tenancy()->end(); - tenancy()->init('foo.localhost'); + tenancy()->initialize($tenant1); $this->assertSame('bar', cache('foo')); } /** @test */ public function cache_is_persisted_when_reidentification_is_used() { - Tenant::new()->withDomains(['foo.localhost'])->save(); - Tenant::new()->withDomains(['bar.localhost'])->save(); - tenancy()->init('foo.localhost'); + $tenant1 = Tenant::create(); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant1); cache(['foo' => 'bar'], 10); $this->assertSame('bar', cache('foo')); - tenancy()->init('bar.localhost'); - tenancy()->endTenancy(); + tenancy()->initialize($tenant2); + tenancy()->end(); - tenancy()->init('foo.localhost'); + tenancy()->initialize($tenant1); $this->assertSame('bar', cache('foo')); } } diff --git a/tests/v3/CachedResolutionTest.php b/tests/CachedResolutionTest.php similarity index 100% rename from tests/v3/CachedResolutionTest.php rename to tests/CachedResolutionTest.php diff --git a/tests/CachedResolverTest.php b/tests/CachedResolverTest.php deleted file mode 100644 index 0d32915e..00000000 --- a/tests/CachedResolverTest.php +++ /dev/null @@ -1,164 +0,0 @@ -markTestSkipped('This test is only relevant for the DB storage driver.'); - } - - config(['tenancy.storage_drivers.db.cache_store' => config('cache.default')]); - } - - /** @test */ - public function a_query_is_not_made_for_tenant_id_once_domain_is_cached() - { - $tenant = Tenant::new() - ->withData(['foo' => 'bar']) - ->withDomains(['foo.localhost']) - ->save(); - - // query is made - $queried = tenancy()->findByDomain('foo.localhost'); - $this->assertEquals($tenant->data, $queried->data); - $this->assertSame($tenant->domains, $queried->domains); - - // cache is set - $this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost')); - $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id)); - $this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); - - // query is not made - DatabaseStorageDriver::getCentralConnection()->enableQueryLog(); - $cached = tenancy()->findByDomain('foo.localhost'); - $this->assertEquals($tenant->data, $cached->data); - $this->assertSame($tenant->domains, $cached->domains); - $this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog()); - } - - /** @test */ - public function a_query_is_not_made_for_tenant_once_id_is_cached() - { - $tenant = Tenant::new() - ->withData(['foo' => 'bar']) - ->withDomains(['foo.localhost']) - ->save(); - - // query is made - $queried = tenancy()->find($tenant->id); - $this->assertEquals($tenant->data, $queried->data); - $this->assertSame($tenant->domains, $queried->domains); - - // cache is set - $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id)); - $this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); - - // query is not made - DatabaseStorageDriver::getCentralConnection()->enableQueryLog(); - $cached = tenancy()->find($tenant->id); - $this->assertEquals($tenant->data, $cached->data); - $this->assertSame($tenant->domains, $cached->domains); - $this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog()); - } - - /** @test */ - public function modifying_tenant_domains_invalidates_the_cached_domain_to_id_mapping() - { - $tenant = Tenant::new() - ->withDomains(['foo.localhost', 'bar.localhost']) - ->save(); - - // queried - $this->assertSame($tenant->id, tenancy()->findByDomain('foo.localhost')->id); - $this->assertSame($tenant->id, tenancy()->findByDomain('bar.localhost')->id); - - // assert cache set - $this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost')); - $this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:bar.localhost')); - - $tenant - ->removeDomains(['foo.localhost', 'bar.localhost']) - ->addDomains(['xyz.localhost']) - ->save(); - - // assert neither domain is cached - $this->assertSame(null, Cache::get('_tenancy_domain_to_id:foo.localhost')); - $this->assertSame(null, Cache::get('_tenancy_domain_to_id:bar.localhost')); - $this->assertSame(null, Cache::get('_tenancy_domain_to_id:xyz.localhost')); - } - - /** @test */ - public function modifying_tenants_data_invalidates_tenant_data_cache() - { - $tenant = Tenant::new()->withData(['foo' => 'bar'])->save(); - - // cache record is set - $this->assertSame('bar', tenancy()->find($tenant->id)->get('foo')); - $this->assertSame('bar', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']); - - // cache record is invalidated - $tenant->set('foo', 'xyz'); - $this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id)); - - // cache record is set - $this->assertSame('xyz', tenancy()->find($tenant->id)->get('foo')); - $this->assertSame('xyz', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']); - - // cache record is invalidated - $tenant->foo = 'abc'; - $tenant->save(); - $this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id)); - } - - /** @test */ - public function modifying_tenants_domains_invalidates_tenant_domain_cache() - { - $tenant = Tenant::new() - ->withData(['foo' => 'bar']) - ->withDomains(['foo.localhost']) - ->save(); - - // cache record is set - $this->assertSame(['foo.localhost'], tenancy()->find($tenant->id)->domains); - $this->assertSame(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id)); - - // cache record is invalidated - $tenant->addDomains(['bar.localhost'])->save(); - $this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); - - $this->assertEquals(['foo.localhost', 'bar.localhost'], tenancy()->find($tenant->id)->domains); - } - - /** @test */ - public function deleting_a_tenant_invalidates_all_caches() - { - $tenant = Tenant::new() - ->withData(['foo' => 'bar']) - ->withDomains(['foo.localhost']) - ->save(); - - tenancy()->findByDomain('foo.localhost'); - $this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost')); - $this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id)); - $this->assertEquals(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id)); - - $tenant->delete(); - $this->assertEquals(null, Cache::get('_tenancy_domain_to_id:foo.localhost')); - $this->assertEquals(null, Cache::get('_tenancy_id_to_data:' . $tenant->id)); - $this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id)); - } -} diff --git a/tests/v3/CombinedDomainAndSubdomainIdentificationTest.php b/tests/CombinedDomainAndSubdomainIdentificationTest.php similarity index 100% rename from tests/v3/CombinedDomainAndSubdomainIdentificationTest.php rename to tests/CombinedDomainAndSubdomainIdentificationTest.php diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index e1522b1e..b7349c11 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -2,18 +2,41 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\v3; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; -use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tests\Etc\ExampleSeeder; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Events\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Events\Listeners\JobPipeline; +use Stancl\Tenancy\Events\Listeners\RevertToCentralContext; +use Stancl\Tenancy\Events\TenancyEnded; +use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper; +use Stancl\Tenancy\Tests\TestCase; class CommandsTest extends TestCase { - public $autoCreateTenant = true; - public $autoInitTenancy = false; + public function setUp(): void + { + parent::setUp(); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + config(['tenancy.bootstrappers' => [ + DatabaseTenancyBootstrapper::class + ]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + Event::listen(TenancyEnded::class, RevertToCentralContext::class); + } /** @test */ public function migrate_command_doesnt_change_the_db_connection() @@ -32,35 +55,42 @@ class CommandsTest extends TestCase /** @test */ public function migrate_command_works_without_options() { + $tenant = Tenant::create(); + $this->assertFalse(Schema::hasTable('users')); Artisan::call('tenants:migrate'); $this->assertFalse(Schema::hasTable('users')); - tenancy()->init('test.localhost'); + + tenancy()->initialize($tenant); + $this->assertTrue(Schema::hasTable('users')); } /** @test */ public function migrate_command_works_with_tenants_option() { - $tenant = Tenant::new()->withDomains(['test2.localhost'])->save(); + $tenant = Tenant::create(); Artisan::call('tenants:migrate', [ '--tenants' => [$tenant['id']], ]); $this->assertFalse(Schema::hasTable('users')); - tenancy()->init('test.localhost'); + tenancy()->initialize(Tenant::create()); $this->assertFalse(Schema::hasTable('users')); - tenancy()->init('test2.localhost'); + tenancy()->initialize($tenant); $this->assertTrue(Schema::hasTable('users')); } /** @test */ public function rollback_command_works() { + $tenant = Tenant::create(); Artisan::call('tenants:migrate'); $this->assertFalse(Schema::hasTable('users')); - tenancy()->init('test.localhost'); + + tenancy()->initialize($tenant); + $this->assertTrue(Schema::hasTable('users')); Artisan::call('tenants:rollback'); $this->assertFalse(Schema::hasTable('users')); @@ -93,7 +123,7 @@ class CommandsTest extends TestCase /** @test */ public function database_connection_is_switched_to_default_when_tenancy_has_been_initialized() { - tenancy()->init('test.localhost'); + tenancy()->initialize(Tenant::create()); $this->database_connection_is_switched_to_default(); } @@ -101,7 +131,7 @@ class CommandsTest extends TestCase /** @test */ public function run_commands_works() { - $id = Tenant::new()->withDomains(['run.localhost'])->save()['id']; + $id = Tenant::create()->id; Artisan::call('tenants:migrate', ['--tenants' => [$id]]); @@ -121,34 +151,25 @@ class CommandsTest extends TestCase mkdir($dir, 0777, true); } - if (app()->version()[0] === '6') { - file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernelv6.stub')); - } else { - file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernelv7.stub')); - } - - $this->artisan('tenancy:install') - ->expectsQuestion('Do you wish to publish the migrations that create these tables?', 'yes'); + $this->artisan('tenancy:install'); $this->assertFileExists(base_path('routes/tenant.php')); $this->assertFileExists(base_path('config/tenancy.php')); $this->assertFileExists(database_path('migrations/2019_09_15_000010_create_tenants_table.php')); $this->assertFileExists(database_path('migrations/2019_09_15_000020_create_domains_table.php')); $this->assertDirectoryExists(database_path('migrations/tenant')); - - if (app()->version()[0] === '6') { - $this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernelv6.stub'), file_get_contents(app_path('Http/Kernel.php'))); - } else { - $this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernelv7.stub'), file_get_contents(app_path('Http/Kernel.php'))); - } } /** @test */ public function migrate_fresh_command_works() { + $tenant = Tenant::create(); + $this->assertFalse(Schema::hasTable('users')); Artisan::call('tenants:migrate-fresh'); $this->assertFalse(Schema::hasTable('users')); - tenancy()->init('test.localhost'); + + tenancy()->initialize($tenant); + $this->assertTrue(Schema::hasTable('users')); $this->assertFalse(DB::table('users')->exists()); @@ -159,17 +180,4 @@ class CommandsTest extends TestCase Artisan::call('tenants:migrate-fresh'); $this->assertFalse(DB::table('users')->exists()); } - - /** @test */ - public function create_command_works() - { - Artisan::call('tenants:create -d aaa.localhost -d bbb.localhost plan=free email=foo@test.local'); - $tenant = tenancy()->all()[1]; // a tenant is autocreated prior to this - $data = $tenant->data; - unset($data['id']); - unset($data['_tenancy_db_name']); - - $this->assertSame(['plan' => 'free', 'email' => 'foo@test.local'], $data); - $this->assertSame(['aaa.localhost', 'bbb.localhost'], $tenant->domains); - } } diff --git a/tests/DataSeparationTest.php b/tests/DataSeparationTest.php deleted file mode 100644 index 8fe21c66..00000000 --- a/tests/DataSeparationTest.php +++ /dev/null @@ -1,154 +0,0 @@ - [$tenant1['id'], $tenant2['id']], - ]); - - tenancy()->init('tenant1.localhost'); - User::create([ - 'name' => 'foo', - 'email' => 'foo@bar.com', - 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), - ]); - $this->assertSame('foo', User::first()->name); - - tenancy()->init('tenant2.localhost'); - $this->assertSame(null, User::first()); - - User::create([ - 'name' => 'xyz', - 'email' => 'xyz@bar.com', - 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), - ]); - - $this->assertSame('xyz', User::first()->name); - $this->assertSame('xyz@bar.com', User::first()->email); - - tenancy()->init('tenant1.localhost'); - $this->assertSame('foo', User::first()->name); - $this->assertSame('foo@bar.com', User::first()->email); - - $tenant3 = Tenant::create('tenant3.localhost'); - \Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant1['id'], $tenant3['id']], - ]); - - tenancy()->init('tenant3.localhost'); - $this->assertSame(null, User::first()); - - tenancy()->init('tenant1.localhost'); - DB::table('users')->where('id', 1)->update(['name' => 'xxx']); - $this->assertSame('xxx', User::first()->name); - } - - /** @test */ - public function redis_is_separated() - { - if (! config('tenancy.redis.tenancy')) { - $this->markTestSkipped('Redis tenancy disabled.'); - } - - Tenant::create('tenant1.localhost'); - Tenant::create('tenant2.localhost'); - - tenancy()->init('tenant1.localhost'); - Redis::set('foo', 'bar'); - $this->assertSame('bar', Redis::get('foo')); - - tenancy()->init('tenant2.localhost'); - $this->assertSame(null, Redis::get('foo')); - Redis::set('foo', 'xyz'); - Redis::set('abc', 'def'); - $this->assertSame('xyz', Redis::get('foo')); - $this->assertSame('def', Redis::get('abc')); - - tenancy()->init('tenant1.localhost'); - $this->assertSame('bar', Redis::get('foo')); - $this->assertSame(null, Redis::get('abc')); - - Tenant::create('tenant3.localhost'); - tenancy()->init('tenant3.localhost'); - $this->assertSame(null, Redis::get('foo')); - $this->assertSame(null, Redis::get('abc')); - } - - /** @test */ - public function cache_is_separated() - { - Tenant::create('tenant1.localhost'); - Tenant::create('tenant2.localhost'); - - tenancy()->init('tenant1.localhost'); - Cache::put('foo', 'bar', 60); - $this->assertSame('bar', Cache::get('foo')); - - tenancy()->init('tenant2.localhost'); - $this->assertSame(null, Cache::get('foo')); - Cache::put('foo', 'xyz', 60); - Cache::put('abc', 'def', 60); - $this->assertSame('xyz', Cache::get('foo')); - $this->assertSame('def', Cache::get('abc')); - - tenancy()->init('tenant1.localhost'); - $this->assertSame('bar', Cache::get('foo')); - $this->assertSame(null, Cache::get('abc')); - - Tenant::create('tenant3.localhost'); - tenancy()->init('tenant3.localhost'); - $this->assertSame(null, Cache::get('foo')); - $this->assertSame(null, Cache::get('abc')); - } - - /** @test */ - public function filesystem_is_separated() - { - Tenant::create('tenant1.localhost'); - Tenant::create('tenant2.localhost'); - - tenancy()->init('tenant1.localhost'); - Storage::disk('public')->put('foo', 'bar'); - $this->assertSame('bar', Storage::disk('public')->get('foo')); - - tenancy()->init('tenant2.localhost'); - $this->assertFalse(Storage::disk('public')->exists('foo')); - Storage::disk('public')->put('foo', 'xyz'); - Storage::disk('public')->put('abc', 'def'); - $this->assertSame('xyz', Storage::disk('public')->get('foo')); - $this->assertSame('def', Storage::disk('public')->get('abc')); - - tenancy()->init('tenant1.localhost'); - $this->assertSame('bar', Storage::disk('public')->get('foo')); - $this->assertFalse(Storage::disk('public')->exists('abc')); - - Tenant::create('tenant3.localhost'); - tenancy()->init('tenant3.localhost'); - $this->assertFalse(Storage::disk('public')->exists('foo')); - $this->assertFalse(Storage::disk('public')->exists('abc')); - } -} diff --git a/tests/DatabaseManagerTest.php b/tests/DatabaseManagerTest.php deleted file mode 100644 index 1bb29663..00000000 --- a/tests/DatabaseManagerTest.php +++ /dev/null @@ -1,73 +0,0 @@ -connection()->getName(); - $this->createTenant(); - $this->initTenancy(); - app(\Stancl\Tenancy\DatabaseManager::class)->reconnect(); - $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); - - $this->assertSame($old_connection_name, $new_connection_name); - $this->assertNotEquals('tenant', $new_connection_name); - } - - /** @test */ - public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() - { - if (file_exists(database_path('foodb'))) { - unlink(database_path('foodb')); // cleanup - } - config(['database.connections.fooconn.driver' => 'sqlite']); - $tenant = Tenant::new()->withData([ - '_tenancy_db_name' => 'foodb', - '_tenancy_db_connection' => 'fooconn', - ])->save(); - app(DatabaseManager::class)->createTenantConnection($tenant); - - $this->assertSame(config('database.connections.tenant.database'), database_path('foodb')); - } - - /** @test */ - public function the_default_db_is_used_when_template_connection_is_null() - { - $this->assertSame('central', config('database.default')); - config([ - 'database.connections.central.foo' => 'bar', - 'tenancy.database.template_connection' => null, - ]); - - $this->createTenant(); - $this->initTenancy(); - - $this->assertSame('tenant', config('database.default')); - $this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo')); - } - - /** @test */ - public function ending_tenancy_doesnt_purge_the_central_connection() - { - $this->markTestIncomplete('Seems like this only happens on MySQL?'); - - // regression test for https://github.com/stancl/tenancy/pull/189 - // config(['tenancy.migrate_after_creation' => true]); - - tenancy()->create(['foo.localhost']); - tenancy()->init('foo.localhost'); - tenancy()->end(); - - $this->assertNotEmpty(tenancy()->all()); - - tenancy()->all()->each->delete(); - } -} diff --git a/tests/DatabasePreparationTest.php b/tests/DatabasePreparationTest.php new file mode 100644 index 00000000..053fa9dd --- /dev/null +++ b/tests/DatabasePreparationTest.php @@ -0,0 +1,135 @@ + 'mysql']); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $tenant = Tenant::create(); + + $this->assertTrue(app(MySQLDatabaseManager::class)->databaseExists($tenant->database()->getName())); + } + + /** @test */ + public function database_can_be_migrated_after_tenant_creation() + { + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $tenant = Tenant::create(); + + $tenant->run(function () { + $this->assertTrue(Schema::hasTable('users')); + }); + } + + /** @test */ + public function database_can_be_seeded_after_tenant_creation() + { + config(['tenancy.seeder_parameters' => [ + '--class' => TestSeeder::class, + ]]); + + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + SeedDatabase::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $tenant = Tenant::create(); + + $tenant->run(function () { + $this->assertSame('Seeded User', User::first()->name); + }); + } + + /** @test */ + public function custom_job_can_be_added_to_the_pipeline() + { + config(['tenancy.seeder_parameters' => [ + '--class' => TestSeeder::class, + ]]); + + Event::listen(TenantCreated::class, JobPipeline::make([ + CreateDatabase::class, + MigrateDatabase::class, + SeedDatabase::class, + CreateSuperuser::class, + ])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + $tenant = Tenant::create(); + + $tenant->run(function () { + $this->assertSame('Foo', User::all()[1]->name); + }); + } +} + +class User extends Authenticable +{ + protected $guarded = []; +} + +class TestSeeder extends Seeder +{ + /** + * Run the database seeds. + * + * @return void + */ + public function run() + { + DB::table('users')->insert([ + 'name' => 'Seeded User', + 'email' => 'seeded@user', + 'password' => bcrypt('password'), + ]); + } +} + +class CreateSuperuser +{ + protected $tenant; + + public function __construct(Tenant $tenant) + { + $this->tenant = $tenant; + } + + public function handle() + { + $this->tenant->run(function () { + User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']); + }); + } +} \ No newline at end of file diff --git a/tests/DatabaseSchemaManagerTest.php b/tests/DatabaseSchemaManagerTest.php deleted file mode 100644 index 7643b5b2..00000000 --- a/tests/DatabaseSchemaManagerTest.php +++ /dev/null @@ -1,144 +0,0 @@ -set([ - 'database.default' => 'pgsql', - 'tenancy.storage_drivers.db.connection' => 'pgsql', - 'database.connections.pgsql.database' => 'main', - 'database.connections.pgsql.schema' => 'public', - 'tenancy.database.template_connection' => null, - 'tenancy.database.suffix' => '', - 'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, - ]); - } - - /** @test */ - public function reconnect_method_works() - { - $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); - - tenancy()->init('test.localhost'); - - app(\Stancl\Tenancy\DatabaseManager::class)->reconnect(); - - $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); - - $this->assertSame($old_connection_name, $new_connection_name); - } - - /** @test */ - public function the_default_db_is_used_when_template_connection_is_null() - { - $this->assertSame('pgsql', config('database.default')); - config([ - 'database.connections.pgsql.foo' => 'bar', - 'tenancy.database.template_connection' => null, - ]); - - tenancy()->init('test.localhost'); - - $this->assertSame('tenant', config('database.default')); - $this->assertSame('bar', config('database.connections.tenant.foo')); - } - - /** @test */ - public function make_sure_using_schema_connection() - { - $tenant = tenancy()->create(['schema.localhost']); - tenancy()->init('schema.localhost'); - - $this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema')); - } - - /** @test */ - public function databases_are_separated_using_schema_and_not_database() - { - tenancy()->create('foo.localhost'); - tenancy()->init('foo.localhost'); - $this->assertSame('tenant', config('database.default')); - $this->assertSame('main', config('database.connections.tenant.database')); - - $schema1 = config('database.connections.' . config('database.default') . '.schema'); - $database1 = config('database.connections.' . config('database.default') . '.database'); - - tenancy()->create('bar.localhost'); - tenancy()->init('bar.localhost'); - $this->assertSame('tenant', config('database.default')); - $this->assertSame('main', config('database.connections.tenant.database')); - - $schema2 = config('database.connections.' . config('database.default') . '.schema'); - $database2 = config('database.connections.' . config('database.default') . '.database'); - - $this->assertSame($database1, $database2); - $this->assertNotSame($schema1, $schema2); - } - - /** @test */ - public function schemas_are_separated() - { - // copied from DataSeparationTest - - $tenant1 = Tenant::create('tenant1.localhost'); - $tenant2 = Tenant::create('tenant2.localhost'); - - \Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant1['id'], $tenant2['id']], - ]); - - tenancy()->init('tenant1.localhost'); - User::create([ - 'name' => 'foo', - 'email' => 'foo@bar.com', - 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), - ]); - $this->assertSame('foo', User::first()->name); - - tenancy()->init('tenant2.localhost'); - $this->assertSame(null, User::first()); - - User::create([ - 'name' => 'xyz', - 'email' => 'xyz@bar.com', - 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), - ]); - - $this->assertSame('xyz', User::first()->name); - $this->assertSame('xyz@bar.com', User::first()->email); - - tenancy()->init('tenant1.localhost'); - $this->assertSame('foo', User::first()->name); - $this->assertSame('foo@bar.com', User::first()->email); - - $tenant3 = Tenant::create('tenant3.localhost'); - \Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant1['id'], $tenant3['id']], - ]); - - tenancy()->init('tenant3.localhost'); - $this->assertSame(null, User::first()); - - tenancy()->init('tenant1.localhost'); - \DB::table('users')->where('id', 1)->update(['name' => 'xxx']); - $this->assertSame('xxx', User::first()->name); - } -} diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php index 999d4839..659679bc 100644 --- a/tests/DatabaseUsersTest.php +++ b/tests/DatabaseUsersTest.php @@ -2,15 +2,20 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\v3; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Str; use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; -use Stancl\Tenancy\Tenant; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Events\Listeners\JobPipeline; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\Tests\TestCase; class DatabaseUsersTest extends TestCase { @@ -21,14 +26,18 @@ class DatabaseUsersTest extends TestCase config([ 'tenancy.database_managers.mysql' => PermissionControlledMySQLDatabaseManager::class, 'tenancy.database.suffix' => '', - 'tenancy.database.template_connection' => 'mysql', + 'tenancy.template_tenant_connection' => 'mysql', ]); + + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); } /** @test */ public function users_are_created_when_permission_controlled_mysql_manager_is_used() { - $tenant = Tenant::new()->withData([ + $tenant = new Tenant([ 'id' => 'foo' . Str::random(10), ]); $tenant->database()->makeCredentials(); @@ -46,9 +55,9 @@ class DatabaseUsersTest extends TestCase public function a_tenants_database_cannot_be_created_when_the_user_already_exists() { $username = 'foo' . Str::random(8); - $tenant = Tenant::new()->withData([ - '_tenancy_db_username' => $username, - ])->save(); + $tenant = Tenant::create([ + 'tenancy_db_username' => $username, + ]); /** @var ManagesDatabaseUsers $manager */ $manager = $tenant->database()->manager(); @@ -56,9 +65,9 @@ class DatabaseUsersTest extends TestCase $this->assertTrue($manager->databaseExists($tenant->database()->getName())); $this->expectException(TenantDatabaseUserAlreadyExistsException::class); - $tenant2 = Tenant::new()->withData([ - '_tenancy_db_username' => $username, - ])->save(); + $tenant2 = Tenant::create([ + 'tenancy_db_username' => $username, + ]); /** @var ManagesDatabaseUsers $manager */ $manager = $tenant2->database()->manager(); @@ -73,9 +82,9 @@ class DatabaseUsersTest extends TestCase 'ALTER', 'ALTER ROUTINE', 'CREATE', ]; - $tenant = Tenant::new()->withData([ - '_tenancy_db_username' => $user = 'user' . Str::random(8), - ])->save(); + $tenant = Tenant::create([ + 'tenancy_db_username' => $user = 'user' . Str::random(8), + ]); $query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`{$tenant->database()->connection()['host']}`")[1]; $this->assertStringStartsWith('GRANT CREATE, ALTER, ALTER ROUTINE ON', $query->{"Grants for {$user}@mysql"}); // @mysql because that's the hostname within the docker network @@ -90,15 +99,15 @@ class DatabaseUsersTest extends TestCase 'tenancy.database.template_connection' => 'mysql', ]); - $tenant = Tenant::new()->withData([ + $tenant = Tenant::create([ 'id' => 'foo' . Str::random(10), - ])->save(); + ]); $this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager); - $tenant = Tenant::new()->withData([ + $tenant = Tenant::create([ 'id' => 'foo' . Str::random(10), - ])->save(); + ]); tenancy()->initialize($tenant); // check if everything works tenancy()->end(); diff --git a/tests/v3/DomainTest.php b/tests/DomainTest.php similarity index 100% rename from tests/v3/DomainTest.php rename to tests/DomainTest.php diff --git a/tests/Etc/HttpKernel.php b/tests/Etc/HttpKernel.php index 3d5033b2..3bb43c53 100644 --- a/tests/Etc/HttpKernel.php +++ b/tests/Etc/HttpKernel.php @@ -39,10 +39,6 @@ class HttpKernel extends Kernel \Illuminate\Routing\Middleware\SubstituteBindings::class, ], - 'tenancy' => [ - \Stancl\Tenancy\Middleware\InitializeTenancy::class, - ], - 'api' => [ 'throttle:60,1', 'bindings', @@ -66,18 +62,5 @@ class HttpKernel extends Kernel 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - - 'tenancy' => \Stancl\Tenancy\Middleware\InitializeTenancy::class, - ]; - - /** - * The priority-sorted list of middleware. - * - * This forces non-global middleware to always be in the given order. - * - * @var array - */ - protected $middlewarePriority = [ - \Stancl\Tenancy\Middleware\InitializeTenancy::class, ]; } diff --git a/tests/v3/EventListenerTest.php b/tests/EventListenerTest.php similarity index 100% rename from tests/v3/EventListenerTest.php rename to tests/EventListenerTest.php diff --git a/tests/FacadeTest.php b/tests/FacadeTest.php deleted file mode 100644 index dd82cfe4..00000000 --- a/tests/FacadeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -assertSame(tenancy()->getTenant(), Tenancy::getTenant()); - } - - /** @test */ - public function tenant_storage_can_be_accessed_using_the_Tenant_facade() - { - tenant()->put('foo', 'bar'); - Tenant::put('abc', 'xyz'); - - $this->assertSame('bar', Tenant::get('foo')); - $this->assertSame('xyz', Tenant::get('abc')); - } - - /** @test */ - public function tenant_can_be_created_using_the_Tenant_facade() - { - $this->assertSame('bar', Tenant::create(['foo.localhost'], ['foo' => 'bar'])->foo); - } -} diff --git a/tests/RedirectTest.php b/tests/Features/RedirectTest.php similarity index 67% rename from tests/RedirectTest.php rename to tests/Features/RedirectTest.php index ededb239..a83fdc27 100644 --- a/tests/RedirectTest.php +++ b/tests/Features/RedirectTest.php @@ -2,17 +2,15 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\Features; -use Route; +use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Features\CrossDomainRedirect; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Tests\TestCase; class RedirectTest extends TestCase { - public $autoCreateTenant = false; - public $autoInitTenancy = false; - /** @test */ public function tenant_redirect_macro_replaces_only_the_hostname() { @@ -28,8 +26,8 @@ class RedirectTest extends TestCase return redirect()->route('home')->domain('abcd'); }); - Tenant::create('foo.localhost'); - tenancy()->init('foo.localhost'); + $tenant = Tenant::create(); + tenancy()->initialize($tenant); $this->get('/redirect') ->assertRedirect('http://abcd/foobar'); @@ -42,9 +40,7 @@ class RedirectTest extends TestCase return 'Foo'; })->name('foo'); - $this->assertSame('http://foo.localhost/abcdef/as/df', tenant_route('foo', ['a' => 'as', 'b' => 'df'], 'foo.localhost')); - $this->assertSame('http://foo.localhost/abcdef', tenant_route('foo', [], 'foo.localhost')); - - $this->assertSame('http://' . request()->getHost() . '/abcdef/x/y', tenant_route('foo', ['a' => 'x', 'b' => 'y'])); + $this->assertSame('http://foo.localhost/abcdef/as/df', tenant_route('foo.localhost', 'foo', ['a' => 'as', 'b' => 'df'])); + $this->assertSame('http://foo.localhost/abcdef', tenant_route('foo.localhost', 'foo', [])); } } diff --git a/tests/FutureTest.php b/tests/FutureTest.php deleted file mode 100644 index 6a44a80e..00000000 --- a/tests/FutureTest.php +++ /dev/null @@ -1,45 +0,0 @@ -withData(['email' => 'foo@example.com', 'role' => 'admin'])->save(); - - $this->assertArrayHasKey('email', $tenant->data); - $tenant->deleteKey('email'); - $this->assertArrayNotHasKey('email', $tenant->data); - $this->assertArrayNotHasKey('email', tenancy()->all()->first()->data); - - $tenant->put(['foo' => 'bar', 'abc' => 'xyz']); - $this->assertArrayHasKey('foo', $tenant->data); - $this->assertArrayHasKey('abc', $tenant->data); - - $tenant->deleteKeys(['foo', 'abc']); - $this->assertArrayNotHasKey('foo', $tenant->data); - $this->assertArrayNotHasKey('abc', $tenant->data); - } - - /** @test */ - public function tenant_can_be_identified_using_an_arbitrary_string() - { - if (! tenancy()->storage instanceof CanFindByAnyKey) { - $this->markTestSkipped(get_class(tenancy()->storage) . ' does not implement the CanFindByAnyKey interface.'); - } - - $tenant = Tenant::new()->withData(['email' => 'foo@example.com'])->save(); - - $this->assertSame($tenant->id, tenancy()->findByEmail('foo@example.com')->id); - } -} diff --git a/tests/GlobalCacheTest.php b/tests/GlobalCacheTest.php index 598568a8..e5d86c92 100644 --- a/tests/GlobalCacheTest.php +++ b/tests/GlobalCacheTest.php @@ -2,15 +2,28 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\v3; -use GlobalCache; -use Stancl\Tenancy\Tenant; +use Illuminate\Support\Facades\Event; +use Stancl\Tenancy\Facades\GlobalCache; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Events\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper; +use Stancl\Tenancy\Tests\TestCase; class GlobalCacheTest extends TestCase { - public $autoCreateTenant = false; - public $autoInitTenancy = false; + public function setUp(): void + { + parent::setUp(); + + config(['tenancy.bootstrappers' => [ + CacheTenancyBootstrapper::class, + ]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + } /** @test */ public function global_cache_manager_stores_data_in_global_cache() @@ -19,28 +32,28 @@ class GlobalCacheTest extends TestCase GlobalCache::put(['foo' => 'bar'], 1); $this->assertSame('bar', GlobalCache::get('foo')); - Tenant::new()->withDomains(['foo.localhost'])->save(); - tenancy()->init('foo.localhost'); + $tenant1 = Tenant::create(); + tenancy()->initialize($tenant1); $this->assertSame('bar', GlobalCache::get('foo')); GlobalCache::put(['abc' => 'xyz'], 1); cache(['def' => 'ghi'], 10); $this->assertSame('ghi', cache('def')); - tenancy()->endTenancy(); + tenancy()->end(); $this->assertSame('xyz', GlobalCache::get('abc')); $this->assertSame('bar', GlobalCache::get('foo')); $this->assertSame(null, cache('def')); - Tenant::new()->withDomains(['bar.localhost'])->save(); - tenancy()->init('bar.localhost'); + $tenant2 = Tenant::create(); + tenancy()->initialize($tenant2); $this->assertSame('xyz', GlobalCache::get('abc')); $this->assertSame('bar', GlobalCache::get('foo')); $this->assertSame(null, cache('def')); cache(['def' => 'xxx'], 1); $this->assertSame('xxx', cache('def')); - tenancy()->init('foo.localhost'); + tenancy()->initialize($tenant1); $this->assertSame('ghi', cache('def')); } } diff --git a/tests/v3/JobPipelineTest.php b/tests/JobPipelineTest.php similarity index 100% rename from tests/v3/JobPipelineTest.php rename to tests/JobPipelineTest.php diff --git a/tests/v3/PathIdentificationTest.php b/tests/PathIdentificationTest.php similarity index 100% rename from tests/v3/PathIdentificationTest.php rename to tests/PathIdentificationTest.php diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 8e218f89..2fdb4da9 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -1,8 +1,6 @@ markTestIncomplete(); + parent::setUp(); + + config([ + 'tenancy.bootstrappers' => [ + QueueTenancyBootstrapper::class, + ], + 'queue.default' => 'redis', + ]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + + $this->valuestore = Valuestore::make(__DIR__ . '/../Etc/tmp/queuetest.json')->flush(); } /** @test */ - public function tenancy_is_initialized_inside_queues() + public function tenant_id_is_passed_to_tenant_queues() { - $this->loadLaravelMigrations(['--database' => 'tenant']); + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + Event::fake(); - dispatch(new TestJob()); + dispatch(new TestJob($this->valuestore)); Event::assertDispatched(JobProcessing::class, function ($event) { return $event->job->payload()['tenant_id'] === tenant('id'); @@ -38,40 +56,81 @@ class QueueTest extends TestCase } /** @test */ - public function tenancy_is_not_initialized_in_non_tenant_queues() + public function tenant_id_is_not_passed_to_central_queues() { - $this->loadLaravelMigrations(['--database' => 'tenant']); + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + Event::fake(); - dispatch(new TestJob())->onConnection('central'); + config(['queue.connections.central' => [ + 'driver' => 'sync', + 'central' => true, + ]]); + + dispatch(new TestJob($this->valuestore))->onConnection('central'); Event::assertDispatched(JobProcessing::class, function ($event) { return ! isset($event->job->payload()['tenant_id']); }); } + + /** @test */ + public function tenancy_is_initialized_inside_queues() + { + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant); + + dispatch(new TestJob($this->valuestore)); + + $this->assertFalse($this->valuestore->has('tenant_id')); + $this->artisan('queue:work --once'); + + $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); + } + + /** @test */ + public function the_tenant_used_by_the_job_doesnt_change_when_the_current_tenant_changes() + { + $tenant1 = Tenant::create([ + 'id' => 'acme', + ]); + + tenancy()->initialize($tenant1); + + dispatch(new TestJob($this->valuestore)); + + $tenant2 = Tenant::create([ + 'id' => 'foobar', + ]); + + tenancy()->initialize($tenant2); + + $this->assertFalse($this->valuestore->has('tenant_id')); + $this->artisan('queue:work --once'); + + $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); + } } class TestJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - /** - * Create a new job instance. - * - * @return void - */ - public function __construct() + /** @var Valuestore */ + protected $valuestore; + + public function __construct(Valuestore $valuestore) { - // + $this->valuestore = $valuestore; } - /** - * Execute the job. - * - * @return void - */ public function handle() { - logger(json_encode(\DB::table('users')->get())); + $this->valuestore->put('tenant_id', "The current tenant id is: " . tenant('id')); } } diff --git a/tests/ReidentificationTest.php b/tests/ReidentificationTest.php deleted file mode 100644 index 84a4781f..00000000 --- a/tests/ReidentificationTest.php +++ /dev/null @@ -1,61 +0,0 @@ -init('test.localhost'); - Tenant::new()->withDomains(['second.localhost'])->save(); - tenancy()->init('second.localhost'); - - foreach (config('tenancy.filesystem.disks') as $disk) { - $suffix = config('tenancy.filesystem.suffix_base') . tenant('id'); - $current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix(); - - if ($override = config("tenancy.filesystem.root_override.{$disk}")) { - $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override); - } else { - if ($base = $originals[$disk]) { - $correct_path_prefix = $base . "/$suffix/"; - } else { - $correct_path_prefix = "$suffix/"; - } - } - - $this->assertSame($correct_path_prefix, $current_path_prefix); - } - } - - /** @test */ - public function storage_path_is_correct() - { - $original = storage_path(); - - tenancy()->init('test.localhost'); - Tenant::new()->withDomains(['second.localhost'])->save(); - tenancy()->init('second.localhost'); - - $suffix = config('tenancy.filesystem.suffix_base') . tenant('id'); - $this->assertSame($original . "/$suffix", storage_path()); - } -} diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php index fcbd0997..86402b66 100644 --- a/tests/RequestDataIdentificationTest.php +++ b/tests/RequestDataIdentificationTest.php @@ -2,27 +2,28 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\v3; use Illuminate\Support\Facades\Route; +use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; -use Stancl\Tenancy\Tenant; +use Stancl\Tenancy\Tests\TestCase; class RequestDataIdentificationTest extends TestCase { - public $autoCreateTenant = false; - public $autoInitTenancy = false; - public function setUp(): void { parent::setUp(); config([ - 'tenancy.exempt_domains' => [ + 'tenancy.central_domains' => [ 'localhost', ], ]); + InitializeTenancyByRequestData::$header = 'X-Tenant'; + InitializeTenancyByRequestData::$queryParameter = 'tenant'; + Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () { return 'Tenant id: ' . tenant('id'); }); @@ -31,11 +32,9 @@ class RequestDataIdentificationTest extends TestCase /** @test */ public function header_identification_works() { - $this->app->bind(InitializeTenancyByRequestData::class, function () { - return new InitializeTenancyByRequestData('X-Tenant'); - }); - $tenant = Tenant::new()->save(); - $tenant2 = Tenant::new()->save(); + InitializeTenancyByRequestData::$header = 'X-Tenant'; + $tenant = Tenant::create(); + $tenant2 = Tenant::create(); $this ->withoutExceptionHandling() @@ -48,11 +47,11 @@ class RequestDataIdentificationTest extends TestCase /** @test */ public function query_parameter_identification_works() { - $this->app->bind(InitializeTenancyByRequestData::class, function () { - return new InitializeTenancyByRequestData(null, 'tenant'); - }); - $tenant = Tenant::new()->save(); - $tenant2 = Tenant::new()->save(); + InitializeTenancyByRequestData::$header = null; + InitializeTenancyByRequestData::$queryParameter = 'tenant'; + + $tenant = Tenant::create(); + $tenant2 = Tenant::create(); $this ->withoutExceptionHandling() diff --git a/tests/v3/SubdomainTest.php b/tests/SubdomainTest.php similarity index 100% rename from tests/v3/SubdomainTest.php rename to tests/SubdomainTest.php diff --git a/tests/TenancyBootstrappersTest.php b/tests/TenancyBootstrappersTest.php deleted file mode 100644 index e4cb9941..00000000 --- a/tests/TenancyBootstrappersTest.php +++ /dev/null @@ -1,97 +0,0 @@ -connection()->getName(); - $this->initTenancy(); - $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName(); - - $this->assertNotEquals($old_connection_name, $new_connection_name); - $this->assertEquals('tenant', $new_connection_name); - } - - /** @test */ - public function redis_is_prefixed() - { - if (! config('tenancy.redis.tenancy')) { - $this->markTestSkipped('Redis tenancy disabled.'); - } - - $this->initTenancy(); - foreach (config('tenancy.redis.prefixed_connections', ['default']) as $connection) { - $prefix = config('tenancy.redis.prefix_base') . tenant('id'); - $client = Redis::connection($connection)->client(); - $this->assertEquals($prefix, $client->getOption($client::OPT_PREFIX)); - } - } - - /** @test */ - public function filesystem_is_suffixed() - { - $old_storage_path = storage_path(); - $old_storage_facade_roots = []; - foreach (config('tenancy.filesystem.disks') as $disk) { - $old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root"); - } - - $this->initTenancy(); - - $new_storage_path = storage_path(); - $this->assertEquals($old_storage_path . '/' . config('tenancy.filesystem.suffix_base') . tenant('id'), $new_storage_path); - - foreach (config('tenancy.filesystem.disks') as $disk) { - $suffix = config('tenancy.filesystem.suffix_base') . tenant('id'); - $current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix(); - - if ($override = config("tenancy.filesystem.root_override.{$disk}")) { - $correct_path_prefix = str_replace('%storage_path%', storage_path(), $override); - } else { - if ($base = $old_storage_facade_roots[$disk]) { - $correct_path_prefix = $base . "/$suffix/"; - } else { - $correct_path_prefix = "$suffix/"; - } - } - - $this->assertSame($correct_path_prefix, $current_path_prefix); - } - } - - /** @test */ - public function cache_is_tagged() - { - $this->assertSame(['foo'], cache()->tags('foo')->getTags()->getNames()); - $this->initTenancy(); - - $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; - $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); - } - - /** @test */ - public function the_default_db_connection_is_used_when_the_config_value_is_null() - { - $original = config('database.default'); - tenancy()->create(['foo.localhost']); - tenancy()->init('foo.localhost'); - - $this->assertSame(null, config("database.connections.$original.foo")); - - config(["database.connections.$original.foo" => 'bar']); - tenancy()->create(['bar.localhost']); - tenancy()->init('bar.localhost'); - - $this->assertSame('bar', config("database.connections.$original.foo")); - } -} diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index b9bbabad..a8b57c15 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -2,29 +2,58 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\v3; -use Stancl\Tenancy\Tenant; +use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Storage; +use Stancl\Tenancy\Controllers\TenantAssetsController; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\Events\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; +use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; +use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper; +use Stancl\Tenancy\Tests\TestCase; class TenantAssetTest extends TestCase { - public $autoCreateTenant = false; - public $autoInitTenancy = false; + public function setUp(): void + { + parent::setUp(); + + config(['tenancy.bootstrappers' => [ + FilesystemTenancyBootstrapper::class + ]]); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + } + + public function tearDown(): void + { + parent::tearDown(); + + // Cleanup + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class; + } /** @test */ public function asset_can_be_accessed_using_the_url_returned_by_the_tenant_asset_helper() { - Tenant::create('localhost'); - tenancy()->init('localhost'); + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); $filename = 'testfile' . $this->randomString(10); - \Storage::disk('public')->put($filename, 'bar'); + Storage::disk('public')->put($filename, 'bar'); $path = storage_path("app/public/$filename"); // response()->file() returns BinaryFileResponse whose content is // inaccessible via getContent, so ->assertSee() can't be used $this->assertFileExists($path); - $response = $this->get(tenant_asset($filename)); + $response = $this->get(tenant_asset($filename), [ + 'X-Tenant' => $tenant->id, + ]); $response->assertSuccessful(); @@ -40,8 +69,8 @@ class TenantAssetTest extends TestCase { config(['app.asset_url' => null]); - Tenant::create('foo.localhost'); - tenancy()->init('foo.localhost'); + $tenant = Tenant::create(); + tenancy()->initialize($tenant); $this->assertSame(route('stancl.tenancy.asset', ['path' => 'foo']), asset('foo')); } @@ -51,8 +80,8 @@ class TenantAssetTest extends TestCase { config(['app.asset_url' => 'https://an-s3-bucket']); - $tenant = Tenant::create(['foo.localhost']); - tenancy()->init('foo.localhost'); + $tenant = Tenant::create(); + tenancy()->initialize($tenant); $this->assertSame("https://an-s3-bucket/tenant{$tenant->id}/foo", asset('foo')); } @@ -63,8 +92,8 @@ class TenantAssetTest extends TestCase $original = global_asset('foobar'); $this->assertSame(asset('foobar'), global_asset('foobar')); - Tenant::create(['foo.localhost']); - tenancy()->init('foo.localhost'); + $tenant = Tenant::create(); + tenancy()->initialize($tenant); $this->assertSame($original, global_asset('foobar')); } @@ -79,8 +108,8 @@ class TenantAssetTest extends TestCase 'tenancy.filesystem.asset_helper_tenancy' => false, ]); - Tenant::create('foo.localhost'); - tenancy()->init('foo.localhost'); + $tenant = Tenant::create(); + tenancy()->initialize($tenant); $this->assertSame($original, asset('foo')); } diff --git a/tests/TenantAwareCommandTest.php b/tests/TenantAwareCommandTest.php new file mode 100644 index 00000000..cfda289d --- /dev/null +++ b/tests/TenantAwareCommandTest.php @@ -0,0 +1,34 @@ + [$tenant1['id'], $tenant2['id']], + ]); + + $this->artisan('user:add') + ->assertExitCode(0); + + tenancy()->initialize($tenant1); + $this->assertNotEmpty(DB::table('users')->get()); + tenancy()->end(); + + tenancy()->initialize($tenant2); + $this->assertNotEmpty(DB::table('users')->get()); + tenancy()->end(); + } +} diff --git a/tests/TenantClassTest.php b/tests/TenantClassTest.php deleted file mode 100644 index ec02775c..00000000 --- a/tests/TenantClassTest.php +++ /dev/null @@ -1,227 +0,0 @@ -makePartial(); - // $this->instance(StorageDriver::class, $spy); - - $tenant = Tenant::create(['foo.localhost'], ['foo' => 'bar']); - $this->assertSame('bar', $tenant->data['foo']); - - $tenant->put('abc', 'xyz'); - $this->assertSame('xyz', $tenant->data['abc']); - - $tenant->put(['aaa' => 'bbb', 'ccc' => 'ddd']); - $this->assertSame('bbb', $tenant->data['aaa']); - $this->assertSame('ddd', $tenant->data['ccc']); - - // $spy->shouldNotHaveReceived('get'); - - $this->assertSame(null, $tenant->dfuighdfuigfhdui); - // $spy->shouldHaveReceived('get')->once(); - - Mockery::close(); - } - - /** @test */ - public function tenant_can_have_multiple_domains() - { - $tenant = Tenant::create(['foo.localhost', 'bar.localhost']); - $this->assertSame(['foo.localhost', 'bar.localhost'], $tenant->domains); - $this->assertSame($tenant->id, Tenancy::findByDomain('foo.localhost')->id); - $this->assertSame($tenant->id, Tenancy::findByDomain('bar.localhost')->id); - } - - /** @test */ - public function updating_a_tenant_works() - { - $id = 'abc' . $this->randomString(); - $tenant = Tenant::create(['foo.localhost'], ['id' => $id]); - $tenant->foo = 'bar'; - $tenant->save(); - $this->assertEquals(['id' => $id, 'foo' => 'bar', '_tenancy_db_name' => $tenant->database()->getName()], $tenant->data); - $this->assertEquals(['id' => $id, 'foo' => 'bar', '_tenancy_db_name' => $tenant->database()->getName()], tenancy()->find($id)->data); - - $tenant->addDomains('abc.localhost'); - $tenant->save(); - $this->assertEqualsCanonicalizing(['foo.localhost', 'abc.localhost'], $tenant->domains); - $this->assertEqualsCanonicalizing(['foo.localhost', 'abc.localhost'], tenancy()->find($id)->domains); - - $tenant->removeDomains(['foo.localhost']); - $tenant->save(); - $this->assertEqualsCanonicalizing(['abc.localhost'], $tenant->domains); - $this->assertEqualsCanonicalizing(['abc.localhost'], tenancy()->find($id)->domains); - - $tenant->withDomains(['completely.localhost', 'different.localhost', 'domains.localhost']); - $tenant->save(); - $this->assertEqualsCanonicalizing(['completely.localhost', 'different.localhost', 'domains.localhost'], $tenant->domains); - $this->assertEqualsCanonicalizing(['completely.localhost', 'different.localhost', 'domains.localhost'], tenancy()->find($id)->domains); - } - - /** @test */ - public function with_methods_work() - { - $id = 'foo' . $this->randomString(); - $tenant = Tenant::new()->withDomains(['foo.localhost'])->with('id', $id); - $this->assertSame($id, $tenant->id); - - $id2 = 'bar' . $this->randomString(); - $tenant2 = Tenant::new()->withDomains(['bar.localhost'])->withId($id2)->withFooBar('xyz'); - $this->assertSame($id2, $tenant2->data['id']); - $this->assertSame('xyz', $tenant2->foo_bar); - $this->assertArrayHasKey('foo_bar', $tenant2->data); - } - - /** @test */ - public function an_exception_is_thrown_when_an_unknown_method_is_called() - { - $tenant = Tenant::new(); - $this->expectException(\BadMethodCallException::class); - $tenant->sdjigndfgnjdfgj(); - } - - /** @test */ - public function tenant_data_can_be_set_during_creation() - { - Tenant::new()->withData(['foo' => 'bar'])->save(); - - $data = tenancy()->all()->first()->data; - unset($data['id']); - unset($data['_tenancy_db_name']); - - $this->assertSame(['foo' => 'bar'], $data); - } - - /** @test */ - public function run_method_works() - { - $this->assertSame(null, tenancy()->getTenant()); - - $users_table_empty = function () { - return count(\DB::table('users')->get()) === 0; - }; - - $tenant = Tenant::new()->save(); - \Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant->id], - ]); - tenancy()->initialize($tenant); - $this->assertTrue($users_table_empty()); - tenancy()->end(); - - $foo = $tenant->run(function () { - \DB::table('users')->insert([ - 'name' => 'foo', - 'email' => 'foo@bar.xy', - 'password' => bcrypt('secret'), - ]); - - return 'foo'; - }); - - // test return value - $this->assertSame('foo', $foo); - - // test that tenancy was ended - $this->assertSame(false, tenancy()->initialized); - $this->assertSame(null, tenancy()->getTenant()); - - // test closure - tenancy()->initialize($tenant); - $this->assertFalse($users_table_empty()); - - // test returning to original tenant - $tenant2 = Tenant::new()->save(); - \Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant2->id], - ]); - - tenancy()->initialize($tenant2); - $this->assertSame($tenant2, tenancy()->getTenant()); - $this->assertTrue($users_table_empty()); - - $tenant->run(function () { - \DB::table('users')->insert([ - 'name' => 'bar', - 'email' => 'bar@bar.xy', - 'password' => bcrypt('secret'), - ]); - }); - - $this->assertSame($tenant2, tenancy()->getTenant()); - - $this->assertSame(2, $tenant->run(function () { - return \DB::table('users')->count(); - })); - - // test that the tenant variable can be accessed - $this->assertSame($tenant->id, $tenant->run(function ($tenant) { - return $tenant->id; - })); - } - - /** @test */ - public function extra_config_is_merged_into_the_connection_config_array() - { - $tenant = Tenant::new()->withData([ - '_tenancy_db_link' => 'foo', - '_tenancy_db_name' => 'dbname', - '_tenancy_db_username' => 'usernamefoo', - '_tenancy_db_password' => 'passwordfoo', - '_tenancy_db_connection' => 'mysql', - ]); - - config(['database.connections.mysql' => [ - 'driver' => 'mysql', - 'url' => null, - 'host' => 'mysql', - 'port' => '3306', - 'database' => 'main', - 'username' => 'root', - 'password' => 'password', - 'unix_socket' => '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => [], - ]]); - - $this->assertEquals([ - 'database' => 'dbname', - 'username' => 'usernamefoo', - 'password' => 'passwordfoo', - 'link' => 'foo', - - 'driver' => 'mysql', - 'url' => null, - 'host' => 'mysql', - 'port' => '3306', - 'unix_socket' => '', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'prefix' => '', - 'prefix_indexes' => true, - 'strict' => true, - 'engine' => null, - 'options' => [], - ], $tenant->database()->connection()); - } -} diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php index 7c4ff514..c078b231 100644 --- a/tests/TenantDatabaseManagerTest.php +++ b/tests/TenantDatabaseManagerTest.php @@ -2,44 +2,50 @@ declare(strict_types=1); -namespace Stancl\Tenancy\Tests; +namespace Stancl\Tenancy\Tests\v3; -use Illuminate\Support\Facades\Queue; -use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator; -use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter; -use Stancl\Tenancy\Tenant; +use Illuminate\Support\Facades\Event; +use Stancl\Tenancy\Database\Models\Tenant; +use Stancl\Tenancy\DatabaseManager; +use Stancl\Tenancy\Events\Listeners\BootstrapTenancy; +use Stancl\Tenancy\Events\Listeners\JobPipeline; +use Stancl\Tenancy\Events\TenancyInitialized; +use Stancl\Tenancy\Events\TenantCreated; +use Stancl\Tenancy\Jobs\CreateDatabase; +use Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager; use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager; +use Stancl\Tenancy\Tests\TestCase; class TenantDatabaseManagerTest extends TestCase { - public $autoInitTenancy = false; - /** * @test * @dataProvider database_manager_provider */ public function databases_can_be_created_and_deleted($driver, $databaseManager) { - if (! $this->isContainerized()) { - $this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.'); - } + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); config()->set([ "tenancy.database_managers.$driver" => $databaseManager, + 'tenancy.internal_prefix' => 'tenancy_', ]); $name = 'db' . $this->randomString(); - $tenant = Tenant::new()->withData([ - '_tenancy_db_name' => $name, - '_tenancy_db_connection' => $driver, - ]); $this->assertFalse(app($databaseManager)->databaseExists($name)); - $tenant->save(); // generate credentials & create DB + + $tenant = Tenant::create([ + 'tenancy_db_name' => $name, + 'tenancy_db_connection' => $driver, + ]); + $this->assertTrue(app($databaseManager)->databaseExists($name)); app($databaseManager)->deleteDatabase($tenant); $this->assertFalse(app($databaseManager)->databaseExists($name)); @@ -50,60 +56,34 @@ class TenantDatabaseManagerTest extends TestCase { $this->assertSame('central', config('database.default')); + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + // todo if the prefix is _tenancy_, this blows up. write a tenantmodel test that the prefix can be _tenancy_ + config(['tenancy.internal_prefix' => 'tenancy_',]); + $database = 'db' . $this->randomString(); - $tenant = Tenant::new()->withData([ - '_tenancy_db_name' => $database, - '_tenancy_db_connection' => 'mysql', - ]); $this->assertFalse(app(MySQLDatabaseManager::class)->databaseExists($database)); - $tenant->save(); // create DB + Tenant::create([ + 'tenancy_db_name' => $database, + 'tenancy_db_connection' => 'mysql', + ]); + $this->assertTrue(app(MySQLDatabaseManager::class)->databaseExists($database)); $database = 'db' . $this->randomString(); - $tenant = Tenant::new()->withData([ - '_tenancy_db_name' => $database, - '_tenancy_db_connection' => 'pgsql', - ]); - $this->assertFalse(app(PostgreSQLDatabaseManager::class)->databaseExists($database)); - $tenant->save(); // create DB + + Tenant::create([ + 'tenancy_db_name' => $database, + 'tenancy_db_connection' => 'pgsql', + ]); + $this->assertTrue(app(PostgreSQLDatabaseManager::class)->databaseExists($database)); } - /** - * @test - * @dataProvider database_manager_provider - */ - public function databases_can_be_created_and_deleted_using_queued_commands($driver, $databaseManager) - { - if (! $this->isContainerized()) { - $this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.'); - } - - config()->set([ - 'database.default' => $driver, - "tenancy.database_managers.$driver" => $databaseManager, - ]); - - $name = 'db' . $this->randomString(); - $tenant = Tenant::new()->withData([ - '_tenancy_db_name' => $name, - '_tenancy_db_connection' => $driver, - ]); - $tenant->database()->makeCredentials(); - - $this->assertFalse(app($databaseManager)->databaseExists($name)); - $job = new QueuedTenantDatabaseCreator(app($databaseManager), $tenant); - - $job->handle(); - $this->assertTrue(app($databaseManager)->databaseExists($name)); - - $job = new QueuedTenantDatabaseDeleter(app($databaseManager), $tenant); - $job->handle(); - $this->assertFalse(app($databaseManager)->databaseExists($name)); - } - public function database_manager_provider() { return [ @@ -116,30 +96,49 @@ class TenantDatabaseManagerTest extends TestCase } /** @test */ - public function database_creation_can_be_queued() + public function db_name_is_prefixed_with_db_path_when_sqlite_is_used() { - Queue::fake(); - - config()->set([ - 'tenancy.queue_database_creation' => true, + if (file_exists(database_path('foodb'))) { + unlink(database_path('foodb')); // cleanup + } + config([ + 'database.connections.fooconn.driver' => 'sqlite', + 'tenancy.internal_prefix' => 'tenancy_', ]); - Tenant::create(['test2.localhost']); - Queue::assertPushed(QueuedTenantDatabaseCreator::class); + $tenant = Tenant::create([ + 'tenancy_db_name' => 'foodb', + 'tenancy_db_connection' => 'fooconn', + ]); + app(DatabaseManager::class)->createTenantConnection($tenant); + + $this->assertSame(config('database.connections.tenant.database'), database_path('foodb')); } /** @test */ - public function database_deletion_can_be_queued() + public function schema_manager_uses_schema_to_separate_tenant_dbs() { - Queue::fake(); - - $tenant = Tenant::create(['test2.localhost']); - config()->set([ - 'tenancy.queue_database_deletion' => true, - 'tenancy.delete_database_after_tenant_deletion' => true, + config([ + 'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, + 'tenancy.boostrappers' => [ + DatabaseTenancyBootstrapper::class, + ], ]); - $tenant->delete(); - Queue::assertPushed(QueuedTenantDatabaseDeleter::class); + Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + Event::listen(TenancyInitialized::class, BootstrapTenancy::class); + + $originalDatabaseName = config(['database.connections.pgsql.database']); + + $tenant = Tenant::create([ + 'tenancy_db_connection' => 'pgsql', + ]); + tenancy()->initialize($tenant); + + $this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema')); + $this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database'])); } } diff --git a/tests/TenantManagerEventsTest.php b/tests/TenantManagerEventsTest.php deleted file mode 100644 index 4dde735c..00000000 --- a/tests/TenantManagerEventsTest.php +++ /dev/null @@ -1,138 +0,0 @@ -withDomains(['foo.localhost'])->save()['id']; - - Tenancy::eventListener('bootstrapping', function ($tenantManager) use ($id) { - if ($tenantManager->getTenant('id') === $id) { - config(['tenancy.foo' => 'bar']); - } - }); - - $this->assertSame(null, config('tenancy.foo')); - tenancy()->init('foo.localhost'); - $this->assertSame('bar', config('tenancy.foo')); - } - - /** @test */ - public function bootstrapped_event_works() - { - $id = Tenant::new()->withDomains(['foo.localhost'])->save()['id']; - - Tenancy::eventListener('bootstrapped', function ($tenantManager) use ($id) { - if ($tenantManager->getTenant('id') === $id) { - config(['tenancy.foo' => 'bar']); - } - }); - - $this->assertSame(null, config('tenancy.foo')); - tenancy()->init('foo.localhost'); - $this->assertSame('bar', config('tenancy.foo')); - } - - /** @test */ - public function ending_event_works() - { - $id = Tenant::new()->withDomains(['foo.localhost'])->save()['id']; - - Tenancy::eventListener('ending', function ($tenantManager) use ($id) { - if ($tenantManager->getTenant('id') === $id) { - config(['tenancy.foo' => 'bar']); - } - }); - - $this->assertSame(null, config('tenancy.foo')); - tenancy()->init('foo.localhost'); - $this->assertSame(null, config('tenancy.foo')); - tenancy()->endTenancy(); - $this->assertSame('bar', config('tenancy.foo')); - } - - /** @test */ - public function ended_event_works() - { - Tenant::new()->withDomains(['foo.localhost'])->save()['id']; - - Tenancy::eventListener('ended', function ($tenantManager) { - config(['tenancy.foo' => 'bar']); - }); - - $this->assertSame(null, config('tenancy.foo')); - tenancy()->init('foo.localhost'); - $this->assertSame(null, config('tenancy.foo')); - tenancy()->endTenancy(); - $this->assertSame('bar', config('tenancy.foo')); - } - - /** @test */ - public function database_can_be_reconnected_using_event_hooks() - { - config(['database.connections.tenantabc' => [ - 'driver' => 'sqlite', - 'database' => database_path('some_special_database.sqlite'), - ]]); - - $id = Tenant::create('abc.localhost')['id']; - - Tenancy::eventListener('bootstrapping', function ($tenancy) use ($id) { - if ($tenancy->getTenant()['id'] === $id) { - $tenancy->database->switchConnection('tenantabc'); - - return ['database']; - } - }); - - $this->assertNotSame('tenantabc', \DB::connection()->getConfig()['name']); - tenancy()->init('abc.localhost'); - $this->assertSame('tenantabc', \DB::connection()->getConfig()['name']); - } - - /** @test */ - public function database_cannot_be_reconnected_without_using_prevents() - { - config(['database.connections.tenantabc' => [ - 'driver' => 'sqlite', - 'database' => database_path('some_special_database.sqlite'), - ]]); - - $id = Tenant::create('abc.localhost')['id']; - - Tenancy::eventListener('bootstrapping', function ($tenancy) use ($id) { - if ($tenancy->getTenant()['id'] === $id) { - $tenancy->database->switchConnection('tenantabc'); - // return ['database']; - } - }); - - $this->assertNotSame('tenantabc', \DB::connection()->getConfig()['name']); - tenancy()->init('abc.localhost'); - $this->assertSame('tenant', \DB::connection()->getConfig()['name']); - } - - /** @test */ - public function tenant_is_persisted_before_the_created_hook_is_called() - { - $was_persisted = false; - - Tenancy::eventListener('tenant.created', function ($tenancy, $tenant) use (&$was_persisted) { - $was_persisted = $tenant->persisted; - }); - - Tenant::new()->save(); - - $this->assertTrue($was_persisted); - } -} diff --git a/tests/TenantManagerTest.php b/tests/TenantManagerTest.php deleted file mode 100644 index 3198123b..00000000 --- a/tests/TenantManagerTest.php +++ /dev/null @@ -1,383 +0,0 @@ -withDomains(['test2.localhost'])->save(); - - tenancy()->init('test2.localhost'); - - $this->assertEquals($tenant, tenancy()->getTenant()); - } - - /** @test */ - public function initById_works() - { - $tenant = Tenant::new()->withDomains(['foo.localhost'])->save(); - - $this->assertNotEquals($tenant, tenancy()->getTenant()); - - tenancy()->initById($tenant['id']); - - $this->assertEquals($tenant, tenancy()->getTenant()); - } - - /** @test */ - public function findByDomain_works() - { - $tenant = Tenant::new()->withDomains(['foo.localhost'])->save(); - - $this->assertEquals($tenant, tenancy()->findByDomain('foo.localhost')); - } - - /** @test */ - public function find_works() - { - Tenant::new()->withDomains(['dev.localhost'])->save(); - tenancy()->init('dev.localhost'); - - $this->assertEquals(tenant(), tenancy()->find(tenant('id'))); - } - - /** @test */ - public function findByDomain_throws_an_exception_when_an_unused_domain_is_supplied() - { - $this->expectException(\Exception::class); - tenancy()->findByDomain('nonexistent.domain'); - } - - /** @test */ - public function tenancy_can_be_ended() - { - $originals = [ - 'databaseName' => DB::connection()->getDatabaseName(), - 'storage_path' => storage_path(), - 'storage_root' => Storage::disk('local')->getAdapter()->getPathPrefix(), - 'cache' => app('cache'), - ]; - - // Verify that these assertions are the right way for testing this - $this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName()); - $this->assertSame($originals['storage_path'], storage_path()); - $this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix()); - $this->assertSame($originals['cache'], app('cache')); - - Tenant::new()->withDomains(['foo.localhost'])->save(); - tenancy()->init('foo.localhost'); - - $this->assertNotSame($originals['databaseName'], DB::connection()->getDatabaseName()); - $this->assertNotSame($originals['storage_path'], storage_path()); - $this->assertNotSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix()); - $this->assertNotSame($originals['cache'], app('cache')); - - tenancy()->endTenancy(); - - $this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName()); - $this->assertSame($originals['storage_path'], storage_path()); - $this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix()); - $this->assertSame($originals['cache'], app('cache')); - } - - /** @test */ - public function tenancy_can_be_ended_after_reidentification() - { - $originals = [ - 'databaseName' => DB::connection()->getDatabaseName(), - 'storage_path' => storage_path(), - 'storage_root' => Storage::disk('local')->getAdapter()->getPathPrefix(), - 'cache' => app('cache'), - ]; - - Tenant::new()->withDomains(['foo.localhost'])->save(); - tenancy()->init('foo.localhost'); - - $this->assertNotSame($originals['databaseName'], DB::connection()->getDatabaseName()); - $this->assertNotSame($originals['storage_path'], storage_path()); - $this->assertNotSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix()); - $this->assertNotSame($originals['cache'], app('cache')); - - tenancy()->endTenancy(); - - $this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName()); - $this->assertSame($originals['storage_path'], storage_path()); - $this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix()); - $this->assertSame($originals['cache'], app('cache')); - - // Reidentify tenant - Tenant::new()->withDomains(['bar.localhost'])->save(); - tenancy()->init('bar.localhost'); - - $this->assertNotSame($originals['databaseName'], DB::connection()->getDatabaseName()); - $this->assertNotSame($originals['storage_path'], storage_path()); - $this->assertNotSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix()); - $this->assertNotSame($originals['cache'], app('cache')); - - tenancy()->endTenancy(); - - $this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName()); - $this->assertSame($originals['storage_path'], storage_path()); - $this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix()); - $this->assertSame($originals['cache'], app('cache')); - } - - /** @test */ - public function tenant_can_be_deleted() - { - $tenant = Tenant::new()->withDomains(['foo.localhost'])->save(); - $this->assertEquals([$tenant], tenancy()->all()->toArray()); - $tenant->delete(); - $this->assertEquals([], tenancy()->all()->toArray()); - } - - /** @test */ - public function all_returns_a_list_of_all_tenants() - { - $tenant1 = Tenant::new()->withDomains(['foo.localhost'])->save(); - $tenant2 = Tenant::new()->withDomains(['bar.localhost'])->save(); - $this->assertEqualsCanonicalizing([$tenant1, $tenant2], tenancy()->all()->toArray()); - } - - /** @test */ - public function data_can_be_passed_in_the_create_method() - { - $data = ['plan' => 'free', 'subscribed_until' => '2020-01-01']; - $tenant = Tenant::create(['foo.localhost'], $data); - - $tenant_data = $tenant->data; - unset($tenant_data['id']); - unset($tenant_data['_tenancy_db_name']); - - $this->assertSame($data, $tenant_data); - } - - /** @test */ - public function database_name_can_be_passed_in_the_create_method() - { - $database = 'abc' . $this->randomString(); - - $tenant = tenancy()->create(['foo.localhost'], [ - '_tenancy_db_name' => $database, - ]); - - $this->assertSame($database, $tenant->database()->getName()); - } - - /** @test */ - public function id_cannot_be_changed() - { - $tenant = Tenant::create(['test2.localhost']); - - $this->expectException(\Stancl\Tenancy\Exceptions\TenantStorageException::class); - $tenant->id = 'bar'; - - $tenant2 = Tenant::create(['test3.localhost']); - - $this->expectException(\Stancl\Tenancy\Exceptions\TenantStorageException::class); - $tenant2->put('id', 'foo'); - } - - /** @test */ - public function all_returns_a_collection_of_tenant_objects() - { - Tenant::create('foo.localhost'); - $this->assertSame('Tenant', class_basename(tenancy()->all()[0])); - } - - /** @test */ - public function Tenant_is_bound_correctly_to_the_service_container() - { - $this->assertSame(null, app(Tenant::class)); - $tenant = Tenant::create(['foo.localhost']); - app(TenantManager::class)->initializeTenancy($tenant); - $this->assertSame($tenant->id, app(Tenant::class)->id); - $this->assertSame(app(Tenant::class), app(TenantManager::class)->getTenant()); - app(TenantManager::class)->endTenancy(); - $this->assertSame(app(Tenant::class), app(TenantManager::class)->getTenant()); - } - - /** @test */ - public function id_can_be_supplied_during_creation() - { - $id = 'abc' . $this->randomString(); - $this->assertSame($id, Tenant::create(['foo.localhost'], ['id' => $id])->id); - $this->assertTrue(tenancy()->all()->contains(function ($tenant) use ($id) { - return $tenant->id === $id; - })); - } - - /** @test */ - public function automatic_migrations_work() - { - $tenant = Tenant::create(['foo.localhost']); - tenancy()->initialize($tenant); - $this->assertFalse(\Schema::hasTable('users')); - - config(['tenancy.migrate_after_creation' => true]); - $tenant2 = Tenant::create(['bar.localhost']); - tenancy()->initialize($tenant2); - $this->assertTrue(\Schema::hasTable('users')); - } - - /** @test */ - public function automatic_seeding_works() - { - config(['tenancy.migrate_after_creation' => true]); - - $tenant = Tenant::create(['foo.localhost']); - tenancy()->initialize($tenant); - $this->assertSame(0, \DB::table('users')->count()); - - config([ - 'tenancy.seed_after_migration' => true, - 'tenancy.seeder_parameters' => [ - '--class' => ExampleSeeder::class, - ], - ]); - - $tenant2 = Tenant::create(['bar.localhost']); - tenancy()->initialize($tenant2); - $this->assertSame(1, \DB::table('users')->count()); - } - - /** @test */ - public function ensureTenantCanBeCreated_works() - { - $id = 'foo' . $this->randomString(); - Tenant::create(['foo.localhost'], ['id' => $id]); - $this->expectException(DomainsOccupiedByOtherTenantException::class); - Tenant::create(['foo.localhost']); - - $this->expectException(TenantWithThisIdAlreadyExistsException::class); - Tenant::create(['bar.localhost'], ['id' => $id]); - } - - /** @test */ - public function automigration_is_queued_when_db_creation_is_queued() - { - Queue::fake(); - - config([ - 'tenancy.queue_database_creation' => true, - 'tenancy.migrate_after_creation' => true, - ]); - - $tenant = Tenant::new()->save(); - - Queue::assertPushedWithChain(QueuedTenantDatabaseCreator::class, [ - QueuedTenantDatabaseMigrator::class, - ]); - - // foreach (Queue::pushedJobs() as $job) { - // $job[0]['job']->handle(); // this doesn't execute the chained job - // } - // tenancy()->initialize($tenant); - // $this->assertTrue(\Schema::hasTable('users')); - } - - /** @test */ - public function autoseeding_is_queued_when_db_creation_is_queued() - { - Queue::fake(); - - config([ - 'tenancy.queue_database_creation' => true, - 'tenancy.migrate_after_creation' => true, - 'tenancy.seed_after_migration' => true, - ]); - - Tenant::new()->save(); - - Queue::assertPushedWithChain(QueuedTenantDatabaseCreator::class, [ - QueuedTenantDatabaseMigrator::class, - QueuedTenantDatabaseSeeder::class, - ]); - } - - /** @test */ - public function TenantDoesNotExistException_is_thrown_when_find_is_called_on_an_id_that_does_not_belong_to_any_tenant() - { - $this->expectException(TenantDoesNotExistException::class); - tenancy()->find('gjnfdgf'); - } - - /** @test */ - public function event_listeners_can_accept_arguments() - { - tenancy()->hook('tenant.creating', function ($tenantManager, $tenant) { - $this->assertSame('bar', $tenant->foo); - }); - - Tenant::new()->withData(['foo' => 'bar'])->save(); - } - - /** @test */ - public function tenant_creating_hook_can_be_used_to_modify_tenants_data() - { - tenancy()->hook('tenant.creating', function ($tm, Tenant $tenant) { - $tenant->put([ - 'foo' => 'bar', - 'abc123' => 'def456', - ]); - }); - $tenant = Tenant::new()->save(); - - $this->assertArrayHasKey('foo', $tenant->data); - $this->assertArrayHasKey('abc123', $tenant->data); - } - - /** @test */ - public function database_creation_can_be_disabled() - { - config(['tenancy.create_database' => false]); - - tenancy()->hook('database.creating', function () { - $this->fail(); - }); - - $tenant = Tenant::new()->save(); - - $this->assertTrue(true); - } - - /** @test */ - public function database_creation_can_be_disabled_for_specific_tenants() - { - config(['tenancy.create_database' => true]); - - tenancy()->hook('database.creating', function () { - $this->assertTrue(true); - }); - - $tenant = Tenant::new()->save(); - - tenancy()->hook('database.creating', function () { - $this->fail(); - }); - - $tenant2 = Tenant::new()->withData([ - '_tenancy_create_database' => false, - ])->save(); - } -} diff --git a/tests/v3/TenantModelTest.php b/tests/TenantModelTest.php similarity index 91% rename from tests/v3/TenantModelTest.php rename to tests/TenantModelTest.php index bf67f851..e99b8337 100644 --- a/tests/v3/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -12,6 +12,7 @@ use Stancl\Tenancy\Events\TenantCreated; use Stancl\Tenancy\Tests\TestCase; use Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator; use Stancl\Tenancy\Contracts; +use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; class TenantModelTest extends TestCase { @@ -34,7 +35,7 @@ class TenantModelTest extends TestCase tenancy()->initialize($tenant); - $this->assertSame($tenant->id, app(Tenant::class)->id); + $this->assertSame($tenant->id, app(Contracts\Tenant::class)->id); tenancy()->end(); @@ -100,10 +101,10 @@ class TenantModelTest extends TestCase $table->bigIncrements('id')->change(); }); - config(['tenancy.id_generator' => null]); + unset(app()[UniqueIdentifierGenerator::class]); - $tenant1 = Tenant::create(); - $tenant2 = Tenant::create(); + $tenant1 = MyTenant::create(); + $tenant2 = MyTenant::create(); $this->assertSame(1, $tenant1->id); $this->assertSame(2, $tenant2->id); @@ -137,6 +138,7 @@ class TenantModelTest extends TestCase class MyTenant extends Tenant { protected $table = 'tenants'; + public $increments = true; } class AnotherTenant extends Model implements Contracts\Tenant @@ -153,4 +155,9 @@ class AnotherTenant extends Model implements Contracts\Tenant { return $this->getAttribute('id'); } + + public function run(callable $callback) + { + $callback(); + } } diff --git a/tests/TenantStorageTest.php b/tests/TenantStorageTest.php deleted file mode 100644 index 65354129..00000000 --- a/tests/TenantStorageTest.php +++ /dev/null @@ -1,197 +0,0 @@ -withDomains(['abc.localhost'])->save(); - $exists = function () use ($abc) { - return tenancy()->all()->contains(function ($tenant) use ($abc) { - return $tenant->id === $abc->id; - }); - }; - - $this->assertTrue($exists()); - - $abc->delete(); - - $this->assertFalse($exists()); - } - - /** @test */ - public function set_is_a_working_alias_for_put() - { - tenant()->set('foo', 'bar'); - - $this->assertSame('bar', tenant()->get('foo')); - } - - /** @test */ - public function put_works_with_key_and_value_as_separate_args() - { - tenant()->put('foo', 'bar'); - - $this->assertSame('bar', tenant()->get('foo')); - } - - /** @test */ - public function put_works_with_key_and_value_as_a_single_arg() - { - $keys = ['foo', 'abc']; - $vals = ['bar', 'xyz']; - $data = array_combine($keys, $vals); - - tenant()->put($data); - - $this->assertSame($data, tenant()->get($keys)); - } - - /** @test */ - public function put_on_the_current_tenant_pushes_the_value_into_the_tenant_property_array() - { - tenant()->put('foo', 'bar'); - - $this->assertSame('bar', tenancy()->getTenant('foo')); - } - - /** @test */ - public function arrays_can_be_stored() - { - tenant()->put('foo', [1, 2]); - - $this->assertSame([1, 2], tenant()->get('foo')); - } - - /** @test */ - public function associative_arrays_can_be_stored() - { - $data = ['a' => 'b', 'c' => 'd']; - tenant()->put('foo', $data); - - $this->assertSame($data, tenant()->get('foo')); - } - - /** @test */ - public function correct_storage_driver_is_used() - { - if (config('tenancy.storage_driver') == 'db') { - $this->assertSame('DatabaseStorageDriver', class_basename(tenancy()->storage)); - } elseif (config('tenancy.storage_driver') == 'redis') { - $this->assertSame('RedisStorageDriver', class_basename(tenancy()->storage)); - } else { - dd(class_basename(config('tenancy.storage_driver'))); - } - } - - /** @test */ - public function data_is_stored_with_correct_data_types() - { - tenant()->put('someBool', false); - $this->assertSame('boolean', gettype(tenant()->get('someBool'))); - $this->assertSame('boolean', gettype(tenant()->get(['someBool'])['someBool'])); - - tenant()->put('someInt', 5); - $this->assertSame('integer', gettype(tenant()->get('someInt'))); - $this->assertSame('integer', gettype(tenant()->get(['someInt'])['someInt'])); - - tenant()->put('someDouble', 11.40); - $this->assertSame('double', gettype(tenant()->get('someDouble'))); - $this->assertSame('double', gettype(tenant()->get(['someDouble'])['someDouble'])); - - tenant()->put('string', 'foo'); - $this->assertSame('string', gettype(tenant()->get('string'))); - $this->assertSame('string', gettype(tenant()->get(['string'])['string'])); - } - - /** @test */ - public function tenant_repository_uses_correct_connection() - { - config(['database.connections.foo' => config('database.connections.sqlite')]); - config(['tenancy.storage_drivers.db.connection' => 'foo']); - $this->assertSame('foo', app(TenantRepository::class)->database->getName()); - } - - /** @test */ - public function retrieving_data_without_cache_works() - { - Tenant::new()->withDomains(['foo.localhost'])->save(); - tenancy()->init('foo.localhost'); - - tenant()->put('foo', 'bar'); - $this->assertSame('bar', tenant()->get('foo')); - $this->assertSame(['foo' => 'bar'], tenant()->get(['foo'])); - - tenancy()->endTenancy(); - tenancy()->init('foo.localhost'); - $this->assertSame('bar', tenant()->get('foo')); - $this->assertSame(['foo' => 'bar'], tenant()->get(['foo'])); - } - - /** @test */ - public function custom_columns_work_with_db_storage_driver() - { - if (config('tenancy.storage_driver') != 'db') { - $this->markTestSkipped(); - } - - tenancy()->endTenancy(); - - $this->loadMigrationsFrom([ - '--path' => __DIR__ . '/Etc', - '--database' => 'central', - ]); - config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom - - config(['tenancy.storage_drivers.db.custom_columns' => [ - 'foo', - ]]); - - tenancy()->create(['foo.localhost']); - tenancy()->init('foo.localhost'); - - tenant()->put('foo', '111'); - $this->assertSame('111', tenant()->get('foo')); - - tenant()->put(['foo' => 'bar', 'abc' => 'xyz']); - $this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc'])); - - $this->assertSame('bar', \DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo); - } - - /** @test */ - public function custom_columns_can_be_used_on_tenant_create() - { - if (config('tenancy.storage_driver') != 'db') { - $this->markTestSkipped(); - } - - tenancy()->endTenancy(); - - $this->loadMigrationsFrom([ - '--path' => __DIR__ . '/Etc', - '--database' => 'central', - ]); - config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom - - config(['tenancy.storage_drivers.db.custom_columns' => [ - 'foo', - ]]); - - tenancy()->create(['foo.localhost'], ['foo' => 'bar', 'abc' => 'xyz']); - tenancy()->init('foo.localhost'); - - $this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc'])); - $this->assertSame('bar', \DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo); - } -} diff --git a/tests/Traits/TenantAwareCommandTest.php b/tests/Traits/TenantAwareCommandTest.php deleted file mode 100644 index fcc3d0e2..00000000 --- a/tests/Traits/TenantAwareCommandTest.php +++ /dev/null @@ -1,35 +0,0 @@ -save(); - $tenant2 = Tenant::new()->save(); - \Artisan::call('tenants:migrate', [ - '--tenants' => [$tenant1['id'], $tenant2['id']], - ]); - - $this->artisan('user:add') - ->assertExitCode(0); - - tenancy()->initializeTenancy($tenant1); - $this->assertNotEmpty(\DB::table('users')->get()); - tenancy()->end(); - - tenancy()->initializeTenancy($tenant2); - $this->assertNotEmpty(\DB::table('users')->get()); - tenancy()->end(); - } -} diff --git a/tests/v3/DatabasePreparationTest.php b/tests/v3/DatabasePreparationTest.php deleted file mode 100644 index 4bd4834b..00000000 --- a/tests/v3/DatabasePreparationTest.php +++ /dev/null @@ -1 +0,0 @@ -// test DB creation, migration, seeding \ No newline at end of file diff --git a/tests/v3/QueueTest.php b/tests/v3/QueueTest.php deleted file mode 100644 index 2fdb4da9..00000000 --- a/tests/v3/QueueTest.php +++ /dev/null @@ -1,136 +0,0 @@ - [ - QueueTenancyBootstrapper::class, - ], - 'queue.default' => 'redis', - ]); - - Event::listen(TenancyInitialized::class, BootstrapTenancy::class); - - $this->valuestore = Valuestore::make(__DIR__ . '/../Etc/tmp/queuetest.json')->flush(); - } - - /** @test */ - public function tenant_id_is_passed_to_tenant_queues() - { - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - Event::fake(); - - dispatch(new TestJob($this->valuestore)); - - Event::assertDispatched(JobProcessing::class, function ($event) { - return $event->job->payload()['tenant_id'] === tenant('id'); - }); - } - - /** @test */ - public function tenant_id_is_not_passed_to_central_queues() - { - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - Event::fake(); - - config(['queue.connections.central' => [ - 'driver' => 'sync', - 'central' => true, - ]]); - - dispatch(new TestJob($this->valuestore))->onConnection('central'); - - Event::assertDispatched(JobProcessing::class, function ($event) { - return ! isset($event->job->payload()['tenant_id']); - }); - } - - /** @test */ - public function tenancy_is_initialized_inside_queues() - { - $tenant = Tenant::create([ - 'id' => 'acme', - ]); - - tenancy()->initialize($tenant); - - dispatch(new TestJob($this->valuestore)); - - $this->assertFalse($this->valuestore->has('tenant_id')); - $this->artisan('queue:work --once'); - - $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); - } - - /** @test */ - public function the_tenant_used_by_the_job_doesnt_change_when_the_current_tenant_changes() - { - $tenant1 = Tenant::create([ - 'id' => 'acme', - ]); - - tenancy()->initialize($tenant1); - - dispatch(new TestJob($this->valuestore)); - - $tenant2 = Tenant::create([ - 'id' => 'foobar', - ]); - - tenancy()->initialize($tenant2); - - $this->assertFalse($this->valuestore->has('tenant_id')); - $this->artisan('queue:work --once'); - - $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id')); - } -} - -class TestJob implements ShouldQueue -{ - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - /** @var Valuestore */ - protected $valuestore; - - public function __construct(Valuestore $valuestore) - { - $this->valuestore = $valuestore; - } - - public function handle() - { - $this->valuestore->put('tenant_id', "The current tenant id is: " . tenant('id')); - } -}