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

Merge branch 'master' into configurable-force-rls

This commit is contained in:
lukinovec 2025-05-15 15:20:21 +02:00 committed by GitHub
commit f9f9e1814a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 1056 additions and 497 deletions

View file

@ -16,7 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- laravel: "^11.0" - laravel: "^12.0"
php: "8.4" php: "8.4"
steps: steps:

View file

@ -10,12 +10,20 @@ jobs:
steps: steps:
- name: Prepare composer version constraint prefix - name: Prepare composer version constraint prefix
run: | run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
# For refs like "refs/tags/v3.9.0", remove "refs/tags/v" prefix to get just "3.9.0"
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION_PREFIX=${VERSION}" >> $GITHUB_ENV
else
BRANCH=${GITHUB_REF#refs/heads/} BRANCH=${GITHUB_REF#refs/heads/}
if [[ $BRANCH =~ ^[0-9] ]]; then if [[ $BRANCH =~ ^[0-9]\.x$ ]]; then
# Branches starting with %d.x need to use -dev suffix
echo "VERSION_PREFIX=${BRANCH}-dev" >> $GITHUB_ENV echo "VERSION_PREFIX=${BRANCH}-dev" >> $GITHUB_ENV
else else
# All other branches use dev-${branch} prefix
echo "VERSION_PREFIX=dev-${BRANCH}" >> $GITHUB_ENV echo "VERSION_PREFIX=dev-${BRANCH}" >> $GITHUB_ENV
fi fi
fi
- name: Clone test suite - name: Clone test suite
run: git clone https://github.com/archtechx/tenancy-queue-tester run: git clone https://github.com/archtechx/tenancy-queue-tester
@ -25,3 +33,5 @@ jobs:
cd tenancy-queue-tester cd tenancy-queue-tester
TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./setup.sh TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./setup.sh
./test.sh ./test.sh
./alternative_config.sh
PERSISTENT=1 ./test.sh

View file

@ -24,10 +24,12 @@ To fix this, simply delete the database memory by shutting down containers and s
Same thing for `SQLSTATE[HY000]: General error: 1615 Prepared statement needs to be re-prepared`. Same thing for `SQLSTATE[HY000]: General error: 1615 Prepared statement needs to be re-prepared`.
### Docker on M1 ### Docker on Apple Silicon
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. 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.
2025 note: By now only MSSQL doesn't have good M1 support. The override also started being a bit problematic, having issues with starts, often requiring multiple starts. This often makes the original image in docker-compose more stable, even if it's amd64-only. With Rosetta enabled, you should be able to use it without issues.
### Coverage reports ### Coverage reports
To run tests and generate coverage reports, use `composer test-full`. To run tests and generate coverage reports, use `composer test-full`.

View file

@ -53,6 +53,7 @@ class TenancyServiceProvider extends ServiceProvider
Events\DeletingTenant::class => [ Events\DeletingTenant::class => [
JobPipeline::make([ JobPipeline::make([
Jobs\DeleteDomains::class, Jobs\DeleteDomains::class,
// Jobs\RemoveStorageSymlinks::class,
])->send(function (Events\DeletingTenant $event) { ])->send(function (Events\DeletingTenant $event) {
return $event->tenant; return $event->tenant;
})->shouldBeQueued(false), })->shouldBeQueued(false),
@ -62,7 +63,6 @@ class TenancyServiceProvider extends ServiceProvider
Events\TenantDeleted::class => [ Events\TenantDeleted::class => [
JobPipeline::make([ JobPipeline::make([
Jobs\DeleteDatabase::class, Jobs\DeleteDatabase::class,
// Jobs\RemoveStorageSymlinks::class,
])->send(function (Events\TenantDeleted $event) { ])->send(function (Events\TenantDeleted $event) {
return $event->tenant; return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
@ -145,24 +145,22 @@ class TenancyServiceProvider extends ServiceProvider
*/ */
protected function overrideUrlInTenantContext(): void protected function overrideUrlInTenantContext(): void
{ {
/** // \Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant, string $originalRootUrl) {
* Import your tenant model! // $tenantDomain = $tenant instanceof \Stancl\Tenancy\Contracts\SingleDomainTenant
* // ? $tenant->domain
* \Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper::$rootUrlOverride = function (Tenant $tenant, string $originalRootUrl) { // : $tenant->domains->first()->domain;
* $tenantDomain = $tenant instanceof \Stancl\Tenancy\Contracts\SingleDomainTenant //
* ? $tenant->domain // $scheme = str($originalRootUrl)->before('://');
* : $tenant->domains->first()->domain; //
* // if (str_contains($tenantDomain, '.')) {
* $scheme = str($originalRootUrl)->before('://'); // // Domain identification
* // return $scheme . '://' . $tenantDomain . '/';
* // If you're using domain identification: // } else {
* return $scheme . '://' . $tenantDomain . '/'; // // Subdomain identification
* // $originalDomain = str($originalRootUrl)->after($scheme . '://')->before('/');
* // If you're using subdomain identification: // return $scheme . '://' . $tenantDomain . '.' . $originalDomain . '/';
* $originalDomain = str($originalRootUrl)->after($scheme . '://'); // }
* return $scheme . '://' . $tenantDomain . '.' . $originalDomain . '/'; // };
* };
*/
} }
public function register() public function register()
@ -178,32 +176,17 @@ class TenancyServiceProvider extends ServiceProvider
$this->makeTenancyMiddlewareHighestPriority(); $this->makeTenancyMiddlewareHighestPriority();
$this->overrideUrlInTenantContext(); $this->overrideUrlInTenantContext();
/** // // Include soft deleted resources in synced resource queries.
* Include soft deleted resources in synced resource queries. // ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) {
* // if ($query->hasMacro('withTrashed')) {
* ResourceSyncing\Listeners\UpdateOrCreateSyncedResource::$scopeGetModelQuery = function (Builder $query) { // $query->withTrashed();
* if ($query->hasMacro('withTrashed')) {
* $query->withTrashed();
* }
* };
*/
/**
* To make Livewire v3 work with Tenancy, make the update route universal.
*
* Livewire::setUpdateRoute(function ($handle) {
* return RouteFacade::post('/livewire/update', $handle)->middleware(['web', 'universal']);
* });
*/
// if (InitializeTenancyByRequestData::inGlobalStack()) {
// FortifyRouteBootstrapper::$fortifyHome = 'dashboard';
// TenancyUrlGenerator::$prefixRouteNames = false;
// } // }
// };
if (tenancy()->globalStackHasMiddleware(config('tenancy.identification.path_identification_middleware'))) { // // To make Livewire v3 work with Tenancy, make the update route universal.
TenancyUrlGenerator::$prefixRouteNames = true; // Livewire::setUpdateRoute(function ($handle) {
} // return RouteFacade::post('/livewire/update', $handle)->middleware(['web', 'universal', \Stancl\Tenancy\Tenancy::defaultMiddleware()]);
// });
} }
protected function bootEvents() protected function bootEvents()
@ -228,10 +211,7 @@ class TenancyServiceProvider extends ServiceProvider
->group(base_path('routes/tenant.php')); ->group(base_path('routes/tenant.php'));
} }
// Delete this condition when using route-level path identification // $this->cloneRoutes();
if (tenancy()->globalStackHasMiddleware(config('tenancy.identification.path_identification_middleware'))) {
$this->cloneRoutes();
}
}); });
} }
@ -245,16 +225,13 @@ class TenancyServiceProvider extends ServiceProvider
/** @var CloneRoutesAsTenant $cloneRoutes */ /** @var CloneRoutesAsTenant $cloneRoutes */
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class); $cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
/** // // You can provide a closure for cloning a specific route, e.g.:
* You can provide a closure for cloning a specific route, e.g.: // $cloneRoutes->cloneUsing('welcome', function () {
* $cloneRoutes->cloneUsing('welcome', function () { // RouteFacade::get('/tenant-welcome', fn () => 'Current tenant: ' . tenant()->getTenantKey())
* RouteFacade::get('/tenant-welcome', fn () => 'Current tenant: ' . tenant()->getTenantKey()) // ->middleware(['universal', InitializeTenancyByPath::class])
* ->middleware(['universal', InitializeTenancyByPath::class]) // ->name('tenant.welcome');
* ->name('tenant.welcome'); // });
* }); // // To see the default behavior of cloning the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant.
*
* To see the default behavior of cloning the universal routes, check out the cloneRoute() method in CloneRoutesAsTenant.
*/
$cloneRoutes->handle(); $cloneRoutes->handle();
} }

View file

@ -15,6 +15,7 @@ return [
'models' => [ 'models' => [
'tenant' => Stancl\Tenancy\Database\Models\Tenant::class, 'tenant' => Stancl\Tenancy\Database\Models\Tenant::class,
'domain' => Stancl\Tenancy\Database\Models\Domain::class, 'domain' => Stancl\Tenancy\Database\Models\Domain::class,
'impersonation_token' => Stancl\Tenancy\Database\Models\ImpersonationToken::class,
/** /**
* Name of the column used to relate models to tenants. * Name of the column used to relate models to tenants.
@ -33,6 +34,7 @@ return [
* *
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator * @see \Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator * @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator
* @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator * @see \Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator
*/ */
'id_generator' => UniqueIdentifierGenerators\UUIDGenerator::class, 'id_generator' => UniqueIdentifierGenerators\UUIDGenerator::class,
@ -90,7 +92,7 @@ return [
/** /**
* Identification middleware tenancy recognizes as path identification middleware. * Identification middleware tenancy recognizes as path identification middleware.
* *
* This is used during determining whether whether a path identification is used * This is used for determining if a path identification middleware is used
* during operations specific to path identification, e.g. forgetting the tenant parameter in ForgetTenantParameter. * during operations specific to path identification, e.g. forgetting the tenant parameter in ForgetTenantParameter.
* *
* If you're using a custom path identification middleware, add it here. * If you're using a custom path identification middleware, add it here.
@ -117,6 +119,7 @@ return [
Resolvers\PathTenantResolver::class => [ Resolvers\PathTenantResolver::class => [
'tenant_parameter_name' => 'tenant', 'tenant_parameter_name' => 'tenant',
'tenant_model_column' => null, // null = tenant key 'tenant_model_column' => null, // null = tenant key
'tenant_route_name_prefix' => null, // null = 'tenant.'
'allowed_extra_model_columns' => [], // used with binding route fields 'allowed_extra_model_columns' => [], // used with binding route fields
'cache' => false, 'cache' => false,
@ -129,8 +132,6 @@ return [
'cache_store' => null, // null = default 'cache_store' => null, // null = default
], ],
], ],
// todo@docs update integration guides to use Stancl\Tenancy::defaultMiddleware()
], ],
/** /**
@ -214,7 +215,14 @@ return [
// 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager::class, // Also permission controlled // 'pgsql' => Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager::class, // Also permission controlled
], ],
// todo@docblock /*
* Drop tenant databases when `php artisan migrate:fresh` is used.
* You may want to use this locally since deleting tenants only
* deletes their databases when they're deleted individually, not
* when the records are mass deleted from the database.
*
* Note: This overrides the default MigrateFresh command.
*/
'drop_tenant_databases_on_migrate_fresh' => false, 'drop_tenant_databases_on_migrate_fresh' => false,
], ],
@ -319,7 +327,6 @@ return [
*/ */
'url_override' => [ 'url_override' => [
// Note that the local disk you add must exist in the tenancy.filesystem.root_override config // Note that the local disk you add must exist in the tenancy.filesystem.root_override config
// todo@v4 Rename url_override to something that describes the config key better
'public' => 'public-%tenant%', 'public' => 'public-%tenant%',
], ],
@ -355,7 +362,7 @@ return [
* leave asset() helper tenancy disabled and explicitly use tenant_asset() calls in places * leave asset() helper tenancy disabled and explicitly use tenant_asset() calls in places
* where you want to use tenant-specific assets (product images, avatars, etc). * where you want to use tenant-specific assets (product images, avatars, etc).
*/ */
'asset_helper_tenancy' => false, // todo@rename asset_helper_override? 'asset_helper_override' => false,
], ],
/** /**

View file

@ -18,26 +18,24 @@
"require": { "require": {
"php": "^8.4", "php": "^8.4",
"ext-json": "*", "ext-json": "*",
"illuminate/support": "^10.1|^11.3", "illuminate/support": "^12.0",
"laravel/tinker": "^2.0", "laravel/tinker": "^2.0",
"facade/ignition-contracts": "^1.0.2", "facade/ignition-contracts": "^1.0.2",
"spatie/ignition": "^1.4", "spatie/ignition": "^1.4",
"ramsey/uuid": "^4.7.3", "ramsey/uuid": "^4.7.3",
"stancl/jobpipeline": "2.0.0-rc2", "stancl/jobpipeline": "2.0.0-rc5",
"stancl/virtualcolumn": "dev-master", "stancl/virtualcolumn": "^1.5.0",
"spatie/invade": "^1.1", "spatie/invade": "*",
"laravel/prompts": "0.*" "laravel/prompts": "0.*"
}, },
"require-dev": { "require-dev": {
"laravel/framework": "^10.1|^11.3", "laravel/framework": "^12.0",
"orchestra/testbench": "^8.0|^9.0", "orchestra/testbench": "^10.0",
"league/flysystem-aws-s3-v3": "^3.12.2", "league/flysystem-aws-s3-v3": "^3.12.2",
"doctrine/dbal": "^3.6.0", "doctrine/dbal": "^3.6.0",
"spatie/valuestore": "^1.2.5", "spatie/valuestore": "^1.2.5",
"pestphp/pest": "^2.0", "pestphp/pest": "^3.0",
"larastan/larastan": "^3.0", "larastan/larastan": "^3.0"
"spatie/invade": "^1.1",
"aws/aws-sdk-php-laravel": "~3.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View file

@ -1,9 +1,3 @@
services: services:
mysql:
# platform: linux/amd64 # either one works
image: arm64v8/mysql
mysql2:
# platform: linux/amd64 # either one works
image: arm64v8/mysql
mssql: mssql:
image: mcr.microsoft.com/azure-sql-edge image: mcr.microsoft.com/azure-sql-edge

View file

@ -33,7 +33,7 @@ services:
stdin_open: true stdin_open: true
tty: true tty: true
mysql: mysql:
image: mysql:5.7 image: mysql:8
environment: environment:
MYSQL_ROOT_PASSWORD: password MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: main MYSQL_DATABASE: main
@ -46,7 +46,7 @@ services:
tmpfs: tmpfs:
- /var/lib/mysql - /var/lib/mysql
mysql2: mysql2:
image: mysql:5.7 image: mysql:8
environment: environment:
MYSQL_ROOT_PASSWORD: password MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: main MYSQL_DATABASE: main
@ -72,12 +72,12 @@ services:
tmpfs: tmpfs:
- /var/lib/postgresql/data - /var/lib/postgresql/data
mssql: mssql:
image: mcr.microsoft.com/mssql/server:2019-latest image: mcr.microsoft.com/mssql/server:2022-latest
environment: environment:
- ACCEPT_EULA=Y - ACCEPT_EULA=Y
- SA_PASSWORD=P@ssword # todo reuse env from above - SA_PASSWORD=P@ssword # todo reuse env from above
healthcheck: # https://github.com/Microsoft/mssql-docker/issues/133#issuecomment-1995615432 healthcheck: # https://github.com/Microsoft/mssql-docker/issues/133#issuecomment-1995615432
test: timeout 1 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/1433' test: timeout 2 bash -c 'cat < /dev/null > /dev/tcp/127.0.0.1/1433'
interval: 10s interval: 10s
timeout: 10s timeout: 10s
retries: 10 retries: 10

View file

@ -1,6 +1,5 @@
includes: includes:
- ./vendor/larastan/larastan/extension.neon - ./vendor/larastan/larastan/extension.neon
- ./vendor/spatie/invade/phpstan-extension.neon
parameters: parameters:
paths: paths:
@ -16,6 +15,12 @@ parameters:
ignoreErrors: ignoreErrors:
- identifier: trait.unused - identifier: trait.unused
- identifier: missingType.iterableValue - identifier: missingType.iterableValue
-
message: '#Spatie\\Invade\\Invader#'
identifier: method.notFound
-
message: '#Spatie\\Invade\\Invader#'
identifier: property.notFound
- '#FFI#' - '#FFI#'
- '#Return type(.*?) of method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should be compatible with return type#' - '#Return type(.*?) of method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should be compatible with return type#'
- '#Method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should return#' - '#Method Stancl\\Tenancy\\Database\\Models\\Tenant\:\:newCollection\(\) should return#'

View file

@ -92,7 +92,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
protected function assetHelper(string|false $suffix): void protected function assetHelper(string|false $suffix): void
{ {
if (! $this->app['config']['tenancy.filesystem.asset_helper_tenancy']) { if (! $this->app['config']['tenancy.filesystem.asset_helper_override']) {
return; return;
} }

View file

@ -7,54 +7,52 @@ namespace Stancl\Tenancy\Bootstrappers\Integrations;
use Illuminate\Config\Repository; use Illuminate\Config\Repository;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Enums\Context;
use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Resolvers\PathTenantResolver;
/** /**
* Allows customizing Fortify action redirects * Allows customizing Fortify action redirects so that they can also redirect
* so that they can also redirect to tenant routes instead of just the central routes. * to tenant routes instead of just the central routes.
* *
* Works with path and query string identification. * This should be used with path/query string identification OR when using Fortify
* universally, including with domains.
*
* When using domain identification, there's no need to pass the tenant parameter,
* you only want to customize the routes being used, so you can set $passTenantParameter
* to false.
*/ */
class FortifyRouteBootstrapper implements TenancyBootstrapper class FortifyRouteBootstrapper implements TenancyBootstrapper
{ {
/** /**
* Make Fortify actions redirect to custom routes. * Fortify redirects that should be used in tenant context.
* *
* For each route redirect, specify the intended route context (central or tenant). * Syntax: ['redirect_name' => 'tenant_route_name']
* Based on the provided context, we pass the tenant parameter to the route (or not).
* The tenant parameter is only passed to the route when you specify its context as tenant.
*
* The route redirects should be in the following format:
*
* 'fortify_action' => [
* 'route_name' => 'tenant.route',
* 'context' => Context::TENANT,
* ]
*
* For example:
*
* FortifyRouteBootstrapper::$fortifyRedirectMap = [
* // On logout, redirect the user to the "bye" route in the central app
* 'logout' => [
* 'route_name' => 'bye',
* 'context' => Context::CENTRAL,
* ],
*
* // On login, redirect the user to the "welcome" route in the tenant app
* 'login' => [
* 'route_name' => 'welcome',
* 'context' => Context::TENANT,
* ],
* ];
*/ */
public static array $fortifyRedirectMap = []; public static array $fortifyRedirectMap = [];
/**
* Should the tenant parameter be passed to fortify routes in the tenant context.
*
* This should be enabled with path/query string identification and disabled with domain identification.
*
* You may also disable this when using path/query string identification if passing the tenant parameter
* is handled in another way (TenancyUrlGenerator::$passTenantParameter for both,
* UrlGeneratorBootstrapper:$addTenantParameterToDefaults for path identification).
*/
public static bool $passTenantParameter = true;
/** /**
* Tenant route that serves as Fortify's home (e.g. a tenant dashboard route). * Tenant route that serves as Fortify's home (e.g. a tenant dashboard route).
* This route will always receive the tenant parameter. * This route will always receive the tenant parameter.
*/ */
public static string $fortifyHome = 'tenant.dashboard'; public static string|null $fortifyHome = 'tenant.dashboard';
/**
* Use default parameter names ('tenant' name and tenant key value) instead of the parameter name
* and column name configured in the path resolver config.
*
* You want to enable this when using query string identification while having customized that config.
*/
public static bool $defaultParameterNames = false;
protected array $originalFortifyConfig = []; protected array $originalFortifyConfig = [];
@ -76,27 +74,22 @@ class FortifyRouteBootstrapper implements TenancyBootstrapper
protected function useTenantRoutesInFortify(Tenant $tenant): void protected function useTenantRoutesInFortify(Tenant $tenant): void
{ {
$tenantKey = $tenant->getTenantKey(); $tenantParameterName = static::$defaultParameterNames ? 'tenant' : PathTenantResolver::tenantParameterName();
$tenantParameterName = PathTenantResolver::tenantParameterName(); $tenantParameterValue = static::$defaultParameterNames ? $tenant->getTenantKey() : PathTenantResolver::tenantParameterValue($tenant);
$generateLink = function (array $redirect) use ($tenantKey, $tenantParameterName) { $generateLink = function (string $redirect) use ($tenantParameterValue, $tenantParameterName) {
// Specifying the context is only required with query string identification return route($redirect, static::$passTenantParameter ? [$tenantParameterName => $tenantParameterValue] : []);
// because with path identification, the tenant parameter should always present
$passTenantParameter = $redirect['context'] === Context::TENANT;
// Only pass the tenant parameter when the user should be redirected to a tenant route
return route($redirect['route_name'], $passTenantParameter ? [$tenantParameterName => $tenantKey] : []);
}; };
// Get redirect URLs for the configured redirect routes // Get redirect URLs for the configured redirect routes
$redirects = array_merge( $redirects = array_merge(
$this->originalFortifyConfig['redirects'] ?? [], // Fortify config redirects $this->originalFortifyConfig['redirects'] ?? [], // Fortify config redirects
array_map(fn (array $redirect) => $generateLink($redirect), static::$fortifyRedirectMap), // Mapped redirects array_map(fn (string $redirect) => $generateLink($redirect), static::$fortifyRedirectMap), // Mapped redirects
); );
if (static::$fortifyHome) { if (static::$fortifyHome) {
// Generate the home route URL with the tenant parameter and make it the Fortify home route // Generate the home route URL with the tenant parameter and make it the Fortify home route
$this->config->set('fortify.home', route(static::$fortifyHome, [$tenantParameterName => $tenantKey])); $this->config->set('fortify.home', route(static::$fortifyHome, static::$passTenantParameter ? [$tenantParameterName => $tenantParameterValue] : []));
} }
$this->config->set('fortify.redirects', $redirects); $this->config->set('fortify.redirects', $redirects);

View file

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Bootstrappers;
use Illuminate\Config\Repository;
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\Queue\Events\JobRetryRequested;
use Illuminate\Queue\QueueManager;
use Illuminate\Support\Testing\Fakes\QueueFake;
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant;
class PersistentQueueTenancyBootstrapper implements TenancyBootstrapper
{
/** @var Repository */
protected $config;
/** @var QueueManager */
protected $queue;
/**
* The normal constructor is only executed after tenancy is bootstrapped.
* However, we're registering a hook to initialize tenancy. Therefore,
* we need to register the hook at service provider execution time.
*/
public static function __constructStatic(Application $app): void
{
static::setUpJobListener($app->make(Dispatcher::class), $app->runningUnitTests());
}
public function __construct(Repository $config, QueueManager $queue)
{
$this->config = $config;
$this->queue = $queue;
$this->setUpPayloadGenerator();
}
protected static function setUpJobListener(Dispatcher $dispatcher, bool $runningTests): void
{
$previousTenant = null;
$dispatcher->listen(JobProcessing::class, function ($event) use (&$previousTenant) {
$previousTenant = tenant();
static::initializeTenancyForQueue($event->job->payload()['tenant_id'] ?? null);
});
$dispatcher->listen(JobRetryRequested::class, function ($event) use (&$previousTenant) {
$previousTenant = tenant();
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
});
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
$revertToPreviousState = function ($event) use (&$previousTenant, $runningTests) {
if ($runningTests) {
static::revertToPreviousState($event->job->payload()['tenant_id'] ?? null, $previousTenant);
// We don't need to reset $previousTenant since the value will be set again when a job is processed.
}
// If we're not running tests, we remain in the tenant's context. This makes other JobProcessed
// listeners able to deserialize the job, including with SerializesModels, since the tenant connection
// remains open.
};
$dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
}
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
{
if (! $tenantId) {
// The job is not tenant-aware
if (tenancy()->initialized) {
// Tenancy was initialized, so we revert back to the central context
tenancy()->end();
}
return;
}
// Re-initialize tenancy between all jobs even if the tenant is the same
// so that we don't work with an outdated tenant() instance in case it
// was updated outside the queue worker.
tenancy()->end();
/** @var Tenant $tenant */
$tenant = tenancy()->find($tenantId);
tenancy()->initialize($tenant);
}
protected static function revertToPreviousState(string|int|null $tenantId, ?Tenant $previousTenant): void
{
// The job was not tenant-aware
if (! $tenantId) {
return;
}
// Revert back to the previous tenant
if (tenant() && $previousTenant && $previousTenant->isNot(tenant())) {
tenancy()->initialize($previousTenant);
}
// End tenancy
if (tenant() && (! $previousTenant)) {
tenancy()->end();
}
}
protected function setUpPayloadGenerator(): void
{
$bootstrapper = &$this;
if (! $this->queue instanceof QueueFake) {
$this->queue->createPayloadUsing(function ($connection) use (&$bootstrapper) {
return $bootstrapper->getPayload($connection);
});
}
}
public function getPayload(string $connection): array
{
if (! tenancy()->initialized) {
return [];
}
if ($this->config["queue.connections.$connection.central"]) {
return [];
}
return [
'tenant_id' => tenant()->getTenantKey(),
];
}
public function bootstrap(Tenant $tenant): void {}
public function revert(): void {}
}

View file

@ -24,16 +24,6 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
/** @var QueueManager */ /** @var QueueManager */
protected $queue; protected $queue;
/**
* Don't persist the same tenant across multiple jobs even if they have the same tenant ID.
*
* 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 $forceRefresh = false;
/** /**
* The normal constructor is only executed after tenancy is bootstrapped. * The normal constructor is only executed after tenancy is bootstrapped.
* However, we're registering a hook to initialize tenancy. Therefore, * However, we're registering a hook to initialize tenancy. Therefore,
@ -68,9 +58,12 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null); static::initializeTenancyForQueue($event->payload()['tenant_id'] ?? null);
}); });
// If we're running tests, we make sure to clean up after any artisan('queue:work') calls
$revertToPreviousState = function ($event) use (&$previousTenant) { $revertToPreviousState = function ($event) use (&$previousTenant) {
static::revertToPreviousState($event, $previousTenant); // In queue worker context, this reverts to the central context.
// In dispatchSync context, this reverts to the previous tenant's context.
// There's no need to reset $previousTenant here since it's always first
// set in the above listeners and the app is reverted back to that context.
static::revertToPreviousState($event->job->payload()['tenant_id'] ?? null, $previousTenant);
}; };
$dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds $dispatcher->listen(JobProcessed::class, $revertToPreviousState); // artisan('queue:work') which succeeds
@ -79,61 +72,25 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
protected static function initializeTenancyForQueue(string|int|null $tenantId): void protected static function initializeTenancyForQueue(string|int|null $tenantId): void
{ {
if ($tenantId === null) {
// The job is not tenant-aware
if (tenancy()->initialized) {
// Tenancy was initialized, so we revert back to the central context
tenancy()->end();
}
return;
}
if (static::$forceRefresh) {
// Re-initialize tenancy between all jobs
if (tenancy()->initialized) {
tenancy()->end();
}
/** @var Tenant $tenant */
$tenant = tenancy()->find($tenantId);
tenancy()->initialize($tenant);
return;
}
if (tenancy()->initialized) {
// Tenancy is already initialized
if (tenant()->getTenantKey() === $tenantId) {
// It's initialized for the same tenant (e.g. dispatchSync was used, or the previous job also ran for this tenant)
return;
}
}
// Tenancy was either not initialized, or initialized for a different tenant.
// Therefore, we initialize it for the correct tenant.
/** @var Tenant $tenant */
$tenant = tenancy()->find($tenantId);
tenancy()->initialize($tenant);
}
protected static function revertToPreviousState(JobProcessed|JobFailed $event, ?Tenant &$previousTenant): void
{
$tenantId = $event->job->payload()['tenant_id'] ?? null;
// The job was not tenant-aware
if (! $tenantId) { if (! $tenantId) {
return; return;
} }
// Revert back to the previous tenant /** @var Tenant $tenant */
if (tenant() && $previousTenant?->isNot(tenant())) { $tenant = tenancy()->find($tenantId);
tenancy()->initialize($previousTenant); tenancy()->initialize($tenant);
} }
// End tenancy protected static function revertToPreviousState(string|int|null $tenantId, ?Tenant $previousTenant): void
if (tenant() && (! $previousTenant)) { {
// The job was not tenant-aware so no context switch was done
if (! $tenantId) {
return;
}
// End tenancy when there's no previous tenant
// (= when running in a queue worker, not dispatchSync)
if (tenant() && ! $previousTenant) {
tenancy()->end(); tenancy()->end();
} }
} }
@ -149,16 +106,6 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
} }
} }
public function bootstrap(Tenant $tenant): void
{
//
}
public function revert(): void
{
//
}
public function getPayload(string $connection): array public function getPayload(string $connection): array
{ {
if (! tenancy()->initialized) { if (! tenancy()->initialized) {
@ -169,10 +116,11 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
return []; return [];
} }
$id = tenant()->getTenantKey();
return [ return [
'tenant_id' => $id, 'tenant_id' => tenant()->getTenantKey(),
]; ];
} }
public function bootstrap(Tenant $tenant): void {}
public function revert(): void {}
} }

View file

@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Bootstrappers;
use Closure; use Closure;
use Illuminate\Config\Repository; use Illuminate\Config\Repository;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Routing\UrlGenerator;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
@ -36,28 +35,43 @@ class RootUrlBootstrapper implements TenancyBootstrapper
protected string|null $originalRootUrl = null; protected string|null $originalRootUrl = null;
/**
* Overriding the root url may cause issues in *some* tests, so you can disable
* the behavior by setting this property to false.
*/
public static bool $rootUrlOverrideInTests = true;
public function __construct( public function __construct(
protected UrlGenerator $urlGenerator,
protected Repository $config, protected Repository $config,
protected Application $app, protected Application $app,
) {} ) {}
public function bootstrap(Tenant $tenant): void public function bootstrap(Tenant $tenant): void
{ {
if ($this->app->runningInConsole() && static::$rootUrlOverride) { if (static::$rootUrlOverride === null) {
$this->originalRootUrl = $this->urlGenerator->to('/'); return;
}
if (! $this->app->runningInConsole()) {
return;
}
if ($this->app->runningUnitTests() && ! static::$rootUrlOverrideInTests) {
return;
}
$this->originalRootUrl = $this->app['url']->to('/');
$newRootUrl = (static::$rootUrlOverride)($tenant, $this->originalRootUrl); $newRootUrl = (static::$rootUrlOverride)($tenant, $this->originalRootUrl);
$this->urlGenerator->forceRootUrl($newRootUrl); $this->app['url']->forceRootUrl($newRootUrl);
$this->config->set('app.url', $newRootUrl); $this->config->set('app.url', $newRootUrl);
} }
}
public function revert(): void public function revert(): void
{ {
if ($this->originalRootUrl) { if ($this->originalRootUrl) {
$this->urlGenerator->forceRootUrl($this->originalRootUrl); $this->app['url']->forceRootUrl($this->originalRootUrl);
$this->config->set('app.url', $this->originalRootUrl); $this->config->set('app.url', $this->originalRootUrl);
} }
} }

View file

@ -10,6 +10,7 @@ use Illuminate\Support\Facades\URL;
use Stancl\Tenancy\Contracts\TenancyBootstrapper; use Stancl\Tenancy\Contracts\TenancyBootstrapper;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Overrides\TenancyUrlGenerator; use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
use Stancl\Tenancy\Resolvers\PathTenantResolver;
/** /**
* Makes the app use TenancyUrlGenerator (instead of Illuminate\Routing\UrlGenerator) which: * Makes the app use TenancyUrlGenerator (instead of Illuminate\Routing\UrlGenerator) which:
@ -19,10 +20,20 @@ use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
* Used with path and query string identification. * Used with path and query string identification.
* *
* @see TenancyUrlGenerator * @see TenancyUrlGenerator
* @see \Stancl\Tenancy\Resolvers\PathTenantResolver * @see PathTenantResolver
*/ */
class UrlGeneratorBootstrapper implements TenancyBootstrapper class UrlGeneratorBootstrapper implements TenancyBootstrapper
{ {
/**
* Should the tenant route parameter get added to TenancyUrlGenerator::defaults().
*
* This is recommended when using path identification since defaults() generally has better support in integrations,
* namely Ziggy, compared to TenancyUrlGenerator::$passTenantParameterToRoutes.
*
* With query string identification, this has no effect since URL::defaults() only works for route paramaters.
*/
public static bool $addTenantParameterToDefaults = true;
public function __construct( public function __construct(
protected Application $app, protected Application $app,
protected UrlGenerator $originalUrlGenerator, protected UrlGenerator $originalUrlGenerator,
@ -32,12 +43,12 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
{ {
URL::clearResolvedInstances(); URL::clearResolvedInstances();
$this->useTenancyUrlGenerator(); $this->useTenancyUrlGenerator($tenant);
} }
public function revert(): void public function revert(): void
{ {
$this->app->bind('url', fn () => $this->originalUrlGenerator); $this->app->extend('url', fn () => $this->originalUrlGenerator);
} }
/** /**
@ -45,16 +56,27 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
* *
* @see \Illuminate\Routing\RoutingServiceProvider registerUrlGenerator() * @see \Illuminate\Routing\RoutingServiceProvider registerUrlGenerator()
*/ */
protected function useTenancyUrlGenerator(): void protected function useTenancyUrlGenerator(Tenant $tenant): void
{ {
$this->app->extend('url', function (UrlGenerator $urlGenerator, Application $app) {
$newGenerator = new TenancyUrlGenerator( $newGenerator = new TenancyUrlGenerator(
$app['router']->getRoutes(), $this->app['router']->getRoutes(),
$urlGenerator->getRequest(), $this->originalUrlGenerator->getRequest(),
$app['config']->get('app.asset_url'), $this->app['config']->get('app.asset_url'),
); );
$newGenerator->defaults($urlGenerator->getDefaultParameters()); $defaultParameters = $this->originalUrlGenerator->getDefaultParameters();
if (static::$addTenantParameterToDefaults) {
$defaultParameters = array_merge(
$defaultParameters,
[
PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue($tenant), // path identification
'tenant' => $tenant->getTenantKey(), // query string identification
],
);
}
$newGenerator->defaults($defaultParameters);
$newGenerator->setSessionResolver(function () { $newGenerator->setSessionResolver(function () {
return $this->app['session'] ?? null; return $this->app['session'] ?? null;
@ -64,7 +86,6 @@ class UrlGeneratorBootstrapper implements TenancyBootstrapper
return $this->app->make('config')->get('app.key'); return $this->app->make('config')->get('app.key');
}); });
return $newGenerator; $this->app->extend('url', fn () => $newGenerator);
});
} }
} }

View file

@ -56,6 +56,6 @@ trait DealsWithTenantSymlinks
/** Determine if the provided path is an existing symlink. */ /** Determine if the provided path is an existing symlink. */
protected function symlinkExists(string $link): bool protected function symlinkExists(string $link): bool
{ {
return file_exists($link) && is_link($link); return is_link($link);
} }
} }

View file

@ -11,5 +11,5 @@ interface UniqueIdentifierGenerator
/** /**
* Generate a unique identifier for a model. * Generate a unique identifier for a model.
*/ */
public static function generate(Model $model): string; public static function generate(Model $model): string|int;
} }

View file

@ -10,16 +10,11 @@ trait CreatesDatabaseUsers
{ {
public function createDatabase(TenantWithDatabase $tenant): bool public function createDatabase(TenantWithDatabase $tenant): bool
{ {
parent::createDatabase($tenant); return parent::createDatabase($tenant) && $this->createUser($tenant->database());
return $this->createUser($tenant->database());
} }
public function deleteDatabase(TenantWithDatabase $tenant): bool public function deleteDatabase(TenantWithDatabase $tenant): bool
{ {
// Some DB engines require the user to be deleted before the database (e.g. Postgres) return $this->deleteUser($tenant->database()) && parent::deleteDatabase($tenant);
$this->deleteUser($tenant->database());
return parent::deleteDatabase($tenant);
} }
} }

View file

@ -23,6 +23,8 @@ use Stancl\Tenancy\Events\PullingPendingTenant;
*/ */
trait HasPending trait HasPending
{ {
public static string $pendingSinceCast = 'timestamp';
/** Boot the trait. */ /** Boot the trait. */
public static function bootHasPending(): void public static function bootHasPending(): void
{ {
@ -32,7 +34,7 @@ trait HasPending
/** Initialize the trait. */ /** Initialize the trait. */
public function initializeHasPending(): void public function initializeHasPending(): void
{ {
$this->casts['pending_since'] = 'timestamp'; $this->casts['pending_since'] = static::$pendingSinceCast;
} }
/** Determine if the model instance is in a pending state. */ /** Determine if the model instance is in a pending state. */

View file

@ -4,16 +4,13 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tenancy;
trait InvalidatesResolverCache trait InvalidatesResolverCache
{ {
public static function bootInvalidatesResolverCache(): void public static function bootInvalidatesResolverCache(): void
{ {
static::saved(function (Tenant&Model $tenant) { static::saved(Tenancy::invalidateResolverCache(...));
Tenancy::invalidateResolverCache($tenant); static::deleting(Tenancy::invalidateResolverCache(...));
});
} }
} }

View file

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Resolvers\Contracts\CachedTenantResolver;
use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tenancy;
/** /**
@ -15,13 +14,9 @@ trait InvalidatesTenantsResolverCache
{ {
public static function bootInvalidatesTenantsResolverCache(): void public static function bootInvalidatesTenantsResolverCache(): void
{ {
static::saved(function (Model $model) { $invalidateCache = static fn (Model $model) => Tenancy::invalidateResolverCache($model->tenant);
foreach (Tenancy::cachedResolvers() as $resolver) {
/** @var CachedTenantResolver $resolver */
$resolver = app($resolver);
$resolver->invalidateCache($model->tenant); static::saved($invalidateCache);
} static::deleting($invalidateCache);
});
} }
} }

View file

@ -25,6 +25,9 @@ class ImpersonationToken extends Model
{ {
use CentralConnection; use CentralConnection;
/** You can set this property to customize the table name */
public static string $tableName = 'tenant_user_impersonation_tokens';
protected $guarded = []; protected $guarded = [];
public $timestamps = false; public $timestamps = false;
@ -33,11 +36,15 @@ class ImpersonationToken extends Model
public $incrementing = false; public $incrementing = false;
protected $table = 'tenant_user_impersonation_tokens';
protected $casts = [ protected $casts = [
'created_at' => 'datetime', 'created_at' => 'datetime',
]; ];
public function getTable()
{
return static::$tableName;
}
public static function booted(): void public static function booted(): void
{ {
static::creating(function ($model) { static::creating(function ($model) {

View file

@ -11,8 +11,6 @@ class MicrosoftSQLDatabaseManager extends TenantDatabaseManager
public function createDatabase(TenantWithDatabase $tenant): bool public function createDatabase(TenantWithDatabase $tenant): bool
{ {
$database = $tenant->database()->getName(); $database = $tenant->database()->getName();
$charset = $this->connection()->getConfig('charset');
$collation = $this->connection()->getConfig('collation'); // todo check why these are not used
return $this->connection()->statement("CREATE DATABASE [{$database}]"); return $this->connection()->statement("CREATE DATABASE [{$database}]");
} }

View file

@ -32,6 +32,8 @@ class PermissionControlledPostgreSQLSchemaManager extends PostgreSQLSchemaManage
// Grant permissions to any existing tables. This is used with RLS // Grant permissions to any existing tables. This is used with RLS
// todo@samuel refactor this along with the todo in TenantDatabaseManager // todo@samuel refactor this along with the todo in TenantDatabaseManager
// and move the grantPermissions() call inside the condition in `ManagesPostgresUsers::createUser()` // and move the grantPermissions() call inside the condition in `ManagesPostgresUsers::createUser()`
// but maybe moving it inside $createUser is wrong because some central user may migrate new tables
// while the RLS user should STILL get access to those tables
foreach ($tables as $table) { foreach ($tables as $table) {
$tableName = $table->table_name; $tableName = $table->table_name;

View file

@ -7,15 +7,11 @@ namespace Stancl\Tenancy\Events\Contracts;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Stancl\Tenancy\Contracts\Tenant; use Stancl\Tenancy\Contracts\Tenant;
abstract class TenantEvent // todo we could add a feature to JobPipeline that automatically gets data for the send() from here abstract class TenantEvent
{ {
use SerializesModels; use SerializesModels;
/** @var Tenant */ public function __construct(
public $tenant; public Tenant $tenant,
) {}
public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
}
} }

View file

@ -2,8 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
// todo perhaps create Identification namespace
namespace Stancl\Tenancy\Exceptions; namespace Stancl\Tenancy\Exceptions;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Features; namespace Stancl\Tenancy\Features;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Stancl\Tenancy\Contracts\Feature; use Stancl\Tenancy\Contracts\Feature;
@ -18,8 +19,8 @@ class UserImpersonation implements Feature
public function bootstrap(Tenancy $tenancy): void public function bootstrap(Tenancy $tenancy): void
{ {
$tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): ImpersonationToken { $tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string|null $authGuard = null, bool $remember = false): Model {
return ImpersonationToken::create([ return UserImpersonation::modelClass()::create([
Tenancy::tenantKeyColumn() => $tenant->getTenantKey(), Tenancy::tenantKeyColumn() => $tenant->getTenantKey(),
'user_id' => $userId, 'user_id' => $userId,
'redirect_url' => $redirectUrl, 'redirect_url' => $redirectUrl,
@ -30,10 +31,15 @@ class UserImpersonation implements Feature
} }
/** Impersonate a user and get an HTTP redirect response. */ /** Impersonate a user and get an HTTP redirect response. */
public static function makeResponse(#[\SensitiveParameter] string|ImpersonationToken $token, ?int $ttl = null): RedirectResponse public static function makeResponse(#[\SensitiveParameter] string|Model $token, ?int $ttl = null): RedirectResponse
{ {
/** @var ImpersonationToken $token */ /**
$token = $token instanceof ImpersonationToken ? $token : ImpersonationToken::findOrFail($token); * The model does NOT have to extend ImpersonationToken, but usually it WILL be a child
* of ImpersonationToken and this makes it clear to phpstan that the model has a redirect_url property.
*
* @var ImpersonationToken $token
*/
$token = $token instanceof Model ? $token : static::modelClass()::findOrFail($token);
$ttl ??= static::$ttl; $ttl ??= static::$ttl;
$tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl; $tokenExpired = $token->created_at->diffInSeconds(now()) > $ttl;
@ -54,6 +60,12 @@ class UserImpersonation implements Feature
return redirect($token->redirect_url); return redirect($token->redirect_url);
} }
/** @return class-string<Model> */
public static function modelClass(): string
{
return config('tenancy.models.impersonation_token');
}
public static function isImpersonating(): bool public static function isImpersonating(): bool
{ {
return session()->has('tenancy_impersonating'); return session()->has('tenancy_impersonating');
@ -62,7 +74,7 @@ class UserImpersonation implements Feature
/** /**
* Logout from the current domain and forget impersonation session. * Logout from the current domain and forget impersonation session.
*/ */
public static function leave(): void // todo@name possibly rename public static function stopImpersonating(): void
{ {
auth()->logout(); auth()->logout();

View file

@ -8,6 +8,8 @@ use Illuminate\Routing\Events\RouteMatched;
use Stancl\Tenancy\Enums\RouteMode; use Stancl\Tenancy\Enums\RouteMode;
use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Resolvers\PathTenantResolver;
// todo@earlyIdReview
/** /**
* Remove the tenant parameter from the matched route when path identification is used globally. * Remove the tenant parameter from the matched route when path identification is used globally.
* *

View file

@ -68,7 +68,7 @@ class PreventAccessFromUnwantedDomains
return in_array($request->getHost(), config('tenancy.identification.central_domains'), true); return in_array($request->getHost(), config('tenancy.identification.central_domains'), true);
} }
// todo@samuel // todo@samuel technically not an identification middleware but probably ok to keep this here
public function requestHasTenant(Request $request): bool public function requestHasTenant(Request $request): bool
{ {
return false; return false;

View file

@ -19,6 +19,10 @@ class ScopeSessions
public function handle(Request $request, Closure $next): mixed public function handle(Request $request, Closure $next): mixed
{ {
if (! tenancy()->initialized) { if (! tenancy()->initialized) {
if (tenancy()->routeIsUniversal(tenancy()->getRoute($request))) {
return $next($request);
}
throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed'); throw new TenancyNotInitializedException('Tenancy needs to be initialized before the session scoping middleware is executed');
} }

View file

@ -6,8 +6,6 @@ namespace Stancl\Tenancy\Overrides;
use Illuminate\Cache\CacheManager as BaseCacheManager; use Illuminate\Cache\CacheManager as BaseCacheManager;
// todo@move move to Cache namespace?
class CacheManager extends BaseCacheManager class CacheManager extends BaseCacheManager
{ {
/** /**

View file

@ -13,39 +13,85 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
/** /**
* This class is used in place of the default UrlGenerator when UrlGeneratorBootstrapper is enabled. * This class is used in place of the default UrlGenerator when UrlGeneratorBootstrapper is enabled.
* *
* TenancyUrlGenerator does two extra things: * TenancyUrlGenerator does a few extra things:
* 1. Autofill the {tenant} parameter in the tenant context with the current tenant if $passTenantParameterToRoutes is enabled (enabled by default) * - Autofills the tenant parameter in the tenant context with the current tenant.
* 2. Prepend the route name with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled (disabled by default) * This is done either by:
* - URL::defaults() -- if UrlGeneratorBootstrapper::$addTenantParameterToDefaults is enabled.
* This generally has the best support since tools like e.g. Ziggy read defaults().
* - Automatically passing ['tenant' => ...] to each route() call -- if TenancyUrlGenerator::$passTenantParameterToRoutes is enabled
* This is a more universal solution since it supports both path identification and query parameter identification.
* *
* Both of these can be skipped by passing the $bypassParameter (`['central' => true]` by default) * - Prepends route names passed to route() and URL::temporarySignedRoute()
* with `tenant.` (or the configured prefix) if $prefixRouteNames is enabled.
* This is primarily useful when using route cloning with path identification.
*
* To bypass this behavior on any single route() call, pass the $bypassParameter as true (['central' => true] by default).
*/ */
class TenancyUrlGenerator extends UrlGenerator class TenancyUrlGenerator extends UrlGenerator
{ {
/** /**
* Parameter which bypasses the behavior modification of route() and temporarySignedRoute(). * Parameter which works as a flag for bypassing the behavior modification of route() and temporarySignedRoute().
* *
* E.g. route('tenant') => app.test/{tenant}/tenant (or app.test/tenant?tenant=tenantKey if the route doesn't accept the tenant parameter) * For example, in tenant context:
* route('tenant', [$bypassParameter => true]) => app.test/tenant. * Route::get('/', ...)->name('home');
* // query string identification
* Route::get('/tenant', ...)->middleware(InitializeTenancyByRequestData::class)->name('tenant.home');
* - route('home') => app.test/tenant?tenant=tenantKey
* - route('home', [$bypassParameter => true]) => app.test/
* - route('tenant.home', [$bypassParameter => true]) => app.test/tenant -- no query string added
*
* Note: UrlGeneratorBootstrapper::$addTenantParameterToDefaults is not affected by this, though
* it doesn't matter since it doesn't pass any extra parameters when not needed.
*
* @see UrlGeneratorBootstrapper
*/ */
public static string $bypassParameter = 'central'; public static string $bypassParameter = 'central';
/** /**
* Determine if the route names passed to `route()` or `temporarySignedRoute()` * Should route names passed to route() or temporarySignedRoute()
* should get prefixed with the tenant route name prefix. * get prefixed with the tenant route name prefix.
* *
* This is useful when using path identification with packages that generate URLs, * This is useful when using e.g. path identification with third-party packages
* like Jetstream, so that you don't have to manually prefix route names passed to each route() call. * where you don't have control over all route() calls or don't want to change
* too many files. Often this will be when using route cloning.
*/ */
public static bool $prefixRouteNames = false; public static bool $prefixRouteNames = false;
/** /**
* Determine if the tenant parameter should get passed * Should the tenant parameter be passed to route() or temporarySignedRoute() calls.
* to the links generated by `route()` or `temporarySignedRoute()` whenever available
* (enabled by default works with both path and query string identification).
* *
* With path identification, you can disable this and use URL::defaults() instead (as an alternative solution). * This is useful with path or query parameter identification. The former can be handled
* more elegantly using UrlGeneratorBootstrapper::$addTenantParameterToDefaults.
*
* @see UrlGeneratorBootstrapper
*/ */
public static bool $passTenantParameterToRoutes = true; public static bool $passTenantParameterToRoutes = false;
/**
* Route name overrides.
*
* Note: This behavior can be bypassed using $bypassParameter just like
* $prefixRouteNames and $passTenantParameterToRoutes.
*
* Example from a Jetstream integration:
* [
* 'profile.show' => 'tenant.profile.show',
* 'two-factor.login' => 'tenant.two-factor.login',
* ]
*
* In the tenant context:
* - `route('profile.show')` will return a URL as if you called `route('tenant.profile.show')`.
* - `route('profile.show', ['central' => true])` will return a URL as if you called `route('profile.show')`.
*/
public static array $overrides = [];
/**
* Use default parameter names ('tenant' name and tenant key value) instead of the parameter name
* and column name configured in the path resolver config.
*
* You want to enable this when using query string identification while having customized that config.
*/
public static bool $defaultParameterNames = false;
/** /**
* Override the route() method so that the route name gets prefixed * Override the route() method so that the route name gets prefixed
@ -99,7 +145,7 @@ class TenancyUrlGenerator extends UrlGenerator
protected function prepareRouteInputs(string $name, array $parameters): array protected function prepareRouteInputs(string $name, array $parameters): array
{ {
if (! $this->routeBehaviorModificationBypassed($parameters)) { if (! $this->routeBehaviorModificationBypassed($parameters)) {
$name = $this->prefixRouteName($name); $name = $this->routeNameOverride($name) ?? $this->prefixRouteName($name);
$parameters = $this->addTenantParameter($parameters); $parameters = $this->addTenantParameter($parameters);
} }
@ -124,10 +170,23 @@ class TenancyUrlGenerator extends UrlGenerator
} }
/** /**
* If `tenant()` isn't null, add tenant paramter to the passed parameters. * If `tenant()` isn't null, add the tenant parameter to the passed parameters.
*/ */
protected function addTenantParameter(array $parameters): array protected function addTenantParameter(array $parameters): array
{ {
return tenant() && static::$passTenantParameterToRoutes ? array_merge($parameters, [PathTenantResolver::tenantParameterName() => tenant()->getTenantKey()]) : $parameters; if (tenant() && static::$passTenantParameterToRoutes) {
if (static::$defaultParameterNames) {
return array_merge($parameters, ['tenant' => tenant()->getTenantKey()]);
} else {
return array_merge($parameters, [PathTenantResolver::tenantParameterName() => PathTenantResolver::tenantParameterValue(tenant())]);
}
} else {
return $parameters;
}
}
protected function routeNameOverride(string $name): string|null
{
return static::$overrides[$name] ?? null;
} }
} }

View file

@ -75,7 +75,10 @@ class TableRLSManager implements RLSPolicyManager
$builder = $this->database->getSchemaBuilder(); $builder = $this->database->getSchemaBuilder();
// We loop through each table in the database // We loop through each table in the database
foreach ($builder->getTableListing() as $table) { foreach ($builder->getTableListing(schema: $this->database->getConfig('search_path')) as $table) {
// E.g. "public.table_name" -> "table_name"
$table = str($table)->afterLast('.')->toString();
// For each table, we get a list of all foreign key columns // For each table, we get a list of all foreign key columns
$foreignKeys = collect($builder->getForeignKeys($table))->map(function ($foreign) use ($table) { $foreignKeys = collect($builder->getForeignKeys($table))->map(function ($foreign) use ($table) {
return $this->formatForeignKey($foreign, $table); return $this->formatForeignKey($foreign, $table);
@ -105,6 +108,12 @@ class TableRLSManager implements RLSPolicyManager
protected function generatePaths(string $table, array $foreign, array &$paths, array $currentPath = []): void protected function generatePaths(string $table, array $foreign, array &$paths, array $currentPath = []): void
{ {
// If the foreign key has a comment of 'no-rls', we skip it
// Also skip the foreign key if implicit scoping is off and the foreign key has no comment
if ($foreign['comment'] === 'no-rls' || (! static::$scopeByDefault && $foreign['comment'] === null)) {
return;
}
if (in_array($foreign['foreignTable'], array_column($currentPath, 'foreignTable'))) { if (in_array($foreign['foreignTable'], array_column($currentPath, 'foreignTable'))) {
throw new RecursiveRelationshipException; throw new RecursiveRelationshipException;
} }
@ -112,15 +121,7 @@ class TableRLSManager implements RLSPolicyManager
$currentPath[] = $foreign; $currentPath[] = $foreign;
if ($foreign['foreignTable'] === tenancy()->model()->getTable()) { if ($foreign['foreignTable'] === tenancy()->model()->getTable()) {
$comments = array_column($currentPath, 'comment');
$pathCanUseRls = static::$scopeByDefault ?
! in_array('no-rls', $comments) :
! in_array('no-rls', $comments) && ! in_array(null, $comments);
if ($pathCanUseRls) {
// If the foreign table is the tenants table, add the current path to $paths
$paths[] = $currentPath; $paths[] = $currentPath;
}
} else { } else {
// If not, recursively generate paths for the foreign table // If not, recursively generate paths for the foreign table
foreach ($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']) as $nextConstraint) { foreach ($this->database->getSchemaBuilder()->getForeignKeys($foreign['foreignTable']) as $nextConstraint) {

View file

@ -73,7 +73,7 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
public static function tenantRouteNamePrefix(): string public static function tenantRouteNamePrefix(): string
{ {
return config('tenancy.identification.resolvers.' . static::class . '.tenant_route_name_prefix') ?? static::tenantParameterName() . '.'; return config('tenancy.identification.resolvers.' . static::class . '.tenant_route_name_prefix') ?? 'tenant.';
} }
public static function tenantModelColumn(): string public static function tenantModelColumn(): string
@ -81,6 +81,11 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
return config('tenancy.identification.resolvers.' . static::class . '.tenant_model_column') ?? tenancy()->model()->getTenantKeyName(); return config('tenancy.identification.resolvers.' . static::class . '.tenant_model_column') ?? tenancy()->model()->getTenantKeyName();
} }
public static function tenantParameterValue(Tenant $tenant): string
{
return $tenant->getAttribute(static::tenantModelColumn());
}
/** @return string[] */ /** @return string[] */
public static function allowedExtraModelColumns(): array public static function allowedExtraModelColumns(): array
{ {

View file

@ -9,8 +9,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
// todo@move move all resource syncing-related things to a separate namespace?
/** /**
* @property-read TenantWithDatabase[]|Collection<int, TenantWithDatabase&Model> $tenants * @property-read TenantWithDatabase[]|Collection<int, TenantWithDatabase&Model> $tenants
*/ */

View file

@ -77,10 +77,12 @@ class Tenancy
public function run(Tenant $tenant, Closure $callback): mixed public function run(Tenant $tenant, Closure $callback): mixed
{ {
$originalTenant = $this->tenant; $originalTenant = $this->tenant;
$result = null;
try {
$this->initialize($tenant); $this->initialize($tenant);
$result = $callback($tenant); $result = $callback($tenant);
} finally {
if ($result instanceof PendingDispatch) { // #1277 if ($result instanceof PendingDispatch) { // #1277
$result = null; $result = null;
} }
@ -90,6 +92,7 @@ class Tenancy
} else { } else {
$this->end(); $this->end();
} }
}
return $result; return $result;
} }
@ -204,8 +207,10 @@ class Tenancy
// Wrap string in array // Wrap string in array
$tenants = is_string($tenants) ? [$tenants] : $tenants; $tenants = is_string($tenants) ? [$tenants] : $tenants;
// Use all tenants if $tenants is falsy // If $tenants is falsy by this point (e.g. an empty array) there's no work to be done
$tenants = $tenants ?: $this->model()->cursor(); // todo@phpstan phpstan thinks this isn't needed, but tests fail without it if (! $tenants) {
return;
}
$originalTenant = $this->tenant; $originalTenant = $this->tenant;

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy; namespace Stancl\Tenancy;
use Closure;
use Illuminate\Cache\CacheManager; use Illuminate\Cache\CacheManager;
use Illuminate\Database\Console\Migrations\FreshCommand; use Illuminate\Database\Console\Migrations\FreshCommand;
use Illuminate\Routing\Events\RouteMatched; use Illuminate\Routing\Events\RouteMatched;
@ -18,9 +19,15 @@ use Stancl\Tenancy\Resolvers\DomainTenantResolver;
class TenancyServiceProvider extends ServiceProvider class TenancyServiceProvider extends ServiceProvider
{ {
public static Closure|null $configure = null;
/* Register services. */ /* Register services. */
public function register(): void public function register(): void
{ {
if (static::$configure) {
(static::$configure)();
}
$this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy'); $this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'tenancy');
$this->app->singleton(Database\DatabaseManager::class); $this->app->singleton(Database\DatabaseManager::class);

View file

@ -18,7 +18,7 @@ class RandomHexGenerator implements UniqueIdentifierGenerator
{ {
public static int $bytes = 6; public static int $bytes = 6;
public static function generate(Model $model): string public static function generate(Model $model): string|int
{ {
return bin2hex(random_bytes(static::$bytes)); return bin2hex(random_bytes(static::$bytes));
} }

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\UniqueIdentifierGenerators;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
/**
* Generates a cryptographically secure random integer for the tenant key.
*/
class RandomIntGenerator implements UniqueIdentifierGenerator
{
public static int $min = 0;
public static int $max = PHP_INT_MAX;
public static function generate(Model $model): string|int
{
return random_int(static::$min, static::$max);
}
}

View file

@ -18,7 +18,7 @@ class RandomStringGenerator implements UniqueIdentifierGenerator
{ {
public static int $length = 8; public static int $length = 8;
public static function generate(Model $model): string public static function generate(Model $model): string|int
{ {
return Str::random(static::$length); return Str::random(static::$length);
} }

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\UniqueIdentifierGenerators;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
/**
* Generates a UUID for the tenant key.
*/
class ULIDGenerator implements UniqueIdentifierGenerator
{
public static function generate(Model $model): string|int
{
return Str::ulid()->toString();
}
}

View file

@ -13,7 +13,7 @@ use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator;
*/ */
class UUIDGenerator implements UniqueIdentifierGenerator class UUIDGenerator implements UniqueIdentifierGenerator
{ {
public static function generate(Model $model): string public static function generate(Model $model): string|int
{ {
return Uuid::uuid4()->toString(); return Uuid::uuid4()->toString();
} }

View file

@ -11,6 +11,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Actions\CreateStorageSymlinksAction; use Stancl\Tenancy\Actions\CreateStorageSymlinksAction;
use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction; use Stancl\Tenancy\Actions\RemoveStorageSymlinksAction;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Illuminate\Support\Facades\File;
beforeEach(function () { beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
@ -35,11 +36,15 @@ test('create storage symlinks action works', function() {
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
$this->assertDirectoryDoesNotExist($publicPath = public_path("public-$tenantKey")); // The symlink doesn't exist
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeFalse();
expect(file_exists($publicPath))->toBeFalse();
(new CreateStorageSymlinksAction)($tenant); (new CreateStorageSymlinksAction)($tenant);
$this->assertDirectoryExists($publicPath); // The symlink exists and is valid
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeTrue();
expect(file_exists($publicPath))->toBeTrue();
$this->assertEquals(storage_path("app/public/"), readlink($publicPath)); $this->assertEquals(storage_path("app/public/"), readlink($publicPath));
}); });
@ -61,9 +66,48 @@ test('remove storage symlinks action works', function() {
(new CreateStorageSymlinksAction)($tenant); (new CreateStorageSymlinksAction)($tenant);
$this->assertDirectoryExists($publicPath = public_path("public-$tenantKey")); // The symlink exists and is valid
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeTrue();
expect(file_exists($publicPath))->toBeTrue();
(new RemoveStorageSymlinksAction)($tenant); (new RemoveStorageSymlinksAction)($tenant);
$this->assertDirectoryDoesNotExist($publicPath); // The symlink doesn't exist
expect(is_link($publicPath))->toBeFalse();
expect(file_exists($publicPath))->toBeFalse();
});
test('removing tenant symlinks works even if the symlinks are invalid', function() {
config([
'tenancy.bootstrappers' => [
FilesystemTenancyBootstrapper::class,
],
'tenancy.filesystem.suffix_base' => 'tenant-',
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
]);
/** @var Tenant $tenant */
$tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
tenancy()->initialize($tenant);
(new CreateStorageSymlinksAction)($tenant);
// The symlink exists and is valid
expect(is_link($publicPath = public_path("public-$tenantKey")))->toBeTrue();
expect(file_exists($publicPath))->toBeTrue();
// Make the symlink invalid by deleting the tenant storage directory
$storagePath = storage_path();
File::deleteDirectory($storagePath);
// The symlink still exists, but isn't valid
expect(is_link($publicPath))->toBeTrue();
expect(file_exists($publicPath))->toBeFalse();
(new RemoveStorageSymlinksAction)($tenant);
expect(is_link($publicPath))->toBeFalse();
}); });

View file

@ -9,6 +9,8 @@ use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
use function Stancl\Tenancy\Tests\withTenantDatabases;
beforeEach(function () { beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);

View file

@ -23,6 +23,7 @@ use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
$this->mockConsoleOutput = false; $this->mockConsoleOutput = false;

View file

@ -15,6 +15,7 @@ use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);

View file

@ -9,6 +9,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper; use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
config(['tenancy.bootstrappers' => [CacheTagsBootstrapper::class]]); config(['tenancy.bootstrappers' => [CacheTagsBootstrapper::class]]);

View file

@ -13,6 +13,7 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Listeners; use Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
/** /**
* This collection of regression tests verifies that SessionTenancyBootstrapper * This collection of regression tests verifies that SessionTenancyBootstrapper

View file

@ -6,6 +6,9 @@ use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Events\TenancyInitialized; use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);

View file

@ -16,6 +16,7 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\DeleteTenantStorage; use Stancl\Tenancy\Listeners\DeleteTenantStorage;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);

View file

@ -13,6 +13,14 @@ use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteBootstrapper;
beforeEach(function () { beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class);
FortifyRouteBootstrapper::$passTenantParameter = true;
});
afterEach(function () {
FortifyRouteBootstrapper::$passTenantParameter = true;
FortifyRouteBootstrapper::$fortifyRedirectMap = [];
FortifyRouteBootstrapper::$fortifyHome = 'tenant.dashboard';
FortifyRouteBootstrapper::$defaultParameterNames = false;
}); });
test('fortify route tenancy bootstrapper updates fortify config correctly', function() { test('fortify route tenancy bootstrapper updates fortify config correctly', function() {
@ -25,53 +33,31 @@ test('fortify route tenancy bootstrapper updates fortify config correctly', func
return true; return true;
})->name($homeRouteName = 'home'); })->name($homeRouteName = 'home');
Route::get('/{tenant}/home', function () {
return true;
})->name($pathIdHomeRouteName = 'tenant.home');
Route::get('/welcome', function () { Route::get('/welcome', function () {
return true; return true;
})->name($welcomeRouteName = 'welcome'); })->name($welcomeRouteName = 'welcome');
Route::get('/{tenant}/welcome', function () {
return true;
})->name($pathIdWelcomeRouteName = 'path.welcome');
FortifyRouteBootstrapper::$fortifyHome = $homeRouteName; FortifyRouteBootstrapper::$fortifyHome = $homeRouteName;
FortifyRouteBootstrapper::$fortifyRedirectMap['login'] = $welcomeRouteName;
// Make login redirect to the central welcome route expect(config('fortify.home'))->toBe($originalFortifyHome);
FortifyRouteBootstrapper::$fortifyRedirectMap['login'] = [ expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
'route_name' => $welcomeRouteName,
'context' => Context::CENTRAL,
];
FortifyRouteBootstrapper::$passTenantParameter = true;
tenancy()->initialize($tenant = Tenant::create()); tenancy()->initialize($tenant = Tenant::create());
// The bootstraper makes fortify.home always receive the tenant parameter
expect(config('fortify.home'))->toBe('http://localhost/home?tenant=' . $tenant->getTenantKey()); expect(config('fortify.home'))->toBe('http://localhost/home?tenant=' . $tenant->getTenantKey());
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]);
// The login redirect route has the central context specified, so it doesn't receive the tenant parameter
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome']);
tenancy()->end(); tenancy()->end();
expect(config('fortify.home'))->toBe($originalFortifyHome); expect(config('fortify.home'))->toBe($originalFortifyHome);
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects); expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
// Making a route's context will pass the tenant parameter to the route FortifyRouteBootstrapper::$passTenantParameter = false;
FortifyRouteBootstrapper::$fortifyRedirectMap['login']['context'] = Context::TENANT;
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
expect(config('fortify.home'))->toBe('http://localhost/home');
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]); expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome']);
// Make the home and login route accept the tenant as a route parameter
// To confirm that tenant route parameter gets filled automatically too (path identification works as well as query string)
FortifyRouteBootstrapper::$fortifyHome = $pathIdHomeRouteName;
FortifyRouteBootstrapper::$fortifyRedirectMap['login']['route_name'] = $pathIdWelcomeRouteName;
tenancy()->end(); tenancy()->end();
expect(config('fortify.home'))->toBe($originalFortifyHome);
tenancy()->initialize($tenant); expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
expect(config('fortify.home'))->toBe("http://localhost/{$tenant->getTenantKey()}/home");
expect(config('fortify.redirects'))->toEqual(['login' => "http://localhost/{$tenant->getTenantKey()}/welcome"]);
}); });

View file

@ -10,18 +10,23 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper; use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
beforeEach(function () { beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class);
RootUrlBootstrapper::$rootUrlOverride = null; RootUrlBootstrapper::$rootUrlOverride = null;
RootUrlBootstrapper::$rootUrlOverrideInTests = true;
}); });
afterEach(function () { afterEach(function () {
RootUrlBootstrapper::$rootUrlOverride = null; RootUrlBootstrapper::$rootUrlOverride = null;
RootUrlBootstrapper::$rootUrlOverrideInTests = false;
}); });
test('root url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one after tenancy ends', function() { test('root url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one when ending tenancy', function() {
config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]); config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]);
Route::group([ Route::group([

View file

@ -19,12 +19,14 @@ beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenancyEnded::class, RevertToCentralContext::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class);
TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$prefixRouteNames = false;
TenancyUrlGenerator::$passTenantParameterToRoutes = true; TenancyUrlGenerator::$passTenantParameterToRoutes = false;
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false;
}); });
afterEach(function () { afterEach(function () {
TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$prefixRouteNames = false;
TenancyUrlGenerator::$passTenantParameterToRoutes = true; TenancyUrlGenerator::$passTenantParameterToRoutes = false;
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = false;
}); });
test('url generator bootstrapper swaps the url generator instance correctly', function() { test('url generator bootstrapper swaps the url generator instance correctly', function() {
@ -41,36 +43,28 @@ test('url generator bootstrapper swaps the url generator instance correctly', fu
->not()->toBeInstanceOf(TenancyUrlGenerator::class); ->not()->toBeInstanceOf(TenancyUrlGenerator::class);
}); });
test('url generator bootstrapper can prefix route names passed to the route helper', function() { test('tenancy url generator can prefix route names passed to the route helper', function() {
Route::get('/central/home', fn () => route('home'))->name('home'); Route::get('/central/home', fn () => route('home'))->name('home');
// Tenant route name prefix is 'tenant.' by default // Tenant route name prefix is 'tenant.' by default
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]); Route::get('/tenant/home', fn () => route('tenant.home'))->name('tenant.home');
$tenant = Tenant::create(); $tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
$centralRouteUrl = route('home'); $centralRouteUrl = route('home');
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenantKey]); $tenantRouteUrl = route('tenant.home');
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]); config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
// Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false // Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false (default)
expect(route('home'))->not()->toBe($centralRouteUrl); expect(route('home'))->toBe($centralRouteUrl);
// When TenancyUrlGenerator::$passTenantParameterToRoutes is true (default)
// The route helper receives the tenant parameter
// So in order to generate central URL, we have to pass the bypass parameter
expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl);
// When $prefixRouteNames is true, the route name passed to the route() helper ('home') gets prefixed with 'tenant.' automatically.
TenancyUrlGenerator::$prefixRouteNames = true; TenancyUrlGenerator::$prefixRouteNames = true;
// The $prefixRouteNames property is true
// The route name passed to the route() helper ('home') gets prefixed prefixed with 'tenant.' automatically
expect(route('home'))->toBe($tenantRouteUrl); expect(route('home'))->toBe($tenantRouteUrl);
// The 'tenant.home' route name doesn't get prefixed because it is already prefixed with 'tenant.' // The 'tenant.home' route name doesn't get prefixed -- it is already prefixed with 'tenant.'
// Also, the route receives the tenant parameter automatically
expect(route('tenant.home'))->toBe($tenantRouteUrl); expect(route('tenant.home'))->toBe($tenantRouteUrl);
// Ending tenancy reverts route() behavior changes // Ending tenancy reverts route() behavior changes
@ -79,6 +73,76 @@ test('url generator bootstrapper can prefix route names passed to the route help
expect(route('home'))->toBe($centralRouteUrl); expect(route('home'))->toBe($centralRouteUrl);
}); });
test('the route helper can receive the tenant parameter automatically', function (
string $identification,
bool $addTenantParameterToDefaults,
bool $passTenantParameterToRoutes,
) {
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
$appUrl = config('app.url');
UrlGeneratorBootstrapper::$addTenantParameterToDefaults = $addTenantParameterToDefaults;
// When the tenant parameter isn't added to defaults, the tenant parameter has to be passed "manually"
// by setting $passTenantParameterToRoutes to true. This is only preferable with query string identification.
// With path identification, this ultimately doesn't have any effect
// if UrlGeneratorBootstrapper::$addTenantParameterToDefaults is true,
// but TenancyUrlGenerator::$passTenantParameterToRoutes can still be used instead.
TenancyUrlGenerator::$passTenantParameterToRoutes = $passTenantParameterToRoutes;
$tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
Route::get('/central/home', fn () => route('home'))->name('home');
$tenantRoute = $identification === InitializeTenancyByPath::class ? "/{tenant}/home" : "/tenant/home";
Route::get($tenantRoute, fn () => route('tenant.home'))
->name('tenant.home')
->middleware(['tenant', $identification]);
tenancy()->initialize($tenant);
$expectedUrl = match (true) {
$identification === InitializeTenancyByRequestData::class && $passTenantParameterToRoutes => "{$appUrl}/tenant/home?tenant={$tenantKey}",
$identification === InitializeTenancyByRequestData::class => "{$appUrl}/tenant/home", // $passTenantParameterToRoutes is false
$identification === InitializeTenancyByPath::class && ($addTenantParameterToDefaults || $passTenantParameterToRoutes) => "{$appUrl}/{$tenantKey}/home",
$identification === InitializeTenancyByPath::class => null, // Should throw an exception -- route() doesn't receive the tenant parameter in this case
};
if ($expectedUrl === null) {
expect(fn () => route('tenant.home'))->toThrow(UrlGenerationException::class, 'Missing parameter: tenant');
} else {
expect(route('tenant.home'))->toBe($expectedUrl);
}
})->with([InitializeTenancyByPath::class, InitializeTenancyByRequestData::class])
->with([true, false]) // UrlGeneratorBootstrapper::$addTenantParameterToDefaults
->with([true, false]); // TenancyUrlGenerator::$passTenantParameterToRoutes
test('url generator can override specific route names', function() {
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
Route::get('/foo', fn () => 'foo')->name('foo');
Route::get('/bar', fn () => 'bar')->name('bar');
Route::get('/baz', fn () => 'baz')->name('baz'); // Not overridden
TenancyUrlGenerator::$overrides = ['foo' => 'bar'];
expect(route('foo'))->toBe(url('/foo'));
expect(route('bar'))->toBe(url('/bar'));
expect(route('baz'))->toBe(url('/baz'));
tenancy()->initialize(Tenant::create());
expect(route('foo'))->toBe(url('/bar'));
expect(route('bar'))->toBe(url('/bar')); // not overridden
expect(route('baz'))->toBe(url('/baz')); // not overridden
// Bypass the override
expect(route('foo', ['central' => true]))->toBe(url('/foo'));
});
test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () { test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () {
$tenantParameterName = PathTenantResolver::tenantParameterName(); $tenantParameterName = PathTenantResolver::tenantParameterName();
@ -105,54 +169,8 @@ test('both the name prefixing and the tenant parameter logic gets skipped when b
->not()->toContain('bypassParameter'); ->not()->toContain('bypassParameter');
// When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home') // When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home')
expect(route('home', ['bypassParameter' => false]))->toBe($tenantRouteUrl) // The tenant parameter is not passed automatically since both
// UrlGeneratorBootstrapper::$addTenantParameterToDefaults and TenancyUrlGenerator::$passTenantParameterToRoutes are false by default
expect(route('home', ['bypassParameter' => false, 'tenant' => $tenant->getTenantKey()]))->toBe($tenantRouteUrl)
->not()->toContain('bypassParameter'); ->not()->toContain('bypassParameter');
}); });
test('url generator bootstrapper can make route helper generate links with the tenant parameter', function() {
Route::get('/query_string', fn () => route('query_string'))->name('query_string')->middleware(['universal', InitializeTenancyByRequestData::class]);
Route::get('/path', fn () => route('path'))->name('path');
Route::get('/{tenant}/path', fn () => route('tenant.path'))->name('tenant.path')->middleware([InitializeTenancyByPath::class]);
$tenant = Tenant::create();
$tenantKey = $tenant->getTenantKey();
$queryStringCentralUrl = route('query_string');
$queryStringTenantUrl = route('query_string', ['tenant' => $tenantKey]);
$pathCentralUrl = route('path');
$pathTenantUrl = route('tenant.path', ['tenant' => $tenantKey]);
// Makes the route helper receive the tenant parameter whenever available
// Unless the bypass parameter is true
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
expect(route('path'))->toBe($pathCentralUrl);
// Tenant parameter required, but not passed since tenancy wasn't initialized
expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class);
tenancy()->initialize($tenant);
// Tenant parameter is passed automatically
expect(route('path'))->not()->toBe($pathCentralUrl); // Parameter added as query string bypassParameter needed
expect(route('path', ['bypassParameter' => true]))->toBe($pathCentralUrl);
expect(route('tenant.path'))->toBe($pathTenantUrl);
expect(route('query_string'))->toBe($queryStringTenantUrl)->toContain('tenant=');
expect(route('query_string', ['bypassParameter' => 'true']))->toBe($queryStringCentralUrl)->not()->toContain('tenant=');
tenancy()->end();
expect(route('query_string'))->toBe($queryStringCentralUrl);
// Tenant parameter required, but shouldn't be passed since tenancy isn't initialized
expect(fn () => route('tenant.path'))->toThrow(UrlGenerationException::class);
// Route-level identification
pest()->get("http://localhost/query_string")->assertSee($queryStringCentralUrl);
pest()->get("http://localhost/query_string?tenant=$tenantKey")->assertSee($queryStringTenantUrl);
pest()->get("http://localhost/path")->assertSee($pathCentralUrl);
pest()->get("http://localhost/$tenantKey/path")->assertSee($pathTenantUrl);
});

View file

@ -19,6 +19,7 @@ use Illuminate\Broadcasting\Broadcasters\NullBroadcaster;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract; use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
use function Stancl\Tenancy\Tests\withTenantDatabases;
beforeEach(function () { beforeEach(function () {
withTenantDatabases(); withTenantDatabases();

View file

@ -11,9 +11,11 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use Illuminate\Support\Facades\Route as RouteFacade; use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\PathIdentificationManager;
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver; use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
use function Stancl\Tenancy\Tests\pest;
test('tenants can be resolved using cached resolvers', function (string $resolver) { test('tenants can be resolved using cached resolvers', function (string $resolver) {
$tenant = Tenant::create(['id' => $tenantKey = 'acme']); $tenant = Tenant::create(['id' => $tenantKey = 'acme']);
@ -84,6 +86,34 @@ test('cache is invalidated when the tenant is updated', function (string $resolv
RequestDataTenantResolver::class, RequestDataTenantResolver::class,
]); ]);
test('cache is invalidated when the tenant is deleted', function (string $resolver) {
DB::statement('SET FOREIGN_KEY_CHECKS=0;'); // allow deleting the tenant
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
$tenant->createDomain($tenantKey);
DB::enableQueryLog();
config(['tenancy.identification.resolvers.' . $resolver . '.cache' => true]);
expect($tenant->is(app($resolver)->resolve(getResolverArgument($resolver, $tenantKey))))->toBeTrue();
expect(DB::getQueryLog())->not()->toBeEmpty();
DB::flushQueryLog();
expect($tenant->is(app($resolver)->resolve(getResolverArgument($resolver, $tenantKey))))->toBeTrue();
expect(DB::getQueryLog())->toBeEmpty();
$tenant->delete();
DB::flushQueryLog();
expect(fn () => app($resolver)->resolve(getResolverArgument($resolver, $tenantKey)))->toThrow(TenantCouldNotBeIdentifiedException::class);
expect(DB::getQueryLog())->not()->toBeEmpty(); // Cache was invalidated, so the DB was queried
})->with([
DomainTenantResolver::class,
PathTenantResolver::class,
RequestDataTenantResolver::class,
]);
test('cache is invalidated when a tenants domain is changed', function () { test('cache is invalidated when a tenants domain is changed', function () {
$tenant = Tenant::create(['id' => $tenantKey = 'acme']); $tenant = Tenant::create(['id' => $tenantKey = 'acme']);
$tenant->createDomain($tenantKey); $tenant->createDomain($tenantKey);
@ -110,6 +140,26 @@ test('cache is invalidated when a tenants domain is changed', function () {
pest()->assertNotEmpty(DB::getQueryLog()); // not empty pest()->assertNotEmpty(DB::getQueryLog()); // not empty
}); });
test('cache is invalidated when a tenants domain is deleted', function () {
$tenant = Tenant::create(['id' => $tenantKey = 'acme']);
$tenant->createDomain($tenantKey);
DB::enableQueryLog();
config(['tenancy.identification.resolvers.' . DomainTenantResolver::class . '.cache' => 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
$tenant->domains->first()->delete();
DB::flushQueryLog();
expect(fn () => $tenant->is(app(DomainTenantResolver::class)->resolve('acme')))->toThrow(TenantCouldNotBeIdentifiedOnDomainException::class);
expect(DB::getQueryLog())->not()->toBeEmpty(); // Cache was invalidated, so the DB was queried
});
test('PathTenantResolver forgets the tenant route parameter when the tenant is resolved from cache', function() { test('PathTenantResolver forgets the tenant route parameter when the tenant is resolved from cache', function() {
config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.cache' => true]); config(['tenancy.identification.resolvers.' . PathTenantResolver::class . '.cache' => true]);
DB::enableQueryLog(); DB::enableQueryLog();

View file

@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Route as RouteFacade;
use Stancl\Tenancy\Tests\Etc\HasMiddlewareController; use Stancl\Tenancy\Tests\Etc\HasMiddlewareController;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use function Stancl\Tenancy\Tests\pest;
test('a route can be universal using path identification', function (array $routeMiddleware, array $globalMiddleware) { test('a route can be universal using path identification', function (array $routeMiddleware, array $globalMiddleware) {
foreach ($globalMiddleware as $middleware) { foreach ($globalMiddleware as $middleware) {

View file

@ -3,10 +3,9 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
use Stancl\Tenancy\Database\Models;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
Route::group([ Route::group([

View file

@ -26,6 +26,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException; use Stancl\Tenancy\Database\Exceptions\TenantDatabaseDoesNotExistException;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) { if (file_exists($schemaPath = 'tests/Etc/tenant-schema-test.dump')) {

View file

@ -24,6 +24,7 @@ use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLData
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager; use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager; use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager; use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
config([ config([

View file

@ -10,6 +10,7 @@ use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
InitializeTenancyByDomain::$onFail = null; InitializeTenancyByDomain::$onFail = null;

View file

@ -24,6 +24,7 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByOriginHeader;
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware; use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware;
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithRouteMiddleware; use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithRouteMiddleware;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
config()->set([ config()->set([

View file

@ -19,6 +19,7 @@ use Stancl\Tenancy\Events\BootstrappingTenancy;
use Stancl\Tenancy\Listeners\QueueableListener; use Stancl\Tenancy\Listeners\QueueableListener;
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
FooListener::$shouldQueue = false; FooListener::$shouldQueue = false;

View file

@ -18,14 +18,9 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
test('sqlite ATTACH statements can be blocked', function (bool $disallow) { test('sqlite ATTACH statements can be blocked', function (bool $disallow) {
try {
readlink(base_path('vendor'));
} catch (\Throwable) {
symlink(base_path('vendor'), '/var/www/html/vendor');
}
if (php_uname('m') == 'aarch64') { if (php_uname('m') == 'aarch64') {
// Escape testbench prison. Can't hardcode /var/www/html/extensions/... here // Escape testbench prison. Can't hardcode /var/www/html/extensions/... here
// since GHA doesn't mount the filesystem on the container's workdir // since GHA doesn't mount the filesystem on the container's workdir

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Features\CrossDomainRedirect; use Stancl\Tenancy\Features\CrossDomainRedirect;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
test('tenant redirect macro replaces only the hostname', function () { test('tenant redirect macro replaces only the hostname', function () {
config([ config([

View file

@ -9,6 +9,7 @@ use Stancl\Tenancy\Features\TenantConfig;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
afterEach(function () { afterEach(function () {
TenantConfig::$storageToConfigMap = []; TenantConfig::$storageToConfigMap = [];

View file

@ -10,6 +10,8 @@ use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy; use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\MailConfigBootstrapper; use Stancl\Tenancy\Bootstrappers\MailConfigBootstrapper;
use function Stancl\Tenancy\Tests\pest;
use function Stancl\Tenancy\Tests\withTenantDatabases;
beforeEach(function() { beforeEach(function() {
config(['mail.default' => 'smtp']); config(['mail.default' => 'smtp']);

View file

@ -8,6 +8,7 @@ use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use function Stancl\Tenancy\Tests\pest;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
beforeEach(function () { beforeEach(function () {

View file

@ -11,7 +11,8 @@ use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Listeners\CreateTenantConnection; use Stancl\Tenancy\Listeners\CreateTenantConnection;
use Stancl\Tenancy\Listeners\UseCentralConnection; use Stancl\Tenancy\Listeners\UseCentralConnection;
use Stancl\Tenancy\Listeners\UseTenantConnection; use Stancl\Tenancy\Listeners\UseTenantConnection;
use \Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
test('manual tenancy initialization works', function () { test('manual tenancy initialization works', function () {
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\InitializeTenancyByOriginHeader; use Stancl\Tenancy\Middleware\InitializeTenancyByOriginHeader;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
InitializeTenancyByOriginHeader::$onFail = null; InitializeTenancyByOriginHeader::$onFail = null;

View file

@ -12,6 +12,7 @@ use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByPathException;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Resolvers\PathTenantResolver; use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
// Make sure the tenant parameter is set to 'tenant' // Make sure the tenant parameter is set to 'tenant'

View file

@ -11,6 +11,7 @@ use Stancl\Tenancy\Events\PendingTenantCreated;
use Stancl\Tenancy\Events\PendingTenantPulled; use Stancl\Tenancy\Events\PendingTenantPulled;
use Stancl\Tenancy\Events\PullingPendingTenant; use Stancl\Tenancy\Events\PullingPendingTenant;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
test('tenants are correctly identified as pending', function (){ test('tenants are correctly identified as pending', function (){
Tenant::createPending(); Tenant::createPending();

View file

@ -1,5 +1,7 @@
<?php <?php
namespace Stancl\Tenancy\Tests;
use Stancl\Tenancy\Tests\TestCase; use Stancl\Tenancy\Tests\TestCase;
use Stancl\JobPipeline\JobPipeline; use Stancl\JobPipeline\JobPipeline;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
@ -8,14 +10,14 @@ use Stancl\Tenancy\Events\TenantCreated;
uses(TestCase::class)->in(__DIR__); uses(TestCase::class)->in(__DIR__);
function pest(): TestCase
{
return Pest\TestSuite::getInstance()->test;
}
function withTenantDatabases() function withTenantDatabases()
{ {
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant; return $event->tenant;
})->toListener()); })->toListener());
} }
function pest(): TestCase
{
return \Pest\TestSuite::getInstance()->test;
}

View file

@ -11,6 +11,7 @@ use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains; use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware; use Stancl\Tenancy\Tests\Etc\EarlyIdentification\ControllerWithMiddleware;
use function Stancl\Tenancy\Tests\pest;
test('correct routes are accessible in route-level identification', function (RouteMode $defaultRouteMode) { test('correct routes are accessible in route-level identification', function (RouteMode $defaultRouteMode) {
config()->set([ config()->set([

View file

@ -22,16 +22,14 @@ use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\PersistentQueueTenancyBootstrapper;
use Stancl\Tenancy\Listeners\QueueableListener; use Stancl\Tenancy\Listeners\QueueableListener;
use function Stancl\Tenancy\Tests\pest;
use function Stancl\Tenancy\Tests\withTenantDatabases;
beforeEach(function () { beforeEach(function () {
$this->mockConsoleOutput = false;
config([ config([
'tenancy.bootstrappers' => [ 'tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class],
QueueTenancyBootstrapper::class,
DatabaseTenancyBootstrapper::class,
],
'queue.default' => 'redis', 'queue.default' => 'redis',
]); ]);
@ -45,7 +43,22 @@ afterEach(function () {
pest()->valuestore->flush(); pest()->valuestore->flush();
}); });
test('tenant id is passed to tenant queues', function () { dataset('queue_bootstrappers', [
QueueTenancyBootstrapper::class,
PersistentQueueTenancyBootstrapper::class,
]);
function withQueueBootstrapper(string $class) {
config(['tenancy.bootstrappers' => [
DatabaseTenancyBootstrapper::class,
$class,
]]);
$class::__constructStatic(app());
}
test('tenant id is passed to tenant queues', function (string $bootstrapper) {
withQueueBootstrapper($bootstrapper);
withTenantDatabases(); withTenantDatabases();
config(['queue.default' => 'sync']); config(['queue.default' => 'sync']);
@ -61,9 +74,10 @@ test('tenant id is passed to tenant queues', function () {
Event::assertDispatched(JobProcessing::class, function ($event) { Event::assertDispatched(JobProcessing::class, function ($event) {
return $event->job->payload()['tenant_id'] === tenant('id'); return $event->job->payload()['tenant_id'] === tenant('id');
}); });
}); })->with('queue_bootstrappers');
test('tenant id is not passed to central queues', function () { test('tenant id is not passed to central queues', function (string $bootstrapper) {
withQueueBootstrapper($bootstrapper);
withTenantDatabases(); withTenantDatabases();
$tenant = Tenant::create(); $tenant = Tenant::create();
@ -82,9 +96,10 @@ test('tenant id is not passed to central queues', function () {
Event::assertDispatched(JobProcessing::class, function ($event) { Event::assertDispatched(JobProcessing::class, function ($event) {
return ! isset($event->job->payload()['tenant_id']); return ! isset($event->job->payload()['tenant_id']);
}); });
}); })->with('queue_bootstrappers');
test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) { test('tenancy is initialized inside queues', function (bool $shouldEndTenancy, string $bootstrapper) {
withQueueBootstrapper($bootstrapper);
withTenantDatabases(); withTenantDatabases();
withFailedJobs(); withFailedJobs();
@ -117,7 +132,7 @@ test('tenancy is initialized inside queues', function (bool $shouldEndTenancy) {
$tenant->run(function () use ($user) { $tenant->run(function () use ($user) {
expect($user->fresh()->name)->toBe('Bar'); expect($user->fresh()->name)->toBe('Bar');
}); });
})->with([true, false]); })->with([true, false])->with('queue_bootstrappers');
test('changing the shouldQueue static property in parent class affects child classes unless the property is redefined', function () { test('changing the shouldQueue static property in parent class affects child classes unless the property is redefined', function () {
// Parent $shouldQueue is true // Parent $shouldQueue is true
@ -142,7 +157,8 @@ test('changing the shouldQueue static property in parent class affects child cla
expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse(); expect(app(ShouldNotQueueListener::class)->shouldQueue(new stdClass()))->toBeFalse();
}); });
test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy) { test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenancy, string $bootstrapper) {
withQueueBootstrapper($bootstrapper);
withFailedJobs(); withFailedJobs();
withTenantDatabases(); withTenantDatabases();
@ -189,9 +205,10 @@ test('tenancy is initialized when retrying jobs', function (bool $shouldEndTenan
$tenant->run(function () use ($user) { $tenant->run(function () use ($user) {
expect($user->fresh()->name)->toBe('Bar'); expect($user->fresh()->name)->toBe('Bar');
}); });
})->with([true, false]); })->with([true, false])->with('queue_bootstrappers');
test('the tenant used by the job doesnt change when the current tenant changes', function () { test('the tenant used by the job doesnt change when the current tenant changes', function (string $bootstrapper) {
withQueueBootstrapper($bootstrapper);
withTenantDatabases(); withTenantDatabases();
$tenant1 = Tenant::create(); $tenant1 = Tenant::create();
@ -208,26 +225,11 @@ test('the tenant used by the job doesnt change when the current tenant changes',
pest()->artisan('queue:work --once'); pest()->artisan('queue:work --once');
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant1->getTenantKey()); expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant1->getTenantKey());
}); })->with('queue_bootstrappers');
test('tenant connections do not persist after tenant jobs get processed', function() {
withTenantDatabases();
$tenant = Tenant::create();
tenancy()->initialize($tenant);
dispatch(new TestJob(pest()->valuestore));
tenancy()->end();
pest()->artisan('queue:work --once');
expect(collect(DB::select('SHOW FULL PROCESSLIST'))->pluck('db'))->not()->toContain($tenant->database()->getName());
});
// Regression test for #1277 // Regression test for #1277
test('dispatching a job from a tenant run arrow function dispatches it immediately', function () { test('dispatching a job from a tenant run arrow function dispatches it immediately', function (string $bootstrapper) {
withQueueBootstrapper($bootstrapper);
withTenantDatabases(); withTenantDatabases();
$tenant = Tenant::create(); $tenant = Tenant::create();
@ -241,7 +243,7 @@ test('dispatching a job from a tenant run arrow function dispatches it immediate
expect(tenant())->toBe(null); expect(tenant())->toBe(null);
expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->getTenantKey()); expect(pest()->valuestore->get('tenant_id'))->toBe('The current tenant id is: ' . $tenant->getTenantKey());
}); })->with('queue_bootstrappers');
function createValueStore(): void function createValueStore(): void
{ {

View file

@ -17,6 +17,7 @@ use Stancl\Tenancy\Commands\CreateUserWithRLSPolicies;
use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager; use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager;
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager; use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper; use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
CreateUserWithRLSPolicies::$forceRls = true; CreateUserWithRLSPolicies::$forceRls = true;

View file

@ -19,6 +19,7 @@ use Stancl\Tenancy\Commands\CreateUserWithRLSPolicies;
use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager; use Stancl\Tenancy\RLS\PolicyManagers\TableRLSManager;
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper; use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException; use Stancl\Tenancy\Database\Exceptions\RecursiveRelationshipException;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
CreateUserWithRLSPolicies::$forceRls = true; CreateUserWithRLSPolicies::$forceRls = true;
@ -509,7 +510,7 @@ test('table rls manager generates relationship trees with tables related to the
// Add non-nullable comment_id foreign key // Add non-nullable comment_id foreign key
Schema::table('ratings', function (Blueprint $table) { Schema::table('ratings', function (Blueprint $table) {
$table->foreignId('comment_id')->onUpdate('cascade')->onDelete('cascade')->comment('rls')->constrained('comments'); $table->foreignId('comment_id')->comment('rls')->constrained('comments')->onUpdate('cascade')->onDelete('cascade');
}); });
// Non-nullable paths are preferred over nullable paths // Non-nullable paths are preferred over nullable paths
@ -744,16 +745,29 @@ test('table rls manager generates queries correctly', function() {
test('table manager throws an exception when encountering a recursive relationship', function() { test('table manager throws an exception when encountering a recursive relationship', function() {
Schema::create('recursive_posts', function (Blueprint $table) { Schema::create('recursive_posts', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('highlighted_comment_id')->constrained('comments')->nullable()->comment('rls'); $table->foreignId('highlighted_comment_id')->nullable()->comment('rls')->constrained('comments');
}); });
Schema::table('comments', function (Blueprint $table) { Schema::table('comments', function (Blueprint $table) {
$table->foreignId('recursive_post_id')->constrained('recursive_posts')->comment('rls'); $table->foreignId('recursive_post_id')->comment('rls')->constrained('recursive_posts');
}); });
expect(fn () => app(TableRLSManager::class)->generateTrees())->toThrow(RecursiveRelationshipException::class); expect(fn () => app(TableRLSManager::class)->generateTrees())->toThrow(RecursiveRelationshipException::class);
}); });
test('table manager ignores recursive relationship if the foreign key responsible for the recursion has no-rls comment', function() {
Schema::create('recursive_posts', function (Blueprint $table) {
$table->id();
$table->foreignId('highlighted_comment_id')->nullable()->comment('no-rls')->constrained('comments');
});
Schema::table('comments', function (Blueprint $table) {
$table->foreignId('recursive_post_id')->comment('rls')->constrained('recursive_posts');
});
expect(fn () => app(TableRLSManager::class)->generateTrees())->not()->toThrow(RecursiveRelationshipException::class);
});
function createPostgresUser(string $username, string $password = 'password'): array function createPostgresUser(string $username, string $password = 'password'): array
{ {
try { try {

View file

@ -25,6 +25,7 @@ use Stancl\Tenancy\Commands\CreateUserWithRLSPolicies;
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager; use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper; use Stancl\Tenancy\Bootstrappers\PostgresRLSBootstrapper;
use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel; use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
CreateUserWithRLSPolicies::$forceRls = true; CreateUserWithRLSPolicies::$forceRls = true;

View file

@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
config([ config([

View file

@ -43,6 +43,7 @@ use Stancl\Tenancy\ResourceSyncing\Events\CentralResourceDetachedFromTenant;
use Stancl\Tenancy\Tests\Etc\ResourceSyncing\CentralUser as BaseCentralUser; use Stancl\Tenancy\Tests\Etc\ResourceSyncing\CentralUser as BaseCentralUser;
use Stancl\Tenancy\ResourceSyncing\CentralResourceNotAvailableInPivotException; use Stancl\Tenancy\ResourceSyncing\CentralResourceNotAvailableInPivotException;
use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase; use Stancl\Tenancy\ResourceSyncing\Events\SyncedResourceSavedInForeignDatabase;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
config(['tenancy.bootstrappers' => [ config(['tenancy.bootstrappers' => [

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Support\Facades\Event;
use Stancl\Tenancy\Events\TenantCreated;
use Stancl\Tenancy\Jobs\CreateDatabase;
use Stancl\Tenancy\Jobs\MigrateDatabase;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Tests\Etc\User;
use Illuminate\Support\Str;
use Stancl\Tenancy\Events\TenancyInitialized;
use Stancl\Tenancy\Listeners\BootstrapTenancy;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
beforeEach(function () {
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
Event::listen(TenantCreated::class, JobPipeline::make([
CreateDatabase::class,
MigrateDatabase::class,
])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
config(['tenancy.bootstrappers' => [
DatabaseTenancyBootstrapper::class,
]]);
});
test('runForMultiple runs the passed closure for the right tenants', function() {
$tenants = [Tenant::create(), Tenant::create(), Tenant::create()];
$createUser = fn ($username) => function () use ($username) {
User::create(['name' => $username, 'email' => Str::random(8) . '@example.com', 'password' => bcrypt('password')]);
};
// tenancy()->runForMultiple([], ...) shouldn't do anything
// No users should be created -- the closure should not run at all
tenancy()->runForMultiple([], $createUser('none'));
// Try the same with an empty collection -- the result should be the same for any traversable
tenancy()->runForMultiple(collect(), $createUser('none'));
foreach ($tenants as $tenant) {
$tenant->run(function() {
expect(User::count())->toBe(0);
});
}
// tenancy()->runForMultiple(['foo', 'bar'], ...) should run the closure only for the passed tenants
tenancy()->runForMultiple([$tenants[0]->getTenantKey(), $tenants[1]->getTenantKey()], $createUser('user'));
// User should be created for tenants[0] and tenants[1], but not for tenants[2]
foreach ($tenants as $tenant) {
$tenant->run(function() use ($tenants) {
if (tenant()->getTenantKey() !== $tenants[2]->getTenantKey()) {
expect(User::first()->name)->toBe('user');
} else {
expect(User::count())->toBe(0);
}
});
}
// tenancy()->runForMultiple(null, ...) should run the closure for all tenants
tenancy()->runForMultiple(null, $createUser('new_user'));
foreach ($tenants as $tenant) {
$tenant->run(function() {
expect(User::all()->pluck('name'))->toContain('new_user');
});
}
});

View file

@ -8,6 +8,7 @@ use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Middleware\ScopeSessions; use Stancl\Tenancy\Middleware\ScopeSessions;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
Route::group([ Route::group([
@ -54,3 +55,15 @@ test('an exception is thrown when the middleware is executed before tenancy is i
pest()->expectException(TenancyNotInitializedException::class); pest()->expectException(TenancyNotInitializedException::class);
$this->withoutExceptionHandling()->get('http://acme.localhost/bar'); $this->withoutExceptionHandling()->get('http://acme.localhost/bar');
}); });
test('scope sessions mw can be used on universal routes', function() {
Route::get('/universal', function () {
return true;
})->middleware(['universal', InitializeTenancyBySubdomain::class, ScopeSessions::class]);
Tenant::create([
'id' => 'acme',
])->createDomain('acme');
pest()->withoutExceptionHandling()->get('http://localhost/universal')->assertSuccessful();
});

View file

@ -23,6 +23,7 @@ use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Middleware\InitializeTenancyByPath; use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains; use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
// todo@tests write similar low-level tests for the cache bootstrapper? including the database driver in a single-db setup // todo@tests write similar low-level tests for the cache bootstrapper? including the database driver in a single-db setup

View file

@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Stancl\Tenancy\Database\Concerns\BelongsToTenant; use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel; use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules; use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
Schema::create('posts', function (Blueprint $table) { Schema::create('posts', function (Blueprint $table) {

View file

@ -11,6 +11,7 @@ use Illuminate\Database\UniqueConstraintViolationException;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains; use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
config(['tenancy.models.tenant' => SingleDomainTenant::class]); config(['tenancy.models.tenant' => SingleDomainTenant::class]);

View file

@ -7,6 +7,7 @@ use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Exceptions\NotASubdomainException; use Stancl\Tenancy\Exceptions\NotASubdomainException;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Database\Models; use Stancl\Tenancy\Database\Models;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
// Global state cleanup after some tests // Global state cleanup after some tests

View file

@ -19,6 +19,7 @@ use Stancl\Tenancy\Controllers\TenantAssetController;
use Stancl\Tenancy\Events\TenancyEnded; use Stancl\Tenancy\Events\TenancyEnded;
use Stancl\Tenancy\Listeners\RevertToCentralContext; use Stancl\Tenancy\Listeners\RevertToCentralContext;
use Stancl\Tenancy\Overrides\TenancyUrlGenerator; use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
config(['tenancy.bootstrappers' => [ config(['tenancy.bootstrappers' => [
@ -65,7 +66,7 @@ test('asset can be accessed using the url returned by the tenant asset helper',
test('asset helper returns a link to tenant asset controller when asset url is null', function () { test('asset helper returns a link to tenant asset controller when asset url is null', function () {
config(['app.asset_url' => null]); config(['app.asset_url' => null]);
config(['tenancy.filesystem.asset_helper_tenancy' => true]); config(['tenancy.filesystem.asset_helper_override' => true]);
$tenant = Tenant::create(); $tenant = Tenant::create();
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
@ -78,7 +79,7 @@ test('asset helper returns a link to tenant asset controller when asset url is n
test('asset helper returns a link to an external url when asset url is not null', function () { 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']); config(['app.asset_url' => 'https://an-s3-bucket']);
config(['tenancy.filesystem.asset_helper_tenancy' => true]); config(['tenancy.filesystem.asset_helper_override' => true]);
$tenant = Tenant::create(); $tenant = Tenant::create();
tenancy()->initialize($tenant); tenancy()->initialize($tenant);
@ -93,7 +94,7 @@ test('asset helper works correctly with path identification', function (bool $ke
TenancyUrlGenerator::$prefixRouteNames = true; TenancyUrlGenerator::$prefixRouteNames = true;
TenancyUrlGenerator::$passTenantParameterToRoutes = true; TenancyUrlGenerator::$passTenantParameterToRoutes = true;
config(['tenancy.filesystem.asset_helper_tenancy' => true]); config(['tenancy.filesystem.asset_helper_override' => true]);
config(['tenancy.identification.default_middleware' => InitializeTenancyByPath::class]); config(['tenancy.identification.default_middleware' => InitializeTenancyByPath::class]);
config(['tenancy.bootstrappers' => array_merge([UrlGeneratorBootstrapper::class], config('tenancy.bootstrappers'))]); config(['tenancy.bootstrappers' => array_merge([UrlGeneratorBootstrapper::class], config('tenancy.bootstrappers'))]);
@ -165,7 +166,7 @@ test('asset helper tenancy can be disabled', function () {
config([ config([
'app.asset_url' => null, 'app.asset_url' => null,
'tenancy.filesystem.asset_helper_tenancy' => false, 'tenancy.filesystem.asset_helper_override' => false,
]); ]);
$tenant = Tenant::create(); $tenant = Tenant::create();

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use function Stancl\Tenancy\Tests\pest;
test('commands run globally are tenant aware and return valid exit code', function () { test('commands run globally are tenant aware and return valid exit code', function () {
$tenant1 = Tenant::create(); $tenant1 = Tenant::create();

View file

@ -28,6 +28,7 @@ use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMySQLData
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager; use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLSchemaManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager; use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledPostgreSQLDatabaseManager;
use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager; use Stancl\Tenancy\Database\TenantDatabaseManagers\PermissionControlledMicrosoftSQLServerDatabaseManager;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
SQLiteDatabaseManager::$path = null; SQLiteDatabaseManager::$path = null;
@ -405,6 +406,42 @@ test('tenant database can be created by using the username and password from ten
expect($manager->databaseExists($name))->toBeTrue(); expect($manager->databaseExists($name))->toBeTrue();
}); });
test('decrypted password can be used to connect to a tenant db while the password is saved as encrypted', function (string|null $tenantDbPassword) {
config([
'tenancy.database.managers.mysql' => PermissionControlledMySQLDatabaseManager::class,
'tenancy.database.template_tenant_connection' => 'mysql',
]);
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant;
})->toListener());
// Create a tenant, either with a specific password, or with a password generated by the DB manager
$tenant = TenantWithEncryptedPassword::create([
'tenancy_db_name' => $name = 'foo' . Str::random(8),
'tenancy_db_username' => 'user' . Str::random(4),
'tenancy_db_password' => $tenantDbPassword,
]);
$decryptedPassword = $tenant->tenancy_db_password;
$encryptedPassword = $tenant->getAttributes()['tenancy_db_password']; // Password encrypted using the TenantWithEncryptedPassword model's encrypted cast
expect($decryptedPassword)->not()->toBe($encryptedPassword);
$passwordSavedInDatabase = json_decode(DB::select('SELECT data FROM tenants LIMIT 1')[0]->data)->tenancy_db_password;
expect($encryptedPassword)->toBe($passwordSavedInDatabase);
app(DatabaseManager::class)->connectToTenant($tenant);
// Check if we got connected to the tenant DB
expect(config('database.default'))->toBe('tenant');
expect(config('database.connections.tenant.database'))->toBe($name);
// Check if the decrypted password is used to connect to the tenant DB
expect(config('database.connections.tenant.password'))->toBe($decryptedPassword);
})->with([
'decrypted' . Str::random(8), // Use this password as the tenant DB password
null, // Let the DB manager generate the tenant DB password
]);
test('path used by sqlite manager can be customized', function () { test('path used by sqlite manager can be customized', function () {
Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { Event::listen(TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
return $event->tenant; return $event->tenant;
@ -529,3 +566,13 @@ function createUsersTable()
$table->timestamps(); $table->timestamps();
}); });
} }
class TenantWithEncryptedPassword extends Tenant
{
protected function casts(): array
{
return [
'tenancy_db_password' => 'encrypted',
];
}
}

View file

@ -20,7 +20,16 @@ use Stancl\Tenancy\Tests\Etc\Tenant;
use Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator; use Stancl\Tenancy\UniqueIdentifierGenerators\UUIDGenerator;
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator; use Stancl\Tenancy\UniqueIdentifierGenerators\RandomHexGenerator;
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomIntGenerator;
use Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator; use Stancl\Tenancy\UniqueIdentifierGenerators\RandomStringGenerator;
use Stancl\Tenancy\UniqueIdentifierGenerators\ULIDGenerator;
use function Stancl\Tenancy\Tests\pest;
afterEach(function () {
RandomIntGenerator::$min = 0;
RandomIntGenerator::$max = PHP_INT_MAX;
});
test('created event is dispatched', function () { test('created event is dispatched', function () {
Event::fake([TenantCreated::class]); Event::fake([TenantCreated::class]);
@ -71,6 +80,20 @@ test('autoincrement ids are supported', function () {
expect($tenant2->id)->toBe(2); expect($tenant2->id)->toBe(2);
}); });
test('ulid ids are supported', function () {
app()->bind(UniqueIdentifierGenerator::class, ULIDGenerator::class);
$tenant1 = Tenant::create();
expect($tenant1->id)->toBeString();
expect(strlen($tenant1->id))->toBe(26);
$tenant2 = Tenant::create();
expect($tenant2->id)->toBeString();
expect(strlen($tenant2->id))->toBe(26);
expect($tenant2->id > $tenant1->id)->toBeTrue();
});
test('hex ids are supported', function () { test('hex ids are supported', function () {
app()->bind(UniqueIdentifierGenerator::class, RandomHexGenerator::class); app()->bind(UniqueIdentifierGenerator::class, RandomHexGenerator::class);
@ -87,6 +110,16 @@ test('hex ids are supported', function () {
RandomHexGenerator::$bytes = 6; // reset RandomHexGenerator::$bytes = 6; // reset
}); });
test('random ints are supported', function () {
app()->bind(UniqueIdentifierGenerator::class, RandomIntGenerator::class);
RandomIntGenerator::$min = 200;
RandomIntGenerator::$max = 1000;
$tenant1 = Tenant::create();
expect($tenant1->id >= 200)->toBeTrue();
expect($tenant1->id <= 1000)->toBeTrue();
});
test('random string ids are supported', function () { test('random string ids are supported', function () {
app()->bind(UniqueIdentifierGenerator::class, RandomStringGenerator::class); app()->bind(UniqueIdentifierGenerator::class, RandomStringGenerator::class);

View file

@ -25,6 +25,7 @@ use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper; use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
use Stancl\Tenancy\Exceptions\StatefulGuardRequiredException; use Stancl\Tenancy\Exceptions\StatefulGuardRequiredException;
use function Stancl\Tenancy\Tests\pest;
beforeEach(function () { beforeEach(function () {
pest()->artisan('migrate', [ pest()->artisan('migrate', [
@ -88,7 +89,7 @@ test('tenant user can be impersonated on a tenant domain', function () {
expect(session('tenancy_impersonating'))->toBeTrue(); expect(session('tenancy_impersonating'))->toBeTrue();
// Leave impersonation // Leave impersonation
UserImpersonation::leave(); UserImpersonation::stopImpersonating();
expect(UserImpersonation::isImpersonating())->toBeFalse(); expect(UserImpersonation::isImpersonating())->toBeFalse();
expect(session('tenancy_impersonating'))->toBeNull(); expect(session('tenancy_impersonating'))->toBeNull();
@ -134,7 +135,7 @@ test('tenant user can be impersonated on a tenant path', function () {
expect(session('tenancy_impersonating'))->toBeTrue(); expect(session('tenancy_impersonating'))->toBeTrue();
// Leave impersonation // Leave impersonation
UserImpersonation::leave(); UserImpersonation::stopImpersonating();
expect(UserImpersonation::isImpersonating())->toBeFalse(); expect(UserImpersonation::isImpersonating())->toBeFalse();
expect(session('tenancy_impersonating'))->toBeNull(); expect(session('tenancy_impersonating'))->toBeNull();

View file

@ -22,6 +22,8 @@ use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper; use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper; use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
use function Stancl\Tenancy\Tests\pest;
abstract class TestCase extends \Orchestra\Testbench\TestCase abstract class TestCase extends \Orchestra\Testbench\TestCase
{ {
@ -85,11 +87,8 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'--realpath' => true, '--realpath' => true,
]); ]);
// Laravel 6.x support todo@refactor clean up \Illuminate\Testing\TestResponse::macro('assertContent', function ($content) {
$testResponse = class_exists('Illuminate\Testing\TestResponse') ? 'Illuminate\Testing\TestResponse' : 'Illuminate\Foundation\Testing\TestResponse'; \Illuminate\Testing\Assert::assertSame($content, $this->baseResponse->getContent());
$testResponse::macro('assertContent', function ($content) {
$assertClass = class_exists('Illuminate\Testing\Assert') ? 'Illuminate\Testing\Assert' : 'Illuminate\Foundation\Testing\Assert';
$assertClass::assertSame($content, $this->baseResponse->getContent());
return $this; return $this;
}); });
@ -175,18 +174,25 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
'tenancy.models.tenant' => Tenant::class, // Use test tenant w/ DBs & domains 'tenancy.models.tenant' => Tenant::class, // Use test tenant w/ DBs & domains
]); ]);
$app->singleton(RedisTenancyBootstrapper::class); // todo@samuel use proper approach eg config for singleton registration // Since we run the TSP with no bootstrappers enabled, we need
$app->singleton(CacheTenancyBootstrapper::class); // todo@samuel use proper approach eg config for singleton registration // to manually register bootstrappers as singletons here.
$app->singleton(RedisTenancyBootstrapper::class);
$app->singleton(CacheTenancyBootstrapper::class);
$app->singleton(BroadcastingConfigBootstrapper::class); $app->singleton(BroadcastingConfigBootstrapper::class);
$app->singleton(BroadcastChannelPrefixBootstrapper::class); $app->singleton(BroadcastChannelPrefixBootstrapper::class);
$app->singleton(PostgresRLSBootstrapper::class); $app->singleton(PostgresRLSBootstrapper::class);
$app->singleton(MailConfigBootstrapper::class); $app->singleton(MailConfigBootstrapper::class);
$app->singleton(RootUrlBootstrapper::class); $app->singleton(RootUrlBootstrapper::class);
$app->singleton(UrlGeneratorBootstrapper::class); $app->singleton(UrlGeneratorBootstrapper::class);
$app->singleton(FilesystemTenancyBootstrapper::class);
} }
protected function getPackageProviders($app) protected function getPackageProviders($app)
{ {
TenancyServiceProvider::$configure = function () {
config(['tenancy.bootstrappers' => []]);
};
return [ return [
TenancyServiceProvider::class, TenancyServiceProvider::class,
]; ];

View file

@ -2,8 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
use Stancl\Tenancy\Tenancy;
use Illuminate\Http\Request;
use Stancl\Tenancy\Enums\RouteMode; use Stancl\Tenancy\Enums\RouteMode;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
use Illuminate\Contracts\Http\Kernel; use Illuminate\Contracts\Http\Kernel;
@ -11,16 +9,14 @@ use Stancl\Tenancy\Resolvers\PathTenantResolver;
use Illuminate\Routing\Controller as BaseController; use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\Route as RouteFacade; use Illuminate\Support\Facades\Route as RouteFacade;
use Stancl\Tenancy\Tests\Etc\HasMiddlewareController; use Stancl\Tenancy\Tests\Etc\HasMiddlewareController;
use Stancl\Tenancy\Middleware\IdentificationMiddleware;
use Stancl\Tenancy\Resolvers\RequestDataTenantResolver;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData; use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains; use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException; use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByRequestDataException;
use function Stancl\Tenancy\Tests\pest;
test('a route can be universal using domain identification', function (array $routeMiddleware, array $globalMiddleware) { test('a route can be universal using domain identification', function (array $routeMiddleware, array $globalMiddleware) {
foreach ($globalMiddleware as $middleware) { foreach ($globalMiddleware as $middleware) {