diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03aa4ee8..0095be7e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,10 +10,16 @@ Run `composer docker-up` to start the containers. Then run `composer test` to ru If you need to pass additional flags to phpunit, use `./test --foo` instead of `composer test --foo`. Composer scripts unfortunately don't pass CLI arguments. -If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'`. +If you want to run a specific test (or test file), you can also use `./t 'name of the test'`. This is equivalent to `./test --no-coverage --filter 'name of the test'` (`--no-coverage` speeds up the execution time). When you're done testing, run `composer docker-down` to shut down the containers. +### Debugging tests + +If you're developing some feature and you encounter `SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry` errors, it's likely that some PHP errors were thrown in past test runs and prevented the test cleanup from running properly. + +To fix this, simply delete the database memory by shutting down containers and starting them again: `composer docker-down && composer docker-up`. + ### Docker on M1 Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1. diff --git a/assets/config.php b/assets/config.php index cfde54ac..3778e107 100644 --- a/assets/config.php +++ b/assets/config.php @@ -6,10 +6,29 @@ use Stancl\Tenancy\Middleware; use Stancl\Tenancy\Resolvers; return [ - 'tenant_model' => Stancl\Tenancy\Database\Models\Tenant::class, - 'domain_model' => Stancl\Tenancy\Database\Models\Domain::class, + /** + * Configuration for the models used by Tenancy. + */ + 'models' => [ + 'tenant' => Stancl\Tenancy\Database\Models\Tenant::class, + 'domain' => Stancl\Tenancy\Database\Models\Domain::class, - 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, + /** + * Name of the column used to relate models to tenants. + * + * This is used by the HasDomains trait, and models that use the BelongsToTenant trait (used in single-database tenancy). + */ + 'tenant_key_column' => 'tenant_id', + + /** + * Used for generating tenant IDs. + * + * - Feel free to override this with a custom class that implements the UniqueIdentifierGenerator interface. + * - To use autoincrement IDs, set this to null and update the `tenants` table migration to use an autoincrement column. + * SECURITY NOTE: Keep in mind that autoincrement IDs come with *potential* enumeration issues (such as tenant storage URLs). + */ + 'id_generator' => Stancl\Tenancy\UUIDGenerator::class, + ], /** * The list of domains hosting your central app. @@ -293,12 +312,4 @@ return [ '--class' => 'Database\Seeders\DatabaseSeeder', // root seeder class // '--force' => true, ], - - /** - * Single-database tenancy config. - */ - 'single_db' => [ - /** The name of the column used by models with the BelongsToTenant trait. */ - 'tenant_id_column' => 'tenant_id', - ], ]; diff --git a/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php b/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php index 32597f38..c720160a 100644 --- a/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php +++ b/assets/impersonation-migrations/2020_05_15_000010_create_tenant_user_impersonation_tokens_table.php @@ -5,8 +5,9 @@ declare(strict_types=1); use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use Stancl\Tenancy\Tenancy; -class CreateTenantUserImpersonationTokensTable extends Migration +return new class extends Migration { /** * Run the migrations. @@ -17,13 +18,13 @@ class CreateTenantUserImpersonationTokensTable extends Migration { Schema::create('tenant_user_impersonation_tokens', function (Blueprint $table) { $table->string('token', 128)->primary(); - $table->string('tenant_id'); + $table->string(Tenancy::tenantKeyColumn()); $table->string('user_id'); $table->string('auth_guard'); $table->string('redirect_url'); $table->timestamp('created_at'); - $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); + $table->foreign(Tenancy::tenantKeyColumn())->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade'); }); } @@ -36,4 +37,4 @@ class CreateTenantUserImpersonationTokensTable extends Migration { Schema::dropIfExists('tenant_user_impersonation_tokens'); } -} +}; diff --git a/assets/migrations/2019_09_15_000010_create_tenants_table.php b/assets/migrations/2019_09_15_000010_create_tenants_table.php index ec730651..a923f2c8 100644 --- a/assets/migrations/2019_09_15_000010_create_tenants_table.php +++ b/assets/migrations/2019_09_15_000010_create_tenants_table.php @@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateTenantsTable extends Migration +return new class extends Migration { /** * Run the migrations. @@ -34,4 +34,4 @@ class CreateTenantsTable extends Migration { Schema::dropIfExists('tenants'); } -} +}; diff --git a/assets/migrations/2019_09_15_000020_create_domains_table.php b/assets/migrations/2019_09_15_000020_create_domains_table.php index 17f706c2..ac238830 100644 --- a/assets/migrations/2019_09_15_000020_create_domains_table.php +++ b/assets/migrations/2019_09_15_000020_create_domains_table.php @@ -5,8 +5,9 @@ declare(strict_types=1); use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use Stancl\Tenancy\Tenancy; -class CreateDomainsTable extends Migration +return new class extends Migration { /** * Run the migrations. @@ -18,10 +19,10 @@ class CreateDomainsTable extends Migration Schema::create('domains', function (Blueprint $table) { $table->increments('id'); $table->string('domain', 255)->unique(); - $table->string('tenant_id'); + $table->string(Tenancy::tenantKeyColumn()); $table->timestamps(); - $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade'); + $table->foreign(Tenancy::tenantKeyColumn())->references('id')->on('tenants')->onUpdate('cascade'); }); } @@ -34,4 +35,4 @@ class CreateDomainsTable extends Migration { Schema::dropIfExists('domains'); } -} +}; diff --git a/assets/routes.php b/assets/routes.php index a27f782d..a9c09797 100644 --- a/assets/routes.php +++ b/assets/routes.php @@ -5,6 +5,7 @@ declare(strict_types=1); use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Controllers\TenantAssetController; +// todo make this work with path identification Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset']) ->where('path', '(.*)') ->name('stancl.tenancy.asset'); diff --git a/src/Commands/ClearPendingTenants.php b/src/Commands/ClearPendingTenants.php index 19d31195..82b01cd0 100644 --- a/src/Commands/ClearPendingTenants.php +++ b/src/Commands/ClearPendingTenants.php @@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Builder; class ClearPendingTenants extends Command { protected $signature = 'tenants:pending-clear - {--all : Override the default settings and deletes all pending tenants} {--older-than-days= : Deletes all pending tenants older than the amount of days} {--older-than-hours= : Deletes all pending tenants older than the amount of hours}'; @@ -24,32 +23,25 @@ class ClearPendingTenants extends Command // We compare the original expiration date to the new one to check if the new one is different later $originalExpirationDate = $expirationDate->copy()->toImmutable(); - // Skip the time constraints if the 'all' option is given - if (! $this->option('all')) { - /** @var ?int $olderThanDays */ - $olderThanDays = $this->option('older-than-days'); + $olderThanDays = (int) $this->option('older-than-days'); + $olderThanHours = (int) $this->option('older-than-hours'); - /** @var ?int $olderThanHours */ - $olderThanHours = $this->option('older-than-hours'); + if ($olderThanDays && $olderThanHours) { + $this->line(" Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components + $this->line('Please, choose only one of these options.'); - if ($olderThanDays && $olderThanHours) { - $this->line(" Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components - $this->line('Please, choose only one of these options.'); - - return 1; // Exit code for failure - } - - if ($olderThanDays) { - $expirationDate->subDays($olderThanDays); - } - - if ($olderThanHours) { - $expirationDate->subHours($olderThanHours); - } + return 1; // Exit code for failure } - $deletedTenantCount = tenancy() - ->query() + if ($olderThanDays) { + $expirationDate->subDays($olderThanDays); + } + + if ($olderThanHours) { + $expirationDate->subHours($olderThanHours); + } + + $deletedTenantCount = tenancy()->query() ->onlyPending() ->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) { $query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp); diff --git a/src/Commands/CreatePendingTenants.php b/src/Commands/CreatePendingTenants.php index 7b2c7934..5c255664 100644 --- a/src/Commands/CreatePendingTenants.php +++ b/src/Commands/CreatePendingTenants.php @@ -30,8 +30,8 @@ class CreatePendingTenants extends Command $createdCount++; } - $this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.'); - $this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); + $this->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.'); + $this->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); return 0; } @@ -39,8 +39,7 @@ class CreatePendingTenants extends Command /** Calculate the number of currently available pending tenants. */ protected function getPendingTenantCount(): int { - return tenancy() - ->query() + return tenancy()->query() ->onlyPending() ->count(); } diff --git a/src/Commands/MigrateFreshOverride.php b/src/Commands/MigrateFreshOverride.php index 88e9e21e..f2fd70b0 100644 --- a/src/Commands/MigrateFreshOverride.php +++ b/src/Commands/MigrateFreshOverride.php @@ -5,13 +5,18 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; use Illuminate\Database\Console\Migrations\FreshCommand; +use Illuminate\Support\Facades\Schema; class MigrateFreshOverride extends FreshCommand { public function handle() { if (config('tenancy.database.drop_tenant_databases_on_migrate_fresh')) { - tenancy()->model()::cursor()->each->delete(); + $tenantModel = tenancy()->model(); + + if (Schema::hasTable($tenantModel->getTable())) { + $tenantModel::cursor()->each->delete(); + } } return parent::handle(); diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php index 3f957bdd..c7bd9b99 100644 --- a/src/Commands/TenantDump.php +++ b/src/Commands/TenantDump.php @@ -23,7 +23,7 @@ class TenantDump extends DumpCommand public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int { if (is_null($this->option('path'))) { - $this->input->setOption('path', database_path('schema/tenant-schema.dump')); + $this->input->setOption('path', config('tenancy.migration_parameters.--schema-path') ?? database_path('schema/tenant-schema.dump')); } $tenant = $this->option('tenant') @@ -41,7 +41,7 @@ class TenantDump extends DumpCommand return 1; } - parent::handle($connections, $dispatcher); + $tenant->run(fn () => parent::handle($connections, $dispatcher)); return 0; } diff --git a/src/Concerns/HasTenantOptions.php b/src/Concerns/HasTenantOptions.php index f8a763a7..b558da64 100644 --- a/src/Concerns/HasTenantOptions.php +++ b/src/Concerns/HasTenantOptions.php @@ -23,8 +23,7 @@ trait HasTenantOptions protected function getTenants(): LazyCollection { - return tenancy() - ->query() + return tenancy()->query() ->when($this->option('tenants'), function ($query) { $query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); }) diff --git a/src/Database/Concerns/BelongsToTenant.php b/src/Database/Concerns/BelongsToTenant.php index 07048a1f..ccf87c81 100644 --- a/src/Database/Concerns/BelongsToTenant.php +++ b/src/Database/Concerns/BelongsToTenant.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Database\TenantScope; +use Stancl\Tenancy\Tenancy; /** * @property-read Tenant $tenant @@ -14,12 +15,7 @@ trait BelongsToTenant { public function tenant() { - return $this->belongsTo(config('tenancy.tenant_model'), static::tenantIdColumn()); - } - - public static function tenantIdColumn(): string - { - return config('tenancy.single_db.tenant_id_column'); + return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn()); } public static function bootBelongsToTenant(): void @@ -27,9 +23,9 @@ trait BelongsToTenant static::addGlobalScope(new TenantScope); static::creating(function ($model) { - if (! $model->getAttribute(static::tenantIdColumn()) && ! $model->relationLoaded('tenant')) { + if (! $model->getAttribute(Tenancy::tenantKeyColumn()) && ! $model->relationLoaded('tenant')) { if (tenancy()->initialized) { - $model->setAttribute(static::tenantIdColumn(), tenant()->getTenantKey()); + $model->setAttribute(Tenancy::tenantKeyColumn(), tenant()->getTenantKey()); $model->setRelation('tenant', tenant()); } } diff --git a/src/Database/Concerns/HasDomains.php b/src/Database/Concerns/HasDomains.php index bd512e23..ae3aed42 100644 --- a/src/Database/Concerns/HasDomains.php +++ b/src/Database/Concerns/HasDomains.php @@ -2,11 +2,10 @@ declare(strict_types=1); -// todo not sure if this should be in Database\ - namespace Stancl\Tenancy\Database\Concerns; use Stancl\Tenancy\Contracts\Domain; +use Stancl\Tenancy\Tenancy; /** * @property-read Domain[]|\Illuminate\Database\Eloquent\Collection $domains @@ -17,12 +16,12 @@ trait HasDomains { public function domains() { - return $this->hasMany(config('tenancy.domain_model'), 'tenant_id'); + return $this->hasMany(config('tenancy.models.domain'), Tenancy::tenantKeyColumn()); } public function createDomain($data): Domain { - $class = config('tenancy.domain_model'); + $class = config('tenancy.models.domain'); if (! is_array($data)) { $data = ['domain' => $data]; diff --git a/src/Database/Concerns/HasScopedValidationRules.php b/src/Database/Concerns/HasScopedValidationRules.php index 7913a215..979a3ecc 100644 --- a/src/Database/Concerns/HasScopedValidationRules.php +++ b/src/Database/Concerns/HasScopedValidationRules.php @@ -6,16 +6,17 @@ namespace Stancl\Tenancy\Database\Concerns; use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Unique; +use Stancl\Tenancy\Tenancy; trait HasScopedValidationRules { public function unique($table, $column = 'NULL') { - return (new Unique($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey()); + return (new Unique($table, $column))->where(Tenancy::tenantKeyColumn(), $this->getTenantKey()); } public function exists($table, $column = 'NULL') { - return (new Exists($table, $column))->where(BelongsToTenant::tenantIdColumn(), $this->getTenantKey()); + return (new Exists($table, $column))->where(Tenancy::tenantKeyColumn(), $this->getTenantKey()); } } diff --git a/src/Database/Models/Domain.php b/src/Database/Models/Domain.php index 16695711..e5c49bcf 100644 --- a/src/Database/Models/Domain.php +++ b/src/Database/Models/Domain.php @@ -28,7 +28,7 @@ class Domain extends Model implements Contracts\Domain public function tenant(): BelongsTo { - return $this->belongsTo(config('tenancy.tenant_model')); + return $this->belongsTo(config('tenancy.models.tenant')); } protected $dispatchesEvents = [ diff --git a/src/Database/TenantScope.php b/src/Database/TenantScope.php index fdab9d70..e3b1db69 100644 --- a/src/Database/TenantScope.php +++ b/src/Database/TenantScope.php @@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Database; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; -use Stancl\Tenancy\Database\Concerns\BelongsToTenant; +use Stancl\Tenancy\Tenancy; class TenantScope implements Scope { @@ -17,7 +17,7 @@ class TenantScope implements Scope return; } - $builder->where($model->qualifyColumn(BelongsToTenant::tenantIdColumn()), tenant()->getTenantKey()); + $builder->where($model->qualifyColumn(Tenancy::tenantKeyColumn()), tenant()->getTenantKey()); } public function extend(Builder $builder): void diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 41bf774b..4c9bb104 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -20,7 +20,7 @@ class UserImpersonation implements Feature { $tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string $authGuard = null): ImpersonationToken { return ImpersonationToken::create([ - 'tenant_id' => $tenant->getTenantKey(), + Tenancy::tenantKeyColumn() => $tenant->getTenantKey(), 'user_id' => $userId, 'redirect_url' => $redirectUrl, 'auth_guard' => $authGuard, @@ -39,7 +39,7 @@ class UserImpersonation implements Feature abort_if($tokenExpired, 403); - $tokenTenantId = (string) $token->tenant_id; + $tokenTenantId = (string) $token->getAttribute(Tenancy::tenantKeyColumn()); $currentTenantId = (string) tenant()->getTenantKey(); abort_unless($tokenTenantId === $currentTenantId, 403); diff --git a/src/Listeners/CreateTenantConnection.php b/src/Listeners/CreateTenantConnection.php index b4983d32..6af18a10 100644 --- a/src/Listeners/CreateTenantConnection.php +++ b/src/Listeners/CreateTenantConnection.php @@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Listeners; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\DatabaseManager; -use Stancl\Tenancy\Events\Contracts\TenantEvent; +use Stancl\Tenancy\Events\Contracts\TenancyEvent; class CreateTenantConnection { @@ -15,11 +15,12 @@ class CreateTenantConnection ) { } - public function handle(TenantEvent $event): void + public function handle(TenancyEvent $event): void { - /** @var TenantWithDatabase */ - $tenant = $event->tenant; + /** @var TenantWithDatabase $tenant */ + $tenant = $event->tenancy->tenant; + $this->database->purgeTenantConnection(); $this->database->createTenantConnection($tenant); } } diff --git a/src/Listeners/UpdateSyncedResource.php b/src/Listeners/UpdateSyncedResource.php index 39391eac..38245a80 100644 --- a/src/Listeners/UpdateSyncedResource.php +++ b/src/Listeners/UpdateSyncedResource.php @@ -14,6 +14,7 @@ use Stancl\Tenancy\Database\TenantCollection; use Stancl\Tenancy\Events\SyncedResourceChangedInForeignDatabase; use Stancl\Tenancy\Events\SyncedResourceSaved; use Stancl\Tenancy\Exceptions\ModelNotSyncMasterException; +use Stancl\Tenancy\Tenancy; // todo@v4 review all code related to resource syncing @@ -77,7 +78,7 @@ class UpdateSyncedResource extends QueueableListener /** @var Tenant */ $tenant = $event->tenant; - return ((string) $model->pivot->tenant_id) === ((string) $tenant->getTenantKey()); + return ((string) $model->pivot->getAttribute(Tenancy::tenantKeyColumn())) === ((string) $tenant->getTenantKey()); }; $mappingExists = $centralModel->tenants->contains($currentTenantMapping); diff --git a/src/Listeners/UseCentralConnection.php b/src/Listeners/UseCentralConnection.php new file mode 100644 index 00000000..716a5148 --- /dev/null +++ b/src/Listeners/UseCentralConnection.php @@ -0,0 +1,21 @@ +database->reconnectToCentral(); + } +} diff --git a/src/Listeners/UseTenantConnection.php b/src/Listeners/UseTenantConnection.php new file mode 100644 index 00000000..a4c12108 --- /dev/null +++ b/src/Listeners/UseTenantConnection.php @@ -0,0 +1,21 @@ +database->setDefaultConnection('tenant'); + } +} diff --git a/src/Resolvers/DomainTenantResolver.php b/src/Resolvers/DomainTenantResolver.php index cf88f579..2163febe 100644 --- a/src/Resolvers/DomainTenantResolver.php +++ b/src/Resolvers/DomainTenantResolver.php @@ -18,7 +18,7 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver { $domain = $args[0]; - $tenant = config('tenancy.tenant_model')::query() + $tenant = config('tenancy.models.tenant')::query() ->whereHas('domains', fn (Builder $query) => $query->where('domain', $domain)) ->with('domains') ->first(); diff --git a/src/Tenancy.php b/src/Tenancy.php index 5b30e3e0..e8187dd8 100644 --- a/src/Tenancy.php +++ b/src/Tenancy.php @@ -97,7 +97,7 @@ class Tenancy public static function model(): Tenant&Model { - $class = config('tenancy.tenant_model'); + $class = config('tenancy.models.tenant'); /** @var Tenant&Model $model */ $model = new $class; @@ -105,6 +105,12 @@ class Tenancy return $model; } + /** Name of the column used to relate models to tenants. */ + public static function tenantKeyColumn(): string + { + return config('tenancy.models.tenant_key_column') ?? 'tenant_id'; + } + /** * Try to find a tenant using an ID. * diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index ca39c194..7a40b84f 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -54,9 +54,9 @@ class TenancyServiceProvider extends ServiceProvider $this->app->singleton($bootstrapper); } - // Bind the class in the tenancy.id_generator config to the UniqueIdentifierGenerator abstract. - if (! is_null($this->app['config']['tenancy.id_generator'])) { - $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.id_generator']); + // Bind the class in the tenancy.models.id_generator config to the UniqueIdentifierGenerator abstract. + if (! is_null($this->app['config']['tenancy.models.id_generator'])) { + $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.models.id_generator']); } $this->app->singleton(Commands\Migrate::class, function ($app) { diff --git a/tests/CombinedDomainAndSubdomainIdentificationTest.php b/tests/CombinedDomainAndSubdomainIdentificationTest.php index 4e3c190b..8d613875 100644 --- a/tests/CombinedDomainAndSubdomainIdentificationTest.php +++ b/tests/CombinedDomainAndSubdomainIdentificationTest.php @@ -16,7 +16,7 @@ beforeEach(function () { }); }); - config(['tenancy.tenant_model' => CombinedTenant::class]); + config(['tenancy.models.tenant' => CombinedTenant::class]); }); test('tenant can be identified by subdomain', function () { diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 95672753..d8484253 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -25,7 +25,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; beforeEach(function () { - if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) { + if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) { unlink($schemaPath); } @@ -111,28 +111,44 @@ test('migrate command loads schema state', function () { test('dump command works', function () { $tenant = Tenant::create(); + $schemaPath = 'tests/Etc/tenant-schema-test.dump'; + Artisan::call('tenants:migrate'); - tenancy()->initialize($tenant); + expect($schemaPath)->not()->toBeFile(); - Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"'); - expect('tests/Etc/tenant-schema-test.dump')->toBeFile(); -}); - -test('tenant dump file gets created as tenant-schema.dump in the database schema folder by default', function() { - config(['tenancy.migration_parameters.--schema-path' => $schemaPath = database_path('schema/tenant-schema.dump')]); - - $tenant = Tenant::create(); - Artisan::call('tenants:migrate'); - - tenancy()->initialize($tenant); - - Artisan::call('tenants:dump'); + Artisan::call('tenants:dump ' . "--tenant='$tenant->id' --path='$schemaPath'"); expect($schemaPath)->toBeFile(); }); -test('migrate command uses the correct schema path by default', function () { +test('dump command generates dump at the passed path', function() { + $tenant = Tenant::create(); + + Artisan::call('tenants:migrate'); + + expect($schemaPath = 'tests/Etc/tenant-schema-test.dump')->not()->toBeFile(); + + Artisan::call("tenants:dump --tenant='$tenant->id' --path='$schemaPath'"); + + expect($schemaPath)->toBeFile(); +}); + +test('dump command generates dump at the path specified in the tenancy migration parameters config', function() { + config(['tenancy.migration_parameters.--schema-path' => $schemaPath = 'tests/Etc/tenant-schema-test.dump']); + + $tenant = Tenant::create(); + + Artisan::call('tenants:migrate'); + + expect($schemaPath)->not()->toBeFile(); + + Artisan::call("tenants:dump --tenant='$tenant->id'"); + + expect($schemaPath)->toBeFile(); +}); + +test('migrate command correctly uses the schema dump located at the configured schema path by default', function () { config(['tenancy.migration_parameters.--schema-path' => 'tests/Etc/tenant-schema.dump']); $tenant = Tenant::create(); @@ -146,6 +162,7 @@ test('migrate command uses the correct schema path by default', function () { tenancy()->initialize($tenant); + // schema_users is a table included in the tests/Etc/tenant-schema dump // Check for both tables to see if missing migrations also get executed expect(Schema::hasTable('schema_users'))->toBeTrue(); expect(Schema::hasTable('users'))->toBeTrue(); diff --git a/tests/DeleteDomainsJobTest.php b/tests/DeleteDomainsJobTest.php index bdee14dd..bd825b71 100644 --- a/tests/DeleteDomainsJobTest.php +++ b/tests/DeleteDomainsJobTest.php @@ -6,7 +6,7 @@ use Stancl\Tenancy\Database\Concerns\HasDomains; use Stancl\Tenancy\Jobs\DeleteDomains; beforeEach(function () { - config(['tenancy.tenant_model' => DatabaseAndDomainTenant::class]); + config(['tenancy.models.tenant' => DatabaseAndDomainTenant::class]); }); test('job delete domains successfully', function (){ @@ -29,4 +29,4 @@ test('job delete domains successfully', function (){ class DatabaseAndDomainTenant extends \Stancl\Tenancy\Tests\Etc\Tenant { use HasDomains; -} \ No newline at end of file +} diff --git a/tests/DomainTest.php b/tests/DomainTest.php index 6995da24..2fc04b76 100644 --- a/tests/DomainTest.php +++ b/tests/DomainTest.php @@ -21,7 +21,7 @@ beforeEach(function () { }); }); - config(['tenancy.tenant_model' => DomainTenant::class]); + config(['tenancy.models.tenant' => DomainTenant::class]); }); test('tenant can be identified using hostname', function () { diff --git a/tests/ManualModeTest.php b/tests/ManualModeTest.php new file mode 100644 index 00000000..fe1ba9a6 --- /dev/null +++ b/tests/ManualModeTest.php @@ -0,0 +1,45 @@ +send(function (TenantCreated $event) { + return $event->tenant; + })->toListener()); + + Event::listen(TenancyInitialized::class, CreateTenantConnection::class); + Event::listen(TenancyInitialized::class, UseTenantConnection::class); + Event::listen(TenancyEnded::class, UseCentralConnection::class); + + $tenant = Tenant::create(); + + expect(app('db')->getDefaultConnection())->toBe('central'); + expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant_host_connection']); + pest()->assertArrayNotHasKey('tenant', config('database.connections')); + + tenancy()->initialize($tenant); + + // Trigger creation of the tenant connection + createUsersTable(); + + expect(app('db')->getDefaultConnection())->toBe('tenant'); + expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant']); + pest()->assertArrayHasKey('tenant', config('database.connections')); + + tenancy()->end(); + + expect(array_keys(app('db')->getConnections()))->toBe(['central']); + expect(config('database.connections.tenant'))->toBeNull(); + expect(app('db')->getDefaultConnection())->toBe(config('tenancy.database.central_connection')); +}); diff --git a/tests/PendingTenantsTest.php b/tests/PendingTenantsTest.php index 8dbda9ee..26fd5c34 100644 --- a/tests/PendingTenantsTest.php +++ b/tests/PendingTenantsTest.php @@ -67,23 +67,6 @@ test('CreatePendingTenants command cannot run with both time constraints', funct ->assertFailed(); }); -test('CreatePendingTenants commands all option overrides any config constraints', function () { - Tenant::createPending(); - Tenant::createPending(); - - tenancy()->model()->query()->onlyPending()->first()->update([ - 'pending_since' => now()->subDays(10) - ]); - - config(['tenancy.pending.older_than_days' => 4]); - - Artisan::call(ClearPendingTenants::class, [ - '--all' => true - ]); - - expect(Tenant::onlyPending()->count())->toBe(0); -}); - test('tenancy can check if there are any pending tenants', function () { expect(Tenant::onlyPending()->exists())->toBeFalse(); diff --git a/tests/SingleDatabaseTenancyTest.php b/tests/SingleDatabaseTenancyTest.php index ec0a0edf..d9f10fc0 100644 --- a/tests/SingleDatabaseTenancyTest.php +++ b/tests/SingleDatabaseTenancyTest.php @@ -31,7 +31,7 @@ beforeEach(function () { $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade'); }); - config(['tenancy.tenant_model' => Tenant::class]); + config(['tenancy.models.tenant' => Tenant::class]); }); test('primary models are scoped to the current tenant', function () { @@ -142,7 +142,7 @@ test('tenant id is not auto added when creating primary resources in central con }); test('tenant id column name can be customized', function () { - config(['tenancy.single_db.tenant_id_column' => 'team_id']); + config(['tenancy.models.tenant_key_column' => 'team_id']); Schema::drop('comments'); Schema::drop('posts'); diff --git a/tests/SubdomainTest.php b/tests/SubdomainTest.php index 0ff52bc0..365ecc47 100644 --- a/tests/SubdomainTest.php +++ b/tests/SubdomainTest.php @@ -20,7 +20,7 @@ beforeEach(function () { }); }); - config(['tenancy.tenant_model' => SubdomainTenant::class]); + config(['tenancy.models.tenant' => SubdomainTenant::class]); }); test('tenant can be identified by subdomain', function () { diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php index fb62260c..1c5a7700 100644 --- a/tests/TenantModelTest.php +++ b/tests/TenantModelTest.php @@ -43,7 +43,7 @@ test('current tenant can be resolved from service container using typehint', fun }); test('id is generated when no id is supplied', function () { - config(['tenancy.id_generator' => UUIDGenerator::class]); + config(['tenancy.models.id_generator' => UUIDGenerator::class]); $this->mock(UUIDGenerator::class, function ($mock) { return $mock->shouldReceive('generate')->once(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 1c0ceb83..7b9deea0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -109,7 +109,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase 'central' => true, ], 'tenancy.seeder_parameters' => [], - 'tenancy.tenant_model' => Tenant::class, // Use test tenant w/ DBs & domains + 'tenancy.models.tenant' => Tenant::class, // Use test tenant w/ DBs & domains ]); $app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration