From 5849089373a09a9661d933e3a41e601b60b44a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sun, 20 Nov 2022 02:32:25 +0100 Subject: [PATCH] Add SessionTenancyBootstrapper (#2) * Add SessionTenancyBootstrapper * Fix code style (php-cs-fixer) * add value to bootstrappers config * tenant aware call test * reproduce issue in tests * fix logic for calling tenant run from central context, finish tests * change laravel version back * bump laravel to ^9.38 * add listener to create tenant DBs Co-authored-by: PHP CS Fixer Co-authored-by: Abrar Ahmad --- assets/config.php | 1 + composer.json | 4 +- phpunit.xml | 1 + .../SessionTenancyBootstrapper.php | 66 ++++++++ tests/Etc/HttpKernel.php | 2 +- ...022_05_11_181442_create_sessions_table.php | 35 +++++ tests/SessionBootstrapperTest.php | 145 ++++++++++++++++++ 7 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 src/Bootstrappers/SessionTenancyBootstrapper.php create mode 100644 tests/Etc/session_migrations/2022_05_11_181442_create_sessions_table.php create mode 100644 tests/SessionBootstrapperTest.php diff --git a/assets/config.php b/assets/config.php index dd3a0dd5..b0223638 100644 --- a/assets/config.php +++ b/assets/config.php @@ -102,6 +102,7 @@ return [ Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class, Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class, + // Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper::class, // Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed ], diff --git a/composer.json b/composer.json index 68f16f25..dfd590b0 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,14 @@ "require": { "php": "^8.1", "ext-json": "*", - "illuminate/support": "^9.0", + "illuminate/support": "^9.38", "spatie/ignition": "^1.4", "ramsey/uuid": "^4.0", "stancl/jobpipeline": "^1.0", "stancl/virtualcolumn": "^1.3" }, "require-dev": { - "laravel/framework": "^9.0", + "laravel/framework": "^9.38", "orchestra/testbench": "^7.0", "league/flysystem-aws-s3-v3": "^3.0", "doctrine/dbal": "^2.10", diff --git a/phpunit.xml b/phpunit.xml index 9d2b9339..0e0a8481 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,6 +23,7 @@ + diff --git a/src/Bootstrappers/SessionTenancyBootstrapper.php b/src/Bootstrappers/SessionTenancyBootstrapper.php new file mode 100644 index 00000000..13dd5bcd --- /dev/null +++ b/src/Bootstrappers/SessionTenancyBootstrapper.php @@ -0,0 +1,66 @@ +resetDatabaseHandler(); + } + + public function revert(): void + { + // 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 + $this->resetDatabaseHandler(config('tenancy.database.central_connection')); + } + + protected function resetDatabaseHandler(string $defaultConnection = null): void + { + $sessionDrivers = $this->session->getDrivers(); + + if (isset($sessionDrivers['database'])) { + /** @var \Illuminate\Session\Store $databaseDriver */ + $databaseDriver = $sessionDrivers['database']; + + $databaseDriver->setHandler($this->createDatabaseHandler($defaultConnection)); + } + } + + protected function createDatabaseHandler(string $defaultConnection = null): DatabaseSessionHandler + { + // Typically returns null, so this falls back to the default DB connection + $connection = $this->config->get('session.connection') ?? $defaultConnection; + + // Based on SessionManager::createDatabaseDriver + return new DatabaseSessionHandler( + $this->container->make('db')->connection($connection), + $this->config->get('session.table'), + $this->config->get('session.lifetime'), + $this->container, + ); + } +} diff --git a/tests/Etc/HttpKernel.php b/tests/Etc/HttpKernel.php index 3bb43c53..4fc4b7dc 100644 --- a/tests/Etc/HttpKernel.php +++ b/tests/Etc/HttpKernel.php @@ -30,7 +30,7 @@ class HttpKernel extends Kernel */ protected $middlewareGroups = [ 'web' => [ - \Orchestra\Testbench\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, // \Illuminate\Session\Middleware\AuthenticateSession::class, diff --git a/tests/Etc/session_migrations/2022_05_11_181442_create_sessions_table.php b/tests/Etc/session_migrations/2022_05_11_181442_create_sessions_table.php new file mode 100644 index 00000000..88b4a316 --- /dev/null +++ b/tests/Etc/session_migrations/2022_05_11_181442_create_sessions_table.php @@ -0,0 +1,35 @@ +string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->text('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('sessions'); + } +} diff --git a/tests/SessionBootstrapperTest.php b/tests/SessionBootstrapperTest.php new file mode 100644 index 00000000..772cb427 --- /dev/null +++ b/tests/SessionBootstrapperTest.php @@ -0,0 +1,145 @@ + 'database']); + config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]); + + Event::listen( + TenantCreated::class, + JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { + return $event->tenant; + })->toListener() + ); + + Event::listen(Events\TenancyInitialized::class, Listeners\BootstrapTenancy::class); + Event::listen(Events\TenancyEnded::class, Listeners\RevertToCentralContext::class); + + // Sessions table for central database + pest()->artisan('migrate', [ + '--path' => __DIR__ . '/Etc/session_migrations', + '--realpath' => true, + ])->assertExitCode(0); + }); + +test('central helper can be used in tenant requests', function (bool $enabled, bool $shouldThrow) { + if ($enabled) { + config()->set( + 'tenancy.bootstrappers', + array_merge(config('tenancy.bootstrappers'), [SessionTenancyBootstrapper::class]), + ); + } + + $tenant = Tenant::create(); + + $tenant->domains()->create(['domain' => 'foo.localhost']); + + // run for tenants + pest()->artisan('tenants:migrate', [ + '--path' => __DIR__ . '/Etc/session_migrations', + '--realpath' => true, + ])->assertExitCode(0); + + Route::middleware(['web', InitializeTenancyByDomain::class])->get('/bar', function () { + session(['message' => 'tenant session']); + + tenancy()->central(function () { + return 'central results'; + }); + + return session('message'); + }); + + // We initialize tenancy before making the request, since sessions work a bit differently in tests + // and we need the DB session handler to use the tenant connection (as it does in a real app on tenant requests). + tenancy()->initialize($tenant); + + try { + $this->withoutExceptionHandling() + ->get('http://foo.localhost/bar') + ->assertOk() + ->assertSee('tenant session'); + + if ($shouldThrow) { + pest()->fail('Exception not thrown'); + } + } catch (Throwable $e) { + if ($shouldThrow) { + pest()->assertTrue(true); // empty assertion to make the test pass + } else { + pest()->fail('Exception thrown: ' . $e->getMessage()); + } + } +})->with([ + ['enabled' => false, 'shouldThrow' => true], + ['enabled' => true, 'shouldThrow' => false], +]); + +test('tenant run helper can be used on central requests', function (bool $enabled, bool $shouldThrow) { + if ($enabled) { + config()->set( + 'tenancy.bootstrappers', + array_merge(config('tenancy.bootstrappers'), [SessionTenancyBootstrapper::class]), + ); + } + + Tenant::create(); + + // run for tenants + pest()->artisan('tenants:migrate', [ + '--path' => __DIR__ . '/Etc/session_migrations', + '--realpath' => true, + ])->assertExitCode(0); + + Route::middleware(['web'])->get('/bar', function () { + session(['message' => 'central session']); + + Tenant::first()->run(function () { + return 'tenant results'; + }); + + return session('message'); + }); + + try { + $this->withoutExceptionHandling() + ->get('http://localhost/bar') + ->assertOk() + ->assertSee('central session'); + + if ($shouldThrow) { + pest()->fail('Exception not thrown'); + } + } catch (Throwable $e) { + if ($shouldThrow) { + pest()->assertTrue(true); // empty assertion to make the test pass + } else { + pest()->fail('Exception thrown: ' . $e->getMessage()); + } + } +})->with([ + ['enabled' => false, 'shouldThrow' => true], + ['enabled' => true, 'shouldThrow' => false], +]);