1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 09:34:04 +00:00

Bootstrapper tests

This commit is contained in:
Samuel Štancl 2020-05-11 03:37:47 +02:00
parent 73fc525126
commit 6f4b9f486c
20 changed files with 266 additions and 79 deletions

View file

@ -8,7 +8,7 @@ use Stancl\Tenancy\Database\Models\Tenant;
return [
'tenant_model' => Tenant::class,
'domain_model' => Domain::class,
'internal_prefix' => 'tenancy_',
'internal_column_prefix' => 'tenancy_',
'central_connection' => 'central',
'template_tenant_connection' => null,

View file

@ -16,6 +16,9 @@ services:
DB_PASSWORD: password
DB_USERNAME: root
DB_DATABASE: main
TENANCY_TEST_REDIS_HOST: redis
TENANCY_TEST_MYSQL_HOST: mysql
TENANCY_TEST_PGSQL_HOST: postgres
stdin_open: true
tty: true
mysql:

View file

@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Console\Migrations\MigrateCommand;
use Illuminate\Database\Migrations\Migrator;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Events\DatabaseMigrated;
use Stancl\Tenancy\Traits\DealsWithMigrations;
@ -56,15 +57,20 @@ class Migrate extends MigrateCommand
return;
}
tenancy()->all($this->option('tenants'))->each(function ($tenant) {
$this->line("Tenant: {$tenant['id']}");
tenancy()
->query()
->when($this->option('tenants'), function ($query) {
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
})
->each(function (TenantWithDatabase $tenant) {
$this->line("Tenant: {$tenant['id']}");
$tenant->run(function () {
// Migrate
parent::handle();
$tenant->run(function () {
// Migrate
parent::handle();
});
event(new DatabaseMigrated($tenant));
});
event(new DatabaseMigrated($tenant));
});
}
}

View file

@ -2,8 +2,12 @@
namespace Stancl\Tenancy\Contracts;
/**
* @see \Stancl\Tenancy\Database\Models\Tenant
*/
interface Tenant
{
public function getTenantKeyName(): string;
public function getTenantKey(): string;
public function run(callable $callback);
}

View file

