diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1303061a..f0fe927a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,12 +2,13 @@ name: CI env: COMPOSE_INTERACTIVE_NO_CLI: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} on: push: - branches: [ 3.x, 2.x, master ] + branches: [ master ] pull_request: - branches: [ 3.x, 2.x, master ] + branches: [ master ] jobs: tests: @@ -15,11 +16,8 @@ jobs: strategy: matrix: - php: ["7.4", "8.0"] - laravel: ["^6.0", "^8.0", "^9.0"] - exclude: - - laravel: "^9.0" - php: "7.4" + php: ["8.1"] + laravel: ["^9.0"] steps: - uses: actions/checkout@v2 @@ -29,3 +27,19 @@ jobs: run: docker-compose exec -T test composer require --no-interaction "laravel/framework:${{ matrix.laravel }}" - name: Run tests run: ./test + + php-cs-fixer: + name: Code style (php-cs-fixer) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install php-cs-fixer + run: composer global require friendsofphp/php-cs-fixer + - name: Run php-cs-fixer + run: $HOME/.composer/vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php + - name: Commit changes from php-cs-fixer + uses: EndBug/add-and-commit@v5 + with: + author_name: "PHP CS Fixer" + author_email: "phpcsfixer@example.com" + message: Fix code style (php-cs-fixer) diff --git a/.gitignore b/.gitignore index b3223156..64d9dc21 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ psysh phpunit_var_*.xml coverage/ clover.xml +tenant-schema-test.dump tests/Etc/tmp/queuetest.json +docker-compose.override.yml +.php-cs-fixer.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000..589838bc --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,141 @@ + ['syntax' => 'short'], + 'binary_operator_spaces' => [ + 'default' => 'single_space', + 'operators' => [ + '=>' => null, + '|' => 'no_space', + ] + ], + 'blank_line_after_namespace' => true, + 'blank_line_after_opening_tag' => true, + 'no_superfluous_phpdoc_tags' => true, + 'blank_line_before_statement' => [ + 'statements' => ['return'] + ], + 'braces' => true, + 'cast_spaces' => true, + 'class_definition' => true, + 'concat_space' => [ + 'spacing' => 'one' + ], + 'declare_equal_normalize' => true, + 'elseif' => true, + 'encoding' => true, + 'full_opening_tag' => true, + 'declare_strict_types' => true, + 'fully_qualified_strict_types' => true, // added by Shift + 'function_declaration' => true, + 'function_typehint_space' => true, + 'heredoc_to_nowdoc' => true, + 'include' => true, + 'increment_style' => ['style' => 'post'], + 'indentation_type' => true, + 'linebreak_after_opening_tag' => true, + 'line_ending' => true, + 'lowercase_cast' => true, + 'constant_case' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, // added from Symfony + 'magic_method_casing' => true, // added from Symfony + 'magic_constant_casing' => true, + 'method_argument_space' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'extra', + 'throw', + 'use', + 'use_trait', + ] + ], + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_closing_tag' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => [ + 'use' => 'echo' + ], + 'no_multiline_whitespace_around_double_arrow' => true, + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line' + ], + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'not_operator_with_successor_space' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'phpdoc_indent' => true, + 'general_phpdoc_tag_rename' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'psr_autoloading' => true, + 'self_accessor' => true, + 'short_scalar_cast' => true, + 'simplified_null_return' => false, // disabled by Shift + 'single_blank_line_at_eof' => true, + 'single_blank_line_before_namespace' => true, + 'single_class_element_per_statement' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'no_unused_imports' => true, + 'single_line_comment_style' => [ + 'comment_types' => ['hash'] + ], + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, +]; + +$project_path = getcwd(); +$finder = Finder::create() + ->in([ + $project_path . '/src', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new Config()) + ->setFinder($finder) + ->setRules($rules) + ->setRiskyAllowed(true) + ->setUsingCache(true); diff --git a/.styleci.yml b/.styleci.yml deleted file mode 100644 index e6d2b2c1..00000000 --- a/.styleci.yml +++ /dev/null @@ -1,7 +0,0 @@ -risky: true -preset: laravel -enabled: -- declare_strict_types -disabled: -- concat_without_spaces -- ternary_operator_spaces diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7dce1b82..12e5e55b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,10 +2,14 @@ ## Code style -StyleCI will flag code style violations in your pull requests. +php-cs-fixer will fix code style violations in your pull requests. ## Running tests -Run `docker-compose up -d` to start the containers. Then run `./test` to run the tests. +Run `composer docker-up` to start the containers. Then run `composer test` to run the tests. -When you're done testing, run `docker-compose down` to shut down the containers. +When you're done testing, run `composer docker-down` to shut down the containers. + +### Docker on M1 + +Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1. diff --git a/Dockerfile b/Dockerfile index 06d97aea..fb63afe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG PHP_VERSION=7.4 ARG PHP_TARGET=php:${PHP_VERSION}-cli -FROM ${PHP_TARGET} +FROM --platform=linux/amd64 ${PHP_TARGET} ARG COMPOSER_TARGET=2.0.3 @@ -22,20 +22,29 @@ ENV LANG=en_GB.UTF-8 # Dockerfile _and pin the versions_! Eg: # RUN pecl install memcached-2.2.0 && docker-php-ext-enable memcached -# install some OS packages we need -RUN apt-get update -RUN apt-get install -y --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat curl sqlite3 libsqlite3-dev libpq-dev libzip-dev unzip vim-tiny gosu git - # install php extensions + +RUN apt-get update \ + && apt-get install -y gnupg2 \ + && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - \ + && curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list \ + && apt-get update \ + && ACCEPT_EULA=Y apt-get install -y unixodbc-dev msodbcsql17 + +RUN apt-get install -y --no-install-recommends locales apt-transport-https libfreetype6-dev libjpeg62-turbo-dev libpng-dev libgmp-dev libldap2-dev netcat 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 \ && docker-php-ext-enable pcov \ - && echo "pcov.enabled = 1" > /usr/local/etc/php/conf.d/pcov.ini + && echo "pcov.enabled = 1" > /usr/local/etc/php/conf.d/pcov.ini \ + # install sqlsrv + && pecl install sqlsrv pdo_sqlsrv \ + && docker-php-ext-enable sqlsrv pdo_sqlsrv # clear the apt cache RUN rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \ 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/SUPPORT.md b/SUPPORT.md
index b7caaa5c..24be468b 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -1,5 +1,5 @@
# Get Support
-If you need help with implementing the package, you can join our community [Discord server](https://discord.gg/7cpgPxv) and ask in `#help`.
+If you need help with implementing the package, you can join our community [Discord server](https://archte.ch/discord) and ask in `#help`.
If you're interested in paid consulting from the maintainer, see the [contact page](https://tenancyforlaravel.com/contact/) on our website.
diff --git a/assets/TenancyServiceProvider.stub.php b/assets/TenancyServiceProvider.stub.php
index 1d15f418..865bb93d 100644
--- a/assets/TenancyServiceProvider.stub.php
+++ b/assets/TenancyServiceProvider.stub.php
@@ -40,7 +40,13 @@ class TenancyServiceProvider extends ServiceProvider
Events\TenantSaved::class => [],
Events\UpdatingTenant::class => [],
Events\TenantUpdated::class => [],
- Events\DeletingTenant::class => [],
+ Events\DeletingTenant::class => [
+ JobPipeline::make([
+ Jobs\DeleteDomains::class,
+ ])->send(function (Events\DeletingTenant $event) {
+ return $event->tenant;
+ })->shouldBeQueued(false),
+ ],
Events\TenantDeleted::class => [
JobPipeline::make([
Jobs\DeleteDatabase::class,
diff --git a/assets/config.php b/assets/config.php
index 85592d14..e1c82e6b 100644
--- a/assets/config.php
+++ b/assets/config.php
@@ -61,6 +61,7 @@ return [
'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
+ 'sqlsrv' => Stancl\Tenancy\TenantDatabaseManagers\MicrosoftSQLDatabaseManager::class,
/**
* Use this database manager for MySQL to have a DB user created for each tenant database.
diff --git a/assets/migrations/2019_09_15_000020_create_domains_table.php b/assets/migrations/2019_09_15_000020_create_domains_table.php
index 77c1b88a..17f706c2 100644
--- a/assets/migrations/2019_09_15_000020_create_domains_table.php
+++ b/assets/migrations/2019_09_15_000020_create_domains_table.php
@@ -21,7 +21,7 @@ class CreateDomainsTable extends Migration
$table->string('tenant_id');
$table->timestamps();
- $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade');
});
}
diff --git a/composer.json b/composer.json
index 88bfea29..8aca9ded 100644
--- a/composer.json
+++ b/composer.json
@@ -1,28 +1,35 @@
{
"name": "stancl/tenancy",
"description": "Automatic multi-tenancy for your Laravel application.",
- "keywords": ["laravel", "multi-tenancy", "multi-database", "tenancy"],
+ "keywords": [
+ "laravel",
+ "multi-tenancy",
+ "multi-database",
+ "tenancy"
+ ],
"license": "MIT",
"authors": [
{
"name": "Samuel Ć tancl",
- "email": "samuel.stancl@gmail.com"
+ "email": "samuel@archte.ch"
}
],
"require": {
+ "php": "^8.1",
"ext-json": "*",
- "illuminate/support": "^6.0|^7.0|^8.0|^9.0",
+ "illuminate/support": "^9.0",
"facade/ignition-contracts": "^1.0",
- "ramsey/uuid": "^3.7|^4.0",
- "stancl/jobpipeline": "^1.0",
- "stancl/virtualcolumn": "^1.0"
+ "ramsey/uuid": "^4.0",
+ "stancl/jobpipeline": "^1.6",
+ "stancl/virtualcolumn": "^1.2"
},
"require-dev": {
- "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",
+ "laravel/framework": "^9.0",
+ "orchestra/testbench": "^7.0",
+ "league/flysystem-aws-s3-v3": "^3.0",
"doctrine/dbal": "^2.10",
- "spatie/valuestore": "^1.2.5"
+ "spatie/valuestore": "^1.2.5",
+ "pestphp/pest": "^1.21"
},
"autoload": {
"psr-4": {
@@ -48,6 +55,18 @@
}
}
},
+ "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-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
+ "test": "PHP_VERSION=8.1 ./test"
+ },
"minimum-stability": "dev",
- "prefer-stable": true
+ "prefer-stable": true,
+ "config": {
+ "allow-plugins": {
+ "pestphp/pest-plugin": true
+ }
+ }
}
diff --git a/docker-compose-m1.override.yml b/docker-compose-m1.override.yml
new file mode 100644
index 00000000..32e163e6
--- /dev/null
+++ b/docker-compose-m1.override.yml
@@ -0,0 +1,7 @@
+services:
+ mysql:
+ platform: linux/amd64
+ mysql2:
+ platform: linux/amd64
+ mssql:
+ image: mcr.microsoft.com/azure-sql-edge
diff --git a/docker-compose.yml b/docker-compose.yml
index e8e8d418..7b635637 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,6 +22,9 @@ services:
TENANCY_TEST_REDIS_HOST: redis
TENANCY_TEST_MYSQL_HOST: mysql
TENANCY_TEST_PGSQL_HOST: postgres
+ TENANCY_TEST_SQLSRV_HOST: mssql
+ TENANCY_TEST_SQLSRV_USERNAME: sa
+ TENANCY_TEST_SQLSRV_PASSWORD: P@ssword
stdin_open: true
tty: true
mysql:
@@ -64,3 +67,11 @@ services:
interval: 1s
timeout: 3s
retries: 30
+ mssql:
+ image: mcr.microsoft.com/mssql/server:2019-latest
+ ports:
+ - 1433:1433
+ environment:
+ - ACCEPT_EULA=Y
+ - SA_PASSWORD=P@ssword # todo reuse values from env above
+ # todo missing health check
diff --git a/src/Bootstrappers/FilesystemTenancyBootstrapper.php b/src/Bootstrappers/FilesystemTenancyBootstrapper.php
index 418be93f..6f720e7c 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;
@@ -57,17 +56,21 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
Storage::forgetDisk($this->app['config']['tenancy.filesystem.disks']);
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%',
- storage_path(),
+ ['%storage_path%', '%tenant%'],
+ [storage_path(), $tenant->getTenantKey()],
$this->app['config']["tenancy.filesystem.root_override.{$disk}"] ?? '',
);
if (! $finalPrefix) {
- $finalPrefix = $originalRoot . '/'. $suffix;
+ $finalPrefix = $originalRoot
+ ? rtrim($originalRoot, '/') . '/' . $suffix
+ : $suffix;
}
$this->app['config']["filesystems.disks.{$disk}.root"] = $finalPrefix;
diff --git a/src/Bootstrappers/QueueTenancyBootstrapper.php b/src/Bootstrappers/QueueTenancyBootstrapper.php
index 6a88f701..2f859ecd 100644
--- a/src/Bootstrappers/QueueTenancyBootstrapper.php
+++ b/src/Bootstrappers/QueueTenancyBootstrapper.php
@@ -4,18 +4,17 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
-use Illuminate\Support\Str;
use Illuminate\Config\Repository;
-use Illuminate\Queue\QueueManager;
-use Stancl\Tenancy\Contracts\Tenant;
+use Illuminate\Contracts\Events\Dispatcher;
+use Illuminate\Contracts\Foundation\Application;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
-use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Queue\Events\JobRetryRequested;
+use Illuminate\Queue\QueueManager;
use Illuminate\Support\Testing\Fakes\QueueFake;
-use Illuminate\Contracts\Foundation\Application;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
+use Stancl\Tenancy\Contracts\Tenant;
class QueueTenancyBootstrapper implements TenancyBootstrapper
{
@@ -30,8 +29,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 +62,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/CacheManager.php b/src/CacheManager.php
index 88428353..09581201 100644
--- a/src/CacheManager.php
+++ b/src/CacheManager.php
@@ -13,7 +13,6 @@ class CacheManager extends BaseCacheManager
*
* @param string $method
* @param array $parameters
- * @return mixed
*/
public function __call($method, $parameters)
{
@@ -21,7 +20,7 @@ class CacheManager extends BaseCacheManager
if ($method === 'tags') {
$count = count($parameters);
-
+
if ($count !== 1) {
throw new \Exception("Method tags() takes exactly 1 argument. $count passed.");
}
diff --git a/src/Commands/Install.php b/src/Commands/Install.php
index dd2dd280..41492b26 100644
--- a/src/Commands/Install.php
+++ b/src/Commands/Install.php
@@ -24,8 +24,6 @@ class Install extends Command
/**
* Execute the console command.
- *
- * @return mixed
*/
public function handle()
{
diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php
index bf92dfcd..52ecd47f 100644
--- a/src/Commands/Migrate.php
+++ b/src/Commands/Migrate.php
@@ -8,39 +8,31 @@ 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();
}
/**
* Execute the console command.
- *
- * @return mixed
*/
public function handle()
{
diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php
index 4d003db0..63860153 100644
--- a/src/Commands/MigrateFresh.php
+++ b/src/Commands/MigrateFresh.php
@@ -23,7 +23,7 @@ final class MigrateFresh extends Command
public function __construct()
{
parent::__construct();
-
+
$this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null);
$this->setName('tenants:migrate-fresh');
@@ -31,8 +31,6 @@ final class MigrateFresh extends Command
/**
* Execute the console command.
- *
- * @return mixed
*/
public function handle()
{
diff --git a/src/Commands/Rollback.php b/src/Commands/Rollback.php
index 081872c8..1c434189 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,14 +37,11 @@ class Rollback extends RollbackCommand
{
parent::__construct($migrator);
- $this->setName('tenants:rollback');
- $this->specifyParameters();
+ $this->specifyTenantSignature();
}
/**
* Execute the console command.
- *
- * @return mixed
*/
public function handle()
{
diff --git a/src/Commands/Run.php b/src/Commands/Run.php
index aa518d7a..2b20d9c3 100644
--- a/src/Commands/Run.php
+++ b/src/Commands/Run.php
@@ -27,8 +27,6 @@ class Run extends Command
/**
* Execute the console command.
- *
- * @return mixed
*/
public function handle()
{
diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php
index dc97ae71..8c525208 100644
--- a/src/Commands/Seed.php
+++ b/src/Commands/Seed.php
@@ -35,8 +35,6 @@ class Seed extends SeedCommand
/**
* Execute the console command.
- *
- * @return mixed
*/
public function handle()
{
diff --git a/src/Commands/TenantDump.php b/src/Commands/TenantDump.php
new file mode 100644
index 00000000..9c8698c6
--- /dev/null
+++ b/src/Commands/TenantDump.php
@@ -0,0 +1,53 @@
+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/Commands/TenantList.php b/src/Commands/TenantList.php
index d01afcb9..13775676 100644
--- a/src/Commands/TenantList.php
+++ b/src/Commands/TenantList.php
@@ -25,8 +25,6 @@ class TenantList extends Command
/**
* Execute the console command.
- *
- * @return mixed
*/
public function handle()
{
diff --git a/src/Concerns/Debuggable.php b/src/Concerns/Debuggable.php
new file mode 100644
index 00000000..98697f1d
--- /dev/null
+++ b/src/Concerns/Debuggable.php
@@ -0,0 +1,70 @@
+eventLog = [];
+ $this->logMode = $mode;
+
+ return $this;
+ }
+
+ public function logMode(): LogMode
+ {
+ return $this->logMode;
+ }
+
+ public function getLog(): array
+ {
+ return $this->eventLog;
+ }
+
+ public function logEvent(TenancyEvent $event): static
+ {
+ $this->eventLog[] = ['time' => now(), 'event' => $event::class, 'tenant' => $this->tenant];
+
+ return $this;
+ }
+
+ public function dump(Closure $dump = null): static
+ {
+ $dump ??= dd(...);
+
+ // Dump the log if we were already logging in silent mode
+ // Otherwise start logging in instant mode
+ match ($this->logMode) {
+ LogMode::NONE => $this->log(LogMode::INSTANT),
+ LogMode::SILENT => $dump($this->eventLog),
+ LogMode::INSTANT => null,
+ };
+
+ return $this;
+ }
+
+ public function dd(Closure $dump = null): void
+ {
+ $dump ??= dd(...);
+
+ if ($this->logMode === LogMode::SILENT) {
+ $dump($this->eventLog);
+ } else {
+ $dump($this);
+ }
+ }
+}
diff --git a/src/Concerns/ExtendsLaravelCommand.php b/src/Concerns/ExtendsLaravelCommand.php
new file mode 100644
index 00000000..d08ad6b6
--- /dev/null
+++ b/src/Concerns/ExtendsLaravelCommand.php
@@ -0,0 +1,25 @@
+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/Concerns/HasATenantsOption.php b/src/Concerns/HasATenantsOption.php
index a2b94ac5..32d508ec 100644
--- a/src/Concerns/HasATenantsOption.php
+++ b/src/Concerns/HasATenantsOption.php
@@ -12,7 +12,7 @@ trait HasATenantsOption
protected function getOptions()
{
return array_merge([
- ['tenants', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, '', null],
+ ['tenants', null, InputOption::VALUE_IS_ARRAY|InputOption::VALUE_OPTIONAL, '', null],
], parent::getOptions());
}
diff --git a/src/Contracts/TenantDatabaseManager.php b/src/Contracts/TenantDatabaseManager.php
index aeed3dce..92801d75 100644
--- a/src/Contracts/TenantDatabaseManager.php
+++ b/src/Contracts/TenantDatabaseManager.php
@@ -20,18 +20,11 @@ interface TenantDatabaseManager
/**
* Does a database exist.
- *
- * @param string $name
- * @return bool
*/
public function databaseExists(string $name): bool;
/**
* Make a DB connection config array.
- *
- * @param array $baseConfig
- * @param string $databaseName
- * @return array
*/
public function makeConnectionConfig(array $baseConfig, string $databaseName): array;
@@ -39,9 +32,6 @@ interface TenantDatabaseManager
* Set the DB connection that should be used by the tenant database manager.
*
* @throws NoConnectionSetException
- *
- * @param string $connection
- * @return void
*/
public function setConnection(string $connection): void;
}
diff --git a/src/Database/Concerns/TenantRun.php b/src/Database/Concerns/TenantRun.php
index d9a444de..29bbedac 100644
--- a/src/Database/Concerns/TenantRun.php
+++ b/src/Database/Concerns/TenantRun.php
@@ -11,9 +11,6 @@ trait TenantRun
/**
* Run a callback in this tenant's context.
* Atomic, safely reverts to previous context.
- *
- * @param callable $callback
- * @return mixed
*/
public function run(callable $callback)
{
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/Database/Models/ImpersonationToken.php b/src/Database/Models/ImpersonationToken.php
index 4aa63252..43c536fb 100644
--- a/src/Database/Models/ImpersonationToken.php
+++ b/src/Database/Models/ImpersonationToken.php
@@ -22,10 +22,15 @@ class ImpersonationToken extends Model
use CentralConnection;
protected $guarded = [];
+
public $timestamps = false;
+
protected $primaryKey = 'token';
+
public $incrementing = false;
+
protected $table = 'tenant_user_impersonation_tokens';
+
protected $dates = [
'created_at',
];
diff --git a/src/Database/Models/Tenant.php b/src/Database/Models/Tenant.php
index 4ec685b7..f88297be 100644
--- a/src/Database/Models/Tenant.php
+++ b/src/Database/Models/Tenant.php
@@ -29,7 +29,9 @@ class Tenant extends Model implements Contracts\Tenant
Concerns\InvalidatesResolverCache;
protected $table = 'tenants';
+
protected $primaryKey = 'id';
+
protected $guarded = [];
public function getTenantKeyName(): string
diff --git a/src/DatabaseConfig.php b/src/DatabaseConfig.php
index c8280632..b3195960 100644
--- a/src/DatabaseConfig.php
+++ b/src/DatabaseConfig.php
@@ -80,8 +80,6 @@ class DatabaseConfig
/**
* Generate DB name, username & password and write them to the tenant model.
- *
- * @return void
*/
public function makeCredentials(): void
{
@@ -113,7 +111,8 @@ class DatabaseConfig
$templateConnection = config("database.connections.{$template}");
return $this->manager()->makeConnectionConfig(
- array_merge($templateConnection, $this->tenantConfig()), $this->getName()
+ array_merge($templateConnection, $this->tenantConfig()),
+ $this->getName()
);
}
diff --git a/src/Enums/LogMode.php b/src/Enums/LogMode.php
new file mode 100644
index 00000000..77d6f073
--- /dev/null
+++ b/src/Enums/LogMode.php
@@ -0,0 +1,12 @@
+route(), static::$middlewareGroup)) {
return $next($request);
}
+ if ($originalOnFail) {
+ return $originalOnFail($exception, $request, $next);
+ }
+
throw $exception;
};
}
diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php
index 48d65bb9..f96465ff 100644
--- a/src/Features/UserImpersonation.php
+++ b/src/Features/UserImpersonation.php
@@ -33,7 +33,6 @@ class UserImpersonation implements Feature
*
* @param string|ImpersonationToken $token
* @param int $ttl
- * @return RedirectResponse
*/
public static function makeResponse($token, int $ttl = null): RedirectResponse
{
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/Jobs/DeleteDomains.php b/src/Jobs/DeleteDomains.php
new file mode 100644
index 00000000..4ea92b7f
--- /dev/null
+++ b/src/Jobs/DeleteDomains.php
@@ -0,0 +1,29 @@
+tenant = $tenant;
+ }
+
+ public function handle()
+ {
+ $this->tenant->domains->each->delete();
+ }
+}
diff --git a/src/Listeners/UpdateSyncedResource.php b/src/Listeners/UpdateSyncedResource.php
index 40d4d644..9be290f0 100644
--- a/src/Listeners/UpdateSyncedResource.php
+++ b/src/Listeners/UpdateSyncedResource.php
@@ -48,8 +48,7 @@ class UpdateSyncedResource extends QueueableListener
protected function updateResourceInCentralDatabaseAndGetTenants($event, $syncedAttributes)
{
/** @var Model|SyncMaster $centralModel */
- $centralModel = $event->model->getCentralModelName()
- ::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey())
+ $centralModel = $event->model->getCentralModelName()::where($event->model->getGlobalIdentifierKeyName(), $event->model->getGlobalIdentifierKey())
->first();
// We disable events for this call, to avoid triggering this event & listener again.
diff --git a/src/Middleware/CheckTenantForMaintenanceMode.php b/src/Middleware/CheckTenantForMaintenanceMode.php
index 16a386f2..a27f5c74 100644
--- a/src/Middleware/CheckTenantForMaintenanceMode.php
+++ b/src/Middleware/CheckTenantForMaintenanceMode.php
@@ -5,9 +5,9 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware;
use Closure;
-use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
+use Symfony\Component\HttpKernel\Exception\HttpException;
class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
{
diff --git a/src/Middleware/InitializeTenancyByDomain.php b/src/Middleware/InitializeTenancyByDomain.php
index 24a1abb7..5a07112d 100644
--- a/src/Middleware/InitializeTenancyByDomain.php
+++ b/src/Middleware/InitializeTenancyByDomain.php
@@ -29,13 +29,13 @@ class InitializeTenancyByDomain extends IdentificationMiddleware
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @return mixed
*/
public function handle($request, Closure $next)
{
return $this->initializeTenancy(
- $request, $next, $request->getHost()
+ $request,
+ $next,
+ $request->getHost()
);
}
}
diff --git a/src/Middleware/InitializeTenancyByDomainOrSubdomain.php b/src/Middleware/InitializeTenancyByDomainOrSubdomain.php
index 94217bba..9b153db3 100644
--- a/src/Middleware/InitializeTenancyByDomainOrSubdomain.php
+++ b/src/Middleware/InitializeTenancyByDomainOrSubdomain.php
@@ -13,8 +13,6 @@ class InitializeTenancyByDomainOrSubdomain
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @return mixed
*/
public function handle($request, Closure $next)
{
diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php
index 6289199b..e66400c5 100644
--- a/src/Middleware/InitializeTenancyByPath.php
+++ b/src/Middleware/InitializeTenancyByPath.php
@@ -38,7 +38,9 @@ class InitializeTenancyByPath extends IdentificationMiddleware
// simply injected into some route controller action.
if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) {
return $this->initializeTenancy(
- $request, $next, $route
+ $request,
+ $next,
+ $route
);
} else {
throw new RouteIsMissingTenantParameterException;
diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php
index de75d8c5..4e1d33ff 100644
--- a/src/Middleware/InitializeTenancyByRequestData.php
+++ b/src/Middleware/InitializeTenancyByRequestData.php
@@ -36,8 +36,6 @@ class InitializeTenancyByRequestData extends IdentificationMiddleware
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @return mixed
*/
public function handle($request, Closure $next)
{
diff --git a/src/Middleware/InitializeTenancyBySubdomain.php b/src/Middleware/InitializeTenancyBySubdomain.php
index 55d76b05..76389df7 100644
--- a/src/Middleware/InitializeTenancyBySubdomain.php
+++ b/src/Middleware/InitializeTenancyBySubdomain.php
@@ -28,8 +28,6 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @return mixed
*/
public function handle($request, Closure $next)
{
diff --git a/src/Resolvers/Contracts/CachedTenantResolver.php b/src/Resolvers/Contracts/CachedTenantResolver.php
index 968ac794..e84f1fb1 100644
--- a/src/Resolvers/Contracts/CachedTenantResolver.php
+++ b/src/Resolvers/Contracts/CachedTenantResolver.php
@@ -75,7 +75,6 @@ abstract class CachedTenantResolver implements TenantResolver
/**
* Get all the arg combinations for resolve() that can be used to find this tenant.
*
- * @param Tenant $tenant
* @return array[]
*/
abstract public function getArgsForTenant(Tenant $tenant): array;
diff --git a/src/Tenancy.php b/src/Tenancy.php
index 30f138e3..439c34cb 100644
--- a/src/Tenancy.php
+++ b/src/Tenancy.php
@@ -7,13 +7,14 @@ namespace Stancl\Tenancy;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Traits\Macroable;
+use Stancl\Tenancy\Concerns\Debuggable;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedById;
class Tenancy
{
- use Macroable;
+ use Macroable, Debuggable;
/** @var Tenant|Model|null */
public $tenant;
@@ -27,7 +28,6 @@ class Tenancy
/**
* Initializes the tenant.
* @param Tenant|int|string $tenant
- * @return void
*/
public function initialize($tenant): void
{
@@ -106,9 +106,6 @@ class Tenancy
/**
* Run a callback in the central context.
* Atomic, safely reverts to previous context.
- *
- * @param callable $callback
- * @return mixed
*/
public function central(callable $callback)
{
@@ -132,7 +129,6 @@ class Tenancy
* More performant than running $tenant->run() one by one.
*
* @param Tenant[]|\Traversable|string[]|null $tenants
- * @param callable $callback
* @return void
*/
public function runForMultiple($tenants, callable $callback)
diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php
index 6091504e..0d065dbc 100644
--- a/src/TenancyServiceProvider.php
+++ b/src/TenancyServiceProvider.php
@@ -5,18 +5,19 @@ declare(strict_types=1);
namespace Stancl\Tenancy;
use Illuminate\Cache\CacheManager;
+use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Contracts\Domain;
use Stancl\Tenancy\Contracts\Tenant;
+use Stancl\Tenancy\Enums\LogMode;
+use Stancl\Tenancy\Events\Contracts\TenancyEvent;
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
class TenancyServiceProvider extends ServiceProvider
{
/**
* Register services.
- *
- * @return void
*/
public function register(): void
{
@@ -76,8 +77,6 @@ class TenancyServiceProvider extends ServiceProvider
/**
* Bootstrap services.
- *
- * @return void
*/
public function boot(): void
{
@@ -88,6 +87,7 @@ class TenancyServiceProvider extends ServiceProvider
Commands\Migrate::class,
Commands\Rollback::class,
Commands\TenantList::class,
+ Commands\TenantDump::class,
Commands\MigrateFresh::class,
Commands\Down::class,
Commands\Up::class,
@@ -117,6 +117,18 @@ class TenancyServiceProvider extends ServiceProvider
$this->loadRoutesFrom(__DIR__ . '/../assets/routes.php');
}
+ Event::listen('Stancl\\Tenancy\\Events\\*', function (string $name, array $data) {
+ $event = $data[0];
+
+ if ($event instanceof TenancyEvent) {
+ match (tenancy()->logMode()) {
+ LogMode::SILENT => tenancy()->logEvent($event),
+ LogMode::INSTANT => dump($event), // todo0 perhaps still log
+ default => null,
+ };
+ }
+ });
+
$this->app->singleton('globalUrl', function ($app) {
if ($app->bound(FilesystemTenancyBootstrapper::class)) {
$instance = clone $app['url'];
diff --git a/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php b/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php
new file mode 100644
index 00000000..0bc34623
--- /dev/null
+++ b/src/TenantDatabaseManagers/MicrosoftSQLDatabaseManager.php
@@ -0,0 +1,57 @@
+connection === null) {
+ throw new NoConnectionSetException(static::class);
+ }
+
+ return DB::connection($this->connection);
+ }
+
+ public function setConnection(string $connection): void
+ {
+ $this->connection = $connection;
+ }
+
+ public function createDatabase(TenantWithDatabase $tenant): bool
+ {
+ $database = $tenant->database()->getName();
+ $charset = $this->database()->getConfig('charset');
+ $collation = $this->database()->getConfig('collation');
+
+ return $this->database()->statement("CREATE DATABASE [{$database}]");
+ }
+
+ public function deleteDatabase(TenantWithDatabase $tenant): bool
+ {
+ return $this->database()->statement("DROP DATABASE [{$tenant->database()->getName()}]");
+ }
+
+ public function databaseExists(string $name): bool
+ {
+ return (bool) $this->database()->select("SELECT name FROM master.sys.databases WHERE name = '$name'");
+ }
+
+ public function makeConnectionConfig(array $baseConfig, string $databaseName): array
+ {
+ $baseConfig['database'] = $databaseName;
+
+ return $baseConfig;
+ }
+}
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/test b/test
index 49535a7a..d8de021e 100755
--- a/test
+++ b/test
@@ -1,3 +1,3 @@
#!/bin/bash
-docker-compose exec -T test vendor/bin/phpunit "$@"
+docker-compose exec -T test vendor/bin/pest "$@"
diff --git a/tests/AutomaticModeTest.php b/tests/AutomaticModeTest.php
index 714092c3..ab484ccf 100644
--- a/tests/AutomaticModeTest.php
+++ b/tests/AutomaticModeTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded;
@@ -12,113 +10,99 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class AutomaticModeTest extends TestCase
+beforeEach(function () {
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+});
+
+test('context is switched when tenancy is initialized', function () {
+ contextIsSwitchedWhenTenancyInitialized();
+});
+
+test('context is reverted when tenancy is ended', function () {
+ contextIsSwitchedWhenTenancyInitialized();
+
+ tenancy()->end();
+
+ expect(app('tenancy_ended'))->toBe(true);
+});
+
+test('context is switched when tenancy is reinitialized', function () {
+ config(['tenancy.bootstrappers' => [
+ MyBootstrapper::class,
+ ]]);
+
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ ]);
+
+ tenancy()->initialize($tenant);
+
+ expect(app('tenancy_initialized_for_tenant'))->toBe('acme');
+
+ $tenant2 = Tenant::create([
+ 'id' => 'foobar',
+ ]);
+
+ tenancy()->initialize($tenant2);
+
+ expect(app('tenancy_initialized_for_tenant'))->toBe('foobar');
+});
+
+test('central helper runs callbacks in the central state', function () {
+ tenancy()->initialize($tenant = Tenant::create());
+
+ tenancy()->central(function () {
+ expect(tenant())->toBe(null);
+ });
+
+ expect(tenant())->toBe($tenant);
+});
+
+test('central helper returns the value from the callback', function () {
+ tenancy()->initialize(Tenant::create());
+
+ pest()->assertSame('foo', tenancy()->central(function () {
+ return 'foo';
+ }));
+});
+
+test('central helper reverts back to tenant context', function () {
+ tenancy()->initialize($tenant = Tenant::create());
+
+ tenancy()->central(function () {
+ //
+ });
+
+ expect(tenant())->toBe($tenant);
+});
+
+test('central helper doesnt change tenancy state when called in central context', function () {
+ expect(tenancy()->initialized)->toBeFalse();
+ expect(tenant())->toBeNull();
+
+ tenancy()->central(function () {
+ //
+ });
+
+ expect(tenancy()->initialized)->toBeFalse();
+ expect(tenant())->toBeNull();
+});
+
+// todo@tests
+function contextIsSwitchedWhenTenancyInitialized()
{
- public function setUp(): void
- {
- parent::setUp();
+ config(['tenancy.bootstrappers' => [
+ MyBootstrapper::class,
+ ]]);
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
- }
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ ]);
- /** @test */
- public function context_is_switched_when_tenancy_is_initialized()
- {
- config(['tenancy.bootstrappers' => [
- MyBootstrapper::class,
- ]]);
+ tenancy()->initialize($tenant);
- $tenant = Tenant::create([
- 'id' => 'acme',
- ]);
-
- tenancy()->initialize($tenant);
-
- $this->assertSame('acme', app('tenancy_initialized_for_tenant'));
- }
-
- /** @test */
- public function context_is_reverted_when_tenancy_is_ended()
- {
- $this->context_is_switched_when_tenancy_is_initialized();
-
- tenancy()->end();
-
- $this->assertSame(true, app('tenancy_ended'));
- }
-
- /** @test */
- public function context_is_switched_when_tenancy_is_reinitialized()
- {
- config(['tenancy.bootstrappers' => [
- MyBootstrapper::class,
- ]]);
-
- $tenant = Tenant::create([
- 'id' => 'acme',
- ]);
-
- tenancy()->initialize($tenant);
-
- $this->assertSame('acme', app('tenancy_initialized_for_tenant'));
-
- $tenant2 = Tenant::create([
- 'id' => 'foobar',
- ]);
-
- tenancy()->initialize($tenant2);
-
- $this->assertSame('foobar', app('tenancy_initialized_for_tenant'));
- }
-
- /** @test */
- public function central_helper_runs_callbacks_in_the_central_state()
- {
- tenancy()->initialize($tenant = Tenant::create());
-
- tenancy()->central(function () {
- $this->assertSame(null, tenant());
- });
-
- $this->assertSame($tenant, tenant());
- }
-
- /** @test */
- public function central_helper_returns_the_value_from_the_callback()
- {
- tenancy()->initialize(Tenant::create());
-
- $this->assertSame('foo', tenancy()->central(function () {
- return 'foo';
- }));
- }
-
- /** @test */
- public function central_helper_reverts_back_to_tenant_context()
- {
- tenancy()->initialize($tenant = Tenant::create());
-
- tenancy()->central(function () {
- //
- });
-
- $this->assertSame($tenant, tenant());
- }
-
- /** @test */
- public function central_helper_doesnt_change_tenancy_state_when_called_in_central_context()
- {
- $this->assertFalse(tenancy()->initialized);
- $this->assertNull(tenant());
-
- tenancy()->central(function () {
- //
- });
-
- $this->assertFalse(tenancy()->initialized);
- $this->assertNull(tenant());
- }
+ expect(app('tenancy_initialized_for_tenant'))->toBe('acme');
}
class MyBootstrapper implements TenancyBootstrapper
diff --git a/tests/BootstrapperTest.php b/tests/BootstrapperTest.php
index 29aa7dc9..96afbc83 100644
--- a/tests/BootstrapperTest.php
+++ b/tests/BootstrapperTest.php
@@ -2,11 +2,7 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Filesystem\FilesystemAdapter;
-use ReflectionObject;
-use ReflectionProperty;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Stancl\JobPipeline\JobPipeline;
@@ -26,202 +22,186 @@ use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
-class BootstrapperTest extends TestCase
+beforeEach(function () {
+ $this->mockConsoleOutput = false;
+
+ Event::listen(
+ TenantCreated::class,
+ JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener()
+ );
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+});
+
+test('database data is separated', function () {
+ config(['tenancy.bootstrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ]]);
+
+ $tenant1 = Tenant::create();
+ $tenant2 = Tenant::create();
+
+ pest()->artisan('tenants:migrate');
+
+ tenancy()->initialize($tenant1);
+
+ // Create Foo user
+ DB::table('users')->insert(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
+ expect(DB::table('users')->get())->toHaveCount(1);
+
+ tenancy()->initialize($tenant2);
+
+ // Assert Foo user is not in this DB
+ expect(DB::table('users')->get())->toHaveCount(0);
+ // Create Bar user
+ DB::table('users')->insert(['name' => 'Bar', 'email' => 'bar@bar.com', 'password' => 'secret']);
+ expect(DB::table('users')->get())->toHaveCount(1);
+
+ tenancy()->initialize($tenant1);
+
+ // Assert Bar user is not in this DB
+ expect(DB::table('users')->get())->toHaveCount(1);
+ expect(DB::table('users')->first()->name)->toBe('Foo');
+});
+
+test('cache data is separated', function () {
+ config([
+ 'tenancy.bootstrappers' => [
+ CacheTenancyBootstrapper::class,
+ ],
+ 'cache.default' => 'redis',
+ ]);
+
+ $tenant1 = Tenant::create();
+ $tenant2 = Tenant::create();
+
+ cache()->set('foo', 'central');
+ expect(Cache::get('foo'))->toBe('central');
+
+ tenancy()->initialize($tenant1);
+
+ // Assert central cache doesn't leak to tenant context
+ expect(Cache::has('foo'))->toBeFalse();
+
+ cache()->set('foo', 'bar');
+ expect(Cache::get('foo'))->toBe('bar');
+
+ tenancy()->initialize($tenant2);
+
+ // Assert one tenant's data doesn't leak to another tenant
+ expect(Cache::has('foo'))->toBeFalse();
+
+ cache()->set('foo', 'xyz');
+ expect(Cache::get('foo'))->toBe('xyz');
+
+ tenancy()->initialize($tenant1);
+
+ // Asset data didn't leak to original tenant
+ expect(Cache::get('foo'))->toBe('bar');
+
+ tenancy()->end();
+
+ // Asset central is still the same
+ expect(Cache::get('foo'))->toBe('central');
+});
+
+test('redis data is separated', function () {
+ config(['tenancy.bootstrappers' => [
+ RedisTenancyBootstrapper::class,
+ ]]);
+
+ $tenant1 = Tenant::create();
+ $tenant2 = Tenant::create();
+
+ tenancy()->initialize($tenant1);
+ Redis::set('foo', 'bar');
+ expect(Redis::get('foo'))->toBe('bar');
+
+ tenancy()->initialize($tenant2);
+ expect(Redis::get('foo'))->toBe(null);
+ Redis::set('foo', 'xyz');
+ Redis::set('abc', 'def');
+ expect(Redis::get('foo'))->toBe('xyz');
+ expect(Redis::get('abc'))->toBe('def');
+
+ tenancy()->initialize($tenant1);
+ expect(Redis::get('foo'))->toBe('bar');
+ expect(Redis::get('abc'))->toBe(null);
+
+ $tenant3 = Tenant::create();
+ tenancy()->initialize($tenant3);
+ expect(Redis::get('foo'))->toBe(null);
+ expect(Redis::get('abc'))->toBe(null);
+});
+
+test('filesystem data is separated', function () {
+ config(['tenancy.bootstrappers' => [
+ FilesystemTenancyBootstrapper::class,
+ ]]);
+
+ $old_storage_path = storage_path();
+ $old_storage_facade_roots = [];
+ foreach (config('tenancy.filesystem.disks') as $disk) {
+ $old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root");
+ }
+
+ $tenant1 = Tenant::create();
+ $tenant2 = Tenant::create();
+
+ tenancy()->initialize($tenant1);
+
+ Storage::disk('public')->put('foo', 'bar');
+ expect(Storage::disk('public')->get('foo'))->toBe('bar');
+
+ tenancy()->initialize($tenant2);
+ expect(Storage::disk('public')->exists('foo'))->toBeFalse();
+ Storage::disk('public')->put('foo', 'xyz');
+ Storage::disk('public')->put('abc', 'def');
+ expect(Storage::disk('public')->get('foo'))->toBe('xyz');
+ expect(Storage::disk('public')->get('abc'))->toBe('def');
+
+ tenancy()->initialize($tenant1);
+ expect(Storage::disk('public')->get('foo'))->toBe('bar');
+ expect(Storage::disk('public')->exists('abc'))->toBeFalse();
+
+ $tenant3 = Tenant::create();
+ tenancy()->initialize($tenant3);
+ expect(Storage::disk('public')->exists('foo'))->toBeFalse();
+ expect(Storage::disk('public')->exists('abc'))->toBeFalse();
+
+ $expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base
+
+ // Check that disk prefixes respect the root_override logic
+ expect(getDiskPrefix('local'))->toBe($expected_storage_path . '/app/');
+ expect(getDiskPrefix('public'))->toBe($expected_storage_path . '/app/public/');
+ pest()->assertSame('tenant' . tenant('id') . '/', getDiskPrefix('s3'), '/');
+
+ // Check suffixing logic
+ $new_storage_path = storage_path();
+ expect($new_storage_path)->toEqual($expected_storage_path);
+});
+
+function getDiskPrefix(string $disk): string
{
- public $mockConsoleOutput = false;
+ /** @var FilesystemAdapter $disk */
+ $disk = Storage::disk($disk);
+ $adapter = $disk->getAdapter();
- public function setUp(): void
- {
- parent::setUp();
-
- Event::listen(
- TenantCreated::class,
- JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener()
- );
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+ if (! Str::startsWith(app()->version(), '9.')) {
+ return $adapter->getPathPrefix();
}
- /** @test */
- public function database_data_is_separated()
- {
- config(['tenancy.bootstrappers' => [
- DatabaseTenancyBootstrapper::class,
- ]]);
+ $prefixer = (new ReflectionObject($adapter))->getProperty('prefixer');
+ $prefixer->setAccessible(true);
- $tenant1 = Tenant::create();
- $tenant2 = Tenant::create();
+ // reflection -> instance
+ $prefixer = $prefixer->getValue($adapter);
- $this->artisan('tenants:migrate');
+ $prefix = (new ReflectionProperty($prefixer, 'prefix'));
+ $prefix->setAccessible(true);
- tenancy()->initialize($tenant1);
-
- // Create Foo user
- DB::table('users')->insert(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
- $this->assertCount(1, DB::table('users')->get());
-
- tenancy()->initialize($tenant2);
-
- // Assert Foo user is not in this DB
- $this->assertCount(0, DB::table('users')->get());
- // Create Bar user
- DB::table('users')->insert(['name' => 'Bar', 'email' => 'bar@bar.com', 'password' => 'secret']);
- $this->assertCount(1, DB::table('users')->get());
-
- tenancy()->initialize($tenant1);
-
- // Assert Bar user is not in this DB
- $this->assertCount(1, DB::table('users')->get());
- $this->assertSame('Foo', DB::table('users')->first()->name);
- }
-
- /** @test */
- public function cache_data_is_separated()
- {
- config([
- 'tenancy.bootstrappers' => [
- CacheTenancyBootstrapper::class,
- ],
- 'cache.default' => 'redis',
- ]);
-
- $tenant1 = Tenant::create();
- $tenant2 = Tenant::create();
-
- cache()->set('foo', 'central');
- $this->assertSame('central', Cache::get('foo'));
-
- tenancy()->initialize($tenant1);
-
- // Assert central cache doesn't leak to tenant context
- $this->assertFalse(Cache::has('foo'));
-
- cache()->set('foo', 'bar');
- $this->assertSame('bar', Cache::get('foo'));
-
- tenancy()->initialize($tenant2);
-
- // Assert one tenant's data doesn't leak to another tenant
- $this->assertFalse(Cache::has('foo'));
-
- cache()->set('foo', 'xyz');
- $this->assertSame('xyz', Cache::get('foo'));
-
- tenancy()->initialize($tenant1);
-
- // Asset data didn't leak to original tenant
- $this->assertSame('bar', Cache::get('foo'));
-
- tenancy()->end();
-
- // Asset central is still the same
- $this->assertSame('central', Cache::get('foo'));
- }
-
- /** @test */
- public function redis_data_is_separated()
- {
- config(['tenancy.bootstrappers' => [
- RedisTenancyBootstrapper::class,
- ]]);
-
- $tenant1 = Tenant::create();
- $tenant2 = Tenant::create();
-
- tenancy()->initialize($tenant1);
- Redis::set('foo', 'bar');
- $this->assertSame('bar', Redis::get('foo'));
-
- tenancy()->initialize($tenant2);
- $this->assertSame(null, Redis::get('foo'));
- Redis::set('foo', 'xyz');
- Redis::set('abc', 'def');
- $this->assertSame('xyz', Redis::get('foo'));
- $this->assertSame('def', Redis::get('abc'));
-
- tenancy()->initialize($tenant1);
- $this->assertSame('bar', Redis::get('foo'));
- $this->assertSame(null, Redis::get('abc'));
-
- $tenant3 = Tenant::create();
- tenancy()->initialize($tenant3);
- $this->assertSame(null, Redis::get('foo'));
- $this->assertSame(null, Redis::get('abc'));
- }
-
- /** @test */
- public function filesystem_data_is_separated()
- {
- config(['tenancy.bootstrappers' => [
- FilesystemTenancyBootstrapper::class,
- ]]);
-
- $old_storage_path = storage_path();
- $old_storage_facade_roots = [];
- foreach (config('tenancy.filesystem.disks') as $disk) {
- $old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root");
- }
-
- $tenant1 = Tenant::create();
- $tenant2 = Tenant::create();
-
- tenancy()->initialize($tenant1);
-
- Storage::disk('public')->put('foo', 'bar');
- $this->assertSame('bar', Storage::disk('public')->get('foo'));
-
- tenancy()->initialize($tenant2);
- $this->assertFalse(Storage::disk('public')->exists('foo'));
- Storage::disk('public')->put('foo', 'xyz');
- Storage::disk('public')->put('abc', 'def');
- $this->assertSame('xyz', Storage::disk('public')->get('foo'));
- $this->assertSame('def', Storage::disk('public')->get('abc'));
-
- tenancy()->initialize($tenant1);
- $this->assertSame('bar', Storage::disk('public')->get('foo'));
- $this->assertFalse(Storage::disk('public')->exists('abc'));
-
- $tenant3 = Tenant::create();
- tenancy()->initialize($tenant3);
- $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') . '/', ltrim($this->getDiskPrefix('s3'), '/'));
-
- // Check suffixing logic
- $new_storage_path = storage_path();
- $this->assertEquals($expected_storage_path, $new_storage_path);
- }
-
- protected function getDiskPrefix(string $disk): string
- {
- /** @var FilesystemAdapter $disk */
- $disk = Storage::disk($disk);
- $adapter = $disk->getAdapter();
-
- 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
+ return $prefix->getValue($prefixer);
}
diff --git a/tests/CacheManagerTest.php b/tests/CacheManagerTest.php
index a54aaa67..03580fe1 100644
--- a/tests/CacheManagerTest.php
+++ b/tests/CacheManagerTest.php
@@ -2,136 +2,110 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class CacheManagerTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
+beforeEach(function () {
+ config(['tenancy.bootstrappers' => [
+ CacheTenancyBootstrapper::class,
+ ]]);
- config(['tenancy.bootstrappers' => [
- CacheTenancyBootstrapper::class,
- ]]);
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+});
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- }
+test('default tag is automatically applied', function () {
+ tenancy()->initialize(Tenant::create());
- /** @test */
- public function default_tag_is_automatically_applied()
- {
- tenancy()->initialize(Tenant::create());
+ pest()->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames());
+});
- $this->assertArrayIsSubset([config('tenancy.cache.tag_base') . tenant('id')], cache()->tags('foo')->getTags()->getNames());
- }
+test('tags are merged when array is passed', function () {
+ tenancy()->initialize(Tenant::create());
- /** @test */
- public function tags_are_merged_when_array_is_passed()
- {
- tenancy()->initialize(Tenant::create());
+ $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
+ expect(cache()->tags(['foo', 'bar'])->getTags()->getNames())->toEqual($expected);
+});
- $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo', 'bar'];
- $this->assertEquals($expected, cache()->tags(['foo', 'bar'])->getTags()->getNames());
- }
+test('tags are merged when string is passed', function () {
+ tenancy()->initialize(Tenant::create());
- /** @test */
- public function tags_are_merged_when_string_is_passed()
- {
- tenancy()->initialize(Tenant::create());
+ $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo'];
+ expect(cache()->tags('foo')->getTags()->getNames())->toEqual($expected);
+});
- $expected = [config('tenancy.cache.tag_base') . tenant('id'), 'foo'];
- $this->assertEquals($expected, cache()->tags('foo')->getTags()->getNames());
- }
+test('exception is thrown when zero arguments are passed to tags method', function () {
+ tenancy()->initialize(Tenant::create());
- /** @test */
- public function exception_is_thrown_when_zero_arguments_are_passed_to_tags_method()
- {
- tenancy()->initialize(Tenant::create());
+ pest()->expectException(\Exception::class);
+ cache()->tags();
+});
- $this->expectException(\Exception::class);
- cache()->tags();
- }
+test('exception is thrown when more than one argument is passed to tags method', function () {
+ tenancy()->initialize(Tenant::create());
- /** @test */
- public function exception_is_thrown_when_more_than_one_argument_is_passed_to_tags_method()
- {
- tenancy()->initialize(Tenant::create());
+ pest()->expectException(\Exception::class);
+ cache()->tags(1, 2);
+});
- $this->expectException(\Exception::class);
- cache()->tags(1, 2);
- }
+test('tags separate cache well enough', function () {
+ $tenant1 = Tenant::create();
+ tenancy()->initialize($tenant1);
- /** @test */
- public function tags_separate_cache_well_enough()
- {
- $tenant1 = Tenant::create();
- tenancy()->initialize($tenant1);
+ cache()->put('foo', 'bar', 1);
+ expect(cache()->get('foo'))->toBe('bar');
- cache()->put('foo', 'bar', 1);
- $this->assertSame('bar', cache()->get('foo'));
+ $tenant2 = Tenant::create();
+ tenancy()->initialize($tenant2);
- $tenant2 = Tenant::create();
- tenancy()->initialize($tenant2);
+ pest()->assertNotSame('bar', cache()->get('foo'));
- $this->assertNotSame('bar', cache()->get('foo'));
+ cache()->put('foo', 'xyz', 1);
+ expect(cache()->get('foo'))->toBe('xyz');
+});
- cache()->put('foo', 'xyz', 1);
- $this->assertSame('xyz', cache()->get('foo'));
- }
+test('invoking the cache helper works', function () {
+ $tenant1 = Tenant::create();
+ tenancy()->initialize($tenant1);
- /** @test */
- public function invoking_the_cache_helper_works()
- {
- $tenant1 = Tenant::create();
- tenancy()->initialize($tenant1);
+ cache(['foo' => 'bar'], 1);
+ expect(cache('foo'))->toBe('bar');
- cache(['foo' => 'bar'], 1);
- $this->assertSame('bar', cache('foo'));
+ $tenant2 = Tenant::create();
+ tenancy()->initialize($tenant2);
- $tenant2 = Tenant::create();
- tenancy()->initialize($tenant2);
+ pest()->assertNotSame('bar', cache('foo'));
- $this->assertNotSame('bar', cache('foo'));
+ cache(['foo' => 'xyz'], 1);
+ expect(cache('foo'))->toBe('xyz');
+});
- cache(['foo' => 'xyz'], 1);
- $this->assertSame('xyz', cache('foo'));
- }
+test('cache is persisted', function () {
+ $tenant1 = Tenant::create();
+ tenancy()->initialize($tenant1);
- /** @test */
- public function cache_is_persisted()
- {
- $tenant1 = Tenant::create();
- tenancy()->initialize($tenant1);
+ cache(['foo' => 'bar'], 10);
+ expect(cache('foo'))->toBe('bar');
- cache(['foo' => 'bar'], 10);
- $this->assertSame('bar', cache('foo'));
+ tenancy()->end();
- tenancy()->end();
+ tenancy()->initialize($tenant1);
+ expect(cache('foo'))->toBe('bar');
+});
- tenancy()->initialize($tenant1);
- $this->assertSame('bar', cache('foo'));
- }
+test('cache is persisted when reidentification is used', function () {
+ $tenant1 = Tenant::create();
+ $tenant2 = Tenant::create();
+ tenancy()->initialize($tenant1);
- /** @test */
- public function cache_is_persisted_when_reidentification_is_used()
- {
- $tenant1 = Tenant::create();
- $tenant2 = Tenant::create();
- tenancy()->initialize($tenant1);
+ cache(['foo' => 'bar'], 10);
+ expect(cache('foo'))->toBe('bar');
- cache(['foo' => 'bar'], 10);
- $this->assertSame('bar', cache('foo'));
+ tenancy()->initialize($tenant2);
+ tenancy()->end();
- tenancy()->initialize($tenant2);
- tenancy()->end();
-
- tenancy()->initialize($tenant1);
- $this->assertSame('bar', cache('foo'));
- }
-}
+ tenancy()->initialize($tenant1);
+ expect(cache('foo'))->toBe('bar');
+});
diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php
index e7eb52d3..d71375be 100644
--- a/tests/CachedTenantResolverTest.php
+++ b/tests/CachedTenantResolverTest.php
@@ -2,111 +2,94 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class CachedTenantResolverTest extends TestCase
-{
- public function tearDown(): void
- {
- DomainTenantResolver::$shouldCache = false;
+afterEach(function () {
+ DomainTenantResolver::$shouldCache = false;
+});
- parent::tearDown();
- }
+test('tenants can be resolved using the cached resolver', function () {
+ $tenant = Tenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'acme',
+ ]);
- /** @test */
- public function tenants_can_be_resolved_using_the_cached_resolver()
- {
- $tenant = Tenant::create();
- $tenant->domains()->create([
- 'domain' => 'acme',
- ]);
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue()->toBeTrue();
+});
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- }
+test('the underlying resolver is not touched when using the cached resolver', function () {
+ $tenant = Tenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'acme',
+ ]);
- /** @test */
- public function the_underlying_resolver_is_not_touched_when_using_the_cached_resolver()
- {
- $tenant = Tenant::create();
- $tenant->domains()->create([
- 'domain' => 'acme',
- ]);
+ DB::enableQueryLog();
- DB::enableQueryLog();
+ DomainTenantResolver::$shouldCache = false;
- DomainTenantResolver::$shouldCache = false;
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ DB::flushQueryLog();
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ pest()->assertNotEmpty(DB::getQueryLog()); // not empty
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- DB::flushQueryLog();
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- $this->assertNotEmpty(DB::getQueryLog()); // not empty
+ DomainTenantResolver::$shouldCache = true;
- DomainTenantResolver::$shouldCache = true;
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ DB::flushQueryLog();
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ expect(DB::getQueryLog())->toBeEmpty(); // empty
+});
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- DB::flushQueryLog();
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- $this->assertEmpty(DB::getQueryLog()); // empty
- }
+test('cache is invalidated when the tenant is updated', function () {
+ $tenant = Tenant::create();
+ $tenant->createDomain([
+ 'domain' => 'acme',
+ ]);
- /** @test */
- public function cache_is_invalidated_when_the_tenant_is_updated()
- {
- $tenant = Tenant::create();
- $tenant->createDomain([
- 'domain' => 'acme',
- ]);
+ DB::enableQueryLog();
- DB::enableQueryLog();
+ DomainTenantResolver::$shouldCache = true;
- DomainTenantResolver::$shouldCache = true;
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ DB::flushQueryLog();
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ expect(DB::getQueryLog())->toBeEmpty(); // empty
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- DB::flushQueryLog();
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- $this->assertEmpty(DB::getQueryLog()); // empty
+ $tenant->update([
+ 'foo' => 'bar',
+ ]);
- $tenant->update([
- 'foo' => 'bar',
- ]);
+ DB::flushQueryLog();
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ pest()->assertNotEmpty(DB::getQueryLog()); // not empty
+});
- DB::flushQueryLog();
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- $this->assertNotEmpty(DB::getQueryLog()); // not empty
- }
+test('cache is invalidated when a tenants domain is changed', function () {
+ $tenant = Tenant::create();
+ $tenant->createDomain([
+ 'domain' => 'acme',
+ ]);
- /** @test */
- public function cache_is_invalidated_when_a_tenants_domain_is_changed()
- {
- $tenant = Tenant::create();
- $tenant->createDomain([
- 'domain' => 'acme',
- ]);
+ DB::enableQueryLog();
- DB::enableQueryLog();
+ DomainTenantResolver::$shouldCache = true;
- DomainTenantResolver::$shouldCache = true;
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ DB::flushQueryLog();
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ expect(DB::getQueryLog())->toBeEmpty(); // empty
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- DB::flushQueryLog();
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- $this->assertEmpty(DB::getQueryLog()); // empty
+ $tenant->createDomain([
+ 'domain' => 'bar',
+ ]);
- $tenant->createDomain([
- 'domain' => 'bar',
- ]);
+ DB::flushQueryLog();
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toBeTrue();
+ pest()->assertNotEmpty(DB::getQueryLog()); // not empty
- DB::flushQueryLog();
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme')));
- $this->assertNotEmpty(DB::getQueryLog()); // not empty
-
- DB::flushQueryLog();
- $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('bar')));
- $this->assertNotEmpty(DB::getQueryLog()); // not empty
- }
-}
+ DB::flushQueryLog();
+ expect($tenant->is(app(DomainTenantResolver::class)->resolve('bar')))->toBeTrue();
+ pest()->assertNotEmpty(DB::getQueryLog()); // not empty
+});
diff --git a/tests/CombinedDomainAndSubdomainIdentificationTest.php b/tests/CombinedDomainAndSubdomainIdentificationTest.php
index 6712458c..4e3c190b 100644
--- a/tests/CombinedDomainAndSubdomainIdentificationTest.php
+++ b/tests/CombinedDomainAndSubdomainIdentificationTest.php
@@ -2,76 +2,64 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Concerns\HasDomains;
-use Stancl\Tenancy\Database\Models;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
+use Stancl\Tenancy\Database\Models;
-class CombinedDomainAndSubdomainIdentificationTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
-
- Route::group([
- 'middleware' => InitializeTenancyByDomainOrSubdomain::class,
- ], function () {
- Route::get('/foo/{a}/{b}', function ($a, $b) {
- return "$a + $b";
- });
+beforeEach(function () {
+ Route::group([
+ 'middleware' => InitializeTenancyByDomainOrSubdomain::class,
+ ], function () {
+ Route::get('/foo/{a}/{b}', function ($a, $b) {
+ return "$a + $b";
});
+ });
- config(['tenancy.tenant_model' => CombinedTenant::class]);
- }
+ config(['tenancy.tenant_model' => CombinedTenant::class]);
+});
- /** @test */
- public function tenant_can_be_identified_by_subdomain()
- {
- config(['tenancy.central_domains' => ['localhost']]);
+test('tenant can be identified by subdomain', function () {
+ config(['tenancy.central_domains' => ['localhost']]);
- $tenant = CombinedTenant::create([
- 'id' => 'acme',
- ]);
+ $tenant = CombinedTenant::create([
+ 'id' => 'acme',
+ ]);
- $tenant->domains()->create([
- 'domain' => 'foo',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'foo',
+ ]);
- $this->assertFalse(tenancy()->initialized);
+ expect(tenancy()->initialized)->toBeFalse();
- $this
- ->get('http://foo.localhost/foo/abc/xyz')
- ->assertSee('abc + xyz');
+ pest()
+ ->get('http://foo.localhost/foo/abc/xyz')
+ ->assertSee('abc + xyz');
- $this->assertTrue(tenancy()->initialized);
- $this->assertSame('acme', tenant('id'));
- }
+ expect(tenancy()->initialized)->toBeTrue();
+ expect(tenant('id'))->toBe('acme');
+});
- /** @test */
- public function tenant_can_be_identified_by_domain()
- {
- config(['tenancy.central_domains' => []]);
+test('tenant can be identified by domain', function () {
+ config(['tenancy.central_domains' => []]);
- $tenant = CombinedTenant::create([
- 'id' => 'acme',
- ]);
+ $tenant = CombinedTenant::create([
+ 'id' => 'acme',
+ ]);
- $tenant->domains()->create([
- 'domain' => 'foobar.localhost',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'foobar.localhost',
+ ]);
- $this->assertFalse(tenancy()->initialized);
+ expect(tenancy()->initialized)->toBeFalse();
- $this
- ->get('http://foobar.localhost/foo/abc/xyz')
- ->assertSee('abc + xyz');
+ pest()
+ ->get('http://foobar.localhost/foo/abc/xyz')
+ ->assertSee('abc + xyz');
- $this->assertTrue(tenancy()->initialized);
- $this->assertSame('acme', tenant('id'));
- }
-}
+ expect(tenancy()->initialized)->toBeTrue();
+ expect(tenant('id'))->toBe('acme');
+});
class CombinedTenant extends Models\Tenant
{
diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php
index d7da0cab..5172d752 100644
--- a/tests/CommandsTest.php
+++ b/tests/CommandsTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
@@ -19,187 +17,196 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\ExampleSeeder;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class CommandsTest extends TestCase
+beforeEach(function () {
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+
+ config(['tenancy.bootstrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ]]);
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+});
+
+afterEach(function () {
+ // Cleanup tenancy config cache
+ if (file_exists(base_path('config/tenancy.php'))) {
+ unlink(base_path('config/tenancy.php'));
+ }
+});
+
+test('migrate command doesnt change the db connection', function () {
+ expect(Schema::hasTable('users'))->toBeFalse();
+
+ $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
+ Artisan::call('tenants:migrate');
+ $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
+
+ expect(Schema::hasTable('users'))->toBeFalse();
+ expect($new_connection_name)->toEqual($old_connection_name);
+ pest()->assertNotEquals('tenant', $new_connection_name);
+});
+
+test('migrate command works without options', function () {
+ $tenant = Tenant::create();
+
+ expect(Schema::hasTable('users'))->toBeFalse();
+ Artisan::call('tenants:migrate');
+ expect(Schema::hasTable('users'))->toBeFalse();
+
+ tenancy()->initialize($tenant);
+
+ expect(Schema::hasTable('users'))->toBeTrue();
+});
+
+test('migrate command works with tenants option', function () {
+ $tenant = Tenant::create();
+ Artisan::call('tenants:migrate', [
+ '--tenants' => [$tenant['id']],
+ ]);
+
+ expect(Schema::hasTable('users'))->toBeFalse();
+ tenancy()->initialize(Tenant::create());
+ expect(Schema::hasTable('users'))->toBeFalse();
+
+ tenancy()->initialize($tenant);
+ expect(Schema::hasTable('users'))->toBeTrue();
+});
+
+test('migrate command loads schema state', function () {
+ $tenant = Tenant::create();
+
+ expect(Schema::hasTable('schema_users'))->toBeFalse();
+ expect(Schema::hasTable('users'))->toBeFalse();
+
+ Artisan::call('tenants:migrate --schema-path="tests/Etc/tenant-schema.dump"');
+
+ expect(Schema::hasTable('schema_users'))->toBeFalse();
+ expect(Schema::hasTable('users'))->toBeFalse();
+
+ tenancy()->initialize($tenant);
+
+ // Check for both tables to see if missing migrations also get executed
+ expect(Schema::hasTable('schema_users'))->toBeTrue();
+ expect(Schema::hasTable('users'))->toBeTrue();
+});
+
+test('dump command works', function () {
+ $tenant = Tenant::create();
+ Artisan::call('tenants:migrate');
+
+ tenancy()->initialize($tenant);
+
+ Artisan::call('tenants:dump --path="tests/Etc/tenant-schema-test.dump"');
+ expect('tests/Etc/tenant-schema-test.dump')->toBeFile();
+});
+
+test('rollback command works', function () {
+ $tenant = Tenant::create();
+ Artisan::call('tenants:migrate');
+ expect(Schema::hasTable('users'))->toBeFalse();
+
+ tenancy()->initialize($tenant);
+
+ expect(Schema::hasTable('users'))->toBeTrue();
+ Artisan::call('tenants:rollback');
+ expect(Schema::hasTable('users'))->toBeFalse();
+});
+
+// Incomplete test
+test('seed command works');
+
+test('database connection is switched to default', function () {
+ databaseConnectionSwitchedToDefault();
+});
+
+test('database connection is switched to default when tenancy has been initialized', function () {
+ tenancy()->initialize(Tenant::create());
+
+ databaseConnectionSwitchedToDefault();
+});
+
+test('run command works', function () {
+ runCommandWorks();
+});
+
+test('install command works', function () {
+ if (! is_dir($dir = app_path('Http'))) {
+ mkdir($dir, 0777, true);
+ }
+ if (! is_dir($dir = base_path('routes'))) {
+ mkdir($dir, 0777, true);
+ }
+
+ pest()->artisan('tenancy:install');
+ expect(base_path('routes/tenant.php'))->toBeFile();
+ expect(base_path('config/tenancy.php'))->toBeFile();
+ expect(app_path('Providers/TenancyServiceProvider.php'))->toBeFile();
+ expect(database_path('migrations/2019_09_15_000010_create_tenants_table.php'))->toBeFile();
+ expect(database_path('migrations/2019_09_15_000020_create_domains_table.php'))->toBeFile();
+ expect(database_path('migrations/tenant'))->toBeDirectory();
+});
+
+test('migrate fresh command works', function () {
+ $tenant = Tenant::create();
+
+ expect(Schema::hasTable('users'))->toBeFalse();
+ Artisan::call('tenants:migrate-fresh');
+ expect(Schema::hasTable('users'))->toBeFalse();
+
+ tenancy()->initialize($tenant);
+
+ expect(Schema::hasTable('users'))->toBeTrue();
+
+ expect(DB::table('users')->exists())->toBeFalse();
+ DB::table('users')->insert(['name' => 'xxx', 'password' => bcrypt('password'), 'email' => 'foo@bar.xxx']);
+ expect(DB::table('users')->exists())->toBeTrue();
+
+ // test that db is wiped
+ Artisan::call('tenants:migrate-fresh');
+ expect(DB::table('users')->exists())->toBeFalse();
+});
+
+test('run command with array of tenants works', function () {
+ $tenantId1 = Tenant::create()->getTenantKey();
+ $tenantId2 = Tenant::create()->getTenantKey();
+ Artisan::call('tenants:migrate-fresh');
+
+ pest()->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'")
+ ->expectsOutput('Tenant: ' . $tenantId1)
+ ->expectsOutput('Tenant: ' . $tenantId2);
+});
+
+// todo@tests
+function runCommandWorks(): void
{
- public function setUp(): void
- {
- parent::setUp();
+ $id = Tenant::create()->getTenantKey();
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
+ Artisan::call('tenants:migrate', ['--tenants' => [$id]]);
- config(['tenancy.bootstrappers' => [
- DatabaseTenancyBootstrapper::class,
- ]]);
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
- }
-
- public function tearDown(): void
- {
- parent::tearDown();
-
- // Cleanup tenancy config cache
- if (file_exists(base_path('config/tenancy.php'))) {
- unlink(base_path('config/tenancy.php'));
- }
- }
-
- /** @test */
- public function migrate_command_doesnt_change_the_db_connection()
- {
- $this->assertFalse(Schema::hasTable('users'));
-
- $old_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
- Artisan::call('tenants:migrate');
- $new_connection_name = app(\Illuminate\Database\DatabaseManager::class)->connection()->getName();
-
- $this->assertFalse(Schema::hasTable('users'));
- $this->assertEquals($old_connection_name, $new_connection_name);
- $this->assertNotEquals('tenant', $new_connection_name);
- }
-
- /** @test */
- public function migrate_command_works_without_options()
- {
- $tenant = Tenant::create();
-
- $this->assertFalse(Schema::hasTable('users'));
- Artisan::call('tenants:migrate');
- $this->assertFalse(Schema::hasTable('users'));
-
- tenancy()->initialize($tenant);
-
- $this->assertTrue(Schema::hasTable('users'));
- }
-
- /** @test */
- public function migrate_command_works_with_tenants_option()
- {
- $tenant = Tenant::create();
- Artisan::call('tenants:migrate', [
- '--tenants' => [$tenant['id']],
- ]);
-
- $this->assertFalse(Schema::hasTable('users'));
- tenancy()->initialize(Tenant::create());
- $this->assertFalse(Schema::hasTable('users'));
-
- tenancy()->initialize($tenant);
- $this->assertTrue(Schema::hasTable('users'));
- }
-
- /** @test */
- public function rollback_command_works()
- {
- $tenant = Tenant::create();
- Artisan::call('tenants:migrate');
- $this->assertFalse(Schema::hasTable('users'));
-
- tenancy()->initialize($tenant);
-
- $this->assertTrue(Schema::hasTable('users'));
- Artisan::call('tenants:rollback');
- $this->assertFalse(Schema::hasTable('users'));
- }
-
- /** @test */
- public function seed_command_works()
- {
- $this->markTestIncomplete();
- }
-
- /** @test */
- public function database_connection_is_switched_to_default()
- {
- $originalDBName = DB::connection()->getDatabaseName();
-
- Artisan::call('tenants:migrate');
- $this->assertSame($originalDBName, DB::connection()->getDatabaseName());
-
- Artisan::call('tenants:seed', ['--class' => ExampleSeeder::class]);
- $this->assertSame($originalDBName, DB::connection()->getDatabaseName());
-
- Artisan::call('tenants:rollback');
- $this->assertSame($originalDBName, DB::connection()->getDatabaseName());
-
- $this->run_commands_works();
- $this->assertSame($originalDBName, DB::connection()->getDatabaseName());
- }
-
- /** @test */
- public function database_connection_is_switched_to_default_when_tenancy_has_been_initialized()
- {
- tenancy()->initialize(Tenant::create());
-
- $this->database_connection_is_switched_to_default();
- }
-
- /** @test */
- public function run_commands_works()
- {
- $id = Tenant::create()->getTenantKey();
-
- Artisan::call('tenants:migrate', ['--tenants' => [$id]]);
-
- $this->artisan("tenants:run foo --tenants=$id --argument='a=foo' --option='b=bar' --option='c=xyz'")
- ->expectsOutput("User's name is Test command")
- ->expectsOutput('foo')
- ->expectsOutput('xyz');
- }
-
- /** @test */
- public function install_command_works()
- {
- if (! is_dir($dir = app_path('Http'))) {
- mkdir($dir, 0777, true);
- }
- if (! is_dir($dir = base_path('routes'))) {
- mkdir($dir, 0777, true);
- }
-
- $this->artisan('tenancy:install');
- $this->assertFileExists(base_path('routes/tenant.php'));
- $this->assertFileExists(base_path('config/tenancy.php'));
- $this->assertFileExists(app_path('Providers/TenancyServiceProvider.php'));
- $this->assertFileExists(database_path('migrations/2019_09_15_000010_create_tenants_table.php'));
- $this->assertFileExists(database_path('migrations/2019_09_15_000020_create_domains_table.php'));
- $this->assertDirectoryExists(database_path('migrations/tenant'));
- }
-
- /** @test */
- public function migrate_fresh_command_works()
- {
- $tenant = Tenant::create();
-
- $this->assertFalse(Schema::hasTable('users'));
- Artisan::call('tenants:migrate-fresh');
- $this->assertFalse(Schema::hasTable('users'));
-
- tenancy()->initialize($tenant);
-
- $this->assertTrue(Schema::hasTable('users'));
-
- $this->assertFalse(DB::table('users')->exists());
- DB::table('users')->insert(['name' => 'xxx', 'password' => bcrypt('password'), 'email' => 'foo@bar.xxx']);
- $this->assertTrue(DB::table('users')->exists());
-
- // test that db is wiped
- Artisan::call('tenants:migrate-fresh');
- $this->assertFalse(DB::table('users')->exists());
- }
-
- /** @test */
- public function run_command_with_array_of_tenants_works()
- {
- $tenantId1 = Tenant::create()->getTenantKey();
- $tenantId2 = Tenant::create()->getTenantKey();
- Artisan::call('tenants:migrate-fresh');
-
- $this->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'")
- ->expectsOutput('Tenant: ' . $tenantId1)
- ->expectsOutput('Tenant: ' . $tenantId2);
- }
+ pest()->artisan("tenants:run foo --tenants=$id --argument='a=foo' --option='b=bar' --option='c=xyz'")
+ ->expectsOutput("User's name is Test command")
+ ->expectsOutput('foo')
+ ->expectsOutput('xyz');
+}
+
+// todo@tests
+function databaseConnectionSwitchedToDefault()
+{
+ $originalDBName = DB::connection()->getDatabaseName();
+
+ Artisan::call('tenants:migrate');
+ expect(DB::connection()->getDatabaseName())->toBe($originalDBName);
+
+ Artisan::call('tenants:seed', ['--class' => ExampleSeeder::class]);
+ expect(DB::connection()->getDatabaseName())->toBe($originalDBName);
+
+ Artisan::call('tenants:rollback');
+ expect(DB::connection()->getDatabaseName())->toBe($originalDBName);
+
+ runCommandWorks();
+
+ expect(DB::connection()->getDatabaseName())->toBe($originalDBName);
}
diff --git a/tests/DatabasePreparationTest.php b/tests/DatabasePreparationTest.php
index 12d30059..64fa1e0f 100644
--- a/tests/DatabasePreparationTest.php
+++ b/tests/DatabasePreparationTest.php
@@ -2,11 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
-use Illuminate\Database\Seeder;
-use Illuminate\Foundation\Auth\User as Authenticable;
-use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema;
use Stancl\JobPipeline\JobPipeline;
@@ -16,111 +11,85 @@ use Stancl\Tenancy\Jobs\MigrateDatabase;
use Stancl\Tenancy\Jobs\SeedDatabase;
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\Tests\Etc\Tenant;
+use Illuminate\Foundation\Auth\User as Authenticable;
+use Stancl\Tenancy\Tests\Etc\TestSeeder;
-class DatabasePreparationTest extends TestCase
-{
- /** @test */
- public function database_can_be_created_after_tenant_creation()
- {
- config(['tenancy.database.template_tenant_connection' => 'mysql']);
+test('database can be created after tenant creation', function () {
+ config(['tenancy.database.template_tenant_connection' => 'mysql']);
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
- $tenant = Tenant::create();
+ $tenant = Tenant::create();
- $manager = app(MySQLDatabaseManager::class);
- $manager->setConnection('mysql');
+ $manager = app(MySQLDatabaseManager::class);
+ $manager->setConnection('mysql');
- $this->assertTrue($manager->databaseExists($tenant->database()->getName()));
- }
+ expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue();
+});
- /** @test */
- public function database_can_be_migrated_after_tenant_creation()
- {
- Event::listen(TenantCreated::class, JobPipeline::make([
- CreateDatabase::class,
- MigrateDatabase::class,
- ])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
+test('database can be migrated after tenant creation', function () {
+ Event::listen(TenantCreated::class, JobPipeline::make([
+ CreateDatabase::class,
+ MigrateDatabase::class,
+ ])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
- $tenant = Tenant::create();
+ $tenant = Tenant::create();
- $tenant->run(function () {
- $this->assertTrue(Schema::hasTable('users'));
- });
- }
+ $tenant->run(function () {
+ expect(Schema::hasTable('users'))->toBeTrue();
+ });
+});
- /** @test */
- public function database_can_be_seeded_after_tenant_creation()
- {
- config(['tenancy.seeder_parameters' => [
- '--class' => TestSeeder::class,
- ]]);
+test('database can be seeded after tenant creation', function () {
+ config(['tenancy.seeder_parameters' => [
+ '--class' => TestSeeder::class,
+ ]]);
- Event::listen(TenantCreated::class, JobPipeline::make([
- CreateDatabase::class,
- MigrateDatabase::class,
- SeedDatabase::class,
- ])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
+ Event::listen(TenantCreated::class, JobPipeline::make([
+ CreateDatabase::class,
+ MigrateDatabase::class,
+ SeedDatabase::class,
+ ])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
- $tenant = Tenant::create();
+ $tenant = Tenant::create();
- $tenant->run(function () {
- $this->assertSame('Seeded User', User::first()->name);
- });
- }
+ $tenant->run(function () {
+ expect(User::first()->name)->toBe('Seeded User');
+ });
+});
- /** @test */
- public function custom_job_can_be_added_to_the_pipeline()
- {
- config(['tenancy.seeder_parameters' => [
- '--class' => TestSeeder::class,
- ]]);
+test('custom job can be added to the pipeline', function () {
+ config(['tenancy.seeder_parameters' => [
+ '--class' => TestSeeder::class,
+ ]]);
- Event::listen(TenantCreated::class, JobPipeline::make([
- CreateDatabase::class,
- MigrateDatabase::class,
- SeedDatabase::class,
- CreateSuperuser::class,
- ])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
+ Event::listen(TenantCreated::class, JobPipeline::make([
+ CreateDatabase::class,
+ MigrateDatabase::class,
+ SeedDatabase::class,
+ CreateSuperuser::class,
+ ])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
- $tenant = Tenant::create();
+ $tenant = Tenant::create();
- $tenant->run(function () {
- $this->assertSame('Foo', User::all()[1]->name);
- });
- }
-}
+ $tenant->run(function () {
+ expect(User::all()[1]->name)->toBe('Foo');
+ });
+});
class User extends Authenticable
{
protected $guarded = [];
}
-class TestSeeder extends Seeder
-{
- /**
- * Run the database seeds.
- *
- * @return void
- */
- public function run()
- {
- DB::table('users')->insert([
- 'name' => 'Seeded User',
- 'email' => 'seeded@user',
- 'password' => bcrypt('password'),
- ]);
- }
-}
-
class CreateSuperuser
{
protected $tenant;
diff --git a/tests/DatabaseUsersTest.php b/tests/DatabaseUsersTest.php
index 0b095024..2635c167 100644
--- a/tests/DatabaseUsersTest.php
+++ b/tests/DatabaseUsersTest.php
@@ -2,14 +2,13 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
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;
@@ -19,107 +18,97 @@ use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class DatabaseUsersTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
+beforeEach(function () {
+ config([
+ 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
+ 'tenancy.database.suffix' => '',
+ 'tenancy.database.template_tenant_connection' => 'mysql',
+ ]);
- config([
- 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
- 'tenancy.database.suffix' => '',
- 'tenancy.database.template_tenant_connection' => 'mysql',
- ]);
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+});
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
- }
+test('users are created when permission controlled mysql manager is used', function () {
+ $tenant = new Tenant([
+ 'id' => 'foo' . Str::random(10),
+ ]);
+ $tenant->database()->makeCredentials();
- /** @test */
- public function users_are_created_when_permission_controlled_mysql_manager_is_used()
- {
- $tenant = new Tenant([
- 'id' => 'foo' . Str::random(10),
- ]);
- $tenant->database()->makeCredentials();
+ /** @var ManagesDatabaseUsers $manager */
+ $manager = $tenant->database()->manager();
+ expect($manager->userExists($tenant->database()->getUsername()))->toBeFalse();
- /** @var ManagesDatabaseUsers $manager */
- $manager = $tenant->database()->manager();
- $this->assertFalse($manager->userExists($tenant->database()->getUsername()));
+ $tenant->save();
- $tenant->save();
+ expect($manager->userExists($tenant->database()->getUsername()))->toBeTrue();
+});
- $this->assertTrue($manager->userExists($tenant->database()->getUsername()));
- }
+test('a tenants database cannot be created when the user already exists', function () {
+ $username = 'foo' . Str::random(8);
+ $tenant = Tenant::create([
+ 'tenancy_db_username' => $username,
+ ]);
- /** @test */
- public function a_tenants_database_cannot_be_created_when_the_user_already_exists()
- {
- $username = 'foo' . Str::random(8);
- $tenant = Tenant::create([
- 'tenancy_db_username' => $username,
- ]);
+ /** @var ManagesDatabaseUsers $manager */
+ $manager = $tenant->database()->manager();
+ expect($manager->userExists($tenant->database()->getUsername()))->toBeTrue();
+ expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue();
- /** @var ManagesDatabaseUsers $manager */
- $manager = $tenant->database()->manager();
- $this->assertTrue($manager->userExists($tenant->database()->getUsername()));
- $this->assertTrue($manager->databaseExists($tenant->database()->getName()));
+ pest()->expectException(TenantDatabaseUserAlreadyExistsException::class);
+ Event::fake([DatabaseCreated::class]);
- $this->expectException(TenantDatabaseUserAlreadyExistsException::class);
- $tenant2 = Tenant::create([
- 'tenancy_db_username' => $username,
- ]);
+ $tenant2 = Tenant::create([
+ 'tenancy_db_username' => $username,
+ ]);
- /** @var ManagesDatabaseUsers $manager */
- $manager = $tenant2->database()->manager();
- // database was not created because of DB transaction
- $this->assertFalse($manager->databaseExists($tenant2->database()->getName()));
- }
+ /** @var ManagesDatabaseUsers $manager */
+ $manager2 = $tenant2->database()->manager();
- /** @test */
- public function correct_grants_are_given_to_users()
- {
- PermissionControlledMySQLDatabaseManager::$grants = [
- 'ALTER', 'ALTER ROUTINE', 'CREATE',
- ];
+ // database was not created because of DB transaction
+ expect($manager2->databaseExists($tenant2->database()->getName()))->toBeFalse();
+ Event::assertNotDispatched(DatabaseCreated::class);
+});
- $tenant = Tenant::create([
- 'tenancy_db_username' => $user = 'user' . Str::random(8),
- ]);
+test('correct grants are given to users', function () {
+ PermissionControlledMySQLDatabaseManager::$grants = [
+ 'ALTER', 'ALTER ROUTINE', 'CREATE',
+ ];
- $query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`%`")[1];
- $this->assertStringStartsWith('GRANT CREATE, ALTER, ALTER ROUTINE ON', $query->{"Grants for {$user}@%"}); // @mysql because that's the hostname within the docker network
- }
+ $tenant = Tenant::create([
+ 'tenancy_db_username' => $user = 'user' . Str::random(8),
+ ]);
- /** @test */
- public function having_existing_databases_without_users_and_switching_to_permission_controlled_mysql_manager_doesnt_break_existing_dbs()
- {
- config([
- 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class,
- 'tenancy.database.suffix' => '',
- 'tenancy.database.template_tenant_connection' => 'mysql',
- 'tenancy.bootstrappers' => [
- DatabaseTenancyBootstrapper::class,
- ],
- ]);
+ $query = DB::connection('mysql')->select("SHOW GRANTS FOR `{$tenant->database()->getUsername()}`@`%`")[1];
+ expect($query->{"Grants for {$user}@%"})->toStartWith('GRANT CREATE, ALTER, ALTER ROUTINE ON'); // @mysql because that's the hostname within the docker network
+});
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+test('having existing databases without users and switching to permission controlled mysql manager doesnt break existing dbs', function () {
+ config([
+ 'tenancy.database.managers.mysql' => MySQLDatabaseManager::class,
+ 'tenancy.database.suffix' => '',
+ 'tenancy.database.template_tenant_connection' => 'mysql',
+ 'tenancy.bootstrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ],
+ ]);
- $tenant = Tenant::create([
- 'id' => 'foo' . Str::random(10),
- ]);
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- $this->assertTrue($tenant->database()->manager() instanceof MySQLDatabaseManager);
+ $tenant = Tenant::create([
+ 'id' => 'foo' . Str::random(10),
+ ]);
- tenancy()->initialize($tenant); // check if everything works
- tenancy()->end();
+ expect($tenant->database()->manager() instanceof MySQLDatabaseManager)->toBeTrue();
- config(['tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class]);
+ tenancy()->initialize($tenant); // check if everything works
+ tenancy()->end();
- tenancy()->initialize($tenant); // check if everything works
+ config(['tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class]);
- $this->assertTrue($tenant->database()->manager() instanceof PermissionControlledMySQLDatabaseManager);
- $this->assertSame('root', config('database.connections.tenant.username'));
- }
-}
+ tenancy()->initialize($tenant); // check if everything works
+
+ expect($tenant->database()->manager() instanceof PermissionControlledMySQLDatabaseManager)->toBeTrue();
+ expect(config('database.connections.tenant.username'))->toBe('root');
+});
diff --git a/tests/DebuggableTest.php b/tests/DebuggableTest.php
new file mode 100644
index 00000000..49e180d7
--- /dev/null
+++ b/tests/DebuggableTest.php
@@ -0,0 +1,68 @@
+log(LogMode::SILENT);
+
+ $tenant = Tenant::create();
+
+ tenancy()->initialize($tenant);
+
+ tenancy()->end();
+
+ assertTenancyInitializedAndEnded(tenancy()->getLog(), $tenant);
+});
+
+test('tenancy logs event silently by default', function () {
+ tenancy()->log();
+
+ expect(tenancy()->logMode())->toBe(LogMode::SILENT);
+});
+
+test('the log can be dumped', function (string $method) {
+ tenancy()->log();
+
+ $tenant = Tenant::create();
+
+ tenancy()->initialize($tenant);
+
+ tenancy()->end();
+
+ $output = [];
+ tenancy()->$method(function ($data) use (&$output) {
+ $output = $data;
+ });
+
+ assertTenancyInitializedAndEnded($output, $tenant);
+})->with([
+ 'dump',
+ 'dd',
+]);
+
+test('tenancy can log events immediately', function () {
+ // todo implement
+ pest()->markTestIncomplete();
+});
+
+// todo test the different behavior of the methods in different contexts, or get rid of the logic and simplify it
+
+function assertTenancyInitializedAndEnded(array $log, Tenant $tenant): void
+{
+ expect($log)->toHaveCount(4);
+
+ expect($log[0]['event'])->toBe(InitializingTenancy::class);
+ expect($log[0]['tenant'])->toBe($tenant);
+ expect($log[1]['event'])->toBe(TenancyInitialized::class);
+ expect($log[1]['tenant'])->toBe($tenant);
+
+ expect($log[2]['event'])->toBe(EndingTenancy::class);
+ expect($log[2]['tenant'])->toBe($tenant);
+ expect($log[3]['event'])->toBe(TenancyEnded::class);
+ expect($log[3]['tenant'])->toBe($tenant);
+}
diff --git a/tests/DeleteDomainsJobTest.php b/tests/DeleteDomainsJobTest.php
new file mode 100644
index 00000000..bdee14dd
--- /dev/null
+++ b/tests/DeleteDomainsJobTest.php
@@ -0,0 +1,32 @@
+ DatabaseAndDomainTenant::class]);
+});
+
+test('job delete domains successfully', function (){
+ $tenant = DatabaseAndDomainTenant::create();
+
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
+ $tenant->domains()->create([
+ 'domain' => 'bar.localhost',
+ ]);
+
+ expect($tenant->domains()->count())->toBe(2);
+
+ (new DeleteDomains($tenant))->handle();
+
+ expect($tenant->refresh()->domains()->count())->toBe(0);
+});
+
+class DatabaseAndDomainTenant extends \Stancl\Tenancy\Tests\Etc\Tenant
+{
+ use HasDomains;
+}
\ No newline at end of file
diff --git a/tests/DomainTest.php b/tests/DomainTest.php
index 9c1bac28..594270e1 100644
--- a/tests/DomainTest.php
+++ b/tests/DomainTest.php
@@ -2,121 +2,113 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Database\Models;
use Stancl\Tenancy\Database\Models\Domain;
use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
+use Stancl\Tenancy\Features\UniversalRoutes;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
-class DomainTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
-
- Route::group([
- 'middleware' => InitializeTenancyByDomain::class,
- ], function () {
- Route::get('/foo/{a}/{b}', function ($a, $b) {
- return "$a + $b";
- });
+beforeEach(function () {
+ Route::group([
+ 'middleware' => InitializeTenancyByDomain::class,
+ ], function () {
+ Route::get('/foo/{a}/{b}', function ($a, $b) {
+ return "$a + $b";
});
+ });
- config(['tenancy.tenant_model' => DomainTenant::class]);
- }
+ config(['tenancy.tenant_model' => DomainTenant::class]);
+});
- /** @test */
- public function tenant_can_be_identified_using_hostname()
- {
- $tenant = DomainTenant::create();
+test('tenant can be identified using hostname', function () {
+ $tenant = DomainTenant::create();
- $id = $tenant->id;
+ $id = $tenant->id;
- $tenant->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
- $resolvedTenant = app(DomainTenantResolver::class)->resolve('foo.localhost');
+ $resolvedTenant = app(DomainTenantResolver::class)->resolve('foo.localhost');
- $this->assertSame($id, $resolvedTenant->id);
- $this->assertSame(['foo.localhost'], $resolvedTenant->domains->pluck('domain')->toArray());
- }
+ expect($resolvedTenant->id)->toBe($id);
+ expect($resolvedTenant->domains->pluck('domain')->toArray())->toBe(['foo.localhost']);
+});
- /** @test */
- public function a_domain_can_belong_to_only_one_tenant()
- {
- $tenant = DomainTenant::create();
+test('a domain can belong to only one tenant', function () {
+ $tenant = DomainTenant::create();
- $tenant->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
- $tenant2 = DomainTenant::create();
+ $tenant2 = DomainTenant::create();
- $this->expectException(DomainOccupiedByOtherTenantException::class);
- $tenant2->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
- }
+ pest()->expectException(DomainOccupiedByOtherTenantException::class);
+ $tenant2->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
+});
- /** @test */
- public function an_exception_is_thrown_if_tenant_cannot_be_identified()
- {
- $this->expectException(TenantCouldNotBeIdentifiedOnDomainException::class);
+test('an exception is thrown if tenant cannot be identified', function () {
+ pest()->expectException(TenantCouldNotBeIdentifiedOnDomainException::class);
- app(DomainTenantResolver::class)->resolve('foo.localhost');
- }
+ app(DomainTenantResolver::class)->resolve('foo.localhost');
+});
- /** @test */
- public function tenant_can_be_identified_by_domain()
- {
- $tenant = DomainTenant::create([
- 'id' => 'acme',
- ]);
+test('tenant can be identified by domain', function () {
+ $tenant = DomainTenant::create([
+ 'id' => 'acme',
+ ]);
- $tenant->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
- $this->assertFalse(tenancy()->initialized);
+ expect(tenancy()->initialized)->toBeFalse();
- $this
- ->get('http://foo.localhost/foo/abc/xyz')
- ->assertSee('abc + xyz');
+ pest()
+ ->get('http://foo.localhost/foo/abc/xyz')
+ ->assertSee('abc + xyz');
- $this->assertTrue(tenancy()->initialized);
- $this->assertSame('acme', tenant('id'));
- }
+ expect(tenancy()->initialized)->toBeTrue();
+ expect(tenant('id'))->toBe('acme');
+});
- /** @test */
- public function onfail_logic_can_be_customized()
- {
- InitializeTenancyByDomain::$onFail = function () {
- return 'foo';
- };
+test('onfail logic can be customized', function () {
+ InitializeTenancyByDomain::$onFail = function () {
+ return 'foo';
+ };
- $this
- ->get('http://foo.localhost/foo/abc/xyz')
- ->assertSee('foo');
- }
+ pest()
+ ->get('http://foo.localhost/foo/abc/xyz')
+ ->assertSee('foo');
+});
- /** @test */
- public function domains_are_always_lowercase()
- {
- $tenant = DomainTenant::create();
+test('throw correct exception when onFail is null and universal routes are enabled', function () {
+ // un-define onFail logic
+ InitializeTenancyByDomain::$onFail = null;
- $tenant->domains()->create([
- 'domain' => 'CAPITALS',
- ]);
+ // Enable UniversalRoute feature
+ Route::middlewareGroup('universal', []);
+ config(['tenancy.features' => [UniversalRoutes::class]]);
- $this->assertSame('capitals', Domain::first()->domain);
- }
-}
+ $this->withoutExceptionHandling()->get('http://foo.localhost/foo/abc/xyz');
+})->throws(TenantCouldNotBeIdentifiedOnDomainException::class);;
+
+test('domains are always lowercase', function () {
+ $tenant = DomainTenant::create();
+
+ $tenant->domains()->create([
+ 'domain' => 'CAPITALS',
+ ]);
+
+ expect(Domain::first()->domain)->toBe('capitals');
+});
class DomainTenant extends Models\Tenant
{
diff --git a/tests/Etc/ConsoleKernel.php b/tests/Etc/ConsoleKernel.php
index 9d37d3c6..a548f113 100644
--- a/tests/Etc/ConsoleKernel.php
+++ b/tests/Etc/ConsoleKernel.php
@@ -8,11 +8,6 @@ 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/ExampleSeeder.php b/tests/Etc/ExampleSeeder.php
index a3e36123..2f97787e 100644
--- a/tests/Etc/ExampleSeeder.php
+++ b/tests/Etc/ExampleSeeder.php
@@ -19,7 +19,7 @@ class ExampleSeeder extends Seeder
{
DB::table('users')->insert([
'name' => Str::random(10),
- 'email' => Str::random(10).'@gmail.com',
+ 'email' => Str::random(10) . '@gmail.com',
'password' => bcrypt('password'),
]);
}
diff --git a/tests/Etc/TestSeeder.php b/tests/Etc/TestSeeder.php
new file mode 100644
index 00000000..3412948e
--- /dev/null
+++ b/tests/Etc/TestSeeder.php
@@ -0,0 +1,23 @@
+insert([
+ 'name' => 'Seeded User',
+ 'email' => 'seeded@user',
+ 'password' => bcrypt('password'),
+ ]);
+ }
+}
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/EventListenerTest.php b/tests/EventListenerTest.php
index 4a45205c..85203f07 100644
--- a/tests/EventListenerTest.php
+++ b/tests/EventListenerTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Events\CallQueuedListener;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
@@ -20,186 +18,166 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Jobs\MigrateDatabase;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\QueueableListener;
-use Stancl\Tenancy\Tenancy;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class EventListenerTest extends TestCase
-{
- /** @test */
- public function listeners_can_be_synchronous()
- {
- Queue::fake();
- Event::listen(TenantCreated::class, FooListener::class);
+test('listeners can be synchronous', function () {
+ Queue::fake();
+ Event::listen(TenantCreated::class, FooListener::class);
- Tenant::create();
+ Tenant::create();
- Queue::assertNothingPushed();
+ Queue::assertNothingPushed();
- $this->assertSame('bar', app('foo'));
- }
+ expect(app('foo'))->toBe('bar');
+});
- /** @test */
- public function listeners_can_be_queued_by_setting_a_static_property()
- {
- Queue::fake();
+test('listeners can be queued by setting a static property', function () {
+ Queue::fake();
- Event::listen(TenantCreated::class, FooListener::class);
- FooListener::$shouldQueue = true;
+ Event::listen(TenantCreated::class, FooListener::class);
+ FooListener::$shouldQueue = true;
- Tenant::create();
+ Tenant::create();
- Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
- return $job->class === FooListener::class;
- });
+ Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
+ return $job->class === FooListener::class;
+ });
- $this->assertFalse(app()->bound('foo'));
- }
+ expect(app()->bound('foo'))->toBeFalse();
+});
- /** @test */
- public function ing_events_can_be_used_to_cancel_tenant_model_actions()
- {
- Event::listen(CreatingTenant::class, function () {
- return false;
- });
+test('ing events can be used to cancel tenant model actions', function () {
+ Event::listen(CreatingTenant::class, function () {
+ return false;
+ });
- $this->assertSame(false, Tenant::create()->exists);
- $this->assertSame(0, Tenant::count());
- }
+ expect(Tenant::create()->exists)->toBe(false);
+ expect(Tenant::count())->toBe(0);
+});
- /** @test */
- public function ing_events_can_be_used_to_cancel_domain_model_actions()
- {
- $tenant = Tenant::create();
+test('ing events can be used to cancel domain model actions', function () {
+ $tenant = Tenant::create();
- Event::listen(UpdatingDomain::class, function () {
- return false;
- });
+ Event::listen(UpdatingDomain::class, function () {
+ return false;
+ });
- $domain = $tenant->domains()->create([
- 'domain' => 'acme',
- ]);
+ $domain = $tenant->domains()->create([
+ 'domain' => 'acme',
+ ]);
- $domain->update([
- 'domain' => 'foo',
- ]);
+ $domain->update([
+ 'domain' => 'foo',
+ ]);
- $this->assertSame('acme', $domain->refresh()->domain);
- }
+ expect($domain->refresh()->domain)->toBe('acme');
+});
- /** @test */
- public function ing_events_can_be_used_to_cancel_db_creation()
- {
- Event::listen(CreatingDatabase::class, function (CreatingDatabase $event) {
- $event->tenant->setInternal('create_database', false);
- });
+test('ing events can be used to cancel db creation', function () {
+ Event::listen(CreatingDatabase::class, function (CreatingDatabase $event) {
+ $event->tenant->setInternal('create_database', false);
+ });
- $tenant = Tenant::create();
- dispatch_now(new CreateDatabase($tenant));
+ $tenant = Tenant::create();
+ dispatch_now(new CreateDatabase($tenant));
- $this->assertFalse($tenant->database()->manager()->databaseExists(
- $tenant->database()->getName()
- ));
- }
+ pest()->assertFalse($tenant->database()->manager()->databaseExists(
+ $tenant->database()->getName()
+ ));
+});
- /** @test */
- public function ing_events_can_be_used_to_cancel_tenancy_bootstrapping()
- {
- config(['tenancy.bootstrappers' => [
- DatabaseTenancyBootstrapper::class,
- RedisTenancyBootstrapper::class,
- ]]);
+test('ing events can be used to cancel tenancy bootstrapping', function () {
+ config(['tenancy.bootstrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ RedisTenancyBootstrapper::class,
+ ]]);
- Event::listen(
- TenantCreated::class,
- JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener()
- );
+ Event::listen(
+ TenantCreated::class,
+ JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener()
+ );
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(BootstrappingTenancy::class, function (BootstrappingTenancy $event) {
- $event->tenancy->getBootstrappersUsing = function () {
- return [DatabaseTenancyBootstrapper::class];
- };
- });
+ Event::listen(BootstrappingTenancy::class, function (BootstrappingTenancy $event) {
+ $event->tenancy->getBootstrappersUsing = function () {
+ return [DatabaseTenancyBootstrapper::class];
+ };
+ });
- tenancy()->initialize(Tenant::create());
+ tenancy()->initialize(Tenant::create());
- $this->assertSame([DatabaseTenancyBootstrapper::class], array_map('get_class', tenancy()->getBootstrappers()));
- }
+ expect(array_map('get_class', tenancy()->getBootstrappers()))->toBe([DatabaseTenancyBootstrapper::class]);
+});
- /** @test */
- public function individual_job_pipelines_can_terminate_while_leaving_others_running()
- {
- $executed = [];
+test('individual job pipelines can terminate while leaving others running', function () {
+ $executed = [];
- Event::listen(
- TenantCreated::class,
- JobPipeline::make([
- function () use (&$executed) {
- $executed[] = 'P1J1';
- },
+ Event::listen(
+ TenantCreated::class,
+ JobPipeline::make([
+ function () use (&$executed) {
+ $executed[] = 'P1J1';
+ },
- function () use (&$executed) {
- $executed[] = 'P1J2';
- },
- ])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener()
- );
+ function () use (&$executed) {
+ $executed[] = 'P1J2';
+ },
+ ])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener()
+ );
- Event::listen(
- TenantCreated::class,
- JobPipeline::make([
- function () use (&$executed) {
- $executed[] = 'P2J1';
+ Event::listen(
+ TenantCreated::class,
+ JobPipeline::make([
+ function () use (&$executed) {
+ $executed[] = 'P2J1';
- return false;
- },
+ return false;
+ },
- function () use (&$executed) {
- $executed[] = 'P2J2';
- },
- ])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener()
- );
+ function () use (&$executed) {
+ $executed[] = 'P2J2';
+ },
+ ])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener()
+ );
- Tenant::create();
+ Tenant::create();
- $this->assertSame([
- 'P1J1',
- 'P1J2',
- 'P2J1', // termminated after this
- // P2J2 was not reached
- ], $executed);
- }
+ pest()->assertSame([
+ 'P1J1',
+ 'P1J2',
+ 'P2J1', // termminated after this
+ // P2J2 was not reached
+ ], $executed);
+});
- /** @test */
- public function database_is_not_migrated_if_creation_is_disabled()
- {
- Event::listen(
- TenantCreated::class,
- JobPipeline::make([
- CreateDatabase::class,
- function () {
- $this->fail("The job pipeline didn't exit.");
- },
- MigrateDatabase::class,
- ])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener()
- );
+test('database is not migrated if creation is disabled', function () {
+ Event::listen(
+ TenantCreated::class,
+ JobPipeline::make([
+ CreateDatabase::class,
+ function () {
+ pest()->fail("The job pipeline didn't exit.");
+ },
+ MigrateDatabase::class,
+ ])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener()
+ );
- Tenant::create([
- 'tenancy_create_database' => false,
- 'tenancy_db_name' => 'already_created',
- ]);
+ Tenant::create([
+ 'tenancy_create_database' => false,
+ 'tenancy_db_name' => 'already_created',
+ ]);
- $this->assertFalse($this->hasFailed());
- }
-}
+ expect(pest()->hasFailed())->toBeFalse();
+});
class FooListener extends QueueableListener
{
diff --git a/tests/Features/RedirectTest.php b/tests/Features/RedirectTest.php
index 4f7f77a1..7aca2e92 100644
--- a/tests/Features/RedirectTest.php
+++ b/tests/Features/RedirectTest.php
@@ -2,45 +2,35 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests\Features;
-
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Features\CrossDomainRedirect;
use Stancl\Tenancy\Tests\Etc\Tenant;
-use Stancl\Tenancy\Tests\TestCase;
-class RedirectTest extends TestCase
-{
- /** @test */
- public function tenant_redirect_macro_replaces_only_the_hostname()
- {
- config([
- 'tenancy.features' => [CrossDomainRedirect::class],
- ]);
+test('tenant redirect macro replaces only the hostname', function () {
+ config([
+ 'tenancy.features' => [CrossDomainRedirect::class],
+ ]);
- Route::get('/foobar', function () {
- return 'Foo';
- })->name('home');
+ Route::get('/foobar', function () {
+ return 'Foo';
+ })->name('home');
- Route::get('/redirect', function () {
- return redirect()->route('home')->domain('abcd');
- });
+ Route::get('/redirect', function () {
+ return redirect()->route('home')->domain('abcd');
+ });
- $tenant = Tenant::create();
- tenancy()->initialize($tenant);
+ $tenant = Tenant::create();
+ tenancy()->initialize($tenant);
- $this->get('/redirect')
- ->assertRedirect('http://abcd/foobar');
- }
+ pest()->get('/redirect')
+ ->assertRedirect('http://abcd/foobar');
+});
- /** @test */
- public function tenant_route_helper_generates_correct_url()
- {
- Route::get('/abcdef/{a?}/{b?}', function () {
- return 'Foo';
- })->name('foo');
+test('tenant route helper generates correct url', function () {
+ Route::get('/abcdef/{a?}/{b?}', function () {
+ return 'Foo';
+ })->name('foo');
- $this->assertSame('http://foo.localhost/abcdef/as/df', tenant_route('foo.localhost', 'foo', ['a' => 'as', 'b' => 'df']));
- $this->assertSame('http://foo.localhost/abcdef', tenant_route('foo.localhost', 'foo', []));
- }
-}
+ expect(tenant_route('foo.localhost', 'foo', ['a' => 'as', 'b' => 'df']))->toBe('http://foo.localhost/abcdef/as/df');
+ expect(tenant_route('foo.localhost', 'foo', []))->toBe('http://foo.localhost/abcdef');
+});
diff --git a/tests/Features/TenantConfigTest.php b/tests/Features/TenantConfigTest.php
index 37e26198..35df35ed 100644
--- a/tests/Features/TenantConfigTest.php
+++ b/tests/Features/TenantConfigTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests\Features;
-
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized;
@@ -11,84 +9,73 @@ use Stancl\Tenancy\Features\TenantConfig;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant;
-use Stancl\Tenancy\Tests\TestCase;
-class TenantConfigTest extends TestCase
-{
- public function tearDown(): void
- {
- TenantConfig::$storageToConfigMap = [];
+afterEach(function () {
+ TenantConfig::$storageToConfigMap = [];
+});
- parent::tearDown();
- }
+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);
- /** @test */
- public function config_is_merged_and_removed()
- {
- $this->assertSame(null, config('services.paypal'));
- config([
- 'tenancy.features' => [TenantConfig::class],
- 'tenancy.bootstrappers' => [],
- ]);
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+ TenantConfig::$storageToConfigMap = [
+ 'paypal_api_public' => 'services.paypal.public',
+ 'paypal_api_private' => 'services.paypal.private',
+ ];
- TenantConfig::$storageToConfigMap = [
- 'paypal_api_public' => 'services.paypal.public',
- 'paypal_api_private' => 'services.paypal.private',
- ];
+ $tenant = Tenant::create([
+ 'paypal_api_public' => 'foo',
+ 'paypal_api_private' => 'bar',
+ ]);
- $tenant = Tenant::create([
- 'paypal_api_public' => 'foo',
- 'paypal_api_private' => 'bar',
- ]);
+ tenancy()->initialize($tenant);
+ expect(config('services.paypal'))->toBe(['public' => 'foo', 'private' => 'bar']);
- tenancy()->initialize($tenant);
- $this->assertSame(['public' => 'foo', 'private' => 'bar'], config('services.paypal'));
+ tenancy()->end();
+ pest()->assertSame([
+ 'public' => null,
+ 'private' => null,
+ ], config('services.paypal'));
+});
- tenancy()->end();
- $this->assertSame([
- 'public' => null,
- 'private' => null,
- ], config('services.paypal'));
- }
+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);
- /** @test */
- public function the_value_can_be_set_to_multiple_config_keys()
- {
- $this->assertSame(null, config('services.paypal'));
- config([
- 'tenancy.features' => [TenantConfig::class],
- 'tenancy.bootstrappers' => [],
- ]);
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+ TenantConfig::$storageToConfigMap = [
+ 'paypal_api_public' => [
+ 'services.paypal.public1',
+ 'services.paypal.public2',
+ ],
+ 'paypal_api_private' => 'services.paypal.private',
+ ];
- TenantConfig::$storageToConfigMap = [
- 'paypal_api_public' => [
- 'services.paypal.public1',
- 'services.paypal.public2',
- ],
- 'paypal_api_private' => 'services.paypal.private',
- ];
+ $tenant = Tenant::create([
+ 'paypal_api_public' => 'foo',
+ 'paypal_api_private' => 'bar',
+ ]);
- $tenant = Tenant::create([
- 'paypal_api_public' => 'foo',
- 'paypal_api_private' => 'bar',
- ]);
+ tenancy()->initialize($tenant);
+ pest()->assertSame([
+ 'public1' => 'foo',
+ 'public2' => 'foo',
+ 'private' => 'bar',
+ ], config('services.paypal'));
- tenancy()->initialize($tenant);
- $this->assertSame([
- 'public1' => 'foo',
- 'public2' => 'foo',
- 'private' => 'bar',
- ], config('services.paypal'));
-
- tenancy()->end();
- $this->assertSame([
- 'public1' => null,
- 'public2' => null,
- 'private' => null,
- ], config('services.paypal'));
- }
-}
+ tenancy()->end();
+ pest()->assertSame([
+ 'public1' => null,
+ 'public2' => null,
+ 'private' => null,
+ ], config('services.paypal'));
+});
diff --git a/tests/GlobalCacheTest.php b/tests/GlobalCacheTest.php
index a39a1f55..8a13395c 100644
--- a/tests/GlobalCacheTest.php
+++ b/tests/GlobalCacheTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded;
@@ -13,49 +11,42 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class GlobalCacheTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
+beforeEach(function () {
+ config(['tenancy.bootstrappers' => [
+ CacheTenancyBootstrapper::class,
+ ]]);
- config(['tenancy.bootstrappers' => [
- CacheTenancyBootstrapper::class,
- ]]);
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+});
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
- }
+test('global cache manager stores data in global cache', function () {
+ expect(cache('foo'))->toBe(null);
+ GlobalCache::put(['foo' => 'bar'], 1);
+ expect(GlobalCache::get('foo'))->toBe('bar');
- /** @test */
- public function global_cache_manager_stores_data_in_global_cache()
- {
- $this->assertSame(null, cache('foo'));
- GlobalCache::put(['foo' => 'bar'], 1);
- $this->assertSame('bar', GlobalCache::get('foo'));
+ $tenant1 = Tenant::create();
+ tenancy()->initialize($tenant1);
+ expect(GlobalCache::get('foo'))->toBe('bar');
- $tenant1 = Tenant::create();
- tenancy()->initialize($tenant1);
- $this->assertSame('bar', GlobalCache::get('foo'));
+ GlobalCache::put(['abc' => 'xyz'], 1);
+ cache(['def' => 'ghi'], 10);
+ expect(cache('def'))->toBe('ghi');
- GlobalCache::put(['abc' => 'xyz'], 1);
- cache(['def' => 'ghi'], 10);
- $this->assertSame('ghi', cache('def'));
+ tenancy()->end();
+ expect(GlobalCache::get('abc'))->toBe('xyz');
+ expect(GlobalCache::get('foo'))->toBe('bar');
+ expect(cache('def'))->toBe(null);
- tenancy()->end();
- $this->assertSame('xyz', GlobalCache::get('abc'));
- $this->assertSame('bar', GlobalCache::get('foo'));
- $this->assertSame(null, cache('def'));
+ $tenant2 = Tenant::create();
+ tenancy()->initialize($tenant2);
+ expect(GlobalCache::get('abc'))->toBe('xyz');
+ expect(GlobalCache::get('foo'))->toBe('bar');
+ expect(cache('def'))->toBe(null);
+ cache(['def' => 'xxx'], 1);
+ expect(cache('def'))->toBe('xxx');
- $tenant2 = Tenant::create();
- tenancy()->initialize($tenant2);
- $this->assertSame('xyz', GlobalCache::get('abc'));
- $this->assertSame('bar', GlobalCache::get('foo'));
- $this->assertSame(null, cache('def'));
- cache(['def' => 'xxx'], 1);
- $this->assertSame('xxx', cache('def'));
+ tenancy()->initialize($tenant1);
+ expect(cache('def'))->toBe('ghi');
+});
- tenancy()->initialize($tenant1);
- $this->assertSame('ghi', cache('def'));
- }
-}
diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php
index 3b88fd3a..897dd8a4 100644
--- a/tests/MaintenanceModeTest.php
+++ b/tests/MaintenanceModeTest.php
@@ -2,86 +2,77 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Artisan;
-use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Tests\Etc\Tenant;
-use Symfony\Component\HttpKernel\Exception\HttpException;
-class MaintenanceModeTest extends TestCase
-{
- /** @test */
- public function tenant_can_be_in_maintenance_mode()
- {
- Route::get('/foo', function () {
- return 'bar';
- })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
+test('tenant can be in maintenance mode', function () {
+ Route::get('/foo', function () {
+ return 'bar';
+ })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
- $tenant = MaintenanceTenant::create();
- $tenant->domains()->create([
- 'domain' => 'acme.localhost',
- ]);
+ $tenant = MaintenanceTenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'acme.localhost',
+ ]);
- $this->get('http://acme.localhost/foo')
- ->assertSuccessful();
+ pest()->get('http://acme.localhost/foo')
+ ->assertSuccessful();
- tenancy()->end(); // flush stored tenant instance
+ tenancy()->end(); // Flush stored tenant instance
- $tenant->putDownForMaintenance();
+ $tenant->putDownForMaintenance();
- $this->expectException(HttpException::class);
- $this->withoutExceptionHandling()
- ->get('http://acme.localhost/foo');
+ pest()->expectException(HttpException::class);
+ pest()->withoutExceptionHandling()
+ ->get('http://acme.localhost/foo');
- tenancy()->end();
+ tenancy()->end();
- $tenant->bringUpFromMaintenance();
+ $tenant->bringUpFromMaintenance();
- tenancy()->end();
+ tenancy()->end();
- $this->get('http://acme.localhost/foo')
- ->assertSuccessful()
- ->assertSeeText('bar');
- }
+ pest()->get('http://acme.localhost/foo')
+ ->assertSuccessful()
+ ->assertSeeText('bar');
+});
- /** @test */
- public function tenant_can_be_in_maintenance_mode_from_command()
- {
- Route::get('/foo', function () {
- return 'bar';
- })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
+test('tenant can be in maintenance mode from command', function() {
+ Route::get('/foo', function () {
+ return 'bar';
+ })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
- $tenant = MaintenanceTenant::create();
- $tenant->domains()->create([
- 'domain' => 'acme.localhost',
- ]);
+ $tenant = MaintenanceTenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'acme.localhost',
+ ]);
- $this->get('http://acme.localhost/foo')
- ->assertSuccessful();
+ pest()->get('http://acme.localhost/foo')
+ ->assertSuccessful();
- tenancy()->end(); // flush stored tenant instance
+ tenancy()->end(); // Flush stored tenant instance
- Artisan::call('tenancy:down');
+ Artisan::call('tenancy:down');
- $this->expectException(HttpException::class);
- $this->withoutExceptionHandling()
- ->get('http://acme.localhost/foo');
+ pest()->expectException(HttpException::class);
+ pest()->withoutExceptionHandling()
+ ->get('http://acme.localhost/foo');
- tenancy()->end();
+ tenancy()->end();
- Artisan::call('tenancy:up');
+ Artisan::call('tenancy:up');
- tenancy()->end();
+ tenancy()->end();
- $this->get('http://acme.localhost/foo')
- ->assertSuccessful()
- ->assertSeeText('bar');
- }
-}
+ pest()->get('http://acme.localhost/foo')
+ ->assertSuccessful()
+ ->assertSeeText('bar');
+});
class MaintenanceTenant extends Tenant
{
diff --git a/tests/PathIdentificationTest.php b/tests/PathIdentificationTest.php
index 7a408ed0..bda0cfcb 100644
--- a/tests/PathIdentificationTest.php
+++ b/tests/PathIdentificationTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Exceptions\RouteIsMissingTenantParameterException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
@@ -11,138 +9,117 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class PathIdentificationTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
+beforeEach(function () {
+ PathTenantResolver::$tenantParameterName = 'tenant';
- PathTenantResolver::$tenantParameterName = 'tenant';
-
- Route::group([
- 'prefix' => '/{tenant}',
- 'middleware' => InitializeTenancyByPath::class,
- ], function () {
- Route::get('/foo/{a}/{b}', function ($a, $b) {
- return "$a + $b";
- });
+ Route::group([
+ 'prefix' => '/{tenant}',
+ 'middleware' => InitializeTenancyByPath::class,
+ ], function () {
+ Route::get('/foo/{a}/{b}', function ($a, $b) {
+ return "$a + $b";
});
- }
+ });
+});
- public function tearDown(): void
- {
- parent::tearDown();
+afterEach(function () {
+ // Global state cleanup
+ PathTenantResolver::$tenantParameterName = 'tenant';
+});
- // Global state cleanup
- PathTenantResolver::$tenantParameterName = 'tenant';
- }
+test('tenant can be identified by path', function () {
+ Tenant::create([
+ 'id' => 'acme',
+ ]);
- /** @test */
- public function tenant_can_be_identified_by_path()
- {
- Tenant::create([
- 'id' => 'acme',
- ]);
+ expect(tenancy()->initialized)->toBeFalse();
- $this->assertFalse(tenancy()->initialized);
+ pest()->get('/acme/foo/abc/xyz');
- $this->get('/acme/foo/abc/xyz');
+ expect(tenancy()->initialized)->toBeTrue();
+ expect(tenant('id'))->toBe('acme');
+});
- $this->assertTrue(tenancy()->initialized);
- $this->assertSame('acme', tenant('id'));
- }
+test('route actions dont get the tenant id', function () {
+ Tenant::create([
+ 'id' => 'acme',
+ ]);
- /** @test */
- public function route_actions_dont_get_the_tenant_id()
- {
- Tenant::create([
- 'id' => 'acme',
- ]);
+ expect(tenancy()->initialized)->toBeFalse();
- $this->assertFalse(tenancy()->initialized);
+ pest()
+ ->get('/acme/foo/abc/xyz')
+ ->assertContent('abc + xyz');
- $this
- ->get('/acme/foo/abc/xyz')
- ->assertContent('abc + xyz');
+ expect(tenancy()->initialized)->toBeTrue();
+ expect(tenant('id'))->toBe('acme');
+});
- $this->assertTrue(tenancy()->initialized);
- $this->assertSame('acme', tenant('id'));
- }
+test('exception is thrown when tenant cannot be identified by path', function () {
+ pest()->expectException(TenantCouldNotBeIdentifiedByPathException::class);
- /** @test */
- public function exception_is_thrown_when_tenant_cannot_be_identified_by_path()
- {
- $this->expectException(TenantCouldNotBeIdentifiedByPathException::class);
+ $this
+ ->withoutExceptionHandling()
+ ->get('/acme/foo/abc/xyz');
- $this
- ->withoutExceptionHandling()
- ->get('/acme/foo/abc/xyz');
+ expect(tenancy()->initialized)->toBeFalse();
+});
- $this->assertFalse(tenancy()->initialized);
- }
+test('onfail logic can be customized', function () {
+ InitializeTenancyByPath::$onFail = function () {
+ return 'foo';
+ };
- /** @test */
- public function onfail_logic_can_be_customized()
- {
- InitializeTenancyByPath::$onFail = function () {
- return 'foo';
- };
+ pest()
+ ->get('/acme/foo/abc/xyz')
+ ->assertContent('foo');
+});
- $this
- ->get('/acme/foo/abc/xyz')
- ->assertContent('foo');
- }
-
- /** @test */
- public function an_exception_is_thrown_when_the_routes_first_parameter_is_not_tenant()
- {
- Route::group([
- // 'prefix' => '/{tenant}', -- intentionally commented
- 'middleware' => InitializeTenancyByPath::class,
- ], function () {
- Route::get('/bar/{a}/{b}', function ($a, $b) {
- return "$a + $b";
- });
+test('an exception is thrown when the routes first parameter is not tenant', function () {
+ Route::group([
+ // 'prefix' => '/{tenant}', -- intentionally commented
+ 'middleware' => InitializeTenancyByPath::class,
+ ], function () {
+ Route::get('/bar/{a}/{b}', function ($a, $b) {
+ return "$a + $b";
});
+ });
- Tenant::create([
- 'id' => 'acme',
- ]);
+ Tenant::create([
+ 'id' => 'acme',
+ ]);
- $this->expectException(RouteIsMissingTenantParameterException::class);
+ pest()->expectException(RouteIsMissingTenantParameterException::class);
- $this
- ->withoutExceptionHandling()
- ->get('/bar/foo/bar');
- }
+ $this
+ ->withoutExceptionHandling()
+ ->get('/bar/foo/bar');
+});
- /** @test */
- public function tenant_parameter_name_can_be_customized()
- {
- PathTenantResolver::$tenantParameterName = 'team';
+test('tenant parameter name can be customized', function () {
+ PathTenantResolver::$tenantParameterName = 'team';
- Route::group([
- 'prefix' => '/{team}',
- 'middleware' => InitializeTenancyByPath::class,
- ], function () {
- Route::get('/bar/{a}/{b}', function ($a, $b) {
- return "$a + $b";
- });
+ Route::group([
+ 'prefix' => '/{team}',
+ 'middleware' => InitializeTenancyByPath::class,
+ ], function () {
+ Route::get('/bar/{a}/{b}', function ($a, $b) {
+ return "$a + $b";
});
+ });
- Tenant::create([
- 'id' => 'acme',
- ]);
+ Tenant::create([
+ 'id' => 'acme',
+ ]);
- $this
- ->get('/acme/bar/abc/xyz')
- ->assertContent('abc + xyz');
+ pest()
+ ->get('/acme/bar/abc/xyz')
+ ->assertContent('abc + xyz');
- // Parameter for resolver is changed, so the /{tenant}/foo route will no longer work.
- $this->expectException(RouteIsMissingTenantParameterException::class);
+ // Parameter for resolver is changed, so the /{tenant}/foo route will no longer work.
+ pest()->expectException(RouteIsMissingTenantParameterException::class);
- $this
- ->withoutExceptionHandling()
- ->get('/acme/foo/abc/xyz');
- }
-}
+ $this
+ ->withoutExceptionHandling()
+ ->get('/acme/foo/abc/xyz');
+});
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 00000000..d7ca8c22
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,10 @@
+in(__DIR__);
+
+function pest(): TestCase
+{
+ return Pest\TestSuite::getInstance()->test;
+}
diff --git a/tests/QueueTest.php b/tests/QueueTest.php
index afe64fea..c1fa24b8 100644
--- a/tests/QueueTest.php
+++ b/tests/QueueTest.php
@@ -2,248 +2,226 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
-use Exception;
-use Illuminate\Support\Str;
use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
use Spatie\Valuestore\Valuestore;
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Tests\Etc\User;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Event;
-use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Schema;
use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Jobs\CreateDatabase;
-use Illuminate\Queue\InteractsWithQueue;
use Stancl\Tenancy\Events\TenantCreated;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
-use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Foundation\Bus\Dispatchable;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
-class QueueTest extends TestCase
+beforeEach(function () {
+ $this->mockConsoleOutput = false;
+
+ config([
+ 'tenancy.bootstrappers' => [
+ QueueTenancyBootstrapper::class,
+ DatabaseTenancyBootstrapper::class,
+ ],
+ 'queue.default' => 'redis',
+ ]);
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+
+ createValueStore();
+});
+
+afterEach(function () {
+ pest()->valuestore->flush();
+});
+
+test('tenant id is passed to tenant queues', function () {
+ config(['queue.default' => 'sync']);
+
+ $tenant = Tenant::create();
+
+ tenancy()->initialize($tenant);
+
+ Event::fake([JobProcessing::class, JobProcessed::class]);
+
+ dispatch(new TestJob(pest()->valuestore));
+
+ Event::assertDispatched(JobProcessing::class, function ($event) {
+ return $event->job->payload()['tenant_id'] === tenant('id');
+ });
+});
+
+test('tenant id is not passed to central queues', function () {
+ $tenant = Tenant::create();
+
+ tenancy()->initialize($tenant);
+
+ Event::fake();
+
+ config(['queue.connections.central' => [
+ 'driver' => 'sync',
+ 'central' => true,
+ ]]);
+
+ dispatch(new TestJob(pest()->valuestore))->onConnection('central');
+
+ Event::assertDispatched(JobProcessing::class, function ($event) {
+ return ! isset($event->job->payload()['tenant_id']);
+ });
+});
+
+test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) {
+ withTenantDatabases();
+ withFailedJobs();
+
+ $tenant = Tenant::create();
+
+ tenancy()->initialize($tenant);
+
+ withUsers();
+
+ $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
+
+ pest()->valuestore->put('userName', 'Bar');
+
+ dispatch(new TestJob(pest()->valuestore, $user));
+
+ expect(pest()->valuestore->has('tenant_id'))->toBeFalse();
+
+ if ($shouldEndTenancy) {
+ tenancy()->end();
+ }
+
+ pest()->artisan('queue:work --once');
+
+ expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0);
+
+ expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id);
+
+ $tenant->run(function () use ($user) {
+ expect($user->fresh()->name)->toBe('Bar');
+ });
+})->with([true, false]);;
+
+test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy) {
+ withFailedJobs();
+ withTenantDatabases();
+
+ $tenant = Tenant::create();
+
+ tenancy()->initialize($tenant);
+
+ withUsers();
+
+ $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
+
+ pest()->valuestore->put('userName', 'Bar');
+ pest()->valuestore->put('shouldFail', true);
+
+ dispatch(new TestJob(pest()->valuestore, $user));
+
+ expect(pest()->valuestore->has('tenant_id'))->toBeFalse();
+
+ if ($shouldEndTenancy) {
+ tenancy()->end();
+ }
+
+ pest()->artisan('queue:work --once');
+
+ expect(DB::connection('central')->table('failed_jobs')->count())->toBe(1);
+ expect(pest()->valuestore->get('tenant_id'))->toBeNull(); // job failed
+
+ pest()->artisan('queue:retry all');
+ pest()->artisan('queue:work --once');
+
+ expect(DB::connection('central')->table('failed_jobs')->count())->toBe(0);
+
+ expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->id); // job succeeded
+
+ $tenant->run(function () use ($user) {
+ expect($user->fresh()->name)->toBe('Bar');
+ });
+})->with([true, false]);
+
+test('the tenant used by the job doesnt change when the current tenant changes', function () {
+ $tenant1 = Tenant::create([
+ 'id' => 'acme',
+ ]);
+
+ tenancy()->initialize($tenant1);
+
+ dispatch(new TestJob(pest()->valuestore));
+
+ $tenant2 = Tenant::create([
+ 'id' => 'foobar',
+ ]);
+
+ tenancy()->initialize($tenant2);
+
+ expect(pest()->valuestore->has('tenant_id'))->toBeFalse();
+ pest()->artisan('queue:work --once');
+
+ expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: acme');
+});
+
+function createValueStore(): void
{
- public $mockConsoleOutput = false;
+ $valueStorePath = __DIR__ . '/Etc/tmp/queuetest.json';
- /** @var Valuestore */
- protected $valuestore;
-
- public function setUp(): void
- {
- parent::setUp();
-
- config([
- 'tenancy.bootstrappers' => [
- QueueTenancyBootstrapper::class,
- DatabaseTenancyBootstrapper::class,
- ],
- 'queue.default' => 'redis',
- ]);
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
-
- $this->valuestore = Valuestore::make(__DIR__ . '/Etc/tmp/queuetest.json')->flush();
- }
-
- public function tearDown(): void
- {
- $this->valuestore->flush();
- }
-
- protected function withFailedJobs()
- {
- Schema::connection('central')->create('failed_jobs', function (Blueprint $table) {
- $table->increments('id');
- $table->string('uuid')->unique();
- $table->text('connection');
- $table->text('queue');
- $table->longText('payload');
- $table->longText('exception');
- $table->timestamp('failed_at')->useCurrent();
- });
- }
-
- protected function withUsers()
- {
- Schema::create('users', function (Blueprint $table) {
- $table->increments('id');
- $table->string('name');
- $table->string('email')->unique();
- $table->string('password');
- $table->rememberToken();
- $table->timestamps();
- });
- }
-
- protected function withTenantDatabases()
- {
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
- }
-
- /** @test */
- public function tenant_id_is_passed_to_tenant_queues()
- {
- config(['queue.default' => 'sync']);
-
- $tenant = Tenant::create();
-
- tenancy()->initialize($tenant);
-
- Event::fake([JobProcessing::class, JobProcessed::class]);
-
- dispatch(new TestJob($this->valuestore));
-
- Event::assertDispatched(JobProcessing::class, function ($event) {
- return $event->job->payload()['tenant_id'] === tenant('id');
- });
- }
-
- /** @test */
- public function tenant_id_is_not_passed_to_central_queues()
- {
- $tenant = Tenant::create();
-
- tenancy()->initialize($tenant);
-
- Event::fake();
-
- config(['queue.connections.central' => [
- 'driver' => 'sync',
- 'central' => true,
- ]]);
-
- dispatch(new TestJob($this->valuestore))->onConnection('central');
-
- Event::assertDispatched(JobProcessing::class, function ($event) {
- return ! isset($event->job->payload()['tenant_id']);
- });
- }
-
- /**
- * @test
- *
- * @testWith [true]
- * [false]
- */
- public function tenancy_is_initialized_inside_queues(bool $shouldEndTenancy)
- {
- $this->withTenantDatabases();
- $this->withFailedJobs();
-
- $tenant = Tenant::create();
-
- tenancy()->initialize($tenant);
-
- $this->withUsers();
-
- $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
-
- $this->valuestore->put('userName', 'Bar');
-
- dispatch(new TestJob($this->valuestore, $user));
-
- $this->assertFalse($this->valuestore->has('tenant_id'));
-
- if ($shouldEndTenancy) {
- tenancy()->end();
+ 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');
}
- $this->artisan('queue:work --once');
-
- $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count());
-
- $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id'));
-
- $tenant->run(function () use ($user) {
- $this->assertSame('Bar', $user->fresh()->name);
- });
+ file_put_contents($valueStorePath, '');
}
- /**
- * @test
- *
- * @testWith [true]
- * [false]
- */
- public function tenancy_is_initialized_when_retrying_jobs(bool $shouldEndTenancy)
- {
- if (! Str::startsWith(app()->version(), '8')) {
- $this->markTestSkipped('queue:retry tenancy is only supported in Laravel 8');
- }
+ pest()->valuestore = Valuestore::make($valueStorePath)->flush();
+}
- $this->withFailedJobs();
- $this->withTenantDatabases();
+function withFailedJobs()
+{
+ Schema::connection('central')->create('failed_jobs', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('uuid')->unique();
+ $table->text('connection');
+ $table->text('queue');
+ $table->longText('payload');
+ $table->longText('exception');
+ $table->timestamp('failed_at')->useCurrent();
+ });
+}
- $tenant = Tenant::create();
+function withUsers()
+{
+ Schema::create('users', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('name');
+ $table->string('email')->unique();
+ $table->string('password');
+ $table->rememberToken();
+ $table->timestamps();
+ });
+}
- tenancy()->initialize($tenant);
-
- $this->withUsers();
-
- $user = User::create(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
-
- $this->valuestore->put('userName', 'Bar');
- $this->valuestore->put('shouldFail', true);
-
- dispatch(new TestJob($this->valuestore, $user));
-
- $this->assertFalse($this->valuestore->has('tenant_id'));
-
- if ($shouldEndTenancy) {
- tenancy()->end();
- }
-
- $this->artisan('queue:work --once');
-
- $this->assertSame(1, DB::connection('central')->table('failed_jobs')->count());
- $this->assertNull($this->valuestore->get('tenant_id')); // job failed
-
- $this->artisan('queue:retry all');
- $this->artisan('queue:work --once');
-
- $this->assertSame(0, DB::connection('central')->table('failed_jobs')->count());
-
- $this->assertSame('The current tenant id is: ' . $tenant->id, $this->valuestore->get('tenant_id')); // job succeeded
-
- $tenant->run(function () use ($user) {
- $this->assertSame('Bar', $user->fresh()->name);
- });
- }
-
- /** @test */
- public function the_tenant_used_by_the_job_doesnt_change_when_the_current_tenant_changes()
- {
- $tenant1 = Tenant::create([
- 'id' => 'acme',
- ]);
-
- tenancy()->initialize($tenant1);
-
- dispatch(new TestJob($this->valuestore));
-
- $tenant2 = Tenant::create([
- 'id' => 'foobar',
- ]);
-
- tenancy()->initialize($tenant2);
-
- $this->assertFalse($this->valuestore->has('tenant_id'));
- $this->artisan('queue:work --once');
-
- $this->assertSame('The current tenant id is: acme', $this->valuestore->get('tenant_id'));
- }
+function withTenantDatabases()
+{
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
}
class TestJob implements ShouldQueue
diff --git a/tests/RequestDataIdentificationTest.php b/tests/RequestDataIdentificationTest.php
index 52a502f9..81bdda53 100644
--- a/tests/RequestDataIdentificationTest.php
+++ b/tests/RequestDataIdentificationTest.php
@@ -2,64 +2,49 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class RequestDataIdentificationTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
+beforeEach(function () {
+ config([
+ 'tenancy.central_domains' => [
+ 'localhost',
+ ],
+ ]);
- config([
- 'tenancy.central_domains' => [
- 'localhost',
- ],
- ]);
+ Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () {
+ return 'Tenant id: ' . tenant('id');
+ });
+});
- Route::middleware(InitializeTenancyByRequestData::class)->get('/test', function () {
- return 'Tenant id: ' . tenant('id');
- });
- }
+afterEach(function () {
+ InitializeTenancyByRequestData::$header = 'X-Tenant';
+ InitializeTenancyByRequestData::$queryParameter = 'tenant';
+});
- public function tearDown(): void
- {
- InitializeTenancyByRequestData::$header = 'X-Tenant';
- InitializeTenancyByRequestData::$queryParameter = 'tenant';
+test('header identification works', function () {
+ InitializeTenancyByRequestData::$header = 'X-Tenant';
+ $tenant = Tenant::create();
+ $tenant2 = Tenant::create();
- parent::tearDown();
- }
+ $this
+ ->withoutExceptionHandling()
+ ->get('test', [
+ 'X-Tenant' => $tenant->id,
+ ])
+ ->assertSee($tenant->id);
+});
- /** @test */
- public function header_identification_works()
- {
- InitializeTenancyByRequestData::$header = 'X-Tenant';
- $tenant = Tenant::create();
- $tenant2 = Tenant::create();
+test('query parameter identification works', function () {
+ InitializeTenancyByRequestData::$header = null;
+ InitializeTenancyByRequestData::$queryParameter = 'tenant';
- $this
- ->withoutExceptionHandling()
- ->get('test', [
- 'X-Tenant' => $tenant->id,
- ])
- ->assertSee($tenant->id);
- }
+ $tenant = Tenant::create();
+ $tenant2 = Tenant::create();
- /** @test */
- public function query_parameter_identification_works()
- {
- InitializeTenancyByRequestData::$header = null;
- InitializeTenancyByRequestData::$queryParameter = 'tenant';
-
- $tenant = Tenant::create();
- $tenant2 = Tenant::create();
-
- $this
- ->withoutExceptionHandling()
- ->get('test?tenant=' . $tenant->id)
- ->assertSee($tenant->id);
- }
-}
+ $this
+ ->withoutExceptionHandling()
+ ->get('test?tenant=' . $tenant->id)
+ ->assertSee($tenant->id);
+});
diff --git a/tests/ResourceSyncingTest.php b/tests/ResourceSyncingTest.php
index 570448d1..806e8706 100644
--- a/tests/ResourceSyncingTest.php
+++ b/tests/ResourceSyncingTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Events\CallQueuedListener;
@@ -30,549 +28,521 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Listeners\UpdateSyncedResource;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class ResourceSyncingTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
+beforeEach(function () {
+ config(['tenancy.bootstrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ]]);
- config(['tenancy.bootstrappers' => [
- DatabaseTenancyBootstrapper::class,
- ]]);
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
+ DatabaseConfig::generateDatabaseNamesUsing(function () {
+ return 'db' . Str::random(16);
+ });
- DatabaseConfig::generateDatabaseNamesUsing(function () {
- return 'db' . Str::random(16);
- });
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+ UpdateSyncedResource::$shouldQueue = false; // global state cleanup
+ Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class);
- UpdateSyncedResource::$shouldQueue = false; // global state cleanup
- Event::listen(SyncedResourceSaved::class, UpdateSyncedResource::class);
+ pest()->artisan('migrate', [
+ '--path' => [
+ __DIR__ . '/Etc/synced_resource_migrations',
+ __DIR__ . '/Etc/synced_resource_migrations/users',
+ ],
+ '--realpath' => true,
+ ])->assertExitCode(0);
+});
- $this->artisan('migrate', [
- '--path' => [
- __DIR__ . '/Etc/synced_resource_migrations',
- __DIR__ . '/Etc/synced_resource_migrations/users',
- ],
- '--realpath' => true,
- ])->assertExitCode(0);
- }
+test('an event is triggered when a synced resource is changed', function () {
+ Event::fake([SyncedResourceSaved::class]);
- protected function migrateTenants()
- {
- $this->artisan('tenants:migrate', [
- '--path' => __DIR__ . '/Etc/synced_resource_migrations/users',
- '--realpath' => true,
- ])->assertExitCode(0);
- }
+ $user = ResourceUser::create([
+ 'name' => 'Foo',
+ 'email' => 'foo@email.com',
+ 'password' => 'secret',
+ 'global_id' => 'foo',
+ 'role' => 'foo',
+ ]);
- /** @test */
- public function an_event_is_triggered_when_a_synced_resource_is_changed()
- {
- Event::fake([SyncedResourceSaved::class]);
+ Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) {
+ return $event->model === $user;
+ });
+});
- $user = ResourceUser::create([
- 'name' => 'Foo',
- 'email' => 'foo@email.com',
- 'password' => 'secret',
- 'global_id' => 'foo',
- 'role' => 'foo',
- ]);
+test('only the synced columns are updated in the central db', function () {
+ // Create user in central DB
+ $user = CentralUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'superadmin', // unsynced
+ ]);
- Event::assertDispatched(SyncedResourceSaved::class, function (SyncedResourceSaved $event) use ($user) {
- return $event->model === $user;
- });
- }
+ $tenant = ResourceTenant::create();
+ migrateTenantsResource();
- /** @test */
- public function only_the_synced_columns_are_updated_in_the_central_db()
- {
- // Create user in central DB
- $user = CentralUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'superadmin', // unsynced
- ]);
+ tenancy()->initialize($tenant);
- $tenant = ResourceTenant::create();
- $this->migrateTenants();
+ // Create the same user in tenant DB
+ $user = ResourceUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
- tenancy()->initialize($tenant);
+ // Update user in tenant DB
+ $user->update([
+ 'name' => 'John Foo', // synced
+ 'email' => 'john@foreignhost', // synced
+ 'role' => 'admin', // unsynced
+ ]);
- // Create the same user in tenant DB
- $user = ResourceUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'commenter', // unsynced
- ]);
+ // Assert new values
+ pest()->assertEquals([
+ 'id' => 1,
+ 'global_id' => 'acme',
+ 'name' => 'John Foo',
+ 'email' => 'john@foreignhost',
+ 'password' => 'secret',
+ 'role' => 'admin',
+ ], $user->getAttributes());
- // Update user in tenant DB
- $user->update([
- 'name' => 'John Foo', // synced
- 'email' => 'john@foreignhost', // synced
- 'role' => 'admin', // unsynced
- ]);
+ tenancy()->end();
- // Assert new values
- $this->assertEquals([
- 'id' => 1,
- 'global_id' => 'acme',
- 'name' => 'John Foo',
- 'email' => 'john@foreignhost',
- 'password' => 'secret',
- 'role' => 'admin',
- ], $user->getAttributes());
+ // Assert changes bubbled up
+ pest()->assertEquals([
+ 'id' => 1,
+ 'global_id' => 'acme',
+ 'name' => 'John Foo', // synced
+ 'email' => 'john@foreignhost', // synced
+ 'password' => 'secret', // no changes
+ 'role' => 'superadmin', // unsynced
+ ], ResourceUser::first()->getAttributes());
+});
- tenancy()->end();
+test('creating the resource in tenant database creates it in central database and creates the mapping', function () {
+ creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase();
+});
- // Assert changes bubbled up
- $this->assertEquals([
- 'id' => 1,
- 'global_id' => 'acme',
- 'name' => 'John Foo', // synced
- 'email' => 'john@foreignhost', // synced
- 'password' => 'secret', // no changes
- 'role' => 'superadmin', // unsynced
- ], ResourceUser::first()->getAttributes());
- }
+test('trying to update synced resources from central context using tenant models results in an exception', function () {
+ creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase();
- /** @test */
- public function creating_the_resource_in_tenant_database_creates_it_in_central_database_and_creates_the_mapping()
- {
- // Assert no user in central DB
- $this->assertCount(0, ResourceUser::all());
+ tenancy()->end();
+ expect(tenancy()->initialized)->toBeFalse();
- $tenant = ResourceTenant::create();
- $this->migrateTenants();
+ pest()->expectException(ModelNotSyncMasterException::class);
+ ResourceUser::first()->update(['role' => 'foobar']);
+});
- tenancy()->initialize($tenant);
+test('attaching a tenant to the central resource triggers a pull from the tenant db', function () {
+ $centralUser = CentralUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
- // Create the same user in tenant DB
+ $tenant = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
+ migrateTenantsResource();
+
+ $tenant->run(function () {
+ expect(ResourceUser::all())->toHaveCount(0);
+ });
+
+ $centralUser->tenants()->attach('t1');
+
+ $tenant->run(function () {
+ expect(ResourceUser::all())->toHaveCount(1);
+ });
+});
+
+test('attaching users to tenants does not do anything', function () {
+ $centralUser = CentralUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
+
+ $tenant = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
+ migrateTenantsResource();
+
+ $tenant->run(function () {
+ expect(ResourceUser::all())->toHaveCount(0);
+ });
+
+ // The child model is inaccessible in the Pivot Model, so we can't fire any events.
+ $tenant->users()->attach($centralUser);
+
+ $tenant->run(function () {
+ // Still zero
+ expect(ResourceUser::all())->toHaveCount(0);
+ });
+});
+
+test('resources are synced only to workspaces that have the resource', function () {
+ $centralUser = CentralUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
+
+ $t1 = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
+
+ $t2 = ResourceTenant::create([
+ 'id' => 't2',
+ ]);
+
+ $t3 = ResourceTenant::create([
+ 'id' => 't3',
+ ]);
+ migrateTenantsResource();
+
+ $centralUser->tenants()->attach('t1');
+ $centralUser->tenants()->attach('t2');
+ // t3 is not attached
+
+ $t1->run(function () {
+ // assert user exists
+ expect(ResourceUser::all())->toHaveCount(1);
+ });
+
+ $t2->run(function () {
+ // assert user exists
+ expect(ResourceUser::all())->toHaveCount(1);
+ });
+
+ $t3->run(function () {
+ // assert user does NOT exist
+ expect(ResourceUser::all())->toHaveCount(0);
+ });
+});
+
+test('when a resource exists in other tenant dbs but is created in a tenant db the synced columns are updated in the other dbs', function () {
+ // create shared resource
+ $centralUser = CentralUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
+
+ $t1 = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
+ $t2 = ResourceTenant::create([
+ 'id' => 't2',
+ ]);
+ migrateTenantsResource();
+
+ // Copy (cascade) user to t1 DB
+ $centralUser->tenants()->attach('t1');
+
+ $t2->run(function () {
+ // Create user with the same global ID in t2 database
ResourceUser::create([
'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
+ 'name' => 'John Foo', // changed
+ 'email' => 'john@foo', // changed
'password' => 'secret',
- 'role' => 'commenter', // unsynced
+ 'role' => 'superadmin', // unsynced
+ ]);
+ });
+
+ $centralUser = CentralUser::first();
+ expect($centralUser->name)->toBe('John Foo'); // name changed
+ expect($centralUser->email)->toBe('john@foo'); // email changed
+ expect($centralUser->role)->toBe('commenter'); // role didn't change
+
+ $t1->run(function () {
+ $user = ResourceUser::first();
+ expect($user->name)->toBe('John Foo'); // name changed
+ expect($user->email)->toBe('john@foo'); // email changed
+ expect($user->role)->toBe('commenter'); // role didn't change, i.e. is the same as from the original copy from central
+ });
+});
+
+test('the synced columns are updated in other tenant dbs where the resource exists', function () {
+ // create shared resource
+ $centralUser = CentralUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
+
+ $t1 = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
+ $t2 = ResourceTenant::create([
+ 'id' => 't2',
+ ]);
+ $t3 = ResourceTenant::create([
+ 'id' => 't3',
+ ]);
+ migrateTenantsResource();
+
+ // Copy (cascade) user to t1 DB
+ $centralUser->tenants()->attach('t1');
+ $centralUser->tenants()->attach('t2');
+ $centralUser->tenants()->attach('t3');
+
+ $t3->run(function () {
+ ResourceUser::first()->update([
+ 'name' => 'John 3',
+ 'role' => 'employee', // unsynced
]);
- tenancy()->end();
+ expect(ResourceUser::first()->role)->toBe('employee');
+ });
- // Asset user was created
- $this->assertSame('acme', CentralUser::first()->global_id);
- $this->assertSame('commenter', CentralUser::first()->role);
+ // Check that change was cascaded to other tenants
+ $t1->run($check = function () {
+ $user = ResourceUser::first();
- // Assert mapping was created
- $this->assertCount(1, CentralUser::first()->tenants);
+ expect($user->name)->toBe('John 3'); // synced
+ expect($user->role)->toBe('commenter'); // unsynced
+ });
+ $t2->run($check);
- // Assert role change doesn't cascade
- CentralUser::first()->update(['role' => 'central superadmin']);
- tenancy()->initialize($tenant);
- $this->assertSame('commenter', ResourceUser::first()->role);
- }
+ // Check that change bubbled up to central DB
+ expect(CentralUser::count())->toBe(1);
+ $centralUser = CentralUser::first();
+ expect($centralUser->name)->toBe('John 3'); // synced
+ expect($centralUser->role)->toBe('commenter'); // unsynced
+});
- /** @test */
- public function trying_to_update_synced_resources_from_central_context_using_tenant_models_results_in_an_exception()
- {
- $this->creating_the_resource_in_tenant_database_creates_it_in_central_database_and_creates_the_mapping();
+test('global id is generated using id generator when its not supplied', function () {
+ $user = CentralUser::create([
+ 'name' => 'John Doe',
+ 'email' => 'john@doe',
+ 'password' => 'secret',
+ 'role' => 'employee',
+ ]);
- tenancy()->end();
- $this->assertFalse(tenancy()->initialized);
+ pest()->assertNotNull($user->global_id);
+});
- $this->expectException(ModelNotSyncMasterException::class);
- ResourceUser::first()->update(['role' => 'foobar']);
- }
+test('when the resource doesnt exist in the tenant db non synced columns will cascade too', function () {
+ $centralUser = CentralUser::create([
+ 'name' => 'John Doe',
+ 'email' => 'john@doe',
+ 'password' => 'secret',
+ 'role' => 'employee',
+ ]);
- /** @test */
- public function attaching_a_tenant_to_the_central_resource_triggers_a_pull_from_the_tenant_db()
- {
- $centralUser = CentralUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'commenter', // unsynced
- ]);
+ $t1 = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
- $tenant = ResourceTenant::create([
- 'id' => 't1',
- ]);
- $this->migrateTenants();
+ migrateTenantsResource();
- $tenant->run(function () {
- $this->assertCount(0, ResourceUser::all());
- });
+ $centralUser->tenants()->attach('t1');
- $centralUser->tenants()->attach('t1');
+ $t1->run(function () {
+ expect(ResourceUser::first()->role)->toBe('employee');
+ });
+});
- $tenant->run(function () {
- $this->assertCount(1, ResourceUser::all());
- });
- }
+test('when the resource doesnt exist in the central db non synced columns will bubble up too', function () {
+ $t1 = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
- /** @test */
- public function attaching_users_to_tenants_DOES_NOT_DO_ANYTHING()
- {
- $centralUser = CentralUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'commenter', // unsynced
- ]);
+ migrateTenantsResource();
- $tenant = ResourceTenant::create([
- 'id' => 't1',
- ]);
- $this->migrateTenants();
-
- $tenant->run(function () {
- $this->assertCount(0, ResourceUser::all());
- });
-
- // The child model is inaccessible in the Pivot Model, so we can't fire any events.
- $tenant->users()->attach($centralUser);
-
- $tenant->run(function () {
- // Still zero
- $this->assertCount(0, ResourceUser::all());
- });
- }
-
- /** @test */
- public function resources_are_synced_only_to_workspaces_that_have_the_resource()
- {
- $centralUser = CentralUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'commenter', // unsynced
- ]);
-
- $t1 = ResourceTenant::create([
- 'id' => 't1',
- ]);
-
- $t2 = ResourceTenant::create([
- 'id' => 't2',
- ]);
-
- $t3 = ResourceTenant::create([
- 'id' => 't3',
- ]);
- $this->migrateTenants();
-
- $centralUser->tenants()->attach('t1');
- $centralUser->tenants()->attach('t2');
- // t3 is not attached
-
- $t1->run(function () {
- // assert user exists
- $this->assertCount(1, ResourceUser::all());
- });
-
- $t2->run(function () {
- // assert user exists
- $this->assertCount(1, ResourceUser::all());
- });
-
- $t3->run(function () {
- // assert user does NOT exist
- $this->assertCount(0, ResourceUser::all());
- });
- }
-
- /** @test */
- public function when_a_resource_exists_in_other_tenant_dbs_but_is_CREATED_in_a_tenant_db_the_synced_columns_are_updated_in_the_other_dbs()
- {
- // create shared resource
- $centralUser = CentralUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'commenter', // unsynced
- ]);
-
- $t1 = ResourceTenant::create([
- 'id' => 't1',
- ]);
- $t2 = ResourceTenant::create([
- 'id' => 't2',
- ]);
- $this->migrateTenants();
-
- // Copy (cascade) user to t1 DB
- $centralUser->tenants()->attach('t1');
-
- $t2->run(function () {
- // Create user with the same global ID in t2 database
- ResourceUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Foo', // changed
- 'email' => 'john@foo', // changed
- 'password' => 'secret',
- 'role' => 'superadmin', // unsynced
- ]);
- });
-
- $centralUser = CentralUser::first();
- $this->assertSame('John Foo', $centralUser->name); // name changed
- $this->assertSame('john@foo', $centralUser->email); // email changed
- $this->assertSame('commenter', $centralUser->role); // role didn't change
-
- $t1->run(function () {
- $user = ResourceUser::first();
- $this->assertSame('John Foo', $user->name); // name changed
- $this->assertSame('john@foo', $user->email); // email changed
- $this->assertSame('commenter', $user->role); // role didn't change, i.e. is the same as from the original copy from central
- });
- }
-
- /** @test */
- public function the_synced_columns_are_updated_in_other_tenant_dbs_where_the_resource_exists()
- {
- // create shared resource
- $centralUser = CentralUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'commenter', // unsynced
- ]);
-
- $t1 = ResourceTenant::create([
- 'id' => 't1',
- ]);
- $t2 = ResourceTenant::create([
- 'id' => 't2',
- ]);
- $t3 = ResourceTenant::create([
- 'id' => 't3',
- ]);
- $this->migrateTenants();
-
- // Copy (cascade) user to t1 DB
- $centralUser->tenants()->attach('t1');
- $centralUser->tenants()->attach('t2');
- $centralUser->tenants()->attach('t3');
-
- $t3->run(function () {
- ResourceUser::first()->update([
- 'name' => 'John 3',
- 'role' => 'employee', // unsynced
- ]);
-
- $this->assertSame('employee', ResourceUser::first()->role);
- });
-
- // Check that change was cascaded to other tenants
- $t1->run($check = function () {
- $user = ResourceUser::first();
-
- $this->assertSame('John 3', $user->name); // synced
- $this->assertSame('commenter', $user->role); // unsynced
- });
- $t2->run($check);
-
- // Check that change bubbled up to central DB
- $this->assertSame(1, CentralUser::count());
- $centralUser = CentralUser::first();
- $this->assertSame('John 3', $centralUser->name); // synced
- $this->assertSame('commenter', $centralUser->role); // unsynced
- }
-
- /** @test */
- public function global_id_is_generated_using_id_generator_when_its_not_supplied()
- {
- $user = CentralUser::create([
+ $t1->run(function () {
+ ResourceUser::create([
'name' => 'John Doe',
'email' => 'john@doe',
'password' => 'secret',
'role' => 'employee',
]);
+ });
- $this->assertNotNull($user->global_id);
- }
+ expect(CentralUser::first()->role)->toBe('employee');
+});
- /** @test */
- public function when_the_resource_doesnt_exist_in_the_tenant_db_non_synced_columns_will_cascade_too()
- {
- $centralUser = CentralUser::create([
+test('the listener can be queued', function () {
+ Queue::fake();
+ UpdateSyncedResource::$shouldQueue = true;
+
+ $t1 = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
+
+ migrateTenantsResource();
+
+ Queue::assertNothingPushed();
+
+ $t1->run(function () {
+ ResourceUser::create([
'name' => 'John Doe',
'email' => 'john@doe',
'password' => 'secret',
'role' => 'employee',
]);
+ });
- $t1 = ResourceTenant::create([
- 'id' => 't1',
+ Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
+ return $job->class === UpdateSyncedResource::class;
+ });
+});
+
+test('an event is fired for all touched resources', function () {
+ Event::fake([SyncedResourceChangedInForeignDatabase::class]);
+
+ // create shared resource
+ $centralUser = CentralUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
+
+ $t1 = ResourceTenant::create([
+ 'id' => 't1',
+ ]);
+ $t2 = ResourceTenant::create([
+ 'id' => 't2',
+ ]);
+ $t3 = ResourceTenant::create([
+ 'id' => 't3',
+ ]);
+ migrateTenantsResource();
+
+ // Copy (cascade) user to t1 DB
+ $centralUser->tenants()->attach('t1');
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return $event->tenant->getTenantKey() === 't1';
+ });
+
+ $centralUser->tenants()->attach('t2');
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return $event->tenant->getTenantKey() === 't2';
+ });
+
+ $centralUser->tenants()->attach('t3');
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return $event->tenant->getTenantKey() === 't3';
+ });
+
+ // Assert no event for central
+ Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return $event->tenant === null;
+ });
+
+ // Flush
+ Event::fake([SyncedResourceChangedInForeignDatabase::class]);
+
+ $t3->run(function () {
+ ResourceUser::first()->update([
+ 'name' => 'John 3',
+ 'role' => 'employee', // unsynced
]);
- $this->migrateTenants();
+ expect(ResourceUser::first()->role)->toBe('employee');
+ });
- $centralUser->tenants()->attach('t1');
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return optional($event->tenant)->getTenantKey() === 't1';
+ });
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return optional($event->tenant)->getTenantKey() === 't2';
+ });
- $t1->run(function () {
- $this->assertSame('employee', ResourceUser::first()->role);
- });
- }
+ // Assert NOT dispatched in t3
+ Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return optional($event->tenant)->getTenantKey() === 't3';
+ });
- /** @test */
- public function when_the_resource_doesnt_exist_in_the_central_db_non_synced_columns_will_bubble_up_too()
- {
- $t1 = ResourceTenant::create([
- 'id' => 't1',
- ]);
+ // Assert dispatched in central
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return $event->tenant === null;
+ });
- $this->migrateTenants();
+ // Flush
+ Event::fake([SyncedResourceChangedInForeignDatabase::class]);
- $t1->run(function () {
- ResourceUser::create([
- 'name' => 'John Doe',
- 'email' => 'john@doe',
- 'password' => 'secret',
- 'role' => 'employee',
- ]);
- });
+ $centralUser->update([
+ 'name' => 'John Central',
+ ]);
- $this->assertSame('employee', CentralUser::first()->role);
- }
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return optional($event->tenant)->getTenantKey() === 't1';
+ });
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return optional($event->tenant)->getTenantKey() === 't2';
+ });
+ Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return optional($event->tenant)->getTenantKey() === 't3';
+ });
+ // Assert NOT dispatched in central
+ Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
+ return $event->tenant === null;
+ });
+});
- /** @test */
- public function the_listener_can_be_queued()
- {
- Queue::fake();
- UpdateSyncedResource::$shouldQueue = true;
+// todo@tests
+function creatingResourceInTenantDatabaseCreatesAndMapInCentralDatabase()
+{
+ // Assert no user in central DB
+ expect(ResourceUser::all())->toHaveCount(0);
- $t1 = ResourceTenant::create([
- 'id' => 't1',
- ]);
+ $tenant = ResourceTenant::create();
+ migrateTenantsResource();
- $this->migrateTenants();
+ tenancy()->initialize($tenant);
- Queue::assertNothingPushed();
+ // Create the same user in tenant DB
+ ResourceUser::create([
+ 'global_id' => 'acme',
+ 'name' => 'John Doe',
+ 'email' => 'john@localhost',
+ 'password' => 'secret',
+ 'role' => 'commenter', // unsynced
+ ]);
- $t1->run(function () {
- ResourceUser::create([
- 'name' => 'John Doe',
- 'email' => 'john@doe',
- 'password' => 'secret',
- 'role' => 'employee',
- ]);
- });
+ tenancy()->end();
- Queue::assertPushed(CallQueuedListener::class, function (CallQueuedListener $job) {
- return $job->class === UpdateSyncedResource::class;
- });
- }
+ // Asset user was created
+ expect(CentralUser::first()->global_id)->toBe('acme');
+ expect(CentralUser::first()->role)->toBe('commenter');
- /** @test */
- public function an_event_is_fired_for_all_touched_resources()
- {
- Event::fake([SyncedResourceChangedInForeignDatabase::class]);
+ // Assert mapping was created
+ expect(CentralUser::first()->tenants)->toHaveCount(1);
- // create shared resource
- $centralUser = CentralUser::create([
- 'global_id' => 'acme',
- 'name' => 'John Doe',
- 'email' => 'john@localhost',
- 'password' => 'secret',
- 'role' => 'commenter', // unsynced
- ]);
+ // Assert role change doesn't cascade
+ CentralUser::first()->update(['role' => 'central superadmin']);
+ tenancy()->initialize($tenant);
+ expect(ResourceUser::first()->role)->toBe('commenter');
+}
- $t1 = ResourceTenant::create([
- 'id' => 't1',
- ]);
- $t2 = ResourceTenant::create([
- 'id' => 't2',
- ]);
- $t3 = ResourceTenant::create([
- 'id' => 't3',
- ]);
- $this->migrateTenants();
-
- // Copy (cascade) user to t1 DB
- $centralUser->tenants()->attach('t1');
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return $event->tenant->getTenantKey() === 't1';
- });
-
- $centralUser->tenants()->attach('t2');
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return $event->tenant->getTenantKey() === 't2';
- });
-
- $centralUser->tenants()->attach('t3');
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return $event->tenant->getTenantKey() === 't3';
- });
-
- // Assert no event for central
- Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return $event->tenant === null;
- });
-
- // Flush
- Event::fake([SyncedResourceChangedInForeignDatabase::class]);
-
- $t3->run(function () {
- ResourceUser::first()->update([
- 'name' => 'John 3',
- 'role' => 'employee', // unsynced
- ]);
-
- $this->assertSame('employee', ResourceUser::first()->role);
- });
-
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return optional($event->tenant)->getTenantKey() === 't1';
- });
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return optional($event->tenant)->getTenantKey() === 't2';
- });
-
- // Assert NOT dispatched in t3
- Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return optional($event->tenant)->getTenantKey() === 't3';
- });
-
- // Assert dispatched in central
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return $event->tenant === null;
- });
-
- // Flush
- Event::fake([SyncedResourceChangedInForeignDatabase::class]);
-
- $centralUser->update([
- 'name' => 'John Central',
- ]);
-
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return optional($event->tenant)->getTenantKey() === 't1';
- });
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return optional($event->tenant)->getTenantKey() === 't2';
- });
- Event::assertDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return optional($event->tenant)->getTenantKey() === 't3';
- });
- // Assert NOT dispatched in central
- Event::assertNotDispatched(SyncedResourceChangedInForeignDatabase::class, function (SyncedResourceChangedInForeignDatabase $event) {
- return $event->tenant === null;
- });
- }
+function migrateTenantsResource()
+{
+ pest()->artisan('tenants:migrate', [
+ '--path' => __DIR__ . '/Etc/synced_resource_migrations/users',
+ '--realpath' => true,
+ ])->assertExitCode(0);
}
class ResourceTenant extends Tenant
@@ -589,7 +559,9 @@ class CentralUser extends Model implements SyncMaster
use ResourceSyncing, CentralConnection;
protected $guarded = [];
+
public $timestamps = false;
+
public $table = 'users';
public function tenants(): BelongsToMany
@@ -633,7 +605,9 @@ class ResourceUser extends Model implements Syncable
use ResourceSyncing;
protected $table = 'users';
+
protected $guarded = [];
+
public $timestamps = false;
public function getGlobalIdentifierKey()
diff --git a/tests/ScopeSessionsTest.php b/tests/ScopeSessionsTest.php
index b5fb962a..27fa911f 100644
--- a/tests/ScopeSessionsTest.php
+++ b/tests/ScopeSessionsTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
@@ -13,69 +11,57 @@ use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Middleware\ScopeSessions;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class ScopeSessionsTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
-
- Route::group([
- 'middleware' => [StartSession::class, InitializeTenancyBySubdomain::class, ScopeSessions::class],
- ], function () {
- Route::get('/foo', function () {
- return 'true';
- });
+beforeEach(function () {
+ Route::group([
+ 'middleware' => [StartSession::class, InitializeTenancyBySubdomain::class, ScopeSessions::class],
+ ], function () {
+ Route::get('/foo', function () {
+ return 'true';
});
+ });
- Event::listen(TenantCreated::class, function (TenantCreated $event) {
- $tenant = $event->tenant;
+ Event::listen(TenantCreated::class, function (TenantCreated $event) {
+ $tenant = $event->tenant;
- /** @var Tenant $tenant */
- $tenant->domains()->create([
- 'domain' => $tenant->id,
- ]);
- });
- }
-
- /** @test */
- public function tenant_id_is_auto_added_to_session_if_its_missing()
- {
- $tenant = Tenant::create([
- 'id' => 'acme',
+ /** @var Tenant $tenant */
+ $tenant->domains()->create([
+ 'domain' => $tenant->id,
]);
+ });
+});
- $this->get('http://acme.localhost/foo')
- ->assertSessionHas(ScopeSessions::$tenantIdKey, 'acme');
- }
+test('tenant id is auto added to session if its missing', function () {
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ ]);
- /** @test */
- public function changing_tenant_id_in_session_will_abort_the_request()
- {
- $tenant = Tenant::create([
- 'id' => 'acme',
- ]);
+ pest()->get('http://acme.localhost/foo')
+ ->assertSessionHas(ScopeSessions::$tenantIdKey, 'acme');
+});
- $this->get('http://acme.localhost/foo')
- ->assertSuccessful();
+test('changing tenant id in session will abort the request', function () {
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ ]);
- session()->put(ScopeSessions::$tenantIdKey, 'foobar');
+ pest()->get('http://acme.localhost/foo')
+ ->assertSuccessful();
- $this->get('http://acme.localhost/foo')
- ->assertStatus(403);
- }
+ session()->put(ScopeSessions::$tenantIdKey, 'foobar');
- /** @test */
- public function an_exception_is_thrown_when_the_middleware_is_executed_before_tenancy_is_initialized()
- {
- Route::get('/bar', function () {
- return true;
- })->middleware([StartSession::class, ScopeSessions::class]);
+ pest()->get('http://acme.localhost/foo')
+ ->assertStatus(403);
+});
- $tenant = Tenant::create([
- 'id' => 'acme',
- ]);
+test('an exception is thrown when the middleware is executed before tenancy is initialized', function () {
+ Route::get('/bar', function () {
+ return true;
+ })->middleware([StartSession::class, ScopeSessions::class]);
- $this->expectException(TenancyNotInitializedException::class);
- $this->withoutExceptionHandling()->get('http://acme.localhost/bar');
- }
-}
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ ]);
+
+ pest()->expectException(TenancyNotInitializedException::class);
+ pest()->withoutExceptionHandling()->get('http://acme.localhost/bar');
+});
diff --git a/tests/SingleDatabaseTenancyTest.php b/tests/SingleDatabaseTenancyTest.php
index b64478cc..34b12383 100644
--- a/tests/SingleDatabaseTenancyTest.php
+++ b/tests/SingleDatabaseTenancyTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
@@ -14,309 +12,293 @@ use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules;
use Stancl\Tenancy\Tests\Etc\Tenant as TestTenant;
-class SingleDatabaseTenancyTest extends TestCase
+beforeEach(function () {
+ BelongsToTenant::$tenantIdColumn = 'tenant_id';
+
+ Schema::create('posts', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('text');
+
+ $table->string('tenant_id');
+
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
+ });
+
+ Schema::create('comments', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('text');
+
+ $table->unsignedInteger('post_id');
+
+ $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade');
+ });
+
+ config(['tenancy.tenant_model' => Tenant::class]);
+});
+
+test('primary models are scoped to the current tenant', function () {
+ primaryModelsScopedToCurrentTenant();
+});
+
+test('primary models are not scoped in the central context', function () {
+ primaryModelsScopedToCurrentTenant();
+
+ tenancy()->end();
+
+ expect(Post::count())->toBe(2);
+});
+
+test('secondary models are scoped to the current tenant when accessed via primary model', function () {
+ secondaryModelsAreScopedToCurrentTenant();
+});
+
+test('secondary models are not scoped to the current tenant when accessed directly', function () {
+ secondaryModelsAreScopedToCurrentTenant();
+
+ // We're in acme context
+ expect(tenant('id'))->toBe('acme');
+
+ expect(Comment::count())->toBe(2);
+});
+
+test('secondary models a r e scoped to the current tenant when accessed directly and parent relationship traitis used', function () {
+ $acme = Tenant::create([
+ 'id' => 'acme',
+ ]);
+
+ $acme->run(function () {
+ $post = Post::create(['text' => 'Foo']);
+ $post->scoped_comments()->create(['text' => 'Comment Text']);
+
+ expect(Post::count())->toBe(1);
+ expect(ScopedComment::count())->toBe(1);
+ });
+
+ $foobar = Tenant::create([
+ 'id' => 'foobar',
+ ]);
+
+ $foobar->run(function () {
+ expect(Post::count())->toBe(0);
+ expect(ScopedComment::count())->toBe(0);
+
+ $post = Post::create(['text' => 'Bar']);
+ $post->scoped_comments()->create(['text' => 'Comment Text 2']);
+
+ expect(Post::count())->toBe(1);
+ expect(ScopedComment::count())->toBe(1);
+ });
+
+ // Global context
+ expect(ScopedComment::count())->toBe(2);
+});
+
+test('secondary models are not scoped in the central context', function () {
+ secondaryModelsAreScopedToCurrentTenant();
+
+ tenancy()->end();
+
+ expect(Comment::count())->toBe(2);
+});
+
+test('global models are not scoped at all', function () {
+ Schema::create('global_resources', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('text');
+ });
+
+ GlobalResource::create(['text' => 'First']);
+ GlobalResource::create(['text' => 'Second']);
+
+ $acme = Tenant::create([
+ 'id' => 'acme',
+ ]);
+
+ $acme->run(function () {
+ expect(GlobalResource::count())->toBe(2);
+
+ GlobalResource::create(['text' => 'Third']);
+ GlobalResource::create(['text' => 'Fourth']);
+ });
+
+ expect(GlobalResource::count())->toBe(4);
+});
+
+test('tenant id and relationship is auto added when creating primary resources in tenant context', function () {
+ tenancy()->initialize($acme = Tenant::create([
+ 'id' => 'acme',
+ ]));
+
+ $post = Post::create(['text' => 'Foo']);
+
+ expect($post->tenant_id)->toBe('acme');
+ expect($post->relationLoaded('tenant'))->toBeTrue();
+ expect($post->tenant)->toBe($acme);
+ expect($post->tenant)->toBe(tenant());
+});
+
+test('tenant id is not auto added when creating primary resources in central context', function () {
+ pest()->expectException(QueryException::class);
+
+ Post::create(['text' => 'Foo']);
+});
+
+test('tenant id column name can be customized', function () {
+ BelongsToTenant::$tenantIdColumn = 'team_id';
+
+ Schema::drop('comments');
+ Schema::drop('posts');
+ Schema::create('posts', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('text');
+
+ $table->string('team_id');
+
+ $table->foreign('team_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
+ });
+
+ tenancy()->initialize($acme = Tenant::create([
+ 'id' => 'acme',
+ ]));
+
+ $post = Post::create(['text' => 'Foo']);
+
+ expect($post->team_id)->toBe('acme');
+
+ // ======================================
+ // foobar context
+ tenancy()->initialize($foobar = Tenant::create([
+ 'id' => 'foobar',
+ ]));
+
+ $post = Post::create(['text' => 'Bar']);
+
+ expect($post->team_id)->toBe('foobar');
+
+ $post = Post::first();
+
+ expect($post->team_id)->toBe('foobar');
+
+ // ======================================
+ // acme context again
+
+ tenancy()->initialize($acme);
+
+ $post = Post::first();
+ expect($post->team_id)->toBe('acme');
+
+ // Assert foobar models are inaccessible in acme context
+ expect(Post::count())->toBe(1);
+});
+
+test('the model returned by the tenant helper has unique and exists validation rules', function () {
+ Schema::table('posts', function (Blueprint $table) {
+ $table->string('slug')->nullable();
+ $table->unique(['tenant_id', 'slug']);
+ });
+
+ tenancy()->initialize($acme = Tenant::create([
+ 'id' => 'acme',
+ ]));
+
+ Post::create(['text' => 'Foo', 'slug' => 'foo']);
+ $data = ['text' => 'Foo 2', 'slug' => 'foo'];
+
+ $uniqueFails = Validator::make($data, [
+ 'slug' => 'unique:posts',
+ ])->fails();
+ $existsFails = Validator::make($data, [
+ 'slug' => 'exists:posts',
+ ])->fails();
+
+ // Assert that 'unique' and 'exists' aren't scoped by default
+ // pest()->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
+ // pest()->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
+
+ $uniqueFails = Validator::make($data, [
+ 'slug' => tenant()->unique('posts'),
+ ])->fails();
+ $existsFails = Validator::make($data, [
+ 'slug' => tenant()->exists('posts'),
+ ])->fails();
+
+ // Assert that tenant()->unique() and tenant()->exists() are scoped
+ expect($uniqueFails)->toBeTrue();
+ expect($existsFails)->toBeFalse();
+});
+
+// todo@tests
+function primaryModelsScopedToCurrentTenant()
{
- public function setUp(): void
- {
- parent::setUp();
+ // acme context
+ tenancy()->initialize($acme = Tenant::create([
+ 'id' => 'acme',
+ ]));
- BelongsToTenant::$tenantIdColumn = 'tenant_id';
+ $post = Post::create(['text' => 'Foo']);
- Schema::create('posts', function (Blueprint $table) {
- $table->increments('id');
- $table->string('text');
+ expect($post->tenant_id)->toBe('acme');
+ expect($post->tenant->id)->toBe('acme');
- $table->string('tenant_id');
+ $post = Post::first();
- $table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
- });
+ expect($post->tenant_id)->toBe('acme');
+ expect($post->tenant->id)->toBe('acme');
- Schema::create('comments', function (Blueprint $table) {
- $table->increments('id');
- $table->string('text');
+ // ======================================
+ // foobar context
+ tenancy()->initialize($foobar = Tenant::create([
+ 'id' => 'foobar',
+ ]));
- $table->unsignedInteger('post_id');
+ $post = Post::create(['text' => 'Bar']);
- $table->foreign('post_id')->references('id')->on('posts')->onUpdate('cascade')->onDelete('cascade');
- });
+ expect($post->tenant_id)->toBe('foobar');
+ expect($post->tenant->id)->toBe('foobar');
- config(['tenancy.tenant_model' => Tenant::class]);
- }
+ $post = Post::first();
- /** @test */
- public function primary_models_are_scoped_to_the_current_tenant()
- {
- // acme context
- tenancy()->initialize($acme = Tenant::create([
- 'id' => 'acme',
- ]));
+ expect($post->tenant_id)->toBe('foobar');
+ expect($post->tenant->id)->toBe('foobar');
- $post = Post::create(['text' => 'Foo']);
+ // ======================================
+ // acme context again
- $this->assertSame('acme', $post->tenant_id);
- $this->assertSame('acme', $post->tenant->id);
+ tenancy()->initialize($acme);
- $post = Post::first();
+ $post = Post::first();
+ expect($post->tenant_id)->toBe('acme');
+ expect($post->tenant->id)->toBe('acme');
- $this->assertSame('acme', $post->tenant_id);
- $this->assertSame('acme', $post->tenant->id);
+ // Assert foobar models are inaccessible in acme context
+ expect(Post::count())->toBe(1);
+}
- // ======================================
- // foobar context
- tenancy()->initialize($foobar = Tenant::create([
- 'id' => 'foobar',
- ]));
+// todo@tests
+function secondaryModelsAreScopedToCurrentTenant()
+{
+ // acme context
+ tenancy()->initialize($acme = Tenant::create([
+ 'id' => 'acme',
+ ]));
- $post = Post::create(['text' => 'Bar']);
+ $post = Post::create(['text' => 'Foo']);
+ $post->comments()->create(['text' => 'Comment text']);
- $this->assertSame('foobar', $post->tenant_id);
- $this->assertSame('foobar', $post->tenant->id);
+ // ================
+ // foobar context
+ tenancy()->initialize($foobar = Tenant::create([
+ 'id' => 'foobar',
+ ]));
- $post = Post::first();
+ $post = Post::create(['text' => 'Bar']);
+ $post->comments()->create(['text' => 'Comment text 2']);
- $this->assertSame('foobar', $post->tenant_id);
- $this->assertSame('foobar', $post->tenant->id);
-
- // ======================================
- // acme context again
-
- tenancy()->initialize($acme);
-
- $post = Post::first();
- $this->assertSame('acme', $post->tenant_id);
- $this->assertSame('acme', $post->tenant->id);
-
- // Assert foobar models are inaccessible in acme context
- $this->assertSame(1, Post::count());
- }
-
- /** @test */
- public function primary_models_are_not_scoped_in_the_central_context()
- {
- $this->primary_models_are_scoped_to_the_current_tenant();
-
- tenancy()->end();
-
- $this->assertSame(2, Post::count());
- }
-
- /** @test */
- public function secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model()
- {
- // acme context
- tenancy()->initialize($acme = Tenant::create([
- 'id' => 'acme',
- ]));
-
- $post = Post::create(['text' => 'Foo']);
- $post->comments()->create(['text' => 'Comment text']);
-
- // ================
- // foobar context
- tenancy()->initialize($foobar = Tenant::create([
- 'id' => 'foobar',
- ]));
-
- $post = Post::create(['text' => 'Bar']);
- $post->comments()->create(['text' => 'Comment text 2']);
-
- // ================
- // acme context again
- tenancy()->initialize($acme);
- $this->assertSame(1, Post::count());
- $this->assertSame(1, Post::first()->comments->count());
- }
-
- /** @test */
- public function secondary_models_are_NOT_scoped_to_the_current_tenant_when_accessed_directly()
- {
- $this->secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model();
-
- // We're in acme context
- $this->assertSame('acme', tenant('id'));
-
- $this->assertSame(2, Comment::count());
- }
-
- /** @test */
- public function secondary_models_ARE_scoped_to_the_current_tenant_when_accessed_directly_AND_PARENT_RELATIONSHIP_TRAIT_IS_USED()
- {
- $acme = Tenant::create([
- 'id' => 'acme',
- ]);
-
- $acme->run(function () {
- $post = Post::create(['text' => 'Foo']);
- $post->scoped_comments()->create(['text' => 'Comment Text']);
-
- $this->assertSame(1, Post::count());
- $this->assertSame(1, ScopedComment::count());
- });
-
- $foobar = Tenant::create([
- 'id' => 'foobar',
- ]);
-
- $foobar->run(function () {
- $this->assertSame(0, Post::count());
- $this->assertSame(0, ScopedComment::count());
-
- $post = Post::create(['text' => 'Bar']);
- $post->scoped_comments()->create(['text' => 'Comment Text 2']);
-
- $this->assertSame(1, Post::count());
- $this->assertSame(1, ScopedComment::count());
- });
-
- // Global context
- $this->assertSame(2, ScopedComment::count());
- }
-
- /** @test */
- public function secondary_models_are_NOT_scoped_in_the_central_context()
- {
- $this->secondary_models_are_scoped_to_the_current_tenant_when_accessed_via_primary_model();
-
- tenancy()->end();
-
- $this->assertSame(2, Comment::count());
- }
-
- /** @test */
- public function global_models_are_not_scoped_at_all()
- {
- Schema::create('global_resources', function (Blueprint $table) {
- $table->increments('id');
- $table->string('text');
- });
-
- GlobalResource::create(['text' => 'First']);
- GlobalResource::create(['text' => 'Second']);
-
- $acme = Tenant::create([
- 'id' => 'acme',
- ]);
-
- $acme->run(function () {
- $this->assertSame(2, GlobalResource::count());
-
- GlobalResource::create(['text' => 'Third']);
- GlobalResource::create(['text' => 'Fourth']);
- });
-
- $this->assertSame(4, GlobalResource::count());
- }
-
- /** @test */
- public function tenant_id_and_relationship_is_auto_added_when_creating_primary_resources_in_tenant_context()
- {
- tenancy()->initialize($acme = Tenant::create([
- 'id' => 'acme',
- ]));
-
- $post = Post::create(['text' => 'Foo']);
-
- $this->assertSame('acme', $post->tenant_id);
- $this->assertTrue($post->relationLoaded('tenant'));
- $this->assertSame($acme, $post->tenant);
- $this->assertSame(tenant(), $post->tenant);
- }
-
- /** @test */
- public function tenant_id_is_not_auto_added_when_creating_primary_resources_in_central_context()
- {
- $this->expectException(QueryException::class);
-
- Post::create(['text' => 'Foo']);
- }
-
- /** @test */
- public function tenant_id_column_name_can_be_customized()
- {
- BelongsToTenant::$tenantIdColumn = 'team_id';
-
- Schema::drop('comments');
- Schema::drop('posts');
- Schema::create('posts', function (Blueprint $table) {
- $table->increments('id');
- $table->string('text');
-
- $table->string('team_id');
-
- $table->foreign('team_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
- });
-
- tenancy()->initialize($acme = Tenant::create([
- 'id' => 'acme',
- ]));
-
- $post = Post::create(['text' => 'Foo']);
-
- $this->assertSame('acme', $post->team_id);
-
- // ======================================
- // foobar context
- tenancy()->initialize($foobar = Tenant::create([
- 'id' => 'foobar',
- ]));
-
- $post = Post::create(['text' => 'Bar']);
-
- $this->assertSame('foobar', $post->team_id);
-
- $post = Post::first();
-
- $this->assertSame('foobar', $post->team_id);
-
- // ======================================
- // acme context again
-
- tenancy()->initialize($acme);
-
- $post = Post::first();
- $this->assertSame('acme', $post->team_id);
-
- // Assert foobar models are inaccessible in acme context
- $this->assertSame(1, Post::count());
- }
-
- /** @test */
- public function the_model_returned_by_the_tenant_helper_has_unique_and_exists_validation_rules()
- {
- Schema::table('posts', function (Blueprint $table) {
- $table->string('slug')->nullable();
- $table->unique(['tenant_id', 'slug']);
- });
-
- tenancy()->initialize($acme = Tenant::create([
- 'id' => 'acme',
- ]));
-
- Post::create(['text' => 'Foo', 'slug' => 'foo']);
- $data = ['text' => 'Foo 2', 'slug' => 'foo'];
-
- $uniqueFails = Validator::make($data, [
- 'slug' => 'unique:posts',
- ])->fails();
- $existsFails = Validator::make($data, [
- 'slug' => 'exists:posts',
- ])->fails();
-
- // Assert that 'unique' and 'exists' aren't scoped by default
- // $this->assertFalse($uniqueFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
- // $this->assertTrue($existsFails); // todo get these two assertions to pass. for some reason, the validator is passing for both 'unique' and 'exists'
-
- $uniqueFails = Validator::make($data, [
- 'slug' => tenant()->unique('posts'),
- ])->fails();
- $existsFails = Validator::make($data, [
- 'slug' => tenant()->exists('posts'),
- ])->fails();
-
- // Assert that tenant()->unique() and tenant()->exists() are scoped
- $this->assertTrue($uniqueFails);
- $this->assertFalse($existsFails);
- }
+ // ================
+ // acme context again
+ tenancy()->initialize($acme);
+ expect(Post::count())->toBe(1);
+ expect(Post::first()->comments->count())->toBe(1);
}
class Tenant extends TestTenant
@@ -329,6 +311,7 @@ class Post extends Model
use BelongsToTenant;
protected $guarded = [];
+
public $timestamps = false;
public function comments()
@@ -345,6 +328,7 @@ class Post extends Model
class Comment extends Model
{
protected $guarded = [];
+
public $timestamps = false;
public function post()
@@ -368,5 +352,6 @@ class ScopedComment extends Comment
class GlobalResource extends Model
{
protected $guarded = [];
+
public $timestamps = false;
}
diff --git a/tests/SubdomainTest.php b/tests/SubdomainTest.php
index 17fbc1b3..00096d8c 100644
--- a/tests/SubdomainTest.php
+++ b/tests/SubdomainTest.php
@@ -2,151 +2,129 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Concerns\HasDomains;
-use Stancl\Tenancy\Database\Models;
use Stancl\Tenancy\Exceptions\NotASubdomainException;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
+use Stancl\Tenancy\Database\Models;
-class SubdomainTest extends TestCase
-{
- public function setUp(): void
- {
- parent::setUp();
+beforeEach(function () {
+ // Global state cleanup after some tests
+ InitializeTenancyBySubdomain::$onFail = null;
- // Global state cleanup after some tests
- InitializeTenancyBySubdomain::$onFail = null;
-
- Route::group([
- 'middleware' => InitializeTenancyBySubdomain::class,
- ], function () {
- Route::get('/foo/{a}/{b}', function ($a, $b) {
- return "$a + $b";
- });
+ Route::group([
+ 'middleware' => InitializeTenancyBySubdomain::class,
+ ], function () {
+ Route::get('/foo/{a}/{b}', function ($a, $b) {
+ return "$a + $b";
});
+ });
- config(['tenancy.tenant_model' => SubdomainTenant::class]);
- }
+ config(['tenancy.tenant_model' => SubdomainTenant::class]);
+});
- /** @test */
- public function tenant_can_be_identified_by_subdomain()
- {
- $tenant = SubdomainTenant::create([
- 'id' => 'acme',
- ]);
+test('tenant can be identified by subdomain', function () {
+ $tenant = SubdomainTenant::create([
+ 'id' => 'acme',
+ ]);
- $tenant->domains()->create([
- 'domain' => 'foo',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'foo',
+ ]);
- $this->assertFalse(tenancy()->initialized);
+ expect(tenancy()->initialized)->toBeFalse();
- $this
- ->get('http://foo.localhost/foo/abc/xyz')
- ->assertSee('abc + xyz');
+ pest()
+ ->get('http://foo.localhost/foo/abc/xyz')
+ ->assertSee('abc + xyz');
- $this->assertTrue(tenancy()->initialized);
- $this->assertSame('acme', tenant('id'));
- }
+ expect(tenancy()->initialized)->toBeTrue();
+ expect(tenant('id'))->toBe('acme');
+});
- /** @test */
- public function onfail_logic_can_be_customized()
- {
- InitializeTenancyBySubdomain::$onFail = function () {
- return 'foo';
- };
+test('onfail logic can be customized', function () {
+ InitializeTenancyBySubdomain::$onFail = function () {
+ return 'foo';
+ };
- $this
- ->get('http://foo.localhost/foo/abc/xyz')
- ->assertSee('foo');
- }
+ pest()
+ ->get('http://foo.localhost/foo/abc/xyz')
+ ->assertSee('foo');
+});
- /** @test */
- public function localhost_is_not_a_valid_subdomain()
- {
- $this->expectException(NotASubdomainException::class);
+test('localhost is not a valid subdomain', function () {
+ pest()->expectException(NotASubdomainException::class);
- $this
- ->withoutExceptionHandling()
- ->get('http://localhost/foo/abc/xyz');
- }
+ $this
+ ->withoutExceptionHandling()
+ ->get('http://localhost/foo/abc/xyz');
+});
- /** @test */
- public function ip_address_is_not_a_valid_subdomain()
- {
- $this->expectException(NotASubdomainException::class);
+test('ip address is not a valid subdomain', function () {
+ pest()->expectException(NotASubdomainException::class);
- $this
- ->withoutExceptionHandling()
- ->get('http://127.0.0.1/foo/abc/xyz');
- }
+ $this
+ ->withoutExceptionHandling()
+ ->get('http://127.0.0.1/foo/abc/xyz');
+});
- /** @test */
- public function oninvalidsubdomain_logic_can_be_customized()
- {
- // in this case, we need to return a response instance
- // since a string would be treated as the subdomain
- InitializeTenancyBySubdomain::$onFail = function ($e) {
- if ($e instanceof NotASubdomainException) {
- return response('foo custom invalid subdomain handler');
- }
+test('oninvalidsubdomain logic can be customized', function () {
+ // in this case, we need to return a response instance
+ // since a string would be treated as the subdomain
+ InitializeTenancyBySubdomain::$onFail = function ($e) {
+ if ($e instanceof NotASubdomainException) {
+ return response('foo custom invalid subdomain handler');
+ }
- throw $e;
- };
+ throw $e;
+ };
- $this
- ->withoutExceptionHandling()
- ->get('http://127.0.0.1/foo/abc/xyz')
- ->assertSee('foo custom invalid subdomain handler');
- }
+ $this
+ ->withoutExceptionHandling()
+ ->get('http://127.0.0.1/foo/abc/xyz')
+ ->assertSee('foo custom invalid subdomain handler');
+});
- /** @test */
- public function we_cant_use_a_subdomain_that_doesnt_belong_to_our_central_domains()
- {
- config(['tenancy.central_domains' => [
- '127.0.0.1',
- // not 'localhost'
- ]]);
+test('we cant use a subdomain that doesnt belong to our central domains', function () {
+ config(['tenancy.central_domains' => [
+ '127.0.0.1',
+ // not 'localhost'
+ ]]);
- $tenant = SubdomainTenant::create([
- 'id' => 'acme',
- ]);
+ $tenant = SubdomainTenant::create([
+ 'id' => 'acme',
+ ]);
- $tenant->domains()->create([
- 'domain' => 'foo',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'foo',
+ ]);
- $this->expectException(NotASubdomainException::class);
+ pest()->expectException(NotASubdomainException::class);
- $this
- ->withoutExceptionHandling()
- ->get('http://foo.localhost/foo/abc/xyz');
- }
+ $this
+ ->withoutExceptionHandling()
+ ->get('http://foo.localhost/foo/abc/xyz');
+});
- /** @test */
- public function central_domain_is_not_a_subdomain()
- {
- config(['tenancy.central_domains' => [
- 'localhost',
- ]]);
+test('central domain is not a subdomain', function () {
+ config(['tenancy.central_domains' => [
+ 'localhost',
+ ]]);
- $tenant = SubdomainTenant::create([
- 'id' => 'acme',
- ]);
+ $tenant = SubdomainTenant::create([
+ 'id' => 'acme',
+ ]);
- $tenant->domains()->create([
- 'domain' => 'acme',
- ]);
+ $tenant->domains()->create([
+ 'domain' => 'acme',
+ ]);
- $this->expectException(NotASubdomainException::class);
+ pest()->expectException(NotASubdomainException::class);
- $this
- ->withoutExceptionHandling()
- ->get('http://localhost/foo/abc/xyz');
- }
-}
+ $this
+ ->withoutExceptionHandling()
+ ->get('http://localhost/foo/abc/xyz');
+});
class SubdomainTenant extends Models\Tenant
{
diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php
index 77a130b4..2c5000f1 100644
--- a/tests/TenantAssetTest.php
+++ b/tests/TenantAssetTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;
@@ -15,115 +13,94 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class TenantAssetTest extends TestCase
+beforeEach(function () {
+ config(['tenancy.bootstrappers' => [
+ FilesystemTenancyBootstrapper::class,
+ ]]);
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+});
+
+afterEach(function () {
+ // Cleanup
+ TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class;
+});
+
+test('asset can be accessed using the url returned by the tenant asset helper', function () {
+ TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
+
+ $tenant = Tenant::create();
+ tenancy()->initialize($tenant);
+
+ $filename = 'testfile' . pest()->randomString(10);
+ Storage::disk('public')->put($filename, 'bar');
+ $path = storage_path("app/public/$filename");
+
+ // response()->file() returns BinaryFileResponse whose content is
+ // inaccessible via getContent, so ->assertSee() can't be used
+ expect($path)->toBeFile();
+ $response = pest()->get(tenant_asset($filename), [
+ 'X-Tenant' => $tenant->id,
+ ]);
+
+ $response->assertSuccessful();
+
+ $f = fopen($path, 'r');
+ $content = fread($f, filesize($path));
+ fclose($f);
+
+ expect($content)->toBe('bar');
+});
+
+test('asset helper returns a link to tenant asset controller when asset url is null', function () {
+ config(['app.asset_url' => null]);
+
+ $tenant = Tenant::create();
+ tenancy()->initialize($tenant);
+
+ expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo']));
+});
+
+test('asset helper returns a link to an external url when asset url is not null', function () {
+ config(['app.asset_url' => 'https://an-s3-bucket']);
+
+ $tenant = Tenant::create();
+ tenancy()->initialize($tenant);
+
+ expect(asset('foo'))->toBe("https://an-s3-bucket/tenant{$tenant->id}/foo");
+});
+
+test('global asset helper returns the same url regardless of tenancy initialization', function () {
+ $original = global_asset('foobar');
+ expect(global_asset('foobar'))->toBe(asset('foobar'));
+
+ $tenant = Tenant::create();
+ tenancy()->initialize($tenant);
+
+ expect(global_asset('foobar'))->toBe($original);
+});
+
+test('asset helper tenancy can be disabled', function () {
+ $original = asset('foo');
+
+ config([
+ 'app.asset_url' => null,
+ 'tenancy.filesystem.asset_helper_tenancy' => false,
+ ]);
+
+ $tenant = Tenant::create();
+ tenancy()->initialize($tenant);
+
+ expect(asset('foo'))->toBe($original);
+});
+
+function getEnvironmentSetUp($app)
{
- public function getEnvironmentSetUp($app)
- {
- parent::getEnvironmentSetUp($app);
-
- $app->booted(function () {
- if (file_exists(base_path('routes/tenant.php'))) {
- Route::middleware(['web'])
- ->namespace($this->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers')
- ->group(base_path('routes/tenant.php'));
- }
- });
- }
-
- public function setUp(): void
- {
- parent::setUp();
-
- config(['tenancy.bootstrappers' => [
- FilesystemTenancyBootstrapper::class,
- ]]);
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- }
-
- public function tearDown(): void
- {
- parent::tearDown();
-
- // Cleanup
- TenantAssetsController::$tenancyMiddleware = InitializeTenancyByDomain::class;
- }
-
- /** @test */
- public function asset_can_be_accessed_using_the_url_returned_by_the_tenant_asset_helper()
- {
- TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class;
-
- $tenant = Tenant::create();
- tenancy()->initialize($tenant);
-
- $filename = 'testfile' . $this->randomString(10);
- Storage::disk('public')->put($filename, 'bar');
- $path = storage_path("app/public/$filename");
-
- // response()->file() returns BinaryFileResponse whose content is
- // inaccessible via getContent, so ->assertSee() can't be used
- $this->assertFileExists($path);
- $response = $this->get(tenant_asset($filename), [
- 'X-Tenant' => $tenant->id,
- ]);
-
- $response->assertSuccessful();
-
- $f = fopen($path, 'r');
- $content = fread($f, filesize($path));
- fclose($f);
-
- $this->assertSame('bar', $content);
- }
-
- /** @test */
- public function asset_helper_returns_a_link_to_TenantAssetController_when_asset_url_is_null()
- {
- config(['app.asset_url' => null]);
-
- $tenant = Tenant::create();
- tenancy()->initialize($tenant);
-
- $this->assertSame(route('stancl.tenancy.asset', ['path' => 'foo']), asset('foo'));
- }
-
- /** @test */
- public function asset_helper_returns_a_link_to_an_external_url_when_asset_url_is_not_null()
- {
- config(['app.asset_url' => 'https://an-s3-bucket']);
-
- $tenant = Tenant::create();
- tenancy()->initialize($tenant);
-
- $this->assertSame("https://an-s3-bucket/tenant{$tenant->id}/foo", asset('foo'));
- }
-
- /** @test */
- public function global_asset_helper_returns_the_same_url_regardless_of_tenancy_initialization()
- {
- $original = global_asset('foobar');
- $this->assertSame(asset('foobar'), global_asset('foobar'));
-
- $tenant = Tenant::create();
- tenancy()->initialize($tenant);
-
- $this->assertSame($original, global_asset('foobar'));
- }
-
- /** @test */
- public function asset_helper_tenancy_can_be_disabled()
- {
- $original = asset('foo');
-
- config([
- 'app.asset_url' => null,
- 'tenancy.filesystem.asset_helper_tenancy' => false,
- ]);
-
- $tenant = Tenant::create();
- tenancy()->initialize($tenant);
-
- $this->assertSame($original, asset('foo'));
- }
+ $app->booted(function () {
+ if (file_exists(base_path('routes/tenant.php'))) {
+ Route::middleware(['web'])
+ ->namespace(pest()->app['config']['tenancy.tenant_route_namespace'] ?? 'App\Http\Controllers')
+ ->group(base_path('routes/tenant.php'));
+ }
+ });
}
diff --git a/tests/TenantAwareCommandTest.php b/tests/TenantAwareCommandTest.php
index b8d75aed..fe49685e 100644
--- a/tests/TenantAwareCommandTest.php
+++ b/tests/TenantAwareCommandTest.php
@@ -2,32 +2,25 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class TenantAwareCommandTest extends TestCase
-{
- /** @test */
- public function commands_run_globally_are_tenant_aware_and_return_valid_exit_code()
- {
- $tenant1 = Tenant::create();
- $tenant2 = Tenant::create();
- Artisan::call('tenants:migrate', [
- '--tenants' => [$tenant1['id'], $tenant2['id']],
- ]);
+test('commands run globally are tenant aware and return valid exit code', function () {
+ $tenant1 = Tenant::create();
+ $tenant2 = Tenant::create();
+ Artisan::call('tenants:migrate', [
+ '--tenants' => [$tenant1['id'], $tenant2['id']],
+ ]);
- $this->artisan('user:add')
- ->assertExitCode(0);
+ pest()->artisan('user:add')
+ ->assertExitCode(0);
- tenancy()->initialize($tenant1);
- $this->assertNotEmpty(DB::table('users')->get());
- tenancy()->end();
+ tenancy()->initialize($tenant1);
+ pest()->assertNotEmpty(DB::table('users')->get());
+ tenancy()->end();
- tenancy()->initialize($tenant2);
- $this->assertNotEmpty(DB::table('users')->get());
- tenancy()->end();
- }
-}
+ tenancy()->initialize($tenant2);
+ pest()->assertNotEmpty(DB::table('users')->get());
+ tenancy()->end();
+});
diff --git a/tests/TenantDatabaseManagerTest.php b/tests/TenantDatabaseManagerTest.php
index f64770b1..bc09e888 100644
--- a/tests/TenantDatabaseManagerTest.php
+++ b/tests/TenantDatabaseManagerTest.php
@@ -2,13 +2,10 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
-use PDO;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Database\DatabaseManager;
@@ -19,6 +16,7 @@ use Stancl\Tenancy\Exceptions\TenantDatabaseAlreadyExistsException;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext;
+use Stancl\Tenancy\TenantDatabaseManagers\MicrosoftSQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager;
use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager;
@@ -26,247 +24,228 @@ use Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager;
use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class TenantDatabaseManagerTest extends TestCase
+test('databases can be created and deleted', function ($driver, $databaseManager) {
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+
+ config()->set([
+ "tenancy.database.managers.$driver" => $databaseManager,
+ ]);
+
+ $name = 'db' . pest()->randomString();
+
+ $manager = app($databaseManager);
+ $manager->setConnection($driver);
+
+ expect($manager->databaseExists($name))->toBeFalse();
+
+ $tenant = Tenant::create([
+ 'tenancy_db_name' => $name,
+ 'tenancy_db_connection' => $driver,
+ ]);
+
+ expect($manager->databaseExists($name))->toBeTrue();
+ $manager->deleteDatabase($tenant);
+ expect($manager->databaseExists($name))->toBeFalse();
+})->with('database_manager_provider');
+
+test('dbs can be created when another driver is used for the central db', function () {
+ expect(config('database.default'))->toBe('central');
+
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+
+ $database = 'db' . pest()->randomString();
+
+ $mysqlmanager = app(MySQLDatabaseManager::class);
+ $mysqlmanager->setConnection('mysql');
+
+ expect($mysqlmanager->databaseExists($database))->toBeFalse();
+ Tenant::create([
+ 'tenancy_db_name' => $database,
+ 'tenancy_db_connection' => 'mysql',
+ ]);
+
+ expect($mysqlmanager->databaseExists($database))->toBeTrue();
+
+ $postgresManager = app(PostgreSQLDatabaseManager::class);
+ $postgresManager->setConnection('pgsql');
+
+ $database = 'db' . pest()->randomString();
+ expect($postgresManager->databaseExists($database))->toBeFalse();
+
+ Tenant::create([
+ 'tenancy_db_name' => $database,
+ 'tenancy_db_connection' => 'pgsql',
+ ]);
+
+ expect($postgresManager->databaseExists($database))->toBeTrue();
+});
+
+test('the tenant connection is fully removed', function () {
+ config([
+ 'tenancy.boostrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ],
+ ]);
+
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+
+ $tenant = Tenant::create();
+
+ expect(array_keys(app('db')->getConnections()))->toBe(['central']);
+ pest()->assertArrayNotHasKey('tenant', config('database.connections'));
+
+ tenancy()->initialize($tenant);
+
+ createUsersTable();
+
+ 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();
+});
+
+test('db name is prefixed with db path when sqlite is used', function () {
+ if (file_exists(database_path('foodb'))) {
+ unlink(database_path('foodb')); // cleanup
+ }
+ config([
+ 'database.connections.fooconn.driver' => 'sqlite',
+ ]);
+
+ $tenant = Tenant::create([
+ 'tenancy_db_name' => 'foodb',
+ 'tenancy_db_connection' => 'fooconn',
+ ]);
+ app(DatabaseManager::class)->createTenantConnection($tenant);
+
+ expect(database_path('foodb'))->toBe(config('database.connections.tenant.database'));
+});
+
+test('schema manager uses schema to separate tenant dbs', function () {
+ config([
+ 'tenancy.database.managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class,
+ 'tenancy.boostrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ],
+ ]);
+
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+
+ $originalDatabaseName = config(['database.connections.pgsql.database']);
+
+ $tenant = Tenant::create([
+ 'tenancy_db_connection' => 'pgsql',
+ ]);
+ tenancy()->initialize($tenant);
+
+ $schemaConfig = version_compare(app()->version(), '9.0', '>=') ?
+ config('database.connections.' . config('database.default') . '.search_path') :
+ config('database.connections.' . config('database.default') . '.schema');
+
+ expect($schemaConfig)->toBe($tenant->database()->getName());
+ expect(config(['database.connections.pgsql.database']))->toBe($originalDatabaseName);
+});
+
+test('a tenants database cannot be created when the database already exists', function () {
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+
+ $name = 'foo' . Str::random(8);
+ $tenant = Tenant::create([
+ 'tenancy_db_name' => $name,
+ ]);
+
+ $manager = $tenant->database()->manager();
+ expect($manager->databaseExists($tenant->database()->getName()))->toBeTrue();
+
+ pest()->expectException(TenantDatabaseAlreadyExistsException::class);
+ $tenant2 = Tenant::create([
+ 'tenancy_db_name' => $name,
+ ]);
+});
+
+test('tenant database can be created on a foreign server', function () {
+ config([
+ 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
+ 'database.connections.mysql2' => [
+ 'driver' => 'mysql',
+ 'host' => 'mysql2', // important line
+ 'port' => 3306,
+ 'database' => 'main',
+ 'username' => 'root',
+ 'password' => 'password',
+ 'unix_socket' => env('DB_SOCKET', ''),
+ 'charset' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ 'strict' => true,
+ 'engine' => null,
+ 'options' => extension_loaded('pdo_mysql') ? array_filter([
+ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+ ]) : [],
+ ],
+ ]);
+
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener());
+
+ $name = 'foo' . Str::random(8);
+ $tenant = Tenant::create([
+ 'tenancy_db_name' => $name,
+ 'tenancy_db_connection' => 'mysql2',
+ ]);
+
+ /** @var PermissionControlledMySQLDatabaseManager $manager */
+ $manager = $tenant->database()->manager();
+
+ $manager->setConnection('mysql');
+ expect($manager->databaseExists($name))->toBeFalse();
+
+ $manager->setConnection('mysql2');
+ expect($manager->databaseExists($name))->toBeTrue();
+});
+
+test('path used by sqlite manager can be customized', function () {
+ pest()->markTestIncomplete();
+});
+
+// Datasets
+dataset('database_manager_provider', [
+ ['mysql', MySQLDatabaseManager::class],
+ ['mysql', PermissionControlledMySQLDatabaseManager::class],
+ ['sqlite', SQLiteDatabaseManager::class],
+ ['pgsql', PostgreSQLDatabaseManager::class],
+ ['pgsql', PostgreSQLSchemaManager::class],
+ ['sqlsrv', MicrosoftSQLDatabaseManager::class]
+]);
+
+function createUsersTable()
{
- /**
- * @test
- * @dataProvider database_manager_provider
- */
- public function databases_can_be_created_and_deleted($driver, $databaseManager)
- {
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
-
- config()->set([
- "tenancy.database.managers.$driver" => $databaseManager,
- ]);
-
- $name = 'db' . $this->randomString();
-
- $manager = app($databaseManager);
- $manager->setConnection($driver);
-
- $this->assertFalse($manager->databaseExists($name));
-
- $tenant = Tenant::create([
- 'tenancy_db_name' => $name,
- 'tenancy_db_connection' => $driver,
- ]);
-
- $this->assertTrue($manager->databaseExists($name));
- $manager->deleteDatabase($tenant);
- $this->assertFalse($manager->databaseExists($name));
- }
-
- /** @test */
- public function dbs_can_be_created_when_another_driver_is_used_for_the_central_db()
- {
- $this->assertSame('central', config('database.default'));
-
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
-
- $database = 'db' . $this->randomString();
-
- $mysqlmanager = app(MySQLDatabaseManager::class);
- $mysqlmanager->setConnection('mysql');
-
- $this->assertFalse($mysqlmanager->databaseExists($database));
- Tenant::create([
- 'tenancy_db_name' => $database,
- 'tenancy_db_connection' => 'mysql',
- ]);
-
- $this->assertTrue($mysqlmanager->databaseExists($database));
-
- $postgresManager = app(PostgreSQLDatabaseManager::class);
- $postgresManager->setConnection('pgsql');
-
- $database = 'db' . $this->randomString();
- $this->assertFalse($postgresManager->databaseExists($database));
-
- Tenant::create([
- 'tenancy_db_name' => $database,
- 'tenancy_db_connection' => 'pgsql',
- ]);
-
- $this->assertTrue($postgresManager->databaseExists($database));
- }
-
- public function database_manager_provider()
- {
- return [
- ['mysql', MySQLDatabaseManager::class],
- ['mysql', PermissionControlledMySQLDatabaseManager::class],
- ['sqlite', SQLiteDatabaseManager::class],
- ['pgsql', PostgreSQLDatabaseManager::class],
- ['pgsql', PostgreSQLSchemaManager::class],
- ];
- }
-
- /** @test */
- public function the_tenant_connection_is_fully_removed()
- {
- config([
- 'tenancy.boostrappers' => [
- DatabaseTenancyBootstrapper::class,
- ],
- ]);
-
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
-
- $tenant = Tenant::create();
-
- $this->assertSame(['central'], array_keys(app('db')->getConnections()));
- $this->assertArrayNotHasKey('tenant', config('database.connections'));
-
- tenancy()->initialize($tenant);
-
- $this->createUsersTable();
-
- $this->assertSame(['central', 'tenant'], array_keys(app('db')->getConnections()));
- $this->assertArrayHasKey('tenant', config('database.connections'));
-
- tenancy()->end();
-
- $this->assertSame(['central'], array_keys(app('db')->getConnections()));
- $this->assertNull(config('database.connections.tenant'));
- }
-
- protected function createUsersTable()
- {
- Schema::create('users', function (Blueprint $table) {
- $table->increments('id');
- $table->string('name');
- $table->string('email')->unique();
- $table->string('password');
- $table->rememberToken();
- $table->timestamps();
- });
- }
-
- /** @test */
- public function db_name_is_prefixed_with_db_path_when_sqlite_is_used()
- {
- if (file_exists(database_path('foodb'))) {
- unlink(database_path('foodb')); // cleanup
- }
- config([
- 'database.connections.fooconn.driver' => 'sqlite',
- ]);
-
- $tenant = Tenant::create([
- 'tenancy_db_name' => 'foodb',
- 'tenancy_db_connection' => 'fooconn',
- ]);
- app(DatabaseManager::class)->createTenantConnection($tenant);
-
- $this->assertSame(config('database.connections.tenant.database'), database_path('foodb'));
- }
-
- /** @test */
- public function schema_manager_uses_schema_to_separate_tenant_dbs()
- {
- config([
- 'tenancy.database.managers.pgsql' => \Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class,
- 'tenancy.boostrappers' => [
- DatabaseTenancyBootstrapper::class,
- ],
- ]);
-
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
-
- $originalDatabaseName = config(['database.connections.pgsql.database']);
-
- $tenant = Tenant::create([
- 'tenancy_db_connection' => 'pgsql',
- ]);
- tenancy()->initialize($tenant);
-
- $this->assertSame($tenant->database()->getName(), config('database.connections.' . config('database.default') . '.schema'));
- $this->assertSame($originalDatabaseName, config(['database.connections.pgsql.database']));
- }
-
- /** @test */
- public function a_tenants_database_cannot_be_created_when_the_database_already_exists()
- {
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
-
- $name = 'foo' . Str::random(8);
- $tenant = Tenant::create([
- 'tenancy_db_name' => $name,
- ]);
-
- $manager = $tenant->database()->manager();
- $this->assertTrue($manager->databaseExists($tenant->database()->getName()));
-
- $this->expectException(TenantDatabaseAlreadyExistsException::class);
- $tenant2 = Tenant::create([
- 'tenancy_db_name' => $name,
- ]);
- }
-
- /** @test */
- public function tenant_database_can_be_created_on_a_foreign_server()
- {
- config([
- 'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
- 'database.connections.mysql2' => [
- 'driver' => 'mysql',
- 'host' => 'mysql2', // important line
- 'port' => 3306,
- 'database' => 'main',
- 'username' => 'root',
- 'password' => 'password',
- 'unix_socket' => env('DB_SOCKET', ''),
- 'charset' => 'utf8mb4',
- 'collation' => 'utf8mb4_unicode_ci',
- 'prefix' => '',
- 'prefix_indexes' => true,
- 'strict' => true,
- 'engine' => null,
- 'options' => extension_loaded('pdo_mysql') ? array_filter([
- PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
- ]) : [],
- ],
- ]);
-
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener());
-
- $name = 'foo' . Str::random(8);
- $tenant = Tenant::create([
- 'tenancy_db_name' => $name,
- 'tenancy_db_connection' => 'mysql2',
- ]);
-
- /** @var PermissionControlledMySQLDatabaseManager $manager */
- $manager = $tenant->database()->manager();
-
- $manager->setConnection('mysql');
- $this->assertFalse($manager->databaseExists($name));
-
- $manager->setConnection('mysql2');
- $this->assertTrue($manager->databaseExists($name));
- }
-
- /** @test */
- public function path_used_by_sqlite_manager_can_be_customized()
- {
- $this->markTestIncomplete();
- }
+ Schema::create('users', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('name');
+ $table->string('email')->unique();
+ $table->string('password');
+ $table->rememberToken();
+ $table->timestamps();
+ });
}
diff --git a/tests/TenantModelTest.php b/tests/TenantModelTest.php
index 46dc6a00..d50c9b6b 100644
--- a/tests/TenantModelTest.php
+++ b/tests/TenantModelTest.php
@@ -2,8 +2,6 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Event;
@@ -21,148 +19,127 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Tests\Etc\Tenant;
use Stancl\Tenancy\UUIDGenerator;
-class TenantModelTest extends TestCase
-{
- /** @test */
- public function created_event_is_dispatched()
- {
- Event::fake([TenantCreated::class]);
+test('created event is dispatched', function () {
+ Event::fake([TenantCreated::class]);
- Event::assertNotDispatched(TenantCreated::class);
+ Event::assertNotDispatched(TenantCreated::class);
- Tenant::create();
+ Tenant::create();
- Event::assertDispatched(TenantCreated::class);
- }
+ Event::assertDispatched(TenantCreated::class);
+});
- /** @test */
- public function current_tenant_can_be_resolved_from_service_container_using_typehint()
- {
- $tenant = Tenant::create();
+test('current tenant can be resolved from service container using typehint', function () {
+ $tenant = Tenant::create();
- tenancy()->initialize($tenant);
+ tenancy()->initialize($tenant);
- $this->assertSame($tenant->id, app(Contracts\Tenant::class)->id);
+ expect(app(Contracts\Tenant::class)->id)->toBe($tenant->id);
- tenancy()->end();
+ tenancy()->end();
- $this->assertSame(null, app(Contracts\Tenant::class));
- }
+ expect(app(Contracts\Tenant::class))->toBe(null);
+});
- /** @test */
- public function id_is_generated_when_no_id_is_supplied()
- {
- config(['tenancy.id_generator' => UUIDGenerator::class]);
+test('id is generated when no id is supplied', function () {
+ config(['tenancy.id_generator' => UUIDGenerator::class]);
- $this->mock(UUIDGenerator::class, function ($mock) {
- return $mock->shouldReceive('generate')->once();
- });
+ $this->mock(UUIDGenerator::class, function ($mock) {
+ return $mock->shouldReceive('generate')->once();
+ });
- $tenant = Tenant::create();
+ $tenant = Tenant::create();
- $this->assertNotNull($tenant->id);
- }
+ pest()->assertNotNull($tenant->id);
+});
- /** @test */
- public function autoincrement_ids_are_supported()
- {
- Schema::drop('domains');
- Schema::table('tenants', function (Blueprint $table) {
- $table->bigIncrements('id')->change();
- });
+test('autoincrement ids are supported', function () {
+ Schema::drop('domains');
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->bigIncrements('id')->change();
+ });
- unset(app()[UniqueIdentifierGenerator::class]);
+ unset(app()[UniqueIdentifierGenerator::class]);
- $tenant1 = Tenant::create();
- $tenant2 = Tenant::create();
+ $tenant1 = Tenant::create();
+ $tenant2 = Tenant::create();
- $this->assertSame(1, $tenant1->id);
- $this->assertSame(2, $tenant2->id);
- }
+ expect($tenant1->id)->toBe(1);
+ expect($tenant2->id)->toBe(2);
+});
- /** @test */
- public function custom_tenant_model_can_be_used()
- {
- $tenant = MyTenant::create();
+test('custom tenant model can be used', function () {
+ $tenant = MyTenant::create();
- tenancy()->initialize($tenant);
+ tenancy()->initialize($tenant);
- $this->assertTrue(tenant() instanceof MyTenant);
- }
+ expect(tenant() instanceof MyTenant)->toBeTrue();
+});
- /** @test */
- public function custom_tenant_model_that_doesnt_extend_vendor_Tenant_model_can_be_used()
- {
- $tenant = AnotherTenant::create([
- 'id' => 'acme',
+test('custom tenant model that doesnt extend vendor tenant model can be used', function () {
+ $tenant = AnotherTenant::create([
+ 'id' => 'acme',
+ ]);
+
+ tenancy()->initialize($tenant);
+
+ expect(tenant() instanceof AnotherTenant)->toBeTrue();
+});
+
+test('tenant can be created even when we are in another tenants context', function () {
+ config(['tenancy.bootstrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ]]);
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function ($event) {
+ return $event->tenant;
+ })->toListener());
+
+ $tenant1 = Tenant::create([
+ 'id' => 'foo',
+ 'tenancy_db_name' => 'db' . Str::random(16),
+ ]);
+
+ tenancy()->initialize($tenant1);
+
+ $tenant2 = Tenant::create([
+ 'id' => 'bar',
+ 'tenancy_db_name' => 'db' . Str::random(16),
+ ]);
+
+ tenancy()->end();
+
+ expect(Tenant::count())->toBe(2);
+});
+
+test('the model uses tenant collection', function () {
+ Tenant::create();
+ Tenant::create();
+
+ expect(Tenant::count())->toBe(2);
+ expect(Tenant::all() instanceof TenantCollection)->toBeTrue();
+});
+
+test('a command can be run on a collection of tenants', function () {
+ Tenant::create([
+ 'id' => 't1',
+ 'foo' => 'bar',
+ ]);
+ Tenant::create([
+ 'id' => 't2',
+ 'foo' => 'bar',
+ ]);
+
+ Tenant::all()->runForEach(function ($tenant) {
+ $tenant->update([
+ 'foo' => 'xyz',
]);
+ });
- tenancy()->initialize($tenant);
-
- $this->assertTrue(tenant() instanceof AnotherTenant);
- }
-
- /** @test */
- public function tenant_can_be_created_even_when_we_are_in_another_tenants_context()
- {
- config(['tenancy.bootstrappers' => [
- DatabaseTenancyBootstrapper::class,
- ]]);
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function ($event) {
- return $event->tenant;
- })->toListener());
-
- $tenant1 = Tenant::create([
- 'id' => 'foo',
- 'tenancy_db_name' => 'db' . Str::random(16),
- ]);
-
- tenancy()->initialize($tenant1);
-
- $tenant2 = Tenant::create([
- 'id' => 'bar',
- 'tenancy_db_name' => 'db' . Str::random(16),
- ]);
-
- tenancy()->end();
-
- $this->assertSame(2, Tenant::count());
- }
-
- /** @test */
- public function the_model_uses_TenantCollection()
- {
- Tenant::create();
- Tenant::create();
-
- $this->assertSame(2, Tenant::count());
- $this->assertTrue(Tenant::all() instanceof TenantCollection);
- }
-
- /** @test */
- public function a_command_can_be_run_on_a_collection_of_tenants()
- {
- Tenant::create([
- 'id' => 't1',
- 'foo' => 'bar',
- ]);
- Tenant::create([
- 'id' => 't2',
- 'foo' => 'bar',
- ]);
-
- Tenant::all()->runForEach(function ($tenant) {
- $tenant->update([
- 'foo' => 'xyz',
- ]);
- });
-
- $this->assertSame('xyz', Tenant::find('t1')->foo);
- $this->assertSame('xyz', Tenant::find('t2')->foo);
- }
-}
+ expect(Tenant::find('t1')->foo)->toBe('xyz');
+ expect(Tenant::find('t2')->foo)->toBe('xyz');
+});
class MyTenant extends Tenant
{
@@ -172,6 +149,7 @@ class MyTenant extends Tenant
class AnotherTenant extends Model implements Contracts\Tenant
{
protected $guarded = [];
+
protected $table = 'tenants';
public function getTenantKeyName(): string
diff --git a/tests/TenantUserImpersonationTest.php b/tests/TenantUserImpersonationTest.php
index c5e83853..65aa380d 100644
--- a/tests/TenantUserImpersonationTest.php
+++ b/tests/TenantUserImpersonationTest.php
@@ -2,13 +2,9 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Carbon\Carbon;
use Carbon\CarbonInterval;
-use Closure;
use Illuminate\Auth\SessionGuard;
-use Illuminate\Foundation\Auth\User as Authenticable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
@@ -26,259 +22,246 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Tests\Etc\Tenant;
+use Illuminate\Foundation\Auth\User as Authenticable;
-class TenantUserImpersonationTest extends TestCase
+beforeEach(function () {
+ pest()->artisan('migrate', [
+ '--path' => __DIR__ . '/../assets/impersonation-migrations',
+ '--realpath' => true,
+ ])->assertExitCode(0);
+
+ config([
+ 'tenancy.bootstrappers' => [
+ DatabaseTenancyBootstrapper::class,
+ ],
+ 'tenancy.features' => [
+ UserImpersonation::class,
+ ],
+ ]);
+
+ Event::listen(
+ TenantCreated::class,
+ JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
+ return $event->tenant;
+ })->toListener()
+ );
+
+ Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
+ Event::listen(TenancyEnded::class, RevertToCentralContext::class);
+
+ config(['auth.providers.users.model' => ImpersonationUser::class]);
+});
+
+test('tenant user can be impersonated on a tenant domain', function () {
+ Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes());
+
+ $tenant = Tenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
+ migrateTenants();
+ $user = $tenant->run(function () {
+ return ImpersonationUser::create([
+ 'name' => 'Joe',
+ 'email' => 'joe@local',
+ 'password' => bcrypt('secret'),
+ ]);
+ });
+
+ // We try to visit the dashboard directly, before impersonating the user.
+ pest()->get('http://foo.localhost/dashboard')
+ ->assertRedirect('http://foo.localhost/login');
+
+ // We impersonate the user
+ $token = tenancy()->impersonate($tenant, $user->id, '/dashboard');
+ pest()->get('http://foo.localhost/impersonate/' . $token->token)
+ ->assertRedirect('http://foo.localhost/dashboard');
+
+ // Now we try to visit the dashboard directly, after impersonating the user.
+ pest()->get('http://foo.localhost/dashboard')
+ ->assertSuccessful()
+ ->assertSee('You are logged in as Joe');
+});
+
+test('tenant user can be impersonated on a tenant path', function () {
+ makeLoginRoute();
+
+ Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group(getRoutes(false));
+
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ 'tenancy_db_name' => 'db' . Str::random(16),
+ ]);
+ migrateTenants();
+ $user = $tenant->run(function () {
+ return ImpersonationUser::create([
+ 'name' => 'Joe',
+ 'email' => 'joe@local',
+ 'password' => bcrypt('secret'),
+ ]);
+ });
+
+ // We try to visit the dashboard directly, before impersonating the user.
+ pest()->get('/acme/dashboard')
+ ->assertRedirect('/login');
+
+ // We impersonate the user
+ $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard');
+ pest()->get('/acme/impersonate/' . $token->token)
+ ->assertRedirect('/acme/dashboard');
+
+ // Now we try to visit the dashboard directly, after impersonating the user.
+ pest()->get('/acme/dashboard')
+ ->assertSuccessful()
+ ->assertSee('You are logged in as Joe');
+});
+
+test('tokens have a limited ttl', function () {
+ Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes());
+
+ $tenant = Tenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
+ migrateTenants();
+ $user = $tenant->run(function () {
+ return ImpersonationUser::create([
+ 'name' => 'Joe',
+ 'email' => 'joe@local',
+ 'password' => bcrypt('secret'),
+ ]);
+ });
+
+ // We impersonate the user
+ $token = tenancy()->impersonate($tenant, $user->id, '/dashboard');
+ $token->update([
+ 'created_at' => Carbon::now()->subtract(CarbonInterval::make('100s')),
+ ]);
+
+ pest()->followingRedirects()
+ ->get('http://foo.localhost/impersonate/' . $token->token)
+ ->assertStatus(403);
+});
+
+test('tokens are deleted after use', function () {
+ Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes());
+
+ $tenant = Tenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
+ migrateTenants();
+ $user = $tenant->run(function () {
+ return ImpersonationUser::create([
+ 'name' => 'Joe',
+ 'email' => 'joe@local',
+ 'password' => bcrypt('secret'),
+ ]);
+ });
+
+ // We impersonate the user
+ $token = tenancy()->impersonate($tenant, $user->id, '/dashboard');
+
+ pest()->assertNotNull(ImpersonationToken::find($token->token));
+
+ pest()->followingRedirects()
+ ->get('http://foo.localhost/impersonate/' . $token->token)
+ ->assertSuccessful()
+ ->assertSee('You are logged in as Joe');
+
+ expect(ImpersonationToken::find($token->token))->toBeNull();
+});
+
+test('impersonation works with multiple models and guards', function () {
+ config([
+ 'auth.guards.another' => [
+ 'driver' => 'session',
+ 'provider' => 'another_users',
+ ],
+ 'auth.providers.another_users' => [
+ 'driver' => 'eloquent',
+ 'model' => AnotherImpersonationUser::class,
+ ],
+ ]);
+
+ Auth::extend('another', function ($app, $name, array $config) {
+ return new SessionGuard($name, Auth::createUserProvider($config['provider']), session());
+ });
+
+ Route::middleware(InitializeTenancyByDomain::class)->group(getRoutes(true, 'another'));
+
+ $tenant = Tenant::create();
+ $tenant->domains()->create([
+ 'domain' => 'foo.localhost',
+ ]);
+ migrateTenants();
+ $user = $tenant->run(function () {
+ return AnotherImpersonationUser::create([
+ 'name' => 'Joe',
+ 'email' => 'joe@local',
+ 'password' => bcrypt('secret'),
+ ]);
+ });
+
+ // We try to visit the dashboard directly, before impersonating the user.
+ pest()->get('http://foo.localhost/dashboard')
+ ->assertRedirect('http://foo.localhost/login');
+
+ // We impersonate the user
+ $token = tenancy()->impersonate($tenant, $user->id, '/dashboard', 'another');
+ pest()->get('http://foo.localhost/impersonate/' . $token->token)
+ ->assertRedirect('http://foo.localhost/dashboard');
+
+ // Now we try to visit the dashboard directly, after impersonating the user.
+ pest()->get('http://foo.localhost/dashboard')
+ ->assertSuccessful()
+ ->assertSee('You are logged in as Joe');
+
+ Tenant::first()->run(function () {
+ expect(auth()->guard('another')->user()->name)->toBe('Joe');
+ expect(auth()->guard('web')->user())->toBe(null);
+ });
+});
+
+function migrateTenants()
{
- protected function migrateTenants()
- {
- $this->artisan('tenants:migrate')->assertExitCode(0);
- }
+ pest()->artisan('tenants:migrate')->assertExitCode(0);
+}
- public function setUp(): void
- {
- parent::setUp();
+function makeLoginRoute()
+{
+ Route::get('/login', function () {
+ return 'Please log in';
+ })->name('login');
+}
- $this->artisan('migrate', [
- '--path' => __DIR__ . '/../assets/impersonation-migrations',
- '--realpath' => true,
- ])->assertExitCode(0);
+function getRoutes($loginRoute = true, $authGuard = 'web'): Closure
+{
+ return function () use ($loginRoute, $authGuard) {
+ if ($loginRoute) {
+ makeLoginRoute();
+ }
- config([
- 'tenancy.bootstrappers' => [
- DatabaseTenancyBootstrapper::class,
- ],
- 'tenancy.features' => [
- UserImpersonation::class,
- ],
- ]);
+ Route::get('/dashboard', function () use ($authGuard) {
+ return 'You are logged in as ' . auth()->guard($authGuard)->user()->name;
+ })->middleware('auth:' . $authGuard);
- Event::listen(
- TenantCreated::class,
- JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
- return $event->tenant;
- })->toListener()
- );
-
- Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
- Event::listen(TenancyEnded::class, RevertToCentralContext::class);
-
- config(['auth.providers.users.model' => ImpersonationUser::class]);
- }
-
- public function makeLoginRoute()
- {
- Route::get('/login', function () {
- return 'Please log in';
- })->name('login');
- }
-
- public function getRoutes($loginRoute = true, $authGuard = 'web'): Closure
- {
- return function () use ($loginRoute, $authGuard) {
- if ($loginRoute) {
- $this->makeLoginRoute();
- }
-
- Route::get('/dashboard', function () use ($authGuard) {
- return 'You are logged in as ' . auth()->guard($authGuard)->user()->name;
- })->middleware('auth:' . $authGuard);
-
- Route::get('/impersonate/{token}', function ($token) {
- return UserImpersonation::makeResponse($token);
- });
- };
- }
-
- /** @test */
- public function tenant_user_can_be_impersonated_on_a_tenant_domain()
- {
- Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes());
-
- $tenant = Tenant::create();
- $tenant->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
- $this->migrateTenants();
- $user = $tenant->run(function () {
- return ImpersonationUser::create([
- 'name' => 'Joe',
- 'email' => 'joe@local',
- 'password' => bcrypt('secret'),
- ]);
+ Route::get('/impersonate/{token}', function ($token) {
+ return UserImpersonation::makeResponse($token);
});
-
- // We try to visit the dashboard directly, before impersonating the user.
- $this->get('http://foo.localhost/dashboard')
- ->assertRedirect('http://foo.localhost/login');
-
- // We impersonate the user
- $token = tenancy()->impersonate($tenant, $user->id, '/dashboard');
- $this->get('http://foo.localhost/impersonate/' . $token->token)
- ->assertRedirect('http://foo.localhost/dashboard');
-
- // Now we try to visit the dashboard directly, after impersonating the user.
- $this->get('http://foo.localhost/dashboard')
- ->assertSuccessful()
- ->assertSee('You are logged in as Joe');
- }
-
- /** @test */
- public function tenant_user_can_be_impersonated_on_a_tenant_path()
- {
- $this->makeLoginRoute();
-
- Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group($this->getRoutes(false));
-
- $tenant = Tenant::create([
- 'id' => 'acme',
- 'tenancy_db_name' => 'db' . Str::random(16),
- ]);
- $this->migrateTenants();
- $user = $tenant->run(function () {
- return ImpersonationUser::create([
- 'name' => 'Joe',
- 'email' => 'joe@local',
- 'password' => bcrypt('secret'),
- ]);
- });
-
- // We try to visit the dashboard directly, before impersonating the user.
- $this->get('/acme/dashboard')
- ->assertRedirect('/login');
-
- // We impersonate the user
- $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard');
- $this->get('/acme/impersonate/' . $token->token)
- ->assertRedirect('/acme/dashboard');
-
- // Now we try to visit the dashboard directly, after impersonating the user.
- $this->get('/acme/dashboard')
- ->assertSuccessful()
- ->assertSee('You are logged in as Joe');
- }
-
- /** @test */
- public function tokens_have_a_limited_ttl()
- {
- Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes());
-
- $tenant = Tenant::create();
- $tenant->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
- $this->migrateTenants();
- $user = $tenant->run(function () {
- return ImpersonationUser::create([
- 'name' => 'Joe',
- 'email' => 'joe@local',
- 'password' => bcrypt('secret'),
- ]);
- });
-
- // We impersonate the user
- $token = tenancy()->impersonate($tenant, $user->id, '/dashboard');
- $token->update([
- 'created_at' => Carbon::now()->subtract(CarbonInterval::make('100s')),
- ]);
-
- $this->followingRedirects()
- ->get('http://foo.localhost/impersonate/' . $token->token)
- ->assertStatus(403);
- }
-
- /** @test */
- public function tokens_are_deleted_after_use()
- {
- Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes());
-
- $tenant = Tenant::create();
- $tenant->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
- $this->migrateTenants();
- $user = $tenant->run(function () {
- return ImpersonationUser::create([
- 'name' => 'Joe',
- 'email' => 'joe@local',
- 'password' => bcrypt('secret'),
- ]);
- });
-
- // We impersonate the user
- $token = tenancy()->impersonate($tenant, $user->id, '/dashboard');
-
- $this->assertNotNull(ImpersonationToken::find($token->token));
-
- $this->followingRedirects()
- ->get('http://foo.localhost/impersonate/' . $token->token)
- ->assertSuccessful()
- ->assertSee('You are logged in as Joe');
-
- $this->assertNull(ImpersonationToken::find($token->token));
- }
-
- /** @test */
- public function impersonation_works_with_multiple_models_and_guards()
- {
- config([
- 'auth.guards.another' => [
- 'driver' => 'session',
- 'provider' => 'another_users',
- ],
- 'auth.providers.another_users' => [
- 'driver' => 'eloquent',
- 'model' => AnotherImpersonationUser::class,
- ],
- ]);
-
- Auth::extend('another', function ($app, $name, array $config) {
- return new SessionGuard($name, Auth::createUserProvider($config['provider']), session());
- });
-
- Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes(true, 'another'));
-
- $tenant = Tenant::create();
- $tenant->domains()->create([
- 'domain' => 'foo.localhost',
- ]);
- $this->migrateTenants();
- $user = $tenant->run(function () {
- return AnotherImpersonationUser::create([
- 'name' => 'Joe',
- 'email' => 'joe@local',
- 'password' => bcrypt('secret'),
- ]);
- });
-
- // We try to visit the dashboard directly, before impersonating the user.
- $this->get('http://foo.localhost/dashboard')
- ->assertRedirect('http://foo.localhost/login');
-
- // We impersonate the user
- $token = tenancy()->impersonate($tenant, $user->id, '/dashboard', 'another');
- $this->get('http://foo.localhost/impersonate/' . $token->token)
- ->assertRedirect('http://foo.localhost/dashboard');
-
- // Now we try to visit the dashboard directly, after impersonating the user.
- $this->get('http://foo.localhost/dashboard')
- ->assertSuccessful()
- ->assertSee('You are logged in as Joe');
-
- Tenant::first()->run(function () {
- $this->assertSame('Joe', auth()->guard('another')->user()->name);
- $this->assertSame(null, auth()->guard('web')->user());
- });
- }
+ };
}
class ImpersonationUser extends Authenticable
{
protected $guarded = [];
+
protected $table = 'users';
}
class AnotherImpersonationUser extends Authenticable
{
protected $guarded = [];
+
protected $table = 'users';
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index cea669a1..554aeb8d 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -23,7 +23,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
Redis::connection('cache')->flushdb();
file_put_contents(database_path('central.sqlite'), '');
- $this->artisan('migrate:fresh', [
+ pest()->artisan('migrate:fresh', [
'--force' => true,
'--path' => __DIR__ . '/../assets/migrations',
'--realpath' => true,
@@ -81,6 +81,10 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
],
'database.connections.sqlite.database' => ':memory:',
'database.connections.mysql.host' => env('TENANCY_TEST_MYSQL_HOST', '127.0.0.1'),
+ 'database.connections.sqlsrv.username' => env('TENANCY_TEST_SQLSRV_USERNAME', 'sa'),
+ 'database.connections.sqlsrv.password' => env('TENANCY_TEST_SQLSRV_PASSWORD', 'P@ssword'),
+ 'database.connections.sqlsrv.host' => env('TENANCY_TEST_SQLSRV_HOST', '127.0.0.1'),
+ 'database.connections.sqlsrv.database' => null,
'database.connections.pgsql.host' => env('TENANCY_TEST_PGSQL_HOST', '127.0.0.1'),
'tenancy.filesystem.disks' => [
'local',
diff --git a/tests/UniversalRouteTest.php b/tests/UniversalRouteTest.php
index c0852545..20723cca 100644
--- a/tests/UniversalRouteTest.php
+++ b/tests/UniversalRouteTest.php
@@ -2,65 +2,76 @@
declare(strict_types=1);
-namespace Stancl\Tenancy\Tests;
-
use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Features\UniversalRoutes;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Tests\Etc\Tenant;
-class UniversalRouteTest extends TestCase
-{
- public function tearDown(): void
- {
- InitializeTenancyByDomain::$onFail = null;
+afterEach(function () {
+ InitializeTenancyByDomain::$onFail = null;
+});
- parent::tearDown();
- }
+test('a route can work in both central and tenant context', function () {
+ Route::middlewareGroup('universal', []);
+ config(['tenancy.features' => [UniversalRoutes::class]]);
- /** @test */
- public function a_route_can_work_in_both_central_and_tenant_context()
- {
- Route::middlewareGroup('universal', []);
- config(['tenancy.features' => [UniversalRoutes::class]]);
+ Route::get('/foo', function () {
+ return tenancy()->initialized
+ ? 'Tenancy is initialized.'
+ : 'Tenancy is not initialized.';
+ })->middleware(['universal', InitializeTenancyByDomain::class]);
- Route::get('/foo', function () {
- return tenancy()->initialized
- ? 'Tenancy is initialized.'
- : 'Tenancy is not initialized.';
- })->middleware(['universal', InitializeTenancyByDomain::class]);
+ pest()->get('http://localhost/foo')
+ ->assertSuccessful()
+ ->assertSee('Tenancy is not initialized.');
- $this->get('http://localhost/foo')
- ->assertSuccessful()
- ->assertSee('Tenancy is not initialized.');
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ ]);
+ $tenant->domains()->create([
+ 'domain' => 'acme.localhost',
+ ]);
- $tenant = Tenant::create([
- 'id' => 'acme',
- ]);
- $tenant->domains()->create([
- 'domain' => 'acme.localhost',
- ]);
+ pest()->get('http://acme.localhost/foo')
+ ->assertSuccessful()
+ ->assertSee('Tenancy is initialized.');
+});
- $this->get('http://acme.localhost/foo')
- ->assertSuccessful()
- ->assertSee('Tenancy is initialized.');
- }
+test('making one route universal doesnt make all routes universal', function () {
+ Route::get('/bar', function () {
+ return tenant('id');
+ })->middleware(InitializeTenancyByDomain::class);
- /** @test */
- public function making_one_route_universal_doesnt_make_all_routes_universal()
- {
- Route::get('/bar', function () {
- return tenant('id');
- })->middleware(InitializeTenancyByDomain::class);
+ Route::middlewareGroup('universal', []);
+ config(['tenancy.features' => [UniversalRoutes::class]]);
- $this->a_route_can_work_in_both_central_and_tenant_context();
- tenancy()->end();
+ Route::get('/foo', function () {
+ return tenancy()->initialized
+ ? 'Tenancy is initialized.'
+ : 'Tenancy is not initialized.';
+ })->middleware(['universal', InitializeTenancyByDomain::class]);
- $this->get('http://localhost/bar')
- ->assertStatus(500);
+ pest()->get('http://localhost/foo')
+ ->assertSuccessful()
+ ->assertSee('Tenancy is not initialized.');
- $this->get('http://acme.localhost/bar')
- ->assertSuccessful()
- ->assertSee('acme');
- }
-}
+ $tenant = Tenant::create([
+ 'id' => 'acme',
+ ]);
+ $tenant->domains()->create([
+ 'domain' => 'acme.localhost',
+ ]);
+
+ pest()->get('http://acme.localhost/foo')
+ ->assertSuccessful()
+ ->assertSee('Tenancy is initialized.');
+
+ tenancy()->end();
+
+ pest()->get('http://localhost/bar')
+ ->assertStatus(500);
+
+ pest()->get('http://acme.localhost/bar')
+ ->assertSuccessful()
+ ->assertSee('acme');
+});