bootApp(); } public function testGetLoginRendersForm(): void { $response = $this->request('GET', '/login'); self::assertSame(200, $response->getStatusCode()); $body = (string) $response->getBody(); self::assertStringContainsString('Sign in', $body); // Local sign-in toggle present (oidc disabled in this fixture). self::assertStringContainsString('name="username"', $body); self::assertStringContainsString('csrf_token', $body); } public function testCorrectCredentialsLogInAndRedirectToMe(): void { $this->enqueueApiResponse(200, [ 'user_id' => 1, 'role' => 'admin', 'email' => null, 'display_name' => 'Local Admin', 'is_local' => true, ]); // Need a session + csrf token; first GET /login to set one up. $this->request('GET', '/login'); $token = (string) ($_SESSION[CsrfMiddleware::SESSION_KEY] ?? ''); self::assertNotEmpty($token); $body = http_build_query(['csrf_token' => $token, 'username' => 'admin', 'password' => 'test1234']); $response = $this->request('POST', '/login/local', [], $body, 'application/x-www-form-urlencoded'); self::assertSame(303, $response->getStatusCode()); self::assertSame('/app/me', $response->getHeaderLine('Location')); self::assertNotNull($_SESSION['_user'] ?? null); self::assertSame('admin', $_SESSION['_user']['role']); } public function testWrongPasswordRedirectsBackToLoginWithFlash(): void { $this->request('GET', '/login'); $token = (string) ($_SESSION[CsrfMiddleware::SESSION_KEY] ?? ''); $body = http_build_query(['csrf_token' => $token, 'username' => 'admin', 'password' => 'WRONG']); $response = $this->request('POST', '/login/local', [], $body, 'application/x-www-form-urlencoded'); self::assertSame(303, $response->getStatusCode()); self::assertSame('/login', $response->getHeaderLine('Location')); $flash = $_SESSION['_flash'] ?? []; self::assertNotEmpty($flash); self::assertSame('error', $flash[0]['type']); } public function testWrongUsernameAlsoRecordsFailure(): void { $this->request('GET', '/login'); $token = (string) ($_SESSION[CsrfMiddleware::SESSION_KEY] ?? ''); $body = http_build_query(['csrf_token' => $token, 'username' => 'someone', 'password' => 'test1234']); $this->request('POST', '/login/local', [], $body, 'application/x-www-form-urlencoded'); $throttle = $_SESSION['_login_throttle'] ?? null; self::assertNotNull($throttle); self::assertSame(1, $throttle['count']); } public function testCsrfMissingIs403(): void { $this->request('GET', '/login'); $body = http_build_query(['username' => 'admin', 'password' => 'test1234']); $response = $this->request('POST', '/login/local', [], $body, 'application/x-www-form-urlencoded'); self::assertSame(403, $response->getStatusCode()); } public function testFiveFailuresLockOutNextAttempt(): void { $this->request('GET', '/login'); $token = (string) ($_SESSION[CsrfMiddleware::SESSION_KEY] ?? ''); $bad = http_build_query(['csrf_token' => $token, 'username' => 'admin', 'password' => 'WRONG']); for ($i = 0; $i < 5; ++$i) { $this->request('POST', '/login/local', [], $bad, 'application/x-www-form-urlencoded'); } // 6th attempt — even with correct credentials — gets the lockout flash. $this->enqueueApiResponse(200, [ 'user_id' => 1, 'role' => 'admin', 'email' => null, 'display_name' => 'Local Admin', 'is_local' => true, ]); $good = http_build_query(['csrf_token' => $token, 'username' => 'admin', 'password' => 'test1234']); $response = $this->request('POST', '/login/local', [], $good, 'application/x-www-form-urlencoded'); self::assertSame(303, $response->getStatusCode()); self::assertSame('/login', $response->getHeaderLine('Location')); $flash = $_SESSION['_flash'] ?? []; self::assertNotEmpty($flash); self::assertStringContainsStringIgnoringCase('too many', $flash[0]['message']); } public function testApiDownDuringUpsertFlashesError(): void { $this->enqueueApiException(new \GuzzleHttp\Exception\ConnectException( 'connection refused', new \GuzzleHttp\Psr7\Request('POST', '/api/v1/auth/users/upsert-local'), )); $this->enqueueApiException(new \GuzzleHttp\Exception\ConnectException( 'connection refused', new \GuzzleHttp\Psr7\Request('POST', '/api/v1/auth/users/upsert-local'), )); $this->request('GET', '/login'); $token = (string) ($_SESSION[CsrfMiddleware::SESSION_KEY] ?? ''); $body = http_build_query(['csrf_token' => $token, 'username' => 'admin', 'password' => 'test1234']); $response = $this->request('POST', '/login/local', [], $body, 'application/x-www-form-urlencoded'); self::assertSame(303, $response->getStatusCode()); self::assertSame('/login', $response->getHeaderLine('Location')); $flash = $_SESSION['_flash'] ?? []; self::assertNotEmpty($flash); self::assertStringContainsStringIgnoringCase('api', $flash[0]['message']); } }