diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9e2ff55..ef9a9b29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,12 +16,13 @@ jobs: strategy: matrix: include: - - laravel: 9 - php: "8.0" - laravel: 10 php: "8.1" - laravel: 11 php: "8.3" + - laravel: 12 + php: "8.3" + # Ideally we'd run at least one of these on PHP 8.4, however the Dockerfile seems to require some changes for that steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/queue.yml b/.github/workflows/queue.yml new file mode 100644 index 00000000..2b7fd4d4 --- /dev/null +++ b/.github/workflows/queue.yml @@ -0,0 +1,35 @@ +name: Queue tests + +on: + - push + +jobs: + queue: + name: Queue application tests + runs-on: ubuntu-latest + steps: + - name: Prepare composer version constraint prefix + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + # For refs like "refs/tags/v3.9.0", remove "refs/tags/v" prefix to get just "3.9.0" + VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION_PREFIX=${VERSION}" >> $GITHUB_ENV + else + BRANCH=${GITHUB_REF#refs/heads/} + if [[ $BRANCH =~ ^[0-9]\.x$ ]]; then + # Branches starting with %d.x need to use -dev suffix + echo "VERSION_PREFIX=${BRANCH}-dev" >> $GITHUB_ENV + else + # All other branches use dev-${branch} prefix + echo "VERSION_PREFIX=dev-${BRANCH}" >> $GITHUB_ENV + fi + fi + + - name: Clone test suite + run: git clone https://github.com/archtechx/tenancy-queue-tester + + - name: Run tests + run: | + cd tenancy-queue-tester + TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./setup.sh + TENANCY_VERSION=${VERSION_PREFIX}#${GITHUB_SHA} ./test.sh diff --git a/assets/config.php b/assets/config.php index a10ad33c..a9fd2b92 100644 --- a/assets/config.php +++ b/assets/config.php @@ -194,6 +194,6 @@ return [ */ 'seeder_parameters' => [ '--class' => 'DatabaseSeeder', // root seeder class - // '--force' => true, + // '--force' => true, // This needs to be true to seed tenant databases in production ], ]; diff --git a/composer.json b/composer.json index 21c0744e..b3070565 100644 --- a/composer.json +++ b/composer.json @@ -12,15 +12,15 @@ "require": { "php": "^8.0", "ext-json": "*", - "illuminate/support": "^9.0|^10.0|^11.0", + "illuminate/support": "^10.0|^11.0|^12.0", "facade/ignition-contracts": "^1.0.2", "ramsey/uuid": "^4.7.3", - "stancl/jobpipeline": "^1.6.2", - "stancl/virtualcolumn": "^1.3.1" + "stancl/jobpipeline": "^1.8.0", + "stancl/virtualcolumn": "^1.5.0" }, "require-dev": { - "laravel/framework": "^9.0|^10.0|^11.0", - "orchestra/testbench": "^7.0|^8.0|^9.0", + "laravel/framework": "^10.0|^11.0|^12.0", + "orchestra/testbench": "^8.0|^9.0|^10.0", "league/flysystem-aws-s3-v3": "^3.12.2", "doctrine/dbal": "^3.6.0", "spatie/valuestore": "^1.3.2" diff --git a/src/Database/Concerns/InvalidatesResolverCache.php b/src/Database/Concerns/InvalidatesResolverCache.php index 7dff35ad..2da00e52 100644 --- a/src/Database/Concerns/InvalidatesResolverCache.php +++ b/src/Database/Concerns/InvalidatesResolverCache.php @@ -18,13 +18,16 @@ trait InvalidatesResolverCache public static function bootInvalidatesResolverCache() { - static::saved(function (Tenant $tenant) { + $invalidateCache = static function (Tenant $tenant) { foreach (static::$resolvers as $resolver) { /** @var CachedTenantResolver $resolver */ $resolver = app($resolver); $resolver->invalidateCache($tenant); } - }); + }; + + static::saved($invalidateCache); + static::deleting($invalidateCache); } } diff --git a/src/Database/Concerns/InvalidatesTenantsResolverCache.php b/src/Database/Concerns/InvalidatesTenantsResolverCache.php index 555aceeb..4101faa7 100644 --- a/src/Database/Concerns/InvalidatesTenantsResolverCache.php +++ b/src/Database/Concerns/InvalidatesTenantsResolverCache.php @@ -21,13 +21,16 @@ trait InvalidatesTenantsResolverCache public static function bootInvalidatesTenantsResolverCache() { - static::saved(function (Model $model) { + $invalidateCache = static function (Model $model) { foreach (static::$resolvers as $resolver) { /** @var CachedTenantResolver $resolver */ $resolver = app($resolver); $resolver->invalidateCache($model->tenant); } - }); + }; + + static::saved($invalidateCache); + static::deleting($invalidateCache); } } diff --git a/src/Exceptions/ModelNotSyncMasterException.php b/src/Exceptions/ModelNotSyncMasterException.php index ee5feb9a..09cabb06 100644 --- a/src/Exceptions/ModelNotSyncMasterException.php +++ b/src/Exceptions/ModelNotSyncMasterException.php @@ -10,6 +10,6 @@ class ModelNotSyncMasterException extends Exception { public function __construct(string $class) { - parent::__construct("Model of $class class is not an SyncMaster model. Make sure you're using the central model to make changes to synced resources when you're in the central context"); + parent::__construct("Model of $class class is not a SyncMaster model. Make sure you're using the central model to make changes to synced resources when you're in the central context."); } } diff --git a/src/Features/UserImpersonation.php b/src/Features/UserImpersonation.php index 48d65bb9..5413375b 100644 --- a/src/Features/UserImpersonation.php +++ b/src/Features/UserImpersonation.php @@ -18,7 +18,7 @@ class UserImpersonation implements Feature public function bootstrap(Tenancy $tenancy): void { - $tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, string $authGuard = null): ImpersonationToken { + $tenancy->macro('impersonate', function (Tenant $tenant, string $userId, string $redirectUrl, ?string $authGuard = null): ImpersonationToken { return ImpersonationToken::create([ 'tenant_id' => $tenant->getTenantKey(), 'user_id' => $userId, @@ -32,10 +32,10 @@ class UserImpersonation implements Feature * Impersonate a user and get an HTTP redirect response. * * @param string|ImpersonationToken $token - * @param int $ttl + * @param int|null $ttl * @return RedirectResponse */ - public static function makeResponse($token, int $ttl = null): RedirectResponse + public static function makeResponse($token, ?int $ttl = null): RedirectResponse { $token = $token instanceof ImpersonationToken ? $token : ImpersonationToken::findOrFail($token); diff --git a/src/Middleware/InitializeTenancyByPath.php b/src/Middleware/InitializeTenancyByPath.php index 6289199b..3b691259 100644 --- a/src/Middleware/InitializeTenancyByPath.php +++ b/src/Middleware/InitializeTenancyByPath.php @@ -40,10 +40,8 @@ class InitializeTenancyByPath extends IdentificationMiddleware return $this->initializeTenancy( $request, $next, $route ); - } else { - throw new RouteIsMissingTenantParameterException; } - return $next($request); + throw new RouteIsMissingTenantParameterException; } } diff --git a/tests/CachedTenantResolverTest.php b/tests/CachedTenantResolverTest.php index e7eb52d3..940c950f 100644 --- a/tests/CachedTenantResolverTest.php +++ b/tests/CachedTenantResolverTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; use Illuminate\Support\Facades\DB; +use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException; use Stancl\Tenancy\Resolvers\DomainTenantResolver; use Stancl\Tenancy\Tests\Etc\Tenant; @@ -80,6 +81,33 @@ class CachedTenantResolverTest extends TestCase $this->assertNotEmpty(DB::getQueryLog()); // not empty } + /** @test */ + public function cache_is_invalidated_when_the_tenant_is_deleted() + { + $tenant = Tenant::create(); + $tenant->createDomain([ + 'domain' => 'acme', + ]); + + DB::enableQueryLog(); + + DomainTenantResolver::$shouldCache = true; + + $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); + DB::flushQueryLog(); + $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); + $this->assertEmpty(DB::getQueryLog()); // empty + + $tenant->delete(); + DB::flushQueryLog(); + + $this->assertThrows(function () { + app(DomainTenantResolver::class)->resolve('acme'); + }, TenantCouldNotBeIdentifiedOnDomainException::class); + + $this->assertNotEmpty(DB::getQueryLog()); // not empty - cache cleared so the DB was queried + } + /** @test */ public function cache_is_invalidated_when_a_tenants_domain_is_changed() { @@ -109,4 +137,31 @@ class CachedTenantResolverTest extends TestCase $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('bar'))); $this->assertNotEmpty(DB::getQueryLog()); // not empty } + + /** @test */ + public function cache_is_invalidated_when_a_tenants_domain_is_deleted() + { + $tenant = Tenant::create(); + $tenant->createDomain([ + 'domain' => 'acme', + ]); + + DB::enableQueryLog(); + + DomainTenantResolver::$shouldCache = true; + + $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); + DB::flushQueryLog(); + $this->assertTrue($tenant->is(app(DomainTenantResolver::class)->resolve('acme'))); + $this->assertEmpty(DB::getQueryLog()); // empty + + $tenant->domains->first()->delete(); + DB::flushQueryLog(); + + $this->assertThrows(function () { + app(DomainTenantResolver::class)->resolve('acme'); + }, TenantCouldNotBeIdentifiedOnDomainException::class); + + $this->assertNotEmpty(DB::getQueryLog()); // not empty - cache cleared so the DB was queried + } }