createStub(TenantContextInterface::class); $tenant->method('identifier')->willReturn('tenant-a'); $tenant->method('configuration')->willReturn(new TenantConfiguration()); $session = new AuthenticationSession( id: 'auth-session', state: AuthenticationSession::STATE_IDENTIFIED, tenantIdentifier: 'tenant-a', userIdentifier: 'user-a', userIdentity: 'person@example.com', methodsAvailable: ['password'], createdAt: time(), expiresAt: time() + 300, ); $cache = $this->createMock(EphemeralCacheInterface::class); $cache->expects($this->once()) ->method('get') ->with('auth-session', CacheScope::Tenant, 'auth') ->willReturn($session); $cache->expects($this->once())->method('set')->willReturn(true); $cache->expects($this->once()) ->method('delete') ->with('auth-session', CacheScope::Tenant, 'auth') ->willReturn(true); $provider = new SuccessfulAuthenticationProvider(); $providers = $this->createStub(ProviderManager::class); $providers->method('resolve')->willReturn($provider); $users = $this->createMock(UserAccountsService::class); $users->expects($this->once()) ->method('fetchByIdentifier') ->with('user-a') ->willReturn([ 'uid' => 'user-a', 'identity' => 'person@example.com', 'label' => 'Person', 'permissions' => [], ]); $tokens = $this->createStub(TokenService::class); $tokens->method('createToken')->willReturn('token'); $events = $this->createMock(EventDispatcherInterface::class); $events->expects($this->once()) ->method('dispatch') ->with(self::callback(static fn($event): bool => $event instanceof AuthenticationSucceededEvent && $event->getUserId() === 'user-a' && $event->tenantIdentifier() === 'tenant-a' )); $manager = new AuthenticationManager( $tenant, $cache, $providers, $tokens, $users, $events, ); $response = $manager->handle(AuthenticationRequest::verify( 'auth-session', 'password', 'correct', )); self::assertTrue($response->isSuccess()); } #[Test] public function rejectedCredentialPublishesAuthenticationFailedEvent(): void { $tenant = $this->createStub(TenantContextInterface::class); $tenant->method('configuration')->willReturn(new TenantConfiguration()); $session = new AuthenticationSession( id: 'auth-session', state: AuthenticationSession::STATE_IDENTIFIED, tenantIdentifier: 'tenant-a', userIdentifier: 'user-a', userIdentity: 'person@example.com', methodsAvailable: ['password'], createdAt: time(), expiresAt: time() + 300, ); $cache = $this->createMock(EphemeralCacheInterface::class); $cache->expects($this->once()) ->method('get') ->with('auth-session', CacheScope::Tenant, 'auth') ->willReturn($session); $cache->expects($this->once()) ->method('set') ->willReturn(true); $provider = new class implements AuthenticationProviderInterface { public int $verificationCount = 0; public function type(): string { return 'authentication'; } public function identifier(): string { return 'password'; } public function label(): string { return 'Password'; } public function description(): string { return 'Test provider'; } public function method(): string { return self::METHOD_CREDENTIAL; } public function icon(): string { return ''; } public function verify(ProviderContext $context, string $secret): ProviderResult { $this->verificationCount++; return ProviderResult::failed(ProviderResult::ERROR_INVALID_FACTOR); } public function beginChallenge(ProviderContext $context): ProviderResult { return ProviderResult::failed(); } public function verifyChallenge(ProviderContext $context, string $code): ProviderResult { return ProviderResult::failed(); } public function beginRedirect( ProviderContext $context, string $callbackUrl, ?string $returnUrl = null, ): ProviderResult { return ProviderResult::failed(); } public function completeRedirect(ProviderContext $context, array $params): ProviderResult { return ProviderResult::failed(); } }; $providers = $this->createMock(ProviderManager::class); $providers->method('resolve')->with('authentication', 'password')->willReturn($provider); $events = $this->createMock(EventDispatcherInterface::class); $events->expects($this->once()) ->method('dispatch') ->with(self::callback(static function ($event): bool { return $event instanceof AuthenticationFailedEvent && $event->getUserId() === 'user-a' && $event->getReason() === ProviderResult::ERROR_INVALID_FACTOR && $event->tenantIdentifier() === 'tenant-a'; })); $manager = new AuthenticationManager( $tenant, $cache, $providers, $this->createStub(TokenService::class), $this->createStub(UserAccountsService::class), $events, ); $response = $manager->handle(AuthenticationRequest::verify( 'auth-session', 'password', 'incorrect', )); self::assertTrue($response->isFailed()); self::assertSame(1, $provider->verificationCount); } } final class SuccessfulAuthenticationProvider implements AuthenticationProviderInterface { public function type(): string { return 'authentication'; } public function identifier(): string { return 'password'; } public function label(): string { return 'Password'; } public function description(): string { return 'Successful test provider'; } public function method(): string { return self::METHOD_CREDENTIAL; } public function icon(): string { return ''; } public function verify(ProviderContext $context, string $secret): ProviderResult { return ProviderResult::success(); } public function beginChallenge(ProviderContext $context): ProviderResult { return ProviderResult::failed(); } public function verifyChallenge(ProviderContext $context, string $code): ProviderResult { return ProviderResult::failed(); } public function beginRedirect( ProviderContext $context, string $callbackUrl, ?string $returnUrl = null, ): ProviderResult { return ProviderResult::failed(); } public function completeRedirect(ProviderContext $context, array $params): ProviderResult { return ProviderResult::failed(); } }