// @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<typeof import("@/lib/auth/user-service")>()),
authenticateWithUserService,
}));
vi.mock("@/lib/db/users", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/db/users")>()),
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();
});
});