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

Rewrite old tests

This commit is contained in:
Samuel Štancl 2020-05-12 23:22:40 +02:00
parent 64383b4c56
commit 89936187ce
71 changed files with 698 additions and 3203 deletions

View file

@ -8,7 +8,7 @@ use Stancl\Tenancy\Database\Models\Tenant;
return [ return [
'tenant_model' => Tenant::class, 'tenant_model' => Tenant::class,
'domain_model' => Domain::class, 'domain_model' => Domain::class,
'internal_column_prefix' => 'tenancy_', 'internal_prefix' => 'tenancy_',
'central_connection' => 'central', 'central_connection' => 'central',
'template_tenant_connection' => null, 'template_tenant_connection' => null,
@ -46,13 +46,6 @@ return [
*/ */
'tenant_route_namespace' => 'App\Http\Controllers', 'tenant_route_namespace' => 'App\Http\Controllers',
/**
* Central domains (hostnames), e.g. domains which host landing pages, sign up pages, etc.
*/
'exempt_domains' => [
// 'localhost',
],
/** /**
* Tenancy bootstrappers are executed when tenancy is initialized. * Tenancy bootstrappers are executed when tenancy is initialized.
* Their responsibility is making Laravel features tenant-aware. * Their responsibility is making Laravel features tenant-aware.

View file

@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Stancl\Tenancy\Tenant;
class CreateTenant extends Command
{
protected $signature = 'tenants:create
{--d|domain=* : The tenant\'s domains.}
{data?* : The tenant\'s data. Separate keys and values by `=`, e.g. `plan=free`.}';
protected $description = 'Create a tenant.';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$tenant = Tenant::new()
->withDomains($this->getDomains())
->withData($this->getData())
->save();
$this->info($tenant->id);
}
public function getDomains(): array
{
return $this->option('domain');
}
public function getData(): array
{
return array_reduce($this->argument('data'), function ($data, $pair) {
[$key, $value] = explode('=', $pair, 2);
$data[$key] = $value;
return $data;
}, []);
}
}

View file

@ -36,17 +36,6 @@ class Install extends Command
]); ]);
$this->info('✔️ Created config/tenancy.php'); $this->info('✔️ Created config/tenancy.php');
$newKernel = $this->setMiddlewarePriority();
$newKernel = str_replace("'web' => [", "'web' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel);
$newKernel = str_replace("'api' => [", "'api' => [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,", $newKernel);
file_put_contents(app_path('Http/Kernel.php'), $newKernel);
$this->info('✔️ Set middleware priority');
if (! file_exists(base_path('routes/tenant.php'))) { if (! file_exists(base_path('routes/tenant.php'))) {
file_put_contents(base_path('routes/tenant.php'), file_get_contents(__DIR__ . '/../../assets/tenant_routes.php.stub')); file_put_contents(base_path('routes/tenant.php'), file_get_contents(__DIR__ . '/../../assets/tenant_routes.php.stub'));
$this->info('✔️ Created routes/tenant.php'); $this->info('✔️ Created routes/tenant.php');
@ -54,15 +43,13 @@ class Install extends Command
$this->info('Found routes/tenant.php.'); $this->info('Found routes/tenant.php.');
} }
$this->line(''); // todo tenancy SP stub
$this->line('This package lets you store data about tenants either in Redis or in a relational database like MySQL. To store data about tenants in a relational database, you need a few database tables.');
if ($this->confirm('Do you wish to publish the migrations that create these tables?', true)) { $this->callSilent('vendor:publish', [
$this->callSilent('vendor:publish', [ '--provider' => 'Stancl\Tenancy\TenancyServiceProvider',
'--provider' => 'Stancl\Tenancy\TenancyServiceProvider', '--tag' => 'migrations',
'--tag' => 'migrations', ]);
]); $this->info('✔️ Created migrations. Remember to run [php artisan migrate]!');
$this->info('✔️ Created migrations. Remember to run [php artisan migrate]!');
}
if (! is_dir(database_path('migrations/tenant'))) { if (! is_dir(database_path('migrations/tenant'))) {
mkdir(database_path('migrations/tenant')); mkdir(database_path('migrations/tenant'));
@ -71,34 +58,4 @@ class Install extends Command
$this->comment('✨️ stancl/tenancy installed successfully.'); $this->comment('✨️ stancl/tenancy installed successfully.');
} }
protected function setMiddlewarePriority(): string
{
if (app()->version()[0] === '6') {
return str_replace(
'protected $middlewarePriority = [',
"protected \$middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\Stancl\Tenancy\Middleware\InitializeTenancy::class,",
file_get_contents(app_path('Http/Kernel.php'))
);
} else {
return str_replace(
"];\n}",
"];\n\n protected \$middlewarePriority = [
\Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains::class,
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}",
file_get_contents(app_path('Http/Kernel.php'))
);
}
}
} }

View file

@ -57,20 +57,13 @@ class Migrate extends MigrateCommand
return; return;
} }
tenancy() tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
->query() $this->line("Tenant: {$tenant['id']}");
->when($this->option('tenants'), function ($query) {
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants')); // Migrate
}) parent::handle();
->each(function (TenantWithDatabase $tenant) {
$this->line("Tenant: {$tenant['id']}");
$tenant->run(function () { event(new DatabaseMigrated($tenant));
// Migrate });
parent::handle();
});
event(new DatabaseMigrated($tenant));
});
} }
} }

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Commands; namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Traits\DealsWithMigrations; use Stancl\Tenancy\Traits\DealsWithMigrations;
use Stancl\Tenancy\Traits\HasATenantsOption; use Stancl\Tenancy\Traits\HasATenantsOption;
@ -33,22 +34,18 @@ final class MigrateFresh extends Command
*/ */
public function handle() public function handle()
{ {
tenancy()->all($this->option('tenants'))->each(function ($tenant) { tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
$this->line("Tenant: {$tenant->id}"); $this->info('Dropping tables.');
$this->call('db:wipe', array_filter([
'--database' => 'tenant',
'--force' => true,
]));
$tenant->run(function ($tenant) { $this->info('Migrating.');
$this->info('Dropping tables.'); $this->callSilent('tenants:migrate', [
$this->call('db:wipe', array_filter([ '--tenants' => [$tenant->id],
'--database' => 'tenant', '--force' => true,
'--force' => true, ]);
]));
$this->info('Migrating.');
$this->callSilent('tenants:migrate', [
'--tenants' => [$tenant->id],
'--force' => true,
]);
});
}); });
$this->info('Done.'); $this->info('Done.');

View file

@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Console\Migrations\RollbackCommand; use Illuminate\Database\Console\Migrations\RollbackCommand;
use Illuminate\Database\Migrations\Migrator; use Illuminate\Database\Migrations\Migrator;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\DatabaseManager; use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Traits\DealsWithMigrations; use Stancl\Tenancy\Traits\DealsWithMigrations;
use Stancl\Tenancy\Traits\HasATenantsOption; use Stancl\Tenancy\Traits\HasATenantsOption;
@ -55,13 +56,13 @@ class Rollback extends RollbackCommand
return; return;
} }
tenancy()->all($this->option('tenants'))->each(function ($tenant) { tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
$this->line("Tenant: {$tenant['id']}"); $this->line("Tenant: {$tenant['id']}");
$tenant->run(function () { // Rollback
// Rollback parent::handle();
parent::handle();
}); // todo DatabaseRolledBack event
}); });
} }
} }

View file

@ -32,8 +32,7 @@ class Run extends Command
*/ */
public function handle() public function handle()
{ {
$originalTenant = tenancy()->getTenant(); tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
tenancy()->all($this->option('tenants'))->each(function ($tenant) {
$this->line("Tenant: {$tenant['id']}"); $this->line("Tenant: {$tenant['id']}");
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
@ -54,12 +53,6 @@ class Run extends Command
// Run command // Run command
$this->call($this->argument('commandname'), array_merge($arguments, $options)); $this->call($this->argument('commandname'), array_merge($arguments, $options));
tenancy()->endTenancy();
}); });
if ($originalTenant) {
tenancy()->initialize($originalTenant);
}
} }
} }

View file

@ -55,18 +55,11 @@ class Seed extends SeedCommand
return; return;
} }
tenancy() tenancy()->runForMultiple($this->option('tenants'), function ($tenant) {
->query()
->when($this->option('tenants'), function ($query) {
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
})
->each(function (TenantWithDatabase $tenant) {
$this->line("Tenant: {$tenant['id']}"); $this->line("Tenant: {$tenant['id']}");
$tenant->run(function () { // Seed
// Seed parent::handle();
parent::handle();
});
event(new DatabaseSeeded($tenant)); event(new DatabaseSeeded($tenant));
}); });

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Commands; namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Stancl\Tenancy\Contracts\Tenant;
class TenantList extends Command class TenantList extends Command
{ {
@ -30,8 +31,14 @@ class TenantList extends Command
public function handle() public function handle()
{ {
$this->info('Listing all tenants.'); $this->info('Listing all tenants.');
tenancy()->all()->each(function ($tenant) { tenancy()
$this->line("[Tenant] id: {$tenant['id']} @ " . implode('; ', $tenant->domains)); ->query()
->when($this->option('tenants'), function ($query) {
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
})
->cursor()
->each(function (Tenant $tenant) {
$this->line("[Tenant] id: {$tenant['id']} @ " . implode('; ', $tenant->domains ?? []));
}); });
} }
} }

View file

@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Contracts; namespace Stancl\Tenancy\Contracts;
use Stancl\Tenancy\TenantManager; use Stancl\Tenancy\Facades\Tenancy;
/** Additional features, like Telescope tags and tenant redirects. */ /** Additional features, like Telescope tags and tenant redirects. */
interface Feature interface Feature
{ {
public function bootstrap(TenantManager $tenantManager): void; public function bootstrap(Tenancy $tenancy): void;
} }

View file

@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Contracts;
use Stancl\Tenancy\Tenant;
interface StorageDriver
{
public function createTenant(Tenant $tenant): void;
public function updateTenant(Tenant $tenant): void;
public function deleteTenant(Tenant $tenant): void;
/**
* Find a tenant using an id.
*
* @param string $id
* @return Tenant
* @throws \Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException
*/
public function findById(string $id): Tenant;
/**
* Find a tenant using a domain name.
*
* @param string $domain
* @return Tenant
*/
public function findByDomain(string $domain): Tenant;
/**
* Get all tenants.
*
* @param string[] $ids
* @return Tenant[]
*/
public function all(array $ids = []): array;
/**
* Ensure a tenant can be created.
*
* @param Tenant $tenant
* @return void
* @throws TenantCannotBeCreatedException
*/
public function ensureTenantCanBeCreated(Tenant $tenant): void;
/**
* Set default tenant (will be used for get/put when no tenant is supplied).
*
* @param Tenant $tenant
* @return self
*/
public function withDefaultTenant(Tenant $tenant);
/**
* Get a value from storage.
*
* @param string $key
* @param ?Tenant $tenant
* @return mixed
*/
public function get(string $key, Tenant $tenant = null);
/**
* Get multiple values from storage.
*
* @param array $keys
* @param ?Tenant $tenant
* @return void
*/
public function getMany(array $keys, Tenant $tenant = null);
/**
* Put a value into storage.
*
* @param string $key
* @param mixed $value
* @param ?Tenant $tenant
* @return void
*/
public function put(string $key, $value, Tenant $tenant = null): void;
/**
* Put multiple values into storage.
*
* @param mixed[string] $kvPairs
* @param ?Tenant $tenant
* @return void
*/
public function putMany(array $kvPairs, Tenant $tenant = null): void;
}

View file

@ -9,6 +9,7 @@ namespace Stancl\Tenancy\Contracts;
*/ */
interface TenancyBootstrapper interface TenancyBootstrapper
{ {
// todo rename methods
public function start(Tenant $tenant); public function start(Tenant $tenant);
public function end(); public function end();

View file

@ -7,4 +7,7 @@ use Stancl\Tenancy\DatabaseConfig;
interface TenantWithDatabase extends Tenant interface TenantWithDatabase extends Tenant
{ {
public function database(): DatabaseConfig; public function database(): DatabaseConfig;
/** Get an internal key. */
public function getInternal(string $key);
} }

View file

@ -8,9 +8,11 @@ use Illuminate\Routing\Controller;
class TenantAssetsController extends Controller class TenantAssetsController extends Controller
{ {
public static $tenancyMiddleware = 'Stancl\Tenancy\Middleware\InitializeTenancyByDomain';
public function __construct() public function __construct()
{ {
$this->middleware('tenancy'); $this->middleware(static::$tenancyMiddleware);
} }
public function asset($path) public function asset($path)

View file

@ -6,9 +6,24 @@ namespace Stancl\Tenancy\Database\Models\Concerns;
// todo rename // todo rename
trait HasADataColumn trait HasADataColumn
{ {
public static $priorityListeners = [];
/**
* We need this property, because both created & saved event listeners
* decode the data (to take precedence before other created & saved)
* listeners, but we don't want the dadta to be decoded twice.
*
* @var string
*/
public $dataEncodingStatus = 'decoded'; // todo write tests for this
public static function bootHasADataColumn() public static function bootHasADataColumn()
{ {
$encode = function (self $model) { $encode = function (self $model) {
if ($model->dataEncodingStatus === 'encoded') {
return;
}
foreach ($model->getAttributes() as $key => $value) { foreach ($model->getAttributes() as $key => $value) {
if (! in_array($key, static::getCustomColums())) { if (! in_array($key, static::getCustomColums())) {
$current = $model->getAttribute(static::getDataColumn()) ?? []; $current = $model->getAttribute(static::getDataColumn()) ?? [];
@ -20,19 +35,64 @@ trait HasADataColumn
unset($model->attributes[$key]); unset($model->attributes[$key]);
} }
} }
$model->dataEncodingStatus = 'encoded';
}; };
$decode = function (self $model) { $decode = function (self $model) {
if ($model->dataEncodingStatus === 'decoded') {
return;
}
foreach ($model->getAttribute(static::getDataColumn()) ?? [] as $key => $value) { foreach ($model->getAttribute(static::getDataColumn()) ?? [] as $key => $value) {
$model->setAttribute($key, $value); $model->setAttribute($key, $value);
} }
$model->setAttribute(static::getDataColumn(), null); $model->setAttribute(static::getDataColumn(), null);
$model->dataEncodingStatus = 'decoded';
}; };
static::saving($encode); static::registerPriorityListener('retrieved', $decode);
static::saved($decode);
static::retrieved($decode); static::registerPriorityListener('saving', $encode);
static::registerPriorityListener('creating', $encode);
static::registerPriorityListener('updating', $encode);
static::registerPriorityListener('saved', $decode);
static::registerPriorityListener('created', $decode);
static::registerPriorityListener('updated', $decode);
}
protected function fireModelEvent($event, $halt = true)
{
$this->runPriorityListeners($event, $halt);
return parent::fireModelEvent($event, $halt);
}
public function runPriorityListeners($event, $halt = true)
{
$listeners = static::$priorityListeners[$event] ?? [];
if (! $event) {
return;
}
foreach ($listeners as $listener) {
if (is_string($listener)) {
$listener = app($listener);
$handle = [$listener, 'handle'];
} else {
$handle = $listener;
}
$handle($this);
}
}
public static function registerPriorityListener(string $event, callable $callback)
{
static::$priorityListeners[$event][] = $callback;
} }
public function getCasts() public function getCasts()

View file

@ -10,7 +10,7 @@ use Stancl\Tenancy\Contracts;
// todo @property // todo @property
class Tenant extends Model implements Contracts\TenantWithDatabase class Tenant extends Model implements Contracts\TenantWithDatabase
{ {
use Concerns\CentralConnection, Concerns\HasADataColumn, Concerns\GeneratesIds, Concerns\HasADataColumn { use Concerns\CentralConnection, Concerns\HasADataColumn, Concerns\GeneratesIds, Concerns\HasADataColumn {
Concerns\HasADataColumn::getCasts as dataColumnCasts; Concerns\HasADataColumn::getCasts as dataColumnCasts;
} }
@ -41,14 +41,11 @@ class Tenant extends Model implements Contracts\TenantWithDatabase
public static function internalPrefix(): string public static function internalPrefix(): string
{ {
return config('tenancy.internal_column_prefix'); return config('tenancy.internal_prefix');
} }
/** /**
* Get an internal key. * Get an internal key.
*
* @param string $key
* @return mixed
*/ */
public function getInternal(string $key) public function getInternal(string $key)
{ {
@ -57,14 +54,10 @@ class Tenant extends Model implements Contracts\TenantWithDatabase
/** /**
* Set internal key. * Set internal key.
*
* @param string $key
* @param mixed $value
* @return $this
*/ */
public function setInternal(string $key, $value) public function setInternal(string $key, $value)
{ {
$this->setAttribute($key, $value); $this->setAttribute(static::internalPrefix() . $key, $value);
return $this; return $this;
} }

View file

@ -82,84 +82,10 @@ class DatabaseManager
* @throws DatabaseManagerNotRegisteredException * @throws DatabaseManagerNotRegisteredException
* @throws TenantDatabaseAlreadyExistsException * @throws TenantDatabaseAlreadyExistsException
*/ */
public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void // todo do we need this?
{ {
if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) { if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) {
throw new TenantDatabaseAlreadyExistsException($database); throw new TenantDatabaseAlreadyExistsException($database);
} }
} }
/**
* Create a database for a tenant.
*
* @param Tenant $tenant
* @param ShouldQueue[]|callable[] $afterCreating
* @return void
* @throws DatabaseManagerNotRegisteredException
*/
public function createDatabase(TenantWithDatabase $tenant, array $afterCreating = [])
{
// todo get rid of aftercreating logic
$afterCreating = array_merge(
$afterCreating,
$this->tenancy->event('database.creating', $tenant->database()->getName(), $tenant)
);
if ($this->app['config']['tenancy.queue_database_creation'] ?? false) {
$this->createDatabaseAsynchronously($tenant, $afterCreating);
} else {
$this->createDatabaseSynchronously($tenant, $afterCreating);
}
$this->tenancy->event('database.created', $tenant->database()->getName(), $tenant);
}
protected function createDatabaseAsynchronously(Tenant $tenant, array $afterCreating)
{
$chain = [];
foreach ($afterCreating as $item) {
if (is_string($item) && class_exists($item)) {
$chain[] = new $item($tenant); // Classes are instantiated and given $tenant
} elseif ($item instanceof ShouldQueue) {
$chain[] = $item;
}
}
QueuedTenantDatabaseCreator::withChain($chain)->dispatch($tenant->database()->manager(), $tenant);
}
protected function createDatabaseSynchronously(Tenant $tenant, array $afterCreating)
{
$manager = $tenant->database()->manager();
$manager->createDatabase($tenant);
foreach ($afterCreating as $item) {
if (is_object($item) && ! $item instanceof Closure) {
$item->handle($tenant);
} else {
$item($tenant);
}
}
}
/**
* Delete a tenant's database.
*
* @throws DatabaseManagerNotRegisteredException
*/
public function deleteDatabase(TenantWithDatabase $tenant)
{
$database = $tenant->database()->getName();
$manager = $tenant->database()->manager();
$this->tenancy->event('database.deleting', $database, $tenant);
if ($this->app['config']['tenancy.queue_database_deletion'] ?? false) {
QueuedTenantDatabaseDeleter::dispatch($manager, $tenant);
} else {
$manager->deleteDatabase($tenant);
}
$this->tenancy->event('database.deleted', $database, $tenant);
}
} }

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Exceptions;
use Facade\IgnitionContracts\BaseSolution;
use Facade\IgnitionContracts\ProvidesSolution;
use Facade\IgnitionContracts\Solution;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
class TenantCouldNotBeIdentifiedByRequestDataException extends TenantCouldNotBeIdentifiedException implements ProvidesSolution
{
public function __construct($tenant_id)
{
parent::__construct("Tenant could not be identified by request data with payload: $tenant_id");
}
public function getSolution(): Solution
{
return BaseSolution::create('Tenant could not be identified with this request data')
->setSolutionDescription('Did you forget to create a tenant with this id?')
->setDocumentationLinks([
'Creating Tenants' => 'https://tenancyforlaravel.com/docs/v2/creating-tenants/', // todo update link for v3
]);
}
}

View file

@ -6,13 +6,15 @@ namespace Stancl\Tenancy\Features;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Contracts\Feature;
use Stancl\Tenancy\TenantManager; use Stancl\Tenancy\Tenancy;
class CrossDomainRedirect implements Feature class CrossDomainRedirect implements Feature
{ {
public function bootstrap(TenantManager $tenantManager): void public function bootstrap(Tenancy $tenancy): void
{ {
RedirectResponse::macro('domain', function (string $domain) { RedirectResponse::macro('domain', function (string $domain) {
/** @var RedirectResponse $this */
// replace first occurance of hostname fragment with $domain // replace first occurance of hostname fragment with $domain
$url = $this->getTargetUrl(); $url = $this->getTargetUrl();
$hostname = parse_url($url, PHP_URL_HOST); $hostname = parse_url($url, PHP_URL_HOST);

View file

@ -8,8 +8,9 @@ use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope; use Laravel\Telescope\Telescope;
use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Contracts\Feature;
use Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains; use Stancl\Tenancy\Middleware\PreventAccessFromTenantDomains;
use Stancl\Tenancy\TenantManager; use Stancl\Tenancy\Tenancy;
// todo rewrite this
class TelescopeTags implements Feature class TelescopeTags implements Feature
{ {
/** @var callable User-specific callback that returns tags. */ /** @var callable User-specific callback that returns tags. */
@ -22,7 +23,7 @@ class TelescopeTags implements Feature
}; };
} }
public function bootstrap(TenantManager $tenantManager): void public function bootstrap(Tenancy $tenancy): void
{ {
if (! class_exists(Telescope::class)) { if (! class_exists(Telescope::class)) {
return; return;
@ -35,6 +36,7 @@ class TelescopeTags implements Feature
return $tags; return $tags;
} }
// todo lines below
$tenantRoute = PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'tenancy') $tenantRoute = PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'tenancy')
|| PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'universal'); || PreventAccessFromTenantDomains::routeHasMiddleware(request()->route(), 'universal');

View file

@ -6,9 +6,11 @@ namespace Stancl\Tenancy\Features;
use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Config\Repository;
use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Contracts\Feature;
use Stancl\Tenancy\Tenancy;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager; use Stancl\Tenancy\TenantManager;
// todo rewrite this
class TenantConfig implements Feature class TenantConfig implements Feature
{ {
/** @var Repository */ /** @var Repository */
@ -30,7 +32,7 @@ class TenantConfig implements Feature
} }
} }
public function bootstrap(TenantManager $tenantManager): void public function bootstrap(Tenancy $tenancy): void
{ {
$tenantManager->eventListener('bootstrapped', function (TenantManager $manager) { $tenantManager->eventListener('bootstrapped', function (TenantManager $manager) {
$this->setTenantConfig($manager->getTenant()); $this->setTenantConfig($manager->getTenant());

View file

@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts\Feature;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager; use Stancl\Tenancy\TenantManager;
// todo rewrite this
class Timestamps implements Feature class Timestamps implements Feature
{ {
/** @var Repository */ /** @var Repository */

View file

@ -27,7 +27,8 @@ class CreateDatabase implements ShouldQueue
public function handle() public function handle()
{ {
if ($this->tenant->getAttribute('_tenancy_create_database') !== false) { if ($this->tenant->getInternal('create_database') !== false) {
$this->tenant->database()->makeCredentials();
$this->tenant->database()->manager()->createDatabase($this->tenant); $this->tenant->database()->manager()->createDatabase($this->tenant);
} }
} }

View file

@ -6,26 +6,28 @@ namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
use Stancl\Tenancy\Tenancy;
class InitializeTenancyByRequestData // todo write tests for this
class InitializeTenancyByRequestData extends IdentificationMiddleware
{ {
/** @var string|null */ /** @var string|null */
protected $header; public static $header = 'X-Tenant';
/** @var string|null */ /** @var string|null */
protected $queryParameter; public static $queryParameter = 'tenant';
/** @var callable */ /** @var Tenancy */
protected $onFail; protected $tenancy;
public function __construct(?string $header = 'X-Tenant', ?string $queryParameter = 'tenant', callable $onFail = null) /** @var TenantResolver */
protected $resolver;
public function __construct(Tenancy $tenancy, RequestDataTenantResolver $resolver)
{ {
$this->header = $header; $this->tenancy = $tenancy;
$this->queryParameter = $queryParameter; $this->resolver = $resolver;
$this->onFail = $onFail ?? function ($e) {
throw $e;
};
} }
/** /**
@ -38,33 +40,21 @@ class InitializeTenancyByRequestData
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
if ($request->method() !== 'OPTIONS') { if ($request->method() !== 'OPTIONS') {
try { return $this->initializeTenancy($request, $next, $this->getPayload($request));
$this->initializeTenancy($request);
} catch (TenantCouldNotBeIdentifiedException $e) {
return ($this->onFail)($e, $request, $next);
}
} }
return $next($request); return $next($request);
} }
protected function initializeTenancy(Request $request) protected function getPayload(Request $request): ?string
{ {
if (tenancy()->initialized) {
return;
}
$tenant = null; $tenant = null;
if ($this->header && $request->hasHeader($this->header)) { if (static::$header && $request->hasHeader(static::$header)) {
$tenant = $request->header($this->header); $tenant = $request->header(static::$header);
} elseif ($this->queryParameter && $request->has($this->queryParameter)) { } elseif (static::$queryParameter && $request->has(static::$queryParameter)) {
$tenant = $request->get($this->queryParameter); $tenant = $request->get(static::$queryParameter);
} }
if (! $tenant) { return $tenant;
throw new TenantCouldNotBeIdentifiedException($request->getHost());
}
tenancy()->initialize(tenancy()->find($tenant));
} }
} }

View file

@ -19,7 +19,7 @@ class PathTenantResolver implements TenantResolver
if ($id = $route->parameter(static::$tenantParameterName)) { if ($id = $route->parameter(static::$tenantParameterName)) {
$route->forgetParameter(static::$tenantParameterName); $route->forgetParameter(static::$tenantParameterName);
if ($tenant = config('tenancy.tenant_model')::find($id)) { if ($tenant = tenancy()->find($id)) {
return $tenant; return $tenant;
} }
} }

View file

@ -0,0 +1,21 @@
<?php
namespace Stancl\Tenancy\Resolvers;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Contracts\TenantResolver;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
class RequestDataTenantResolver implements TenantResolver
{
public function resolve(...$args): Tenant
{
$payload = $args[0];
if ($payload && $tenant = tenancy()->find($payload)) {
return $tenant;
}
throw new TenantCouldNotBeIdentifiedByRequestDataException($payload);
}
}

View file

@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
use Closure;
use Illuminate\Cache\CacheManager;
use Illuminate\Cache\Repository as CacheRepository;
use Illuminate\Config\Repository as ConfigRepository;
class CachedTenantResolver
{
/** @var CacheRepository */
protected $cache;
/** @var ConfigRepository */
protected $config;
public function __construct(CacheManager $cacheManager, ConfigRepository $config)
{
$this->cache = $cacheManager->store($config->get('tenancy.storage_drivers.db.cache_store'));
$this->config = $config;
}
protected function ttl(): int
{
return $this->config->get('tenancy.storage_drivers.db.cache_ttl');
}
public function getTenantIdByDomain(string $domain, Closure $query): ?string
{
return $this->cache->remember('_tenancy_domain_to_id:' . $domain, $this->ttl(), $query);
}
public function getDataById(string $id, Closure $dataQuery): ?array
{
return $this->cache->remember('_tenancy_id_to_data:' . $id, $this->ttl(), $dataQuery);
}
public function getDomainsById(string $id, Closure $domainsQuery): ?array
{
return $this->cache->remember('_tenancy_id_to_domains:' . $id, $this->ttl(), $domainsQuery);
}
public function invalidateTenant(string $id): void
{
$this->invalidateTenantData($id);
$this->invalidateTenantDomains($id);
}
public function invalidateTenantData(string $id): void
{
$this->cache->forget('_tenancy_id_to_data:' . $id);
}
public function invalidateTenantDomains(string $id): void
{
$this->cache->forget('_tenancy_id_to_domains:' . $id);
}
public function invalidateDomainToIdMapping(array $domains): void
{
foreach ($domains as $domain) {
$this->cache->forget('_tenancy_domain_to_id:' . $domain);
}
}
}

View file

@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
trait CentralConnection
{
public function getConnectionName()
{
return DatabaseStorageDriver::getCentralConnectionName();
}
}

View file

@ -1,256 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Database\Connection;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Contracts\Future\CanDeleteKeys;
use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\Tenant;
class DatabaseStorageDriver implements StorageDriver, CanDeleteKeys, CanFindByAnyKey
{
/** @var Application */
public $app;
/** @var Connection */
public $centralDatabase;
/** @var TenantRepository */
public $tenants;
/** @var DomainRepository */
public $domains;
/** @var CachedTenantResolver */
public $cache;
/** @var Tenant The default tenant. */
public $tenant;
public function __construct(Application $app, ConfigRepository $config, CachedTenantResolver $cache)
{
$this->app = $app;
$this->cache = $cache;
$this->centralDatabase = $this->getCentralConnection();
$this->tenants = new TenantRepository($config);
$this->domains = new DomainRepository($config);
}
/**
* Get the central database connection.
*
* @return Connection
*/
public static function getCentralConnection(): Connection
{
return DB::connection(static::getCentralConnectionName());
}
public static function getCentralConnectionName(): string
{
return config('tenancy.storage_drivers.db.connection') ?? DatabaseManager::$originalDefaultConnectionName;
}
public function findByDomain(string $domain): Tenant
{
$query = function () use ($domain) {
return $this->domains->getTenantIdByDomain($domain);
};
if ($this->usesCache()) {
$id = $this->cache->getTenantIdByDomain($domain, $query);
} else {
$id = $query();
}
if (! $id) {
throw new TenantCouldNotBeIdentifiedException($domain);
}
return $this->findById($id);
}
public function findById(string $id): Tenant
{
$dataQuery = function () use ($id) {
$data = $this->tenants->find($id);
return $data ? $this->tenants->decodeData($data) : null;
};
$domainsQuery = function () use ($id) {
return $this->domains->getTenantDomains($id);
};
if ($this->usesCache()) {
$data = $this->cache->getDataById($id, $dataQuery);
$domains = $this->cache->getDomainsById($id, $domainsQuery);
} else {
$data = $dataQuery();
$domains = $domainsQuery();
}
if (! $data) {
throw new TenantDoesNotExistException($id);
}
return Tenant::fromStorage($data)
->withDomains($domains);
}
/**
* Find a tenant using an arbitrary key.
*
* @param string $key
* @param mixed $value
* @return Tenant
* @throws TenantDoesNotExistException
*/
public function findBy(string $key, $value): Tenant
{
$tenant = $this->tenants->findBy($key, $value);
if (! $tenant) {
throw new TenantDoesNotExistException($value, $key);
}
return Tenant::fromStorage($this->tenants->decodeData($tenant))
->withDomains($this->domains->getTenantDomains($tenant['id']));
}
public function ensureTenantCanBeCreated(Tenant $tenant): void
{
if ($this->tenants->exists($tenant)) {
throw new TenantWithThisIdAlreadyExistsException($tenant->id);
}
if ($this->domains->occupied($tenant->domains)) {
throw new DomainsOccupiedByOtherTenantException;
}
}
public function withDefaultTenant(Tenant $tenant): self
{
$this->tenant = $tenant;
return $this;
}
public function createTenant(Tenant $tenant): void
{
$this->centralDatabase->transaction(function () use ($tenant) {
$this->tenants->insert($tenant);
$this->domains->insertTenantDomains($tenant);
});
}
public function updateTenant(Tenant $tenant): void
{
$originalDomains = $this->domains->getTenantDomains($tenant);
$this->centralDatabase->transaction(function () use ($tenant, $originalDomains) {
$this->tenants->updateTenant($tenant);
$this->domains->updateTenantDomains($tenant, $originalDomains);
});
if ($this->usesCache()) {
$this->cache->invalidateTenant($tenant->id);
$this->cache->invalidateDomainToIdMapping($originalDomains);
}
}
public function deleteTenant(Tenant $tenant): void
{
$originalDomains = $this->domains->getTenantDomains($tenant);
$this->centralDatabase->transaction(function () use ($tenant) {
$this->tenants->where('id', $tenant->id)->delete();
$this->domains->where('tenant_id', $tenant->id)->delete();
});
if ($this->usesCache()) {
$this->cache->invalidateTenant($tenant->id);
$this->cache->invalidateDomainToIdMapping($originalDomains);
}
}
/**
* Get all tenants.
*
* @param string[] $ids
* @return Tenant[]
*/
public function all(array $ids = []): array
{
return $this->tenants->all($ids)->map(function ($data) {
return Tenant::fromStorage($data)
->withDomains($this->domains->getTenantDomains($data['id']));
})->toArray();
}
/**
* Get the current tenant.
*
* @return Tenant
*/
protected function currentTenant()
{
return $this->tenant ?? $this->app[Tenant::class];
}
public function get(string $key, Tenant $tenant = null)
{
return $this->tenants->get($key, $tenant ?? $this->currentTenant());
}
public function getMany(array $keys, Tenant $tenant = null): array
{
return $this->tenants->getMany($keys, $tenant ?? $this->currentTenant());
}
public function put(string $key, $value, Tenant $tenant = null): void
{
$tenant = $tenant ?? $this->currentTenant();
$this->tenants->put($key, $value, $tenant);
if ($this->usesCache()) {
$this->cache->invalidateTenantData($tenant->id);
}
}
public function putMany(array $kvPairs, Tenant $tenant = null): void
{
$tenant = $tenant ?? $this->currentTenant();
$this->tenants->putMany($kvPairs, $tenant);
if ($this->usesCache()) {
$this->cache->invalidateTenantData($tenant->id);
}
}
public function deleteMany(array $keys, Tenant $tenant = null): void
{
$tenant = $tenant ?? $this->currentTenant();
$this->tenants->deleteMany($keys, $tenant);
if ($this->usesCache()) {
$this->cache->invalidateTenantData($tenant->id);
}
}
public function usesCache(): bool
{
return $this->app['config']['tenancy.storage_drivers.db.cache_store'] !== null;
}
}

View file

@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
use Illuminate\Config\Repository as ConfigRepository;
use Stancl\Tenancy\Tenant;
class DomainRepository extends Repository
{
public function getTenantIdByDomain(string $domain): ?string
{
return $this->where('domain', $domain)->first()->tenant_id ?? null;
}
public function occupied(array $domains): bool
{
return $this->whereIn('domain', $domains)->exists();
}
public function getTenantDomains($tenant)
{
$id = $tenant instanceof Tenant ? $tenant->id : $tenant;
return $this->where('tenant_id', $id)->get('domain')->pluck('domain')->all();
}
public function insertTenantDomains(Tenant $tenant)
{
$this->insert(array_map(function ($domain) use ($tenant) {
return ['domain' => $domain, 'tenant_id' => $tenant->id];
}, $tenant->domains));
}
public function updateTenantDomains(Tenant $tenant, array $originalDomains)
{
$deletedDomains = array_diff($originalDomains, $tenant->domains);
$newDomains = array_diff($tenant->domains, $originalDomains);
$this->whereIn('domain', $deletedDomains)->delete();
foreach ($newDomains as $domain) {
$this->insert([
'tenant_id' => $tenant->id,
'domain' => $domain,
]);
}
}
public function getTable(ConfigRepository $config)
{
return $config->get('tenancy.storage_drivers.db.table_names.domains') ?? 'domains';
}
}

View file

@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Database\Connection;
use Illuminate\Database\Query\Builder;
/** @mixin Builder */
abstract class Repository
{
/** @var Connection */
public $database;
/** @var string */
protected $tableName;
/** @var Builder */
private $table;
public function __construct(ConfigRepository $config)
{
$this->database = DatabaseStorageDriver::getCentralConnection();
$this->tableName = $this->getTable($config);
$this->table = $this->database->table($this->tableName);
}
public function table()
{
return $this->table->newQuery()->from($this->tableName);
}
abstract public function getTable(ConfigRepository $config);
public function __call($method, $parameters)
{
return $this->table()->$method(...$parameters);
}
}

View file

@ -1,186 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\StorageDrivers\Database;
use Illuminate\Config\Repository as ConfigRepository;
use Stancl\Tenancy\Tenant;
use stdClass;
class TenantRepository extends Repository
{
public function all($ids = [])
{
if ($ids) {
$data = $this->whereIn('id', $ids)->get();
} else {
$data = $this->table()->get();
}
return $data->map(function (stdClass $obj) {
return $this->decodeData((array) $obj);
});
}
public function find($tenant)
{
return (array) $this->table()->find(
$tenant instanceof Tenant ? $tenant->id : $tenant
);
}
public function findBy(string $key, $value)
{
if (in_array($key, static::customColumns())) {
return (array) $this->table()->where($key, $value)->first();
}
return (array) $this->table()->where(
static::dataColumn() . '->' . $key,
$value
)->first();
}
public function updateTenant(Tenant $tenant)
{
$this->putMany($tenant->data, $tenant);
}
public function exists(Tenant $tenant)
{
return $this->where('id', $tenant->id)->exists();
}
public function get(string $key, Tenant $tenant)
{
return $this->decodeFreshDataForTenant($tenant)[$key] ?? null;
}
public function getMany(array $keys, Tenant $tenant)
{
$decodedData = $this->decodeFreshDataForTenant($tenant);
$result = [];
foreach ($keys as $key) {
$result[$key] = $decodedData[$key] ?? null;
}
return $result;
}
public function put(string $key, $value, Tenant $tenant)
{
$record = $this->where('id', $tenant->id);
if (in_array($key, static::customColumns())) {
$record->update([$key => $value]);
} else {
$data = json_decode($record->first()->{static::dataColumn()}, true);
$data[$key] = $value;
$record->update([static::dataColumn() => $data]);
}
}
public function putMany(array $kvPairs, Tenant $tenant)
{
$record = $this->where('id', $tenant->id);
$data = [];
$jsonData = json_decode($record->first()->{static::dataColumn()}, true);
foreach ($kvPairs as $key => $value) {
if (in_array($key, static::customColumns())) {
$data[$key] = $value;
continue;
} else {
$jsonData[$key] = $value;
}
}
$data[static::dataColumn()] = json_encode($jsonData);
$record->update($data);
}
public function deleteMany(array $keys, Tenant $tenant)
{
$record = $this->where('id', $tenant->id);
$data = [];
$jsonData = json_decode($record->first(static::dataColumn())->data, true);
foreach ($keys as $key) {
if (in_array($key, static::customColumns())) {
$data[$key] = null;
continue;
} else {
unset($jsonData[$key]);
}
}
$data[static::dataColumn()] = json_encode($jsonData);
$record->update($data);
}
public function decodeFreshDataForTenant(Tenant $tenant): array
{
return $this->decodeData(
(array) $this->table()->where('id', $tenant->id)->first()
);
}
public static function decodeData(array $columns): array
{
$dataColumn = static::dataColumn();
$decoded = json_decode($columns[$dataColumn], true);
$columns = array_merge($columns, $decoded);
// If $columns[$dataColumn] has been overriden by a value, don't delete the key.
if (! array_key_exists($dataColumn, $decoded)) {
unset($columns[$dataColumn]);
}
return $columns;
}
public function insert(Tenant $tenant)
{
$this->table()->insert(array_merge(
$this->encodeData($tenant->data),
['id' => $tenant->id]
));
}
public static function encodeData(array $data): array
{
$result = [];
foreach (array_intersect(static::customColumns(), array_keys($data)) as $customColumn) {
$result[$customColumn] = $data[$customColumn];
unset($data[$customColumn]);
}
$result = array_merge($result, [static::dataColumn() => json_encode($data)]);
return $result;
}
public static function customColumns(): array
{
return config('tenancy.storage_drivers.db.custom_columns', []);
}
public static function dataColumn(): string
{
return config('tenancy.storage_drivers.db.data_column', 'data');
}
public function getTable(ConfigRepository $config)
{
return $config->get('tenancy.storage_drivers.db.table_names.TenantModel') // legacy
?? $config->get('tenancy.storage_drivers.db.table_names.tenants')
?? 'tenants';
}
}

View file

@ -33,6 +33,10 @@ class Tenancy
public function end(): void public function end(): void
{ {
if (! $this->initialized) {
return;
}
$this->initialized = false; $this->initialized = false;
event(new Events\TenancyEnded($this)); event(new Events\TenancyEnded($this));
@ -68,4 +72,38 @@ class Tenancy
{ {
return $this->model()->find($id); return $this->model()->find($id);
} }
/**
* Run a callback for multiple tenants.
* More performant than running $tenant->run() one by one.
*
* @param Tenant[]|\Illuminate\Support\Collection|string[]|null $tenants
* @param callable $callback
* @return void
*/
public function runForMultiple($tenants, callable $callback)
{
// Wrap string in array
$tenants = is_string($tenants) ? [$tenants] : $tenants;
// Use all tenants if $tenants is falsy
$tenants = $tenants ?: $this->model()->cursor();
$originalTenant = $this->tenant;
foreach ($tenants as $tenant) {
if (is_string($tenant)) {
$tenant = $this->find($tenant);
}
$this->initialize($tenant);
$callback($tenant);
}
if ($originalTenant) {
$this->initialize($originalTenant);
} else {
$this->end();
}
}
} }

View file

@ -24,6 +24,13 @@ class TenancyServiceProvider extends ServiceProvider
$this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.id_generator']); $this->app->bind(Contracts\UniqueIdentifierGenerator::class, $this->app['config']['tenancy.id_generator']);
$this->app->singleton(DatabaseManager::class); $this->app->singleton(DatabaseManager::class);
$this->app->singleton(Tenancy::class); $this->app->singleton(Tenancy::class);
$this->app->extend(Tenancy::class, function (Tenancy $tenancy) {
foreach ($this->app['config']['tenancy.features'] as $feature) {
$this->app[$feature]->bootstrap($tenancy);
}
return $tenancy;
});
$this->app->bind(Tenant::class, function ($app) { $this->app->bind(Tenant::class, function ($app) {
return $app[Tenancy::class]->tenant; return $app[Tenancy::class]->tenant;
}); });
@ -63,7 +70,6 @@ class TenancyServiceProvider extends ServiceProvider
Commands\Migrate::class, Commands\Migrate::class,
Commands\Rollback::class, Commands\Rollback::class,
Commands\TenantList::class, Commands\TenantList::class,
Commands\CreateTenant::class,
Commands\MigrateFresh::class, Commands\MigrateFresh::class,
]); ]);

View file

@ -21,11 +21,6 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection
$this->connection = $config->get('tenancy.database_manager_connections.mysql'); $this->connection = $config->get('tenancy.database_manager_connections.mysql');
} }
public function getSeparator(): string
{
return 'database';
}
protected function database(): Connection protected function database(): Connection
{ {
return DB::connection($this->connection); return DB::connection($this->connection);

View file

@ -21,11 +21,6 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti
$this->connection = $config->get('tenancy.database_manager_connections.pgsql'); $this->connection = $config->get('tenancy.database_manager_connections.pgsql');
} }
public function getSeparator(): string
{
return 'database';
}
protected function database(): Connection protected function database(): Connection
{ {
return DB::connection($this->connection); return DB::connection($this->connection);

View file

@ -21,11 +21,6 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection
$this->connection = $config->get('tenancy.database_manager_connections.pgsql'); $this->connection = $config->get('tenancy.database_manager_connections.pgsql');
} }
public function getSeparator(): string
{
return 'schema';
}
protected function database(): Connection protected function database(): Connection
{ {
return DB::connection($this->connection); return DB::connection($this->connection);

View file

@ -9,11 +9,6 @@ use Stancl\Tenancy\Contracts\TenantWithDatabase;
class SQLiteDatabaseManager implements TenantDatabaseManager class SQLiteDatabaseManager implements TenantDatabaseManager
{ {
public function getSeparator(): string
{
return 'database';
}
public function createDatabase(TenantWithDatabase $tenant): bool public function createDatabase(TenantWithDatabase $tenant): bool
{ {
try { try {
@ -39,7 +34,7 @@ class SQLiteDatabaseManager implements TenantDatabaseManager
public function makeConnectionConfig(array $baseConfig, string $databaseName): array public function makeConnectionConfig(array $baseConfig, string $databaseName): array
{ {
$baseConfig['database'] = database_path($databaseName);; $baseConfig['database'] = database_path($databaseName);
return $baseConfig; return $baseConfig;
} }

View file

@ -13,7 +13,7 @@ class TenantRouteServiceProvider extends RouteServiceProvider
{ {
$this->app->booted(function () { $this->app->booted(function () {
if (file_exists(base_path('routes/tenant.php'))) { if (file_exists(base_path('routes/tenant.php'))) {
Route::middleware(['web', 'tenancy']) Route::middleware(['web'])
->namespace($this->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers') ->namespace($this->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers')
->group(base_path('routes/tenant.php')); ->group(base_path('routes/tenant.php'));
} }

View file

@ -4,11 +4,11 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Traits; namespace Stancl\Tenancy\Traits;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Contracts\TenantWithDatabase;
trait CreatesDatabaseUsers trait CreatesDatabaseUsers
{ {
public function createDatabase(Tenant $tenant): bool public function createDatabase(TenantWithDatabase $tenant): bool
{ {
return $this->database()->transaction(function () use ($tenant) { return $this->database()->transaction(function () use ($tenant) {
parent::createDatabase($tenant); parent::createDatabase($tenant);
@ -17,7 +17,7 @@ trait CreatesDatabaseUsers
}); });
} }
public function deleteDatabase(Tenant $tenant): bool public function deleteDatabase(TenantWithDatabase $tenant): bool
{ {
return $this->database()->transaction(function () use ($tenant) { return $this->database()->transaction(function () use ($tenant) {
parent::deleteDatabase($tenant); parent::deleteDatabase($tenant);

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Traits; namespace Stancl\Tenancy\Traits;
use Illuminate\Support\LazyCollection;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
trait HasATenantsOption trait HasATenantsOption
@ -15,9 +16,14 @@ trait HasATenantsOption
], parent::getOptions()); ], parent::getOptions());
} }
protected function getTenants(): array protected function getTenants(): LazyCollection
{ {
return tenancy()->all($this->option('tenants'))->all(); return tenancy()
->query()
->when($this->option('tenants'), function ($query) {
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
})
->cursor();
} }
public function __construct() public function __construct()

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Traits;
trait HasArrayAccess
{
public function offsetExists($offset): bool
{
return property_exists($this, $offset);
}
public function offsetGet($offset)
{
return $this->$offset;
}
public function offsetSet($offset, $value): void
{
$this->$offset = $value;
}
public function offsetUnset($offset): void
{
unset($this->$offset);
}
}

View file

@ -32,7 +32,7 @@ trait TenantAwareCommand
/** /**
* Get an array of tenants for which the command should be executed. * Get an array of tenants for which the command should be executed.
* *
* @return Tenant[] * @return Tenant[]|mixed
*/ */
abstract protected function getTenants(): array; abstract protected function getTenants();
} }

View file

@ -2,8 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
Route::middleware(['tenancy'])->group(function () { use Illuminate\Support\Facades\Route;
Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset')
->where('path', '(.*)') Route::get('/tenancy/assets/{path?}', 'Stancl\Tenancy\Controllers\TenantAssetsController@asset')
->name('stancl.tenancy.asset'); ->where('path', '(.*)')
}); ->name('stancl.tenancy.asset');

View file

@ -1,164 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Cache;
use Stancl\Tenancy\StorageDrivers\Database\DatabaseStorageDriver;
use Stancl\Tenancy\Tenant;
class CachedResolverTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
public function setUp(): void
{
parent::setUp();
if (config('tenancy.storage_driver') !== 'db') {
$this->markTestSkipped('This test is only relevant for the DB storage driver.');
}
config(['tenancy.storage_drivers.db.cache_store' => config('cache.default')]);
}
/** @test */
public function a_query_is_not_made_for_tenant_id_once_domain_is_cached()
{
$tenant = Tenant::new()
->withData(['foo' => 'bar'])
->withDomains(['foo.localhost'])
->save();
// query is made
$queried = tenancy()->findByDomain('foo.localhost');
$this->assertEquals($tenant->data, $queried->data);
$this->assertSame($tenant->domains, $queried->domains);
// cache is set
$this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost'));
$this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id));
$this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
// query is not made
DatabaseStorageDriver::getCentralConnection()->enableQueryLog();
$cached = tenancy()->findByDomain('foo.localhost');
$this->assertEquals($tenant->data, $cached->data);
$this->assertSame($tenant->domains, $cached->domains);
$this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog());
}
/** @test */
public function a_query_is_not_made_for_tenant_once_id_is_cached()
{
$tenant = Tenant::new()
->withData(['foo' => 'bar'])
->withDomains(['foo.localhost'])
->save();
// query is made
$queried = tenancy()->find($tenant->id);
$this->assertEquals($tenant->data, $queried->data);
$this->assertSame($tenant->domains, $queried->domains);
// cache is set
$this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id));
$this->assertSame($tenant->domains, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
// query is not made
DatabaseStorageDriver::getCentralConnection()->enableQueryLog();
$cached = tenancy()->find($tenant->id);
$this->assertEquals($tenant->data, $cached->data);
$this->assertSame($tenant->domains, $cached->domains);
$this->assertSame([], DatabaseStorageDriver::getCentralConnection()->getQueryLog());
}
/** @test */
public function modifying_tenant_domains_invalidates_the_cached_domain_to_id_mapping()
{
$tenant = Tenant::new()
->withDomains(['foo.localhost', 'bar.localhost'])
->save();
// queried
$this->assertSame($tenant->id, tenancy()->findByDomain('foo.localhost')->id);
$this->assertSame($tenant->id, tenancy()->findByDomain('bar.localhost')->id);
// assert cache set
$this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost'));
$this->assertSame($tenant->id, Cache::get('_tenancy_domain_to_id:bar.localhost'));
$tenant
->removeDomains(['foo.localhost', 'bar.localhost'])
->addDomains(['xyz.localhost'])
->save();
// assert neither domain is cached
$this->assertSame(null, Cache::get('_tenancy_domain_to_id:foo.localhost'));
$this->assertSame(null, Cache::get('_tenancy_domain_to_id:bar.localhost'));
$this->assertSame(null, Cache::get('_tenancy_domain_to_id:xyz.localhost'));
}
/** @test */
public function modifying_tenants_data_invalidates_tenant_data_cache()
{
$tenant = Tenant::new()->withData(['foo' => 'bar'])->save();
// cache record is set
$this->assertSame('bar', tenancy()->find($tenant->id)->get('foo'));
$this->assertSame('bar', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']);
// cache record is invalidated
$tenant->set('foo', 'xyz');
$this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id));
// cache record is set
$this->assertSame('xyz', tenancy()->find($tenant->id)->get('foo'));
$this->assertSame('xyz', Cache::get('_tenancy_id_to_data:' . $tenant->id)['foo']);
// cache record is invalidated
$tenant->foo = 'abc';
$tenant->save();
$this->assertSame(null, Cache::get('_tenancy_id_to_data:' . $tenant->id));
}
/** @test */
public function modifying_tenants_domains_invalidates_tenant_domain_cache()
{
$tenant = Tenant::new()
->withData(['foo' => 'bar'])
->withDomains(['foo.localhost'])
->save();
// cache record is set
$this->assertSame(['foo.localhost'], tenancy()->find($tenant->id)->domains);
$this->assertSame(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id));
// cache record is invalidated
$tenant->addDomains(['bar.localhost'])->save();
$this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
$this->assertEquals(['foo.localhost', 'bar.localhost'], tenancy()->find($tenant->id)->domains);
}
/** @test */
public function deleting_a_tenant_invalidates_all_caches()
{
$tenant = Tenant::new()
->withData(['foo' => 'bar'])
->withDomains(['foo.localhost'])
->save();
tenancy()->findByDomain('foo.localhost');
$this->assertEquals($tenant->id, Cache::get('_tenancy_domain_to_id:foo.localhost'));
$this->assertEquals($tenant->data, Cache::get('_tenancy_id_to_data:' . $tenant->id));
$this->assertEquals(['foo.localhost'], Cache::get('_tenancy_id_to_domains:' . $tenant->id));
$tenant->delete();
$this->assertEquals(null, Cache::get('_tenancy_domain_to_id:foo.localhost'));
$this->assertEquals(null, Cache::get('_tenancy_id_to_data:' . $tenant->id));
$this->assertEquals(null, Cache::get('_tenancy_id_to_domains:' . $tenant->id));
}
}

View file

@ -1,154 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\Tests\Etc\User;
class DataSeparationTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */
public function databases_are_separated()
{
$tenant1 = Tenant::create('tenant1.localhost');
$tenant2 = Tenant::create('tenant2.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant1['id'], $tenant2['id']],
]);
tenancy()->init('tenant1.localhost');
User::create([
'name' => 'foo',
'email' => 'foo@bar.com',
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
]);
$this->assertSame('foo', User::first()->name);
tenancy()->init('tenant2.localhost');
$this->assertSame(null, User::first());
User::create([
'name' => 'xyz',
'email' => 'xyz@bar.com',
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
]);
$this->assertSame('xyz', User::first()->name);
$this->assertSame('xyz@bar.com', User::first()->email);
tenancy()->init('tenant1.localhost');
$this->assertSame('foo', User::first()->name);
$this->assertSame('foo@bar.com', User::first()->email);
$tenant3 = Tenant::create('tenant3.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant1['id'], $tenant3['id']],
]);
tenancy()->init('tenant3.localhost');
$this->assertSame(null, User::first());
tenancy()->init('tenant1.localhost');
DB::table('users')->where('id', 1)->update(['name' => 'xxx']);
$this->assertSame('xxx', User::first()->name);
}
/** @test */
public function redis_is_separated()
{
if (! config('tenancy.redis.tenancy')) {
$this->markTestSkipped('Redis tenancy disabled.');
}
Tenant::create('tenant1.localhost');
Tenant::create('tenant2.localhost');
tenancy()->init('tenant1.localhost');
Redis::set('foo', 'bar');
$this->assertSame('bar', Redis::get('foo'));
tenancy()->init('tenant2.localhost');
$this->assertSame(null, Redis::get('foo'));
Redis::set('foo', 'xyz');
Redis::set('abc', 'def');
$this->assertSame('xyz', Redis::get('foo'));
$this->assertSame('def', Redis::get('abc'));
tenancy()->init('tenant1.localhost');
$this->assertSame('bar', Redis::get('foo'));
$this->assertSame(null, Redis::get('abc'));
Tenant::create('tenant3.localhost');
tenancy()->init('tenant3.localhost');
$this->assertSame(null, Redis::get('foo'));
$this->assertSame(null, Redis::get('abc'));
}
/** @test */
public function cache_is_separated()
{
Tenant::create('tenant1.localhost');
Tenant::create('tenant2.localhost');
tenancy()->init('tenant1.localhost');
Cache::put('foo', 'bar', 60);
$this->assertSame('bar', Cache::get('foo'));
tenancy()->init('tenant2.localhost');
$this->assertSame(null, Cache::get('foo'));
Cache::put('foo', 'xyz', 60);
Cache::put('abc', 'def', 60);
$this->assertSame('xyz', Cache::get('foo'));
$this->assertSame('def', Cache::get('abc'));
tenancy()->init('tenant1.localhost');
$this->assertSame('bar', Cache::get('foo'));
$this->assertSame(null, Cache::get('abc'));
Tenant::create('tenant3.localhost');
tenancy()->init('tenant3.localhost');
$this->assertSame(null, Cache::get('foo'));
$this->assertSame(null, Cache::get('abc'));
}
/** @test */
public function filesystem_is_separated()
{
Tenant::create('tenant1.localhost');
Tenant::create('tenant2.localhost');
tenancy()->init('tenant1.localhost');
Storage::disk('public')->put('foo', 'bar');
$this->assertSame('bar', Storage::disk('public')->get('foo'));
tenancy()->init('tenant2.localhost');
$this->assertFalse(Storage::disk('public')->exists('foo'));
Storage::disk('public')->put('foo', 'xyz');
Storage::disk('public')->put('abc', 'def');
$this->assertSame('xyz', Storage::disk('public')->get('foo'));
$this->assertSame('def', Storage::disk('public')->get('abc'));
tenancy()->init('tenant1.localhost');
$this->assertSame('bar', Storage::disk('public')->get('foo'));
$this->assertFalse(Storage::disk('public')->exists('abc'));
Tenant::create('tenant3.localhost');
tenancy()->init('tenant3.localhost');
$this->assertFalse(Storage::disk('public')->exists('foo'));
$this->assertFalse(Storage::disk('public')->exists('abc'));
}
}

View file

@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Tenant;
class DatabaseManagerTest extends TestCase
{
/** @test */
public function reconnect_method_works()
{
$old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
$this->createTenant();
$this->initTenancy();
app(\Stancl\Tenancy\DatabaseManager::class)->reconnect();
$new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
$this->assertSame($old_connection_name, $new_connection_name);
$this->assertNotEquals('tenant', $new_connection_name);
}
/** @test */
public function db_name_is_prefixed_with_db_path_when_sqlite_is_used()
{
if (file_exists(database_path('foodb'))) {
unlink(database_path('foodb')); // cleanup
}
config(['database.connections.fooconn.driver' => 'sqlite']);
$tenant = Tenant::new()->withData([
'_tenancy_db_name' => 'foodb',
'_tenancy_db_connection' => 'fooconn',
])->save();
app(DatabaseManager::class)->createTenantConnection($tenant);
$this->assertSame(config('database.connections.tenant.database'), database_path('foodb'));
}
/** @test */
public function the_default_db_is_used_when_template_connection_is_null()
{
$this->assertSame('central', config('database.default'));
config([
'database.connections.central.foo' => 'bar',
'tenancy.database.template_connection' => null,
]);
$this->createTenant();
$this->initTenancy();
$this->assertSame('tenant', config('database.default'));
$this->assertSame('bar', config('database.connections.' . config('database.default') . '.foo'));
}
/** @test */
public function ending_tenancy_doesnt_purge_the_central_connection()
{
$this->markTestIncomplete('Seems like this only happens on MySQL?');
// regression test for https://github.com/stancl/tenancy/pull/189
// config(['tenancy.migrate_after_creation' => true]);
tenancy()->create(['foo.localhost']);
tenancy()->init('foo.localhost');
tenancy()->end();
$this->assertNotEmpty(tenancy()->all());
tenancy()->all()->each->delete();
}
}

View file

@ -1,144 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Str;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\Tests\Etc\User;
class DatabaseSchemaManagerTest extends TestCase
{
public $autoCreateTenant = true;
public $autoInitTenancy = false;
protected function getEnvironmentSetUp($app)
{
parent::getEnvironmentSetUp($app);
$app['config']->set([
'database.default' => 'pgsql',
'tenancy.storage_drivers.db.connection' => 'pgsql',
'database.connections.pgsql.database' => 'main',
'database.connections.pgsql.schema' => 'public',
'tenancy.database.template_connection' => null,
'tenancy.database.suffix' => '',
'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class,
]);
}
/** @test */
public function reconnect_method_works()
{
$old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
tenancy()->init('test.localhost');
app(\Stancl\Tenancy\DatabaseManager::class)->reconnect();
$new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
$this->assertSame($old_connection_name, $new_connection_name);
}
/** @test */
public function the_default_db_is_used_when_template_connection_is_null()
{
$this->assertSame('pgsql', config('database.default'));
config([
'database.connections.pgsql.foo' => 'bar',
'tenancy.database.template_connection' => null,
]);
tenancy()->init('test.localhost');
$this->assertSame('tenant', config('database.default'));
$this->assertSame('bar', config('database.connections.tenant.foo'));
}
/** @test */
public function make_sure_using_schema_connection()
{
$tenant = tenancy()->create(['schema.localhost']);
tenancy()->init('schema.localhost');
$this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema'));
}
/** @test */
public function databases_are_separated_using_schema_and_not_database()
{
tenancy()->create('foo.localhost');
tenancy()->init('foo.localhost');
$this->assertSame('tenant', config('database.default'));
$this->assertSame('main', config('database.connections.tenant.database'));
$schema1 = config('database.connections.' . config('database.default') . '.schema');
$database1 = config('database.connections.' . config('database.default') . '.database');
tenancy()->create('bar.localhost');
tenancy()->init('bar.localhost');
$this->assertSame('tenant', config('database.default'));
$this->assertSame('main', config('database.connections.tenant.database'));
$schema2 = config('database.connections.' . config('database.default') . '.schema');
$database2 = config('database.connections.' . config('database.default') . '.database');
$this->assertSame($database1, $database2);
$this->assertNotSame($schema1, $schema2);
}
/** @test */
public function schemas_are_separated()
{
// copied from DataSeparationTest
$tenant1 = Tenant::create('tenant1.localhost');
$tenant2 = Tenant::create('tenant2.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant1['id'], $tenant2['id']],
]);
tenancy()->init('tenant1.localhost');
User::create([
'name' => 'foo',
'email' => 'foo@bar.com',
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
]);
$this->assertSame('foo', User::first()->name);
tenancy()->init('tenant2.localhost');
$this->assertSame(null, User::first());
User::create([
'name' => 'xyz',
'email' => 'xyz@bar.com',
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
]);
$this->assertSame('xyz', User::first()->name);
$this->assertSame('xyz@bar.com', User::first()->email);
tenancy()->init('tenant1.localhost');
$this->assertSame('foo', User::first()->name);
$this->assertSame('foo@bar.com', User::first()->email);
$tenant3 = Tenant::create('tenant3.localhost');
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant1['id'], $tenant3['id']],
]);
tenancy()->init('tenant3.localhost');
$this->assertSame(null, User::first());
tenancy()->init('tenant1.localhost');
\DB::table('users')->where('id', 1)->update(['name' => 'xxx']);
$this->assertSame('xxx', User::first()->name);
}
}

View file

@ -39,10 +39,6 @@ class HttpKernel extends Kernel
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
], ],
'tenancy' => [
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
],
'api' => [ 'api' => [
'throttle:60,1', 'throttle:60,1',
'bindings', 'bindings',
@ -66,18 +62,5 @@ class HttpKernel extends Kernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'tenancy' => \Stancl\Tenancy\Middleware\InitializeTenancy::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Stancl\Tenancy\Middleware\InitializeTenancy::class,
]; ];
} }

View file

@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Tenancy;
use Tenant;
class FacadeTest extends TestCase
{
public $autoCreateTenant = true;
public $autoInitTenancy = true;
/** @test */
public function tenant_manager_can_be_accessed_using_the_Tenancy_facade()
{
$this->assertSame(tenancy()->getTenant(), Tenancy::getTenant());
}
/** @test */
public function tenant_storage_can_be_accessed_using_the_Tenant_facade()
{
tenant()->put('foo', 'bar');
Tenant::put('abc', 'xyz');
$this->assertSame('bar', Tenant::get('foo'));
$this->assertSame('xyz', Tenant::get('abc'));
}
/** @test */
public function tenant_can_be_created_using_the_Tenant_facade()
{
$this->assertSame('bar', Tenant::create(['foo.localhost'], ['foo' => 'bar'])->foo);
}
}

View file

@ -2,17 +2,15 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests\Features;
use Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Features\CrossDomainRedirect; use Stancl\Tenancy\Features\CrossDomainRedirect;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Tests\TestCase;
class RedirectTest extends TestCase class RedirectTest extends TestCase
{ {
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */ /** @test */
public function tenant_redirect_macro_replaces_only_the_hostname() public function tenant_redirect_macro_replaces_only_the_hostname()
{ {
@ -28,8 +26,8 @@ class RedirectTest extends TestCase
return redirect()->route('home')->domain('abcd'); return redirect()->route('home')->domain('abcd');
}); });
Tenant::create('foo.localhost'); $tenant = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant);
$this->get('/redirect') $this->get('/redirect')
->assertRedirect('http://abcd/foobar'); ->assertRedirect('http://abcd/foobar');
@ -42,9 +40,7 @@ class RedirectTest extends TestCase
return 'Foo'; return 'Foo';
})->name('foo'); })->name('foo');
$this->assertSame('http://foo.localhost/abcdef/as/df', tenant_route('foo', ['a' => 'as', 'b' => 'df'], 'foo.localhost')); $this->assertSame('http://foo.localhost/abcdef/as/df', tenant_route('foo.localhost', 'foo', ['a' => 'as', 'b' => 'df']));
$this->assertSame('http://foo.localhost/abcdef', tenant_route('foo', [], 'foo.localhost')); $this->assertSame('http://foo.localhost/abcdef', tenant_route('foo.localhost', 'foo', []));
$this->assertSame('http://' . request()->getHost() . '/abcdef/x/y', tenant_route('foo', ['a' => 'x', 'b' => 'y']));
} }
} }

View file

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Stancl\Tenancy\Contracts\Future\CanFindByAnyKey;
use Stancl\Tenancy\Tenant;
class FutureTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */
public function keys_can_be_deleted_from_tenant_storage()
{
$tenant = Tenant::new()->withData(['email' => 'foo@example.com', 'role' => 'admin'])->save();
$this->assertArrayHasKey('email', $tenant->data);
$tenant->deleteKey('email');
$this->assertArrayNotHasKey('email', $tenant->data);
$this->assertArrayNotHasKey('email', tenancy()->all()->first()->data);
$tenant->put(['foo' => 'bar', 'abc' => 'xyz']);
$this->assertArrayHasKey('foo', $tenant->data);
$this->assertArrayHasKey('abc', $tenant->data);
$tenant->deleteKeys(['foo', 'abc']);
$this->assertArrayNotHasKey('foo', $tenant->data);
$this->assertArrayNotHasKey('abc', $tenant->data);
}
/** @test */
public function tenant_can_be_identified_using_an_arbitrary_string()
{
if (! tenancy()->storage instanceof CanFindByAnyKey) {
$this->markTestSkipped(get_class(tenancy()->storage) . ' does not implement the CanFindByAnyKey interface.');
}
$tenant = Tenant::new()->withData(['email' => 'foo@example.com'])->save();
$this->assertSame($tenant->id, tenancy()->findByEmail('foo@example.com')->id);
}
}

View file

@ -1,77 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Event;
class QueueTest extends TestCase
{
public $autoCreateTenant = true;
public $autoInitTenancy = true;
/** @test */
public function queues_use_non_tenant_db_connection()
{
// requires using the db driver
$this->markTestIncomplete();
}
/** @test */
public function tenancy_is_initialized_inside_queues()
{
$this->loadLaravelMigrations(['--database' => 'tenant']);
Event::fake();
dispatch(new TestJob());
Event::assertDispatched(JobProcessing::class, function ($event) {
return $event->job->payload()['tenant_id'] === tenant('id');
});
}
/** @test */
public function tenancy_is_not_initialized_in_non_tenant_queues()
{
$this->loadLaravelMigrations(['--database' => 'tenant']);
Event::fake();
dispatch(new TestJob())->onConnection('central');
Event::assertDispatched(JobProcessing::class, function ($event) {
return ! isset($event->job->payload()['tenant_id']);
});
}
}
class TestJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
logger(json_encode(\DB::table('users')->get()));
}
}

View file

@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Stancl\Tenancy\Tenant;
class ReidentificationTest extends TestCase
{
public $autoCreateTenant = true;
public $autoInitTenancy = false;
/**
* These tests are run when a tenant is identified after another tenant has already been identified.
*/
/** @test */
public function storage_facade_roots_are_correct()
{
$originals = [];
foreach (config('tenancy.filesystem.disks') as $disk) {
$originals[$disk] = config("filesystems.disks.{$disk}.root");
}
tenancy()->init('test.localhost');
Tenant::new()->withDomains(['second.localhost'])->save();
tenancy()->init('second.localhost');
foreach (config('tenancy.filesystem.disks') as $disk) {
$suffix = config('tenancy.filesystem.suffix_base') . tenant('id');
$current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($override = config("tenancy.filesystem.root_override.{$disk}")) {
$correct_path_prefix = str_replace('%storage_path%', storage_path(), $override);
} else {
if ($base = $originals[$disk]) {
$correct_path_prefix = $base . "/$suffix/";
} else {
$correct_path_prefix = "$suffix/";
}
}
$this->assertSame($correct_path_prefix, $current_path_prefix);
}
}
/** @test */
public function storage_path_is_correct()
{
$original = storage_path();
tenancy()->init('test.localhost');
Tenant::new()->withDomains(['second.localhost'])->save();
tenancy()->init('second.localhost');
$suffix = config('tenancy.filesystem.suffix_base') . tenant('id');
$this->assertSame($original . "/$suffix", storage_path());
}
}

View file

@ -1,97 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Redis;
class TenancyBootstrappersTest extends TestCase
{
public $autoCreateTenant = true;
public $autoInitTenancy = false;
/** @test */
public function database_connection_is_switched()
{
$old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
$this->initTenancy();
$new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
$this->assertNotEquals($old_connection_name, $new_connection_name);
$this->assertEquals('tenant', $new_connection_name);
}
/** @test */
public function redis_is_prefixed()
{
if (! config('tenancy.redis.tenancy')) {
$this->markTestSkipped('Redis tenancy disabled.');
}
$this->initTenancy();
foreach (config('tenancy.redis.prefixed_connections', ['default']) as $connection) {
$prefix = config('tenancy.redis.prefix_base') . tenant('id');
$client = Redis::connection($connection)->client();
$this->assertEquals($prefix, $client->getOption($client::OPT_PREFIX));
}
}
/** @test */
public function filesystem_is_suffixed()
{
$old_storage_path = storage_path();
$old_storage_facade_roots = [];
foreach (config('tenancy.filesystem.disks') as $disk) {
$old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root");
}
$this->initTenancy();
$new_storage_path = storage_path();
$this->assertEquals($old_storage_path . '/' . config('tenancy.filesystem.suffix_base') . tenant('id'), $new_storage_path);
foreach (config('tenancy.filesystem.disks') as $disk) {
$suffix = config('tenancy.filesystem.suffix_base') . tenant('id');
$current_path_prefix = \Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($override = config("tenancy.filesystem.root_override.{$disk}")) {
$correct_path_prefix = str_replace('%storage_path%', storage_path(), $override);
} else {
if ($base = $old_storage_facade_roots[$disk]) {
$correct_path_prefix = $base . "/$suffix/";
} else {
$correct_path_prefix = "$suffix/";
}
}
$this->assertSame($correct_path_prefix, $current_path_prefix);
}
}
/** @test */
public function cache_is_tagged()
{
$this->assertSame(['foo'], cache()->tags('foo')->getTags()->getNames());
$this->initTenancy();
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
$this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames());
}
/** @test */
public function the_default_db_connection_is_used_when_the_config_value_is_null()
{
$original = config('database.default');
tenancy()->create(['foo.localhost']);
tenancy()->init('foo.localhost');
$this->assertSame(null, config("database.connections.$original.foo"));
config(["database.connections.$original.foo" => 'bar']);
tenancy()->create(['bar.localhost']);
tenancy()->init('bar.localhost');
$this->assertSame('bar', config("database.connections.$original.foo"));
}
}

View file

@ -1,227 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Mockery;
use Stancl\Tenancy\Contracts\StorageDriver;
use Stancl\Tenancy\Tenant;
use Tenancy;
class TenantClassTest extends TestCase
{
public $autoInitTenancy = false;
public $autoCreateTenant = false;
/** @test */
public function data_cache_works_properly()
{
// $spy = Mockery::spy(config('tenancy.storage_driver'))->makePartial();
// $this->instance(StorageDriver::class, $spy);
$tenant = Tenant::create(['foo.localhost'], ['foo' => 'bar']);
$this->assertSame('bar', $tenant->data['foo']);
$tenant->put('abc', 'xyz');
$this->assertSame('xyz', $tenant->data['abc']);
$tenant->put(['aaa' => 'bbb', 'ccc' => 'ddd']);
$this->assertSame('bbb', $tenant->data['aaa']);
$this->assertSame('ddd', $tenant->data['ccc']);
// $spy->shouldNotHaveReceived('get');
$this->assertSame(null, $tenant->dfuighdfuigfhdui);
// $spy->shouldHaveReceived('get')->once();
Mockery::close();
}
/** @test */
public function tenant_can_have_multiple_domains()
{
$tenant = Tenant::create(['foo.localhost', 'bar.localhost']);
$this->assertSame(['foo.localhost', 'bar.localhost'], $tenant->domains);
$this->assertSame($tenant->id, Tenancy::findByDomain('foo.localhost')->id);
$this->assertSame($tenant->id, Tenancy::findByDomain('bar.localhost')->id);
}
/** @test */
public function updating_a_tenant_works()
{
$id = 'abc' . $this->randomString();
$tenant = Tenant::create(['foo.localhost'], ['id' => $id]);
$tenant->foo = 'bar';
$tenant->save();
$this->assertEquals(['id' => $id, 'foo' => 'bar', '_tenancy_db_name' => $tenant->database()->getName()], $tenant->data);
$this->assertEquals(['id' => $id, 'foo' => 'bar', '_tenancy_db_name' => $tenant->database()->getName()], tenancy()->find($id)->data);
$tenant->addDomains('abc.localhost');
$tenant->save();
$this->assertEqualsCanonicalizing(['foo.localhost', 'abc.localhost'], $tenant->domains);
$this->assertEqualsCanonicalizing(['foo.localhost', 'abc.localhost'], tenancy()->find($id)->domains);
$tenant->removeDomains(['foo.localhost']);
$tenant->save();
$this->assertEqualsCanonicalizing(['abc.localhost'], $tenant->domains);
$this->assertEqualsCanonicalizing(['abc.localhost'], tenancy()->find($id)->domains);
$tenant->withDomains(['completely.localhost', 'different.localhost', 'domains.localhost']);
$tenant->save();
$this->assertEqualsCanonicalizing(['completely.localhost', 'different.localhost', 'domains.localhost'], $tenant->domains);
$this->assertEqualsCanonicalizing(['completely.localhost', 'different.localhost', 'domains.localhost'], tenancy()->find($id)->domains);
}
/** @test */
public function with_methods_work()
{
$id = 'foo' . $this->randomString();
$tenant = Tenant::new()->withDomains(['foo.localhost'])->with('id', $id);
$this->assertSame($id, $tenant->id);
$id2 = 'bar' . $this->randomString();
$tenant2 = Tenant::new()->withDomains(['bar.localhost'])->withId($id2)->withFooBar('xyz');
$this->assertSame($id2, $tenant2->data['id']);
$this->assertSame('xyz', $tenant2->foo_bar);
$this->assertArrayHasKey('foo_bar', $tenant2->data);
}
/** @test */
public function an_exception_is_thrown_when_an_unknown_method_is_called()
{
$tenant = Tenant::new();
$this->expectException(\BadMethodCallException::class);
$tenant->sdjigndfgnjdfgj();
}
/** @test */
public function tenant_data_can_be_set_during_creation()
{
Tenant::new()->withData(['foo' => 'bar'])->save();
$data = tenancy()->all()->first()->data;
unset($data['id']);
unset($data['_tenancy_db_name']);
$this->assertSame(['foo' => 'bar'], $data);
}
/** @test */
public function run_method_works()
{
$this->assertSame(null, tenancy()->getTenant());
$users_table_empty = function () {
return count(\DB::table('users')->get()) === 0;
};
$tenant = Tenant::new()->save();
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant->id],
]);
tenancy()->initialize($tenant);
$this->assertTrue($users_table_empty());
tenancy()->end();
$foo = $tenant->run(function () {
\DB::table('users')->insert([
'name' => 'foo',
'email' => 'foo@bar.xy',
'password' => bcrypt('secret'),
]);
return 'foo';
});
// test return value
$this->assertSame('foo', $foo);
// test that tenancy was ended
$this->assertSame(false, tenancy()->initialized);
$this->assertSame(null, tenancy()->getTenant());
// test closure
tenancy()->initialize($tenant);
$this->assertFalse($users_table_empty());
// test returning to original tenant
$tenant2 = Tenant::new()->save();
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant2->id],
]);
tenancy()->initialize($tenant2);
$this->assertSame($tenant2, tenancy()->getTenant());
$this->assertTrue($users_table_empty());
$tenant->run(function () {
\DB::table('users')->insert([
'name' => 'bar',
'email' => 'bar@bar.xy',
'password' => bcrypt('secret'),
]);
});
$this->assertSame($tenant2, tenancy()->getTenant());
$this->assertSame(2, $tenant->run(function () {
return \DB::table('users')->count();
}));
// test that the tenant variable can be accessed
$this->assertSame($tenant->id, $tenant->run(function ($tenant) {
return $tenant->id;
}));
}
/** @test */
public function extra_config_is_merged_into_the_connection_config_array()
{
$tenant = Tenant::new()->withData([
'_tenancy_db_link' => 'foo',
'_tenancy_db_name' => 'dbname',
'_tenancy_db_username' => 'usernamefoo',
'_tenancy_db_password' => 'passwordfoo',
'_tenancy_db_connection' => 'mysql',
]);
config(['database.connections.mysql' => [
'driver' => 'mysql',
'url' => null,
'host' => 'mysql',
'port' => '3306',
'database' => 'main',
'username' => 'root',
'password' => 'password',
'unix_socket' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => [],
]]);
$this->assertEquals([
'database' => 'dbname',
'username' => 'usernamefoo',
'password' => 'passwordfoo',
'link' => 'foo',
'driver' => 'mysql',
'url' => null,
'host' => 'mysql',
'port' => '3306',
'unix_socket' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => [],
], $tenant->database()->connection());
}
}

View file

@ -1,145 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\Queue;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseDeleter;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager;
use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;
class TenantDatabaseManagerTest extends TestCase
{
public $autoInitTenancy = false;
/**
* @test
* @dataProvider database_manager_provider
*/
public function databases_can_be_created_and_deleted($driver, $databaseManager)
{
if (! $this->isContainerized()) {
$this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.');
}
config()->set([
"tenancy.database_managers.$driver" => $databaseManager,
]);
$name = 'db' . $this->randomString();
$tenant = Tenant::new()->withData([
'_tenancy_db_name' => $name,
'_tenancy_db_connection' => $driver,
]);
$this->assertFalse(app($databaseManager)->databaseExists($name));
$tenant->save(); // generate credentials & create DB
$this->assertTrue(app($databaseManager)->databaseExists($name));
app($databaseManager)->deleteDatabase($tenant);
$this->assertFalse(app($databaseManager)->databaseExists($name));
}
/** @test */
public function dbs_can_be_created_when_another_driver_is_used_for_the_central_db()
{
$this->assertSame('central', config('database.default'));
$database = 'db' . $this->randomString();
$tenant = Tenant::new()->withData([
'_tenancy_db_name' => $database,
'_tenancy_db_connection' => 'mysql',
]);
$this->assertFalse(app(MySQLDatabaseManager::class)->databaseExists($database));
$tenant->save(); // create DB
$this->assertTrue(app(MySQLDatabaseManager::class)->databaseExists($database));
$database = 'db' . $this->randomString();
$tenant = Tenant::new()->withData([
'_tenancy_db_name' => $database,
'_tenancy_db_connection' => 'pgsql',
]);
$this->assertFalse(app(PostgreSQLDatabaseManager::class)->databaseExists($database));
$tenant->save(); // create DB
$this->assertTrue(app(PostgreSQLDatabaseManager::class)->databaseExists($database));
}
/**
* @test
* @dataProvider database_manager_provider
*/
public function databases_can_be_created_and_deleted_using_queued_commands($driver, $databaseManager)
{
if (! $this->isContainerized()) {
$this->markTestSkipped('As to not bloat your computer with test databases, this test is not run by default.');
}
config()->set([
'database.default' => $driver,
"tenancy.database_managers.$driver" => $databaseManager,
]);
$name = 'db' . $this->randomString();
$tenant = Tenant::new()->withData([
'_tenancy_db_name' => $name,
'_tenancy_db_connection' => $driver,
]);
$tenant->database()->makeCredentials();
$this->assertFalse(app($databaseManager)->databaseExists($name));
$job = new QueuedTenantDatabaseCreator(app($databaseManager), $tenant);
$job->handle();
$this->assertTrue(app($databaseManager)->databaseExists($name));
$job = new QueuedTenantDatabaseDeleter(app($databaseManager), $tenant);
$job->handle();
$this->assertFalse(app($databaseManager)->databaseExists($name));
}
public function database_manager_provider()
{
return [
['mysql', MySQLDatabaseManager::class],
['mysql', PermissionControlledMySQLDatabaseManager::class],
['sqlite', SQLiteDatabaseManager::class],
['pgsql', PostgreSQLDatabaseManager::class],
['pgsql', PostgreSQLSchemaManager::class],
];
}
/** @test */
public function database_creation_can_be_queued()
{
Queue::fake();
config()->set([
'tenancy.queue_database_creation' => true,
]);
Tenant::create(['test2.localhost']);
Queue::assertPushed(QueuedTenantDatabaseCreator::class);
}
/** @test */
public function database_deletion_can_be_queued()
{
Queue::fake();
$tenant = Tenant::create(['test2.localhost']);
config()->set([
'tenancy.queue_database_deletion' => true,
'tenancy.delete_database_after_tenant_deletion' => true,
]);
$tenant->delete();
Queue::assertPushed(QueuedTenantDatabaseDeleter::class);
}
}

View file

@ -1,138 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Stancl\Tenancy\Tenant;
use Tenancy;
class TenantManagerEventsTest extends TestCase
{
public $autoInitTenancy = false;
/** @test */
public function bootstrapping_event_works()
{
$id = Tenant::new()->withDomains(['foo.localhost'])->save()['id'];
Tenancy::eventListener('bootstrapping', function ($tenantManager) use ($id) {
if ($tenantManager->getTenant('id') === $id) {
config(['tenancy.foo' => 'bar']);
}
});
$this->assertSame(null, config('tenancy.foo'));
tenancy()->init('foo.localhost');
$this->assertSame('bar', config('tenancy.foo'));
}
/** @test */
public function bootstrapped_event_works()
{
$id = Tenant::new()->withDomains(['foo.localhost'])->save()['id'];
Tenancy::eventListener('bootstrapped', function ($tenantManager) use ($id) {
if ($tenantManager->getTenant('id') === $id) {
config(['tenancy.foo' => 'bar']);
}
});
$this->assertSame(null, config('tenancy.foo'));
tenancy()->init('foo.localhost');
$this->assertSame('bar', config('tenancy.foo'));
}
/** @test */
public function ending_event_works()
{
$id = Tenant::new()->withDomains(['foo.localhost'])->save()['id'];
Tenancy::eventListener('ending', function ($tenantManager) use ($id) {
if ($tenantManager->getTenant('id') === $id) {
config(['tenancy.foo' => 'bar']);
}
});
$this->assertSame(null, config('tenancy.foo'));
tenancy()->init('foo.localhost');
$this->assertSame(null, config('tenancy.foo'));
tenancy()->endTenancy();
$this->assertSame('bar', config('tenancy.foo'));
}
/** @test */
public function ended_event_works()
{
Tenant::new()->withDomains(['foo.localhost'])->save()['id'];
Tenancy::eventListener('ended', function ($tenantManager) {
config(['tenancy.foo' => 'bar']);
});
$this->assertSame(null, config('tenancy.foo'));
tenancy()->init('foo.localhost');
$this->assertSame(null, config('tenancy.foo'));
tenancy()->endTenancy();
$this->assertSame('bar', config('tenancy.foo'));
}
/** @test */
public function database_can_be_reconnected_using_event_hooks()
{
config(['database.connections.tenantabc' => [
'driver' => 'sqlite',
'database' => database_path('some_special_database.sqlite'),
]]);
$id = Tenant::create('abc.localhost')['id'];
Tenancy::eventListener('bootstrapping', function ($tenancy) use ($id) {
if ($tenancy->getTenant()['id'] === $id) {
$tenancy->database->switchConnection('tenantabc');
return ['database'];
}
});
$this->assertNotSame('tenantabc', \DB::connection()->getConfig()['name']);
tenancy()->init('abc.localhost');
$this->assertSame('tenantabc', \DB::connection()->getConfig()['name']);
}
/** @test */
public function database_cannot_be_reconnected_without_using_prevents()
{
config(['database.connections.tenantabc' => [
'driver' => 'sqlite',
'database' => database_path('some_special_database.sqlite'),
]]);
$id = Tenant::create('abc.localhost')['id'];
Tenancy::eventListener('bootstrapping', function ($tenancy) use ($id) {
if ($tenancy->getTenant()['id'] === $id) {
$tenancy->database->switchConnection('tenantabc');
// return ['database'];
}
});
$this->assertNotSame('tenantabc', \DB::connection()->getConfig()['name']);
tenancy()->init('abc.localhost');
$this->assertSame('tenant', \DB::connection()->getConfig()['name']);
}
/** @test */
public function tenant_is_persisted_before_the_created_hook_is_called()
{
$was_persisted = false;
Tenancy::eventListener('tenant.created', function ($tenancy, $tenant) use (&$was_persisted) {
$was_persisted = $tenant->persisted;
});
Tenant::new()->save();
$this->assertTrue($was_persisted);
}
}

View file

@ -1,383 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Exceptions\DomainsOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantDoesNotExistException;
use Stancl\Tenancy\Exceptions\TenantWithThisIdAlreadyExistsException;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseCreator;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseMigrator;
use Stancl\Tenancy\Jobs\QueuedTenantDatabaseSeeder;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantManager;
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
class TenantManagerTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */
public function current_tenant_can_be_retrieved_using_getTenant()
{
$tenant = Tenant::new()->withDomains(['test2.localhost'])->save();
tenancy()->init('test2.localhost');
$this->assertEquals($tenant, tenancy()->getTenant());
}
/** @test */
public function initById_works()
{
$tenant = Tenant::new()->withDomains(['foo.localhost'])->save();
$this->assertNotEquals($tenant, tenancy()->getTenant());
tenancy()->initById($tenant['id']);
$this->assertEquals($tenant, tenancy()->getTenant());
}
/** @test */
public function findByDomain_works()
{
$tenant = Tenant::new()->withDomains(['foo.localhost'])->save();
$this->assertEquals($tenant, tenancy()->findByDomain('foo.localhost'));
}
/** @test */
public function find_works()
{
Tenant::new()->withDomains(['dev.localhost'])->save();
tenancy()->init('dev.localhost');
$this->assertEquals(tenant(), tenancy()->find(tenant('id')));
}
/** @test */
public function findByDomain_throws_an_exception_when_an_unused_domain_is_supplied()
{
$this->expectException(\Exception::class);
tenancy()->findByDomain('nonexistent.domain');
}
/** @test */
public function tenancy_can_be_ended()
{
$originals = [
'databaseName' => DB::connection()->getDatabaseName(),
'storage_path' => storage_path(),
'storage_root' => Storage::disk('local')->getAdapter()->getPathPrefix(),
'cache' => app('cache'),
];
// Verify that these assertions are the right way for testing this
$this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName());
$this->assertSame($originals['storage_path'], storage_path());
$this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix());
$this->assertSame($originals['cache'], app('cache'));
Tenant::new()->withDomains(['foo.localhost'])->save();
tenancy()->init('foo.localhost');
$this->assertNotSame($originals['databaseName'], DB::connection()->getDatabaseName());
$this->assertNotSame($originals['storage_path'], storage_path());
$this->assertNotSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix());
$this->assertNotSame($originals['cache'], app('cache'));
tenancy()->endTenancy();
$this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName());
$this->assertSame($originals['storage_path'], storage_path());
$this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix());
$this->assertSame($originals['cache'], app('cache'));
}
/** @test */
public function tenancy_can_be_ended_after_reidentification()
{
$originals = [
'databaseName' => DB::connection()->getDatabaseName(),
'storage_path' => storage_path(),
'storage_root' => Storage::disk('local')->getAdapter()->getPathPrefix(),
'cache' => app('cache'),
];
Tenant::new()->withDomains(['foo.localhost'])->save();
tenancy()->init('foo.localhost');
$this->assertNotSame($originals['databaseName'], DB::connection()->getDatabaseName());
$this->assertNotSame($originals['storage_path'], storage_path());
$this->assertNotSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix());
$this->assertNotSame($originals['cache'], app('cache'));
tenancy()->endTenancy();
$this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName());
$this->assertSame($originals['storage_path'], storage_path());
$this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix());
$this->assertSame($originals['cache'], app('cache'));
// Reidentify tenant
Tenant::new()->withDomains(['bar.localhost'])->save();
tenancy()->init('bar.localhost');
$this->assertNotSame($originals['databaseName'], DB::connection()->getDatabaseName());
$this->assertNotSame($originals['storage_path'], storage_path());
$this->assertNotSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix());
$this->assertNotSame($originals['cache'], app('cache'));
tenancy()->endTenancy();
$this->assertSame($originals['databaseName'], DB::connection()->getDatabaseName());
$this->assertSame($originals['storage_path'], storage_path());
$this->assertSame($originals['storage_root'], Storage::disk('local')->getAdapter()->getPathPrefix());
$this->assertSame($originals['cache'], app('cache'));
}
/** @test */
public function tenant_can_be_deleted()
{
$tenant = Tenant::new()->withDomains(['foo.localhost'])->save();
$this->assertEquals([$tenant], tenancy()->all()->toArray());
$tenant->delete();
$this->assertEquals([], tenancy()->all()->toArray());
}
/** @test */
public function all_returns_a_list_of_all_tenants()
{
$tenant1 = Tenant::new()->withDomains(['foo.localhost'])->save();
$tenant2 = Tenant::new()->withDomains(['bar.localhost'])->save();
$this->assertEqualsCanonicalizing([$tenant1, $tenant2], tenancy()->all()->toArray());
}
/** @test */
public function data_can_be_passed_in_the_create_method()
{
$data = ['plan' => 'free', 'subscribed_until' => '2020-01-01'];
$tenant = Tenant::create(['foo.localhost'], $data);
$tenant_data = $tenant->data;
unset($tenant_data['id']);
unset($tenant_data['_tenancy_db_name']);
$this->assertSame($data, $tenant_data);
}
/** @test */
public function database_name_can_be_passed_in_the_create_method()
{
$database = 'abc' . $this->randomString();
$tenant = tenancy()->create(['foo.localhost'], [
'_tenancy_db_name' => $database,
]);
$this->assertSame($database, $tenant->database()->getName());
}
/** @test */
public function id_cannot_be_changed()
{
$tenant = Tenant::create(['test2.localhost']);
$this->expectException(\Stancl\Tenancy\Exceptions\TenantStorageException::class);
$tenant->id = 'bar';
$tenant2 = Tenant::create(['test3.localhost']);
$this->expectException(\Stancl\Tenancy\Exceptions\TenantStorageException::class);
$tenant2->put('id', 'foo');
}
/** @test */
public function all_returns_a_collection_of_tenant_objects()
{
Tenant::create('foo.localhost');
$this->assertSame('Tenant', class_basename(tenancy()->all()[0]));
}
/** @test */
public function Tenant_is_bound_correctly_to_the_service_container()
{
$this->assertSame(null, app(Tenant::class));
$tenant = Tenant::create(['foo.localhost']);
app(TenantManager::class)->initializeTenancy($tenant);
$this->assertSame($tenant->id, app(Tenant::class)->id);
$this->assertSame(app(Tenant::class), app(TenantManager::class)->getTenant());
app(TenantManager::class)->endTenancy();
$this->assertSame(app(Tenant::class), app(TenantManager::class)->getTenant());
}
/** @test */
public function id_can_be_supplied_during_creation()
{
$id = 'abc' . $this->randomString();
$this->assertSame($id, Tenant::create(['foo.localhost'], ['id' => $id])->id);
$this->assertTrue(tenancy()->all()->contains(function ($tenant) use ($id) {
return $tenant->id === $id;
}));
}
/** @test */
public function automatic_migrations_work()
{
$tenant = Tenant::create(['foo.localhost']);
tenancy()->initialize($tenant);
$this->assertFalse(\Schema::hasTable('users'));
config(['tenancy.migrate_after_creation' => true]);
$tenant2 = Tenant::create(['bar.localhost']);
tenancy()->initialize($tenant2);
$this->assertTrue(\Schema::hasTable('users'));
}
/** @test */
public function automatic_seeding_works()
{
config(['tenancy.migrate_after_creation' => true]);
$tenant = Tenant::create(['foo.localhost']);
tenancy()->initialize($tenant);
$this->assertSame(0, \DB::table('users')->count());
config([
'tenancy.seed_after_migration' => true,
'tenancy.seeder_parameters' => [
'--class' => ExampleSeeder::class,
],
]);
$tenant2 = Tenant::create(['bar.localhost']);
tenancy()->initialize($tenant2);
$this->assertSame(1, \DB::table('users')->count());
}
/** @test */
public function ensureTenantCanBeCreated_works()
{
$id = 'foo' . $this->randomString();
Tenant::create(['foo.localhost'], ['id' => $id]);
$this->expectException(DomainsOccupiedByOtherTenantException::class);
Tenant::create(['foo.localhost']);
$this->expectException(TenantWithThisIdAlreadyExistsException::class);
Tenant::create(['bar.localhost'], ['id' => $id]);
}
/** @test */
public function automigration_is_queued_when_db_creation_is_queued()
{
Queue::fake();
config([
'tenancy.queue_database_creation' => true,
'tenancy.migrate_after_creation' => true,
]);
$tenant = Tenant::new()->save();
Queue::assertPushedWithChain(QueuedTenantDatabaseCreator::class, [
QueuedTenantDatabaseMigrator::class,
]);
// foreach (Queue::pushedJobs() as $job) {
// $job[0]['job']->handle(); // this doesn't execute the chained job
// }
// tenancy()->initialize($tenant);
// $this->assertTrue(\Schema::hasTable('users'));
}
/** @test */
public function autoseeding_is_queued_when_db_creation_is_queued()
{
Queue::fake();
config([
'tenancy.queue_database_creation' => true,
'tenancy.migrate_after_creation' => true,
'tenancy.seed_after_migration' => true,
]);
Tenant::new()->save();
Queue::assertPushedWithChain(QueuedTenantDatabaseCreator::class, [
QueuedTenantDatabaseMigrator::class,
QueuedTenantDatabaseSeeder::class,
]);
}
/** @test */
public function TenantDoesNotExistException_is_thrown_when_find_is_called_on_an_id_that_does_not_belong_to_any_tenant()
{
$this->expectException(TenantDoesNotExistException::class);
tenancy()->find('gjnfdgf');
}
/** @test */
public function event_listeners_can_accept_arguments()
{
tenancy()->hook('tenant.creating', function ($tenantManager, $tenant) {
$this->assertSame('bar', $tenant->foo);
});
Tenant::new()->withData(['foo' => 'bar'])->save();
}
/** @test */
public function tenant_creating_hook_can_be_used_to_modify_tenants_data()
{
tenancy()->hook('tenant.creating', function ($tm, Tenant $tenant) {
$tenant->put([
'foo' => 'bar',
'abc123' => 'def456',
]);
});
$tenant = Tenant::new()->save();
$this->assertArrayHasKey('foo', $tenant->data);
$this->assertArrayHasKey('abc123', $tenant->data);
}
/** @test */
public function database_creation_can_be_disabled()
{
config(['tenancy.create_database' => false]);
tenancy()->hook('database.creating', function () {
$this->fail();
});
$tenant = Tenant::new()->save();
$this->assertTrue(true);
}
/** @test */
public function database_creation_can_be_disabled_for_specific_tenants()
{
config(['tenancy.create_database' => true]);
tenancy()->hook('database.creating', function () {
$this->assertTrue(true);
});
$tenant = Tenant::new()->save();
tenancy()->hook('database.creating', function () {
$this->fail();
});
$tenant2 = Tenant::new()->withData([
'_tenancy_create_database' => false,
])->save();
}
}

View file

@ -1,197 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
use Stancl\Tenancy\StorageDrivers\Database\TenantRepository;
use Stancl\Tenancy\Tenant;
class TenantStorageTest extends TestCase
{
public $autoCreateTenant = true;
public $autoInitTenancy = true;
/** @test */
public function deleting_a_tenant_works()
{
$abc = Tenant::new()->withDomains(['abc.localhost'])->save();
$exists = function () use ($abc) {
return tenancy()->all()->contains(function ($tenant) use ($abc) {
return $tenant->id === $abc->id;
});
};
$this->assertTrue($exists());
$abc->delete();
$this->assertFalse($exists());
}
/** @test */
public function set_is_a_working_alias_for_put()
{
tenant()->set('foo', 'bar');
$this->assertSame('bar', tenant()->get('foo'));
}
/** @test */
public function put_works_with_key_and_value_as_separate_args()
{
tenant()->put('foo', 'bar');
$this->assertSame('bar', tenant()->get('foo'));
}
/** @test */
public function put_works_with_key_and_value_as_a_single_arg()
{
$keys = ['foo', 'abc'];
$vals = ['bar', 'xyz'];
$data = array_combine($keys, $vals);
tenant()->put($data);
$this->assertSame($data, tenant()->get($keys));
}
/** @test */
public function put_on_the_current_tenant_pushes_the_value_into_the_tenant_property_array()
{
tenant()->put('foo', 'bar');
$this->assertSame('bar', tenancy()->getTenant('foo'));
}
/** @test */
public function arrays_can_be_stored()
{
tenant()->put('foo', [1, 2]);
$this->assertSame([1, 2], tenant()->get('foo'));
}
/** @test */
public function associative_arrays_can_be_stored()
{
$data = ['a' => 'b', 'c' => 'd'];
tenant()->put('foo', $data);
$this->assertSame($data, tenant()->get('foo'));
}
/** @test */
public function correct_storage_driver_is_used()
{
if (config('tenancy.storage_driver') == 'db') {
$this->assertSame('DatabaseStorageDriver', class_basename(tenancy()->storage));
} elseif (config('tenancy.storage_driver') == 'redis') {
$this->assertSame('RedisStorageDriver', class_basename(tenancy()->storage));
} else {
dd(class_basename(config('tenancy.storage_driver')));
}
}
/** @test */
public function data_is_stored_with_correct_data_types()
{
tenant()->put('someBool', false);
$this->assertSame('boolean', gettype(tenant()->get('someBool')));
$this->assertSame('boolean', gettype(tenant()->get(['someBool'])['someBool']));
tenant()->put('someInt', 5);
$this->assertSame('integer', gettype(tenant()->get('someInt')));
$this->assertSame('integer', gettype(tenant()->get(['someInt'])['someInt']));
tenant()->put('someDouble', 11.40);
$this->assertSame('double', gettype(tenant()->get('someDouble')));
$this->assertSame('double', gettype(tenant()->get(['someDouble'])['someDouble']));
tenant()->put('string', 'foo');
$this->assertSame('string', gettype(tenant()->get('string')));
$this->assertSame('string', gettype(tenant()->get(['string'])['string']));
}
/** @test */
public function tenant_repository_uses_correct_connection()
{
config(['database.connections.foo' => config('database.connections.sqlite')]);
config(['tenancy.storage_drivers.db.connection' => 'foo']);
$this->assertSame('foo', app(TenantRepository::class)->database->getName());
}
/** @test */
public function retrieving_data_without_cache_works()
{
Tenant::new()->withDomains(['foo.localhost'])->save();
tenancy()->init('foo.localhost');
tenant()->put('foo', 'bar');
$this->assertSame('bar', tenant()->get('foo'));
$this->assertSame(['foo' => 'bar'], tenant()->get(['foo']));
tenancy()->endTenancy();
tenancy()->init('foo.localhost');
$this->assertSame('bar', tenant()->get('foo'));
$this->assertSame(['foo' => 'bar'], tenant()->get(['foo']));
}
/** @test */
public function custom_columns_work_with_db_storage_driver()
{
if (config('tenancy.storage_driver') != 'db') {
$this->markTestSkipped();
}
tenancy()->endTenancy();
$this->loadMigrationsFrom([
'--path' => __DIR__ . '/Etc',
'--database' => 'central',
]);
config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
config(['tenancy.storage_drivers.db.custom_columns' => [
'foo',
]]);
tenancy()->create(['foo.localhost']);
tenancy()->init('foo.localhost');
tenant()->put('foo', '111');
$this->assertSame('111', tenant()->get('foo'));
tenant()->put(['foo' => 'bar', 'abc' => 'xyz']);
$this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc']));
$this->assertSame('bar', \DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo);
}
/** @test */
public function custom_columns_can_be_used_on_tenant_create()
{
if (config('tenancy.storage_driver') != 'db') {
$this->markTestSkipped();
}
tenancy()->endTenancy();
$this->loadMigrationsFrom([
'--path' => __DIR__ . '/Etc',
'--database' => 'central',
]);
config(['database.default' => 'sqlite']); // fix issue caused by loadMigrationsFrom
config(['tenancy.storage_drivers.db.custom_columns' => [
'foo',
]]);
tenancy()->create(['foo.localhost'], ['foo' => 'bar', 'abc' => 'xyz']);
tenancy()->init('foo.localhost');
$this->assertSame(['foo' => 'bar', 'abc' => 'xyz'], tenant()->get(['foo', 'abc']));
$this->assertSame('bar', \DB::connection('central')->table('tenants')->where('id', tenant('id'))->first()->foo);
}
}

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests\Traits;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\Tests\TestCase;
class TenantAwareCommandTest extends TestCase
{
public $autoCreateTenant = false;
public $autoInitTenancy = false;
/** @test */
public function commands_run_globally_are_tenant_aware_and_return_valid_exit_code()
{
$tenant1 = Tenant::new()->save();
$tenant2 = Tenant::new()->save();
\Artisan::call('tenants:migrate', [
'--tenants' => [$tenant1['id'], $tenant2['id']],
]);
$this->artisan('user:add')
->assertExitCode(0);
tenancy()->initializeTenancy($tenant1);
$this->assertNotEmpty(\DB::table('users')->get());
tenancy()->end();
tenancy()->initializeTenancy($tenant2);
$this->assertNotEmpty(\DB::table('users')->get());
tenancy()->end();
}
}

