mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 21:34:04 +00:00
Cache prefixing logic rewrite, session scoping improvements, tests refactor (#43)
* Run cache tests on all supported drivers * update ci healthcheck for memcached * remove memcached healthcheck * fix typos in test comments, expand internal.md [ci skip] * add empty line [ci skip] * switch to using $store->setPrefix() * add dynamodb * refactor try-finally to try-catch * remove unnecessary clearResolvedInstances() call * add dual Cache:: and cache() assertions * add apc * Flush APCu cache in test setup * Revert "add dual Cache:: and cache() assertions" This reverts commit a0bab162fbe2dd0d25e7056ceca4fb7ce54efc77. * phpstan fix * Add logic for scoping 'file' disks to FilesystemTenancyBootstrapper * minor changes, add todos * refactor how the session.connection is used in the DB session bootstrapper * add session forgery prevention logic to the db session bootstrapper * only use the fs bootstrapper for file disk in 'cache data is separated' dataset * minor session scoping test changes * Add session scoping logic to FilesystemTenancyBootstrapper, correctly update disk roots even with storage_path_tenancy disabled * Fix code style (php-cs-fixer) * update docblock * make not-null check more explicit * separate bootstrapper tests, fix swapped test names for two tests * refactor cache bootstrapper tests * resolve global cache todo * expand tests: session separation tests, more filesystem separation assertions; change prefix_base-type config keys to templates/formats * add apc session scoping test, various session separation bugfixes * phpstan + minor logic fixes * prefix_format -> prefix * fix database session separation test * revert composer.json changes, update laravel dependencies to expected next release * only run session scoping logic in cache bootstrapper for redis, memcached, dynamodb, apc; update gitattributes * tenancy.central_domains -> tenancy.identification.central_domains * db session separation test: add datasets --------- Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
This commit is contained in:
parent
943b960718
commit
eecf6f21c8
40 changed files with 1856 additions and 1177 deletions
19
.gitattributes
vendored
19
.gitattributes
vendored
|
|
@ -3,15 +3,22 @@
|
||||||
|
|
||||||
# Ignore all test and documentation with "export-ignore".
|
# Ignore all test and documentation with "export-ignore".
|
||||||
/.github export-ignore
|
/.github export-ignore
|
||||||
/art export-ignore
|
|
||||||
/tests export-ignore
|
|
||||||
/.gitattributes export-ignore
|
/.gitattributes export-ignore
|
||||||
/.gitignore export-ignore
|
/.gitignore export-ignore
|
||||||
/.styleci.yml export-ignore
|
/.styleci.yml export-ignore
|
||||||
/docker-compose.yml export-ignore
|
|
||||||
/Dockerfile export-ignore
|
|
||||||
/test export-ignore
|
|
||||||
/phpunit.xml export-ignore
|
|
||||||
/.editorconfig export-ignore
|
/.editorconfig export-ignore
|
||||||
/.coverage.xml export-ignore
|
/.coverage.xml export-ignore
|
||||||
|
/art export-ignore
|
||||||
/coverage export-ignore
|
/coverage export-ignore
|
||||||
|
/CONTRIBUTING.md export-ignore
|
||||||
|
/INTERNAL.md export-ignore
|
||||||
|
/SUPPORT.md export-ignore
|
||||||
|
/docker-compose.yml export-ignore
|
||||||
|
/docker-compose-override.yml export-ignore
|
||||||
|
/docker-compose-m1.override.yml export-ignore
|
||||||
|
/Dockerfile export-ignore
|
||||||
|
/doctum export-ignore
|
||||||
|
/phpunit.xml export-ignore
|
||||||
|
/t export-ignore
|
||||||
|
/test export-ignore
|
||||||
|
/tests export-ignore
|
||||||
|
|
|
||||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
|
@ -89,6 +89,18 @@ jobs:
|
||||||
- 6379/tcp
|
- 6379/tcp
|
||||||
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||||
|
|
||||||
|
memcached:
|
||||||
|
image: memcached
|
||||||
|
ports:
|
||||||
|
- 11211/tcp
|
||||||
|
# options: --health-cmd="/bin/nc -z 127.0.0.1 11211" --health-interval=10s --health-timeout=5s --health-retries=3 # TODO: Add a working health check
|
||||||
|
|
||||||
|
dynamodb:
|
||||||
|
image: amazon/dynamodb-local:latest
|
||||||
|
ports:
|
||||||
|
- 8000/tcp
|
||||||
|
# options: --health-cmd="/bin/nc -z 127.0.0.1 8000" --health-interval=10s --health-timeout=5s --health-retries=3 # TODO: Add a working health check
|
||||||
|
|
||||||
php-cs-fixer:
|
php-cs-fixer:
|
||||||
name: Code style (php-cs-fixer)
|
name: Code style (php-cs-fixer)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ If you're developing some feature and you encounter `SQLSTATE[23000]: Integrity
|
||||||
|
|
||||||
To fix this, simply delete the database memory by shutting down containers and starting them again: `composer docker-down && composer docker-up`.
|
To fix this, simply delete the database memory by shutting down containers and starting them again: `composer docker-down && composer docker-up`.
|
||||||
|
|
||||||
|
Same thing for `SQLSTATE[HY000]: General error: 1615 Prepared statement needs to be re-prepared`.
|
||||||
|
|
||||||
### Docker on M1
|
### Docker on M1
|
||||||
|
|
||||||
Run `composer docker-m1` to symlink `docker-compose-m1.override.yml` to `docker-compose.override.yml`. This will reconfigure a few services in the docker compose config to be compatible with M1.
|
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.
|
||||||
|
|
|
||||||
22
Dockerfile
22
Dockerfile
|
|
@ -29,19 +29,17 @@ RUN update-alternatives --set php /usr/bin/php$PHP_VERSION \
|
||||||
&& update-alternatives --set phpize /usr/bin/phpize$PHP_VERSION \
|
&& update-alternatives --set phpize /usr/bin/phpize$PHP_VERSION \
|
||||||
&& update-alternatives --set php-config /usr/bin/php-config$PHP_VERSION
|
&& update-alternatives --set php-config /usr/bin/php-config$PHP_VERSION
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends libhiredis0.14 libjemalloc2 liblua5.1-0 lua-bitop lua-cjson redis redis-server redis-tools
|
|
||||||
|
|
||||||
RUN pecl install redis-5.3.7 sqlsrv pdo_sqlsrv pcov \
|
|
||||||
&& printf "; priority=20\nextension=redis.so\n" > /etc/php/$PHP_VERSION/mods-available/redis.ini \
|
|
||||||
&& printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/$PHP_VERSION/mods-available/sqlsrv.ini \
|
|
||||||
&& printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/$PHP_VERSION/mods-available/pdo_sqlsrv.ini \
|
|
||||||
&& printf "; priority=40\nextension=pcov.so\n" > /etc/php/$PHP_VERSION/mods-available/pcov.ini \
|
|
||||||
&& phpenmod -v $PHP_VERSION redis sqlsrv pdo_sqlsrv pcov
|
|
||||||
|
|
||||||
# install composer
|
# install composer
|
||||||
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
|
||||||
|
|
||||||
# set the system timezone
|
# set the system timezone
|
||||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
&& echo $TZ > /etc/timezone
|
|
||||||
|
# install PHP extensions
|
||||||
|
RUN pecl install redis && printf "; priority=20\nextension=redis.so\n" > /etc/php/$PHP_VERSION/mods-available/redis.ini && phpenmod -v $PHP_VERSION redis
|
||||||
|
RUN pecl install pdo_sqlsrv && printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/$PHP_VERSION/mods-available/pdo_sqlsrv.ini && phpenmod -v $PHP_VERSION pdo_sqlsrv
|
||||||
|
RUN pecl install pcov && printf "; priority=40\nextension=pcov.so\n" > /etc/php/$PHP_VERSION/mods-available/pcov.ini && phpenmod -v $PHP_VERSION pcov
|
||||||
|
|
||||||
|
RUN apt-get install -y --no-install-recommends libmemcached-dev zlib1g-dev
|
||||||
|
RUN pecl install memcached && printf "; priority=50\nextension=memcached.so\n" > /etc/php/$PHP_VERSION/mods-available/memcached.ini && phpenmod -v $PHP_VERSION memcached
|
||||||
|
RUN pecl install apcu && printf "; priority=60\nextension=apcu.so\napc.enable_cli=1\n" > /etc/php/$PHP_VERSION/mods-available/apcu.ini && phpenmod -v $PHP_VERSION apcu
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@
|
||||||
|
|
||||||
## Updating the docker image used by the GH action
|
## Updating the docker image used by the GH action
|
||||||
|
|
||||||
1. Login in to Docker Hub: `docker login -u archtechx -p`
|
1. Login in to Docker Hub: `docker login -u archtechx`
|
||||||
1. Build the image (probably shut down docker-compose containers first): `DOCKER_DEFAULT_PLATFORM=linux/amd64 docker-compose build --no-cache`
|
1. Shut down containers: `composer docker-down`
|
||||||
|
1. Build the image: `DOCKER_DEFAULT_PLATFORM=linux/amd64 docker-compose build --no-cache`
|
||||||
|
1. Start containers again, using the amd64 image for the `test` service: `composer docker-up`
|
||||||
1. Verify that tests pass on the new image: `composer test`
|
1. Verify that tests pass on the new image: `composer test`
|
||||||
1. Tag a new image: `docker tag tenancy-test archtechx/tenancy:latest`
|
1. Tag a new image: `docker tag tenancy-test archtechx/tenancy:latest`
|
||||||
1. Push the image: `docker push archtechx/tenancy:latest`
|
1. Push the image: `docker push archtechx/tenancy:latest`
|
||||||
|
1. Optional: Rebuild the image again locally for arm64: `composer docker-rebuild`
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,16 @@ return [
|
||||||
'id_generator' => UniqueIdentifierGenerators\UUIDGenerator::class,
|
'id_generator' => UniqueIdentifierGenerators\UUIDGenerator::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'identification' => [
|
||||||
/**
|
/**
|
||||||
* The list of domains hosting your central app.
|
* The list of domains hosting your central app.
|
||||||
*
|
*
|
||||||
* Only relevant if you're using the domain or subdomain identification middleware.
|
* Only relevant if you're using the domain or subdomain identification middleware.
|
||||||
*/
|
*/
|
||||||
'central_domains' => [
|
'central_domains' => [ // todo@docs this was moved into the identification section
|
||||||
str(env('APP_URL'))->after('://')->before('/')->toString(),
|
str(env('APP_URL'))->after('://')->before('/')->toString(),
|
||||||
],
|
],
|
||||||
|
|
||||||
'identification' => [
|
|
||||||
/**
|
/**
|
||||||
* The default middleware used for tenant identification.
|
* The default middleware used for tenant identification.
|
||||||
*
|
*
|
||||||
|
|
@ -222,11 +222,18 @@ return [
|
||||||
* You can clear cache selectively by specifying the tag.
|
* You can clear cache selectively by specifying the tag.
|
||||||
*/
|
*/
|
||||||
'cache' => [
|
'cache' => [
|
||||||
'prefix_base' => 'tenant', // This prefix_base, followed by the tenant_id, will form a cache prefix that will be used for every cache key.
|
'prefix' => 'tenant_%tenant%_', // This format, with the %tenant% replaced by the tenant key, and prepended by the original store prefix, will form a cache prefix that will be used for every cache key.
|
||||||
'stores' => [
|
'stores' => [
|
||||||
env('CACHE_STORE'),
|
env('CACHE_STORE'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Should sessions be tenant-aware (only used when your session driver is cache-based).
|
||||||
|
*
|
||||||
|
* Note: This will implicitly add your configured session store to the list of prefixed stores above.
|
||||||
|
*/
|
||||||
|
'scope_sessions' => true,
|
||||||
|
|
||||||
'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call.
|
'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call.
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -273,6 +280,20 @@ return [
|
||||||
'public' => 'public-%tenant%',
|
'public' => 'public-%tenant%',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Should the `file` cache driver be tenant-aware.
|
||||||
|
*
|
||||||
|
* When this is enabled, cache files will be stored in storage/{tenant}/framework/cache.
|
||||||
|
*/
|
||||||
|
'scope_cache' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Should the `file` session driver be tenant-aware.
|
||||||
|
*
|
||||||
|
* When this is enabled, session files will be stored in storage/{tenant}/framework/sessions.
|
||||||
|
*/
|
||||||
|
'scope_sessions' => true,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should storage_path() be suffixed.
|
* Should storage_path() be suffixed.
|
||||||
*
|
*
|
||||||
|
|
@ -304,7 +325,7 @@ return [
|
||||||
* either using the Redis facade or by injecting it as a dependency.
|
* either using the Redis facade or by injecting it as a dependency.
|
||||||
*/
|
*/
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'prefix_base' => 'tenant', // Each key in Redis will be prepended by this prefix_base, followed by the tenant id.
|
'prefix' => 'tenant_%tenant%_', // Each key in Redis will be prepended by this prefix format, with %tenant% replaced by the tenant key.
|
||||||
'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another.
|
'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another.
|
||||||
'default',
|
'default',
|
||||||
// 'cache', // Enable this if you want to scope cache using RedisTenancyBootstrapper
|
// 'cache', // Enable this if you want to scope cache using RedisTenancyBootstrapper
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware;
|
||||||
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -20,8 +19,9 @@ use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
|
|
||||||
Route::middleware([
|
Route::middleware([
|
||||||
'web',
|
'web',
|
||||||
InitializeTenancyByDomain::class,
|
Middleware\InitializeTenancyByDomain::class,
|
||||||
PreventAccessFromUnwantedDomains::class,
|
Middleware\PreventAccessFromUnwantedDomains::class,
|
||||||
|
Middleware\ScopeSessions::class,
|
||||||
])->group(function () {
|
])->group(function () {
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
|
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"illuminate/support": "^10.1|^11.0",
|
"illuminate/support": "^10.1|^11.3",
|
||||||
"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",
|
||||||
|
|
@ -28,14 +28,15 @@
|
||||||
"laravel/prompts": "^0.1.9"
|
"laravel/prompts": "^0.1.9"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "^10.1|^11.2",
|
"laravel/framework": "^10.1|^11.3",
|
||||||
"orchestra/testbench": "^8.0|^9.0",
|
"orchestra/testbench": "^8.0|^9.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": "^2.0",
|
||||||
"larastan/larastan": "^2.4",
|
"larastan/larastan": "^2.4",
|
||||||
"spatie/invade": "^1.1"
|
"spatie/invade": "^1.1",
|
||||||
|
"aws/aws-sdk-php-laravel": "~3.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,18 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
mysql2:
|
||||||
|
condition: service_healthy
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
# mssql:
|
mssql:
|
||||||
# condition: service_healthy
|
condition: service_healthy
|
||||||
|
memcached:
|
||||||
|
condition: service_healthy
|
||||||
|
dynamodb:
|
||||||
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- .:/var/www/html:delegated
|
- .:/var/www/html:delegated
|
||||||
environment:
|
environment:
|
||||||
|
|
@ -68,22 +74,34 @@ services:
|
||||||
retries: 5
|
retries: 5
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /var/lib/postgresql/data
|
- /var/lib/postgresql/data
|
||||||
|
mssql:
|
||||||
|
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||||
|
environment:
|
||||||
|
- ACCEPT_EULA=Y
|
||||||
|
- SA_PASSWORD=P@ssword # todo reuse env from above
|
||||||
|
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'
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 1s
|
interval: 1s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 30
|
retries: 20
|
||||||
mssql:
|
memcached:
|
||||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
image: memcached:alpine
|
||||||
ports:
|
|
||||||
- 1433:1433
|
|
||||||
environment:
|
|
||||||
- ACCEPT_EULA=Y
|
|
||||||
- SA_PASSWORD=P@ssword # todo reuse values from env above
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P P@ssword -Q "SELECT 1" -b -o /dev/null
|
test: ["CMD-SHELL", "echo version | nc localhost 11211 | grep -q VERSION"]
|
||||||
interval: 10s
|
interval: 1s
|
||||||
timeout: 10s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 20
|
||||||
|
dynamodb:
|
||||||
|
image: amazon/dynamodb-local:latest
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "cat < /dev/null > /dev/tcp/127.0.0.1/8000"]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ declare(strict_types=1);
|
||||||
namespace Stancl\Tenancy\Bootstrappers;
|
namespace Stancl\Tenancy\Bootstrappers;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
|
use Exception;
|
||||||
use Illuminate\Cache\CacheManager;
|
use Illuminate\Cache\CacheManager;
|
||||||
use Illuminate\Cache\Repository;
|
use Illuminate\Contracts\Cache\Store;
|
||||||
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
use Illuminate\Contracts\Config\Repository as ConfigRepository;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Session\CacheBasedSessionHandler;
|
||||||
|
use Illuminate\Session\SessionManager;
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
|
@ -17,70 +19,136 @@ use Stancl\Tenancy\Contracts\Tenant;
|
||||||
*/
|
*/
|
||||||
class CacheTenancyBootstrapper implements TenancyBootstrapper
|
class CacheTenancyBootstrapper implements TenancyBootstrapper
|
||||||
{
|
{
|
||||||
|
/** @var Closure(Tenant, string): string */
|
||||||
public static Closure|null $prefixGenerator = null;
|
public static Closure|null $prefixGenerator = null;
|
||||||
|
|
||||||
protected string|null $originalPrefix = null;
|
/** @var array<string, string> */
|
||||||
|
protected array $originalPrefixes = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected ConfigRepository $config,
|
protected ConfigRepository $config,
|
||||||
protected CacheManager $cacheManager,
|
protected CacheManager $cache,
|
||||||
|
protected SessionManager $session,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant): void
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
$this->originalPrefix = $this->config->get('cache.prefix');
|
foreach ($this->getCacheStores() as $name) {
|
||||||
|
$store = $this->cache->driver($name)->getStore();
|
||||||
|
|
||||||
$prefix = $this->generatePrefix($tenant);
|
$this->originalPrefixes[$name] = $store->getPrefix();
|
||||||
|
$this->setCachePrefix($store, $this->generatePrefix($tenant, $name));
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->config->get('tenancy.cache.stores') as $store) {
|
if ($this->shouldScopeSessions()) {
|
||||||
$this->setCachePrefix($store, $prefix);
|
$name = $this->getSessionCacheStoreName();
|
||||||
|
$handler = $this->session->driver()->getHandler();
|
||||||
|
|
||||||
// Now that the store uses the passed prefix
|
if ($handler instanceof CacheBasedSessionHandler) {
|
||||||
// Set the configured prefix back to the default one
|
// The CacheBasedSessionHandler is constructed with a *clone* of
|
||||||
$this->config->set('cache.prefix', $this->originalPrefix);
|
// an existing cache store, so we need to set the prefix separately.
|
||||||
|
$store = $handler->getCache()->getStore();
|
||||||
|
|
||||||
|
// We also don't need to set the original prefix, since the cache store
|
||||||
|
// is implicitly added to the configured cache stores when session scoping
|
||||||
|
// is enabled.
|
||||||
|
|
||||||
|
$this->setCachePrefix($store, $this->generatePrefix($tenant, $name));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert(): void
|
public function revert(): void
|
||||||
{
|
{
|
||||||
foreach ($this->config->get('tenancy.cache.stores') as $store) {
|
foreach ($this->getCacheStores() as $name) {
|
||||||
$this->setCachePrefix($store, $this->originalPrefix);
|
$store = $this->cache->driver($name)->getStore();
|
||||||
|
|
||||||
|
$this->setCachePrefix($store, $this->originalPrefixes[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->shouldScopeSessions()) {
|
||||||
|
$name = $this->getSessionCacheStoreName();
|
||||||
|
$handler = $this->session->driver()->getHandler();
|
||||||
|
|
||||||
|
if ($handler instanceof CacheBasedSessionHandler) {
|
||||||
|
$store = $handler->getCache()->getStore();
|
||||||
|
|
||||||
|
$this->setCachePrefix($store, $this->originalPrefixes[$name]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setCachePrefix(string $driver, string|null $prefix): void
|
protected function getSessionCacheStoreName(): string
|
||||||
{
|
{
|
||||||
$this->config->set('cache.prefix', $prefix);
|
return $this->config->get('session.store') ?? $this->config->get('session.driver');
|
||||||
|
|
||||||
// Refresh driver's store to make the driver use the current prefix
|
|
||||||
$this->refreshStore($driver);
|
|
||||||
|
|
||||||
// It is needed when a call to the facade has been made before bootstrapping tenancy
|
|
||||||
// The facade has its own cache, separate from the container
|
|
||||||
Cache::clearResolvedInstances();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function generatePrefix(Tenant $tenant): string
|
protected function shouldScopeSessions(): bool
|
||||||
{
|
{
|
||||||
$defaultPrefix = $this->originalPrefix . $this->config->get('tenancy.cache.prefix_base') . $tenant->getTenantKey();
|
// We don't want to scope sessions if:
|
||||||
|
// 1. The user has disabled session scoping via this bootstrapper, AND
|
||||||
return static::$prefixGenerator ? (static::$prefixGenerator)($tenant) : $defaultPrefix;
|
// 2. The session driver hasn't been instantiated yet (if this is the case,
|
||||||
|
// it will be instantiated later by cloning an existing cache store
|
||||||
|
// that will have already been prefixed in this bootstrapper).
|
||||||
|
return $this->config->get('tenancy.cache.scope_sessions', true)
|
||||||
|
&& count($this->session->getDrivers()) !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return string[] */
|
||||||
|
protected function getCacheStores(): array
|
||||||
|
{
|
||||||
|
$names = $this->config->get('tenancy.cache.stores');
|
||||||
|
|
||||||
|
if (
|
||||||
|
$this->config->get('tenancy.cache.scope_sessions', true) &&
|
||||||
|
in_array($this->config->get('session.driver'), ['redis', 'memcached', 'dynamodb', 'apc'], true)
|
||||||
|
) {
|
||||||
|
$names[] = $this->getSessionCacheStoreName();
|
||||||
|
}
|
||||||
|
|
||||||
|
$names = array_unique($names);
|
||||||
|
|
||||||
|
return array_filter($names, function ($name) {
|
||||||
|
$store = $this->config->get("cache.stores.{$name}");
|
||||||
|
|
||||||
|
if ($store === null || $store['driver'] === 'file') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($store['driver'] === 'array') {
|
||||||
|
throw new Exception('Cache store [' . $name . '] is not supported by this bootstrapper.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setCachePrefix(Store $store, string|null $prefix): void
|
||||||
|
{
|
||||||
|
if (! method_exists($store, 'setPrefix')) {
|
||||||
|
throw new Exception('Cache store [' . get_class($store) . '] does not support setting a prefix.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$store->setPrefix($prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generatePrefix(Tenant $tenant, string $store): string
|
||||||
|
{
|
||||||
|
return static::$prefixGenerator
|
||||||
|
? (static::$prefixGenerator)($tenant, $store)
|
||||||
|
: $this->originalPrefixes[$store] . str($this->config->get('tenancy.cache.prefix'))
|
||||||
|
->replace('%tenant%', (string) $tenant->getTenantKey())->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a custom prefix generator.
|
||||||
|
*
|
||||||
|
* The first argument is the tenant, the second argument is the cache store name.
|
||||||
|
*
|
||||||
|
* @param Closure(Tenant, string): string $prefixGenerator
|
||||||
|
*/
|
||||||
public static function generatePrefixUsing(Closure $prefixGenerator): void
|
public static function generatePrefixUsing(Closure $prefixGenerator): void
|
||||||
{
|
{
|
||||||
static::$prefixGenerator = $prefixGenerator;
|
static::$prefixGenerator = $prefixGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh cache driver's store.
|
|
||||||
*/
|
|
||||||
protected function refreshStore(string $driver): void
|
|
||||||
{
|
|
||||||
$newStore = $this->cacheManager->resolve($driver)->getStore();
|
|
||||||
/** @var Repository $repository */
|
|
||||||
$repository = $this->cacheManager->driver($driver);
|
|
||||||
|
|
||||||
$repository->setStore($newStore);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ use Stancl\Tenancy\Contracts\Tenant;
|
||||||
/**
|
/**
|
||||||
* This resets the database connection used by the database session driver.
|
* This resets the database connection used by the database session driver.
|
||||||
*
|
*
|
||||||
|
* It also includes a mechanism to prevent session forgery when SESSION_CONNECTION is specified.
|
||||||
|
*
|
||||||
* It runs each time tenancy is initialized or ended.
|
* It runs each time tenancy is initialized or ended.
|
||||||
* That way the session driver always uses the current DB connection.
|
* That way the session driver always uses the current DB connection.
|
||||||
*/
|
*/
|
||||||
|
|
@ -25,23 +27,37 @@ class DatabaseSessionBootstrapper implements TenancyBootstrapper
|
||||||
protected SessionManager $session,
|
protected SessionManager $session,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
protected string|null $originalConnection = null;
|
||||||
|
|
||||||
public function bootstrap(Tenant $tenant): void
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
|
$this->originalConnection = $this->config->get('session.connection');
|
||||||
|
|
||||||
if ($this->config->get('session.driver') === 'database') {
|
if ($this->config->get('session.driver') === 'database') {
|
||||||
$this->resetDatabaseHandler();
|
// At first, this bootstrapper runs before the StartSession middleware, so
|
||||||
|
// changing the session.connection here will affect what connection the session
|
||||||
|
// driver will use. This is helpful to override the SESSION_CONNECTION that might
|
||||||
|
// otherwise allow for session forgery in the tenant context.
|
||||||
|
$this->config->set('session.connection', 'tenant');
|
||||||
|
|
||||||
|
$this->resetDatabaseHandler('tenant');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function revert(): void
|
public function revert(): void
|
||||||
{
|
{
|
||||||
if ($this->config->get('session.driver') === 'database') {
|
if ($this->config->get('session.driver') === 'database') {
|
||||||
|
$connection = $this->originalConnection ?? config('tenancy.database.central_connection');
|
||||||
|
|
||||||
// When ending tenancy, this runs *before* the DatabaseTenancyBootstrapper, so DB tenancy
|
// When ending tenancy, this runs *before* the DatabaseTenancyBootstrapper, so DB tenancy
|
||||||
// is still bootstrapped. For that reason, we have to explicitly use the central connection
|
// is still bootstrapped. For that reason, we have to explicitly use the central connection
|
||||||
$this->resetDatabaseHandler(config('tenancy.database.central_connection'));
|
// instead of null for the default connection.
|
||||||
|
$this->config->set('session.connection', $connection);
|
||||||
|
$this->resetDatabaseHandler($connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resetDatabaseHandler(string $defaultConnection = null): void
|
protected function resetDatabaseHandler(string $connection): void
|
||||||
{
|
{
|
||||||
$sessionDrivers = $this->session->getDrivers();
|
$sessionDrivers = $this->session->getDrivers();
|
||||||
|
|
||||||
|
|
@ -49,15 +65,12 @@ class DatabaseSessionBootstrapper implements TenancyBootstrapper
|
||||||
/** @var \Illuminate\Session\Store $databaseDriver */
|
/** @var \Illuminate\Session\Store $databaseDriver */
|
||||||
$databaseDriver = $sessionDrivers['database'];
|
$databaseDriver = $sessionDrivers['database'];
|
||||||
|
|
||||||
$databaseDriver->setHandler($this->createDatabaseHandler($defaultConnection));
|
$databaseDriver->setHandler($this->createDatabaseHandler($connection));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createDatabaseHandler(string $defaultConnection = null): DatabaseSessionHandler
|
protected function createDatabaseHandler(string $connection): DatabaseSessionHandler
|
||||||
{
|
{
|
||||||
// Typically returns null, so this falls back to the default DB connection
|
|
||||||
$connection = $this->config->get('session.connection') ?? $defaultConnection;
|
|
||||||
|
|
||||||
// Based on SessionManager::createDatabaseDriver
|
// Based on SessionManager::createDatabaseDriver
|
||||||
return new DatabaseSessionHandler(
|
return new DatabaseSessionHandler(
|
||||||
$this->container->make('db')->connection($connection),
|
$this->container->make('db')->connection($connection),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Bootstrappers;
|
||||||
|
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Routing\UrlGenerator;
|
use Illuminate\Routing\UrlGenerator;
|
||||||
|
use Illuminate\Session\FileSessionHandler;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
@ -37,6 +38,9 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->storagePath($suffix);
|
$this->storagePath($suffix);
|
||||||
$this->assetHelper($suffix);
|
$this->assetHelper($suffix);
|
||||||
$this->forgetDisks();
|
$this->forgetDisks();
|
||||||
|
$this->scopeCache($suffix);
|
||||||
|
$this->scopeSessions($suffix);
|
||||||
|
// todo@docs update fs docs
|
||||||
|
|
||||||
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
||||||
$this->diskRoot($disk, $tenant);
|
$this->diskRoot($disk, $tenant);
|
||||||
|
|
@ -55,6 +59,8 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->storagePath(false);
|
$this->storagePath(false);
|
||||||
$this->assetHelper(false);
|
$this->assetHelper(false);
|
||||||
$this->forgetDisks();
|
$this->forgetDisks();
|
||||||
|
$this->scopeCache(false);
|
||||||
|
$this->scopeSessions(false);
|
||||||
|
|
||||||
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
foreach ($this->app['config']['tenancy.filesystem.disks'] as $disk) {
|
||||||
$this->diskRoot($disk, false);
|
$this->diskRoot($disk, false);
|
||||||
|
|
@ -76,10 +82,15 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
if ($suffix === false) {
|
if ($suffix === false) {
|
||||||
$this->app->useStoragePath($this->originalStoragePath);
|
$this->app->useStoragePath($this->originalStoragePath);
|
||||||
} else {
|
} else {
|
||||||
$this->app->useStoragePath($this->originalStoragePath . "/{$suffix}");
|
$this->app->useStoragePath($this->tenantStoragePath($suffix));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function tenantStoragePath(string $suffix): string
|
||||||
|
{
|
||||||
|
return $this->originalStoragePath . "/{$suffix}";
|
||||||
|
}
|
||||||
|
|
||||||
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_tenancy']) {
|
||||||
|
|
@ -125,7 +136,7 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
// This is executed if the disk is in tenancy.filesystem.disks AND has a root_override
|
// This is executed if the disk is in tenancy.filesystem.disks AND has a root_override
|
||||||
// This behavior is used for local disks.
|
// This behavior is used for local disks.
|
||||||
$newRoot = str($override)
|
$newRoot = str($override)
|
||||||
->replace('%storage_path%', storage_path())
|
->replace('%storage_path%', $this->tenantStoragePath($suffix))
|
||||||
->replace('%original_storage_path%', $this->originalStoragePath)
|
->replace('%original_storage_path%', $this->originalStoragePath)
|
||||||
->replace('%tenant%', (string) $tenant->getTenantKey())
|
->replace('%tenant%', (string) $tenant->getTenantKey())
|
||||||
->toString();
|
->toString();
|
||||||
|
|
@ -156,4 +167,70 @@ class FilesystemTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$this->app['config']["filesystems.disks.{$disk}.url"] = url($override);
|
$this->app['config']["filesystems.disks.{$disk}.url"] = url($override);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scopeCache(string|false $suffix): void
|
||||||
|
{
|
||||||
|
if (! $this->app['config']['tenancy.filesystem.scope_cache']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$storagePath = $suffix
|
||||||
|
? $this->tenantStoragePath($suffix)
|
||||||
|
: $this->originalStoragePath;
|
||||||
|
|
||||||
|
$stores = array_filter($this->app['config']['tenancy.cache.stores'], function ($name) {
|
||||||
|
$store = $this->app['config']["cache.stores.{$name}"];
|
||||||
|
|
||||||
|
if ($store === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $store['driver'] === 'file';
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ($stores as $name) {
|
||||||
|
$path = $storagePath . '/framework/cache/data';
|
||||||
|
$this->app['config']["cache.stores.{$name}.path"] = $path;
|
||||||
|
$this->app['config']["cache.stores.{$name}.lock_path"] = $path;
|
||||||
|
|
||||||
|
/** @var \Illuminate\Cache\FileStore $store */
|
||||||
|
$store = $this->app['cache']->store($name)->getStore();
|
||||||
|
$store->setDirectory($path);
|
||||||
|
$store->setLockDirectory($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeSessions(string|false $suffix): void
|
||||||
|
{
|
||||||
|
if (! $this->app['config']['tenancy.filesystem.scope_sessions']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $suffix
|
||||||
|
? $this->tenantStoragePath($suffix) . '/framework/sessions'
|
||||||
|
: $this->originalStoragePath . '/framework/sessions';
|
||||||
|
|
||||||
|
if (! is_dir($path)) {
|
||||||
|
// Create tenant framework/sessions directory if it does not exist
|
||||||
|
mkdir($path, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->app['config']['session.files'] = $path;
|
||||||
|
|
||||||
|
/** @var \Illuminate\Session\SessionManager $sessionManager */
|
||||||
|
$sessionManager = $this->app['session'];
|
||||||
|
|
||||||
|
// Since this bootstrapper runs much earlier than the StartSession middleware, this doesn't execute
|
||||||
|
// on the average tenant request. It only executes when the context is switched *after* original
|
||||||
|
// middleware initialization.
|
||||||
|
if (isset($sessionManager->getDrivers()['file'])) {
|
||||||
|
$handler = new FileSessionHandler(
|
||||||
|
$this->app->make('files'),
|
||||||
|
$path,
|
||||||
|
$this->app['config']->get('session.lifetime'),
|
||||||
|
);
|
||||||
|
|
||||||
|
$sessionManager->getDrivers()['file']->setHandler($handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class RedisTenancyBootstrapper implements TenancyBootstrapper
|
||||||
public function bootstrap(Tenant $tenant): void
|
public function bootstrap(Tenant $tenant): void
|
||||||
{
|
{
|
||||||
foreach ($this->prefixedConnections() as $connection) {
|
foreach ($this->prefixedConnections() as $connection) {
|
||||||
$prefix = $this->config['tenancy.redis.prefix_base'] . $tenant->getTenantKey();
|
$prefix = str($this->config['tenancy.redis.prefix'])->replace('%tenant%', (string) $tenant->getTenantKey())->toString();
|
||||||
$client = Redis::connection($connection)->client();
|
$client = Redis::connection($connection)->client();
|
||||||
|
|
||||||
/** @var string $originalPrefix */
|
/** @var string $originalPrefix */
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ class InitializeTenancyByDomain extends IdentificationMiddleware implements Usab
|
||||||
*/
|
*/
|
||||||
public function requestHasTenant(Request $request): bool
|
public function requestHasTenant(Request $request): bool
|
||||||
{
|
{
|
||||||
return ! in_array($this->getDomain($request), config('tenancy.central_domains'));
|
return ! in_array($this->getDomain($request), config('tenancy.identification.central_domains'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDomain(Request $request): string
|
public function getDomain(Request $request): string
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,6 @@ class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
|
||||||
|
|
||||||
protected function isSubdomain(string $hostname): bool
|
protected function isSubdomain(string $hostname): bool
|
||||||
{
|
{
|
||||||
return Str::endsWith($hostname, config('tenancy.central_domains'));
|
return Str::endsWith($hostname, config('tenancy.identification.central_domains'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,9 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
||||||
$isIpAddress = count(array_filter($parts, 'is_numeric')) === count($parts);
|
$isIpAddress = count(array_filter($parts, 'is_numeric')) === count($parts);
|
||||||
|
|
||||||
// If we're on localhost or an IP address, then we're not visiting a subdomain.
|
// If we're on localhost or an IP address, then we're not visiting a subdomain.
|
||||||
$isACentralDomain = in_array($hostname, config('tenancy.central_domains'), true);
|
$isACentralDomain = in_array($hostname, config('tenancy.identification.central_domains'), true);
|
||||||
$notADomain = $isLocalhost || $isIpAddress;
|
$notADomain = $isLocalhost || $isIpAddress;
|
||||||
$thirdPartyDomain = ! Str::endsWith($hostname, config('tenancy.central_domains'));
|
$thirdPartyDomain = ! Str::endsWith($hostname, config('tenancy.identification.central_domains'));
|
||||||
|
|
||||||
if ($isACentralDomain || $notADomain || $thirdPartyDomain) {
|
if ($isACentralDomain || $notADomain || $thirdPartyDomain) {
|
||||||
return new NotASubdomainException($hostname);
|
return new NotASubdomainException($hostname);
|
||||||
|
|
|
||||||
|
|
@ -51,20 +51,20 @@ class PreventAccessFromUnwantedDomains
|
||||||
protected function accessingTenantRouteFromCentralDomain(Request $request, Route $route): bool
|
protected function accessingTenantRouteFromCentralDomain(Request $request, Route $route): bool
|
||||||
{
|
{
|
||||||
return tenancy()->getRouteMode($route) === RouteMode::TENANT // Current route's middleware context is tenant
|
return tenancy()->getRouteMode($route) === RouteMode::TENANT // Current route's middleware context is tenant
|
||||||
&& $this->isCentralDomain($request); // The request comes from a domain that IS present in the configured `tenancy.central_domains`
|
&& $this->isCentralDomain($request); // The request comes from a domain that IS present in the configured `tenancy.identification.central_domains`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function accessingCentralRouteFromTenantDomain(Request $request, Route $route): bool
|
protected function accessingCentralRouteFromTenantDomain(Request $request, Route $route): bool
|
||||||
{
|
{
|
||||||
return tenancy()->getRouteMode($route) === RouteMode::CENTRAL // Current route's middleware context is central
|
return tenancy()->getRouteMode($route) === RouteMode::CENTRAL // Current route's middleware context is central
|
||||||
&& ! $this->isCentralDomain($request); // The request comes from a domain that ISN'T present in the configured `tenancy.central_domains`
|
&& ! $this->isCentralDomain($request); // The request comes from a domain that ISN'T present in the configured `tenancy.identification.central_domains`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the request's host name is present in the configured `tenancy.central_domains`.
|
* Check if the request's host name is present in the configured `tenancy.identification.central_domains`.
|
||||||
*/
|
*/
|
||||||
protected function isCentralDomain(Request $request): bool
|
protected function isCentralDomain(Request $request): bool
|
||||||
{
|
{
|
||||||
return in_array($request->getHost(), config('tenancy.central_domains'), true);
|
return in_array($request->getHost(), config('tenancy.identification.central_domains'), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ class ScopeSessions
|
||||||
{
|
{
|
||||||
public static string $tenantIdKey = '_tenant_id';
|
public static string $tenantIdKey = '_tenant_id';
|
||||||
|
|
||||||
|
/** @var Closure(Request): mixed */
|
||||||
|
public static Closure|null $onFail = null;
|
||||||
|
|
||||||
/** @return \Illuminate\Http\Response|mixed */
|
/** @return \Illuminate\Http\Response|mixed */
|
||||||
public function handle(Request $request, Closure $next): mixed
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
|
|
@ -23,7 +26,9 @@ class ScopeSessions
|
||||||
$request->session()->put(static::$tenantIdKey, tenant()->getTenantKey());
|
$request->session()->put(static::$tenantIdKey, tenant()->getTenantKey());
|
||||||
} else {
|
} else {
|
||||||
if ($request->session()->get(static::$tenantIdKey) !== tenant()->getTenantKey()) {
|
if ($request->session()->get(static::$tenantIdKey) !== tenant()->getTenantKey()) {
|
||||||
abort(403);
|
return static::$onFail !== null
|
||||||
|
? (static::$onFail)($request)
|
||||||
|
: abort(403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,16 @@ class Tenancy
|
||||||
return array_map('app', $resolve($this->tenant));
|
return array_map('app', $resolve($this->tenant));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a bootstrapper is being used.
|
||||||
|
*
|
||||||
|
* @param class-string<TenancyBootstrapper> $bootstrapper
|
||||||
|
*/
|
||||||
|
public function usingBootstrapper(string $bootstrapper): bool
|
||||||
|
{
|
||||||
|
return in_array($bootstrapper, static::getBootstrappers(), true);
|
||||||
|
}
|
||||||
|
|
||||||
public static function query(): Builder
|
public static function query(): Builder
|
||||||
{
|
{
|
||||||
return static::model()->query();
|
return static::model()->query();
|
||||||
|
|
|
||||||
|
|
@ -1,916 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Stancl\Tenancy\Enums\Context;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\URL;
|
|
||||||
use Stancl\JobPipeline\JobPipeline;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Support\Facades\Redis;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
|
||||||
use Stancl\Tenancy\Jobs\CreateDatabase;
|
|
||||||
use Stancl\Tenancy\Events\TenantCreated;
|
|
||||||
use Stancl\Tenancy\Events\TenantDeleted;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Stancl\Tenancy\Events\DeletingTenant;
|
|
||||||
use Illuminate\Filesystem\FilesystemAdapter;
|
|
||||||
use Illuminate\Broadcasting\BroadcastManager;
|
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
|
||||||
use Illuminate\Contracts\Routing\UrlGenerator;
|
|
||||||
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
|
|
||||||
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
|
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
|
||||||
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
|
||||||
use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
|
|
||||||
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
|
||||||
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
|
||||||
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper;
|
|
||||||
use Stancl\Tenancy\Overrides\TenancyBroadcastManager;
|
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
|
||||||
use Illuminate\Routing\Exceptions\UrlGenerationException;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\MailConfigBootstrapper;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
|
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
|
||||||
use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteBootstrapper;
|
|
||||||
|
|
||||||
// todo refactor this file -- too much stuff happening here
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
$this->mockConsoleOutput = false;
|
|
||||||
|
|
||||||
config([
|
|
||||||
'cache.default' => 'redis',
|
|
||||||
'tenancy.cache.stores' => ['redis'],
|
|
||||||
]);
|
|
||||||
// Reset static properties of classes used in this test file to their default values
|
|
||||||
BroadcastingConfigBootstrapper::$credentialsMap = [];
|
|
||||||
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
|
|
||||||
RootUrlBootstrapper::$rootUrlOverride = null;
|
|
||||||
|
|
||||||
Event::listen(
|
|
||||||
TenantCreated::class,
|
|
||||||
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
|
||||||
return $event->tenant;
|
|
||||||
})->toListener()
|
|
||||||
);
|
|
||||||
|
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
|
||||||
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
// Reset static properties of classes used in this test file to their default values
|
|
||||||
RootUrlBootstrapper::$rootUrlOverride = null;
|
|
||||||
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
|
|
||||||
BroadcastingConfigBootstrapper::$credentialsMap = [];
|
|
||||||
TenancyUrlGenerator::$prefixRouteNames = false;
|
|
||||||
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('database data is separated', function () {
|
|
||||||
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
|
|
||||||
pest()->artisan('tenants:migrate');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
// Create Foo user
|
|
||||||
DB::table('users')->insert(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
|
|
||||||
expect(DB::table('users')->get())->toHaveCount(1);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
|
|
||||||
// Assert Foo user is not in this DB
|
|
||||||
expect(DB::table('users')->get())->toHaveCount(0);
|
|
||||||
// Create Bar user
|
|
||||||
DB::table('users')->insert(['name' => 'Bar', 'email' => 'bar@bar.com', 'password' => 'secret']);
|
|
||||||
expect(DB::table('users')->get())->toHaveCount(1);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
// Assert Bar user is not in this DB
|
|
||||||
expect(DB::table('users')->get())->toHaveCount(1);
|
|
||||||
expect(DB::table('users')->first()->name)->toBe('Foo');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cache data is separated', function (string $bootstrapper) {
|
|
||||||
config([
|
|
||||||
'tenancy.bootstrappers' => [$bootstrapper],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
|
|
||||||
cache()->set('foo', 'central');
|
|
||||||
expect(Cache::get('foo'))->toBe('central');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
// Assert central cache doesn't leak to tenant context
|
|
||||||
expect(Cache::has('foo'))->toBeFalse();
|
|
||||||
|
|
||||||
cache()->set('foo', 'bar');
|
|
||||||
expect(Cache::get('foo'))->toBe('bar');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
|
|
||||||
// Assert one tenant's data doesn't leak to another tenant
|
|
||||||
expect(Cache::has('foo'))->toBeFalse();
|
|
||||||
|
|
||||||
cache()->set('foo', 'xyz');
|
|
||||||
expect(Cache::get('foo'))->toBe('xyz');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
// Asset data didn't leak to original tenant
|
|
||||||
expect(Cache::get('foo'))->toBe('bar');
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
// Asset central is still the same
|
|
||||||
expect(Cache::get('foo'))->toBe('central');
|
|
||||||
})->with([
|
|
||||||
CacheTagsBootstrapper::class,
|
|
||||||
CacheTenancyBootstrapper::class,
|
|
||||||
]);
|
|
||||||
|
|
||||||
test('redis data is separated', function () {
|
|
||||||
config(['tenancy.bootstrappers' => [
|
|
||||||
RedisTenancyBootstrapper::class,
|
|
||||||
]]);
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
Redis::set('foo', 'bar');
|
|
||||||
expect(Redis::get('foo'))->toBe('bar');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
expect(Redis::get('foo'))->toBe(null);
|
|
||||||
Redis::set('foo', 'xyz');
|
|
||||||
Redis::set('abc', 'def');
|
|
||||||
expect(Redis::get('foo'))->toBe('xyz');
|
|
||||||
expect(Redis::get('abc'))->toBe('def');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
expect(Redis::get('foo'))->toBe('bar');
|
|
||||||
expect(Redis::get('abc'))->toBe(null);
|
|
||||||
|
|
||||||
$tenant3 = Tenant::create();
|
|
||||||
tenancy()->initialize($tenant3);
|
|
||||||
expect(Redis::get('foo'))->toBe(null);
|
|
||||||
expect(Redis::get('abc'))->toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('filesystem data is separated', function () {
|
|
||||||
config(['tenancy.bootstrappers' => [
|
|
||||||
FilesystemTenancyBootstrapper::class,
|
|
||||||
]]);
|
|
||||||
|
|
||||||
$old_storage_path = storage_path();
|
|
||||||
$old_storage_facade_roots = [];
|
|
||||||
foreach (config('tenancy.filesystem.disks') as $disk) {
|
|
||||||
$old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root");
|
|
||||||
}
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
Storage::disk('public')->put('foo', 'bar');
|
|
||||||
expect(Storage::disk('public')->get('foo'))->toBe('bar');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
expect(Storage::disk('public')->exists('foo'))->toBeFalse();
|
|
||||||
Storage::disk('public')->put('foo', 'xyz');
|
|
||||||
Storage::disk('public')->put('abc', 'def');
|
|
||||||
expect(Storage::disk('public')->get('foo'))->toBe('xyz');
|
|
||||||
expect(Storage::disk('public')->get('abc'))->toBe('def');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
expect(Storage::disk('public')->get('foo'))->toBe('bar');
|
|
||||||
expect(Storage::disk('public')->exists('abc'))->toBeFalse();
|
|
||||||
|
|
||||||
$tenant3 = Tenant::create();
|
|
||||||
tenancy()->initialize($tenant3);
|
|
||||||
expect(Storage::disk('public')->exists('foo'))->toBeFalse();
|
|
||||||
expect(Storage::disk('public')->exists('abc'))->toBeFalse();
|
|
||||||
|
|
||||||
$expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base
|
|
||||||
|
|
||||||
// Check that disk prefixes respect the root_override logic
|
|
||||||
expect(getDiskPrefix('local'))->toBe($expected_storage_path . '/app/');
|
|
||||||
expect(getDiskPrefix('public'))->toBe($expected_storage_path . '/app/public/');
|
|
||||||
pest()->assertSame('tenant' . tenant('id') . '/', getDiskPrefix('s3'), '/');
|
|
||||||
|
|
||||||
// Check suffixing logic
|
|
||||||
$new_storage_path = storage_path();
|
|
||||||
expect($new_storage_path)->toEqual($expected_storage_path);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function () {
|
|
||||||
config([
|
|
||||||
'tenancy.bootstrappers' => [
|
|
||||||
FilesystemTenancyBootstrapper::class,
|
|
||||||
],
|
|
||||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
|
||||||
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
$tenant1StorageUrl = 'http://localhost/public-' . $tenant1->getKey().'/';
|
|
||||||
$tenant2StorageUrl = 'http://localhost/public-' . $tenant2->getKey().'/';
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
$this->assertEquals(
|
|
||||||
$tenant1StorageUrl,
|
|
||||||
Storage::disk('public')->url('')
|
|
||||||
);
|
|
||||||
|
|
||||||
Storage::disk('public')->put($tenant1FileName = 'tenant1.txt', 'text');
|
|
||||||
|
|
||||||
$this->assertEquals(
|
|
||||||
$tenant1StorageUrl . $tenant1FileName,
|
|
||||||
Storage::disk('public')->url($tenant1FileName)
|
|
||||||
);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
|
|
||||||
$this->assertEquals(
|
|
||||||
$tenant2StorageUrl,
|
|
||||||
Storage::disk('public')->url('')
|
|
||||||
);
|
|
||||||
|
|
||||||
Storage::disk('public')->put($tenant2FileName = 'tenant2.txt', 'text');
|
|
||||||
|
|
||||||
$this->assertEquals(
|
|
||||||
$tenant2StorageUrl . $tenant2FileName,
|
|
||||||
Storage::disk('public')->url($tenant2FileName)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('files can get fetched using the storage url', function() {
|
|
||||||
config([
|
|
||||||
'tenancy.bootstrappers' => [
|
|
||||||
FilesystemTenancyBootstrapper::class,
|
|
||||||
],
|
|
||||||
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
|
||||||
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
|
|
||||||
pest()->artisan('tenants:link');
|
|
||||||
|
|
||||||
// First tenant
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
Storage::disk('public')->put($tenantFileName = 'tenant1.txt', $tenantKey = $tenant1->getTenantKey());
|
|
||||||
|
|
||||||
$url = Storage::disk('public')->url($tenantFileName);
|
|
||||||
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant%', $tenantKey);
|
|
||||||
$hostname = Str::of($url)->before($tenantDiskName);
|
|
||||||
$parsedUrl = Str::of($url)->after($hostname);
|
|
||||||
|
|
||||||
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
|
||||||
|
|
||||||
// Second tenant
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
Storage::disk('public')->put($tenantFileName = 'tenant2.txt', $tenantKey = $tenant2->getTenantKey());
|
|
||||||
|
|
||||||
$url = Storage::disk('public')->url($tenantFileName);
|
|
||||||
$tenantDiskName = Str::of(config('tenancy.filesystem.url_override.public'))->replace('%tenant%', $tenantKey);
|
|
||||||
$hostname = Str::of($url)->before($tenantDiskName);
|
|
||||||
$parsedUrl = Str::of($url)->after($hostname);
|
|
||||||
|
|
||||||
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
|
||||||
|
|
||||||
// Central
|
|
||||||
tenancy()->end();
|
|
||||||
Storage::disk('public')->put($centralFileName = 'central.txt', $centralFileContent = 'central');
|
|
||||||
|
|
||||||
pest()->artisan('storage:link');
|
|
||||||
$url = Storage::disk('public')->url($centralFileName);
|
|
||||||
|
|
||||||
expect(file_get_contents(public_path($url)))->toBe($centralFileContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('storage_path helper does not change if suffix_storage_path is off', function() {
|
|
||||||
$originalStoragePath = storage_path();
|
|
||||||
|
|
||||||
// todo@tests https://github.com/tenancy-for-laravel/v4/pull/44#issue-2228530362
|
|
||||||
|
|
||||||
config([
|
|
||||||
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
|
||||||
'tenancy.filesystem.suffix_storage_path' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
tenancy()->initialize(Tenant::create());
|
|
||||||
|
|
||||||
$this->assertEquals($originalStoragePath, storage_path());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('links to storage disks with a configured root are suffixed if not overridden', function() {
|
|
||||||
config([
|
|
||||||
'filesystems.disks.public.root' => 'http://sample-s3-url.com/my-app',
|
|
||||||
'tenancy.bootstrappers' => [
|
|
||||||
FilesystemTenancyBootstrapper::class,
|
|
||||||
],
|
|
||||||
'tenancy.filesystem.root_override.public' => null,
|
|
||||||
'tenancy.filesystem.url_override.public' => null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
|
|
||||||
$expectedStoragePath = storage_path() . '/tenant' . $tenant->getTenantKey(); // /tenant = suffix base
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
// Check suffixing logic
|
|
||||||
expect(storage_path())->toEqual($expectedStoragePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('create and delete storage symlinks jobs work', function() {
|
|
||||||
Event::listen(
|
|
||||||
TenantCreated::class,
|
|
||||||
JobPipeline::make([CreateStorageSymlinks::class])->send(function (TenantCreated $event) {
|
|
||||||
return $event->tenant;
|
|
||||||
})->toListener()
|
|
||||||
);
|
|
||||||
|
|
||||||
Event::listen(
|
|
||||||
TenantDeleted::class,
|
|
||||||
JobPipeline::make([RemoveStorageSymlinks::class])->send(function (TenantDeleted $event) {
|
|
||||||
return $event->tenant;
|
|
||||||
})->toListener()
|
|
||||||
);
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
$tenantKey = $tenant->getTenantKey();
|
|
||||||
|
|
||||||
$this->assertDirectoryExists(storage_path("app/public"));
|
|
||||||
$this->assertEquals(storage_path("app/public/"), readlink(public_path("public-$tenantKey")));
|
|
||||||
|
|
||||||
$tenant->delete();
|
|
||||||
|
|
||||||
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('local storage public urls are generated correctly', function() {
|
|
||||||
Event::listen(DeletingTenant::class, DeleteTenantStorage::class);
|
|
||||||
|
|
||||||
tenancy()->initialize(Tenant::create());
|
|
||||||
$tenantStoragePath = storage_path();
|
|
||||||
|
|
||||||
Storage::fake('test');
|
|
||||||
|
|
||||||
expect(File::isDirectory($tenantStoragePath))->toBeTrue();
|
|
||||||
|
|
||||||
Storage::put('test.txt', 'testing file');
|
|
||||||
|
|
||||||
tenant()->delete();
|
|
||||||
|
|
||||||
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('BroadcastingConfigBootstrapper binds TenancyBroadcastManager to BroadcastManager and reverts the binding when tenancy is ended', function() {
|
|
||||||
config(['tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class]]);
|
|
||||||
|
|
||||||
expect(app(BroadcastManager::class))->toBeInstanceOf(BroadcastManager::class);
|
|
||||||
|
|
||||||
tenancy()->initialize(Tenant::create());
|
|
||||||
|
|
||||||
expect(app(BroadcastManager::class))->toBeInstanceOf(TenancyBroadcastManager::class);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
expect(app(BroadcastManager::class))->toBeInstanceOf(BroadcastManager::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('BroadcastingConfigBootstrapper maps tenant broadcaster credentials to config as specified in the $credentialsMap property and reverts the config after ending tenancy', function() {
|
|
||||||
config([
|
|
||||||
'broadcasting.connections.testing.driver' => 'testing',
|
|
||||||
'broadcasting.connections.testing.message' => $defaultMessage = 'default',
|
|
||||||
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
|
||||||
]);
|
|
||||||
|
|
||||||
BroadcastingConfigBootstrapper::$credentialsMap = [
|
|
||||||
'broadcasting.connections.testing.message' => 'testing_broadcaster_message',
|
|
||||||
];
|
|
||||||
|
|
||||||
$tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']);
|
|
||||||
$tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
expect(array_key_exists('testing_broadcaster_message', tenant()->getAttributes()))->toBeTrue();
|
|
||||||
expect(config('broadcasting.connections.testing.message'))->toBe($tenantMessage);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
|
|
||||||
expect(config('broadcasting.connections.testing.message'))->toBe($secondTenantMessage);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
expect(config('broadcasting.connections.testing.message'))->toBe($defaultMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('BroadcastingConfigBootstrapper makes the app use broadcasters with the correct credentials', function() {
|
|
||||||
config([
|
|
||||||
'broadcasting.default' => 'testing',
|
|
||||||
'broadcasting.connections.testing.driver' => 'testing',
|
|
||||||
'broadcasting.connections.testing.message' => $defaultMessage = 'default',
|
|
||||||
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
|
||||||
]);
|
|
||||||
|
|
||||||
TenancyBroadcastManager::$tenantBroadcasters[] = 'testing';
|
|
||||||
BroadcastingConfigBootstrapper::$credentialsMap = [
|
|
||||||
'broadcasting.connections.testing.message' => 'testing_broadcaster_message',
|
|
||||||
];
|
|
||||||
|
|
||||||
$registerTestingBroadcaster = fn() => app(BroadcastManager::class)->extend('testing', fn($app, $config) => new TestingBroadcaster($config['message']));
|
|
||||||
|
|
||||||
$registerTestingBroadcaster();
|
|
||||||
|
|
||||||
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage);
|
|
||||||
|
|
||||||
$tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']);
|
|
||||||
$tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
$registerTestingBroadcaster();
|
|
||||||
|
|
||||||
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($tenantMessage);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
$registerTestingBroadcaster();
|
|
||||||
|
|
||||||
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($secondTenantMessage);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
$registerTestingBroadcaster();
|
|
||||||
|
|
||||||
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() {
|
|
||||||
MailConfigBootstrapper::$credentialsMap = [
|
|
||||||
'mail.mailers.smtp.username' => 'smtp_username',
|
|
||||||
'mail.mailers.smtp.password' => 'smtp_password'
|
|
||||||
];
|
|
||||||
|
|
||||||
config([
|
|
||||||
'mail.default' => 'smtp',
|
|
||||||
'mail.mailers.smtp.username' => $defaultUsername = 'default username',
|
|
||||||
'mail.mailers.smtp.password' => 'no password',
|
|
||||||
'tenancy.bootstrappers' => [MailConfigBootstrapper::class],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant = Tenant::create(['smtp_password' => $password = 'testing password']);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
expect(array_key_exists('smtp_password', tenant()->getAttributes()))->toBeTrue();
|
|
||||||
expect(array_key_exists('smtp_host', tenant()->getAttributes()))->toBeFalse();
|
|
||||||
expect(config('mail.mailers.smtp.username'))->toBe($defaultUsername);
|
|
||||||
expect(config('mail.mailers.smtp.password'))->toBe(tenant()->smtp_password);
|
|
||||||
|
|
||||||
// Assert that the current mailer uses tenant's smtp_password
|
|
||||||
assertMailerTransportUsesPassword($password);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('MailTenancyBootstrapper reverts the config and mailer credentials to default when tenancy ends', function() {
|
|
||||||
MailConfigBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
|
||||||
config([
|
|
||||||
'mail.default' => 'smtp',
|
|
||||||
'mail.mailers.smtp.password' => $defaultPassword = 'no password',
|
|
||||||
'tenancy.bootstrappers' => [MailConfigBootstrapper::class],
|
|
||||||
]);
|
|
||||||
|
|
||||||
tenancy()->initialize(Tenant::create(['smtp_password' => $tenantPassword = 'testing password']));
|
|
||||||
|
|
||||||
expect(config('mail.mailers.smtp.password'))->toBe($tenantPassword);
|
|
||||||
|
|
||||||
assertMailerTransportUsesPassword($tenantPassword);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
expect(config('mail.mailers.smtp.password'))->toBe($defaultPassword);
|
|
||||||
|
|
||||||
// Assert that the current mailer uses the default SMTP password
|
|
||||||
assertMailerTransportUsesPassword($defaultPassword);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one after tenancy ends', function() {
|
|
||||||
config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]);
|
|
||||||
|
|
||||||
Route::group([
|
|
||||||
'middleware' => InitializeTenancyBySubdomain::class,
|
|
||||||
], function () {
|
|
||||||
Route::get('/', function () {
|
|
||||||
return true;
|
|
||||||
})->name('home');
|
|
||||||
});
|
|
||||||
|
|
||||||
$baseUrl = url(route('home'));
|
|
||||||
config(['app.url' => $baseUrl]);
|
|
||||||
|
|
||||||
$rootUrlOverride = function (Tenant $tenant) use ($baseUrl) {
|
|
||||||
$scheme = str($baseUrl)->before('://');
|
|
||||||
$hostname = str($baseUrl)->after($scheme . '://');
|
|
||||||
|
|
||||||
return $scheme . '://' . $tenant->getTenantKey() . '.' . $hostname;
|
|
||||||
};
|
|
||||||
|
|
||||||
RootUrlBootstrapper::$rootUrlOverride = $rootUrlOverride;
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenantUrl = $rootUrlOverride($tenant);
|
|
||||||
|
|
||||||
expect($tenantUrl)->not()->toBe($baseUrl);
|
|
||||||
|
|
||||||
expect(url(route('home')))->toBe($baseUrl);
|
|
||||||
expect(URL::to('/'))->toBe($baseUrl);
|
|
||||||
expect(config('app.url'))->toBe($baseUrl);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
expect(url(route('home')))->toBe($tenantUrl);
|
|
||||||
expect(URL::to('/'))->toBe($tenantUrl);
|
|
||||||
expect(config('app.url'))->toBe($tenantUrl);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
expect(url(route('home')))->toBe($baseUrl);
|
|
||||||
expect(URL::to('/'))->toBe($baseUrl);
|
|
||||||
expect(config('app.url'))->toBe($baseUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('url binding tenancy bootstrapper swaps the url generator instance correctly', function() {
|
|
||||||
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
|
||||||
|
|
||||||
tenancy()->initialize(Tenant::create());
|
|
||||||
expect(app('url'))->toBeInstanceOf(TenancyUrlGenerator::class);
|
|
||||||
expect(url())->toBeInstanceOf(TenancyUrlGenerator::class);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
expect(app('url'))->toBeInstanceOf(UrlGenerator::class)
|
|
||||||
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
|
|
||||||
expect(url())->toBeInstanceOf(UrlGenerator::class)
|
|
||||||
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('url generator bootstrapper can prefix route names passed to the route helper', function() {
|
|
||||||
Route::get('/central/home', fn () => route('home'))->name('home');
|
|
||||||
// Tenant route name prefix is 'tenant.' by default
|
|
||||||
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]);
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenantKey = $tenant->getTenantKey();
|
|
||||||
$centralRouteUrl = route('home');
|
|
||||||
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenantKey]);
|
|
||||||
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
|
|
||||||
|
|
||||||
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
// Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false
|
|
||||||
expect(route('home'))->not()->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);
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// The 'tenant.home' route name doesn't get prefixed because it is already prefixed with 'tenant.'
|
|
||||||
// Also, the route receives the tenant parameter automatically
|
|
||||||
expect(route('tenant.home'))->toBe($tenantRouteUrl);
|
|
||||||
|
|
||||||
// Ending tenancy reverts route() behavior changes
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
expect(route('home'))->toBe($centralRouteUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () {
|
|
||||||
$tenantParameterName = PathTenantResolver::tenantParameterName();
|
|
||||||
|
|
||||||
Route::get('/central/home', fn () => route('home'))->name('home');
|
|
||||||
// Tenant route name prefix is 'tenant.' by default
|
|
||||||
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]);
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$centralRouteUrl = route('home');
|
|
||||||
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenant->getTenantKey()]);
|
|
||||||
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
|
||||||
|
|
||||||
TenancyUrlGenerator::$prefixRouteNames = true;
|
|
||||||
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
// The $bypassParameter parameter ('central' by default) can bypass the route name prefixing
|
|
||||||
// When the bypass parameter is true, the generated route URL points to the route named 'home'
|
|
||||||
expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl)
|
|
||||||
// Bypass parameter prevents passing the tenant parameter directly
|
|
||||||
->not()->toContain($tenantParameterName . '=')
|
|
||||||
// Bypass parameter gets removed from the generated URL automatically
|
|
||||||
->not()->toContain('bypassParameter');
|
|
||||||
|
|
||||||
// When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home')
|
|
||||||
expect(route('home', ['bypassParameter' => false]))->toBe($tenantRouteUrl)
|
|
||||||
->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);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fortify route tenancy bootstrapper updates fortify config correctly', function() {
|
|
||||||
config(['tenancy.bootstrappers' => [FortifyRouteBootstrapper::class]]);
|
|
||||||
|
|
||||||
$originalFortifyHome = config('fortify.home');
|
|
||||||
$originalFortifyRedirects = config('fortify.redirects');
|
|
||||||
|
|
||||||
Route::get('/home', function () {
|
|
||||||
return true;
|
|
||||||
})->name($homeRouteName = 'home');
|
|
||||||
|
|
||||||
Route::get('/{tenant}/home', function () {
|
|
||||||
return true;
|
|
||||||
})->name($pathIdHomeRouteName = 'tenant.home');
|
|
||||||
|
|
||||||
Route::get('/welcome', function () {
|
|
||||||
return true;
|
|
||||||
})->name($welcomeRouteName = 'welcome');
|
|
||||||
|
|
||||||
Route::get('/{tenant}/welcome', function () {
|
|
||||||
return true;
|
|
||||||
})->name($pathIdWelcomeRouteName = 'path.welcome');
|
|
||||||
|
|
||||||
FortifyRouteBootstrapper::$fortifyHome = $homeRouteName;
|
|
||||||
|
|
||||||
// Make login redirect to the central welcome route
|
|
||||||
FortifyRouteBootstrapper::$fortifyRedirectMap['login'] = [
|
|
||||||
'route_name' => $welcomeRouteName,
|
|
||||||
'context' => Context::CENTRAL,
|
|
||||||
];
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
expect(config('fortify.home'))->toBe($originalFortifyHome);
|
|
||||||
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
|
|
||||||
|
|
||||||
// Making a route's context will pass the tenant parameter to the route
|
|
||||||
FortifyRouteBootstrapper::$fortifyRedirectMap['login']['context'] = Context::TENANT;
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]);
|
|
||||||
|
|
||||||
// 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()->initialize($tenant);
|
|
||||||
|
|
||||||
expect(config('fortify.home'))->toBe("http://localhost/{$tenant->getTenantKey()}/home");
|
|
||||||
expect(config('fortify.redirects'))->toEqual(['login' => "http://localhost/{$tenant->getTenantKey()}/welcome"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', function (string|null $databaseUrl) {
|
|
||||||
if ($databaseUrl) {
|
|
||||||
config(['database.connections.central.url' => $databaseUrl]);
|
|
||||||
|
|
||||||
pest()->expectException(Exception::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
|
|
||||||
pest()->artisan('tenants:migrate');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
expect(true)->toBe(true);
|
|
||||||
})->with(['abc.us-east-1.rds.amazonaws.com', null]);
|
|
||||||
|
|
||||||
test('BroadcastChannelPrefixBootstrapper prefixes the channels events are broadcast on while tenancy is initialized', function() {
|
|
||||||
config([
|
|
||||||
'broadcasting.default' => $driver = 'testing',
|
|
||||||
'broadcasting.connections.testing.driver' => $driver,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Use custom broadcaster
|
|
||||||
app(BroadcastManager::class)->extend($driver, fn () => new TestingBroadcaster('original broadcaster'));
|
|
||||||
|
|
||||||
config(['tenancy.bootstrappers' => [BroadcastChannelPrefixBootstrapper::class, DatabaseTenancyBootstrapper::class]]);
|
|
||||||
|
|
||||||
Schema::create('users', function (Blueprint $table) {
|
|
||||||
$table->increments('id');
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('email')->unique();
|
|
||||||
$table->string('password');
|
|
||||||
$table->rememberToken();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
universal_channel('users.{userId}', function ($user, $userId) {
|
|
||||||
return User::find($userId)->is($user);
|
|
||||||
});
|
|
||||||
|
|
||||||
$broadcaster = app(BroadcastManager::class)->driver();
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
|
|
||||||
pest()->artisan('tenants:migrate');
|
|
||||||
|
|
||||||
// Set up the 'testing' broadcaster override
|
|
||||||
// Identical to the default Pusher override (BroadcastChannelPrefixBootstrapper::pusher())
|
|
||||||
// Except for the parent class (TestingBroadcaster instead of PusherBroadcaster)
|
|
||||||
BroadcastChannelPrefixBootstrapper::$broadcasterOverrides['testing'] = function (BroadcastManager $broadcastManager) {
|
|
||||||
$broadcastManager->extend('testing', function ($app, $config) {
|
|
||||||
return new class('tenant broadcaster') extends TestingBroadcaster {
|
|
||||||
protected function formatChannels(array $channels)
|
|
||||||
{
|
|
||||||
$formatChannel = function (string $channel) {
|
|
||||||
$prefixes = ['private-', 'presence-'];
|
|
||||||
$defaultPrefix = '';
|
|
||||||
|
|
||||||
foreach ($prefixes as $prefix) {
|
|
||||||
if (str($channel)->startsWith($prefix)) {
|
|
||||||
$defaultPrefix = $prefix;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip prefixing channels flagged with the global channel prefix
|
|
||||||
if (! str($channel)->startsWith('global__')) {
|
|
||||||
$channel = str($channel)->after($defaultPrefix)->prepend($defaultPrefix . tenant()->getTenantKey() . '.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (string) $channel;
|
|
||||||
};
|
|
||||||
|
|
||||||
return array_map($formatChannel, parent::formatChannels($channels));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
auth()->login($user = User::create(['name' => 'central', 'email' => 'test@central.cz', 'password' => 'test']));
|
|
||||||
|
|
||||||
// The channel names used for testing the formatChannels() method (not real channels)
|
|
||||||
$channelNames = [
|
|
||||||
'channel',
|
|
||||||
'global__channel', // Channels prefixed with 'global__' shouldn't get prefixed with the tenant key
|
|
||||||
'private-user.' . $user->id,
|
|
||||||
];
|
|
||||||
|
|
||||||
// formatChannels doesn't prefix the channel names until tenancy is initialized
|
|
||||||
expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqual($channelNames);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
$tenantBroadcaster = app(BroadcastManager::class)->driver();
|
|
||||||
|
|
||||||
auth()->login($tenantUser = User::create(['name' => 'tenant', 'email' => 'test@tenant.cz', 'password' => 'test']));
|
|
||||||
|
|
||||||
// The current (tenant) broadcaster isn't the same as the central one
|
|
||||||
expect($tenantBroadcaster->message)->not()->toBe($broadcaster->message);
|
|
||||||
// Tenant broadcaster has the same channels as the central broadcaster
|
|
||||||
expect($tenantBroadcaster->getChannels())->toEqualCanonicalizing($broadcaster->getChannels());
|
|
||||||
// formatChannels prefixes the channel names now
|
|
||||||
expect(invade($tenantBroadcaster)->formatChannels($channelNames))->toEqualCanonicalizing([
|
|
||||||
'global__channel',
|
|
||||||
$tenant->getTenantKey() . '.channel',
|
|
||||||
'private-' . $tenant->getTenantKey() . '.user.' . $tenantUser->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Initialize another tenant
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
|
|
||||||
auth()->login($tenantUser = User::create(['name' => 'tenant', 'email' => 'test2@tenant.cz', 'password' => 'test']));
|
|
||||||
|
|
||||||
// formatChannels prefixes channels with the second tenant's key now
|
|
||||||
expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqualCanonicalizing([
|
|
||||||
'global__channel',
|
|
||||||
$tenant2->getTenantKey() . '.channel',
|
|
||||||
'private-' . $tenant2->getTenantKey() . '.user.' . $tenantUser->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// The bootstrapper reverts to the tenant context – the channel names won't be prefixed anymore
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
// The current broadcaster is the same as the central one again
|
|
||||||
expect(app(BroadcastManager::class)->driver())->toBe($broadcaster);
|
|
||||||
expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqual($channelNames);
|
|
||||||
});
|
|
||||||
|
|
||||||
function getDiskPrefix(string $disk): string
|
|
||||||
{
|
|
||||||
/** @var FilesystemAdapter $disk */
|
|
||||||
$disk = Storage::disk($disk);
|
|
||||||
$adapter = $disk->getAdapter();
|
|
||||||
$prefix = invade(invade($adapter)->prefixer)->prefix;
|
|
||||||
|
|
||||||
return $prefix;
|
|
||||||
}
|
|
||||||
248
tests/Bootstrappers/BootstrapperTest.php
Normal file
248
tests/Bootstrappers/BootstrapperTest.php
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Filesystem\FilesystemAdapter;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->mockConsoleOutput = false;
|
||||||
|
|
||||||
|
config([
|
||||||
|
'cache.default' => 'redis',
|
||||||
|
'tenancy.cache.stores' => ['redis'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
TenantCreated::class,
|
||||||
|
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener()
|
||||||
|
);
|
||||||
|
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('database data is separated', function () {
|
||||||
|
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
pest()->artisan('tenants:migrate');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
// Create Foo user
|
||||||
|
DB::table('users')->insert(['name' => 'Foo', 'email' => 'foo@bar.com', 'password' => 'secret']);
|
||||||
|
expect(DB::table('users')->get())->toHaveCount(1);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
|
||||||
|
// Assert Foo user is not in this DB
|
||||||
|
expect(DB::table('users')->get())->toHaveCount(0);
|
||||||
|
// Create Bar user
|
||||||
|
DB::table('users')->insert(['name' => 'Bar', 'email' => 'bar@bar.com', 'password' => 'secret']);
|
||||||
|
expect(DB::table('users')->get())->toHaveCount(1);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
// Assert Bar user is not in this DB
|
||||||
|
expect(DB::table('users')->get())->toHaveCount(1);
|
||||||
|
expect(DB::table('users')->first()->name)->toBe('Foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cache data is separated', function (string $store, string $bootstrapper) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [$bootstrapper],
|
||||||
|
'tenancy.cache.stores' => [$store],
|
||||||
|
'cache.default' => $store,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($store === 'database') {
|
||||||
|
config([
|
||||||
|
'cache.stores.database.connection' => 'central',
|
||||||
|
'cache.stores.database.lock_connection' => 'central',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
cache()->set('foo', 'central');
|
||||||
|
expect(Cache::get('foo'))->toBe('central');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
// Assert central cache doesn't leak to tenant context
|
||||||
|
expect(Cache::has('foo'))->toBeFalse();
|
||||||
|
|
||||||
|
cache()->set('foo', 'bar');
|
||||||
|
expect(Cache::get('foo'))->toBe('bar');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
|
||||||
|
// Assert one tenant's data doesn't leak to another tenant
|
||||||
|
expect(Cache::has('foo'))->toBeFalse();
|
||||||
|
|
||||||
|
cache()->set('foo', 'xyz');
|
||||||
|
expect(Cache::get('foo'))->toBe('xyz');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
// Assert data didn't leak to original tenant
|
||||||
|
expect(Cache::get('foo'))->toBe('bar');
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// Assert central is still the same
|
||||||
|
expect(Cache::get('foo'))->toBe('central');
|
||||||
|
})->with([
|
||||||
|
['redis', CacheTagsBootstrapper::class],
|
||||||
|
['memcached', CacheTagsBootstrapper::class],
|
||||||
|
|
||||||
|
['file', FilesystemTenancyBootstrapper::class],
|
||||||
|
|
||||||
|
['redis', CacheTenancyBootstrapper::class],
|
||||||
|
['apc', CacheTenancyBootstrapper::class],
|
||||||
|
['memcached', CacheTenancyBootstrapper::class],
|
||||||
|
['database', CacheTenancyBootstrapper::class],
|
||||||
|
['dynamodb', CacheTenancyBootstrapper::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('redis data is separated', function () {
|
||||||
|
config(['tenancy.bootstrappers' => [
|
||||||
|
RedisTenancyBootstrapper::class,
|
||||||
|
]]);
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
Redis::set('foo', 'bar');
|
||||||
|
expect(Redis::get('foo'))->toBe('bar');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
expect(Redis::get('foo'))->toBe(null);
|
||||||
|
Redis::set('foo', 'xyz');
|
||||||
|
Redis::set('abc', 'def');
|
||||||
|
expect(Redis::get('foo'))->toBe('xyz');
|
||||||
|
expect(Redis::get('abc'))->toBe('def');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
expect(Redis::get('foo'))->toBe('bar');
|
||||||
|
expect(Redis::get('abc'))->toBe(null);
|
||||||
|
|
||||||
|
$tenant3 = Tenant::create();
|
||||||
|
tenancy()->initialize($tenant3);
|
||||||
|
expect(Redis::get('foo'))->toBe(null);
|
||||||
|
expect(Redis::get('abc'))->toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('filesystem data is separated', function () {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
||||||
|
'session.driver' => 'file',
|
||||||
|
'cache.default' => 'file',
|
||||||
|
'tenancy.cache.stores' => ['file'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$old_storage_path = storage_path();
|
||||||
|
$old_storage_facade_roots = [];
|
||||||
|
foreach (config('tenancy.filesystem.disks') as $disk) {
|
||||||
|
$old_storage_facade_roots[$disk] = config("filesystems.disks.{$disk}.root");
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
Storage::disk('public')->put('foo', 'bar');
|
||||||
|
expect(Storage::disk('public')->get('foo'))->toBe('bar');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
expect(Storage::disk('public')->exists('foo'))->toBeFalse();
|
||||||
|
Storage::disk('public')->put('foo', 'xyz');
|
||||||
|
Storage::disk('public')->put('abc', 'def');
|
||||||
|
expect(Storage::disk('public')->get('foo'))->toBe('xyz');
|
||||||
|
expect(Storage::disk('public')->get('abc'))->toBe('def');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
expect(Storage::disk('public')->get('foo'))->toBe('bar');
|
||||||
|
expect(Storage::disk('public')->exists('abc'))->toBeFalse();
|
||||||
|
|
||||||
|
$tenant3 = Tenant::create();
|
||||||
|
tenancy()->initialize($tenant3);
|
||||||
|
expect(Storage::disk('public')->exists('foo'))->toBeFalse();
|
||||||
|
expect(Storage::disk('public')->exists('abc'))->toBeFalse();
|
||||||
|
|
||||||
|
$expected_storage_path = $old_storage_path . '/tenant' . tenant('id'); // /tenant = suffix base
|
||||||
|
|
||||||
|
// Check that disk prefixes respect the root_override logic
|
||||||
|
expect(getDiskPrefix('local'))->toBe($expected_storage_path . '/app/');
|
||||||
|
expect(getDiskPrefix('public'))->toBe($expected_storage_path . '/app/public/');
|
||||||
|
pest()->assertSame('tenant' . tenant('id') . '/', getDiskPrefix('s3'), '/');
|
||||||
|
|
||||||
|
// Check suffixing logic
|
||||||
|
$new_storage_path = storage_path();
|
||||||
|
expect($new_storage_path)->toEqual($expected_storage_path);
|
||||||
|
|
||||||
|
// Check cache path
|
||||||
|
$cachePath = cache()->store()->getStore()->getDirectory();
|
||||||
|
expect($cachePath)
|
||||||
|
->toBe(config('cache.stores.file.path'))
|
||||||
|
->toBe(storage_path('framework/cache/data'));
|
||||||
|
expect($cachePath)->toContain(tenant('id'));
|
||||||
|
|
||||||
|
// Check session path
|
||||||
|
$sessionPath = invade(app('session')->driver()->getHandler())->path;
|
||||||
|
expect($sessionPath)
|
||||||
|
->toBe(config('session.files'))
|
||||||
|
->toBe(storage_path('framework/sessions'));
|
||||||
|
expect($sessionPath)->toContain(tenant('id'));
|
||||||
|
|
||||||
|
// URL generation is tested separately in FilesystemTenancyBootstrapperTest
|
||||||
|
});
|
||||||
|
|
||||||
|
function getDiskPrefix(string $disk): string
|
||||||
|
{
|
||||||
|
/** @var FilesystemAdapter $disk */
|
||||||
|
$disk = Storage::disk($disk);
|
||||||
|
$adapter = $disk->getAdapter();
|
||||||
|
$prefix = invade(invade($adapter)->prefixer)->prefix;
|
||||||
|
|
||||||
|
return $prefix;
|
||||||
|
}
|
||||||
142
tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php
Normal file
142
tests/Bootstrappers/BroadcastChannelPrefixBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\BroadcastManager;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
TenantCreated::class,
|
||||||
|
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BroadcastChannelPrefixBootstrapper prefixes the channels events are broadcast on while tenancy is initialized', function() {
|
||||||
|
config([
|
||||||
|
'broadcasting.default' => $driver = 'testing',
|
||||||
|
'broadcasting.connections.testing.driver' => $driver,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Use custom broadcaster
|
||||||
|
app(BroadcastManager::class)->extend($driver, fn () => new TestingBroadcaster('original broadcaster'));
|
||||||
|
|
||||||
|
config(['tenancy.bootstrappers' => [BroadcastChannelPrefixBootstrapper::class, DatabaseTenancyBootstrapper::class]]);
|
||||||
|
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->string('password');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
universal_channel('users.{userId}', function ($user, $userId) {
|
||||||
|
return User::find($userId)->is($user);
|
||||||
|
});
|
||||||
|
|
||||||
|
$broadcaster = app(BroadcastManager::class)->driver();
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
pest()->artisan('tenants:migrate');
|
||||||
|
|
||||||
|
// Set up the 'testing' broadcaster override
|
||||||
|
// Identical to the default Pusher override (BroadcastChannelPrefixBootstrapper::pusher())
|
||||||
|
// Except for the parent class (TestingBroadcaster instead of PusherBroadcaster)
|
||||||
|
BroadcastChannelPrefixBootstrapper::$broadcasterOverrides['testing'] = function (BroadcastManager $broadcastManager) {
|
||||||
|
$broadcastManager->extend('testing', function ($app, $config) {
|
||||||
|
return new class('tenant broadcaster') extends TestingBroadcaster {
|
||||||
|
protected function formatChannels(array $channels)
|
||||||
|
{
|
||||||
|
$formatChannel = function (string $channel) {
|
||||||
|
$prefixes = ['private-', 'presence-'];
|
||||||
|
$defaultPrefix = '';
|
||||||
|
|
||||||
|
foreach ($prefixes as $prefix) {
|
||||||
|
if (str($channel)->startsWith($prefix)) {
|
||||||
|
$defaultPrefix = $prefix;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip prefixing channels flagged with the global channel prefix
|
||||||
|
if (! str($channel)->startsWith('global__')) {
|
||||||
|
$channel = str($channel)->after($defaultPrefix)->prepend($defaultPrefix . tenant()->getTenantKey() . '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $channel;
|
||||||
|
};
|
||||||
|
|
||||||
|
return array_map($formatChannel, parent::formatChannels($channels));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
auth()->login($user = User::create(['name' => 'central', 'email' => 'test@central.cz', 'password' => 'test']));
|
||||||
|
|
||||||
|
// The channel names used for testing the formatChannels() method (not real channels)
|
||||||
|
$channelNames = [
|
||||||
|
'channel',
|
||||||
|
'global__channel', // Channels prefixed with 'global__' shouldn't get prefixed with the tenant key
|
||||||
|
'private-user.' . $user->id,
|
||||||
|
];
|
||||||
|
|
||||||
|
// formatChannels doesn't prefix the channel names until tenancy is initialized
|
||||||
|
expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqual($channelNames);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
$tenantBroadcaster = app(BroadcastManager::class)->driver();
|
||||||
|
|
||||||
|
auth()->login($tenantUser = User::create(['name' => 'tenant', 'email' => 'test@tenant.cz', 'password' => 'test']));
|
||||||
|
|
||||||
|
// The current (tenant) broadcaster isn't the same as the central one
|
||||||
|
expect($tenantBroadcaster->message)->not()->toBe($broadcaster->message);
|
||||||
|
// Tenant broadcaster has the same channels as the central broadcaster
|
||||||
|
expect($tenantBroadcaster->getChannels())->toEqualCanonicalizing($broadcaster->getChannels());
|
||||||
|
// formatChannels prefixes the channel names now
|
||||||
|
expect(invade($tenantBroadcaster)->formatChannels($channelNames))->toEqualCanonicalizing([
|
||||||
|
'global__channel',
|
||||||
|
$tenant->getTenantKey() . '.channel',
|
||||||
|
'private-' . $tenant->getTenantKey() . '.user.' . $tenantUser->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Initialize another tenant
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
|
||||||
|
auth()->login($tenantUser = User::create(['name' => 'tenant', 'email' => 'test2@tenant.cz', 'password' => 'test']));
|
||||||
|
|
||||||
|
// formatChannels prefixes channels with the second tenant's key now
|
||||||
|
expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqualCanonicalizing([
|
||||||
|
'global__channel',
|
||||||
|
$tenant2->getTenantKey() . '.channel',
|
||||||
|
'private-' . $tenant2->getTenantKey() . '.user.' . $tenantUser->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// The bootstrapper reverts to the tenant context – the channel names won't be prefixed anymore
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// The current broadcaster is the same as the central one again
|
||||||
|
expect(app(BroadcastManager::class)->driver())->toBe($broadcaster);
|
||||||
|
expect(invade(app(BroadcastManager::class)->driver())->formatChannels($channelNames))->toEqual($channelNames);
|
||||||
|
});
|
||||||
|
|
||||||
105
tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php
Normal file
105
tests/Bootstrappers/BroadcastingConfigBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Broadcasting\BroadcastManager;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\BroadcastingConfigBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use Stancl\Tenancy\Overrides\TenancyBroadcastManager;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\TestingBroadcaster;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
|
||||||
|
BroadcastingConfigBootstrapper::$credentialsMap = [];
|
||||||
|
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
BroadcastingConfigBootstrapper::$credentialsMap = [];
|
||||||
|
TenancyBroadcastManager::$tenantBroadcasters = ['pusher', 'ably'];
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BroadcastingConfigBootstrapper binds TenancyBroadcastManager to BroadcastManager and reverts the binding when tenancy is ended', function() {
|
||||||
|
config(['tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class]]);
|
||||||
|
|
||||||
|
expect(app(BroadcastManager::class))->toBeInstanceOf(BroadcastManager::class);
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
|
||||||
|
expect(app(BroadcastManager::class))->toBeInstanceOf(TenancyBroadcastManager::class);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
expect(app(BroadcastManager::class))->toBeInstanceOf(BroadcastManager::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BroadcastingConfigBootstrapper maps tenant broadcaster credentials to config as specified in the $credentialsMap property and reverts the config after ending tenancy', function() {
|
||||||
|
config([
|
||||||
|
'broadcasting.connections.testing.driver' => 'testing',
|
||||||
|
'broadcasting.connections.testing.message' => $defaultMessage = 'default',
|
||||||
|
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
BroadcastingConfigBootstrapper::$credentialsMap = [
|
||||||
|
'broadcasting.connections.testing.message' => 'testing_broadcaster_message',
|
||||||
|
];
|
||||||
|
|
||||||
|
$tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']);
|
||||||
|
$tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
expect(array_key_exists('testing_broadcaster_message', tenant()->getAttributes()))->toBeTrue();
|
||||||
|
expect(config('broadcasting.connections.testing.message'))->toBe($tenantMessage);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
|
||||||
|
expect(config('broadcasting.connections.testing.message'))->toBe($secondTenantMessage);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
expect(config('broadcasting.connections.testing.message'))->toBe($defaultMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BroadcastingConfigBootstrapper makes the app use broadcasters with the correct credentials', function() {
|
||||||
|
config([
|
||||||
|
'broadcasting.default' => 'testing',
|
||||||
|
'broadcasting.connections.testing.driver' => 'testing',
|
||||||
|
'broadcasting.connections.testing.message' => $defaultMessage = 'default',
|
||||||
|
'tenancy.bootstrappers' => [BroadcastingConfigBootstrapper::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
TenancyBroadcastManager::$tenantBroadcasters[] = 'testing';
|
||||||
|
BroadcastingConfigBootstrapper::$credentialsMap = [
|
||||||
|
'broadcasting.connections.testing.message' => 'testing_broadcaster_message',
|
||||||
|
];
|
||||||
|
|
||||||
|
$registerTestingBroadcaster = fn() => app(BroadcastManager::class)->extend('testing', fn ($app, $config) => new TestingBroadcaster($config['message']));
|
||||||
|
|
||||||
|
$registerTestingBroadcaster();
|
||||||
|
|
||||||
|
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage);
|
||||||
|
|
||||||
|
$tenant = Tenant::create(['testing_broadcaster_message' => $tenantMessage = 'first testing']);
|
||||||
|
$tenant2 = Tenant::create(['testing_broadcaster_message' => $secondTenantMessage = 'second testing']);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
$registerTestingBroadcaster();
|
||||||
|
|
||||||
|
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($tenantMessage);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
$registerTestingBroadcaster();
|
||||||
|
|
||||||
|
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($secondTenantMessage);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
$registerTestingBroadcaster();
|
||||||
|
|
||||||
|
expect(invade(app(BroadcastManager::class)->driver())->message)->toBe($defaultMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -7,11 +7,14 @@ use Illuminate\Support\Facades\Event;
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\CacheTagsBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config(['tenancy.bootstrappers' => [CacheTagsBootstrapper::class]]);
|
config(['tenancy.bootstrappers' => [CacheTagsBootstrapper::class]]);
|
||||||
|
|
||||||
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('default tag is automatically applied', function () {
|
test('default tag is automatically applied', function () {
|
||||||
|
|
@ -48,7 +51,7 @@ test('exception is thrown when more than one argument is passed to tags method',
|
||||||
cache()->tags(1, 2);
|
cache()->tags(1, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tags separate cache well enough', function () {
|
test('tags separate cache properly', function () {
|
||||||
$tenant1 = Tenant::create();
|
$tenant1 = Tenant::create();
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
|
@ -17,9 +17,9 @@ beforeEach(function () {
|
||||||
'tenancy.bootstrappers' => [
|
'tenancy.bootstrappers' => [
|
||||||
CacheTenancyBootstrapper::class
|
CacheTenancyBootstrapper::class
|
||||||
],
|
],
|
||||||
'cache.default' => $cacheDriver = 'redis',
|
'cache.default' => 'redis',
|
||||||
'cache.stores.' . $secondCacheDriver = 'redis2' => config('cache.stores.redis'),
|
'cache.stores.redis2' => config('cache.stores.redis'),
|
||||||
'tenancy.cache.stores' => [$cacheDriver, $secondCacheDriver],
|
'tenancy.cache.stores' => ['redis', 'redis2'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
CacheTenancyBootstrapper::$prefixGenerator = null;
|
CacheTenancyBootstrapper::$prefixGenerator = null;
|
||||||
|
|
@ -34,8 +34,8 @@ afterEach(function () {
|
||||||
|
|
||||||
test('correct cache prefix is used in all contexts', function () {
|
test('correct cache prefix is used in all contexts', function () {
|
||||||
$originalPrefix = config('cache.prefix');
|
$originalPrefix = config('cache.prefix');
|
||||||
$prefixBase = config('tenancy.cache.prefix_base');
|
$prefixFormat = config('tenancy.cache.prefix');
|
||||||
$getDefaultPrefixForTenant = fn (Tenant $tenant) => $originalPrefix . $prefixBase . $tenant->getTenantKey();
|
$getDefaultPrefixForTenant = fn (Tenant $tenant) => $originalPrefix . str($prefixFormat)->replace('%tenant%', $tenant->getTenantKey())->toString();
|
||||||
$bootstrapper = app(CacheTenancyBootstrapper::class);
|
$bootstrapper = app(CacheTenancyBootstrapper::class);
|
||||||
|
|
||||||
$expectCachePrefixToBe = function (string $prefix) {
|
$expectCachePrefixToBe = function (string $prefix) {
|
||||||
|
|
@ -43,7 +43,7 @@ test('correct cache prefix is used in all contexts', function () {
|
||||||
->toBe(app('cache')->getPrefix())
|
->toBe(app('cache')->getPrefix())
|
||||||
->toBe(app('cache.store')->getPrefix())
|
->toBe(app('cache.store')->getPrefix())
|
||||||
->toBe(cache()->getPrefix())
|
->toBe(cache()->getPrefix())
|
||||||
->toBe(cache()->store('redis2')->getPrefix()); // Non-default cache stores specified in $tenantCacheStores are prefixed too
|
->toBe(cache()->store('redis2')->getPrefix());
|
||||||
};
|
};
|
||||||
|
|
||||||
$expectCachePrefixToBe($originalPrefix);
|
$expectCachePrefixToBe($originalPrefix);
|
||||||
|
|
@ -55,13 +55,13 @@ test('correct cache prefix is used in all contexts', function () {
|
||||||
cache()->set('key', 'tenantone-value');
|
cache()->set('key', 'tenantone-value');
|
||||||
$tenantOnePrefix = $getDefaultPrefixForTenant($tenant1);
|
$tenantOnePrefix = $getDefaultPrefixForTenant($tenant1);
|
||||||
$expectCachePrefixToBe($tenantOnePrefix);
|
$expectCachePrefixToBe($tenantOnePrefix);
|
||||||
expect($bootstrapper->generatePrefix($tenant1))->toBe($tenantOnePrefix);
|
expect($bootstrapper->generatePrefix($tenant1, 'redis'))->toBe($tenantOnePrefix);
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
tenancy()->initialize($tenant2);
|
||||||
cache()->set('key', 'tenanttwo-value');
|
cache()->set('key', 'tenanttwo-value');
|
||||||
$tenantTwoPrefix = $getDefaultPrefixForTenant($tenant2);
|
$tenantTwoPrefix = $getDefaultPrefixForTenant($tenant2);
|
||||||
$expectCachePrefixToBe($tenantTwoPrefix);
|
$expectCachePrefixToBe($tenantTwoPrefix);
|
||||||
expect($bootstrapper->generatePrefix($tenant2))->toBe($tenantTwoPrefix);
|
expect($bootstrapper->generatePrefix($tenant2, 'redis'))->toBe($tenantTwoPrefix);
|
||||||
|
|
||||||
// Prefix gets reverted to default after ending tenancy
|
// Prefix gets reverted to default after ending tenancy
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
|
|
@ -132,9 +132,68 @@ test('central cache is persisted', function () {
|
||||||
expect(cache()->get('key2'))->toBeNull();
|
expect(cache()->get('key2'))->toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('only the stores specified in the config get prefixed', function () {
|
||||||
|
// Make sure the currently used store ('redis') is the only store in the config
|
||||||
|
// This means that the 'redis2' store won't be prefixed
|
||||||
|
config(['tenancy.cache.stores' => ['redis']]);
|
||||||
|
|
||||||
|
cache()->store('redis')->put('key', 'central');
|
||||||
|
expect(cache()->store('redis')->get('key'))->toBe('central');
|
||||||
|
// same values -- the stores use the same connection, with the same prefix here
|
||||||
|
expect(cache()->store('redis2')->get('key'))->toBe('central');
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// now the 'redis' store is prefixed, but 'redis2' isn't
|
||||||
|
expect(cache()->store('redis2')->get('key'))->toBe('central');
|
||||||
|
expect(cache()->store('redis')->get('key'))->toBe(null); // central value not leaked to tenant context
|
||||||
|
|
||||||
|
cache()->store('redis')->put('key', 'tenant'); // change the value of the prefixed store
|
||||||
|
expect(cache()->store('redis')->get('key'))->toBe('tenant'); // prefixed store
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
// still central
|
||||||
|
expect(cache()->store('redis2')->get('key'))->toBe('central');
|
||||||
|
expect(cache()->store('redis')->get('key'))->toBe('central');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
expect(cache()->store('redis2')->get('key'))->toBe('central'); // still central
|
||||||
|
expect(cache()->store('redis')->get('key'))->toBe('tenant');
|
||||||
|
cache()->store('redis2')->put('key', 'foo'); // override non-prefixed store value
|
||||||
|
cache()->store('redis')->put('key', 'tenant'); // the connection with the prefix still retains the tenant value
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
// both redis2 and redis should now be 'foo' since they got overridden previously
|
||||||
|
expect(cache()->store('redis2')->get('key'))->toBe('foo');
|
||||||
|
expect(cache()->store('redis')->get('key'))->toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non default stores get prefixed too when specified in the config', function () {
|
||||||
|
config([
|
||||||
|
'cache.default' => 'redis',
|
||||||
|
'tenancy.cache.stores' => ['redis', 'redis2'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$defaultPrefix = cache()->store()->getPrefix();
|
||||||
|
$bootstrapper = app(CacheTenancyBootstrapper::class);
|
||||||
|
|
||||||
|
expect(cache()->store('redis')->getPrefix())->toBe($defaultPrefix);
|
||||||
|
expect(cache()->store('redis2')->getPrefix())->toBe($defaultPrefix);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
expect($bootstrapper->generatePrefix($tenant, 'redis2'))
|
||||||
|
->toBe(cache()->getPrefix())
|
||||||
|
->toBe(cache()->store('redis2')->getPrefix()); // Non-default store
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
});
|
||||||
|
|
||||||
test('cache base prefix is customizable', function () {
|
test('cache base prefix is customizable', function () {
|
||||||
config([
|
config([
|
||||||
'tenancy.cache.prefix_base' => $prefixBase = 'custom_'
|
'tenancy.cache.prefix' => 'custom_%tenant%_'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$originalPrefix = config('cache.prefix');
|
$originalPrefix = config('cache.prefix');
|
||||||
|
|
@ -142,13 +201,35 @@ test('cache base prefix is customizable', function () {
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
expect($originalPrefix . $prefixBase . $tenant1->getTenantKey())
|
expect($originalPrefix . 'custom_' . $tenant1->getTenantKey() . '_')
|
||||||
->toBe(cache()->getPrefix())
|
->toBe(cache()->getPrefix())
|
||||||
->toBe(cache()->store('redis2')->getPrefix()) // Non-default store gets prefixed correctly too
|
->toBe(cache()->store('redis2')->getPrefix())
|
||||||
->toBe(app('cache')->getPrefix())
|
->toBe(app('cache')->getPrefix())
|
||||||
->toBe(app('cache.store')->getPrefix());
|
->toBe(app('cache.store')->getPrefix());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cache store prefix generation can be customized', function() {
|
||||||
|
// Use custom prefix generator
|
||||||
|
CacheTenancyBootstrapper::generatePrefixUsing($customPrefixGenerator = function (Tenant $tenant) {
|
||||||
|
return 'redis_tenant_cache_' . $tenant->getTenantKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(CacheTenancyBootstrapper::$prefixGenerator)->toBe($customPrefixGenerator);
|
||||||
|
expect(app(CacheTenancyBootstrapper::class)->generatePrefix($tenant = Tenant::create(), 'redis'))
|
||||||
|
->toBe($customPrefixGenerator($tenant));
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant = Tenant::create());
|
||||||
|
|
||||||
|
// Expect the 'redis' store to use the prefix generated by the custom generator
|
||||||
|
expect($customPrefixGenerator($tenant))
|
||||||
|
->toBe(cache()->getPrefix())
|
||||||
|
->toBe(cache()->store('redis2')->getPrefix())
|
||||||
|
->toBe(app('cache')->getPrefix())
|
||||||
|
->toBe(app('cache.store')->getPrefix());
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
});
|
||||||
|
|
||||||
test('cache is prefixed correctly when using a repository injected in a singleton', function () {
|
test('cache is prefixed correctly when using a repository injected in a singleton', function () {
|
||||||
$this->app->singleton(CacheService::class);
|
$this->app->singleton(CacheService::class);
|
||||||
|
|
||||||
|
|
@ -183,7 +264,7 @@ test('specific central cache store can be used inside a service', function () {
|
||||||
// Name of the non-default, central cache store that we'll use using cache()->store($cacheStore)
|
// Name of the non-default, central cache store that we'll use using cache()->store($cacheStore)
|
||||||
$cacheStore = 'redis2';
|
$cacheStore = 'redis2';
|
||||||
|
|
||||||
// Service uses the 'redis2' store which is central/not prefixed (not present in PrefixCacheTenancyBootstrapper::$tenantCacheStores)
|
// Service uses the 'redis2' store which is central/not prefixed (not present in tenancy.cache.stores config)
|
||||||
// The service's handle() method sets the value of the cache key 'key' to the current tenant key
|
// The service's handle() method sets the value of the cache key 'key' to the current tenant key
|
||||||
// Or to 'central-value' if tenancy isn't initialized
|
// Or to 'central-value' if tenancy isn't initialized
|
||||||
$this->app->singleton(SpecificCacheStoreService::class, function() use ($cacheStore) {
|
$this->app->singleton(SpecificCacheStoreService::class, function() use ($cacheStore) {
|
||||||
|
|
@ -213,111 +294,3 @@ test('specific central cache store can be used inside a service', function () {
|
||||||
// We last executed handle() in tenant2's context, so the value should persist as tenant2's id
|
// We last executed handle() in tenant2's context, so the value should persist as tenant2's id
|
||||||
expect(cache()->store($cacheStore)->get('key'))->toBe($tenant2->getTenantKey());
|
expect(cache()->store($cacheStore)->get('key'))->toBe($tenant2->getTenantKey());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('only the stores specified in tenantCacheStores get prefixed', function () {
|
|
||||||
// Make sure the currently used store ('redis') is the only store in $tenantCacheStores
|
|
||||||
config(['tenancy.cache.stores' => [$prefixedStore = 'redis']]);
|
|
||||||
|
|
||||||
$centralValue = 'central-value';
|
|
||||||
$assertStoreIsNotPrefixed = function (string $unprefixedStore) use ($prefixedStore, $centralValue) {
|
|
||||||
// Switch to the unprefixed store
|
|
||||||
config(['cache.default' => $unprefixedStore]);
|
|
||||||
expect(cache('key'))->toBe($centralValue);
|
|
||||||
// Switch back to the prefixed store
|
|
||||||
config(['cache.default' => $prefixedStore]);
|
|
||||||
};
|
|
||||||
|
|
||||||
$this->app->singleton(CacheService::class);
|
|
||||||
|
|
||||||
$this->app->make(CacheService::class)->handle();
|
|
||||||
expect(cache('key'))->toBe($centralValue);
|
|
||||||
|
|
||||||
$tenant1 = Tenant::create();
|
|
||||||
$tenant2 = Tenant::create();
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant1);
|
|
||||||
|
|
||||||
expect(cache('key'))->toBeNull();
|
|
||||||
$this->app->make(CacheService::class)->handle();
|
|
||||||
expect(cache('key'))->toBe($tenant1->getTenantKey());
|
|
||||||
|
|
||||||
$assertStoreIsNotPrefixed('redis2');
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant2);
|
|
||||||
|
|
||||||
expect(cache('key'))->toBeNull();
|
|
||||||
$this->app->make(CacheService::class)->handle();
|
|
||||||
expect(cache('key'))->toBe($tenant2->getTenantKey());
|
|
||||||
|
|
||||||
$assertStoreIsNotPrefixed('redis2');
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
expect(cache('key'))->toBe($centralValue);
|
|
||||||
|
|
||||||
$this->app->make(CacheService::class)->handle();
|
|
||||||
expect(cache('key'))->toBe($centralValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('non default stores get prefixed too when specified in tenantCacheStores', function () {
|
|
||||||
// In beforeEach, we set $tenantCacheStores to ['redis', 'redis2']
|
|
||||||
// Make 'redis' the default cache driver
|
|
||||||
config(['cache.default' => 'redis']);
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$defaultPrefix = cache()->store()->getPrefix();
|
|
||||||
$bootstrapper = app(CacheTenancyBootstrapper::class);
|
|
||||||
|
|
||||||
// The prefix is the same for both drivers in the central context
|
|
||||||
expect(cache()->store('redis')->getPrefix())->toBe($defaultPrefix);
|
|
||||||
expect(cache()->store('redis2')->getPrefix())->toBe($defaultPrefix);
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
// We didn't add a prefix generator for our 'redis2' driver, so we expect the prefix to be generated using the 'default' generator
|
|
||||||
expect($bootstrapper->generatePrefix($tenant))
|
|
||||||
->toBe(cache()->getPrefix())
|
|
||||||
->toBe(cache()->store('redis2')->getPrefix()); // Non-default store
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cache store prefix generation can be customized', function() {
|
|
||||||
// Use custom prefix generator
|
|
||||||
CacheTenancyBootstrapper::generatePrefixUsing($customPrefixGenerator = function (Tenant $tenant) {
|
|
||||||
return 'redis_tenant_cache_' . $tenant->getTenantKey();
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(CacheTenancyBootstrapper::$prefixGenerator)->toBe($customPrefixGenerator);
|
|
||||||
expect(app(CacheTenancyBootstrapper::class)->generatePrefix($tenant = Tenant::create()))
|
|
||||||
->toBe($customPrefixGenerator($tenant));
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant = Tenant::create());
|
|
||||||
|
|
||||||
// Expect the 'redis' store to use the prefix generated by the custom generator
|
|
||||||
expect($customPrefixGenerator($tenant))
|
|
||||||
->toBe(cache()->getPrefix())
|
|
||||||
->toBe(cache()->store('redis2')->getPrefix()) // Non-default cache stores specified in $tenantCacheStores are prefixed too
|
|
||||||
->toBe(app('cache')->getPrefix())
|
|
||||||
->toBe(app('cache.store')->getPrefix());
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('stores get prefixed using the default way if no prefix generator is specified', function() {
|
|
||||||
$originalPrefix = config('cache.prefix');
|
|
||||||
$prefixBase = config('tenancy.cache.prefix_base');
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
$defaultPrefix = $originalPrefix . $prefixBase . $tenant->getTenantKey();
|
|
||||||
|
|
||||||
// Don't specify a prefix generator
|
|
||||||
// Let the prefix get created using the default approach
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
// All stores use the default way of generating the prefix when the prefix generator isn't specified
|
|
||||||
expect($defaultPrefix)
|
|
||||||
->toBe(app(CacheTenancyBootstrapper::class)->generatePrefix($tenant))
|
|
||||||
->toBe(cache()->getPrefix()) // Get prefix of the default store ('redis')
|
|
||||||
->toBe(cache()->store('redis2')->getPrefix());
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
});
|
|
||||||
|
|
@ -39,7 +39,7 @@ use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
// Sessions table for central database
|
// Sessions table for central database
|
||||||
pest()->artisan('migrate', [
|
pest()->artisan('migrate', [
|
||||||
'--path' => __DIR__ . '/Etc/session_migrations',
|
'--path' => __DIR__ . '/../Etc/session_migrations',
|
||||||
'--realpath' => true,
|
'--realpath' => true,
|
||||||
])->assertExitCode(0);
|
])->assertExitCode(0);
|
||||||
});
|
});
|
||||||
|
|
@ -58,7 +58,7 @@ test('central helper can be used in tenant requests', function (bool $enabled, b
|
||||||
|
|
||||||
// run for tenants
|
// run for tenants
|
||||||
pest()->artisan('tenants:migrate', [
|
pest()->artisan('tenants:migrate', [
|
||||||
'--path' => __DIR__ . '/Etc/session_migrations',
|
'--path' => __DIR__ . '/../Etc/session_migrations',
|
||||||
'--realpath' => true,
|
'--realpath' => true,
|
||||||
])->assertExitCode(0);
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ test('tenant run helper can be used on central requests', function (bool $enable
|
||||||
|
|
||||||
// run for tenants
|
// run for tenants
|
||||||
pest()->artisan('tenants:migrate', [
|
pest()->artisan('tenants:migrate', [
|
||||||
'--path' => __DIR__ . '/Etc/session_migrations',
|
'--path' => __DIR__ . '/../Etc/session_migrations',
|
||||||
'--realpath' => true,
|
'--realpath' => true,
|
||||||
])->assertExitCode(0);
|
])->assertExitCode(0);
|
||||||
|
|
||||||
32
tests/Bootstrappers/DatabaseTenancyBootstrapper.php
Normal file
32
tests/Bootstrappers/DatabaseTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('database tenancy bootstrapper throws an exception if DATABASE_URL is set', function (string|null $databaseUrl) {
|
||||||
|
if ($databaseUrl) {
|
||||||
|
config(['database.connections.central.url' => $databaseUrl]);
|
||||||
|
|
||||||
|
pest()->expectException(Exception::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
|
||||||
|
pest()->artisan('tenants:migrate');
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
expect(true)->toBe(true);
|
||||||
|
})->with(['abc.us-east-1.rds.amazonaws.com', null]);
|
||||||
|
|
||||||
202
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
Normal file
202
tests/Bootstrappers/FilesystemTenancyBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events\DeletingTenant;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Events\TenantDeleted;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateStorageSymlinks;
|
||||||
|
use Stancl\Tenancy\Jobs\RemoveStorageSymlinks;
|
||||||
|
use Stancl\Tenancy\Listeners\DeleteTenantStorage;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local storage public urls are generated correctly', function () {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [
|
||||||
|
FilesystemTenancyBootstrapper::class,
|
||||||
|
],
|
||||||
|
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||||
|
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
$tenant1StorageUrl = 'http://localhost/public-' . $tenant1->getKey().'/';
|
||||||
|
$tenant2StorageUrl = 'http://localhost/public-' . $tenant2->getKey().'/';
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$tenant1StorageUrl,
|
||||||
|
Storage::disk('public')->url('')
|
||||||
|
);
|
||||||
|
|
||||||
|
Storage::disk('public')->put($tenant1FileName = 'tenant1.txt', 'text');
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$tenant1StorageUrl . $tenant1FileName,
|
||||||
|
Storage::disk('public')->url($tenant1FileName)
|
||||||
|
);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$tenant2StorageUrl,
|
||||||
|
Storage::disk('public')->url('')
|
||||||
|
);
|
||||||
|
|
||||||
|
Storage::disk('public')->put($tenant2FileName = 'tenant2.txt', 'text');
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
$tenant2StorageUrl . $tenant2FileName,
|
||||||
|
Storage::disk('public')->url($tenant2FileName)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('files can get fetched using the storage url', function() {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [
|
||||||
|
FilesystemTenancyBootstrapper::class,
|
||||||
|
],
|
||||||
|
'tenancy.filesystem.root_override.public' => '%storage_path%/app/public/',
|
||||||
|
'tenancy.filesystem.url_override.public' => 'public-%tenant%'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant1 = Tenant::create();
|
||||||
|
$tenant2 = Tenant::create();
|
||||||
|
|
||||||
|
pest()->artisan('tenants:link');
|
||||||
|
|
||||||
|
// First tenant
|
||||||
|
tenancy()->initialize($tenant1);
|
||||||
|
Storage::disk('public')->put($tenantFileName = 'tenant1.txt', $tenantKey = $tenant1->getTenantKey());
|
||||||
|
|
||||||
|
$url = Storage::disk('public')->url($tenantFileName);
|
||||||
|
$tenantDiskName = str(config('tenancy.filesystem.url_override.public'))->replace('%tenant%', $tenantKey);
|
||||||
|
$hostname = str($url)->before($tenantDiskName);
|
||||||
|
$parsedUrl = str($url)->after($hostname);
|
||||||
|
|
||||||
|
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
||||||
|
|
||||||
|
// Second tenant
|
||||||
|
tenancy()->initialize($tenant2);
|
||||||
|
Storage::disk('public')->put($tenantFileName = 'tenant2.txt', $tenantKey = $tenant2->getTenantKey());
|
||||||
|
|
||||||
|
$url = Storage::disk('public')->url($tenantFileName);
|
||||||
|
$tenantDiskName = str(config('tenancy.filesystem.url_override.public'))->replace('%tenant%', $tenantKey);
|
||||||
|
$hostname = str($url)->before($tenantDiskName);
|
||||||
|
$parsedUrl = str($url)->after($hostname);
|
||||||
|
|
||||||
|
expect(file_get_contents(public_path($parsedUrl)))->toBe($tenantKey);
|
||||||
|
|
||||||
|
// Central
|
||||||
|
tenancy()->end();
|
||||||
|
Storage::disk('public')->put($centralFileName = 'central.txt', $centralFileContent = 'central');
|
||||||
|
|
||||||
|
pest()->artisan('storage:link');
|
||||||
|
$url = Storage::disk('public')->url($centralFileName);
|
||||||
|
|
||||||
|
expect(file_get_contents(public_path($url)))->toBe($centralFileContent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('storage_path helper does not change if suffix_storage_path is off', function() {
|
||||||
|
$originalStoragePath = storage_path();
|
||||||
|
|
||||||
|
// todo@tests https://github.com/tenancy-for-laravel/v4/pull/44#issue-2228530362
|
||||||
|
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
||||||
|
'tenancy.filesystem.suffix_storage_path' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
|
||||||
|
$this->assertEquals($originalStoragePath, storage_path());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('links to storage disks with a configured root are suffixed if not overridden', function() {
|
||||||
|
config([
|
||||||
|
'filesystems.disks.public.root' => 'http://sample-s3-url.com/my-app',
|
||||||
|
'tenancy.bootstrappers' => [
|
||||||
|
FilesystemTenancyBootstrapper::class,
|
||||||
|
],
|
||||||
|
'tenancy.filesystem.root_override.public' => null,
|
||||||
|
'tenancy.filesystem.url_override.public' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
$expectedStoragePath = storage_path() . '/tenant' . $tenant->getTenantKey(); // /tenant = suffix base
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// Check suffixing logic
|
||||||
|
expect(storage_path())->toEqual($expectedStoragePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create and delete storage symlinks jobs work', function() {
|
||||||
|
Event::listen(
|
||||||
|
TenantCreated::class,
|
||||||
|
JobPipeline::make([CreateStorageSymlinks::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener()
|
||||||
|
);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
TenantDeleted::class,
|
||||||
|
JobPipeline::make([RemoveStorageSymlinks::class])->send(function (TenantDeleted $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener()
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
$tenantKey = $tenant->getTenantKey();
|
||||||
|
|
||||||
|
$this->assertDirectoryExists(storage_path("app/public"));
|
||||||
|
$this->assertEquals(storage_path("app/public/"), readlink(public_path("public-$tenantKey")));
|
||||||
|
|
||||||
|
$tenant->delete();
|
||||||
|
|
||||||
|
$this->assertDirectoryDoesNotExist(public_path("public-$tenantKey"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tenant storage can get deleted after the tenant when DeletingTenant listens to DeleteTenantStorage', function() {
|
||||||
|
Event::listen(DeletingTenant::class, DeleteTenantStorage::class);
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
$tenantStoragePath = storage_path();
|
||||||
|
|
||||||
|
Storage::fake('test');
|
||||||
|
|
||||||
|
expect(File::isDirectory($tenantStoragePath))->toBeTrue();
|
||||||
|
|
||||||
|
Storage::put('test.txt', 'testing file');
|
||||||
|
|
||||||
|
tenant()->delete();
|
||||||
|
|
||||||
|
expect(File::isDirectory($tenantStoragePath))->toBeFalse();
|
||||||
|
});
|
||||||
76
tests/Bootstrappers/FortifyRouteBootstrapperTest.php
Normal file
76
tests/Bootstrappers/FortifyRouteBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\Integrations\FortifyRouteBootstrapper;
|
||||||
|
use Stancl\Tenancy\Enums\Context;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fortify route tenancy bootstrapper updates fortify config correctly', function() {
|
||||||
|
config(['tenancy.bootstrappers' => [FortifyRouteBootstrapper::class]]);
|
||||||
|
|
||||||
|
$originalFortifyHome = config('fortify.home');
|
||||||
|
$originalFortifyRedirects = config('fortify.redirects');
|
||||||
|
|
||||||
|
Route::get('/home', function () {
|
||||||
|
return true;
|
||||||
|
})->name($homeRouteName = 'home');
|
||||||
|
|
||||||
|
Route::get('/{tenant}/home', function () {
|
||||||
|
return true;
|
||||||
|
})->name($pathIdHomeRouteName = 'tenant.home');
|
||||||
|
|
||||||
|
Route::get('/welcome', function () {
|
||||||
|
return true;
|
||||||
|
})->name($welcomeRouteName = 'welcome');
|
||||||
|
|
||||||
|
Route::get('/{tenant}/welcome', function () {
|
||||||
|
return true;
|
||||||
|
})->name($pathIdWelcomeRouteName = 'path.welcome');
|
||||||
|
|
||||||
|
FortifyRouteBootstrapper::$fortifyHome = $homeRouteName;
|
||||||
|
|
||||||
|
// Make login redirect to the central welcome route
|
||||||
|
FortifyRouteBootstrapper::$fortifyRedirectMap['login'] = [
|
||||||
|
'route_name' => $welcomeRouteName,
|
||||||
|
'context' => Context::CENTRAL,
|
||||||
|
];
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
expect(config('fortify.home'))->toBe($originalFortifyHome);
|
||||||
|
expect(config('fortify.redirects'))->toBe($originalFortifyRedirects);
|
||||||
|
|
||||||
|
// Making a route's context will pass the tenant parameter to the route
|
||||||
|
FortifyRouteBootstrapper::$fortifyRedirectMap['login']['context'] = Context::TENANT;
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
expect(config('fortify.redirects'))->toEqual(['login' => 'http://localhost/welcome?tenant=' . $tenant->getTenantKey()]);
|
||||||
|
|
||||||
|
// 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()->initialize($tenant);
|
||||||
|
|
||||||
|
expect(config('fortify.home'))->toBe("http://localhost/{$tenant->getTenantKey()}/home");
|
||||||
|
expect(config('fortify.redirects'))->toEqual(['login' => "http://localhost/{$tenant->getTenantKey()}/welcome"]);
|
||||||
|
});
|
||||||
62
tests/Bootstrappers/MailTenancyBootstrapper.php
Normal file
62
tests/Bootstrappers/MailTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\MailConfigBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MailTenancyBootstrapper maps tenant mail credentials to config as specified in the $credentialsMap property and makes the mailer use tenant credentials', function() {
|
||||||
|
MailConfigBootstrapper::$credentialsMap = [
|
||||||
|
'mail.mailers.smtp.username' => 'smtp_username',
|
||||||
|
'mail.mailers.smtp.password' => 'smtp_password'
|
||||||
|
];
|
||||||
|
|
||||||
|
config([
|
||||||
|
'mail.default' => 'smtp',
|
||||||
|
'mail.mailers.smtp.username' => $defaultUsername = 'default username',
|
||||||
|
'mail.mailers.smtp.password' => 'no password',
|
||||||
|
'tenancy.bootstrappers' => [MailConfigBootstrapper::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::create(['smtp_password' => $password = 'testing password']);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
expect(array_key_exists('smtp_password', tenant()->getAttributes()))->toBeTrue();
|
||||||
|
expect(array_key_exists('smtp_host', tenant()->getAttributes()))->toBeFalse();
|
||||||
|
expect(config('mail.mailers.smtp.username'))->toBe($defaultUsername);
|
||||||
|
expect(config('mail.mailers.smtp.password'))->toBe(tenant()->smtp_password);
|
||||||
|
|
||||||
|
// Assert that the current mailer uses tenant's smtp_password
|
||||||
|
assertMailerTransportUsesPassword($password);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('MailTenancyBootstrapper reverts the config and mailer credentials to default when tenancy ends', function() {
|
||||||
|
MailConfigBootstrapper::$credentialsMap = ['mail.mailers.smtp.password' => 'smtp_password'];
|
||||||
|
config([
|
||||||
|
'mail.default' => 'smtp',
|
||||||
|
'mail.mailers.smtp.password' => $defaultPassword = 'no password',
|
||||||
|
'tenancy.bootstrappers' => [MailConfigBootstrapper::class],
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create(['smtp_password' => $tenantPassword = 'testing password']));
|
||||||
|
|
||||||
|
expect(config('mail.mailers.smtp.password'))->toBe($tenantPassword);
|
||||||
|
|
||||||
|
assertMailerTransportUsesPassword($tenantPassword);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
expect(config('mail.mailers.smtp.password'))->toBe($defaultPassword);
|
||||||
|
|
||||||
|
// Assert that the current mailer uses the default SMTP password
|
||||||
|
assertMailerTransportUsesPassword($defaultPassword);
|
||||||
|
});
|
||||||
|
|
||||||
67
tests/Bootstrappers/RootUrlBootstrapperTest.php
Normal file
67
tests/Bootstrappers/RootUrlBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Illuminate\Support\Facades\URL;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\RootUrlBootstrapper;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
RootUrlBootstrapper::$rootUrlOverride = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
RootUrlBootstrapper::$rootUrlOverride = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('root url bootstrapper overrides the root url when tenancy gets initialized and reverts the url to the central one after tenancy ends', function() {
|
||||||
|
config(['tenancy.bootstrappers' => [RootUrlBootstrapper::class]]);
|
||||||
|
|
||||||
|
Route::group([
|
||||||
|
'middleware' => InitializeTenancyBySubdomain::class,
|
||||||
|
], function () {
|
||||||
|
Route::get('/', function () {
|
||||||
|
return true;
|
||||||
|
})->name('home');
|
||||||
|
});
|
||||||
|
|
||||||
|
$baseUrl = url(route('home'));
|
||||||
|
config(['app.url' => $baseUrl]);
|
||||||
|
|
||||||
|
$rootUrlOverride = function (Tenant $tenant) use ($baseUrl) {
|
||||||
|
$scheme = str($baseUrl)->before('://');
|
||||||
|
$hostname = str($baseUrl)->after($scheme . '://');
|
||||||
|
|
||||||
|
return $scheme . '://' . $tenant->getTenantKey() . '.' . $hostname;
|
||||||
|
};
|
||||||
|
|
||||||
|
RootUrlBootstrapper::$rootUrlOverride = $rootUrlOverride;
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenantUrl = $rootUrlOverride($tenant);
|
||||||
|
|
||||||
|
expect($tenantUrl)->not()->toBe($baseUrl);
|
||||||
|
|
||||||
|
expect(url(route('home')))->toBe($baseUrl);
|
||||||
|
expect(URL::to('/'))->toBe($baseUrl);
|
||||||
|
expect(config('app.url'))->toBe($baseUrl);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
expect(url(route('home')))->toBe($tenantUrl);
|
||||||
|
expect(URL::to('/'))->toBe($tenantUrl);
|
||||||
|
expect(config('app.url'))->toBe($tenantUrl);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
expect(url(route('home')))->toBe($baseUrl);
|
||||||
|
expect(URL::to('/'))->toBe($baseUrl);
|
||||||
|
expect(config('app.url'))->toBe($baseUrl);
|
||||||
|
});
|
||||||
|
|
||||||
157
tests/Bootstrappers/UrlGeneratorBootstrapperTest.php
Normal file
157
tests/Bootstrappers/UrlGeneratorBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Routing\Exceptions\UrlGenerationException;
|
||||||
|
use Illuminate\Routing\UrlGenerator;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\UrlGeneratorBootstrapper;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||||
|
use Stancl\Tenancy\Overrides\TenancyUrlGenerator;
|
||||||
|
use Stancl\Tenancy\Resolvers\PathTenantResolver;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
TenancyUrlGenerator::$prefixRouteNames = false;
|
||||||
|
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
TenancyUrlGenerator::$prefixRouteNames = false;
|
||||||
|
TenancyUrlGenerator::$passTenantParameterToRoutes = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('url generator bootstrapper swaps the url generator instance correctly', function() {
|
||||||
|
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||||
|
|
||||||
|
tenancy()->initialize(Tenant::create());
|
||||||
|
expect(app('url'))->toBeInstanceOf(TenancyUrlGenerator::class);
|
||||||
|
expect(url())->toBeInstanceOf(TenancyUrlGenerator::class);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
expect(app('url'))->toBeInstanceOf(UrlGenerator::class)
|
||||||
|
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
|
||||||
|
expect(url())->toBeInstanceOf(UrlGenerator::class)
|
||||||
|
->not()->toBeInstanceOf(TenancyUrlGenerator::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('url generator bootstrapper can prefix route names passed to the route helper', function() {
|
||||||
|
Route::get('/central/home', fn () => route('home'))->name('home');
|
||||||
|
// Tenant route name prefix is 'tenant.' by default
|
||||||
|
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenantKey = $tenant->getTenantKey();
|
||||||
|
$centralRouteUrl = route('home');
|
||||||
|
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenantKey]);
|
||||||
|
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
|
||||||
|
|
||||||
|
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// Route names don't get prefixed when TenancyUrlGenerator::$prefixRouteNames is false
|
||||||
|
expect(route('home'))->not()->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);
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// The 'tenant.home' route name doesn't get prefixed because it is already prefixed with 'tenant.'
|
||||||
|
// Also, the route receives the tenant parameter automatically
|
||||||
|
expect(route('tenant.home'))->toBe($tenantRouteUrl);
|
||||||
|
|
||||||
|
// Ending tenancy reverts route() behavior changes
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
expect(route('home'))->toBe($centralRouteUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('both the name prefixing and the tenant parameter logic gets skipped when bypass parameter is used', function () {
|
||||||
|
$tenantParameterName = PathTenantResolver::tenantParameterName();
|
||||||
|
|
||||||
|
Route::get('/central/home', fn () => route('home'))->name('home');
|
||||||
|
// Tenant route name prefix is 'tenant.' by default
|
||||||
|
Route::get('/{tenant}/home', fn () => route('tenant.home'))->name('tenant.home')->middleware(['tenant', InitializeTenancyByPath::class]);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$centralRouteUrl = route('home');
|
||||||
|
$tenantRouteUrl = route('tenant.home', ['tenant' => $tenant->getTenantKey()]);
|
||||||
|
config(['tenancy.bootstrappers' => [UrlGeneratorBootstrapper::class]]);
|
||||||
|
|
||||||
|
TenancyUrlGenerator::$prefixRouteNames = true;
|
||||||
|
TenancyUrlGenerator::$bypassParameter = 'bypassParameter';
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
// The $bypassParameter parameter ('central' by default) can bypass the route name prefixing
|
||||||
|
// When the bypass parameter is true, the generated route URL points to the route named 'home'
|
||||||
|
expect(route('home', ['bypassParameter' => true]))->toBe($centralRouteUrl)
|
||||||
|
// Bypass parameter prevents passing the tenant parameter directly
|
||||||
|
->not()->toContain($tenantParameterName . '=')
|
||||||
|
// Bypass parameter gets removed from the generated URL automatically
|
||||||
|
->not()->toContain('bypassParameter');
|
||||||
|
|
||||||
|
// When the bypass parameter is false, the generated route URL points to the prefixed route ('tenant.home')
|
||||||
|
expect(route('home', ['bypassParameter' => false]))->toBe($tenantRouteUrl)
|
||||||
|
->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);
|
||||||
|
});
|
||||||
|
|
@ -20,7 +20,7 @@ beforeEach(function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant can be identified by subdomain', function () {
|
test('tenant can be identified by subdomain', function () {
|
||||||
config(['tenancy.central_domains' => ['localhost']]);
|
config(['tenancy.identification.central_domains' => ['localhost']]);
|
||||||
|
|
||||||
$tenant = CombinedTenant::create([
|
$tenant = CombinedTenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
|
|
@ -41,7 +41,7 @@ test('tenant can be identified by subdomain', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant can be identified by domain', function () {
|
test('tenant can be identified by domain', function () {
|
||||||
config(['tenancy.central_domains' => []]);
|
config(['tenancy.identification.central_domains' => []]);
|
||||||
|
|
||||||
$tenant = CombinedTenant::create([
|
$tenant = CombinedTenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ test('global cache manager stores data in global cache', function (string $boots
|
||||||
cache(['def' => 'ghi'], 10);
|
cache(['def' => 'ghi'], 10);
|
||||||
expect(cache('def'))->toBe('ghi');
|
expect(cache('def'))->toBe('ghi');
|
||||||
|
|
||||||
|
// different stores, same underlying connection. the prefix is set ON THE STORE
|
||||||
|
expect(cache()->store()->getStore() === GlobalCache::store()->getStore())->toBeFalse();
|
||||||
|
expect(cache()->store()->getStore()->connection() === GlobalCache::store()->getStore()->connection())->toBeTrue();
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
expect(GlobalCache::get('abc'))->toBe('xyz');
|
expect(GlobalCache::get('abc'))->toBe('xyz');
|
||||||
expect(GlobalCache::get('foo'))->toBe('bar');
|
expect(GlobalCache::get('foo'))->toBe('bar');
|
||||||
|
|
@ -63,6 +67,10 @@ test('the global_cache helper supports the same syntax as the cache helper', fun
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
$tenant->enter();
|
$tenant->enter();
|
||||||
|
|
||||||
|
// different stores, same underlying connection. the prefix is set ON THE STORE
|
||||||
|
expect(cache()->store()->getStore() === global_cache()->store()->getStore())->toBeFalse();
|
||||||
|
expect(cache()->store()->getStore()->connection() === global_cache()->store()->getStore()->connection())->toBeTrue();
|
||||||
|
|
||||||
expect(cache('foo'))->toBe(null); // tenant cache is empty
|
expect(cache('foo'))->toBe(null); // tenant cache is empty
|
||||||
|
|
||||||
global_cache(['foo' => 'bar']);
|
global_cache(['foo' => 'bar']);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ beforeEach(function () {
|
||||||
InitializeTenancyByOriginHeader::$onFail = null;
|
InitializeTenancyByOriginHeader::$onFail = null;
|
||||||
|
|
||||||
config([
|
config([
|
||||||
'tenancy.central_domains' => [
|
'tenancy.identification.central_domains' => [
|
||||||
'localhost',
|
'localhost',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
config([
|
config([
|
||||||
'tenancy.central_domains' => [
|
'tenancy.identification.central_domains' => [
|
||||||
'localhost',
|
'localhost',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Events\TenantCreated;
|
|
||||||
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
|
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;
|
||||||
|
|
@ -19,30 +17,21 @@ beforeEach(function () {
|
||||||
return 'true';
|
return 'true';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Event::listen(TenantCreated::class, function (TenantCreated $event) {
|
|
||||||
$tenant = $event->tenant;
|
|
||||||
|
|
||||||
/** @var Tenant $tenant */
|
|
||||||
$tenant->domains()->create([
|
|
||||||
'domain' => $tenant->id,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant id is auto added to session if its missing', function () {
|
test('tenant id is auto added to session if its missing', function () {
|
||||||
$tenant = Tenant::create([
|
Tenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
]);
|
])->createDomain('acme');
|
||||||
|
|
||||||
pest()->get('http://acme.localhost/foo')
|
pest()->get('http://acme.localhost/foo')
|
||||||
->assertSessionHas(ScopeSessions::$tenantIdKey, 'acme');
|
->assertSessionHas(ScopeSessions::$tenantIdKey, 'acme');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('changing tenant id in session will abort the request', function () {
|
test('changing tenant id in session will abort the request', function () {
|
||||||
$tenant = Tenant::create([
|
Tenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
]);
|
])->createDomain('acme');
|
||||||
|
|
||||||
pest()->get('http://acme.localhost/foo')
|
pest()->get('http://acme.localhost/foo')
|
||||||
->assertSuccessful();
|
->assertSuccessful();
|
||||||
|
|
@ -58,10 +47,10 @@ test('an exception is thrown when the middleware is executed before tenancy is i
|
||||||
return true;
|
return true;
|
||||||
})->middleware([StartSession::class, ScopeSessions::class]);
|
})->middleware([StartSession::class, ScopeSessions::class]);
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
Tenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
]);
|
])->createDomain('acme');
|
||||||
|
|
||||||
pest()->expectException(TenancyNotInitializedException::class);
|
pest()->expectException(TenancyNotInitializedException::class);
|
||||||
pest()->withoutExceptionHandling()->get('http://acme.localhost/bar');
|
$this->withoutExceptionHandling()->get('http://acme.localhost/bar');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
247
tests/SessionSeparationTest.php
Normal file
247
tests/SessionSeparationTest.php
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseSessionBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events\TenancyEnded;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Jobs\MigrateDatabase;
|
||||||
|
use Stancl\Tenancy\Listeners\BootstrapTenancy;
|
||||||
|
use Stancl\Tenancy\Listeners\RevertToCentralContext;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Event::listen(TenancyInitialized::class, BootstrapTenancy::class);
|
||||||
|
Event::listen(TenancyEnded::class, RevertToCentralContext::class);
|
||||||
|
|
||||||
|
// Middleware priority logic
|
||||||
|
$tenancyMiddleware = array_merge([PreventAccessFromUnwantedDomains::class], config('tenancy.identification.middleware'));
|
||||||
|
foreach (array_reverse($tenancyMiddleware) as $middleware) {
|
||||||
|
app()->make(\Illuminate\Contracts\Http\Kernel::class)->prependToMiddlewarePriority($middleware);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('file sessions are separated', function (bool $scopeSessions) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class],
|
||||||
|
'tenancy.filesystem.suffix_storage_path' => false,
|
||||||
|
'tenancy.filesystem.scope_sessions' => $scopeSessions,
|
||||||
|
'session.driver' => 'file',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sessionPath = fn () => invade(app('session')->driver()->getHandler())->path;
|
||||||
|
|
||||||
|
expect($sessionPath())->toBe(storage_path('framework/sessions'));
|
||||||
|
File::cleanDirectory(storage_path("framework/sessions")); // clean up the sessions dir from past test runs
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
$tenant->enter();
|
||||||
|
|
||||||
|
if ($scopeSessions) {
|
||||||
|
expect($sessionPath())->toBe(storage_path('tenant' . $tenant->getTenantKey() . '/framework/sessions'));
|
||||||
|
} else {
|
||||||
|
expect($sessionPath())->toBe(storage_path('framework/sessions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant->leave();
|
||||||
|
|
||||||
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
|
|
||||||
|
if ($scopeSessions) {
|
||||||
|
expect(File::files(storage_path("tenant{$tenant->id}/framework/sessions")))->toHaveCount(0);
|
||||||
|
} else {
|
||||||
|
expect(File::exists(storage_path("tenant{$tenant->id}/framework/sessions")))->toBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
|
if ($scopeSessions) {
|
||||||
|
expect(File::files(storage_path("tenant{$tenant->id}/framework/sessions")))->toHaveCount(1);
|
||||||
|
expect(File::files(storage_path("framework/sessions")))->toHaveCount(0);
|
||||||
|
} else {
|
||||||
|
expect(File::exists(storage_path("tenant{$tenant->id}/framework/sessions")))->toBeFalse();
|
||||||
|
expect(File::files(storage_path("framework/sessions")))->toHaveCount(1);
|
||||||
|
}
|
||||||
|
})->with([true, false]);
|
||||||
|
|
||||||
|
test('redis sessions are separated using the redis bootstrapper', function (bool $bootstrappedEnabled) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => $bootstrappedEnabled ? [RedisTenancyBootstrapper::class] : [],
|
||||||
|
'session.driver' => 'redis',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$redisClient = app('session')->driver()->getHandler()->getCache()->getStore()->connection()->client();
|
||||||
|
expect($redisClient->getOption($redisClient::OPT_PREFIX))->toBe('foo'); // default prefix configured in TestCase
|
||||||
|
|
||||||
|
expect(Redis::keys('*'))->toHaveCount(0);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
|
expect($redisClient->getOption($redisClient::OPT_PREFIX) === "tenant_{$tenant->id}_")->toBe($bootstrappedEnabled);
|
||||||
|
|
||||||
|
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
|
||||||
|
return str($key)->startsWith("tenant_{$tenant->id}_laravel_cache_");
|
||||||
|
}))->toHaveCount($bootstrappedEnabled ? 1 : 0);
|
||||||
|
})->with([true, false]);
|
||||||
|
|
||||||
|
test('redis sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
|
||||||
|
'session.driver' => 'redis',
|
||||||
|
'tenancy.cache.stores' => [], // will be implicitly filled
|
||||||
|
'tenancy.cache.scope_sessions' => $scopeSessions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(Redis::keys('*'))->toHaveCount(0);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
||||||
|
|
||||||
|
expect(array_filter(Redis::keys('*'), function (string $key) use ($tenant) {
|
||||||
|
return str($key)->startsWith("foolaravel_cache_tenant_{$tenant->id}");
|
||||||
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
|
})->with([true, false]);
|
||||||
|
|
||||||
|
test('memcached sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
|
||||||
|
'session.driver' => 'memcached',
|
||||||
|
'tenancy.cache.stores' => [], // will be implicitly filled
|
||||||
|
'tenancy.cache.scope_sessions' => $scopeSessions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$allMemcachedKeys = fn () => cache()->store('memcached')->getStore()->getMemcached()->getAllKeys();
|
||||||
|
|
||||||
|
if (count($allMemcachedKeys()) !== 0) {
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($allMemcachedKeys())->toHaveCount(0);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
||||||
|
|
||||||
|
sleep(1.1); // 1s+ sleep is necessary for getAllKeys() to work. if this causes race conditions or we want to avoid the delay, we can refactor this to some type of a mock
|
||||||
|
expect(array_filter($allMemcachedKeys(), function (string $key) use ($tenant) {
|
||||||
|
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
|
||||||
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
|
|
||||||
|
Artisan::call('cache:clear memcached');
|
||||||
|
})->with([true, false]);
|
||||||
|
|
||||||
|
test('dynamodb sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
|
||||||
|
'session.driver' => 'dynamodb',
|
||||||
|
'tenancy.cache.stores' => [], // will be implicitly filled
|
||||||
|
'tenancy.cache.scope_sessions' => $scopeSessions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$allDynamodbKeys = fn () => array_map(fn ($res) => $res['key']['S'], cache()->store('dynamodb')->getStore()->getClient()->scan(['TableName' => 'cache'])['Items']);
|
||||||
|
|
||||||
|
expect($allDynamodbKeys())->toHaveCount(0);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
||||||
|
|
||||||
|
expect(array_filter($allDynamodbKeys(), function (string $key) use ($tenant) {
|
||||||
|
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
|
||||||
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
|
})->with([true, false]);
|
||||||
|
|
||||||
|
test('apc sessions are separated using the cache bootstrapper', function (bool $scopeSessions) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => [CacheTenancyBootstrapper::class],
|
||||||
|
'session.driver' => 'apc',
|
||||||
|
'tenancy.cache.stores' => [], // will be implicitly filled
|
||||||
|
'tenancy.cache.scope_sessions' => $scopeSessions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$allApcuKeys = fn () => array_column(apcu_cache_info()['cache_list'], 'info');
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix() === "laravel_cache_tenant_{$tenant->id}_")->toBe($scopeSessions);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
expect(app('session')->driver()->getHandler()->getCache()->getStore()->getPrefix())->toBe('laravel_cache_');
|
||||||
|
|
||||||
|
expect(array_filter($allApcuKeys(), function (string $key) use ($tenant) {
|
||||||
|
return str($key)->startsWith("laravel_cache_tenant_{$tenant->id}");
|
||||||
|
}))->toHaveCount($scopeSessions ? 1 : 0);
|
||||||
|
})->with([true, false]);
|
||||||
|
|
||||||
|
test('database sessions are separated regardless of whether the session bootstrapper is enabled', function (bool $sessionBootstrappedEnabled, bool $connectionSet) {
|
||||||
|
config([
|
||||||
|
'tenancy.bootstrappers' => $sessionBootstrappedEnabled
|
||||||
|
? [DatabaseTenancyBootstrapper::class, DatabaseSessionBootstrapper::class]
|
||||||
|
: [DatabaseTenancyBootstrapper::class],
|
||||||
|
'session.driver' => 'database',
|
||||||
|
'session.connection' => $connectionSet ? 'central' : null,
|
||||||
|
'tenancy.migration_parameters.--schema-path' => 'tests/Etc/session_migrations',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
TenantCreated::class,
|
||||||
|
JobPipeline::make([CreateDatabase::class, MigrateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener()
|
||||||
|
);
|
||||||
|
|
||||||
|
pest()->artisan('migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/session_migrations',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
expect(DB::connection('central')->table('sessions')->count())->toBe(0);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
Route::middleware(StartSession::class, InitializeTenancyByPath::class)->get('/{tenant}/foo', fn () => 'bar');
|
||||||
|
pest()->get("/{$tenant->id}/foo");
|
||||||
|
|
||||||
|
expect(invade(app('session')->driver()->getHandler())->connection->getName())->toBe('tenant');
|
||||||
|
|
||||||
|
expect(DB::connection('tenant')->table('sessions')->count())->toBe(1);
|
||||||
|
expect(DB::connection('central')->table('sessions')->count())->toBe(0);
|
||||||
|
})->with([
|
||||||
|
[true, true],
|
||||||
|
[true, false],
|
||||||
|
// [false, true], // when the connection IS set, the session bootstrapper becomes necessary
|
||||||
|
[false, false],
|
||||||
|
]);
|
||||||
|
|
@ -87,7 +87,7 @@ test('oninvalidsubdomain logic can be customized', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('we cant use a subdomain that doesnt belong to our central domains', function () {
|
test('we cant use a subdomain that doesnt belong to our central domains', function () {
|
||||||
config(['tenancy.central_domains' => [
|
config(['tenancy.identification.central_domains' => [
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
// not 'localhost'
|
// not 'localhost'
|
||||||
]]);
|
]]);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Tests;
|
namespace Stancl\Tenancy\Tests;
|
||||||
|
|
||||||
|
use Aws\DynamoDb\DynamoDbClient;
|
||||||
use PDO;
|
use PDO;
|
||||||
use Dotenv\Dotenv;
|
use Dotenv\Dotenv;
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
use Illuminate\Support\Facades\Redis;
|
use Illuminate\Support\Facades\Redis;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\BroadcastChannelPrefixBootstrapper;
|
||||||
use Stancl\Tenancy\Facades\GlobalCache;
|
use Stancl\Tenancy\Facades\GlobalCache;
|
||||||
use Stancl\Tenancy\TenancyServiceProvider;
|
use Stancl\Tenancy\TenancyServiceProvider;
|
||||||
|
|
@ -34,6 +35,46 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
|
|
||||||
Redis::connection('default')->flushdb();
|
Redis::connection('default')->flushdb();
|
||||||
Redis::connection('cache')->flushdb();
|
Redis::connection('cache')->flushdb();
|
||||||
|
Artisan::call('cache:clear memcached'); // flush memcached
|
||||||
|
Artisan::call('cache:clear file'); // flush file cache
|
||||||
|
apcu_clear_cache(); // flush APCu cache
|
||||||
|
|
||||||
|
// re-create dynamodb `cache` table
|
||||||
|
$dynamodb = new DynamoDbClient([
|
||||||
|
'region' => 'us-east-1',
|
||||||
|
'version' => 'latest',
|
||||||
|
'endpoint' => 'http://dynamodb:8000',
|
||||||
|
'credentials' => [
|
||||||
|
'key' => env('TENANCY_TEST_DYNAMODB_KEY', 'DUMMYIDEXAMPLE'),
|
||||||
|
'secret' => env('TENANCY_TEST_DYNAMODB_KEY', 'DUMMYEXAMPLEKEY'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dynamodb->deleteTable([
|
||||||
|
'TableName' => 'cache',
|
||||||
|
]);
|
||||||
|
} catch (\Throwable) {}
|
||||||
|
|
||||||
|
$dynamodb->createTable([
|
||||||
|
'TableName' => 'cache',
|
||||||
|
'KeySchema' => [
|
||||||
|
[
|
||||||
|
'AttributeName' => 'key', // Partition key
|
||||||
|
'KeyType' => 'HASH',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'AttributeDefinitions' => [
|
||||||
|
[
|
||||||
|
'AttributeName' => 'key',
|
||||||
|
'AttributeType' => 'S', // String
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'ProvisionedThroughput' => [
|
||||||
|
'ReadCapacityUnits' => 100,
|
||||||
|
'WriteCapacityUnits' => 100,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
file_put_contents(database_path('central.sqlite'), '');
|
file_put_contents(database_path('central.sqlite'), '');
|
||||||
pest()->artisan('migrate:fresh', [
|
pest()->artisan('migrate:fresh', [
|
||||||
|
|
@ -67,10 +108,18 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
$app['config']->set([
|
$app['config']->set([
|
||||||
'database.default' => 'central',
|
'database.default' => 'central',
|
||||||
'cache.default' => 'redis',
|
'cache.default' => 'redis',
|
||||||
'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'),
|
'session.driver' => 'redis',
|
||||||
'database.redis.default.host' => env('TENANCY_TEST_REDIS_HOST', '127.0.0.1'),
|
'database.redis.cache.host' => env('TENANCY_TEST_REDIS_HOST', 'redis'),
|
||||||
|
'database.redis.default.host' => env('TENANCY_TEST_REDIS_HOST', 'redis'),
|
||||||
'database.redis.options.prefix' => 'foo',
|
'database.redis.options.prefix' => 'foo',
|
||||||
'database.redis.client' => 'predis',
|
'database.redis.client' => 'predis',
|
||||||
|
'cache.stores.memcached.servers.0.host' => env('TENANCY_TEST_MEMCACHED_HOST', 'memcached'),
|
||||||
|
'cache.stores.dynamodb.key' => env('TENANCY_TEST_DYNAMODB_KEY', 'DUMMYIDEXAMPLE'),
|
||||||
|
'cache.stores.dynamodb.secret' => env('TENANCY_TEST_DYNAMODB_SECRET', 'DUMMYEXAMPLEKEY'),
|
||||||
|
'cache.stores.dynamodb.endpoint' => 'http://dynamodb:8000',
|
||||||
|
'cache.stores.dynamodb.region' => 'us-east-1',
|
||||||
|
'cache.stores.dynamodb.table' => 'cache',
|
||||||
|
'cache.stores.apc' => ['driver' => 'apc'],
|
||||||
'database.connections.central' => [
|
'database.connections.central' => [
|
||||||
'driver' => 'mysql',
|
'driver' => 'mysql',
|
||||||
'url' => env('DATABASE_URL'),
|
'url' => env('DATABASE_URL'),
|
||||||
|
|
@ -114,7 +163,7 @@ abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||||
'--realpath' => true,
|
'--realpath' => true,
|
||||||
'--force' => true,
|
'--force' => true,
|
||||||
],
|
],
|
||||||
'tenancy.central_domains' => ['localhost', '127.0.0.1'],
|
'tenancy.identification.central_domains' => ['localhost', '127.0.0.1'],
|
||||||
'tenancy.bootstrappers' => [],
|
'tenancy.bootstrappers' => [],
|
||||||
'queue.connections.central' => [
|
'queue.connections.central' => [
|
||||||
'driver' => 'sync',
|
'driver' => 'sync',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue