mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 19:54:04 +00:00
Merge branch 'master' into resource-syncing
This commit is contained in:
commit
e8071944ad
106 changed files with 1609 additions and 451 deletions
69
tests/ActionTest.php
Normal file
69
tests/ActionTest.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Database\Models\Tenant;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
|
||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||
|
||||
beforeEach(function () {
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
||||
// todo move these to be in the same file as the other tests from this PR (#909) rather than generic "action tests"
|
||||
|
||||
test('create storage symlinks action works', function() {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::create();
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$this->assertDirectoryDoesNotExist($publicPath = public_path("public-$tenantKey"));
|
||||
|
||||
CreateStorageSymlinksAction::handle($tenant);
|
||||
|
||||
$this->assertDirectoryExists($publicPath);
|
||||
$this->assertEquals(storage_path("app/public/"), readlink($publicPath));
|
||||
});
|
||||
|
||||
test('remove storage symlinks action works', function() {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::create();
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
CreateStorageSymlinksAction::handle($tenant);
|
||||
|
||||
$this->assertDirectoryExists($publicPath = public_path("public-$tenantKey"));
|
||||
|
||||
RemoveStorageSymlinksAction::handle($tenant);
|
||||
|
||||
$this->assertDirectoryDoesNotExist($publicPath);
|
||||
});
|
||||
|
|
@ -107,12 +107,12 @@ function contextIsSwitchedWhenTenancyInitialized()
|
|||
|
||||
class MyBootstrapper implements TenancyBootstrapper
|
||||
{
|
||||
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant)
|
||||
public function bootstrap(\Stancl\Tenancy\Contracts\Tenant $tenant): void
|
||||
{
|
||||
app()->instance('tenancy_initialized_for_tenant', $tenant->getTenantKey());
|
||||
}
|
||||
|
||||
public function revert()
|
||||
public function revert(): void
|
||||
{
|
||||
app()->instance('tenancy_ended', true);
|
||||
}
|
||||
|
|
|
|||
44
tests/BatchTest.php
Normal file
44
tests/BatchTest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Bus\BatchRepository;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
BatchTenancyBootstrapper::class,
|
||||
],
|
||||
]);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
||||
test('batch repository is set to tenant connection and reverted', function () {
|
||||
$tenant = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
expect(getBatchRepositoryConnectionName())->toBe('central');
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
expect(getBatchRepositoryConnectionName())->toBe('tenant');
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
expect(getBatchRepositoryConnectionName())->toBe('tenant');
|
||||
|
||||
tenancy()->end();
|
||||
expect(getBatchRepositoryConnectionName())->toBe('central');
|
||||
});
|
||||
|
||||
function getBatchRepositoryConnectionName()
|
||||
{
|
||||
return app(BatchRepository::class)->getConnection()->getName();
|
||||
}
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
|
@ -14,8 +14,14 @@ use Illuminate\Support\Facades\Storage;
|
|||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\TenantDeleted;
|
||||
use Stancl\Tenancy\Events\DeletingTenant;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
|
||||
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||
|
|
@ -184,24 +190,156 @@ test('filesystem data is separated', function () {
|
|||
expect($new_storage_path)->toEqual($expected_storage_path);
|
||||
});
|
||||
|
||||
test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function () {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
$tenant1 = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
$tenant1StorageUrl = 'http://localhost/public-' . $tenant1->getKey().'/';
|
||||
$tenant2StorageUrl = 'http://localhost/public-' . $tenant2->getKey().'/';
|
||||
|
||||
tenancy()->initialize($tenant1);
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant1StorageUrl,
|
||||
Storage::disk('public')->url('')
|
||||
);
|
||||
|
||||
Storage::disk('public')->put($tenant1FileName = 'tenant1.txt', 'text');
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant1StorageUrl . $tenant1FileName,
|
||||
Storage::disk('public')->url($tenant1FileName)
|
||||
);
|
||||
|
||||
tenancy()->initialize($tenant2);
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant2StorageUrl,
|
||||
Storage::disk('public')->url('')
|
||||
);
|
||||
|
||||
Storage::disk('public')->put($tenant2FileName = 'tenant2.txt', 'text');
|
||||
|
||||
$this->assertEquals(
|
||||
$tenant2StorageUrl . $tenant2FileName,
|
||||
Storage::disk('public')->url($tenant2FileName)
|
||||
);
|
||||
});
|
||||
|
||||
test('files can get fetched using the storage url', function() {
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
$tenant1 = Tenant::create();
|
||||
$tenant2 = Tenant::create();
|
||||
|
||||
pest()->artisan('tenants:link');
|
||||
|
||||
// First tenant
|
||||
tenancy()->initialize($tenant1);
|
||||
Storage::disk('public')->put($tenantFileName = 'tenant1.txt', $tenantKey = $tenant1->getTenantKey());
|
||||
|
||||
$url = Storage::disk('public')->url($tenantFileName);
|
||||
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant_id%', $tenantKey);
|
||||
$hostname = Str::of($url)->before($tenantDiskName);
|
||||
$parsedUrl = Str::of($url)->after($hostname);
|
||||
|
||||
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
||||
|
||||
// Second tenant
|
||||
tenancy()->initialize($tenant2);
|
||||
Storage::disk('public')->put($tenantFileName = 'tenant2.txt', $tenantKey = $tenant2->getTenantKey());
|
||||
|
||||
$url = Storage::disk('public')->url($tenantFileName);
|
||||
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant_id%', $tenantKey);
|
||||
$hostname = Str::of($url)->before($tenantDiskName);
|
||||
$parsedUrl = Str::of($url)->after($hostname);
|
||||
|
||||
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
||||
});
|
||||
|
||||
test('create and delete storage symlinks jobs work', function() {
|
||||
Event::listen(
|
||||
TenantCreated::class,
|
||||
JobPipeline::make([CreateStorageSymlinks::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener()
|
||||
);
|
||||
|
||||
Event::listen(
|
||||
TenantDeleted::class,
|
||||
JobPipeline::make([RemoveStorageSymlinks::class])->send(function (TenantDeleted $event) {
|
||||
return $event->tenant;
|
||||
})->toListener()
|
||||
);
|
||||
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
FilesystemTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = Tenant::create();
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$tenantKey = $tenant->getTenantKey();
|
||||
|
||||
$this->assertDirectoryExists(storage_path("app/public"));
|
||||
$this->assertEquals(storage_path("app/public/"), readlink(public_path("public-$tenantKey")));
|
||||
|
||||
$tenant->delete();
|
||||
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
|
||||
});
|
||||
|
||||
test('local storage public urls are generated correctly', function() {
|
||||
Event::listen(DeletingTenant::class, DeleteTenantStorage::class);
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
$tenantStoragePath = storage_path();
|
||||
|
||||
Storage::fake('test');
|
||||
|
||||
expect(File::isDirectory($tenantStoragePath))->toBeTrue();
|
||||
|
||||
Storage::put('test.txt', 'testing file');
|
||||
|
||||
tenant()->delete();
|
||||
|
||||
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
|
||||
});
|
||||
|
||||
function getDiskPrefix(string $disk): string
|
||||
{
|
||||
/** @var FilesystemAdapter $disk */
|
||||
$disk = Storage::disk($disk);
|
||||
$adapter = $disk->getAdapter();
|
||||
|
||||
if (! Str::startsWith(app()->version(), '9.')) {
|
||||
return $adapter->getPathPrefix();
|
||||
}
|
||||
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
|
||||
$prefixer->setAccessible(true);
|
||||
|
||||
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
|
||||
$prefixer->setAccessible(true);
|
||||
// reflection -> instance
|
||||
$prefixer = $prefixer->getValue($adapter);
|
||||
|
||||
// reflection -> instance
|
||||
$prefixer = $prefixer->getValue($adapter);
|
||||
$prefix = (new ReflectionProperty($prefixer, 'prefix'));
|
||||
$prefix->setAccessible(true);
|
||||
|
||||
$prefix = (new ReflectionProperty($prefixer, 'prefix'));
|
||||
$prefix->setAccessible(true);
|
||||
|
||||
return $prefix->getValue($prefixer);
|
||||
return $prefix->getValue($prefixer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
|
@ -16,6 +18,8 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\Tests\Etc\TestSeeder;
|
||||
use Stancl\Tenancy\Tests\Etc\User;
|
||||
|
||||
beforeEach(function () {
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
|
|
@ -26,6 +30,15 @@ beforeEach(function () {
|
|||
DatabaseTenancyBootstrapper::class,
|
||||
]]);
|
||||
|
||||
config([
|
||||
'tenancy.bootstrappers' => [
|
||||
DatabaseTenancyBootstrapper::class,
|
||||
],
|
||||
'tenancy.filesystem.suffix_base' => 'tenant-',
|
||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||
'tenancy.filesystem.url_override.public' => 'public-%tenant_id%'
|
||||
]);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
|
@ -40,9 +53,9 @@ afterEach(function () {
|
|||
test('migrate command doesnt change the db connection', function () {
|
||||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
|
||||
$old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
|
||||
$old_connection_name = app(DatabaseManager::class)->connection()->getName();
|
||||
Artisan::call('tenants:migrate');
|
||||
$new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
|
||||
$new_connection_name = app(DatabaseManager::class)->connection()->getName();
|
||||
|
||||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
expect($new_connection_name)->toEqual($old_connection_name);
|
||||
|
|
@ -115,8 +128,22 @@ test('rollback command works', function () {
|
|||
expect(Schema::hasTable('users'))->toBeFalse();
|
||||
});
|
||||
|
||||
// Incomplete test
|
||||
test('seed command works');
|
||||
test('seed command works', function (){
|
||||
$tenant = Tenant::create();
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
$tenant->run(function (){
|
||||
expect(DB::table('users')->count())->toBe(0);
|
||||
});
|
||||
|
||||
Artisan::call('tenants:seed', ['--class' => TestSeeder::class]);
|
||||
|
||||
$tenant->run(function (){
|
||||
$user = DB::table('users');
|
||||
expect($user->count())->toBe(1)
|
||||
->and($user->first()->email)->toBe('seeded@user');
|
||||
});
|
||||
});
|
||||
|
||||
test('database connection is switched to default', function () {
|
||||
databaseConnectionSwitchedToDefault();
|
||||
|
|
@ -175,8 +202,69 @@ test('run command with array of tenants works', function () {
|
|||
Artisan::call('tenants:migrate-fresh');
|
||||
|
||||
pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'")
|
||||
->expectsOutput('Tenant: ' . $tenantId1)
|
||||
->expectsOutput('Tenant: ' . $tenantId2);
|
||||
->expectsOutputToContain('Tenant: ' . $tenantId1)
|
||||
->expectsOutputToContain('Tenant: ' . $tenantId2)
|
||||
->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('link command works', function() {
|
||||
$tenantId1 = Tenant::create()->getTenantKey();
|
||||
$tenantId2 = Tenant::create()->getTenantKey();
|
||||
pest()->artisan('tenants:link');
|
||||
|
||||
$this->assertDirectoryExists(storage_path("tenant-$tenantId1/app/public"));
|
||||
$this->assertEquals(storage_path("tenant-$tenantId1/app/public/"), readlink(public_path("public-$tenantId1")));
|
||||
|
||||
$this->assertDirectoryExists(storage_path("tenant-$tenantId2/app/public"));
|
||||
$this->assertEquals(storage_path("tenant-$tenantId2/app/public/"), readlink(public_path("public-$tenantId2")));
|
||||
|
||||
pest()->artisan('tenants:link', [
|
||||
'--remove' => true,
|
||||
]);
|
||||
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId1"));
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantId2"));
|
||||
});
|
||||
|
||||
test('link command works with a specified tenant', function() {
|
||||
$tenantKey = Tenant::create()->getTenantKey();
|
||||
|
||||
pest()->artisan('tenants:link', [
|
||||
'--tenants' => [$tenantKey],
|
||||
]);
|
||||
|
||||
$this->assertDirectoryExists(storage_path("tenant-$tenantKey/app/public"));
|
||||
$this->assertEquals(storage_path("tenant-$tenantKey/app/public/"), readlink(public_path("public-$tenantKey")));
|
||||
|
||||
pest()->artisan('tenants:link', [
|
||||
'--remove' => true,
|
||||
'--tenants' => [$tenantKey],
|
||||
]);
|
||||
|
||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
|
||||
});
|
||||
|
||||
test('run command works when sub command asks questions and accepts arguments', function () {
|
||||
$tenant = Tenant::create();
|
||||
$id = $tenant->getTenantKey();
|
||||
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
pest()->artisan("tenants:run --tenants=$id 'user:addwithname Abrar' ")
|
||||
->expectsQuestion('What is your email?', 'email@localhost')
|
||||
->expectsOutput("Tenant: $id")
|
||||
->expectsOutput("User created: Abrar(email@localhost)");
|
||||
|
||||
// Assert we are in central context
|
||||
expect(tenancy()->initialized)->toBeFalse();
|
||||
|
||||
// Assert user was created in tenant context
|
||||
tenancy()->initialize($tenant);
|
||||
$user = User::first();
|
||||
|
||||
// Assert user is same as provided using the command
|
||||
expect($user->name)->toBe('Abrar');
|
||||
expect($user->email)->toBe('email@localhost');
|
||||
});
|
||||
|
||||
// todo@tests
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ test('tenant can be identified by domain', function () {
|
|||
|
||||
test('onfail logic can be customized', function () {
|
||||
InitializeTenancyByDomain::$onFail = function () {
|
||||
return 'foo';
|
||||
return response('foo');
|
||||
};
|
||||
|
||||
pest()
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc;
|
||||
namespace Stancl\Tenancy\Tests\Etc\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Concerns\HasATenantsOption;
|
||||
use Stancl\Tenancy\Concerns\TenantAwareCommand;
|
||||
use Stancl\Tenancy\Tests\Etc\User;
|
||||
|
||||
class AddUserCommand extends Command
|
||||
{
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc;
|
||||
namespace Stancl\Tenancy\Tests\Etc\Console;
|
||||
|
||||
use Orchestra\Testbench\Foundation\Console\Kernel;
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ class ConsoleKernel extends Kernel
|
|||
{
|
||||
protected $commands = [
|
||||
ExampleCommand::class,
|
||||
ExampleQuestionCommand::class,
|
||||
AddUserCommand::class,
|
||||
];
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc;
|
||||
namespace Stancl\Tenancy\Tests\Etc\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
46
tests/Etc/Console/ExampleQuestionCommand.php
Normal file
46
tests/Etc/Console/ExampleQuestionCommand.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Stancl\Tenancy\Tests\Etc\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\Tenancy\Tests\Etc\User;
|
||||
|
||||
class ExampleQuestionCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:addwithname {name}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$email = $this->ask('What is your email?');
|
||||
|
||||
User::create([
|
||||
'name' => $this->argument('name'),
|
||||
'email' => $email,
|
||||
'email_verified_at' => now(),
|
||||
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
|
||||
'remember_token' => Str::random(10),
|
||||
]);
|
||||
|
||||
$this->line("User created: ". $this->argument('name') . "($email)");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,13 @@ 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\MaintenanceMode;
|
||||
use Stancl\Tenancy\Database\Models;
|
||||
|
||||
/**
|
||||
* @method static static create(array $attributes = [])
|
||||
*/
|
||||
class Tenant extends Models\Tenant implements TenantWithDatabase
|
||||
{
|
||||
use HasDatabase, HasDomains;
|
||||
use HasDatabase, HasDomains, MaintenanceMode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,3 +50,17 @@ test('global cache manager stores data in global cache', function () {
|
|||
expect(cache('def'))->toBe('ghi');
|
||||
});
|
||||
|
||||
test('the global_cache helper supports the same syntax as the cache helper', function () {
|
||||
$tenant = Tenant::create();
|
||||
$tenant->enter();
|
||||
|
||||
expect(cache('foo'))->toBe(null); // tenant cache is empty
|
||||
|
||||
global_cache(['foo' => 'bar']);
|
||||
expect(global_cache('foo'))->toBe('bar');
|
||||
|
||||
global_cache()->set('foo', 'baz');
|
||||
expect(global_cache()->get('foo'))->toBe('baz');
|
||||
|
||||
expect(cache('foo'))->toBe(null); // tenant cache is not affected
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
test('tenant can be in maintenance mode', function () {
|
||||
test('tenants can be in maintenance mode', function () {
|
||||
Route::get('/foo', function () {
|
||||
return 'bar';
|
||||
})->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
|
||||
|
|
@ -19,16 +19,40 @@ test('tenant can be in maintenance mode', function () {
|
|||
'domain' => 'acme.localhost',
|
||||
]);
|
||||
|
||||
pest()->get('http://acme.localhost/foo')
|
||||
->assertSuccessful();
|
||||
|
||||
tenancy()->end(); // flush stored tenant instance
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(200);
|
||||
|
||||
$tenant->putDownForMaintenance();
|
||||
|
||||
pest()->expectException(HttpException::class);
|
||||
pest()->withoutExceptionHandling()
|
||||
->get('http://acme.localhost/foo');
|
||||
tenancy()->end(); // End tenancy before making a request
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(503);
|
||||
|
||||
$tenant->bringUpFromMaintenance();
|
||||
|
||||
tenancy()->end(); // End tenancy before making a request
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(200);
|
||||
});
|
||||
|
||||
test('tenants can be put into maintenance mode using artisan commands', function() {
|
||||
Route::get('/foo', function () {
|
||||
return 'bar';
|
||||
})->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
|
||||
|
||||
$tenant = MaintenanceTenant::create();
|
||||
$tenant->domains()->create([
|
||||
'domain' => 'acme.localhost',
|
||||
]);
|
||||
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(200);
|
||||
|
||||
Artisan::call('tenants:down');
|
||||
|
||||
tenancy()->end(); // End tenancy before making a request
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(503);
|
||||
|
||||
Artisan::call('tenants:up');
|
||||
|
||||
tenancy()->end(); // End tenancy before making a request
|
||||
pest()->get('http://acme.localhost/foo')->assertStatus(200);
|
||||
});
|
||||
|
||||
class MaintenanceTenant extends Tenant
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ beforeEach(function () {
|
|||
], function () {
|
||||
Route::get('/foo/{a}/{b}', function ($a, $b) {
|
||||
return "$a + $b";
|
||||
});
|
||||
})->name('foo');
|
||||
|
||||
Route::get('/baz/{a}/{b}', function ($a, $b) {
|
||||
return "$a - $b";
|
||||
})->name('baz');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -67,7 +71,7 @@ test('exception is thrown when tenant cannot be identified by path', function ()
|
|||
|
||||
test('onfail logic can be customized', function () {
|
||||
InitializeTenancyByPath::$onFail = function () {
|
||||
return 'foo';
|
||||
return response('foo');
|
||||
};
|
||||
|
||||
pest()
|
||||
|
|
@ -123,3 +127,23 @@ test('tenant parameter name can be customized', function () {
|
|||
->withoutExceptionHandling()
|
||||
->get('/acme/foo/abc/xyz');
|
||||
});
|
||||
|
||||
test('tenant parameter is set for all routes as the default parameter once the tenancy initialized', function () {
|
||||
Tenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
||||
expect(tenancy()->initialized)->toBeFalse();
|
||||
|
||||
// make a request that will initialize tenancy
|
||||
pest()->get(route('foo', ['tenant' => 'acme', 'a' => 1, 'b' => 2]));
|
||||
|
||||
expect(tenancy()->initialized)->toBeTrue();
|
||||
expect(tenant('id'))->toBe('acme');
|
||||
|
||||
// assert that the route WITHOUT the tenant parameter matches the route WITH the tenant parameter
|
||||
expect(route('baz', ['a' => 1, 'b' => 2]))->toBe(route('baz', ['tenant' => 'acme', 'a' => 1, 'b' => 2]));
|
||||
|
||||
expect(route('baz', ['a' => 1, 'b' => 2]))->toBe('http://localhost/acme/baz/1/2'); // assert the full route string
|
||||
pest()->get(route('baz', ['a' => 1, 'b' => 2]))->assertOk(); // Assert route don't need tenant parameter
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ test('header identification works', function () {
|
|||
});
|
||||
|
||||
test('query parameter identification works', function () {
|
||||
InitializeTenancyByRequestData::$header = null;
|
||||
InitializeTenancyByRequestData::$queryParameter = 'tenant';
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ test('secondary models are not scoped to the current tenant when accessed direct
|
|||
expect(Comment::count())->toBe(2);
|
||||
});
|
||||
|
||||
test('secondary models a r e scoped to the current tenant when accessed directly and parent relationship traitis used', function () {
|
||||
test('secondary models ARE scoped to the current tenant when accessed directly and parent relationship trait is used', function () {
|
||||
$acme = Tenant::create([
|
||||
'id' => 'acme',
|
||||
]);
|
||||
|
|
@ -207,13 +207,13 @@ test('the model returned by the tenant helper has unique and exists validation r
|
|||
$uniqueFails = Validator::make($data, [
|
||||
'slug' => 'unique:posts',
|
||||
])->fails();
|
||||
$existsFails = Validator::make($data, [
|
||||
$existsPass = Validator::make($data, [
|
||||
'slug' => 'exists:posts',
|
||||
])->fails();
|
||||
])->passes();
|
||||
|
||||
// Assert that 'unique' and 'exists' aren't scoped by default
|
||||
// pest()->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
|
||||
// pest()->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
|
||||
expect($uniqueFails)->toBeTrue(); // Expect unique rule failed to pass because slug 'foo' already exists
|
||||
expect($existsPass)->toBeTrue(); // Expect exists rule pass because slug 'foo' exists
|
||||
|
||||
$uniqueFails = Validator::make($data, [
|
||||
'slug' => tenant()->unique('posts'),
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ test('tenant can be identified by subdomain', function () {
|
|||
|
||||
test('onfail logic can be customized', function () {
|
||||
InitializeTenancyBySubdomain::$onFail = function () {
|
||||
return 'foo';
|
||||
return response('foo');
|
||||
};
|
||||
|
||||
pest()
|
||||
|
|
|
|||
|
|
@ -154,9 +154,7 @@ test('schema manager uses schema to separate tenant dbs', function () {
|
|||
]);
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
$schemaConfig = version_compare(app()->version(), '9.0', '>=') ?
|
||||
config('database.connections.' . config('database.default') . '.search_path') :
|
||||
config('database.connections.' . config('database.default') . '.schema');
|
||||
$schemaConfig = config('database.connections.' . config('database.default') . '.search_path');
|
||||
|
||||
expect($schemaConfig)->toBe($tenant->database()->getName());
|
||||
expect(config(['database.connections.pgsql.database']))->toBe($originalDatabaseName);
|
||||
|
|
@ -225,7 +223,25 @@ test('tenant database can be created on a foreign server', function () {
|
|||
});
|
||||
|
||||
test('path used by sqlite manager can be customized', function () {
|
||||
pest()->markTestIncomplete();
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
// Set custom path for SQLite file
|
||||
SQLiteDatabaseManager::$path = $customPath = database_path('custom_' . Str::random(8));
|
||||
|
||||
if (! is_dir($customPath)) {
|
||||
// Create custom directory
|
||||
mkdir($customPath);
|
||||
}
|
||||
|
||||
$name = Str::random(8). '.sqlite';
|
||||
Tenant::create([
|
||||
'tenancy_db_name' => $name,
|
||||
'tenancy_db_connection' => 'sqlite',
|
||||
]);
|
||||
|
||||
expect(file_exists( $customPath . '/' . $name))->toBeTrue();
|
||||
});
|
||||
|
||||
// Datasets
|
||||
|
|
|
|||
|
|
@ -4,25 +4,27 @@ declare(strict_types=1);
|
|||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterval;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Auth\TokenGuard;
|
||||
use Illuminate\Auth\SessionGuard;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Database\Models\ImpersonationToken;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Features\UserImpersonation;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Features\UserImpersonation;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Illuminate\Foundation\Auth\User as Authenticable;
|
||||
use Stancl\Tenancy\Database\Models\ImpersonationToken;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Exceptions\StatefulGuardRequiredException;
|
||||
|
||||
beforeEach(function () {
|
||||
pest()->artisan('migrate', [
|
||||
|
|
@ -223,6 +225,46 @@ test('impersonation works with multiple models and guards', function () {
|
|||
});
|
||||
});
|
||||
|
||||
test('impersonation tokens can be created only with stateful guards', function () {
|
||||
config([
|
||||
'auth.guards' => [
|
||||
'nonstateful' => [
|
||||
'driver' => 'nonstateful',
|
||||
'provider' => 'provider',
|
||||
],
|
||||
'stateful' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'provider',
|
||||
],
|
||||
],
|
||||
'auth.providers.provider' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => ImpersonationUser::class,
|
||||
],
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
migrateTenants();
|
||||
|
||||
$user = $tenant->run(function () {
|
||||
return ImpersonationUser::create([
|
||||
'name' => 'Joe',
|
||||
'email' => 'joe@local',
|
||||
'password' => bcrypt('secret'),
|
||||
]);
|
||||
});
|
||||
|
||||
Auth::extend('nonstateful', fn($app, $name, array $config) => new TokenGuard(Auth::createUserProvider($config['provider']), request()));
|
||||
|
||||
expect(fn() => tenancy()->impersonate($tenant, $user->id, '/dashboard', 'nonstateful'))
|
||||
->toThrow(StatefulGuardRequiredException::class);
|
||||
|
||||
Auth::extend('stateful', fn ($app, $name, array $config) => new SessionGuard($name, Auth::createUserProvider($config['provider']), session()));
|
||||
|
||||
expect(tenancy()->impersonate($tenant, $user->id, '/dashboard', 'stateful'))
|
||||
->toBeInstanceOf(ImpersonationToken::class);
|
||||
});
|
||||
|
||||
function migrateTenants()
|
||||
{
|
||||
pest()->artisan('tenants:migrate')->assertExitCode(0);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,15 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Tests;
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use PDO;
|
||||
use Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Facades\GlobalCache;
|
||||
use Stancl\Tenancy\Facades\Tenancy;
|
||||
use Stancl\Tenancy\TenancyServiceProvider;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||
|
|
@ -42,13 +49,13 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
/**
|
||||
* Define environment setup.
|
||||
*
|
||||
* @param \Illuminate\Foundation\Application $app
|
||||
* @param Application $app
|
||||
* @return void
|
||||
*/
|
||||
protected function getEnvironmentSetUp($app)
|
||||
{
|
||||
if (file_exists(__DIR__ . '/../.env')) {
|
||||
\Dotenv\Dotenv::createImmutable(__DIR__ . '/..')->load();
|
||||
Dotenv::createImmutable(__DIR__ . '/..')->load();
|
||||
}
|
||||
|
||||
$app['config']->set([
|
||||
|
|
@ -96,7 +103,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
'--realpath' => true,
|
||||
'--force' => true,
|
||||
],
|
||||
'tenancy.bootstrappers.redis' => \Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class,
|
||||
'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo0 change this to []? two tests in TenantDatabaseManagerTest are failing with that
|
||||
'queue.connections.central' => [
|
||||
'driver' => 'sync',
|
||||
'central' => true,
|
||||
|
|
@ -105,28 +112,28 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
'tenancy.tenant_model' => Tenant::class, // Use test tenant w/ DBs & domains
|
||||
]);
|
||||
|
||||
$app->singleton(\Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class);
|
||||
$app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
|
||||
}
|
||||
|
||||
protected function getPackageProviders($app)
|
||||
{
|
||||
return [
|
||||
\Stancl\Tenancy\TenancyServiceProvider::class,
|
||||
TenancyServiceProvider::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getPackageAliases($app)
|
||||
{
|
||||
return [
|
||||
'Tenancy' => \Stancl\Tenancy\Facades\Tenancy::class,
|
||||
'GlobalCache' => \Stancl\Tenancy\Facades\GlobalCache::class,
|
||||
'Tenancy' => Tenancy::class,
|
||||
'GlobalCache' => GlobalCache::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve application HTTP Kernel implementation.
|
||||
*
|
||||
* @param \Illuminate\Foundation\Application $app
|
||||
* @param Application $app
|
||||
* @return void
|
||||
*/
|
||||
protected function resolveApplicationHttpKernel($app)
|
||||
|
|
@ -137,12 +144,12 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
/**
|
||||
* Resolve application Console Kernel implementation.
|
||||
*
|
||||
* @param \Illuminate\Foundation\Application $app
|
||||
* @param Application $app
|
||||
* @return void
|
||||
*/
|
||||
protected function resolveApplicationConsoleKernel($app)
|
||||
{
|
||||
$app->singleton('Illuminate\Contracts\Console\Kernel', Etc\ConsoleKernel::class);
|
||||
$app->singleton('Illuminate\Contracts\Console\Kernel', Etc\Console\ConsoleKernel::class);
|
||||
}
|
||||
|
||||
public function randomString(int $length = 10)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue