mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 23:14:03 +00:00
Merge branch 'master' into cache-prefix
This commit is contained in:
commit
f8f0e1e5da
32 changed files with 440 additions and 105 deletions
|
|
@ -1,6 +1,7 @@
|
|||
# add amd64 platform to support Mac M1
|
||||
FROM --platform=linux/amd64 shivammathur/node:latest-amd64
|
||||
|
||||
# todo update this to 8.2 once shivammathur/node supports that
|
||||
ARG PHP_VERSION=8.1
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
|
|
|||
8
INTERNAL.md
Normal file
8
INTERNAL.md
Normal 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`
|
||||
|
|
@ -103,6 +103,7 @@ return [
|
|||
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
||||
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
|
||||
// Stancl\Tenancy\Bootstrappers\PrefixCacheTenancyBootstrapper::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
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Schema\Blueprint;
|
|||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
class CreateTenantUserImpersonationTokensTable extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
@ -37,4 +37,4 @@ class CreateTenantUserImpersonationTokensTable extends Migration
|
|||
{
|
||||
Schema::dropIfExists('tenant_user_impersonation_tokens');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateTenantsTable extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
@ -34,4 +34,4 @@ class CreateTenantsTable extends Migration
|
|||
{
|
||||
Schema::dropIfExists('tenants');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Database\Schema\Blueprint;
|
|||
use Illuminate\Support\Facades\Schema;
|
||||
use Stancl\Tenancy\Tenancy;
|
||||
|
||||
class CreateDomainsTable extends Migration
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
|
|
@ -35,4 +35,4 @@ class CreateDomainsTable extends Migration
|
|||
{
|
||||
Schema::dropIfExists('domains');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
use Illuminate\Support\Facades\Route;
|
||||
use Stancl\Tenancy\Controllers\TenantAssetController;
|
||||
|
||||
// todo make this work with path identification
|
||||
Route::get('/tenancy/assets/{path?}', [TenantAssetController::class, 'asset'])
|
||||
->where('path', '(.*)')
|
||||
->name('stancl.tenancy.asset');
|
||||
|
|
|
|||
|
|
@ -58,16 +58,16 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"docker-up": "PHP_VERSION=8.1 docker-compose up -d",
|
||||
"docker-down": "PHP_VERSION=8.1 docker-compose down",
|
||||
"docker-rebuild": "PHP_VERSION=8.1 docker-compose up -d --no-deps --build",
|
||||
"docker-up": "PHP_VERSION=8.2 docker-compose up -d",
|
||||
"docker-down": "PHP_VERSION=8.2 docker-compose down",
|
||||
"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",
|
||||
"coverage": "open coverage/phpunit/html/index.html",
|
||||
"phpstan": "vendor/bin/phpstan",
|
||||
"phpstan-pro": "vendor/bin/phpstan --pro",
|
||||
"cs": "php-cs-fixer fix --config=.php-cs-fixer.php",
|
||||
"test": "PHP_VERSION=8.1 ./test --no-coverage",
|
||||
"test-full": "PHP_VERSION=8.1 ./test"
|
||||
"test": "PHP_VERSION=8.2 ./test --no-coverage",
|
||||
"test-full": "PHP_VERSION=8.2 ./test"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ services:
|
|||
build:
|
||||
context: .
|
||||
args:
|
||||
PHP_VERSION: ${PHP_VERSION:-8.1}
|
||||
PHP_VERSION: ${PHP_VERSION:-8.2}
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ parameters:
|
|||
paths:
|
||||
- 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:
|
||||
- src/helpers.php
|
||||
-
|
||||
|
|
@ -49,5 +49,9 @@ parameters:
|
|||
- src/Database/DatabaseConfig.php
|
||||
- '#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
|
||||
treatPhpDocTypesAsCertain: false
|
||||
|
|
|
|||
79
src/Bootstrappers/MailTenancyBootstrapper.php
Normal file
79
src/Bootstrappers/MailTenancyBootstrapper.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ use Illuminate\Database\Eloquent\Builder;
|
|||
class ClearPendingTenants extends Command
|
||||
{
|
||||
protected $signature = 'tenants:pending-clear
|
||||
{--all : Override the default settings and deletes all pending tenants}
|
||||
{--older-than-days= : Deletes all pending tenants older than the amount of days}
|
||||
{--older-than-hours= : Deletes all pending tenants older than the amount of hours}';
|
||||
|
||||
|
|
@ -18,23 +17,17 @@ class ClearPendingTenants extends Command
|
|||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Removing pending tenants.');
|
||||
$this->components->info('Removing pending tenants.');
|
||||
|
||||
$expirationDate = now();
|
||||
// We compare the original expiration date to the new one to check if the new one is different later
|
||||
$originalExpirationDate = $expirationDate->copy()->toImmutable();
|
||||
|
||||
// Skip the time constraints if the 'all' option is given
|
||||
if (! $this->option('all')) {
|
||||
/** @var ?int $olderThanDays */
|
||||
$olderThanDays = $this->option('older-than-days');
|
||||
|
||||
/** @var ?int $olderThanHours */
|
||||
$olderThanHours = $this->option('older-than-hours');
|
||||
$olderThanDays = (int) $this->option('older-than-days');
|
||||
$olderThanHours = (int) $this->option('older-than-hours');
|
||||
|
||||
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->line('Please, choose only one of these options.');
|
||||
$this->components->error("Cannot use '--older-than-days' and '--older-than-hours' together. Please, choose only one of these options.");
|
||||
|
||||
return 1; // Exit code for failure
|
||||
}
|
||||
|
|
@ -46,10 +39,8 @@ class ClearPendingTenants extends Command
|
|||
if ($olderThanHours) {
|
||||
$expirationDate->subHours($olderThanHours);
|
||||
}
|
||||
}
|
||||
|
||||
$deletedTenantCount = tenancy()
|
||||
->query()
|
||||
$deletedTenantCount = tenancy()->query()
|
||||
->onlyPending()
|
||||
->when($originalExpirationDate->notEqualTo($expirationDate), function (Builder $query) use ($expirationDate) {
|
||||
$query->where($query->getModel()->getColumnForQuery('pending_since'), '<', $expirationDate->timestamp);
|
||||
|
|
@ -59,7 +50,7 @@ class ClearPendingTenants extends Command
|
|||
->delete()
|
||||
->count();
|
||||
|
||||
$this->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.');
|
||||
$this->components->info($deletedTenantCount . ' pending ' . str('tenant')->plural($deletedTenantCount) . ' deleted.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class CreatePendingTenants extends Command
|
|||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Creating pending tenants.');
|
||||
$this->components->info('Creating pending tenants.');
|
||||
|
||||
$maxPendingTenantCount = (int) ($this->option('count') ?? config('tenancy.pending.count'));
|
||||
$pendingTenantCount = $this->getPendingTenantCount();
|
||||
|
|
@ -30,8 +30,8 @@ class CreatePendingTenants extends Command
|
|||
$createdCount++;
|
||||
}
|
||||
|
||||
$this->info($createdCount . ' ' . str('tenant')->plural($createdCount) . ' created.');
|
||||
$this->info($maxPendingTenantCount . ' ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
||||
$this->components->info($createdCount . ' pending ' . str('tenant')->plural($createdCount) . ' created.');
|
||||
$this->components->info($maxPendingTenantCount . ' pending ' . str('tenant')->plural($maxPendingTenantCount) . ' ready to be used.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -39,8 +39,7 @@ class CreatePendingTenants extends Command
|
|||
/** Calculate the number of currently available pending tenants. */
|
||||
protected function getPendingTenantCount(): int
|
||||
{
|
||||
return tenancy()
|
||||
->query()
|
||||
return tenancy()->query()
|
||||
->onlyPending()
|
||||
->count();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Link extends Command
|
|||
$this->createLinks($tenants);
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
$this->components->error($exception->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Commands;
|
||||
|
||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class MigrateFreshOverride extends FreshCommand
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
if (config('tenancy.database.drop_tenant_databases_on_migrate_fresh')) {
|
||||
tenancy()->model()::cursor()->each->delete();
|
||||
$tenantModel = tenancy()->model();
|
||||
|
||||
if (Schema::hasTable($tenantModel->getTable())) {
|
||||
$tenantModel::cursor()->each->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return parent::handle();
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class TenantDump extends DumpCommand
|
|||
public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int
|
||||
{
|
||||
if (is_null($this->option('path'))) {
|
||||
$this->input->setOption('path', database_path('schema/tenant-schema.dump'));
|
||||
$this->input->setOption('path', config('tenancy.migration_parameters.--schema-path') ?? database_path('schema/tenant-schema.dump'));
|
||||
}
|
||||
|
||||
$tenant = $this->option('tenant')
|
||||
|
|
@ -41,7 +41,7 @@ class TenantDump extends DumpCommand
|
|||
return 1;
|
||||
}
|
||||
|
||||
parent::handle($connections, $dispatcher);
|
||||
$tenant->run(fn () => parent::handle($connections, $dispatcher));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ trait HasTenantOptions
|
|||
|
||||
protected function getTenants(): LazyCollection
|
||||
{
|
||||
return tenancy()
|
||||
->query()
|
||||
return tenancy()->query()
|
||||
->when($this->option('tenants'), function ($query) {
|
||||
$query->whereIn(tenancy()->model()->getTenantKeyName(), $this->option('tenants'));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
*
|
||||
* @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.
|
||||
* @mixin \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -48,6 +48,23 @@ class UserImpersonation implements Feature
|
|||
|
||||
$token->delete();
|
||||
|
||||
session()->put('tenancy_impersonating', true);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Stancl\Tenancy\Listeners;
|
|||
|
||||
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Contracts\TenantEvent;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
|
||||
class CreateTenantConnection
|
||||
{
|
||||
|
|
@ -15,11 +15,12 @@ class CreateTenantConnection
|
|||
) {
|
||||
}
|
||||
|
||||
public function handle(TenantEvent $event): void
|
||||
public function handle(TenancyEvent $event): void
|
||||
{
|
||||
/** @var TenantWithDatabase */
|
||||
$tenant = $event->tenant;
|
||||
/** @var TenantWithDatabase $tenant */
|
||||
$tenant = $event->tenancy->tenant;
|
||||
|
||||
$this->database->purgeTenantConnection();
|
||||
$this->database->createTenantConnection($tenant);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/Listeners/UseCentralConnection.php
Normal file
21
src/Listeners/UseCentralConnection.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
|
||||
class UseCentralConnection
|
||||
{
|
||||
public function __construct(
|
||||
protected DatabaseManager $database,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(TenancyEvent $event): void
|
||||
{
|
||||
$this->database->reconnectToCentral();
|
||||
}
|
||||
}
|
||||
21
src/Listeners/UseTenantConnection.php
Normal file
21
src/Listeners/UseTenantConnection.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Stancl\Tenancy\Listeners;
|
||||
|
||||
use Stancl\Tenancy\Database\DatabaseManager;
|
||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
||||
|
||||
class UseTenantConnection
|
||||
{
|
||||
public function __construct(
|
||||
protected DatabaseManager $database,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(TenancyEvent $event): void
|
||||
{
|
||||
$this->database->setDefaultConnection('tenant');
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Stancl\Tenancy\Resolvers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Stancl\Tenancy\Contracts\Domain;
|
||||
use Stancl\Tenancy\Contracts\Tenant;
|
||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||
|
|
@ -39,14 +40,16 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
|||
|
||||
protected function setCurrentDomain(Tenant $tenant, string $domain): void
|
||||
{
|
||||
/** @var Tenant&Model $tenant */
|
||||
static::$currentDomain = $tenant->domains->where('domain', $domain)->first();
|
||||
}
|
||||
|
||||
public function getArgsForTenant(Tenant $tenant): array
|
||||
{
|
||||
/** @var Tenant&Model $tenant */
|
||||
$tenant->unsetRelation('domains');
|
||||
|
||||
return $tenant->domains->map(function (Domain $domain) {
|
||||
return $tenant->domains->map(function (Domain&Model $domain) {
|
||||
return [$domain->domain];
|
||||
})->toArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ class Tenancy
|
|||
*/
|
||||
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 */
|
||||
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Mail\MailManager;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
|
@ -24,6 +25,7 @@ use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
|
|||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
|
|
@ -331,20 +333,55 @@ test('local storage public urls are generated correctly', function() {
|
|||
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
|
||||
});
|
||||
|
||||
test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() {
|
||||
MailTenancyBootstrapper::$credentialsMap = [
|
||||
'mail.mailers.smtp.username' => 'smtp_username',
|
||||
'mail.mailers.smtp.password' => 'smtp_password'
|
||||
];
|
||||
|
||||
config([
|
||||
'mail.default' => 'smtp',
|
||||
'mail.mailers.smtp.username' => $defaultUsername = 'default username',
|
||||
'mail.mailers.smtp.password' => 'no password'
|
||||
]);
|
||||
|
||||
$tenant = Tenant::create(['smtp_password' => $password = 'testing password']);
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
expect(array_key_exists('smtp_password', tenant()->getAttributes()))->toBeTrue();
|
||||
expect(array_key_exists('smtp_host', tenant()->getAttributes()))->toBeFalse();
|
||||
expect(config('mail.mailers.smtp.username'))->toBe($defaultUsername);
|
||||
expect(config('mail.mailers.smtp.password'))->toBe(tenant()->smtp_password);
|
||||
|
||||
// Assert that the current mailer uses tenant's smtp_password
|
||||
assertMailerTransportUsesPassword($password);
|
||||
});
|
||||
|
||||
test('MailTenancyBootstrapper reverts the config and mailer credentials to default when tenancy ends', function() {
|
||||
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
||||
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'no password']);
|
||||
|
||||
tenancy()->initialize(Tenant::create(['smtp_password' => $tenantPassword = 'testing password']));
|
||||
|
||||
expect(config('mail.mailers.smtp.password'))->toBe($tenantPassword);
|
||||
|
||||
assertMailerTransportUsesPassword($tenantPassword);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
expect(config('mail.mailers.smtp.password'))->toBe($defaultPassword);
|
||||
|
||||
// Assert that the current mailer uses the default SMTP password
|
||||
assertMailerTransportUsesPassword($defaultPassword);
|
||||
});
|
||||
|
||||
function getDiskPrefix(string $disk): string
|
||||
{
|
||||
/** @var FilesystemAdapter $disk */
|
||||
$disk = Storage::disk($disk);
|
||||
$adapter = $disk->getAdapter();
|
||||
$prefix = invade(invade($adapter)->prefixer)->prefix;
|
||||
|
||||
$prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
|
||||
$prefixer->setAccessible(true);
|
||||
|
||||
// reflection -> instance
|
||||
$prefixer = $prefixer->getValue($adapter);
|
||||
|
||||
$prefix = (new ReflectionProperty($prefixer, 'prefix'));
|
||||
$prefix->setAccessible(true);
|
||||
|
||||
return $prefix->getValue($prefixer);
|
||||
return $prefix;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
|||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||
|
||||
beforeEach(function () {
|
||||
if (file_exists($schemaPath = database_path('schema/tenant-schema.dump'))) {
|
||||
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
|
||||
unlink($schemaPath);
|
||||
}
|
||||
|
||||
|
|
@ -111,28 +111,44 @@ test('migrate command loads schema state', function () {
|
|||
|
||||
test('dump command works', function () {
|
||||
$tenant = Tenant::create();
|
||||
$schemaPath = 'tests/Etc/tenant-schema-test.dump';
|
||||
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
expect($schemaPath)->not()->toBeFile();
|
||||
|
||||
Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"');
|
||||
expect('tests/Etc/tenant-schema-test.dump')->toBeFile();
|
||||
});
|
||||
|
||||
test('tenant dump file gets created as tenant-schema.dump in the database schema folder by default', function() {
|
||||
config(['tenancy.migration_parameters.--schema-path' => $schemaPath = database_path('schema/tenant-schema.dump')]);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
Artisan::call('tenants:dump');
|
||||
Artisan::call('tenants:dump ' . "--tenant='$tenant->id' --path='$schemaPath'");
|
||||
|
||||
expect($schemaPath)->toBeFile();
|
||||
});
|
||||
|
||||
test('migrate command uses the correct schema path by default', function () {
|
||||
test('dump command generates dump at the passed path', function() {
|
||||
$tenant = Tenant::create();
|
||||
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
expect($schemaPath = 'tests/Etc/tenant-schema-test.dump')->not()->toBeFile();
|
||||
|
||||
Artisan::call("tenants:dump --tenant='$tenant->id' --path='$schemaPath'");
|
||||
|
||||
expect($schemaPath)->toBeFile();
|
||||
});
|
||||
|
||||
test('dump command generates dump at the path specified in the tenancy migration parameters config', function() {
|
||||
config(['tenancy.migration_parameters.--schema-path' => $schemaPath = 'tests/Etc/tenant-schema-test.dump']);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
Artisan::call('tenants:migrate');
|
||||
|
||||
expect($schemaPath)->not()->toBeFile();
|
||||
|
||||
Artisan::call("tenants:dump --tenant='$tenant->id'");
|
||||
|
||||
expect($schemaPath)->toBeFile();
|
||||
});
|
||||
|
||||
test('migrate command correctly uses the schema dump located at the configured schema path by default', function () {
|
||||
config(['tenancy.migration_parameters.--schema-path' => 'tests/Etc/tenant-schema.dump']);
|
||||
$tenant = Tenant::create();
|
||||
|
||||
|
|
@ -146,6 +162,7 @@ test('migrate command uses the correct schema path by default', function () {
|
|||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
// schema_users is a table included in the tests/Etc/tenant-schema dump
|
||||
// Check for both tables to see if missing migrations also get executed
|
||||
expect(Schema::hasTable('schema_users'))->toBeTrue();
|
||||
expect(Schema::hasTable('users'))->toBeTrue();
|
||||
|
|
@ -355,7 +372,7 @@ function runCommandWorks(): void
|
|||
Artisan::call('tenants:migrate', ['--tenants' => [$id]]);
|
||||
|
||||
pest()->artisan("tenants:run --tenants=$id 'foo foo --b=bar --c=xyz' ")
|
||||
->expectsOutput("User's name is Test command")
|
||||
->expectsOutput("User's name is Test user")
|
||||
->expectsOutput('foo')
|
||||
->expectsOutput('xyz');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Stancl\Tenancy\Tests\Etc\Console;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ExampleCommand extends Command
|
||||
|
|
@ -22,14 +23,13 @@ class ExampleCommand extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
User::create([
|
||||
'id' => 999,
|
||||
'name' => 'Test command',
|
||||
'email' => 'test@command.com',
|
||||
$id = User::create([
|
||||
'name' => 'Test user',
|
||||
'email' => Str::random(8) . '@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
])->id;
|
||||
|
||||
$this->line("User's name is " . User::find(999)->name);
|
||||
$this->line("User's name is " . User::find($id)->name);
|
||||
$this->line($this->argument('a'));
|
||||
$this->line($this->option('c'));
|
||||
}
|
||||
|
|
|
|||
72
tests/MailTest.php
Normal file
72
tests/MailTest.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Mail\MailManager;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
|
||||
|
||||
beforeEach(function() {
|
||||
config(['mail.default' => 'smtp']);
|
||||
|
||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||
});
|
||||
|
||||
// Initialize tenancy as $tenant and assert that the smtp mailer's transport has the correct password
|
||||
function assertMailerTransportUsesPassword(string|null $password) {
|
||||
$manager = app(MailManager::class);
|
||||
$mailer = invade($manager)->get('smtp');
|
||||
$mailerPassword = invade($mailer->getSymfonyTransport())->password;
|
||||
|
||||
expect($mailerPassword)->toBe((string) $password);
|
||||
};
|
||||
|
||||
test('mailer transport uses the correct credentials', function() {
|
||||
config(['mail.default' => 'smtp', 'mail.mailers.smtp.password' => $defaultPassword = 'DEFAULT']);
|
||||
MailTenancyBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
||||
|
||||
tenancy()->initialize($tenant = Tenant::create());
|
||||
assertMailerTransportUsesPassword($defaultPassword); // $tenant->smtp_password is not set, so the default password should be used
|
||||
tenancy()->end();
|
||||
|
||||
// Assert mailer uses the updated password
|
||||
$tenant->update(['smtp_password' => $newPassword = 'changed']);
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
assertMailerTransportUsesPassword($newPassword);
|
||||
tenancy()->end();
|
||||
|
||||
// Assert mailer uses the correct password after switching to a different tenant
|
||||
tenancy()->initialize(Tenant::create(['smtp_password' => $newTenantPassword = 'updated']));
|
||||
assertMailerTransportUsesPassword($newTenantPassword);
|
||||
tenancy()->end();
|
||||
|
||||
// Assert mailer uses the default password after tenancy ends
|
||||
assertMailerTransportUsesPassword($defaultPassword);
|
||||
});
|
||||
|
||||
|
||||
test('initializing and ending tenancy binds a fresh MailManager instance without cached mailers', function() {
|
||||
$mailers = fn() => invade(app(MailManager::class))->mailers;
|
||||
|
||||
app(MailManager::class)->mailer('smtp');
|
||||
|
||||
expect($mailers())->toHaveCount(1);
|
||||
|
||||
tenancy()->initialize(Tenant::create());
|
||||
|
||||
expect($mailers())->toHaveCount(0);
|
||||
|
||||
app(MailManager::class)->mailer('smtp');
|
||||
|
||||
expect($mailers())->toHaveCount(1);
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
expect($mailers())->toHaveCount(0);
|
||||
});
|
||||
45
tests/ManualModeTest.php
Normal file
45
tests/ManualModeTest.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Stancl\JobPipeline\JobPipeline;
|
||||
use Stancl\Tenancy\Events\TenancyEnded;
|
||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||
use Stancl\Tenancy\Events\TenantCreated;
|
||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||
use Stancl\Tenancy\Listeners\CreateTenantConnection;
|
||||
use Stancl\Tenancy\Listeners\UseCentralConnection;
|
||||
use Stancl\Tenancy\Listeners\UseTenantConnection;
|
||||
use \Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
|
||||
test('manual tenancy initialization works', function () {
|
||||
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||
return $event->tenant;
|
||||
})->toListener());
|
||||
|
||||
Event::listen(TenancyInitialized::class, CreateTenantConnection::class);
|
||||
Event::listen(TenancyInitialized::class, UseTenantConnection::class);
|
||||
Event::listen(TenancyEnded::class, UseCentralConnection::class);
|
||||
|
||||
$tenant = Tenant::create();
|
||||
|
||||
expect(app('db')->getDefaultConnection())->toBe('central');
|
||||
expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant_host_connection']);
|
||||
pest()->assertArrayNotHasKey('tenant', config('database.connections'));
|
||||
|
||||
tenancy()->initialize($tenant);
|
||||
|
||||
// Trigger creation of the tenant connection
|
||||
createUsersTable();
|
||||
|
||||
expect(app('db')->getDefaultConnection())->toBe('tenant');
|
||||
expect(array_keys(app('db')->getConnections()))->toBe(['central', 'tenant']);
|
||||
pest()->assertArrayHasKey('tenant', config('database.connections'));
|
||||
|
||||
tenancy()->end();
|
||||
|
||||
expect(array_keys(app('db')->getConnections()))->toBe(['central']);
|
||||
expect(config('database.connections.tenant'))->toBeNull();
|
||||
expect(app('db')->getDefaultConnection())->toBe(config('tenancy.database.central_connection'));
|
||||
});
|
||||
|
|
@ -67,23 +67,6 @@ test('CreatePendingTenants command cannot run with both time constraints', funct
|
|||
->assertFailed();
|
||||
});
|
||||
|
||||
test('CreatePendingTenants commands all option overrides any config constraints', function () {
|
||||
Tenant::createPending();
|
||||
Tenant::createPending();
|
||||
|
||||
tenancy()->model()->query()->onlyPending()->first()->update([
|
||||
'pending_since' => now()->subDays(10)
|
||||
]);
|
||||
|
||||
config(['tenancy.pending.older_than_days' => 4]);
|
||||
|
||||
Artisan::call(ClearPendingTenants::class, [
|
||||
'--all' => true
|
||||
]);
|
||||
|
||||
expect(Tenant::onlyPending()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('tenancy can check if there are any pending tenants', function () {
|
||||
expect(Tenant::onlyPending()->exists())->toBeFalse();
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,19 @@ test('tenant user can be impersonated on a tenant domain', function () {
|
|||
pest()->get('http://foo.localhost/dashboard')
|
||||
->assertSuccessful()
|
||||
->assertSee('You are logged in as Joe');
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeTrue();
|
||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||
|
||||
// Leave impersonation
|
||||
UserImpersonation::leave();
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
expect(session('tenancy_impersonating'))->toBeNull();
|
||||
|
||||
// Assert can't access the tenant dashboard
|
||||
pest()->get('http://foo.localhost/dashboard')
|
||||
->assertRedirect('http://foo.localhost/login');
|
||||
});
|
||||
|
||||
test('tenant user can be impersonated on a tenant path', function () {
|
||||
|
|
@ -116,6 +129,19 @@ test('tenant user can be impersonated on a tenant path', function () {
|
|||
pest()->get('/acme/dashboard')
|
||||
->assertSuccessful()
|
||||
->assertSee('You are logged in as Joe');
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeTrue();
|
||||
expect(session('tenancy_impersonating'))->toBeTrue();
|
||||
|
||||
// Leave impersonation
|
||||
UserImpersonation::leave();
|
||||
|
||||
expect(UserImpersonation::isImpersonating())->toBeFalse();
|
||||
expect(session('tenancy_impersonating'))->toBeNull();
|
||||
|
||||
// Assert can't access the tenant dashboard
|
||||
pest()->get('/acme/dashboard')
|
||||
->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('tokens have a limited ttl', function () {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use Stancl\Tenancy\Facades\GlobalCache;
|
|||
use Stancl\Tenancy\Facades\Tenancy;
|
||||
use Stancl\Tenancy\TenancyServiceProvider;
|
||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||
use Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper;
|
||||
|
||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||
{
|
||||
|
|
@ -104,6 +105,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
'--force' => true,
|
||||
],
|
||||
'tenancy.bootstrappers.redis' => RedisTenancyBootstrapper::class, // todo1 change this to []? two tests in TenantDatabaseManagerTest are failing with that
|
||||
'tenancy.bootstrappers.mail' => MailTenancyBootstrapper::class,
|
||||
'queue.connections.central' => [
|
||||
'driver' => 'sync',
|
||||
'central' => true,
|
||||
|
|
@ -114,6 +116,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
|||
|
||||
$app->singleton(RedisTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
|
||||
$app->singleton(PrefixCacheTenancyBootstrapper::class); // todo (Samuel) use proper approach eg config for singleton registration
|
||||
$app->singleton(MailTenancyBootstrapper::class);
|
||||
}
|
||||
|
||||
protected function getPackageProviders($app)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue