createMock(LoginRateLimiterInterface::class); $rateLimiter->expects(self::never())->method('recordFailure'); $eventBus = $this->createMock(MessageBusInterface::class); $eventBus->expects(self::never())->method('dispatch'); $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-07 10:00:00'); } }; $tenantResolver = $this->createMock(TenantResolver::class); $handler = new LoginFailureHandler($rateLimiter, $eventBus, $clock, $tenantResolver, $this->createMetricsCollector()); $request = Request::create('/api/login', 'POST', [], [], [], [], json_encode(['email' => 'blocked@example.com', 'password' => 'test'])); $exception = new CustomUserMessageAccountStatusException('Votre compte a été suspendu. Contactez votre établissement.'); $response = $handler->onAuthenticationFailure($request, $exception); self::assertSame(403, $response->getStatusCode()); $data = json_decode($response->getContent(), true); self::assertSame('/errors/account-suspended', $data['type']); self::assertSame('Compte suspendu', $data['title']); } #[Test] public function standardFailureReturns401WithRateLimiting(): void { $rateLimiter = $this->createMock(LoginRateLimiterInterface::class); $rateLimiter->expects(self::once()) ->method('recordFailure') ->willReturn(LoginRateLimitResult::allowed(1, 0, false)); $eventBus = $this->createMock(MessageBusInterface::class); $eventBus->method('dispatch')->willReturnCallback( static fn (object $message) => new Envelope($message), ); $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-07 10:00:00'); } }; $tenantResolver = $this->createMock(TenantResolver::class); $tenantResolver->method('resolve')->willThrowException(TenantNotFoundException::withSubdomain('unknown')); $handler = new LoginFailureHandler($rateLimiter, $eventBus, $clock, $tenantResolver, $this->createMetricsCollector()); $request = Request::create('/api/login', 'POST', [], [], [], [], json_encode(['email' => 'user@example.com', 'password' => 'wrong'])); $exception = new AuthenticationException('Invalid credentials.'); $response = $handler->onAuthenticationFailure($request, $exception); self::assertSame(401, $response->getStatusCode()); $data = json_decode($response->getContent(), true); self::assertSame('/errors/authentication-failed', $data['type']); } }