mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 21:14:03 +00:00
Merge branch 'master' into add-skip-failing-options-to-migrate
This commit is contained in:
commit
29d13ae5b4
86 changed files with 2001 additions and 236 deletions
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Mail\MailManager;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
|
@ -23,6 +24,7 @@ use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
|
|||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
|
|
@ -326,20 +328,55 @@ test('local storage public urls are generated correctly', function() {
|
|||
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
|
||||
});
|
||||
|
||||
test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() {
|
||||
MailTenancyBootstrapper::$credentialsMap = [
|
||||
'mail.mailers.smtp.username' => 'smtp_username',
|
||||
'mail.mailers.smtp.password' => 'smtp_password'
|
||||
];
|
||||
|
||||
config([
|
||||
'mail.default' => 'smtp',
|
||||
'mail.mailers.smtp.username' => $defaultUsername = 'default username',
|
||||
'mail.mailers.smtp.password' => 'no password'
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create(['smtp_password' => $password = 'testing password']);
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
expect(array_key_exists('smtp_password', tenant()->getAttributes()))->toBeTrue();
|
||||
expect(array_key_exists('smtp_host', tenant()->getAttributes()))->toBeFalse();
|
||||
expect(config('mail.mailers.smtp.username'))->toBe($defaultUsername);
|
||||
expect(config('mail.mailers.smtp.password'))->toBe(tenant()->smtp_password);
|
||||
|
||||
// Assert that the current mailer uses tenant's smtp_password
|
||||
assertMailerTransportUsesPassword($password);
|
||||
});
|
||||
|
||||
test('MailTenancyBootstrapper reverts the config and mailer credentials to default when tenancy ends', function() {
|
||||
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
||||
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'no password']);
|
||||
|
||||
tenancy()->initialize(Tenant::create(['smtp_password' => $tenantPassword = 'testing password']));
|
||||
|
||||
expect(config('mail.mailers.smtp.password'))->toBe($tenantPassword);
|
||||
|
||||
assertMailerTransportUsesPassword($tenantPassword);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
expect(config('mail.mailers.smtp.password'))->toBe($defaultPassword);
|
||||
|
||||
// Assert that the current mailer uses the default SMTP password
|
||||
assertMailerTransportUsesPassword($defaultPassword);
|
||||
});
|
||||
|
||||
function getDiskPrefix(string $disk): string
|
||||
{
|
||||
/** @var FilesystemAdapter $disk */
|
||||
$disk = Storage::disk($disk);
|
||||
$adapter = $disk->getAdapter();
|
||||
$prefix = invade(invade($adapter)->prefixer)->prefix;
|
||||
|
||||
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
|
||||
$prefixer->setAccessible(true);
|
||||
|
||||
// reflection -> instance
|
||||
$prefixer = $prefixer->getValue($adapter);
|
||||
|
||||
$prefix = (new ReflectionProperty($prefixer, 'prefix'));
|
||||
$prefix->setAccessible(true);
|
||||
|
||||
return $prefix->getValue($prefixer);
|
||||
return $prefix;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
|||
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
|
||||
|
||||
beforeEach(function () {
|
||||
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
||||
unlink($schemaPath);
|
||||
}
|
||||
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
config(['tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
]]);
|
||||
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
|
|
@ -153,12 +153,61 @@ test('migrate command does not stop after the first failure if skip-failing is p
|
|||
|
||||
test('dump command works', function () {
|
||||
$tenant = Tenant::create();
|
||||
$schemaPath = 'tests/Etc/tenant-schema-test.dump';
|
||||
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
expect($schemaPath)->not()->toBeFile();
|
||||
|
||||
Artisan::call('tenants:dump ' . "--tenant='$tenant->id' --path='$schemaPath'");
|
||||
|
||||
expect($schemaPath)->toBeFile();
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(Schema::hasTable('schema_users'))->toBeFalse();
|
||||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
expect(Schema::hasTable('schema_users'))->toBeFalse();
|
||||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"');
|
||||
expect('tests/Etc/tenant-schema-test.dump')->toBeFile();
|
||||
// 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();
|
||||
});
|
||||
|
||||
test('rollback command works', function () {
|
||||
|
|
@ -365,7 +414,7 @@ function runCommandWorks(): void
|
|||
Artisan::call('tenants:migrate', ['--tenants' => [$id]]);
|
||||
|
||||
pest()->artisan("tenants:run --tenants=$id 'foo foo --b=bar --c=xyz' ")
|
||||
->expectsOutput("User's name is Test command")
|
||||
->expectsOutput("User's name is Test user")
|
||||
->expectsOutput('foo')
|
||||
->expectsOutput('xyz');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ test('database can be created after tenant creation', function () {
|
|||
})->toListener());
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
$manager = app(MySQLDatabaseManager::class);
|
||||
$manager->setConnection('mysql');
|
||||
$manager = $tenant->database()->manager();
|
||||
|
||||
expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ namespace Stancl\Tenancy\Tests\Etc\Console;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
use Stancl\Tenancy\Concerns\HasTenantOptions;
|
||||
use Stancl\Tenancy\Concerns\TenantAwareCommand;
|
||||
use Stancl\Tenancy\Tests\Etc\User;
|
||||
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
use TenantAwareCommand, HasATenantsOption;
|
||||
use TenantAwareCommand, HasTenantOptions;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Tests\Etc\Console;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ExampleCommand extends Command
|
||||
|
|
@ -22,14 +23,13 @@ class ExampleCommand extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
User::create([
|
||||
'id' => 999,
|
||||
'name' => 'Test command',
|
||||
'email' => 'test@command.com',
|
||||
$id = User::create([
|
||||
'name' => 'Test user',
|
||||
'email' => Str::random(8) . '@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
])->id;
|
||||
|
||||
$this->line("User's name is " . User::find(999)->name);
|
||||
$this->line("User's name is " . User::find($id)->name);
|
||||
$this->line($this->argument('a'));
|
||||
$this->line($this->option('c'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Tests\Etc;
|
|||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDatabase;
|
||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||
use Stancl\Tenancy\Database\Concerns\HasPending;
|
||||
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
|
||||
|
|
@ -15,5 +16,5 @@ use Stancl\Tenancy\Database\Models;
|
|||
*/
|
||||
class Tenant extends Models\Tenant implements TenantWithDatabase
|
||||
{
|
||||
use HasDatabase, HasDomains, MaintenanceMode;
|
||||
use HasDatabase, HasDomains, HasPending, MaintenanceMode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddExtraColumnToCentralUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('foo');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
}
|
||||
}
|
||||
72
tests/MailTest.php
Normal file
72
tests/MailTest.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Mail\MailManager;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
|
||||
|
||||
beforeEach(function() {
|
||||
config(['mail.default' => 'smtp']);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
||||
// Initialize tenancy as $tenant and assert that the smtp mailer's transport has the correct password
|
||||
function assertMailerTransportUsesPassword(string|null $password) {
|
||||
$manager = app(MailManager::class);
|
||||
$mailer = invade($manager)->get('smtp');
|
||||
$mailerPassword = invade($mailer->getSymfonyTransport())->password;
|
||||
|
||||
expect($mailerPassword)->toBe((string) $password);
|
||||
};
|
||||
|
||||
test('mailer transport uses the correct credentials', function() {
|
||||
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'DEFAULT']);
|
||||
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
||||
|
||||
tenancy()->initialize($tenant = Tenant::create());
|
||||
assertMailerTransportUsesPassword($defaultPassword); // $tenant->smtp_password is not set, so the default password should be used
|
||||
tenancy()->end();
|
||||
|
||||
// Assert mailer uses the updated password
|
||||
$tenant->update(['smtp_password' => $newPassword = 'changed']);
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
assertMailerTransportUsesPassword($newPassword);
|
||||
tenancy()->end();
|
||||
|
||||
// Assert mailer uses the correct password after switching to a different tenant
|
||||
tenancy()->initialize(Tenant::create(['smtp_password' => $newTenantPassword = 'updated']));
|
||||
assertMailerTransportUsesPassword($newTenantPassword);
|
||||
tenancy()->end();
|
||||
|
||||
// Assert mailer uses the default password after tenancy ends
|
||||
assertMailerTransportUsesPassword($defaultPassword);
|
||||
});
|
||||
|
||||
|
||||
test('initializing and ending tenancy binds a fresh MailManager instance without cached mailers', function() {
|
||||
$mailers = fn() => invade(app(MailManager::class))->mailers;
|
||||
|
||||
app(MailManager::class)->mailer('smtp');
|
||||
|
||||
expect($mailers())->toHaveCount(1);
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
|
||||
expect($mailers())->toHaveCount(0);
|
||||
|
||||
app(MailManager::class)->mailer('smtp');
|
||||
|
||||
expect($mailers())->toHaveCount(1);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
expect($mailers())->toHaveCount(0);
|
||||
});
|
||||
45
tests/ManualModeTest.php
Normal file
45
tests/ManualModeTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Listeners\CreateTenantConnection;
|
||||
use Stancl\Tenancy\Listeners\UseCentralConnection;
|
||||
use Stancl\Tenancy\Listeners\UseTenantConnection;
|
||||
use \Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
test('manual tenancy initialization works', function () {
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->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'));
|
||||
});
|
||||
192
tests/PendingTenantsTest.php
Normal file
192
tests/PendingTenantsTest.php
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Commands\ClearPendingTenants;
|
||||
use Stancl\Tenancy\Commands\CreatePendingTenants;
|
||||
use Stancl\Tenancy\Events\CreatingPendingTenant;
|
||||
use Stancl\Tenancy\Events\PendingTenantCreated;
|
||||
use Stancl\Tenancy\Events\PendingTenantPulled;
|
||||
use Stancl\Tenancy\Events\PullingPendingTenant;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
test('tenants are correctly identified as pending', function (){
|
||||
Tenant::createPending();
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toBe(1);
|
||||
|
||||
Tenant::onlyPending()->first()->update([
|
||||
'pending_since' => null
|
||||
]);
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('pending trait adds query scopes', function () {
|
||||
Tenant::createPending();
|
||||
Tenant::create();
|
||||
Tenant::create();
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toBe(1)
|
||||
->and(Tenant::withPending(true)->count())->toBe(3)
|
||||
->and(Tenant::withPending(false)->count())->toBe(2)
|
||||
->and(Tenant::withoutPending()->count())->toBe(2);
|
||||
|
||||
});
|
||||
|
||||
test('pending tenants can be created and deleted using commands', function () {
|
||||
config(['tenancy.pending.count' => 4]);
|
||||
|
||||
Artisan::call(CreatePendingTenants::class);
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toBe(4);
|
||||
|
||||
Artisan::call(ClearPendingTenants::class);
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('CreatePendingTenants command can have an older than constraint', function () {
|
||||
config(['tenancy.pending.count' => 2]);
|
||||
|
||||
Artisan::call(CreatePendingTenants::class);
|
||||
|
||||
tenancy()->model()->query()->onlyPending()->first()->update([
|
||||
'pending_since' => now()->subDays(5)->timestamp
|
||||
]);
|
||||
|
||||
Artisan::call('tenants:pending-clear --older-than-days=2');
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('CreatePendingTenants command cannot run with both time constraints', function () {
|
||||
pest()->artisan('tenants:pending-clear --older-than-days=2 --older-than-hours=2')
|
||||
->assertFailed();
|
||||
});
|
||||
|
||||
test('tenancy can check if there are any pending tenants', function () {
|
||||
expect(Tenant::onlyPending()->exists())->toBeFalse();
|
||||
|
||||
Tenant::createPending();
|
||||
|
||||
expect(Tenant::onlyPending()->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('tenancy can pull a pending tenant', function () {
|
||||
Tenant::createPending();
|
||||
|
||||
expect(Tenant::pullPendingFromPool())->toBeInstanceOf(Tenant::class);
|
||||
});
|
||||
|
||||
test('pulling a tenant from the pending tenant pool removes it from the pool', function () {
|
||||
Tenant::createPending();
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toEqual(1);
|
||||
|
||||
Tenant::pullPendingFromPool();
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toEqual(0);
|
||||
});
|
||||
|
||||
test('a new tenant gets created while pulling a pending tenant if the pending pool is empty', function () {
|
||||
expect(Tenant::withPending()->get()->count())->toBe(0); // All tenants
|
||||
|
||||
Tenant::pullPending();
|
||||
|
||||
expect(Tenant::withPending()->get()->count())->toBe(1); // All tenants
|
||||
});
|
||||
|
||||
test('pending tenants are included in all queries based on the include_in_queries config', function () {
|
||||
Tenant::createPending();
|
||||
|
||||
config(['tenancy.pending.include_in_queries' => false]);
|
||||
|
||||
expect(Tenant::all()->count())->toBe(0);
|
||||
|
||||
config(['tenancy.pending.include_in_queries' => true]);
|
||||
|
||||
expect(Tenant::all()->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('pending events are dispatched', function () {
|
||||
Event::fake([
|
||||
CreatingPendingTenant::class,
|
||||
PendingTenantCreated::class,
|
||||
PullingPendingTenant::class,
|
||||
PendingTenantPulled::class,
|
||||
]);
|
||||
|
||||
Tenant::createPending();
|
||||
|
||||
Event::assertDispatched(CreatingPendingTenant::class);
|
||||
Event::assertDispatched(PendingTenantCreated::class);
|
||||
|
||||
Tenant::pullPending();
|
||||
|
||||
Event::assertDispatched(PullingPendingTenant::class);
|
||||
Event::assertDispatched(PendingTenantPulled::class);
|
||||
});
|
||||
|
||||
test('commands do not run for pending tenants if tenancy.pending.include_in_queries is false and the with pending option does not get passed', function() {
|
||||
config(['tenancy.pending.include_in_queries' => false]);
|
||||
|
||||
$tenants = collect([
|
||||
Tenant::create(),
|
||||
Tenant::create(),
|
||||
Tenant::createPending(),
|
||||
Tenant::createPending(),
|
||||
]);
|
||||
|
||||
pest()->artisan('tenants:migrate --with-pending');
|
||||
|
||||
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'");
|
||||
|
||||
$pendingTenants = $tenants->filter->pending();
|
||||
$readyTenants = $tenants->reject->pending();
|
||||
|
||||
$pendingTenants->each(fn ($tenant) => $artisan->doesntExpectOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
$readyTenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
|
||||
$artisan->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('commands run for pending tenants too if tenancy.pending.include_in_queries is true', function() {
|
||||
config(['tenancy.pending.include_in_queries' => true]);
|
||||
|
||||
$tenants = collect([
|
||||
Tenant::create(),
|
||||
Tenant::create(),
|
||||
Tenant::createPending(),
|
||||
Tenant::createPending(),
|
||||
]);
|
||||
|
||||
pest()->artisan('tenants:migrate --with-pending');
|
||||
|
||||
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz'");
|
||||
|
||||
$tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
|
||||
$artisan->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('commands run for pending tenants too if the with pending option is passed', function() {
|
||||
config(['tenancy.pending.include_in_queries' => false]);
|
||||
|
||||
$tenants = collect([
|
||||
Tenant::create(),
|
||||
Tenant::create(),
|
||||
Tenant::createPending(),
|
||||
Tenant::createPending(),
|
||||
]);
|
||||
|
||||
pest()->artisan('tenants:migrate --with-pending');
|
||||
|
||||
$artisan = pest()->artisan("tenants:run 'foo foo --b=bar --c=xyz' --with-pending");
|
||||
|
||||
$tenants->each(fn ($tenant) => $artisan->expectsOutputToContain("Tenant: {$tenant->getTenantKey()}"));
|
||||
|
||||
$artisan->assertExitCode(0);
|
||||
});
|
||||
|
|
@ -44,9 +44,10 @@ beforeEach(function () {
|
|||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
|
||||
UpdateSyncedResource::$shouldQueue = false; // global state cleanup
|
||||
UpdateSyncedResource::$shouldQueue = false; // Global state cleanup
|
||||
Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class);
|
||||
|
||||
// Run migrations on central connection
|
||||
pest()->artisan('migrate', [
|
||||
'--path' => [
|
||||
__DIR__ . '/Etc/synced_resource_migrations',
|
||||
|
|
@ -83,7 +84,7 @@ test('only the synced columns are updated in the central db', function () {
|
|||
]);
|
||||
|
||||
$tenant = ResourceTenant::create();
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
|
|
@ -126,6 +127,231 @@ test('only the synced columns are updated in the central db', function () {
|
|||
], ResourceUser::first()->getAttributes());
|
||||
});
|
||||
|
||||
// This tests attribute list on the central side, and default values on the tenant side
|
||||
// Those two don't depend on each other, we're just testing having each option on each side
|
||||
// using tests that combine the two, to avoid having an excessively long and complex test suite
|
||||
test('sync resource creation works when central model provides attributes and tenant model provides default values', function () {
|
||||
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
|
||||
|
||||
addExtraColumnToCentralDB();
|
||||
|
||||
$centralUser = CentralUserProvidingAttributeNames::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'secret',
|
||||
'role' => 'commenter',
|
||||
'foo' => 'bar', // foo does not exist in resource model
|
||||
]);
|
||||
|
||||
$tenant1->run(function () {
|
||||
expect(TenantUserProvidingDefaultValues::all())->toHaveCount(0);
|
||||
});
|
||||
|
||||
// When central model provides the list of attributes, resource model will be created from the provided list of attributes' values
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
$tenant1->run(function () {
|
||||
$resourceUser = TenantUserProvidingDefaultValues::all();
|
||||
expect($resourceUser)->toHaveCount(1);
|
||||
expect($resourceUser->first()->global_id)->toBe('acme');
|
||||
expect($resourceUser->first()->email)->toBe('john@localhost');
|
||||
// 'foo' attribute is not provided by central model
|
||||
expect($resourceUser->first()->foo)->toBeNull();
|
||||
});
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
// When resource model provides the list of default values, central model will be created from the provided list of default values
|
||||
TenantUserProvidingDefaultValues::create([
|
||||
'global_id' => 'asdf',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'secret',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
// Assert central user was created using the list of default values
|
||||
$centralUser = CentralUserProvidingAttributeNames::whereGlobalId('asdf')->first();
|
||||
expect($centralUser)->not()->toBeNull();
|
||||
expect($centralUser->name)->toBe('Default Name');
|
||||
expect($centralUser->email)->toBe('default@localhost');
|
||||
expect($centralUser->password)->toBe('password');
|
||||
expect($centralUser->role)->toBe('admin');
|
||||
expect($centralUser->foo)->toBe('bar');
|
||||
});
|
||||
|
||||
// This tests default values on the central side, and attribute list on the tenant side
|
||||
// Those two don't depend on each other, we're just testing having each option on each side
|
||||
// using tests that combine the two, to avoid having an excessively long and complex test suite
|
||||
test('sync resource creation works when central model provides default values and tenant model provides attributes', function () {
|
||||
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
|
||||
|
||||
addExtraColumnToCentralDB();
|
||||
|
||||
$centralUser = CentralUserProvidingDefaultValues::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'secret',
|
||||
'role' => 'commenter',
|
||||
'foo' => 'bar', // foo does not exist in resource model
|
||||
]);
|
||||
|
||||
$tenant1->run(function () {
|
||||
expect(TenantUserProvidingDefaultValues::all())->toHaveCount(0);
|
||||
});
|
||||
|
||||
// When central model provides the list of default values, resource model will be created from the provided list of default values
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
$tenant1->run(function () {
|
||||
// Assert resource user was created using the list of default values
|
||||
$resourceUser = TenantUserProvidingDefaultValues::first();
|
||||
expect($resourceUser)->not()->toBeNull();
|
||||
expect($resourceUser->global_id)->toBe('acme');
|
||||
expect($resourceUser->email)->toBe('default@localhost');
|
||||
expect($resourceUser->password)->toBe('password');
|
||||
expect($resourceUser->role)->toBe('admin');
|
||||
});
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
// When resource model provides the list of attributes, central model will be created from the provided list of attributes' values
|
||||
TenantUserProvidingAttributeNames::create([
|
||||
'global_id' => 'asdf',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'secret',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
// Assert central user was created using the list of provided attributes
|
||||
$centralUser = CentralUserProvidingAttributeNames::whereGlobalId('asdf')->first();
|
||||
expect($centralUser)->not()->toBeNull();
|
||||
expect($centralUser->email)->toBe('john@localhost');
|
||||
expect($centralUser->password)->toBe('secret');
|
||||
expect($centralUser->role)->toBe('commenter');
|
||||
});
|
||||
|
||||
// This tests mixed attribute list/defaults on the central side, and no specified attributes on the tenant side
|
||||
// Those two don't depend on each other, we're just testing having each option on each side
|
||||
// using tests that combine the two, to avoid having an excessively long and complex test suite
|
||||
test('sync resource creation works when central model provides mixture and tenant model provides nothing', function () {
|
||||
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
|
||||
|
||||
$centralUser = CentralUserProvidingMixture::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commentator'
|
||||
]);
|
||||
|
||||
$tenant1->run(function () {
|
||||
expect(ResourceUser::all())->toHaveCount(0);
|
||||
});
|
||||
|
||||
// When central model provides the list of a mixture (attributes and default values), resource model will be created from the provided list of mixture (attributes and default values)
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
$tenant1->run(function () {
|
||||
$resourceUser = ResourceUser::first();
|
||||
|
||||
// Assert resource user was created using the provided attributes and default values
|
||||
expect($resourceUser->global_id)->toBe('acme');
|
||||
expect($resourceUser->name)->toBe('John Doe');
|
||||
expect($resourceUser->email)->toBe('john@localhost');
|
||||
// default values
|
||||
expect($resourceUser->role)->toBe('admin');
|
||||
expect($resourceUser->password)->toBe('secret');
|
||||
});
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
// When resource model provides nothing/null, the central model will be created as a 1:1 copy of resource model
|
||||
$resourceUser = ResourceUser::create([
|
||||
'global_id' => 'acmey',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commentator'
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
$centralUser = CentralUserProvidingMixture::whereGlobalId('acmey')->first();
|
||||
expect($resourceUser->getSyncedCreationAttributes())->toBeNull();
|
||||
|
||||
$centralUser = $centralUser->toArray();
|
||||
$resourceUser = $resourceUser->toArray();
|
||||
unset($centralUser['id']);
|
||||
unset($resourceUser['id']);
|
||||
|
||||
// Assert central user created as 1:1 copy of resource model except "id"
|
||||
expect($centralUser)->toBe($resourceUser);
|
||||
});
|
||||
|
||||
// This tests no specified attributes on the central side, and mixed attribute list/defaults on the tenant side
|
||||
// Those two don't depend on each other, we're just testing having each option on each side
|
||||
// using tests that combine the two, to avoid having an excessively long and complex test suite
|
||||
test('sync resource creation works when central model provides nothing and tenant model provides mixture', function () {
|
||||
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
|
||||
|
||||
$centralUser = CentralUser::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
$tenant1->run(function () {
|
||||
expect(TenantUserProvidingMixture::all())->toHaveCount(0);
|
||||
});
|
||||
|
||||
// When central model provides nothing/null, the resource model will be created as a 1:1 copy of central model
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
expect($centralUser->getSyncedCreationAttributes())->toBeNull();
|
||||
$tenant1->run(function () use ($centralUser) {
|
||||
$resourceUser = TenantUserProvidingMixture::first();
|
||||
expect($resourceUser)->not()->toBeNull();
|
||||
$resourceUser = $resourceUser->toArray();
|
||||
$centralUser = $centralUser->withoutRelations()->toArray();
|
||||
unset($resourceUser['id']);
|
||||
unset($centralUser['id']);
|
||||
|
||||
expect($resourceUser)->toBe($centralUser);
|
||||
});
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
// When resource model provides the list of a mixture (attributes and default values), central model will be created from the provided list of mixture (attributes and default values)
|
||||
TenantUserProvidingMixture::create([
|
||||
'global_id' => 'absd',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
$centralUser = CentralUser::whereGlobalId('absd')->first();
|
||||
|
||||
// Assert central user was created using the provided list of attributes and default values
|
||||
expect($centralUser->name)->toBe('John Doe');
|
||||
expect($centralUser->email)->toBe('john@localhost');
|
||||
// default values
|
||||
expect($centralUser->role)->toBe('admin');
|
||||
expect($centralUser->password)->toBe('secret');
|
||||
});
|
||||
|
||||
test('creating the resource in tenant database creates it in central database and creates the mapping', function () {
|
||||
creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase();
|
||||
});
|
||||
|
|
@ -152,7 +378,7 @@ test('attaching a tenant to the central resource triggers a pull from the tenant
|
|||
$tenant = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
$tenant->run(function () {
|
||||
expect(ResourceUser::all())->toHaveCount(0);
|
||||
|
|
@ -177,7 +403,7 @@ test('attaching users to tenants does not do anything', function () {
|
|||
$tenant = ResourceTenant::create([
|
||||
'id' => 't1',
|
||||
]);
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
$tenant->run(function () {
|
||||
expect(ResourceUser::all())->toHaveCount(0);
|
||||
|
|
@ -212,7 +438,7 @@ test('resources are synced only to workspaces that have the resource', function
|
|||
$t3 = ResourceTenant::create([
|
||||
'id' => 't3',
|
||||
]);
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
$centralUser->tenants()->attach('t1');
|
||||
$centralUser->tenants()->attach('t2');
|
||||
|
|
@ -250,7 +476,7 @@ test('when a resource exists in other tenant dbs but is created in a tenant db t
|
|||
$t2 = ResourceTenant::create([
|
||||
'id' => 't2',
|
||||
]);
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
// Copy (cascade) user to t1 DB
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
|
@ -298,7 +524,7 @@ test('the synced columns are updated in other tenant dbs where the resource exis
|
|||
$t3 = ResourceTenant::create([
|
||||
'id' => 't3',
|
||||
]);
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
// Copy (cascade) user to t1 DB
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
|
@ -353,7 +579,7 @@ test('when the resource doesnt exist in the tenant db non synced columns will ca
|
|||
'id' => 't1',
|
||||
]);
|
||||
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
||||
|
|
@ -367,7 +593,7 @@ test('when the resource doesnt exist in the central db non synced columns will b
|
|||
'id' => 't1',
|
||||
]);
|
||||
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
$t1->run(function () {
|
||||
ResourceUser::create([
|
||||
|
|
@ -389,7 +615,7 @@ test('the listener can be queued', function () {
|
|||
'id' => 't1',
|
||||
]);
|
||||
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
|
||||
|
|
@ -428,7 +654,7 @@ test('an event is fired for all touched resources', function () {
|
|||
$t3 = ResourceTenant::create([
|
||||
'id' => 't3',
|
||||
]);
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
// Copy (cascade) user to t1 DB
|
||||
$centralUser->tenants()->attach('t1');
|
||||
|
|
@ -509,7 +735,7 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase()
|
|||
expect(ResourceUser::all())->toHaveCount(0);
|
||||
|
||||
$tenant = ResourceTenant::create();
|
||||
migrateTenantsResource();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
|
|
@ -524,7 +750,7 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase()
|
|||
|
||||
tenancy()->end();
|
||||
|
||||
// Asset user was created
|
||||
// Assert user was created
|
||||
expect(CentralUser::first()->global_id)->toBe('acme');
|
||||
expect(CentralUser::first()->role)->toBe('commenter');
|
||||
|
||||
|
|
@ -537,7 +763,65 @@ function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase()
|
|||
expect(ResourceUser::first()->role)->toBe('commenter');
|
||||
}
|
||||
|
||||
function migrateTenantsResource()
|
||||
test('resources are synced only when sync is enabled', function (bool $enabled) {
|
||||
app()->instance('_tenancy_test_shouldSync', $enabled);
|
||||
|
||||
[$tenant1, $tenant2] = createTenantsAndRunMigrations();
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
TenantUserWithConditionalSync::create([
|
||||
'global_id' => 'absd',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
expect(CentralUserWithConditionalSync::all())->toHaveCount($enabled ? 1 : 0);
|
||||
expect(CentralUserWithConditionalSync::whereGlobalId('absd')->exists())->toBe($enabled);
|
||||
|
||||
$centralUser = CentralUserWithConditionalSync::create([
|
||||
'global_id' => 'acme',
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'commenter',
|
||||
]);
|
||||
|
||||
$centralUser->tenants()->attach('t2');
|
||||
|
||||
$tenant2->run(function () use ($enabled) {
|
||||
expect(TenantUserWithConditionalSync::all())->toHaveCount($enabled ? 1 : 0);
|
||||
expect(TenantUserWithConditionalSync::whereGlobalId('acme')->exists())->toBe($enabled);
|
||||
});
|
||||
})->with([[true], [false]]);
|
||||
|
||||
/**
|
||||
* Create two tenants and run migrations for those tenants.
|
||||
*/
|
||||
function createTenantsAndRunMigrations(): array
|
||||
{
|
||||
[$tenant1, $tenant2] = [ResourceTenant::create(['id' => 't1']), ResourceTenant::create(['id' => 't2'])];
|
||||
|
||||
migrateUsersTableForTenants();
|
||||
|
||||
return [$tenant1, $tenant2];
|
||||
}
|
||||
|
||||
function addExtraColumnToCentralDB(): void
|
||||
{
|
||||
// migrate extra column "foo" in central DB
|
||||
pest()->artisan('migrate', [
|
||||
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users_extra',
|
||||
'--realpath' => true,
|
||||
])->assertExitCode(0);
|
||||
}
|
||||
|
||||
function migrateUsersTableForTenants(): void
|
||||
{
|
||||
pest()->artisan('tenants:migrate', [
|
||||
'--path' => __DIR__ . '/Etc/synced_resource_migrations/users',
|
||||
|
|
@ -545,6 +829,7 @@ function migrateTenantsResource()
|
|||
])->assertExitCode(0);
|
||||
}
|
||||
|
||||
// Tenant model used for resource syncing setup
|
||||
class ResourceTenant extends Tenant
|
||||
{
|
||||
public function users()
|
||||
|
|
@ -593,6 +878,7 @@ class CentralUser extends Model implements SyncMaster
|
|||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return [
|
||||
'global_id',
|
||||
'name',
|
||||
'password',
|
||||
'email',
|
||||
|
|
@ -600,6 +886,7 @@ class CentralUser extends Model implements SyncMaster
|
|||
}
|
||||
}
|
||||
|
||||
// Tenant users
|
||||
class ResourceUser extends Model implements Syncable
|
||||
{
|
||||
use ResourceSyncing;
|
||||
|
|
@ -628,9 +915,122 @@ class ResourceUser extends Model implements Syncable
|
|||
public function getSyncedAttributeNames(): array
|
||||
{
|
||||
return [
|
||||
'global_id',
|
||||
'name',
|
||||
'password',
|
||||
'email',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// override method in ResourceUser class to return default attribute values
|
||||
class TenantUserProvidingDefaultValues extends ResourceUser
|
||||
{
|
||||
public function getSyncedCreationAttributes(): array
|
||||
{
|
||||
// Default values when creating resources from tenant to central DB
|
||||
return
|
||||
[
|
||||
'name' => 'Default Name',
|
||||
'email' => 'default@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'admin',
|
||||
'foo' => 'bar'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// override method in ResourceUser class to return attribute names
|
||||
class TenantUserProvidingAttributeNames extends ResourceUser
|
||||
{
|
||||
public function getSyncedCreationAttributes(): array
|
||||
{
|
||||
// Attributes used when creating resources from tenant to central DB
|
||||
// Notice here we are not adding "code" filed because it doesn't
|
||||
// exist in central model
|
||||
return
|
||||
[
|
||||
'name',
|
||||
'password',
|
||||
'email',
|
||||
'role',
|
||||
'foo' => 'bar'
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// override method in CentralUser class to return attribute default values
|
||||
class CentralUserProvidingDefaultValues extends CentralUser
|
||||
{
|
||||
public function getSyncedCreationAttributes(): array
|
||||
{
|
||||
// Attributes default values when creating resources from central to tenant model
|
||||
return
|
||||
[
|
||||
'name' => 'Default User',
|
||||
'email' => 'default@localhost',
|
||||
'password' => 'password',
|
||||
'role' => 'admin',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// override method in CentralUser class to return attribute names
|
||||
class CentralUserProvidingAttributeNames extends CentralUser
|
||||
{
|
||||
public function getSyncedCreationAttributes(): array
|
||||
{
|
||||
// Attributes used when creating resources from central to tenant DB
|
||||
return
|
||||
[
|
||||
'global_id',
|
||||
'name',
|
||||
'password',
|
||||
'email',
|
||||
'role',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CentralUserProvidingMixture extends CentralUser
|
||||
{
|
||||
public function getSyncedCreationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'name',
|
||||
'email',
|
||||
'role' => 'admin',
|
||||
'password' => 'secret',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class TenantUserProvidingMixture extends ResourceUser
|
||||
{
|
||||
public function getSyncedCreationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'name',
|
||||
'email',
|
||||
'role' => 'admin',
|
||||
'password' => 'secret',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class CentralUserWithConditionalSync extends CentralUser
|
||||
{
|
||||
public function shouldSync(): bool
|
||||
{
|
||||
return app('_tenancy_test_shouldSync');
|
||||
}
|
||||
}
|
||||
|
||||
class TenantUserWithConditionalSync extends ResourceUser
|
||||
{
|
||||
public function shouldSync(): bool
|
||||
{
|
||||
return app('_tenancy_test_shouldSync');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Database\Contracts\StatefulTenantDatabaseManager;
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
|
|
@ -36,7 +38,10 @@ test('databases can be created and deleted', function ($driver, $databaseManager
|
|||
$name = 'db' . pest()->randomString();
|
||||
|
||||
$manager = app($databaseManager);
|
||||
$manager->setConnection($driver);
|
||||
|
||||
if ($manager instanceof StatefulTenantDatabaseManager) {
|
||||
$manager->setConnection($driver);
|
||||
}
|
||||
|
||||
expect($manager->databaseExists($name))->toBeFalse();
|
||||
|
||||
|
|
@ -48,7 +53,7 @@ test('databases can be created and deleted', function ($driver, $databaseManager
|
|||
expect($manager->databaseExists($name))->toBeTrue();
|
||||
$manager->deleteDatabase($tenant);
|
||||
expect($manager->databaseExists($name))->toBeFalse();
|
||||
})->with('database_manager_provider');
|
||||
})->with('database_managers');
|
||||
|
||||
test('dbs can be created when another driver is used for the central db', function () {
|
||||
expect(config('database.default'))->toBe('central');
|
||||
|
|
@ -100,7 +105,7 @@ test('the tenant connection is fully removed', function () {
|
|||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
expect(array_keys(app('db')->getConnections()))->toBe(['central']);
|
||||
expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant_host_connection']);
|
||||
pest()->assertArrayNotHasKey('tenant', config('database.connections'));
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
|
@ -179,7 +184,7 @@ test('a tenants database cannot be created when the database already exists', fu
|
|||
]);
|
||||
});
|
||||
|
||||
test('tenant database can be created on a foreign server', function () {
|
||||
test('tenant database can be created and deleted on a foreign server', function () {
|
||||
config([
|
||||
'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
|
||||
'database.connections.mysql2' => [
|
||||
|
|
@ -215,10 +220,151 @@ test('tenant database can be created on a foreign server', function () {
|
|||
/** @var PermissionControlledMySQLDatabaseManager $manager */
|
||||
$manager = $tenant->database()->manager();
|
||||
|
||||
$manager->setConnection('mysql');
|
||||
expect($manager->databaseExists($name))->toBeFalse();
|
||||
expect($manager->databaseExists($name))->toBeTrue(); // mysql2
|
||||
|
||||
$manager->setConnection('mysql2');
|
||||
$manager->setConnection('mysql');
|
||||
expect($manager->databaseExists($name))->toBeFalse(); // check that the DB doesn't exist in 'mysql'
|
||||
|
||||
$manager->setConnection('mysql2'); // set the connection back
|
||||
$manager->deleteDatabase($tenant);
|
||||
|
||||
expect($manager->databaseExists($name))->toBeFalse();
|
||||
});
|
||||
|
||||
test('tenant database can be created on a foreign server by using the host from tenant config', function () {
|
||||
config([
|
||||
'tenancy.database.managers.mysql' => MySQLDatabaseManager::class,
|
||||
'tenancy.database.template_tenant_connection' => 'mysql', // This will be overridden by tenancy_db_host
|
||||
'database.connections.mysql2' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => 'mysql2',
|
||||
'port' => 3306,
|
||||
'database' => 'main',
|
||||
'username' => 'root',
|
||||
'password' => 'password',
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
]);
|
||||
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
$name = 'foo' . Str::random(8);
|
||||
$tenant = Tenant::create([
|
||||
'tenancy_db_name' => $name,
|
||||
'tenancy_db_host' => 'mysql2',
|
||||
]);
|
||||
|
||||
/** @var MySQLDatabaseManager $manager */
|
||||
$manager = $tenant->database()->manager();
|
||||
|
||||
expect($manager->databaseExists($name))->toBeTrue();
|
||||
});
|
||||
|
||||
test('database credentials can be provided to PermissionControlledMySQLDatabaseManager by specifying a connection', function () {
|
||||
config([
|
||||
'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
|
||||
'tenancy.database.template_tenant_connection' => 'mysql',
|
||||
'database.connections.mysql2' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => 'mysql2',
|
||||
'port' => 3306,
|
||||
'database' => 'main',
|
||||
'username' => 'root',
|
||||
'password' => 'password',
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
]);
|
||||
|
||||
// Create a new random database user with privileges to use with mysql2 connection
|
||||
$username = 'dbuser' . Str::random(4);
|
||||
$password = Str::random('8');
|
||||
$mysql2DB = DB::connection('mysql2');
|
||||
$mysql2DB->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}';");
|
||||
$mysql2DB->statement("GRANT ALL PRIVILEGES ON *.* TO `{$username}`@`%` identified by '{$password}' WITH GRANT OPTION;");
|
||||
$mysql2DB->statement("FLUSH PRIVILEGES;");
|
||||
|
||||
DB::purge('mysql2'); // forget the mysql2 connection so that it uses the new credentials the next time
|
||||
|
||||
config(['database.connections.mysql2.username' => $username]);
|
||||
config(['database.connections.mysql2.password' => $password]);
|
||||
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
$name = 'foo' . Str::random(8);
|
||||
$usernameForNewDB = 'user_for_new_db' . Str::random(4);
|
||||
$passwordForNewDB = Str::random(8);
|
||||
$tenant = Tenant::create([
|
||||
'tenancy_db_name' => $name,
|
||||
'tenancy_db_connection' => 'mysql2',
|
||||
'tenancy_db_username' => $usernameForNewDB,
|
||||
'tenancy_db_password' => $passwordForNewDB,
|
||||
]);
|
||||
|
||||
/** @var PermissionControlledMySQLDatabaseManager $manager */
|
||||
$manager = $tenant->database()->manager();
|
||||
|
||||
expect($manager->database()->getConfig('username'))->toBe($username); // user created for the HOST connection
|
||||
expect($manager->userExists($usernameForNewDB))->toBeTrue();
|
||||
expect($manager->databaseExists($name))->toBeTrue();
|
||||
});
|
||||
|
||||
test('tenant database can be created by using the username and password from tenant config', function () {
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
config([
|
||||
'tenancy.database.managers.mysql' => MySQLDatabaseManager::class,
|
||||
'tenancy.database.template_tenant_connection' => 'mysql',
|
||||
]);
|
||||
|
||||
// Create a new random database user with privileges to use with `mysql` connection
|
||||
$username = 'dbuser' . Str::random(4);
|
||||
$password = Str::random('8');
|
||||
$mysqlDB = DB::connection('mysql');
|
||||
$mysqlDB->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}';");
|
||||
$mysqlDB->statement("GRANT ALL PRIVILEGES ON *.* TO `{$username}`@`%` identified by '{$password}' WITH GRANT OPTION;");
|
||||
$mysqlDB->statement("FLUSH PRIVILEGES;");
|
||||
|
||||
DB::purge('mysql2'); // forget the mysql2 connection so that it uses the new credentials the next time
|
||||
|
||||
// Remove `mysql` credentials to make sure we will be using the credentials from the tenant config
|
||||
config(['database.connections.mysql.username' => null]);
|
||||
config(['database.connections.mysql.password' => null]);
|
||||
|
||||
$name = 'foo' . Str::random(8);
|
||||
$tenant = Tenant::create([
|
||||
'tenancy_db_name' => $name,
|
||||
'tenancy_db_username' => $username,
|
||||
'tenancy_db_password' => $password,
|
||||
]);
|
||||
|
||||
/** @var MySQLDatabaseManager $manager */
|
||||
$manager = $tenant->database()->manager();
|
||||
|
||||
expect($manager->database()->getConfig('username'))->toBe($username); // user created for the HOST connection
|
||||
expect($manager->databaseExists($name))->toBeTrue();
|
||||
});
|
||||
|
||||
|
|
@ -241,11 +387,11 @@ test('path used by sqlite manager can be customized', function () {
|
|||
'tenancy_db_connection' => 'sqlite',
|
||||
]);
|
||||
|
||||
expect(file_exists( $customPath . '/' . $name))->toBeTrue();
|
||||
expect(file_exists($customPath . '/' . $name))->toBeTrue();
|
||||
});
|
||||
|
||||
// Datasets
|
||||
dataset('database_manager_provider', [
|
||||
dataset('database_managers', [
|
||||
['mysql', MySQLDatabaseManager::class],
|
||||
['mysql', PermissionControlledMySQLDatabaseManager::class],
|
||||
['sqlite', SQLiteDatabaseManager::class],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -83,6 +83,19 @@ test('tenant user can be impersonated on a tenant domain', function () {
|
|||
pest()->get('http://foo.localhost/dashboard')
|
||||
->assertSuccessful()
|
||||
->assertSee('You are logged in as Joe');
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeTrue();
|
||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||
|
||||
// Leave impersonation
|
||||
UserImpersonation::leave();
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
expect(session('tenancy_impersonating'))->toBeNull();
|
||||
|
||||
// Assert can't access the tenant dashboard
|
||||
pest()->get('http://foo.localhost/dashboard')
|
||||
->assertRedirect('http://foo.localhost/login');
|
||||
});
|
||||
|
||||
test('tenant user can be impersonated on a tenant path', function () {
|
||||
|
|
@ -116,6 +129,19 @@ test('tenant user can be impersonated on a tenant path', function () {
|
|||
pest()->get('/acme/dashboard')
|
||||
->assertSuccessful()
|
||||
->assertSee('You are logged in as Joe');
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeTrue();
|
||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||
|
||||
// Leave impersonation
|
||||
UserImpersonation::leave();
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
expect(session('tenancy_impersonating'))->toBeNull();
|
||||
|
||||
// Assert can't access the tenant dashboard
|
||||
pest()->get('/acme/dashboard')
|
||||
->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('tokens have a limited ttl', function () {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use Stancl\Tenancy\Facades\GlobalCache;
|
|||
use Stancl\Tenancy\Facades\Tenancy;
|
||||
use Stancl\Tenancy\TenancyServiceProvider;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
|
||||
|
||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||
{
|
||||
|
|
@ -104,15 +105,17 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
'--force' => true,
|
||||
],
|
||||
'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that
|
||||
'tenancy.bootstrappers.mail' => MailTenancyBootstrapper::class,
|
||||
'queue.connections.central' => [
|
||||
'driver' => 'sync',
|
||||
'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
|
||||
$app->singleton(MailTenancyBootstrapper::class);
|
||||
}
|
||||
|
||||
protected function getPackageProviders($app)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue