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

Merge branch 'master' of github.com:archtechx/tenancy

This commit is contained in:
Samuel Štancl 2023-01-04 03:27:04 +01:00
commit a0256fd5f3
20 changed files with 285 additions and 34 deletions

View file

@ -1,6 +1,7 @@
# add amd64 platform to support Mac M1 # add amd64 platform to support Mac M1
FROM --platform=linux/amd64 shivammathur/node:latest-amd64 FROM --platform=linux/amd64 shivammathur/node:latest-amd64
# todo update this to 8.2 once shivammathur/node supports that
ARG PHP_VERSION=8.1 ARG PHP_VERSION=8.1
WORKDIR /var/www/html WORKDIR /var/www/html

8
INTERNAL.md Normal file
View file

@ -0,0 +1,8 @@
# Internal development notes
## Updating the docker image used by the GH action
1. Login in to Docker Hub: `docker login -u archtechx -p`
2. Build the image (probably shut down docker-compose containers first): `docker-compose build --no-cache`
3. Tag a new image: `docker tag tenancy_test archtechx/tenancy:latest`
4. Push the image: `docker push archtechx/tenancy:latest`

View file

@ -103,6 +103,7 @@ return [
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
// Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper::class, // Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper::class,
// Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper::class, // Queueing mail requires using QueueTenancyBootstrapper with $forceRefresh set to true
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
], ],

View file

@ -58,16 +58,16 @@
} }
}, },
"scripts": { "scripts": {
"docker-up": "PHP_VERSION=8.1 docker-compose up -d", "docker-up": "PHP_VERSION=8.2 docker-compose up -d",
"docker-down": "PHP_VERSION=8.1 docker-compose down", "docker-down": "PHP_VERSION=8.2 docker-compose down",
"docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build", "docker-rebuild": "PHP_VERSION=8.2 docker-compose up -d --no-deps --build",
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml", "docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
"coverage": "open coverage/phpunit/html/index.html", "coverage": "open coverage/phpunit/html/index.html",
"phpstan": "vendor/bin/phpstan", "phpstan": "vendor/bin/phpstan",
"phpstan-pro": "vendor/bin/phpstan --pro", "phpstan-pro": "vendor/bin/phpstan --pro",
"cs": "php-cs-fixer fix --config=.php-cs-fixer.php", "cs": "php-cs-fixer fix --config=.php-cs-fixer.php",
"test": "PHP_VERSION=8.1 ./test --no-coverage", "test": "PHP_VERSION=8.2 ./test --no-coverage",
"test-full": "PHP_VERSION=8.1 ./test" "test-full": "PHP_VERSION=8.2 ./test"
}, },
"minimum-stability": "dev", "minimum-stability": "dev",
"prefer-stable": true, "prefer-stable": true,

View file

@ -4,7 +4,7 @@ services:
build: build:
context: . context: .
args: args:
PHP_VERSION: ${PHP_VERSION:-8.1} PHP_VERSION: ${PHP_VERSION:-8.2}
depends_on: depends_on:
mysql: mysql:
condition: service_healthy condition: service_healthy

View file

@ -28,7 +28,7 @@ parameters:
paths: paths:
- src/Features/TelescopeTags.php - src/Features/TelescopeTags.php
- -
message: '#Parameter \#1 \$key of method Illuminate\\Contracts\\Cache\\Repository::put\(\) expects string#' message: '#Parameter \#1 \$key of method Illuminate\\Cache\\Repository::put\(\) expects#'
paths: paths:
- src/helpers.php - src/helpers.php
- -
@ -49,5 +49,9 @@ parameters:
- src/Database/DatabaseConfig.php - src/Database/DatabaseConfig.php
- '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#' - '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#'
# php 8.2
# - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#'
# - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#'
checkMissingIterableValueType: false checkMissingIterableValueType: false
treatPhpDocTypesAsCertain: false treatPhpDocTypesAsCertain: false

View file

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Config\Repository;
use Illuminate\Foundation\Application;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
class MailTenancyBootstrapper implements TenancyBootstrapper
{
/**
* Tenant properties to be mapped to config (similarly to the TenantConfig feature).
*
* For example:
* [
* 'config.key.name' => 'tenant_property',
* ]
*/
public static array $credentialsMap = [];
public static string|null $mailer = null;
protected array $originalConfig = [];
public static array $mapPresets = [
'smtp' => [
'mail.mailers.smtp.host' => 'smtp_host',
'mail.mailers.smtp.port' => 'smtp_port',
'mail.mailers.smtp.username' => 'smtp_username',
'mail.mailers.smtp.password' => 'smtp_password',
],
];
public function __construct(
protected Repository $config,
protected Application $app
) {
static::$mailer ??= $config->get('mail.default');
static::$credentialsMap = array_merge(static::$credentialsMap, static::$mapPresets[static::$mailer] ?? []);
}
public function bootstrap(Tenant $tenant): void
{
// Forget the mail manager instance to clear the cached mailers
$this->app->forgetInstance('mail.manager');
$this->setConfig($tenant);
}
public function revert(): void
{
$this->unsetConfig();
$this->app->forgetInstance('mail.manager');
}
protected function setConfig(Tenant $tenant): void
{
foreach (static::$credentialsMap as $configKey => $storageKey) {
$override = $tenant->$storageKey;
if (array_key_exists($storageKey, $tenant->getAttributes())) {
$this->originalConfig[$configKey] ??= $this->config->get($configKey);
$this->config->set($configKey, $override);
}
}
}
protected function unsetConfig(): void
{
foreach ($this->originalConfig as $key => $value) {
$this->config->set($key, $value);
}
}
}

