import { http, HttpResponse } from "msw"; import { afterEach, describe, expect, test, vi } from "vitest"; import { AccessDeniedError, ApiError, UNKNOWN_API_ERROR_MESSAGE } from "../api-error"; import { createApiClient } from "../client"; import { server } from "../../test/server"; import { apiBaseUrl, createTestClient, valueSchema } from "./client-test-helpers"; afterEach(() => { vi.unstubAllEnvs(); }); describe("authenticated API requests", () => { test("sends Korean language without bearer authentication to login", async () => { // Given vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl); const { client } = createTestClient(); let acceptLanguage: string | null = null; let authorization: string | null = null; server.use( http.post(`${apiBaseUrl}/admin/member/login`, ({ request }) => { acceptLanguage = request.headers.get("Accept-Language"); authorization = request.headers.get("Authorization"); return HttpResponse.json({ success: true, message: null, data: { value: "ok" } }); }), ); // When await client.request({ path: "/admin/member/login", method: "POST", headers: { Authorization: "Bearer caller-supplied-token", "Content-Type": "application/json", }, body: JSON.stringify({ email: "admin@test.com", password: "secret" }), responseSchema: valueSchema, authentication: "none", }); // Then expect(acceptLanguage).toBe("ko"); expect(authorization).toBeNull(); }); test("sends bearer authentication to a protected request", async () => { // Given vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl); const { client } = createTestClient(); let authorization: string | null = null; server.use( http.get(`${apiBaseUrl}/api/v2/protected`, ({ request }) => { authorization = request.headers.get("Authorization"); return HttpResponse.json({ success: true, message: null, data: { value: "ok" } }); }), ); // When await client.request({ path: "/api/v2/protected", responseSchema: valueSchema, authentication: "required", }); // Then expect(authorization).toBe("Bearer header.payload.signature"); }); test("sends bearer authentication to logout", async () => { // Given vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl); const { client } = createTestClient(); let authorization: string | null = null; server.use( http.post(`${apiBaseUrl}/member/logout`, ({ request }) => { authorization = request.headers.get("Authorization"); return HttpResponse.json({ success: true, message: null, data: { value: "ok" } }); }), ); // When await client.request({ path: "/member/logout", method: "POST", responseSchema: valueSchema, authentication: "required", }); // Then expect(authorization).toBe("Bearer header.payload.signature"); }); test("clears the session and redirects once for each concurrent 401 burst", async () => { // Given vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl); const clearSession = vi.fn(); const onAuthExpired = vi.fn(); const client = createApiClient({ getToken: () => "header.payload.signature", clearSession, onAuthExpired, }); server.use( http.get(`${apiBaseUrl}/protected`, () => HttpResponse.json( { success: false, message: "인증 정보가 없습니다.", data: null, errorProperty: null, }, { status: 401 }, ), ), ); // When const firstBurst = await Promise.allSettled([ client.request({ path: "/protected", responseSchema: valueSchema, authentication: "required", }), client.request({ path: "/protected", responseSchema: valueSchema, authentication: "required", }), ]); const secondBurst = await Promise.allSettled([ client.request({ path: "/protected", responseSchema: valueSchema, authentication: "required", }), client.request({ path: "/protected", responseSchema: valueSchema, authentication: "required", }), ]); // Then expect(firstBurst).toHaveLength(2); expect(secondBurst).toHaveLength(2); expect(clearSession).toHaveBeenCalledTimes(2); expect(onAuthExpired).toHaveBeenCalledTimes(2); }); test.each([ ["malformed JSON", () => new HttpResponse("not-json", { status: 401 })], ["empty body", () => new HttpResponse(null, { status: 401 })], ["schema mismatch", () => HttpResponse.json({ success: false, data: null, errorProperty: null }, { status: 401 })], ])("clears the session once for concurrent protected 401 responses with %s", async (_label, responseFactory) => { // Given vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl); const { client, clearSession, onAuthExpired } = createTestClient(); server.use(http.get(`${apiBaseUrl}/protected`, responseFactory)); // When const results = await Promise.allSettled([ client.request({ path: "/protected", responseSchema: valueSchema, authentication: "required", }), client.request({ path: "/protected", responseSchema: valueSchema, authentication: "required", }), ]); // Then for (const result of results) { expect(result.status).toBe("rejected"); if (result.status === "rejected") { expect(result.reason).toBeInstanceOf(ApiError); expect(result.reason).toMatchObject({ status: 401, message: UNKNOWN_API_ERROR_MESSAGE, errorProperty: null }); } } expect(clearSession).toHaveBeenCalledTimes(1); expect(onAuthExpired).toHaveBeenCalledTimes(1); }); test("surfaces access denied without clearing the session", async () => { // Given vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl); const { client, clearSession, onAuthExpired } = createTestClient(); server.use( http.get(`${apiBaseUrl}/protected`, () => HttpResponse.json( { success: false, message: "접근 권한이 없습니다.", data: null, errorProperty: null, }, { status: 403 }, ), ), ); // When const request = client.request({ path: "/protected", responseSchema: valueSchema, authentication: "required", }); // Then await expect(request).rejects.toBeInstanceOf(AccessDeniedError); expect(clearSession).not.toHaveBeenCalled(); expect(onAuthExpired).not.toHaveBeenCalled(); }); test("does not write sensitive request data to the console", async () => { // Given vi.stubEnv("VITE_API_BASE_URL", apiBaseUrl); const { client } = createTestClient("jwt.secret.value"); const multipartBody = new FormData(); multipartBody.append("request", JSON.stringify({ password: "secret-password" })); multipartBody.append("file", new File(["private-content"], "private.txt")); const consoleDebug = vi.spyOn(console, "debug"); const consoleError = vi.spyOn(console, "error"); const consoleInfo = vi.spyOn(console, "info"); const consoleLog = vi.spyOn(console, "log"); const consoleWarn = vi.spyOn(console, "warn"); server.use( http.post(`${apiBaseUrl}/api/v2/protected`, () => HttpResponse.json({ success: true, message: null, data: { value: "ok" } }), ), ); // When await client.request({ path: "/api/v2/protected?signedUrl=https%3A%2F%2Fcdn.example.com%2Fprivate%3Fsignature%3Dabc", method: "POST", body: multipartBody, responseSchema: valueSchema, authentication: "required", }); // Then expect(consoleDebug).not.toHaveBeenCalled(); expect(consoleError).not.toHaveBeenCalled(); expect(consoleInfo).not.toHaveBeenCalled(); expect(consoleLog).not.toHaveBeenCalled(); expect(consoleWarn).not.toHaveBeenCalled(); }); });