aegida-console / app / api / auth / routes.test.ts
routes.test.ts
Raw
// @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();
  });
});