View file

@ -17,7 +17,7 @@ class ClearPendingTenants extends Command
public function handle(): int public function handle(): int
{ {
$this->info('Removing pending tenants.'); $this->components->info('Removing pending tenants.');
$expirationDate = now(); $expirationDate = now();
// We compare the original expiration date to the new one to check if the new one is different later // We compare the original expiration date to the new one to check if the new one is different later
@ -27,8 +27,7 @@ class ClearPendingTenants extends Command
$olderThanHours = (int) $this->option('older-than-hours'); $olderThanHours = (int) $this->option('older-than-hours');
if ($olderThanDays && $olderThanHours) { if ($olderThanDays && $olderThanHours) {
$this->line("<options=bold,reverse;fg=red> Cannot use '--older-than-days' and '--older-than-hours' together \n"); // todo@cli refactor all of these styled command outputs to use $this->components $this->components->error("Cannot use '--older-than-days' and '--older-than-hours' together. Please, choose only one of these options.");
$this->line('Please, choose only one of these options.');
return 1; // Exit code for failure return 1; // Exit code for failure
} }
@ -51,7 +50,7 @@ class ClearPendingTenants extends Command
->delete() ->delete()
->count(); ->count();
$this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.'); $this->components->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.');
return 0; return 0;
} }

View file

@ -14,7 +14,7 @@ class CreatePendingTenants extends Command
public function handle(): int public function handle(): int
{ {
$this->info('Creating pending tenants.'); $this->components->info('Creating pending tenants.');
$maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count')); $maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count'));
$pendingTenantCount = $this->getPendingTenantCount(); $pendingTenantCount = $this->getPendingTenantCount();
@ -30,8 +30,8 @@ class CreatePendingTenants extends Command
$createdCount++; $createdCount++;
} }
$this->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.'); $this->components->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.');
$this->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.'); $this->components->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
return 0; return 0;
} }

View file

@ -34,7 +34,7 @@ class Link extends Command
$this->createLinks($tenants); $this->createLinks($tenants);
} }
} catch (Exception $exception) { } catch (Exception $exception) {
$this->error($exception->getMessage()); $this->components->error($exception->getMessage());
return 1; return 1;
} }

View file

@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* *
* @see \Stancl\Tenancy\Database\Models\Domain * @see \Stancl\Tenancy\Database\Models\Domain
* *
* @method __call(string $method, array $parameters) IDE support. This will be a model. * @method __call(string $method, array $parameters) IDE support. This will be a model. // todo check if we can remove these now
* @method static __callStatic(string $method, array $parameters) IDE support. This will be a model. * @method static __callStatic(string $method, array $parameters) IDE support. This will be a model.
* @mixin \Illuminate\Database\Eloquent\Model * @mixin \Illuminate\Database\Eloquent\Model
*/ */

View file

@ -48,6 +48,23 @@ class UserImpersonation implements Feature
$token->delete(); $token->delete();
session()->put('tenancy_impersonating', true);
return redirect($token->redirect_url); return redirect($token->redirect_url);
} }
public static function isImpersonating(): bool
{
return session()->has('tenancy_impersonating');
}
/**
* Logout from the current domain and forget impersonation session.
*/
public static function leave(): void // todo possibly rename
{
auth()->logout();
session()->forget('tenancy_impersonating');
}
} }

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Resolvers; namespace Stancl\Tenancy\Resolvers;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Contracts\Domain; use Stancl\Tenancy\Contracts\Domain;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
@ -39,14 +40,16 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
protected function setCurrentDomain(Tenant $tenant, string $domain): void protected function setCurrentDomain(Tenant $tenant, string $domain): void
{ {
/** @var Tenant&Model $tenant */
static::$currentDomain = $tenant->domains->where('domain', $domain)->first(); static::$currentDomain = $tenant->domains->where('domain', $domain)->first();
} }
public function getArgsForTenant(Tenant $tenant): array public function getArgsForTenant(Tenant $tenant): array
{ {
/** @var Tenant&Model $tenant */
$tenant->unsetRelation('domains'); $tenant->unsetRelation('domains');
return $tenant->domains->map(function (Domain $domain) { return $tenant->domains->map(function (Domain&Model $domain) {
return [$domain->domain]; return [$domain->domain];
})->toArray(); })->toArray();
} }

