245 lines
8.0 KiB
TypeScript
245 lines
8.0 KiB
TypeScript
import { z } from "zod";
|
|
import { describe, expect, test, vi } from "vitest";
|
|
|
|
import { login, logout } from "@/features/auth/api/auth-api";
|
|
import { createApiClient } from "@/shared/api/client";
|
|
import { createApiResponseSchema } from "@/shared/api/types";
|
|
import { createMockHandlers, createMockStore } from "@/shared/mocks/handlers";
|
|
import { server } from "@/shared/test/server";
|
|
|
|
const apiBaseUrl = "https://api.example.com";
|
|
const adminToken = "mock-admin-jwt";
|
|
|
|
const aiCharactersPreviewSchema = z.object({
|
|
totalCount: z.number(),
|
|
content: z.array(z.unknown()),
|
|
});
|
|
|
|
function createClient(token: string | null = adminToken) {
|
|
return createApiClient({
|
|
getToken: () => token,
|
|
clearSession: vi.fn(),
|
|
onAuthExpired: vi.fn(),
|
|
});
|
|
}
|
|
|
|
function useMockHandlers(store = createMockStore()) {
|
|
server.use(...createMockHandlers(store, apiBaseUrl));
|
|
return store;
|
|
}
|
|
|
|
describe("mock auth handlers", () => {
|
|
test("use the production admin login endpoint, request body, headers, and response envelope", async () => {
|
|
// Given
|
|
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
|
useMockHandlers();
|
|
|
|
// When
|
|
const session = await login(createClient("caller-token"), { email: "admin@test.com", password: "password" });
|
|
const invalidBodyResponse = await fetch(`${apiBaseUrl}/admin/member/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: "admin@test.com" }),
|
|
});
|
|
const authorizationResponse = await fetch(`${apiBaseUrl}/admin/member/login`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${adminToken}`, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: "admin@test.com", password: "password" }),
|
|
});
|
|
|
|
// Then
|
|
expect(session).toEqual({ token: adminToken, role: "ADMIN" });
|
|
await expect(invalidBodyResponse.json()).resolves.toEqual({
|
|
success: false,
|
|
message: "잘못된 요청입니다.",
|
|
data: null,
|
|
errorProperty: null,
|
|
});
|
|
expect(invalidBodyResponse.status).toBe(400);
|
|
expect(authorizationResponse.status).toBe(400);
|
|
});
|
|
|
|
test("does not handle login requests from a different API origin", async () => {
|
|
// Given
|
|
useMockHandlers();
|
|
|
|
// When
|
|
const request = fetch("https://wrong-origin.example/admin/member/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: "admin@test.com", password: "password" }),
|
|
});
|
|
|
|
// Then
|
|
await expect(request).rejects.toThrow();
|
|
});
|
|
|
|
test("requires Bearer and no body for production logout", async () => {
|
|
// Given
|
|
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
|
useMockHandlers();
|
|
|
|
// When
|
|
const missingBearerResponse = await fetch(`${apiBaseUrl}/member/logout`, { method: "POST" });
|
|
const bodyResponse = await fetch(`${apiBaseUrl}/member/logout`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
body: "{}",
|
|
});
|
|
|
|
// Then
|
|
expect(missingBearerResponse.status).toBe(401);
|
|
expect(bodyResponse.status).toBe(400);
|
|
});
|
|
|
|
test("returns 403 when logout uses a non-ADMIN Bearer token", async () => {
|
|
// Given
|
|
useMockHandlers();
|
|
|
|
// When
|
|
const response = await fetch(`${apiBaseUrl}/member/logout`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer mock-member-jwt" },
|
|
});
|
|
|
|
// Then
|
|
expect(response.status).toBe(403);
|
|
await expect(response.json()).resolves.toEqual({
|
|
success: false,
|
|
message: "접근 권한이 없습니다.",
|
|
data: null,
|
|
errorProperty: null,
|
|
});
|
|
});
|
|
|
|
test("returns 415 when login uses a non-JSON media type", async () => {
|
|
// Given
|
|
useMockHandlers();
|
|
|
|
// When
|
|
const response = await fetch(`${apiBaseUrl}/admin/member/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "text/plain" },
|
|
body: JSON.stringify({ email: "admin@test.com", password: "password" }),
|
|
});
|
|
|
|
// Then
|
|
expect(response.status).toBe(415);
|
|
await expect(response.json()).resolves.toEqual({
|
|
success: false,
|
|
message: "지원하지 않는 미디어 타입입니다.",
|
|
data: null,
|
|
errorProperty: null,
|
|
});
|
|
});
|
|
|
|
test("returns 401 when logout uses an invalid or already revoked token", async () => {
|
|
// Given
|
|
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
|
useMockHandlers();
|
|
|
|
// When
|
|
const invalidTokenResponse = await fetch(`${apiBaseUrl}/member/logout`, {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer invalid-token" },
|
|
});
|
|
await logout(createClient(adminToken));
|
|
const revokedTokenResponse = await fetch(`${apiBaseUrl}/member/logout`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
});
|
|
|
|
// Then
|
|
expect(invalidTokenResponse.status).toBe(401);
|
|
expect(revokedTokenResponse.status).toBe(401);
|
|
});
|
|
|
|
test("resets the in-memory auth store to seed when a fresh mock store is created", async () => {
|
|
// Given
|
|
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
|
useMockHandlers();
|
|
await logout(createClient(adminToken));
|
|
|
|
// When
|
|
const staleStoreResponse = await fetch(`${apiBaseUrl}/api/v2/admin/ai-characters?page=0&size=20`, {
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
});
|
|
useMockHandlers(createMockStore());
|
|
const freshStoreResponse = await fetch(`${apiBaseUrl}/api/v2/admin/ai-characters?page=0&size=20`, {
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
});
|
|
const parsedFreshResponse = createApiResponseSchema(aiCharactersPreviewSchema).parse(await freshStoreResponse.json());
|
|
|
|
// Then
|
|
expect(staleStoreResponse.status).toBe(401);
|
|
expect(freshStoreResponse.status).toBe(200);
|
|
expect(parsedFreshResponse).toEqual({
|
|
success: true,
|
|
message: null,
|
|
data: { totalCount: 2, content: expect.arrayContaining([expect.objectContaining({ name: "루나" })]) },
|
|
errorProperty: null,
|
|
});
|
|
});
|
|
|
|
test("reactivates the admin preview token after logout and login in the same store", async () => {
|
|
// Given
|
|
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
|
useMockHandlers();
|
|
await login(createClient("caller-token"), { email: "admin@test.com", password: "password" });
|
|
await logout(createClient(adminToken));
|
|
|
|
// When
|
|
await login(createClient("caller-token"), { email: "admin@test.com", password: "password" });
|
|
const response = await fetch(`${apiBaseUrl}/api/v2/admin/ai-characters?page=0&size=20`, {
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
|
});
|
|
|
|
// Then
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
test("returns the contract 403 fixture for a non-ADMIN Bearer token", async () => {
|
|
// Given
|
|
useMockHandlers();
|
|
|
|
// When
|
|
const response = await fetch(`${apiBaseUrl}/api/v2/admin/ai-characters?page=0&size=20`, {
|
|
headers: { Authorization: "Bearer mock-member-jwt" },
|
|
});
|
|
|
|
// Then
|
|
expect(response.status).toBe(403);
|
|
await expect(response.json()).resolves.toEqual({
|
|
success: false,
|
|
message: "접근 권한이 없습니다.",
|
|
data: null,
|
|
errorProperty: null,
|
|
});
|
|
});
|
|
|
|
test("keeps mock fixture state out of browser persistent storage and logs", async () => {
|
|
// Given
|
|
vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl);
|
|
const indexedDbOpen = vi.fn();
|
|
const consoleLog = vi.spyOn(console, "log");
|
|
const consoleWarn = vi.spyOn(console, "warn");
|
|
const consoleError = vi.spyOn(console, "error");
|
|
vi.stubGlobal("indexedDB", { open: indexedDbOpen });
|
|
const cookieBefore = document.cookie;
|
|
useMockHandlers();
|
|
|
|
// When
|
|
await login(createClient("caller-token"), { email: "admin@test.com", password: "password" });
|
|
await logout(createClient(adminToken));
|
|
|
|
// Then
|
|
expect(localStorage).toHaveLength(0);
|
|
expect(sessionStorage).toHaveLength(0);
|
|
expect(indexedDbOpen).not.toHaveBeenCalled();
|
|
expect(document.cookie).toBe(cookieBefore);
|
|
expect(consoleLog).not.toHaveBeenCalled();
|
|
expect(consoleWarn).not.toHaveBeenCalled();
|
|
expect(consoleError).not.toHaveBeenCalled();
|
|
});
|
|
});
|