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