View file

@ -118,6 +118,7 @@ class Tenancy
*/ */
public static function find(int|string $id): Tenant|null public static function find(int|string $id): Tenant|null
{ {
// todo update all syntax like this once we're fully on PHP 8.2
/** @var (Tenant&Model)|null */ /** @var (Tenant&Model)|null */
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first(); $tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();

View file

@ -3,6 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Mail\MailManager;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Stancl\JobPipeline\JobPipeline; use Stancl\JobPipeline\JobPipeline;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
@ -23,6 +24,7 @@ use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\DeleteTenantStorage; use Stancl\Tenancy\Listeners\DeleteTenantStorage;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
@ -326,20 +328,55 @@ test('local storage public urls are generated correctly', function() {
expect(File::isDirectory($tenantStoragePath))->toBeFalse(); expect(File::isDirectory($tenantStoragePath))->toBeFalse();
}); });
test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() {
MailTenancyBootstrapper::$credentialsMap = [
'mail.mailers.smtp.username' => 'smtp_username',
'mail.mailers.smtp.password' => 'smtp_password'
];
config([
'mail.default' => 'smtp',
'mail.mailers.smtp.username' => $defaultUsername = 'default username',
'mail.mailers.smtp.password' => 'no password'
]);
$tenant = Tenant::create(['smtp_password' => $password = 'testing password']);
tenancy()->initialize($tenant);
expect(array_key_exists('smtp_password', tenant()->getAttributes()))->toBeTrue();
expect(array_key_exists('smtp_host', tenant()->getAttributes()))->toBeFalse();
expect(config('mail.mailers.smtp.username'))->toBe($defaultUsername);
expect(config('mail.mailers.smtp.password'))->toBe(tenant()->smtp_password);
// Assert that the current mailer uses tenant's smtp_password
assertMailerTransportUsesPassword($password);
});
test('MailTenancyBootstrapper reverts the config and mailer credentials to default when tenancy ends', function() {
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'no password']);
tenancy()->initialize(Tenant::create(['smtp_password' => $tenantPassword = 'testing password']));
expect(config('mail.mailers.smtp.password'))->toBe($tenantPassword);
assertMailerTransportUsesPassword($tenantPassword);
tenancy()->end();
expect(config('mail.mailers.smtp.password'))->toBe($defaultPassword);
// Assert that the current mailer uses the default SMTP password
assertMailerTransportUsesPassword($defaultPassword);
});
function getDiskPrefix(string $disk): string function getDiskPrefix(string $disk): string
{ {
/** @var FilesystemAdapter $disk */ /** @var FilesystemAdapter $disk */
$disk = Storage::disk($disk); $disk = Storage::disk($disk);
$adapter = $disk->getAdapter(); $adapter = $disk->getAdapter();
$prefix = invade(invade($adapter)->prefixer)->prefix;
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer'); return $prefix;
$prefixer->setAccessible(true);
// reflection -> instance
$prefixer = $prefixer->getValue($adapter);
$prefix = (new ReflectionProperty($prefixer, 'prefix'));
$prefix->setAccessible(true);
return $prefix->getValue($prefixer);
} }

View file

@ -372,7 +372,7 @@ function runCommandWorks(): void
Artisan::call('tenants:migrate', ['--tenants' => [$id]]); Artisan::call('tenants:migrate', ['--tenants' => [$id]]);
pest()->artisan("tenants:run --tenants=$id 'foo foo --b=bar --c=xyz' ") pest()->artisan("tenants:run --tenants=$id 'foo foo --b=bar --c=xyz' ")
->expectsOutput("User's name is Test command") ->expectsOutput("User's name is Test user")
->expectsOutput('foo') ->expectsOutput('foo')
->expectsOutput('xyz'); ->expectsOutput('xyz');
} }

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests\Etc\Console; namespace Stancl\Tenancy\Tests\Etc\Console;
use Illuminate\Support\Str;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class ExampleCommand extends Command class ExampleCommand extends Command
@ -22,14 +23,13 @@ class ExampleCommand extends Command
*/ */
public function handle() public function handle()
{ {
User::create([ $id = User::create([
'id' => 999, 'name' => 'Test user',
'name' => 'Test command', 'email' => Str::random(8) . '@example.com',
'email' => 'test@command.com',
'password' => bcrypt('password'), 'password' => bcrypt('password'),
]); ])->id;
$this->line("User's name is " . User::find(999)->name); $this->line("User's name is " . User::find($id)->name);
$this->line($this->argument('a')); $this->line($this->argument('a'));
$this->line($this->option('c')); $this->line($this->option('c'));
} }