@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts;
use Stancl\Tenancy\Tenant;
interface TenantDatabaseManager
{
/**
@ -18,12 +16,12 @@ interface TenantDatabaseManager
/**
* Create a database.
*/
public function createDatabase(Tenant $tenant): bool;
public function createDatabase(TenantWithDatabase $tenant): bool;
/**
* Delete a database.
*/
public function deleteDatabase(Tenant $tenant): bool;
public function deleteDatabase(TenantWithDatabase $tenant): bool;
/**
* Does a database exist.

View file

@ -8,7 +8,7 @@ use Stancl\Tenancy\Events;
use Stancl\Tenancy\Contracts;
// todo @property
class Tenant extends Model implements Contracts\Tenant
class Tenant extends Model implements Contracts\TenantWithDatabase
{
use Concerns\CentralConnection, Concerns\HasADataColumn, Concerns\GeneratesIds, Concerns\HasADataColumn {
Concerns\HasADataColumn::getCasts as dataColumnCasts;
@ -41,7 +41,7 @@ class Tenant extends Model implements Contracts\Tenant
public static function internalPrefix(): string
{
return config('tenancy.database_prefix');
return config('tenancy.internal_column_prefix');
}
/**
@ -76,15 +76,15 @@ class Tenant extends Model implements Contracts\Tenant
public function run(callable $callback)
{
// todo new logic with the manager
$originalTenant = $this->manager->getTenant();
$originalTenant = tenant();
$this->manager->initializeTenancy($this);
tenancy()->initialize($this);
$result = $callback($this);
$this->manager->endTenancy($this);
if ($originalTenant) {
$this->manager->initializeTenancy($originalTenant);
tenancy()->initialize($originalTenant);
} else {
tenancy()->end();
}
return $result;

View file

@ -93,7 +93,7 @@ class DatabaseConfig
{
return $this->tenant->getInternal('db_connection')
?? config('tenancy.template_tenant_connection')
?? DatabaseManager::$originalDefaultConnectionName;
?? config('tenancy.central_connection');
}
/**
@ -105,6 +105,7 @@ class DatabaseConfig
$templateConnection = config("database.connections.{$template}");
// todo move a lot of this logic to the tenant DB manager so that we dont have to deal with the separators & modifying DB names here
$databaseName = $this->getName();
if (($manager = $this->manager()) instanceof ModifiesDatabaseNameForConnection) {
/** @var ModifiesDatabaseNameForConnection $manager */

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy;
use Closure;
use Illuminate\Config\Repository;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
use Illuminate\Foundation\Application;
@ -18,58 +19,43 @@ use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter;
/**
* @internal Class is subject to breaking changes in minor and patch versions.
*/
// todo rewrite everything
class DatabaseManager
{
/** @var string */
public static $originalDefaultConnectionName;
/** @var Application */
protected $app;
/** @var BaseDatabaseManager */
protected $database;
/** @var TenantManager */
protected $tenancy;
/** @var Repository */
protected $config;
public function __construct(Application $app, BaseDatabaseManager $database)
public function __construct(Application $app, BaseDatabaseManager $database, Repository $config)
{
$this->app = $app;
$this->database = $database;
static::$originalDefaultConnectionName = $app['config']['database.default'];
}
/**
* Set the TenantManager instance, used to dispatch tenancy events.
*/
public function withTenantManager(Tenancy $tenantManager): self
{
$this->tenancy = $tenantManager;
return $this;
$this->config = $config;
}
/**
* Connect to a tenant's database.
*/
public function connect(TenantWithDatabase $tenant)
public function connectToTenant(TenantWithDatabase $tenant)
{
$this->createTenantConnection($tenant);
$this->setDefaultConnection('tenant');
$this->switchConnection('tenant');
$this->database->purge('tenant');
}
/**
* Reconnect to the default non-tenant connection.
*/
public function reconnect()
public function reconnectToCentral()
{
if ($this->tenancy->initialized) {
if (tenancy()->initialized) {
$this->database->purge('tenant');
}
$this->setDefaultConnection(static::$originalDefaultConnectionName);
$this->switchConnection(static::$originalDefaultConnectionName);
$this->setDefaultConnection($this->config->get('tenancy.central_connection'));
}
/**
@ -78,25 +64,17 @@ class DatabaseManager
public function setDefaultConnection(string $connection)
{
$this->app['config']['database.default'] = $connection;
$this->database->setDefaultConnection($connection);
}
/**
* Create the tenant database connection.
*/
public function createTenantConnection(Tenant $tenant)
public function createTenantConnection(TenantWithDatabase $tenant)
{
$this->app['config']['database.connections.tenant'] = $tenant->database()->connection();
}
/**
* Switch the application's connection.
*/
public function switchConnection(string $connection)
{
$this->database->reconnect($connection);
$this->database->setDefaultConnection($connection);
}
/**
* Check if a tenant can be created.
*
@ -104,7 +82,7 @@ class DatabaseManager
* @throws DatabaseManagerNotRegisteredException
* @throws TenantDatabaseAlreadyExistsException
*/
public function ensureTenantCanBeCreated(Tenant $tenant): void
public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void
{
if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) {
throw new TenantDatabaseAlreadyExistsException($database);
@ -119,8 +97,9 @@ class DatabaseManager
* @return void
* @throws DatabaseManagerNotRegisteredException
*/
public function createDatabase(Tenant $tenant, array $afterCreating = [])
public function createDatabase(TenantWithDatabase $tenant, array $afterCreating = [])
{
// todo get rid of aftercreating logic
$afterCreating = array_merge(
$afterCreating,
$this->tenancy->event('database.creating', $tenant->database()->getName(), $tenant)
@ -168,7 +147,7 @@ class DatabaseManager
*
* @throws DatabaseManagerNotRegisteredException
*/
public function deleteDatabase(Tenant $tenant)
public function deleteDatabase(TenantWithDatabase $tenant)
{
$database = $tenant->database()->getName();
$manager = $tenant->database()->manager();

View file

@ -3,7 +3,7 @@
namespace Stancl\Tenancy\Events\Contracts;
use Illuminate\Queue\SerializesModels;
use Stancl\Tenancy\Database\Models\Domain;
use Stancl\Tenancy\Contracts\Domain;
abstract class DomainEvent
{

View file

@ -3,7 +3,7 @@
namespace Stancl\Tenancy\Events\Contracts;
use Illuminate\Queue\SerializesModels;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Contracts\Tenant;
abstract class TenantEvent
{

View file

@ -61,7 +61,7 @@ class JobPipeline implements ShouldQueue
public function handle(): void
{
foreach ($this->jobs as $job) {
app($job)->handle($this->passable);
app()->call([new $job(...$this->passable), 'handle']);
}
}
@ -82,7 +82,10 @@ class JobPipeline implements ShouldQueue
{
$clone = clone $this;
$clone->passable = ($clone->send)(...$listenerArgs);
$passable = ($clone->send)(...$listenerArgs);
$passable = is_array($passable) ? $passable : [$passable];
$clone->passable = $passable;
unset($clone->send);
return $clone;

View file

@ -6,16 +6,18 @@ namespace Stancl\Tenancy\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Contracts\Tenant;
class CreateDatabase implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var Tenant */
/** @var TenantWithDatabase|Model */
protected $tenant;
public function __construct(Tenant $tenant)

View file

@ -2,12 +2,14 @@
namespace Stancl\Tenancy;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
class Tenancy
{
/** @var Tenant|null */
/** @var Tenant|Model|null */
public $tenant;
/** @var callable|null */
@ -48,4 +50,17 @@ class Tenancy
return $resolve($this->tenant);
}
public function query(): Builder
{
return $this->model()->query();
}
/** @return Tenant|Model */
public function model()
{
$class = config('tenancy.tenant_model');
return new $class;
}
}

View file

@ -6,6 +6,7 @@ namespace Stancl\Tenancy\TenancyBootstrappers;
use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Cache;
use Stancl\Tenancy\CacheManager as TenantCacheManager;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
@ -25,6 +26,8 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
public function start(Tenant $tenant)
{
$this->resetFacadeCache();
$this->originalCache = $this->originalCache ?? $this->app['cache'];
$this->app->extend('cache', function () {
return new TenantCacheManager($this->app);
@ -33,10 +36,22 @@ class CacheTenancyBootstrapper implements TenancyBootstrapper
public function end()
{
$this->resetFacadeCache();
$this->app->extend('cache', function () {
return $this->originalCache;
});
$this->originalCache = null;
}
/**
* This wouldn't be necessary, but is needed when a call to the
* facade has been made prior to bootstrapping tenancy. The
* facade has its own cache, separate from the container.
*/
public function resetFacadeCache()
{
Cache::clearResolvedInstances();
}
}

View file

@ -8,6 +8,7 @@ use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Exceptions\TenantDatabaseDoesNotExistException;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Contracts\Tenant;
class DatabaseTenancyBootstrapper implements TenancyBootstrapper
{
@ -19,8 +20,10 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
$this->database = $database;
}
public function start(TenantWithDatabase $tenant)
public function start(Tenant $tenant)
{
/** @var TenantWithDatabase $tenant */
$database = $tenant->database()->getName();
if (! $tenant->database()->manager()->databaseExists($database)) {
throw new TenantDatabaseDoesNotExistException($database);
@ -31,6 +34,6 @@ class DatabaseTenancyBootstrapper implements TenancyBootstrapper
public function end()
{
$this->database->revertToCentral();
$this->database->reconnectToCentral();
}
}

View file

@ -8,10 +8,9 @@ use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Database\TenantObserver;
use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
class TenancyServiceProvider extends ServiceProvider
{

View file

@ -6,7 +6,7 @@ namespace Stancl\Tenancy\TenantDatabaseManagers;
use Stancl\Tenancy\Contracts\ModifiesDatabaseNameForConnection;
use Stancl\Tenancy\Contracts\TenantDatabaseManager;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
class SQLiteDatabaseManager implements TenantDatabaseManager, ModifiesDatabaseNameForConnection
{
@ -15,7 +15,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager, ModifiesDatabaseNa
return 'database';
}
public function createDatabase(Tenant $tenant): bool
public function createDatabase(TenantWithDatabase $tenant): bool
{
try {
return fclose(fopen(database_path($tenant->database()->getName()), 'w'));
@ -24,7 +24,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager, ModifiesDatabaseNa
}
}
public function deleteDatabase(Tenant $tenant): bool
public function deleteDatabase(TenantWithDatabase $tenant): bool
{
try {
return unlink(database_path($tenant->database()->getName()));

View file

@ -2,8 +2,8 @@
declare(strict_types=1);
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Tenancy;
use Stancl\Tenancy\Contracts\Tenant;
if (! function_exists('tenancy')) {
/** @return Tenancy */
@ -18,10 +18,14 @@ if (! function_exists('tenant')) {
* Get a key from the current tenant's storage.
*
* @param string|null $key
* @return Tenant|mixed
* @return Tenant|null|mixed
*/
function tenant($key = null)
{
if (! app()->bound(Tenant::class)) {
return null;
}
if (is_null($key)) {
return app(Tenant::class);
}

View file

@ -2,37 +2,185 @@
namespace Stancl\Tenancy\Tests\v3;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Events\Listeners\JobPipeline;
use Stancl\Tenancy\Events\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\TenancyBootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Tests\TestCase;
class BootstrapperTest extends TestCase
{
public $mockConsoleOutput = false;
public function setUp(): void
{
parent::setUp();
Event::listen(
TenantCreated::class,
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener()
);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
}
/** @test */
public function database_data_is_separated()
{
config(['tenancy.bootstrappers' => [
DatabaseTenancyBootstrapper::class
]]);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
$this->artisan('tenants:migrate');
tenancy()->initialize($tenant1);
// Create Foo user
DB::table('users')->insert(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
$this->assertCount(1, DB::table('users')->get());
tenancy()->initialize($tenant2);
// Assert Foo user is not in this DB
$this->assertCount(0, DB::table('users')->get());
// Create Bar user
DB::table('users')->insert(['name' => 'Bar', 'email' => 'bar@bar.com', 'password' => 'secret']);
$this->assertCount(1, DB::table('users')->get());
tenancy()->initialize($tenant1);
// Assert Bar user is not in this DB
$this->assertCount(1, DB::table('users')->get());
$this->assertSame('Foo', DB::table('users')->first()->name);
}
/** @test */
public function cache_data_is_separated()
{
config([
'tenancy.bootstrappers' => [
CacheTenancyBootstrapper::class
],
'cache.default' => 'redis',
]);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
cache()->set('foo', 'central');
$this->assertSame('central', Cache::get('foo'));
tenancy()->initialize($tenant1);
// Assert central cache doesn't leak to tenant context
$this->assertFalse(Cache::has('foo'));
cache()->set('foo', 'bar');
$this->assertSame('bar', Cache::get('foo'));
tenancy()->initialize($tenant2);
// Assert one tenant's data doesn't leak to another tenant
$this->assertFalse(Cache::has('foo'));
cache()->set('foo', 'xyz');
$this->assertSame('xyz', Cache::get('foo'));
tenancy()->initialize($tenant1);
// Asset data didn't leak to original tenant
$this->assertSame('bar', Cache::get('foo'));
tenancy()->end();
// Asset central is still the same
$this->assertSame('central', Cache::get('foo'));
}
/** @test */
public function redis_data_is_separated()
{
config(['tenancy.bootstrappers' => [
RedisTenancyBootstrapper::class
]]);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
Redis::set('foo', 'bar');
$this->assertSame('bar', Redis::get('foo'));
tenancy()->initialize($tenant2);
$this->assertSame(null, Redis::get('foo'));
Redis::set('foo', 'xyz');
Redis::set('abc', 'def');
$this->assertSame('xyz', Redis::get('foo'));
$this->assertSame('def', Redis::get('abc'));
tenancy()->initialize($tenant1);
$this->assertSame('bar', Redis::get('foo'));
$this->assertSame(null, Redis::get('abc'));
$tenant3 = Tenant::create();
tenancy()->initialize($tenant3);
$this->assertSame(null, Redis::get('foo'));
$this->assertSame(null, Redis::get('abc'));
}
/** @test */
public function filesystem_data_is_separated()
{
config(['tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class
]]);
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
Storage::disk('public')->put('foo', 'bar');
$this->assertSame('bar', Storage::disk('public')->get('foo'));
tenancy()->initialize($tenant2);
$this->assertFalse(Storage::disk('public')->exists('foo'));
Storage::disk('public')->put('foo', 'xyz');
Storage::disk('public')->put('abc', 'def');
$this->assertSame('xyz', Storage::disk('public')->get('foo'));
$this->assertSame('def', Storage::disk('public')->get('abc'));
tenancy()->initialize($tenant1);
$this->assertSame('bar', Storage::disk('public')->get('foo'));
$this->assertFalse(Storage::disk('public')->exists('abc'));
$tenant3 = Tenant::create();
tenancy()->initialize($tenant3);
$this->assertFalse(Storage::disk('public')->exists('foo'));
$this->assertFalse(Storage::disk('public')->exists('abc'));
}
/** @test */
public function queue_data_is_separated()
{
// todo
}
}

View file

@ -9,6 +9,7 @@ use Stancl\Tenancy\Events\Listeners\JobPipeline;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Tests\TestCase;
// todo the shouldQueue() doesnt make sense? test if it really works or if its just because of sync queue driver
class JobPipelineTest extends TestCase
{
/** @test */
@ -60,6 +61,12 @@ class JobPipelineTest extends TestCase
$this->assertSame('first job changed property', app('foo'));
}
/** @test */
public function send_can_return_multiple_arguments()
{
// todo
}
}
class FooJob