You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

301 lines
7.6 KiB

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<Response>((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);
});
});