72
tests/MailTest.php Normal file
View file

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
use Illuminate\Mail\MailManager;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
beforeEach(function() {
config(['mail.default' => 'smtp']);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
});
// Initialize tenancy as $tenant and assert that the smtp mailer's transport has the correct password
function assertMailerTransportUsesPassword(string|null $password) {
$manager = app(MailManager::class);
$mailer = invade($manager)->get('smtp');
$mailerPassword = invade($mailer->getSymfonyTransport())->password;
expect($mailerPassword)->toBe((string) $password);
};
test('mailer transport uses the correct credentials', function() {
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'DEFAULT']);
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
tenancy()->initialize($tenant = Tenant::create());
assertMailerTransportUsesPassword($defaultPassword); // $tenant->smtp_password is not set, so the default password should be used
tenancy()->end();
// Assert mailer uses the updated password
$tenant->update(['smtp_password' => $newPassword = 'changed']);
tenancy()->initialize($tenant);
assertMailerTransportUsesPassword($newPassword);
tenancy()->end();
// Assert mailer uses the correct password after switching to a different tenant
tenancy()->initialize(Tenant::create(['smtp_password' => $newTenantPassword = 'updated']));
assertMailerTransportUsesPassword($newTenantPassword);
tenancy()->end();
// Assert mailer uses the default password after tenancy ends
assertMailerTransportUsesPassword($defaultPassword);
});
test('initializing and ending tenancy binds a fresh MailManager instance without cached mailers', function() {
$mailers = fn() => invade(app(MailManager::class))->mailers;
app(MailManager::class)->mailer('smtp');
expect($mailers())->toHaveCount(1);
tenancy()->initialize(Tenant::create());
expect($mailers())->toHaveCount(0);
app(MailManager::class)->mailer('smtp');
expect($mailers())->toHaveCount(1);
tenancy()->end();
expect($mailers())->toHaveCount(0);
});

View file

@ -83,6 +83,19 @@ test('tenant user can be impersonated on a tenant domain', function () {
pest()->get('http://foo.localhost/dashboard') pest()->get('http://foo.localhost/dashboard')
->assertSuccessful() ->assertSuccessful()
->assertSee('You are logged in as Joe'); ->assertSee('You are logged in as Joe');
expect(UserImpersonation::isImpersonating())->toBeTrue();
expect(session('tenancy_impersonating'))->toBeTrue();
// Leave impersonation
UserImpersonation::leave();
expect(UserImpersonation::isImpersonating())->toBeFalse();
expect(session('tenancy_impersonating'))->toBeNull();
// Assert can't access the tenant dashboard
pest()->get('http://foo.localhost/dashboard')
->assertRedirect('http://foo.localhost/login');
}); });
test('tenant user can be impersonated on a tenant path', function () { test('tenant user can be impersonated on a tenant path', function () {
@ -116,6 +129,19 @@ test('tenant user can be impersonated on a tenant path', function () {
pest()->get('/acme/dashboard') pest()->get('/acme/dashboard')
->assertSuccessful() ->assertSuccessful()
->assertSee('You are logged in as Joe'); ->assertSee('You are logged in as Joe');
expect(UserImpersonation::isImpersonating())->toBeTrue();
expect(session('tenancy_impersonating'))->toBeTrue();
// Leave impersonation
UserImpersonation::leave();
expect(UserImpersonation::isImpersonating())->toBeFalse();
expect(session('tenancy_impersonating'))->toBeNull();
// Assert can't access the tenant dashboard
pest()->get('/acme/dashboard')
->assertRedirect('/login');
}); });
test('tokens have a limited ttl', function () { test('tokens have a limited ttl', function () {

View file

@ -14,6 +14,7 @@ use Stancl\Tenancy\Facades\GlobalCache;
use Stancl\Tenancy\Facades\Tenancy; use Stancl\Tenancy\Facades\Tenancy;
use Stancl\Tenancy\TenancyServiceProvider; use Stancl\Tenancy\TenancyServiceProvider;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
abstract class TestCase extends \Orchestra\Testbench\TestCase abstract class TestCase extends \Orchestra\Testbench\TestCase
{ {
@ -104,6 +105,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'--force' => true, '--force' => true,
], ],
'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that 'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that
'tenancy.bootstrappers.mail' => MailTenancyBootstrapper::class,
'queue.connections.central' => [ 'queue.connections.central' => [
'driver' => 'sync', 'driver' => 'sync',
'central' => true, 'central' => true,
@ -113,6 +115,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
]); ]);
$app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration $app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
$app->singleton(MailTenancyBootstrapper::class);
} }
protected function getPackageProviders($app) protected function getPackageProviders($app)