View file

@ -154,6 +154,12 @@ class BootstrapperTest extends TestCase
FilesystemTenancyBootstrapper::class FilesystemTenancyBootstrapper::class
]]); ]]);
$old_storage_path = storage_path();
$old_storage_facade_roots = [];
foreach (config('tenancy.filesystem.disks') as $disk) {
$old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root");
}
$tenant1 = Tenant::create(); $tenant1 = Tenant::create();
$tenant2 = Tenant::create(); $tenant2 = Tenant::create();
@ -176,6 +182,27 @@ class BootstrapperTest extends TestCase
tenancy()->initialize($tenant3); tenancy()->initialize($tenant3);
$this->assertFalse(Storage::disk('public')->exists('foo')); $this->assertFalse(Storage::disk('public')->exists('foo'));
$this->assertFalse(Storage::disk('public')->exists('abc')); $this->assertFalse(Storage::disk('public')->exists('abc'));
// Check suffixing logic
$new_storage_path = storage_path();
$this->assertEquals($old_storage_path . '/' . config('tenancy.filesystem.suffix_base') . tenant('id'), $new_storage_path);
foreach (config('tenancy.filesystem.disks') as $disk) {
$suffix = config('tenancy.filesystem.suffix_base') . tenant('id');
$current_path_prefix = Storage::disk($disk)->getAdapter()->getPathPrefix();
if ($override = config("tenancy.filesystem.root_override.{$disk}")) {
$correct_path_prefix = str_replace('%storage_path%', storage_path(), $override);
} else {
if ($base = $old_storage_facade_roots[$disk]) {
$correct_path_prefix = $base . "/$suffix/";
} else {
$correct_path_prefix = "$suffix/";
}
}
$this->assertSame($correct_path_prefix, $current_path_prefix);
}
} }
// for queues see QueueTest // for queues see QueueTest

View file

@ -2,25 +2,41 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests\v3;
use Stancl\Tenancy\Tenant; use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Tests\TestCase;
class CacheManagerTest extends TestCase class CacheManagerTest extends TestCase
{ {
public function setUp(): void
{
parent::setUp();
config(['tenancy.bootstrappers' => [
CacheTenancyBootstrapper::class,
]]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
}
/** @test */ /** @test */
public function default_tag_is_automatically_applied() public function default_tag_is_automatically_applied()
{ {
$this->createTenant(); tenancy()->initialize(Tenant::create());
$this->initTenancy();
$this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames()); $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames());
} }
/** @test */ /** @test */
public function tags_are_merged_when_array_is_passed() public function tags_are_merged_when_array_is_passed()
{ {
$this->createTenant(); tenancy()->initialize(Tenant::create());
$this->initTenancy();
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar']; $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
$this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames()); $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames());
} }
@ -28,8 +44,8 @@ class CacheManagerTest extends TestCase
/** @test */ /** @test */
public function tags_are_merged_when_string_is_passed() public function tags_are_merged_when_string_is_passed()
{ {
$this->createTenant(); tenancy()->initialize(Tenant::create());
$this->initTenancy();
$expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo']; $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo'];
$this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames()); $this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames());
} }
@ -37,8 +53,8 @@ class CacheManagerTest extends TestCase
/** @test */ /** @test */
public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method() public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method()
{ {
$this->createTenant(); tenancy()->initialize(Tenant::create());
$this->initTenancy();
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
cache()->tags(); cache()->tags();
} }
@ -46,8 +62,8 @@ class CacheManagerTest extends TestCase
/** @test */ /** @test */
public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method() public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method()
{ {
$this->createTenant(); tenancy()->initialize(Tenant::create());
$this->initTenancy();
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
cache()->tags(1, 2); cache()->tags(1, 2);
} }
@ -55,14 +71,14 @@ class CacheManagerTest extends TestCase
/** @test */ /** @test */
public function tags_separate_cache_well_enough() public function tags_separate_cache_well_enough()
{ {
Tenant::new()->withDomains(['foo.localhost'])->save(); $tenant1 = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
cache()->put('foo', 'bar', 1); cache()->put('foo', 'bar', 1);
$this->assertSame('bar', cache()->get('foo')); $this->assertSame('bar', cache()->get('foo'));
Tenant::new()->withDomains(['bar.localhost'])->save(); $tenant2 = Tenant::create();
tenancy()->init('bar.localhost'); tenancy()->initialize($tenant2);
$this->assertNotSame('bar', cache()->get('foo')); $this->assertNotSame('bar', cache()->get('foo'));
@ -73,14 +89,14 @@ class CacheManagerTest extends TestCase
/** @test */ /** @test */
public function invoking_the_cache_helper_works() public function invoking_the_cache_helper_works()
{ {
Tenant::new()->withDomains(['foo.localhost'])->save(); $tenant1 = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
cache(['foo' => 'bar'], 1); cache(['foo' => 'bar'], 1);
$this->assertSame('bar', cache('foo')); $this->assertSame('bar', cache('foo'));
Tenant::new()->withDomains(['bar.localhost'])->save(); $tenant2 = Tenant::create();
tenancy()->init('bar.localhost'); tenancy()->initialize($tenant2);
$this->assertNotSame('bar', cache('foo')); $this->assertNotSame('bar', cache('foo'));
@ -91,32 +107,32 @@ class CacheManagerTest extends TestCase
/** @test */ /** @test */
public function cache_is_persisted() public function cache_is_persisted()
{ {
Tenant::new()->withDomains(['foo.localhost'])->save(); $tenant1 = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
cache(['foo' => 'bar'], 10); cache(['foo' => 'bar'], 10);
$this->assertSame('bar', cache('foo')); $this->assertSame('bar', cache('foo'));
tenancy()->endTenancy(); tenancy()->end();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
$this->assertSame('bar', cache('foo')); $this->assertSame('bar', cache('foo'));
} }
/** @test */ /** @test */
public function cache_is_persisted_when_reidentification_is_used() public function cache_is_persisted_when_reidentification_is_used()
{ {
Tenant::new()->withDomains(['foo.localhost'])->save(); $tenant1 = Tenant::create();
Tenant::new()->withDomains(['bar.localhost'])->save(); $tenant2 = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
cache(['foo' => 'bar'], 10); cache(['foo' => 'bar'], 10);
$this->assertSame('bar', cache('foo')); $this->assertSame('bar', cache('foo'));
tenancy()->init('bar.localhost'); tenancy()->initialize($tenant2);
tenancy()->endTenancy(); tenancy()->end();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
$this->assertSame('bar', cache('foo')); $this->assertSame('bar', cache('foo'));
} }
} }

View file

@ -2,18 +2,41 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests\v3;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\Tests\Etc\ExampleSeeder; use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Events\Listeners\JobPipeline;
use Stancl\Tenancy\Events\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Tests\TestCase;
class CommandsTest extends TestCase class CommandsTest extends TestCase
{ {
public $autoCreateTenant = true; public function setUp(): void
public $autoInitTenancy = false; {
parent::setUp();
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
config(['tenancy.bootstrappers' => [
DatabaseTenancyBootstrapper::class
]]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
}
/** @test */ /** @test */
public function migrate_command_doesnt_change_the_db_connection() public function migrate_command_doesnt_change_the_db_connection()
@ -32,35 +55,42 @@ class CommandsTest extends TestCase
/** @test */ /** @test */
public function migrate_command_works_without_options() public function migrate_command_works_without_options()
{ {
$tenant = Tenant::create();
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
Artisan::call('tenants:migrate'); Artisan::call('tenants:migrate');
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
tenancy()->init('test.localhost');
tenancy()->initialize($tenant);
$this->assertTrue(Schema::hasTable('users')); $this->assertTrue(Schema::hasTable('users'));
} }
/** @test */ /** @test */
public function migrate_command_works_with_tenants_option() public function migrate_command_works_with_tenants_option()
{ {
$tenant = Tenant::new()->withDomains(['test2.localhost'])->save(); $tenant = Tenant::create();
Artisan::call('tenants:migrate', [ Artisan::call('tenants:migrate', [
'--tenants' => [$tenant['id']], '--tenants' => [$tenant['id']],
]); ]);
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
tenancy()->init('test.localhost'); tenancy()->initialize(Tenant::create());
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
tenancy()->init('test2.localhost'); tenancy()->initialize($tenant);
$this->assertTrue(Schema::hasTable('users')); $this->assertTrue(Schema::hasTable('users'));
} }
/** @test */ /** @test */
public function rollback_command_works() public function rollback_command_works()
{ {
$tenant = Tenant::create();
Artisan::call('tenants:migrate'); Artisan::call('tenants:migrate');
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
tenancy()->init('test.localhost');
tenancy()->initialize($tenant);
$this->assertTrue(Schema::hasTable('users')); $this->assertTrue(Schema::hasTable('users'));
Artisan::call('tenants:rollback'); Artisan::call('tenants:rollback');
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
@ -93,7 +123,7 @@ class CommandsTest extends TestCase
/** @test */ /** @test */
public function database_connection_is_switched_to_default_when_tenancy_has_been_initialized() public function database_connection_is_switched_to_default_when_tenancy_has_been_initialized()
{ {
tenancy()->init('test.localhost'); tenancy()->initialize(Tenant::create());
$this->database_connection_is_switched_to_default(); $this->database_connection_is_switched_to_default();
} }
@ -101,7 +131,7 @@ class CommandsTest extends TestCase
/** @test */ /** @test */
public function run_commands_works() public function run_commands_works()
{ {
$id = Tenant::new()->withDomains(['run.localhost'])->save()['id']; $id = Tenant::create()->id;
Artisan::call('tenants:migrate', ['--tenants' => [$id]]); Artisan::call('tenants:migrate', ['--tenants' => [$id]]);
@ -121,34 +151,25 @@ class CommandsTest extends TestCase
mkdir($dir, 0777, true); mkdir($dir, 0777, true);
} }
if (app()->version()[0] === '6') { $this->artisan('tenancy:install');
file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernelv6.stub'));
} else {
file_put_contents(app_path('Http/Kernel.php'), file_get_contents(__DIR__ . '/Etc/defaultHttpKernelv7.stub'));
}
$this->artisan('tenancy:install')
->expectsQuestion('Do you wish to publish the migrations that create these tables?', 'yes');
$this->assertFileExists(base_path('routes/tenant.php')); $this->assertFileExists(base_path('routes/tenant.php'));
$this->assertFileExists(base_path('config/tenancy.php')); $this->assertFileExists(base_path('config/tenancy.php'));
$this->assertFileExists(database_path('migrations/2019_09_15_000010_create_tenants_table.php')); $this->assertFileExists(database_path('migrations/2019_09_15_000010_create_tenants_table.php'));
$this->assertFileExists(database_path('migrations/2019_09_15_000020_create_domains_table.php')); $this->assertFileExists(database_path('migrations/2019_09_15_000020_create_domains_table.php'));
$this->assertDirectoryExists(database_path('migrations/tenant')); $this->assertDirectoryExists(database_path('migrations/tenant'));
if (app()->version()[0] === '6') {
$this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernelv6.stub'), file_get_contents(app_path('Http/Kernel.php')));
} else {
$this->assertSame(file_get_contents(__DIR__ . '/Etc/modifiedHttpKernelv7.stub'), file_get_contents(app_path('Http/Kernel.php')));
}
} }
/** @test */ /** @test */
public function migrate_fresh_command_works() public function migrate_fresh_command_works()
{ {
$tenant = Tenant::create();
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
Artisan::call('tenants:migrate-fresh'); Artisan::call('tenants:migrate-fresh');
$this->assertFalse(Schema::hasTable('users')); $this->assertFalse(Schema::hasTable('users'));
tenancy()->init('test.localhost');
tenancy()->initialize($tenant);
$this->assertTrue(Schema::hasTable('users')); $this->assertTrue(Schema::hasTable('users'));
$this->assertFalse(DB::table('users')->exists()); $this->assertFalse(DB::table('users')->exists());
@ -159,17 +180,4 @@ class CommandsTest extends TestCase
Artisan::call('tenants:migrate-fresh'); Artisan::call('tenants:migrate-fresh');
$this->assertFalse(DB::table('users')->exists()); $this->assertFalse(DB::table('users')->exists());
} }
/** @test */
public function create_command_works()
{
Artisan::call('tenants:create -d aaa.localhost -d bbb.localhost plan=free email=foo@test.local');
$tenant = tenancy()->all()[1]; // a tenant is autocreated prior to this
$data = $tenant->data;
unset($data['id']);
unset($data['_tenancy_db_name']);
$this->assertSame(['plan' => 'free', 'email' => 'foo@test.local'], $data);
$this->assertSame(['aaa.localhost', 'bbb.localhost'], $tenant->domains);
}
} }

View file

@ -2,15 +2,20 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests\v3;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Stancl\Tenancy\Contracts\ManagesDatabaseUsers; use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException; use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException;
use Stancl\Tenancy\Tenant;
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager; use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\Listeners\JobPipeline;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Tests\TestCase;
class DatabaseUsersTest extends TestCase class DatabaseUsersTest extends TestCase
{ {
@ -21,14 +26,18 @@ class DatabaseUsersTest extends TestCase
config([ config([
'tenancy.database_managers.mysql' => PermissionControlledMySQLDatabaseManager::class, 'tenancy.database_managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
'tenancy.database.suffix' => '', 'tenancy.database.suffix' => '',
'tenancy.database.template_connection' => 'mysql', 'tenancy.template_tenant_connection' => 'mysql',
]); ]);
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
} }
/** @test */ /** @test */
public function users_are_created_when_permission_controlled_mysql_manager_is_used() public function users_are_created_when_permission_controlled_mysql_manager_is_used()
{ {
$tenant = Tenant::new()->withData([ $tenant = new Tenant([
'id' => 'foo' . Str::random(10), 'id' => 'foo' . Str::random(10),
]); ]);
$tenant->database()->makeCredentials(); $tenant->database()->makeCredentials();
@ -46,9 +55,9 @@ class DatabaseUsersTest extends TestCase
public function a_tenants_database_cannot_be_created_when_the_user_already_exists() public function a_tenants_database_cannot_be_created_when_the_user_already_exists()
{ {
$username = 'foo' . Str::random(8); $username = 'foo' . Str::random(8);
$tenant = Tenant::new()->withData([ $tenant = Tenant::create([
'_tenancy_db_username' => $username, 'tenancy_db_username' => $username,
])->save(); ]);
/** @var ManagesDatabaseUsers $manager */ /** @var ManagesDatabaseUsers $manager */
$manager = $tenant->database()->manager(); $manager = $tenant->database()->manager();
@ -56,9 +65,9 @@ class DatabaseUsersTest extends TestCase
$this->assertTrue($manager->databaseExists($tenant->database()->getName())); $this->assertTrue($manager->databaseExists($tenant->database()->getName()));
$this->expectException(TenantDatabaseUserAlreadyExistsException::class); $this->expectException(TenantDatabaseUserAlreadyExistsException::class);
$tenant2 = Tenant::new()->withData([ $tenant2 = Tenant::create([
'_tenancy_db_username' => $username, 'tenancy_db_username' => $username,
])->save(); ]);
/** @var ManagesDatabaseUsers $manager */ /** @var ManagesDatabaseUsers $manager */
$manager = $tenant2->database()->manager(); $manager = $tenant2->database()->manager();
@ -73,9 +82,9 @@ class DatabaseUsersTest extends TestCase
'ALTER', 'ALTER ROUTINE', 'CREATE', 'ALTER', 'ALTER ROUTINE', 'CREATE',
]; ];
$tenant = Tenant::new()->withData([ $tenant = Tenant::create([
'_tenancy_db_username' => $user = 'user' . Str::random(8), 'tenancy_db_username' => $user = 'user' . Str::random(8),
])->save(); ]);
$query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`{$tenant->database()->connection()['host']}`")[1]; $query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`{$tenant->database()->connection()['host']}`")[1];
$this->assertStringStartsWith('GRANT CREATE, ALTER, ALTER ROUTINE ON', $query->{"Grants for {$user}@mysql"}); // @mysql because that's the hostname within the docker network $this->assertStringStartsWith('GRANT CREATE, ALTER, ALTER ROUTINE ON', $query->{"Grants for {$user}@mysql"}); // @mysql because that's the hostname within the docker network
@ -90,15 +99,15 @@ class DatabaseUsersTest extends TestCase
'tenancy.database.template_connection' => 'mysql', 'tenancy.database.template_connection' => 'mysql',
]); ]);
$tenant = Tenant::new()->withData([ $tenant = Tenant::create([
'id' => 'foo' . Str::random(10), 'id' => 'foo' . Str::random(10),
])->save(); ]);
$this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager); $this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager);
$tenant = Tenant::new()->withData([ $tenant = Tenant::create([
'id' => 'foo' . Str::random(10), 'id' => 'foo' . Str::random(10),
])->save(); ]);
tenancy()->initialize($tenant); // check if everything works tenancy()->initialize($tenant); // check if everything works
tenancy()->end(); tenancy()->end();

View file

@ -2,15 +2,28 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests\v3;
use GlobalCache; use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Facades\GlobalCache;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\TenancyBootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Tests\TestCase;
class GlobalCacheTest extends TestCase class GlobalCacheTest extends TestCase
{ {
public $autoCreateTenant = false; public function setUp(): void
public $autoInitTenancy = false; {
parent::setUp();
config(['tenancy.bootstrappers' => [
CacheTenancyBootstrapper::class,
]]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
}
/** @test */ /** @test */
public function global_cache_manager_stores_data_in_global_cache() public function global_cache_manager_stores_data_in_global_cache()
@ -19,28 +32,28 @@ class GlobalCacheTest extends TestCase
GlobalCache::put(['foo' => 'bar'], 1); GlobalCache::put(['foo' => 'bar'], 1);
$this->assertSame('bar', GlobalCache::get('foo')); $this->assertSame('bar', GlobalCache::get('foo'));
Tenant::new()->withDomains(['foo.localhost'])->save(); $tenant1 = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
$this->assertSame('bar', GlobalCache::get('foo')); $this->assertSame('bar', GlobalCache::get('foo'));
GlobalCache::put(['abc' => 'xyz'], 1); GlobalCache::put(['abc' => 'xyz'], 1);
cache(['def' => 'ghi'], 10); cache(['def' => 'ghi'], 10);
$this->assertSame('ghi', cache('def')); $this->assertSame('ghi', cache('def'));
tenancy()->endTenancy(); tenancy()->end();
$this->assertSame('xyz', GlobalCache::get('abc')); $this->assertSame('xyz', GlobalCache::get('abc'));
$this->assertSame('bar', GlobalCache::get('foo')); $this->assertSame('bar', GlobalCache::get('foo'));
$this->assertSame(null, cache('def')); $this->assertSame(null, cache('def'));
Tenant::new()->withDomains(['bar.localhost'])->save(); $tenant2 = Tenant::create();
tenancy()->init('bar.localhost'); tenancy()->initialize($tenant2);
$this->assertSame('xyz', GlobalCache::get('abc')); $this->assertSame('xyz', GlobalCache::get('abc'));
$this->assertSame('bar', GlobalCache::get('foo')); $this->assertSame('bar', GlobalCache::get('foo'));
$this->assertSame(null, cache('def')); $this->assertSame(null, cache('def'));
cache(['def' => 'xxx'], 1); cache(['def' => 'xxx'], 1);
$this->assertSame('xxx', cache('def')); $this->assertSame('xxx', cache('def'));
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant1);
$this->assertSame('ghi', cache('def')); $this->assertSame('ghi', cache('def'));
} }
} }

View file

@ -2,27 +2,28 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests\v3;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Tenant; use Stancl\Tenancy\Tests\TestCase;
class RequestDataIdentificationTest extends TestCase class RequestDataIdentificationTest extends TestCase
{ {
public $autoCreateTenant = false;
public $autoInitTenancy = false;
public function setUp(): void public function setUp(): void
{ {
parent::setUp(); parent::setUp();
config([ config([
'tenancy.exempt_domains' => [ 'tenancy.central_domains' => [
'localhost', 'localhost',
], ],
]); ]);
InitializeTenancyByRequestData::$header = 'X-Tenant';
InitializeTenancyByRequestData::$queryParameter = 'tenant';
Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () { Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () {
return 'Tenant id: ' . tenant('id'); return 'Tenant id: ' . tenant('id');
}); });
@ -31,11 +32,9 @@ class RequestDataIdentificationTest extends TestCase
/** @test */ /** @test */
public function header_identification_works() public function header_identification_works()
{ {
$this->app->bind(InitializeTenancyByRequestData::class, function () { InitializeTenancyByRequestData::$header = 'X-Tenant';
return new InitializeTenancyByRequestData('X-Tenant'); $tenant = Tenant::create();
}); $tenant2 = Tenant::create();
$tenant = Tenant::new()->save();
$tenant2 = Tenant::new()->save();
$this $this
->withoutExceptionHandling() ->withoutExceptionHandling()
@ -48,11 +47,11 @@ class RequestDataIdentificationTest extends TestCase
/** @test */ /** @test */
public function query_parameter_identification_works() public function query_parameter_identification_works()
{ {
$this->app->bind(InitializeTenancyByRequestData::class, function () { InitializeTenancyByRequestData::$header = null;
return new InitializeTenancyByRequestData(null, 'tenant'); InitializeTenancyByRequestData::$queryParameter = 'tenant';
});
$tenant = Tenant::new()->save(); $tenant = Tenant::create();
$tenant2 = Tenant::new()->save(); $tenant2 = Tenant::create();
$this $this
->withoutExceptionHandling() ->withoutExceptionHandling()

View file

@ -2,29 +2,58 @@
declare(strict_types=1); declare(strict_types=1);
namespace Stancl\Tenancy\Tests; namespace Stancl\Tenancy\Tests\v3;
use Stancl\Tenancy\Tenant; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Controllers\TenantAssetsController;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\TenancyBootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Tests\TestCase;
class TenantAssetTest extends TestCase class TenantAssetTest extends TestCase
{ {
public $autoCreateTenant = false; public function setUp(): void
public $autoInitTenancy = false; {
parent::setUp();
config(['tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class
]]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
}
public function tearDown(): void
{
parent::tearDown();
// Cleanup
TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class;
}
/** @test */ /** @test */
public function asset_can_be_accessed_using_the_url_returned_by_the_tenant_asset_helper() public function asset_can_be_accessed_using_the_url_returned_by_the_tenant_asset_helper()
{ {
Tenant::create('localhost'); TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
tenancy()->init('localhost');
$tenant = Tenant::create();
tenancy()->initialize($tenant);
$filename = 'testfile' . $this->randomString(10); $filename = 'testfile' . $this->randomString(10);
\Storage::disk('public')->put($filename, 'bar'); Storage::disk('public')->put($filename, 'bar');
$path = storage_path("app/public/$filename"); $path = storage_path("app/public/$filename");
// response()->file() returns BinaryFileResponse whose content is // response()->file() returns BinaryFileResponse whose content is
// inaccessible via getContent, so ->assertSee() can't be used // inaccessible via getContent, so ->assertSee() can't be used
$this->assertFileExists($path); $this->assertFileExists($path);
$response = $this->get(tenant_asset($filename)); $response = $this->get(tenant_asset($filename), [
'X-Tenant' => $tenant->id,
]);
$response->assertSuccessful(); $response->assertSuccessful();
@ -40,8 +69,8 @@ class TenantAssetTest extends TestCase
{ {
config(['app.asset_url' => null]); config(['app.asset_url' => null]);
Tenant::create('foo.localhost'); $tenant = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant);
$this->assertSame(route('stancl.tenancy.asset', ['path' => 'foo']), asset('foo')); $this->assertSame(route('stancl.tenancy.asset', ['path' => 'foo']), asset('foo'));
} }
@ -51,8 +80,8 @@ class TenantAssetTest extends TestCase
{ {
config(['app.asset_url' => 'https://an-s3-bucket']); config(['app.asset_url' => 'https://an-s3-bucket']);
$tenant = Tenant::create(['foo.localhost']); $tenant = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant);
$this->assertSame("https://an-s3-bucket/tenant{$tenant->id}/foo", asset('foo')); $this->assertSame("https://an-s3-bucket/tenant{$tenant->id}/foo", asset('foo'));
} }
@ -63,8 +92,8 @@ class TenantAssetTest extends TestCase
$original = global_asset('foobar'); $original = global_asset('foobar');
$this->assertSame(asset('foobar'), global_asset('foobar')); $this->assertSame(asset('foobar'), global_asset('foobar'));
Tenant::create(['foo.localhost']); $tenant = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant);
$this->assertSame($original, global_asset('foobar')); $this->assertSame($original, global_asset('foobar'));
} }
@ -79,8 +108,8 @@ class TenantAssetTest extends TestCase
'tenancy.filesystem.asset_helper_tenancy' => false, 'tenancy.filesystem.asset_helper_tenancy' => false,
]); ]);
Tenant::create('foo.localhost'); $tenant = Tenant::create();
tenancy()->init('foo.localhost'); tenancy()->initialize($tenant);
$this->assertSame($original, asset('foo')); $this->assertSame($original, asset('foo'));
} }

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests\v3;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\Tests\TestCase;
class TenantAwareCommandTest extends TestCase
{
/** @test */
public function commands_run_globally_are_tenant_aware_and_return_valid_exit_code()
{
$tenant1 = Tenant::create();
$tenant2 = Tenant::create();
Artisan::call('tenants:migrate', [
'--tenants' => [$tenant1['id'], $tenant2['id']],
]);
$this->artisan('user:add')
->assertExitCode(0);
tenancy()->initialize($tenant1);
$this->assertNotEmpty(DB::table('users')->get());
tenancy()->end();
tenancy()->initialize($tenant2);
$this->assertNotEmpty(DB::table('users')->get());
tenancy()->end();
}
}

View file

@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Tests\v3;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Database\Models\Tenant;
use Stancl\Tenancy\DatabaseManager;
use Stancl\Tenancy\Events\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Events\Listeners\JobPipeline;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\TenancyBootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager;
use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;
use Stancl\Tenancy\Tests\TestCase;
class TenantDatabaseManagerTest extends TestCase
{
/**
* @test
* @dataProvider database_manager_provider
*/
public function databases_can_be_created_and_deleted($driver, $databaseManager)
{
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
config()->set([
"tenancy.database_managers.$driver" => $databaseManager,
'tenancy.internal_prefix' => 'tenancy_',
]);
$name = 'db' . $this->randomString();
$this->assertFalse(app($databaseManager)->databaseExists($name));
$tenant = Tenant::create([
'tenancy_db_name' => $name,
'tenancy_db_connection' => $driver,
]);
$this->assertTrue(app($databaseManager)->databaseExists($name));
app($databaseManager)->deleteDatabase($tenant);
$this->assertFalse(app($databaseManager)->databaseExists($name));
}
/** @test */
public function dbs_can_be_created_when_another_driver_is_used_for_the_central_db()
{
$this->assertSame('central', config('database.default'));
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
// todo if the prefix is _tenancy_, this blows up. write a tenantmodel test that the prefix can be _tenancy_
config(['tenancy.internal_prefix' => 'tenancy_',]);
$database = 'db' . $this->randomString();
$this->assertFalse(app(MySQLDatabaseManager::class)->databaseExists($database));
Tenant::create([
'tenancy_db_name' => $database,
'tenancy_db_connection' => 'mysql',
]);
$this->assertTrue(app(MySQLDatabaseManager::class)->databaseExists($database));
$database = 'db' . $this->randomString();
$this->assertFalse(app(PostgreSQLDatabaseManager::class)->databaseExists($database));
Tenant::create([
'tenancy_db_name' => $database,
'tenancy_db_connection' => 'pgsql',
]);
$this->assertTrue(app(PostgreSQLDatabaseManager::class)->databaseExists($database));
}
public function database_manager_provider()
{
return [
['mysql', MySQLDatabaseManager::class],
['mysql', PermissionControlledMySQLDatabaseManager::class],
['sqlite', SQLiteDatabaseManager::class],
['pgsql', PostgreSQLDatabaseManager::class],
['pgsql', PostgreSQLSchemaManager::class],
];
}
/** @test */
public function db_name_is_prefixed_with_db_path_when_sqlite_is_used()
{
if (file_exists(database_path('foodb'))) {
unlink(database_path('foodb')); // cleanup
}
config([
'database.connections.fooconn.driver' => 'sqlite',
'tenancy.internal_prefix' => 'tenancy_',
]);
$tenant = Tenant::create([
'tenancy_db_name' => 'foodb',
'tenancy_db_connection' => 'fooconn',
]);
app(DatabaseManager::class)->createTenantConnection($tenant);
$this->assertSame(config('database.connections.tenant.database'), database_path('foodb'));
}
/** @test */
public function schema_manager_uses_schema_to_separate_tenant_dbs()
{
config([
'tenancy.database_managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class,
'tenancy.boostrappers' => [
DatabaseTenancyBootstrapper::class,
],
]);
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
$originalDatabaseName = config(['database.connections.pgsql.database']);
$tenant = Tenant::create([
'tenancy_db_connection' => 'pgsql',
]);
tenancy()->initialize($tenant);
$this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema'));
$this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database']));
}
}

View file

@ -12,6 +12,7 @@ use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Tests\TestCase; use Stancl\Tenancy\Tests\TestCase;
use Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator; use Stancl\Tenancy\UniqueIDGenerators\UUIDGenerator;
use Stancl\Tenancy\Contracts; use Stancl\Tenancy\Contracts;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
class TenantModelTest extends TestCase class TenantModelTest extends TestCase
{ {
@ -34,7 +35,7 @@ class TenantModelTest extends TestCase
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
$this->assertSame($tenant->id, app(Tenant::class)->id); $this->assertSame($tenant->id, app(Contracts\Tenant::class)->id);
tenancy()->end(); tenancy()->end();
@ -100,10 +101,10 @@ class TenantModelTest extends TestCase
$table->bigIncrements('id')->change(); $table->bigIncrements('id')->change();
}); });
config(['tenancy.id_generator' => null]); unset(app()[UniqueIdentifierGenerator::class]);
$tenant1 = Tenant::create(); $tenant1 = MyTenant::create();
$tenant2 = Tenant::create(); $tenant2 = MyTenant::create();
$this->assertSame(1, $tenant1->id); $this->assertSame(1, $tenant1->id);
$this->assertSame(2, $tenant2->id); $this->assertSame(2, $tenant2->id);
@ -137,6 +138,7 @@ class TenantModelTest extends TestCase
class MyTenant extends Tenant class MyTenant extends Tenant
{ {
protected $table = 'tenants'; protected $table = 'tenants';
public $increments = true;
} }
class AnotherTenant extends Model implements Contracts\Tenant class AnotherTenant extends Model implements Contracts\Tenant
@ -153,4 +155,9 @@ class AnotherTenant extends Model implements Contracts\Tenant
{ {
return $this->getAttribute('id'); return $this->getAttribute('id');
} }
public function run(callable $callback)
{
$callback();
}
} }