import { afterEach, describe, expect, it, vi } from "vitest"; import { openLoginModal } from "@/store/loginModalStore"; import { USER_INFO_ENDPOINT, USER_API_KEY_ENDPOINT, USER_SESSION_STORAGE_KEY, fetchGatewayApiKeyByToken, fetchUserInfoByToken, useUserStore, } from "../userStore"; vi.mock("@/store/loginModalStore", () => ({ openLoginModal: vi.fn(), })); const user = { id: 10561, code: "u10561", name: "Test User", avatar: "https://example.com/avatar.jpeg", gender: 1, birthday: "", phone: "17313164895", email: "test@example.com", wechat: "", signature: "hello", country: "", province: "", city: "", isMember: true, memberLevel: 3, memberCoins: 0, otherCoins: 0, pointPackageCoins: 0, allCoins: 0, isUsedCoinsRecently: false, power: 0, powerConsumed: 0, powerRecharged: 0, followingNum: 1, fansNum: 0, likeNum: 2, postNum: 0, taskNum: 0, characterDesignGuide: false, status: 1, createTime: "2026-01-13T14:37:09.351889+08:00", updateTime: "2026-05-12T17:15:45.363937+08:00", }; describe("fetchUserInfoByToken", () => { it("fetches current user info through the Next.js API route", async () => { const fetcher = vi.fn().mockResolvedValue( new Response( JSON.stringify({ data: { token: "new-token", user, }, message: "ok", status: "0000", }), { status: 200 } ) ); await expect(fetchUserInfoByToken("old-token", fetcher)).resolves.toEqual({ token: "new-token", userInfo: user, }); expect(fetcher).toHaveBeenCalledWith(USER_INFO_ENDPOINT, { method: "GET", headers: { Accept: "application/json", Authorization: "Bearer old-token", token: "old-token", }, }); }); it("throws when the API response is not successful", async () => { const fetcher = vi.fn().mockResolvedValue( new Response( JSON.stringify({ data: null, message: "unauthorized", status: "0401", }), { status: 200 } ) ); await expect(fetchUserInfoByToken("bad-token", fetcher)).rejects.toThrow( "unauthorized" ); }); it("deduplicates concurrent requests for the same token", async () => { let resolveResponse: ((response: Response) => void) | undefined; const fetcher = vi.fn( () => new Promise((resolve) => { resolveResponse = resolve; }) ); const first = fetchUserInfoByToken("same-token", fetcher); const second = fetchUserInfoByToken("same-token", fetcher); resolveResponse?.( new Response( JSON.stringify({ data: { token: "same-token", user, }, message: "ok", status: "0000", }), { status: 200 } ) ); await expect(Promise.all([first, second])).resolves.toEqual([ { token: "same-token", userInfo: user }, { token: "same-token", userInfo: user }, ]); expect(fetcher).toHaveBeenCalledTimes(1); }); }); describe("fetchGatewayApiKeyByToken", () => { it("fetches the current user gateway API key through the Next.js API route", async () => { const fetcher = vi.fn().mockResolvedValue( new Response( JSON.stringify({ data: { apikey: "sk-user-gateway" }, message: "ok", status: "0000", }), { status: 200 } ) ); await expect(fetchGatewayApiKeyByToken("login-token", fetcher)).resolves.toBe( "sk-user-gateway" ); expect(fetcher).toHaveBeenCalledWith(USER_API_KEY_ENDPOINT, { method: "GET", headers: { Accept: "application/json", Authorization: "Bearer login-token", token: "login-token", }, }); }); }); describe("useUserStore.fetchUserInfo", () => { afterEach(() => { vi.mocked(openLoginModal).mockClear(); vi.unstubAllGlobals(); window.localStorage.removeItem(USER_SESSION_STORAGE_KEY); useUserStore.setState({ token: null, userInfo: null, gatewayApiKey: null, isLoadingUserInfo: false, isLoadingGatewayApiKey: false, userInfoError: null, gatewayApiKeyError: null, }); }); it("uses the persisted token when called without a token", async () => { window.localStorage.setItem( USER_SESSION_STORAGE_KEY, JSON.stringify({ state: { token: "stored-token", userInfo: null, }, version: 0, }) ); const fetcher = vi .fn() .mockResolvedValueOnce( new Response( JSON.stringify({ data: { token: "stored-token", user, }, message: "ok", status: "0000", }), { status: 200 } ) ) .mockResolvedValueOnce( new Response( JSON.stringify({ data: { apikey: "stored-gateway-key" }, message: "ok", status: "0000", }), { status: 200 } ) ); vi.stubGlobal("fetch", fetcher); await expect(useUserStore.getState().fetchUserInfo("")).resolves.toEqual( user ); expect(fetcher).toHaveBeenCalledWith(USER_INFO_ENDPOINT, { method: "GET", headers: { Accept: "application/json", Authorization: "Bearer stored-token", token: "stored-token", }, }); expect(fetcher).toHaveBeenCalledWith(USER_API_KEY_ENDPOINT, { method: "GET", headers: { Accept: "application/json", Authorization: "Bearer stored-token", token: "stored-token", }, }); expect(useUserStore.getState().gatewayApiKey).toBe("stored-gateway-key"); expect(openLoginModal).not.toHaveBeenCalled(); }); it("quietly clears login state when no token is available", async () => { const fetcher = vi.fn(); vi.stubGlobal("fetch", fetcher); await expect( useUserStore.getState().fetchUserInfo(" ") ).resolves.toBeNull(); expect(fetcher).not.toHaveBeenCalled(); expect(openLoginModal).not.toHaveBeenCalled(); }); it("keeps a manually entered gateway key when no token is available", async () => { const fetcher = vi.fn(); vi.stubGlobal("fetch", fetcher); useUserStore.getState().setGatewayApiKey(" manual-gateway-key "); await expect( useUserStore.getState().fetchUserInfo(" ") ).resolves.toBeNull(); expect(fetcher).not.toHaveBeenCalled(); expect(useUserStore.getState().gatewayApiKey).toBe("manual-gateway-key"); expect(openLoginModal).not.toHaveBeenCalled(); }); it("refreshes user info when forceRefresh is enabled", async () => { const cachedUser = { ...user, allCoins: 10 }; const refreshedUser = { ...user, allCoins: 99 }; useUserStore.setState({ token: "stored-token", userInfo: cachedUser, gatewayApiKey: "stored-gateway-key", }); const fetcher = vi.fn().mockResolvedValueOnce( new Response( JSON.stringify({ data: { token: "stored-token", user: refreshedUser, }, message: "ok", status: "0000", }), { status: 200 } ) ); vi.stubGlobal("fetch", fetcher); await expect( useUserStore.getState().fetchUserInfo("", { forceRefresh: true }) ).resolves.toEqual(refreshedUser); expect(fetcher).toHaveBeenCalledWith(USER_INFO_ENDPOINT, { method: "GET", headers: { Accept: "application/json", Authorization: "Bearer stored-token", token: "stored-token", }, }); expect(useUserStore.getState().userInfo?.allCoins).toBe(99); }); });