1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-04 23:14:05 +00:00

Merge remote-tracking branch 'origin/august' into tenant-param-before-route-prefix

This commit is contained in:
lukinovec 2025-09-03 15:35:54 +02:00
commit d9ee7cac44
37 changed files with 344 additions and 211 deletions

View file

@ -18,8 +18,6 @@ beforeEach(function () {
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
});
// todo@move move these to be in the same file as the other tests from this PR (#909) rather than generic "action tests"
test('create storage symlinks action works', function() {
config([
'tenancy.bootstrappers' => [

View file

@ -115,8 +115,6 @@ test('files can get fetched using the storage url', function() {
test('storage_path helper does not change if suffix_storage_path is off', function() {
$originalStoragePath = storage_path();
// todo@tests https://github.com/tenancy-for-laravel/v4/pull/44#issue-2228530362
config([
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
'tenancy.filesystem.suffix_storage_path' => false,

View file

@ -27,6 +27,7 @@ use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
use function Stancl\Tenancy\Tests\pest;
use Stancl\Tenancy\Events\MigratingDatabase;
beforeEach(function () {
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {
@ -95,6 +96,60 @@ test('migrate command works with tenants option', function () {
expect(Schema::hasTable('users'))->toBeTrue();
});
test('migrate command uses the passed database option as the template tenant connection', function () {
$originalTemplateConnection = config('tenancy.database.template_tenant_connection');
// Add a custom connection that will be used as the template for the tenant connection
// Identical to the default (mysql), just with different charset and collation
config(['database.connections.custom_connection' => [
"driver" => "mysql",
"url" => "",
"host" => "mysql",
"port" => "3306",
"database" => "main",
"username" => "root",
"password" => "password",
"unix_socket" => "",
"charset" => "latin1", // Different from the default (utf8mb4)
"collation" => "latin1_swedish_ci", // Different from the default (utf8mb4_unicode_ci)
"prefix" => "",
"prefix_indexes" => true,
"strict" => true,
"engine" => null,
"options" => []
]]);
$templateConnectionDuringMigration = null;
$tenantConnectionDuringMigration = null;
Event::listen(MigratingDatabase::class, function() use (&$templateConnectionDuringMigration, &$tenantConnectionDuringMigration) {
$templateConnectionDuringMigration = config('tenancy.database.template_tenant_connection');
$tenantConnectionDuringMigration = DB::connection('tenant')->getConfig();
});
// The original tenant template connection config remains default
expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection);
Tenant::create();
// The original template connection is used when the --database option is not passed
pest()->artisan('tenants:migrate');
expect($templateConnectionDuringMigration)->toBe($originalTemplateConnection);
Tenant::create();
// The migrate command temporarily uses the connection passed in the --database option
pest()->artisan('tenants:migrate', ['--database' => 'custom_connection']);
expect($templateConnectionDuringMigration)->toBe('custom_connection');
// The tenant connection during migration actually used custom_connection's config
expect($tenantConnectionDuringMigration['charset'])->toBe('latin1');
expect($tenantConnectionDuringMigration['collation'])->toBe('latin1_swedish_ci');
// The tenant template connection config is restored to the original after migrating
expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection);
});
test('migrate command only throws exceptions if skip-failing is not passed', function() {
Tenant::create();
@ -311,6 +366,21 @@ test('migrate fresh command works', function () {
expect(DB::table('users')->exists())->toBeFalse();
});
test('migrate fresh command respects force option in production', function () {
// Set environment to production
app()->detectEnvironment(fn() => 'production');
Tenant::create();
// Without --force in production, command should prompt for confirmation
pest()->artisan('tenants:migrate-fresh')
->expectsConfirmation('Are you sure you want to run this command?');
// With --force, command should succeed without prompting
pest()->artisan('tenants:migrate-fresh', ['--force' => true])
->assertSuccessful();
});
test('run command with array of tenants works', function () {
$tenantId1 = Tenant::create()->getTenantKey();
$tenantId2 = Tenant::create()->getTenantKey();

View file

@ -10,6 +10,7 @@ use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyInitialized;
use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Str;
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
@ -120,7 +121,7 @@ test('early identification works with path identification', function (bool $useK
RouteFacade::get('/{post}/comment/{comment}/edit', [$controller, 'computePost']);
});
$tenant = Tenant::create(['tenancy_db_name' => pest()->randomString()]);
$tenant = Tenant::create(['tenancy_db_name' => Str::random(10)]);
// Migrate users and comments tables on tenant connection
pest()->artisan('tenants:migrate', [

View file

@ -63,7 +63,7 @@ test('sqlite ATTACH statements can be blocked', function (bool $disallow) {
return json_encode(DB::select(request('q2')));
});
tenancy(); // trigger features: todo@samuel remove after feature refactor
tenancy()->bootstrapFeatures();
if ($disallow) {
expect(fn () => pest()->post('/central-sqli', [

View file

@ -12,6 +12,8 @@ test('tenant redirect macro replaces only the hostname', function () {
'tenancy.features' => [CrossDomainRedirect::class],
]);
tenancy()->bootstrapFeatures();
Route::get('/foobar', function () {
return 'Foo';
})->name('home');

View file

@ -11,16 +11,21 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () {
config([
'tenancy.features' => [TenantConfig::class],
'tenancy.bootstrappers' => [],
]);
tenancy()->bootstrapFeatures();
});
afterEach(function () {
TenantConfig::$storageToConfigMap = [];
});
test('nested tenant values are merged', function () {
expect(config('whitelabel.theme'))->toBeNull();
config([
'tenancy.features' => [TenantConfig::class],
'tenancy.bootstrappers' => [],
]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
@ -39,10 +44,6 @@ test('nested tenant values are merged', function () {
test('config is merged and removed', function () {
expect(config('services.paypal'))->toBe(null);
config([
'tenancy.features' => [TenantConfig::class],
'tenancy.bootstrappers' => [],
]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
@ -68,10 +69,6 @@ test('config is merged and removed', function () {
test('the value can be set to multiple config keys', function () {
expect(config('services.paypal'))->toBe(null);
config([
'tenancy.features' => [TenantConfig::class],
'tenancy.bootstrappers' => [],
]);
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);

View file

@ -3,27 +3,42 @@
declare(strict_types=1);
use Illuminate\Foundation\Vite;
use Stancl\Tenancy\Tests\Etc\Tenant;
use Stancl\Tenancy\Overrides\Vite as StanclVite;
use Illuminate\Support\Facades\File;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Features\ViteBundler;
use Stancl\Tenancy\Tests\Etc\Tenant;
test('vite helper uses our custom class', function() {
$vite = app(Vite::class);
expect($vite)->toBeInstanceOf(Vite::class);
expect($vite)->not()->toBeInstanceOf(StanclVite::class);
use function Stancl\Tenancy\Tests\withBootstrapping;
beforeEach(function () {
config([
'tenancy.features' => [ViteBundler::class],
'tenancy.filesystem.asset_helper_override' => true,
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
]);
$tenant = Tenant::create();
tenancy()->initialize($tenant);
app()->forgetInstance(Vite::class);
$vite = app(Vite::class);
expect($vite)->toBeInstanceOf(StanclVite::class);
File::ensureDirectoryExists(dirname($manifestPath = public_path('build/manifest.json')));
File::put($manifestPath, json_encode([
'foo' => [
'file' => 'assets/foo-AbC123.js',
'src' => 'js/foo.js',
],
]));
});
test('vite bundler ensures vite assets use global_asset when asset_helper_override is enabled', function () {
config(['tenancy.features' => [ViteBundler::class]]);
tenancy()->bootstrapFeatures();
withBootstrapping();
tenancy()->initialize(Tenant::create());
// Not what we want
expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo']));
$viteAssetUrl = app(Vite::class)->asset('foo');
$expectedGlobalUrl = global_asset('build/assets/foo-AbC123.js');
expect($viteAssetUrl)->toBe($expectedGlobalUrl);
expect($viteAssetUrl)->toBe('http://localhost/build/assets/foo-AbC123.js');
});

View file

@ -8,6 +8,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Stancl\Tenancy\Actions\CloneRoutesAsTenant;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
@ -44,7 +45,7 @@ test('asset can be accessed using the url returned by the tenant asset helper',
$tenant = Tenant::create();
tenancy()->initialize($tenant);
$filename = 'testfile' . pest()->randomString(10);
$filename = 'testfile' . Str::random(8);
Storage::disk('public')->put($filename, 'bar');
$path = storage_path("app/public/$filename");
@ -136,7 +137,7 @@ test('TenantAssetController headers are configurable', function () {
tenancy()->initialize($tenant);
$tenant->createDomain('foo.localhost');
$filename = 'testfile' . pest()->randomString(10);
$filename = 'testfile' . Str::random(10);
Storage::disk('public')->put($filename, 'bar');
$this->withoutExceptionHandling();

View file

@ -29,6 +29,8 @@ use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQ
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
use function Stancl\Tenancy\Tests\pest;
use function Stancl\Tenancy\Tests\withBootstrapping;
use function Stancl\Tenancy\Tests\withTenantDatabases;
beforeEach(function () {
SQLiteDatabaseManager::$path = null;
@ -43,7 +45,7 @@ test('databases can be created and deleted', function ($driver, $databaseManager
"tenancy.database.managers.$driver" => $databaseManager,
]);
$name = 'db' . pest()->randomString();
$name = 'db' . Str::random(10);
$manager = app($databaseManager);
@ -70,7 +72,7 @@ test('dbs can be created when another driver is used for the central db', functi
return $event->tenant;
})->toListener());
$database = 'db' . pest()->randomString();
$database = 'db' . Str::random(10);
$mysqlmanager = app(MySQLDatabaseManager::class);
$mysqlmanager->setConnection('mysql');
@ -86,7 +88,7 @@ test('dbs can be created when another driver is used for the central db', functi
$postgresManager = app(PostgreSQLDatabaseManager::class);
$postgresManager->setConnection('pgsql');
$database = 'db' . pest()->randomString();
$database = 'db' . Str::random(10);
expect($postgresManager->databaseExists($database))->toBeFalse();
Tenant::create([
@ -146,18 +148,15 @@ test('db name is prefixed with db path when sqlite is used', function () {
expect(database_path('foodb'))->toBe(config('database.connections.tenant.database'));
});
test('sqlite databases use the WAL journal mode by default', function (bool|null $wal) {
$expected = $wal ? 'wal' : 'delete';
if ($wal !== null) {
SQLiteDatabaseManager::$WAL = $wal;
} else {
// default behavior
$expected = 'wal';
}
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
test('sqlite databases respect the template journal_mode config', function (string $journal_mode) {
withTenantDatabases();
withBootstrapping();
config([
'database.connections.sqlite.journal_mode' => $journal_mode,
'tenancy.bootstrappers' => [
DatabaseTenancyBootstrapper::class,
],
]);
$tenant = Tenant::create([
'tenancy_db_connection' => 'sqlite',
@ -170,11 +169,18 @@ test('sqlite databases use the WAL journal mode by default', function (bool|null
$db = new PDO('sqlite:' . $dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe($expected);
// Before we connect to the DB using Laravel, it will be in default delete mode
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe('delete');
// cleanup
SQLiteDatabaseManager::$WAL = true;
})->with([true, false, null]);
// This will trigger the logic in Laravel's SQLiteConnector
$tenant->run(fn () => DB::select('select 1'));
$db = new PDO('sqlite:' . $dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Once we connect to the DB, it will be in the configured journal mode
expect($db->query('pragma journal_mode')->fetch(PDO::FETCH_ASSOC)['journal_mode'])->toBe($journal_mode);
})->with(['delete', 'wal']);
test('schema manager uses schema to separate tenant dbs', function () {
config([

View file

@ -42,6 +42,8 @@ beforeEach(function () {
],
]);
tenancy()->bootstrapFeatures();
Event::listen(
TenantCreated::class,
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {

View file

@ -236,11 +236,6 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
$app->singleton('Illuminate\Contracts\Console\Kernel', Etc\Console\ConsoleKernel::class);
}
public function randomString(int $length = 10)
{
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', (int) (ceil($length / strlen($x))))), 1, $length);
}
public function assertArrayIsSubset($subset, $array, string $message = ''): void
{
parent::assertTrue(array_intersect($subset, $array) == $subset, $message);