diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7b64d2d..81d37af5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,9 @@ env: on: push: - branches: [ 3.x, 2.x, master ] + branches: [ master ] pull_request: - branches: [ 3.x, 2.x, master ] + branches: [ master ] jobs: tests: @@ -15,8 +15,8 @@ jobs: strategy: matrix: - php: ["7.4", "8.0"] - laravel: ["^6.0", "^8.0"] + php: ["8.1"] + laravel: ["^9.0"] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index b3223156..95522c34 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ psysh phpunit_var_*.xml coverage/ clover.xml +tenant-schema-test.dump tests/Etc/tmp/queuetest.json +docker-compose.override.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7dce1b82..a5a6ec3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,3 +9,16 @@ StyleCI will flag code style violations in your pull requests. Run `docker-compose up -d` to start the containers. Then run `./test` to run the tests. When you're done testing, run `docker-compose down` to shut down the containers. + +### Docker on M1 + +You can add: +```yaml +services: + mysql: + platform: linux/amd64 + mysql2: + platform: linux/amd64 +``` + +to `docker-compose.override.yml` to make `docker-compose up-d` work on M1. diff --git a/Dockerfile b/Dockerfile index f5b9b067..c9da07ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,13 +29,13 @@ RUN apt-get update \ && curl https://packages.microsoft.com/config/debian/9/prod.list \ > /etc/apt/sources.list.d/mssql-release.list -RUN apt-get install -y --no-install-recommends locales apt-transport-https libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat unixodbc-dev msodbcsql17 curl sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git +RUN apt-get install -y --no-install-recommends locales apt-transport-https libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat unixodbc-dev msodbcsql17 curl mariadb-client sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git RUN docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \ # && if [ "${PHP_VERSION}" = "7.4" ]; then docker-php-ext-configure gd --with-freetype --with-jpeg; else docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/; fi \ && docker-php-ext-install -j$(nproc) gd pdo pdo_mysql pdo_pgsql pdo_sqlite pgsql zip gmp bcmath pcntl ldap sysvmsg exif \ # install the redis php extension - && pecl install redis-5.3.2 \ + && pecl install redis-5.3.7 \ && docker-php-ext-enable redis \ # install the pcov extention && pecl install pcov \ diff --git a/README.md b/README.md index f4d28288..95fb7c60 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
-
+
diff --git a/composer.json b/composer.json
index bf66e1f2..8e932658 100644
--- a/composer.json
+++ b/composer.json
@@ -11,16 +11,16 @@
],
"require": {
"ext-json": "*",
- "illuminate/support": "^6.0|^7.0|^8.0",
+ "illuminate/support": "^6.0|^7.0|^8.0|^9.0",
"facade/ignition-contracts": "^1.0",
"ramsey/uuid": "^3.7|^4.0",
- "stancl/jobpipeline": "^1.0",
- "stancl/virtualcolumn": "^1.0"
+ "stancl/jobpipeline": "dev-master",
+ "stancl/virtualcolumn": "dev-master"
},
"require-dev": {
- "laravel/framework": "^6.0|^7.0|^8.0",
- "orchestra/testbench-browser-kit": "^4.0|^5.0|^6.0",
- "league/flysystem-aws-s3-v3": "~1.0",
+ "laravel/framework": "^6.0|^7.0|^8.0|^9.0",
+ "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0",
+ "league/flysystem-aws-s3-v3": "^1.0|^3.0",
"doctrine/dbal": "^2.10",
"spatie/valuestore": "^1.2.5"
},
@@ -48,6 +48,12 @@
}
}
},
+ "scripts": {
+ "docker-up": "PHP_VERSION=8.0.11 docker-compose up -d",
+ "docker-down": "PHP_VERSION=8.0.11 docker-compose down",
+ "docker-rebuild": "PHP_VERSION=8.0.11 docker-compose up -d --no-deps --build",
+ "test": "PHP_VERSION=8.0.11 ./test"
+ },
"minimum-stability": "dev",
"prefer-stable": true
}
diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php
index d5ae2d50..2b4f8dfe 100644
--- a/src/Bootstrappers/FilesystemTenancyBootstrapper.php
+++ b/src/Bootstrappers/FilesystemTenancyBootstrapper.php
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Contracts\Foundation\Application;
-use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
@@ -54,20 +53,24 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
}
// Storage facade
- foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
- /** @var FilesystemAdapter $filesystemDisk */
- $filesystemDisk = Storage::disk($disk);
- $this->originalPaths['disks'][$disk] = $filesystemDisk->getAdapter()->getPathPrefix();
+ Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
- if ($root = str_replace(
- '%storage_path%',
- storage_path(),
- $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? ''
- )) {
- $filesystemDisk->getAdapter()->setPathPrefix($finalPrefix = $root);
- } else {
- $root = $this->app['config']["filesystems.disks.{$disk}.root"];
- $filesystemDisk->getAdapter()->setPathPrefix($finalPrefix = $root . "/{$suffix}");
+ foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
+ // todo@v4 \League\Flysystem\PathPrefixer is making this a lot more painful in flysystem v2
+
+ $originalRoot = $this->app['config']["filesystems.disks.{$disk}.root"];
+ $this->originalPaths['disks'][$disk] = $originalRoot;
+
+ $finalPrefix = str_replace(
+ ['%storage_path%', '%tenant%'],
+ [storage_path(), $tenant->getTenantKey()],
+ $this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '',
+ );
+
+ if (! $finalPrefix) {
+ $finalPrefix = $originalRoot
+ ? rtrim($originalRoot, '/') . '/'. $suffix
+ : $suffix;
}
$this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix;
@@ -84,14 +87,9 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
$this->app['url']->setAssetRoot($this->app['config']['app.asset_url']);
// Storage facade
+ Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
- /** @var FilesystemAdapter $filesystemDisk */
- $filesystemDisk = Storage::disk($disk);
-
- $root = $this->originalPaths['disks'][$disk];
-
- $filesystemDisk->getAdapter()->setPathPrefix($root);
- $this->app['config']["filesystems.disks.{$disk}.root"] = $root;
+ $this->app['config']["filesystems.disks.{$disk}.root"] = $this->originalPaths['disks'][$disk];
}
}
}
diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php
index 6a88f701..790e1344 100644
--- a/src/Bootstrappers/QueueTenancyBootstrapper.php
+++ b/src/Bootstrappers/QueueTenancyBootstrapper.php
@@ -30,8 +30,10 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
*
* This is useful when you're changing the tenant's state (e.g. properties in the `data` column) and want the next job to initialize tenancy again
* with the new data. Features like the Tenant Config are only executed when tenancy is initialized, so the re-initialization is needed in some cases.
+ *
+ * @var bool
*/
- public static bool $forceRefresh = false;
+ public static $forceRefresh = false;
/**
* The normal constructor is only executed after tenancy is bootstrapped.
@@ -61,8 +63,8 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
});
- if (Str::startsWith(app()->version(), '8')) {
- // JobRetryRequested only exists since Laravel 8
+ if (version_compare(app()->version(), '8.64', '>=')) {
+ // JobRetryRequested only exists since Laravel 8.64
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
$previousTenant = tenant();
diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php
index bf92dfcd..c67d3598 100644
--- a/src/Commands/Migrate.php
+++ b/src/Commands/Migrate.php
@@ -8,32 +8,26 @@ use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Console\Migrations\MigrateCommand;
use Illuminate\Database\Migrations\Migrator;
use Stancl\Tenancy\Concerns\DealsWithMigrations;
+use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
use Stancl\Tenancy\Concerns\HasATenantsOption;
use Stancl\Tenancy\Events\DatabaseMigrated;
use Stancl\Tenancy\Events\MigratingDatabase;
class Migrate extends MigrateCommand
{
- use HasATenantsOption, DealsWithMigrations;
+ use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand;
- /**
- * The console command description.
- *
- * @var string
- */
protected $description = 'Run migrations for tenant(s)';
- /**
- * Create a new command instance.
- *
- * @param Migrator $migrator
- * @param Dispatcher $dispatcher
- */
+ protected static function getTenantCommandName(): string
+ {
+ return 'tenants:migrate';
+ }
+
public function __construct(Migrator $migrator, Dispatcher $dispatcher)
{
parent::__construct($migrator, $dispatcher);
- $this->setName('tenants:migrate');
$this->specifyParameters();
}
diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php
index f50e2f5f..283d70b0 100644
--- a/src/Commands/MigrateFresh.php
+++ b/src/Commands/MigrateFresh.php
@@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Stancl\Tenancy\Concerns\DealsWithMigrations;
use Stancl\Tenancy\Concerns\HasATenantsOption;
+use Symfony\Component\Console\Input\InputOption;
final class MigrateFresh extends Command
{
@@ -23,6 +24,8 @@ final class MigrateFresh extends Command
{
parent::__construct();
+ $this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null);
+
$this->setName('tenants:migrate-fresh');
}
@@ -37,6 +40,7 @@ final class MigrateFresh extends Command
$this->info('Dropping tables.');
$this->call('db:wipe', array_filter([
'--database' => 'tenant',
+ '--drop-views' => $this->option('drop-views'),
'--force' => true,
]));
diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php
index 081872c8..e60d974b 100644
--- a/src/Commands/Rollback.php
+++ b/src/Commands/Rollback.php
@@ -7,13 +7,19 @@ namespace Stancl\Tenancy\Commands;
use Illuminate\Database\Console\Migrations\RollbackCommand;
use Illuminate\Database\Migrations\Migrator;
use Stancl\Tenancy\Concerns\DealsWithMigrations;
+use Stancl\Tenancy\Concerns\ExtendsLaravelCommand;
use Stancl\Tenancy\Concerns\HasATenantsOption;
use Stancl\Tenancy\Events\DatabaseRolledBack;
use Stancl\Tenancy\Events\RollingBackDatabase;
class Rollback extends RollbackCommand
{
- use HasATenantsOption, DealsWithMigrations;
+ use HasATenantsOption, DealsWithMigrations, ExtendsLaravelCommand;
+
+ protected static function getTenantCommandName(): string
+ {
+ return 'tenants:rollback';
+ }
/**
* The console command description.
@@ -31,8 +37,7 @@ class Rollback extends RollbackCommand
{
parent::__construct($migrator);
- $this->setName('tenants:rollback');
- $this->specifyParameters();
+ $this->specifyTenantSignature();
}
/**
diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php
new file mode 100644
index 00000000..557c6975
--- /dev/null
+++ b/src/Commands/TenantDump.php
@@ -0,0 +1,54 @@
+setName('tenants:dump');
+ $this->specifyParameters();
+ }
+
+
+ public function handle(ConnectionResolverInterface $connections, Dispatcher $dispatcher): int
+ {
+ $this->tenant()->run(fn() => parent::handle($connections, $dispatcher));
+
+ return Command::SUCCESS;
+ }
+
+ public function tenant(): Tenant
+ {
+ $tenant = $this->option('tenant')
+ ?? tenant()
+ ?? $this->ask('What tenant do you want to dump the schema for?')
+ ?? tenancy()->query()->first();
+
+ if (! $tenant instanceof Tenant) {
+ $tenant = tenancy()->find($tenant);
+ }
+
+ throw_if(! $tenant, 'Could not identify the tenant to use for dumping the schema.');
+
+ return $tenant;
+ }
+
+ protected function getOptions(): array
+ {
+ return array_merge([
+ ['tenant', null, InputOption::VALUE_OPTIONAL, '', null],
+ ], parent::getOptions());
+ }
+}
diff --git a/src/Concerns/ExtendsLaravelCommand.php b/src/Concerns/ExtendsLaravelCommand.php
new file mode 100644
index 00000000..bdafc8f7
--- /dev/null
+++ b/src/Concerns/ExtendsLaravelCommand.php
@@ -0,0 +1,23 @@
+specifyParameters();
+ }
+
+ public function getName(): ?string
+ {
+ return static::getTenantCommandName();
+ }
+
+ public static function getDefaultName(): ?string
+ {
+ return static::getTenantCommandName();
+ }
+
+ abstract protected static function getTenantCommandName(): string;
+}
diff --git a/src/Database/DatabaseManager.php b/src/Database/DatabaseManager.php
index e85fd659..6242ffa9 100644
--- a/src/Database/DatabaseManager.php
+++ b/src/Database/DatabaseManager.php
@@ -7,10 +7,12 @@ namespace Stancl\Tenancy\Database;
use Illuminate\Config\Repository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Database\DatabaseManager as BaseDatabaseManager;
+use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
use Stancl\Tenancy\Contracts\TenantCannotBeCreatedException;
use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Exceptions\DatabaseManagerNotRegisteredException;
use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException;
+use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException;
/**
* @internal Class is subject to breaking changes in minor and patch versions.
@@ -90,8 +92,14 @@ class DatabaseManager
*/
public function ensureTenantCanBeCreated(TenantWithDatabase $tenant): void
{
- if ($tenant->database()->manager()->databaseExists($database = $tenant->database()->getName())) {
+ $manager = $tenant->database()->manager();
+
+ if ($manager->databaseExists($database = $tenant->database()->getName())) {
throw new TenantDatabaseAlreadyExistsException($database);
}
+
+ if ($manager instanceof ManagesDatabaseUsers && $manager->userExists($username = $tenant->database()->getUsername())) {
+ throw new TenantDatabaseUserAlreadyExistsException($username);
+ }
}
}
diff --git a/src/Jobs/CreateDatabase.php b/src/Jobs/CreateDatabase.php
index 3a74534d..3cb2a6b4 100644
--- a/src/Jobs/CreateDatabase.php
+++ b/src/Jobs/CreateDatabase.php
@@ -36,8 +36,8 @@ class CreateDatabase implements ShouldQueue
return false;
}
- $databaseManager->ensureTenantCanBeCreated($this->tenant);
$this->tenant->database()->makeCredentials();
+ $databaseManager->ensureTenantCanBeCreated($this->tenant);
$this->tenant->database()->manager()->createDatabase($this->tenant);
event(new DatabaseCreated($this->tenant));
diff --git a/src/Middleware/CheckTenantForMaintenanceMode.php b/src/Middleware/CheckTenantForMaintenanceMode.php
index 5554663f..8e29a31e 100644
--- a/src/Middleware/CheckTenantForMaintenanceMode.php
+++ b/src/Middleware/CheckTenantForMaintenanceMode.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware;
use Closure;
-use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
use Symfony\Component\HttpFoundation\IpUtils;
@@ -29,7 +29,12 @@ class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
return $next($request);
}
- throw new MaintenanceModeException($data['time'], $data['retry'], $data['message']);
+ throw new HttpException(
+ 503,
+ 'Service Unavailable',
+ null,
+ isset($data['retry']) ? ['Retry-After' => $data['retry']] : []
+ );
}
return $next($request);
diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php
index 4faaccf3..dd061af3 100644
--- a/src/TenancyServiceProvider.php
+++ b/src/TenancyServiceProvider.php
@@ -88,6 +88,7 @@ class TenancyServiceProvider extends ServiceProvider
Commands\Migrate::class,
Commands\Rollback::class,
Commands\TenantList::class,
+ Commands\TenantDump::class,
Commands\MigrateFresh::class,
]);
diff --git a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
index f8bedc97..918601a8 100644
--- a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
+++ b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php
@@ -7,7 +7,6 @@ namespace Stancl\Tenancy\TenantDatabaseManagers;
use Stancl\Tenancy\Concerns\CreatesDatabaseUsers;
use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
use Stancl\Tenancy\DatabaseConfig;
-use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException;
class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager implements ManagesDatabaseUsers
{
@@ -26,10 +25,6 @@ class PermissionControlledMySQLDatabaseManager extends MySQLDatabaseManager impl
$hostname = $databaseConfig->connection()['host'];
$password = $databaseConfig->getPassword();
- if ($this->userExists($username)) {
- throw new TenantDatabaseUserAlreadyExistsException($username);
- }
-
$this->database()->statement("CREATE USER `{$username}`@`%` IDENTIFIED BY '{$password}'");
$grants = implode(', ', static::$grants);
diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
index 9d815b25..55f049d0 100644
--- a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
+++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php
@@ -46,7 +46,11 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager
public function makeConnectionConfig(array $baseConfig, string $databaseName): array
{
- $baseConfig['schema'] = $databaseName;
+ if (version_compare(app()->version(), '9.0', '>=')) {
+ $baseConfig['search_path'] = $databaseName;
+ } else {
+ $baseConfig['schema'] = $databaseName;
+ }
return $baseConfig;
}
diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php
index 1b0c880d..588fadd8 100644
--- a/tests/BootstrapperTest.php
+++ b/tests/BootstrapperTest.php
@@ -4,23 +4,27 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
-use Illuminate\Support\Facades\Cache;
+use Illuminate\Filesystem\FilesystemAdapter;
+use ReflectionObject;
+use ReflectionProperty;
+use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
+use Stancl\JobPipeline\JobPipeline;
+use Stancl\Tenancy\Tests\Etc\Tenant;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
-use Stancl\JobPipeline\JobPipeline;
-use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
-use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
-use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
-use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded;
-use Stancl\Tenancy\Events\TenancyInitialized;
-use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
+use Stancl\Tenancy\Events\TenantCreated;
+use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
-use Stancl\Tenancy\Tests\Etc\Tenant;
+use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
+use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
+use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
+use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
class BootstrapperTest extends TestCase
{
@@ -165,6 +169,7 @@ class BootstrapperTest extends TestCase
$tenant2 = Tenant::create();
tenancy()->initialize($tenant1);
+
Storage::disk('public')->put('foo', 'bar');
$this->assertSame('bar', Storage::disk('public')->get('foo'));
@@ -184,30 +189,38 @@ class BootstrapperTest extends TestCase
$this->assertFalse(Storage::disk('public')->exists('foo'));
$this->assertFalse(Storage::disk('public')->exists('abc'));
+ $expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base
+
+ // Check that disk prefixes respect the root_override logic
+ $this->assertSame($expected_storage_path . '/app/', $this->getDiskPrefix('local'));
+ $this->assertSame($expected_storage_path . '/app/public/', $this->getDiskPrefix('public'));
+ $this->assertSame('tenant' . tenant('id') . '/', $this->getDiskPrefix('s3'), '/');
+
// Check suffixing logic
$new_storage_path = storage_path();
- $this->assertEquals($old_storage_path . '/' . config('tenancy.filesystem.suffix_base') . tenant('id'), $new_storage_path);
+ $this->assertEquals($expected_storage_path, $new_storage_path);
+ }
- foreach (config('tenancy.filesystem.disks') as $disk) {
- $suffix = config('tenancy.filesystem.suffix_base') . tenant('id');
+ protected function getDiskPrefix(string $disk): string
+ {
+ /** @var FilesystemAdapter $disk */
+ $disk = Storage::disk($disk);
+ $adapter = $disk->getAdapter();
- /** @var FilesystemAdapter $filesystemDisk */
- $filesystemDisk = Storage::disk($disk);
-
- $current_path_prefix = $filesystemDisk->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);
+ if (! Str::startsWith(app()->version(), '9.')) {
+ return $adapter->getPathPrefix();
}
+
+ $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);
}
// for queues see QueueTest
diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php
index d7da0cab..145a93c5 100644
--- a/tests/CommandsTest.php
+++ b/tests/CommandsTest.php
@@ -91,6 +91,38 @@ class CommandsTest extends TestCase
$this->assertTrue(Schema::hasTable('users'));
}
+ /** @test */
+ public function migrate_command_loads_schema_state()
+ {
+ $tenant = Tenant::create();
+
+ $this->assertFalse(Schema::hasTable('schema_users'));
+ $this->assertFalse(Schema::hasTable('users'));
+
+ Artisan::call('tenants:migrate --schema-path="tests/Etc/tenant-schema.dump"');
+
+ $this->assertFalse(Schema::hasTable('schema_users'));
+ $this->assertFalse(Schema::hasTable('users'));
+
+ tenancy()->initialize($tenant);
+
+ // Check for both tables to see if missing migrations also get executed
+ $this->assertTrue(Schema::hasTable('schema_users'));
+ $this->assertTrue(Schema::hasTable('users'));
+ }
+
+ /** @test */
+ public function dump_command_works()
+ {
+ $tenant = Tenant::create();
+ Artisan::call('tenants:migrate');
+
+ tenancy()->initialize($tenant);
+
+ Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"');
+ $this->assertFileExists('tests/Etc/tenant-schema-test.dump');
+ }
+
/** @test */
public function rollback_command_works()
{
diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php
index 0b095024..344239d1 100644
--- a/tests/DatabaseUsersTest.php
+++ b/tests/DatabaseUsersTest.php
@@ -10,6 +10,7 @@ use Illuminate\Support\Str;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Contracts\ManagesDatabaseUsers;
+use Stancl\Tenancy\Events\DatabaseCreated;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Exceptions\TenantDatabaseUserAlreadyExistsException;
@@ -67,14 +68,18 @@ class DatabaseUsersTest extends TestCase
$this->assertTrue($manager->databaseExists($tenant->database()->getName()));
$this->expectException(TenantDatabaseUserAlreadyExistsException::class);
+ Event::fake([DatabaseCreated::class]);
+
$tenant2 = Tenant::create([
'tenancy_db_username' => $username,
]);
/** @var ManagesDatabaseUsers $manager */
- $manager = $tenant2->database()->manager();
+ $manager2 = $tenant2->database()->manager();
+
// database was not created because of DB transaction
- $this->assertFalse($manager->databaseExists($tenant2->database()->getName()));
+ $this->assertFalse($manager2->databaseExists($tenant2->database()->getName()));
+ Event::assertNotDispatched(DatabaseCreated::class);
}
/** @test */
diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/ConsoleKernel.php
index 1bc66365..a548f113 100644
--- a/tests/Etc/ConsoleKernel.php
+++ b/tests/Etc/ConsoleKernel.php
@@ -4,15 +4,10 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests\Etc;
-use Orchestra\Testbench\Console\Kernel;
+use Orchestra\Testbench\Foundation\Console\Kernel;
class ConsoleKernel extends Kernel
{
- /**
- * The Artisan commands provided by your application.
- *
- * @var array
- */
protected $commands = [
ExampleCommand::class,
AddUserCommand::class,
diff --git a/tests/Etc/tenant-schema.dump b/tests/Etc/tenant-schema.dump
new file mode 100644
index 00000000..6af9f019
--- /dev/null
+++ b/tests/Etc/tenant-schema.dump
@@ -0,0 +1,66 @@
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+DROP TABLE IF EXISTS `failed_jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `failed_jobs` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `migrations`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `migrations` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `batch` int(11) NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `password_resets`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `password_resets` (
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ KEY `password_resets_email_index` (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `schema_users` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email_verified_at` timestamp NULL DEFAULT NULL,
+ `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `users_email_unique` (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+INSERT INTO `migrations` VALUES (2,'2014_10_12_100000_testbench_create_password_resets_table',1);
+INSERT INTO `migrations` VALUES (3,'2019_08_19_000000_testbench_create_failed_jobs_table',1);
diff --git a/tests/Etc/tmp/queuetest.json b/tests/Etc/tmp/queuetest.json
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php
index a8ecb064..4a8d8d0c 100644
--- a/tests/MaintenanceModeTest.php
+++ b/tests/MaintenanceModeTest.php
@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
+use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Http\Exceptions\MaintenanceModeException;
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Tests\Etc\Tenant;
+use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
class MaintenanceModeTest extends TestCase
{
@@ -32,7 +34,7 @@ class MaintenanceModeTest extends TestCase
$tenant->putDownForMaintenance();
- $this->expectException(MaintenanceModeException::class);
+ $this->expectException(HttpException::class);
$this->withoutExceptionHandling()
->get('http://acme.localhost/foo');
}
diff --git a/tests/QueueTest.php b/tests/QueueTest.php
index afe64fea..a3df9cd7 100644
--- a/tests/QueueTest.php
+++ b/tests/QueueTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Tests;
+use Closure;
use Exception;
use Illuminate\Support\Str;
use Illuminate\Bus\Queueable;
@@ -24,6 +25,7 @@ use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
+use PDO;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
@@ -52,7 +54,7 @@ class QueueTest extends TestCase
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
- $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush();
+ $this->createValueStore();
}
public function tearDown(): void
@@ -60,6 +62,22 @@ class QueueTest extends TestCase
$this->valuestore->flush();
}
+ protected function createValueStore(): void
+ {
+ $valueStorePath = __DIR__ . '/Etc/tmp/queuetest.json';
+
+ if (! file_exists($valueStorePath)) {
+ // The directory sometimes goes missing as well when the file is deleted in git
+ if (! is_dir(__DIR__ . '/Etc/tmp')) {
+ mkdir(__DIR__ . '/Etc/tmp');
+ }
+
+ file_put_contents($valueStorePath, '');
+ }
+
+ $this->valuestore = Valuestore::make($valueStorePath)->flush();
+ }
+
protected function withFailedJobs()
{
Schema::connection('central')->create('failed_jobs', function (Blueprint $table) {
diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php
index 527adc6a..0e1464c0 100644
--- a/tests/TenantDatabaseManagerTest.php
+++ b/tests/TenantDatabaseManagerTest.php
@@ -196,7 +196,11 @@ class TenantDatabaseManagerTest extends TestCase
]);
tenancy()->initialize($tenant);
- $this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema'));
+ $schemaConfig = version_compare(app()->version(), '9.0', '>=') ?
+ config('database.connections.' . config('database.default') . '.search_path') :
+ config('database.connections.' . config('database.default') . '.schema');
+
+ $this->assertSame($tenant->database()->getName(), $schemaConfig);
$this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database']));
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index d3e42ea1..cea669a1 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -87,6 +87,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'public',
's3',
],
+ 'filesystems.disks.s3.bucket' => 'foo',
'tenancy.redis.tenancy' => env('TENANCY_TEST_REDIS_TENANCY', true),
'database.redis.client' => env('TENANCY_TEST_REDIS_CLIENT', 'phpredis'),
'tenancy.redis.prefixed_connections' => ['default'],