tenantContext = new TenantContext(); } #[Test] public function itAbstainsForNonTenantAwareSubjects(): void { $voter = new TenantVoter($this->tenantContext); $token = $this->createMock(TokenInterface::class); $subject = new stdClass(); $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]); self::assertSame(VoterInterface::ACCESS_ABSTAIN, $result); } #[Test] public function itAbstainsForNonTenantAccessAttributes(): void { $tenantIdString = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; $this->setCurrentTenant($tenantIdString, 'ecole-alpha'); $voter = new TenantVoter($this->tenantContext); $user = $this->createMock(UserInterface::class); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn($user); $subject = $this->createTenantAwareSubject($tenantIdString); // Voter should abstain for other attributes to not bypass other voters foreach (['VIEW', 'EDIT', 'DELETE', 'ROLE_ADMIN'] as $attribute) { $result = $voter->vote($token, $subject, [$attribute]); self::assertSame(VoterInterface::ACCESS_ABSTAIN, $result, "Should abstain for: {$attribute}"); } } #[Test] public function itDeniesAccessWhenUserNotAuthenticated(): void { $this->setCurrentTenant('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'ecole-alpha'); $voter = new TenantVoter($this->tenantContext); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn(null); $subject = $this->createTenantAwareSubject('a1b2c3d4-e5f6-7890-abcd-ef1234567890'); $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]); self::assertSame(VoterInterface::ACCESS_DENIED, $result); } #[Test] public function itGrantsAccessWhenSubjectBelongsToCurrentTenant(): void { $tenantIdString = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; $this->setCurrentTenant($tenantIdString, 'ecole-alpha'); $voter = new TenantVoter($this->tenantContext); $user = $this->createMock(UserInterface::class); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn($user); $subject = $this->createTenantAwareSubject($tenantIdString); $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]); self::assertSame(VoterInterface::ACCESS_GRANTED, $result); } #[Test] public function itDeniesAccessWhenSubjectBelongsToDifferentTenant(): void { // Current tenant is alpha $this->setCurrentTenant('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'ecole-alpha'); $voter = new TenantVoter($this->tenantContext); $user = $this->createMock(UserInterface::class); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn($user); // Subject belongs to beta tenant $subject = $this->createTenantAwareSubject('b2c3d4e5-f6a7-8901-bcde-f12345678901'); $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]); // Should be DENIED (will be converted to 404 by access denied handler) self::assertSame(VoterInterface::ACCESS_DENIED, $result); } #[Test] public function itDeniesAccessWhenNoTenantContextSet(): void { // Don't set any tenant context $voter = new TenantVoter($this->tenantContext); $user = $this->createMock(UserInterface::class); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn($user); $subject = $this->createTenantAwareSubject('a1b2c3d4-e5f6-7890-abcd-ef1234567890'); $result = $voter->vote($token, $subject, [TenantVoter::ATTRIBUTE]); self::assertSame(VoterInterface::ACCESS_DENIED, $result); } private function setCurrentTenant(string $tenantIdString, string $subdomain): void { $tenantId = TenantId::fromString($tenantIdString); $config = new TenantConfig( tenantId: $tenantId, subdomain: $subdomain, databaseUrl: "postgresql://user:pass@localhost:5432/classeo_{$subdomain}", ); $this->tenantContext->setCurrentTenant($config); } private function createTenantAwareSubject(string $tenantIdString): TenantAwareInterface { $tenantId = TenantId::fromString($tenantIdString); $subject = $this->createMock(TenantAwareInterface::class); $subject->method('getTenantId')->willReturn($tenantId); return $subject; } }