// @vitest-environment node import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { authenticateWithUserService, findUserById, upsertExternalUser } = vi.hoisted( () => ({ authenticateWithUserService: vi.fn(), findUserById: vi.fn(), upsertExternalUser: vi.fn(), }), ); vi.mock("server-only", () => ({})); vi.mock("@/lib/db/pool", () => ({ getPool: () => ({}) })); vi.mock("@/lib/auth/user-service", async (importOriginal) => ({ ...(await importOriginal()), authenticateWithUserService, })); vi.mock("@/lib/db/users", async (importOriginal) => ({ ...(await importOriginal()), findUserById, upsertExternalUser, })); import { POST as login } from "@/app/api/auth/login/route"; import { GET as me } from "@/app/api/auth/me/route"; import { signAuthToken, verifyAuthToken } from "@/lib/auth/server"; beforeEach(() => { vi.stubEnv("JWT_SECRET", "ai-control-center-local-development-secret-2026"); vi.stubEnv("JWT_ISSUER", "corp-ui"); vi.stubEnv("JWT_AUDIENCE", "aegida-gate"); vi.stubEnv("AEGIDA_TENANT_ID", "tenant-1"); authenticateWithUserService.mockReset(); findUserById.mockReset(); upsertExternalUser.mockReset(); }); afterEach(() => vi.unstubAllEnvs()); describe("auth routes", () => { const profile = { id: "42", login: "alice", fullName: "Alice Smith", position: "Engineer", email: "alice@example.com", firstName: "Alice", lastName: "Smith", middleName: "Jane", roles: ["admin", "employee"], createdAt: "2026-07-01T12:00:00.000Z", updatedAt: "2026-07-02T12:00:00.000Z", }; const databaseUser = { id: "00000000-0000-4000-8000-000000000001", externalUserId: "42", login: "alice", fullName: "Alice Smith", position: "Engineer", email: "alice@example.com", firstName: "Alice", lastName: "Smith", middleName: "Jane", roles: ["admin", "employee"], lastModelId: "chatgpt" as const, passwordHash: "", createdAt: "2026-07-01T12:00:00.000Z", updatedAt: "2026-07-02T12:00:00.000Z", }; it("authenticates upstream, upserts the profile and issues an app JWT", async () => { const log = vi.spyOn(console, "log").mockImplementation(() => undefined); const warn = vi.spyOn(console, "warn").mockImplementation(() => undefined); const error = vi.spyOn(console, "error").mockImplementation(() => undefined); authenticateWithUserService.mockResolvedValue(profile); upsertExternalUser.mockResolvedValue(databaseUser); const response = await login( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "alice", password: "secret", }), }), ); expect(response.status).toBe(200); expect(response.headers.get("Cache-Control")).toBe("no-store"); const responseBody = await response.json(); expect(responseBody).toMatchObject({ token: expect.any(String), user: { id: databaseUser.id, externalUserId: "42", fullName: "Alice Smith", position: "Engineer", roles: ["admin", "employee"], }, }); await expect(verifyAuthToken(responseBody.token)).resolves.toMatchObject({ id: databaseUser.id, email: databaseUser.email, externalUserId: databaseUser.externalUserId, tenantId: "tenant-1", }); for (const spy of [log, warn, error]) { expect(spy).not.toHaveBeenCalled(); } expect(authenticateWithUserService).toHaveBeenCalledWith( "alice", "secret", expect.any(AbortSignal), ); expect(upsertExternalUser).toHaveBeenCalledWith(expect.anything(), profile); }); it("returns the generic invalid-credentials response from the user service", async () => { const { UserServiceAuthError } = await import("@/lib/auth/user-service"); authenticateWithUserService.mockRejectedValue( new UserServiceAuthError("invalid_credentials"), ); const response = await login( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "demo@ai-control.local", password: "wrong-password", }), }), ); expect(response.status).toBe(401); expect(await response.json()).toEqual({ error: "Неверная почта или пароль" }); }); it("returns service-unavailable when the user service cannot authenticate", async () => { const { UserServiceAuthError } = await import("@/lib/auth/user-service"); authenticateWithUserService.mockRejectedValue(new UserServiceAuthError("unavailable")); const response = await login( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "alice", password: "secret" }), }), ); expect(response.status).toBe(503); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(await response.json()).toEqual({ error: "Сервис аутентификации временно недоступен", }); }); it("returns a bad-request response for malformed login JSON", async () => { const response = await login( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{", }), ); expect(response.status).toBe(400); expect(await response.json()).toEqual({ error: "Некорректный запрос" }); }); it.each([ { email: " ", password: "secret" }, { email: "alice", password: " \t " }, ])("rejects blank credentials before calling user-service", async (body) => { const response = await login( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }), ); expect(response.status).toBe(400); expect(authenticateWithUserService).not.toHaveBeenCalled(); }); it("returns the authenticated identity for a valid bearer token", async () => { findUserById.mockResolvedValue(databaseUser); const token = await signAuthToken({ id: databaseUser.id, email: databaseUser.email, externalUserId: databaseUser.externalUserId, }); const response = await me( new Request("http://localhost/api/auth/me", { headers: { Authorization: `Bearer ${token}` }, }), ); expect(response.status).toBe(200); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(await response.json()).toEqual({ user: { id: databaseUser.id, externalUserId: "42", login: "alice", fullName: "Alice Smith", position: "Engineer", email: "alice@example.com", firstName: "Alice", lastName: "Smith", middleName: "Jane", roles: ["admin", "employee"], lastModelId: "chatgpt", }, }); }); it("returns a generic unauthorized response for an invalid bearer token", async () => { const response = await me( new Request("http://localhost/api/auth/me", { headers: { Authorization: "Bearer invalid" }, }), ); expect(response.status).toBe(401); expect(response.headers.get("Cache-Control")).toBe("no-store"); expect(await response.json()).toEqual({ error: "Unauthorized" }); }); it("rejects a non-UUID JWT subject before querying users", async () => { const token = await signAuthToken({ id: "legacy-user", email: "alice@example.com", externalUserId: "42", }); const response = await me( new Request("http://localhost/api/auth/me", { headers: { Authorization: `Bearer ${token}` }, }), ); expect(response.status).toBe(401); expect(findUserById).not.toHaveBeenCalled(); }); });