import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AuthGate } from "@/components/auth/auth-gate";
import { AUTH_TOKEN_KEY } from "@/lib/auth/client";
const identity = {
id: "00000000-0000-4000-8000-000000000042",
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,
};
function jsonResponse(body: unknown, init?: ResponseInit): Response {
return new Response(JSON.stringify(body), {
headers: { "Content-Type": "application/json" },
...init,
});
}
describe("AuthGate", () => {
let tokens: Map<string, string>;
beforeEach(() => {
tokens = new Map();
vi.stubGlobal("localStorage", {
clear: vi.fn(() => tokens.clear()),
getItem: vi.fn((key: string) => tokens.get(key) ?? null),
removeItem: vi.fn((key: string) => tokens.delete(key)),
setItem: vi.fn((key: string, value: string) => tokens.set(key, value)),
});
vi.stubGlobal("fetch", vi.fn());
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
it("never renders login while a stored token is being validated", () => {
tokens.set(AUTH_TOKEN_KEY, "valid-token");
vi.mocked(fetch).mockReturnValue(new Promise(() => undefined));
render(<AuthGate />);
expect(screen.getByLabelText("Проверяем доступ")).toBeInTheDocument();
expect(screen.getByText("Проверяем доступ")).toBeInTheDocument();
expect(
screen.queryByRole("heading", { name: "Вход" }),
).not.toBeInTheDocument();
});
it("opens the chat after a valid identity response", async () => {
tokens.set(AUTH_TOKEN_KEY, "valid-token");
vi.mocked(fetch).mockResolvedValue(
jsonResponse({
user: identity,
}),
);
render(<AuthGate />);
expect(await screen.findByText("Новый чат")).toBeInTheDocument();
});
it("rejects an identity with an invalid model slug", async () => {
tokens.set(AUTH_TOKEN_KEY, "valid-token");
vi.mocked(fetch).mockResolvedValue(
jsonResponse({ user: { ...identity, lastModelId: "GPT-4" } }),
);
render(<AuthGate />);
expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
});
it("sends the stored token with every identity request", async () => {
tokens.set(AUTH_TOKEN_KEY, "valid-token");
vi.mocked(fetch).mockResolvedValue(
jsonResponse({
user: identity,
}),
);
render(<AuthGate />);
await screen.findByText("Новый чат");
const [, init] = vi.mocked(fetch).mock.calls[0];
expect(init?.cache).toBe("no-store");
expect(new Headers(init?.headers).get("Authorization")).toBe(
"Bearer valid-token",
);
});
it("shows login when no token is stored", async () => {
render(<AuthGate />);
expect(screen.getByLabelText("Проверяем доступ")).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
expect(vi.mocked(fetch)).not.toHaveBeenCalled();
});
it("clears an invalid token and shows login", async () => {
localStorage.setItem(AUTH_TOKEN_KEY, "invalid-token");
vi.mocked(fetch).mockResolvedValue(jsonResponse({ error: "Unauthorized" }, { status: 401 }));
render(<AuthGate />);
expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
expect(tokens.get(AUTH_TOKEN_KEY)).toBeUndefined();
});
it("stores the token and opens chat after successful login", async () => {
const user = userEvent.setup();
vi.mocked(fetch).mockResolvedValue(
jsonResponse({
token: "new-token",
user: identity,
}),
);
render(<AuthGate />);
await screen.findByRole("heading", { name: "Вход" });
await user.type(screen.getByLabelText("Логин или почта"), "alice");
await user.type(screen.getByLabelText("Пароль"), "Demo1234!");
await user.click(screen.getByRole("button", { name: "Продолжить" }));
expect(await screen.findByText("Новый чат")).toBeInTheDocument();
expect(tokens.get(AUTH_TOKEN_KEY)).toBe("new-token");
});
it("removes the token when the authenticated user logs out", async () => {
const user = userEvent.setup();
tokens.set(AUTH_TOKEN_KEY, "valid-token");
vi.mocked(fetch).mockResolvedValue(
jsonResponse({
user: identity,
}),
);
render(<AuthGate />);
await user.click(await screen.findByRole("button", { name: "Выйти" }));
expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
expect(tokens.get(AUTH_TOKEN_KEY)).toBeUndefined();
});
it("ends the authenticated session when the chat API returns 401", async () => {
const user = userEvent.setup();
tokens.set(AUTH_TOKEN_KEY, "expired-token");
vi.mocked(fetch)
.mockResolvedValueOnce(
jsonResponse({
user: identity,
}),
)
.mockResolvedValueOnce(
jsonResponse({ error: "Unauthorized" }, { status: 401 }),
);
render(<AuthGate />);
await user.type(await screen.findByLabelText("Сообщение"), "Привет{enter}");
expect(await screen.findByRole("heading", { name: "Вход" })).toBeInTheDocument();
expect(tokens.get(AUTH_TOKEN_KEY)).toBeUndefined();
expect(screen.queryByLabelText("Сообщение")).not.toBeInTheDocument();
});
});