fix(auth): handle logout and refresh results
Propagate storage Result failures through logout, guest restoration, token refresh, and user state actors. Clear invalid sessions after failed automatic refreshes and cover both business and guest flows.
This commit is contained in:
@@ -3,7 +3,7 @@ import { createStorage } from "unstorage";
|
||||
import memoryDriver from "unstorage/drivers/memory";
|
||||
|
||||
import { AuthStorage } from "@/data/storage/auth";
|
||||
import { SpAsyncUtil } from "@/utils";
|
||||
import { deviceIdentifier, Result, SpAsyncUtil } from "@/utils";
|
||||
|
||||
import { ErrorCode } from "../api_result";
|
||||
import { createHttpClient } from "../http_client";
|
||||
@@ -29,6 +29,7 @@ describe("createHttpClient", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("refreshes a business token and replays the original request once after 401", async () => {
|
||||
@@ -143,5 +144,122 @@ describe("createHttpClient", () => {
|
||||
"/api/chat/history",
|
||||
"/api/auth/refresh",
|
||||
]);
|
||||
await expect(authStorage.getLoginToken()).resolves.toEqual(Result.ok(null));
|
||||
await expect(authStorage.getRefreshToken()).resolves.toEqual(Result.ok(null));
|
||||
});
|
||||
|
||||
it("rejects instead of replaying when the refreshed token cannot be stored", async () => {
|
||||
const authStorage = AuthStorage.getInstance();
|
||||
await authStorage.setLoginToken("old-login-token");
|
||||
await authStorage.setRefreshToken("refresh-token");
|
||||
vi.spyOn(authStorage, "setLoginToken").mockResolvedValue(
|
||||
Result.err(new Error("login token write failed")),
|
||||
);
|
||||
|
||||
const fetchMock = vi.fn<typeof fetch>(async (input) => {
|
||||
const url = new URL(input instanceof Request ? input.url : String(input));
|
||||
if (url.pathname === "/api/auth/refresh") {
|
||||
return jsonResponse({
|
||||
success: true,
|
||||
data: {
|
||||
token: "new-login-token",
|
||||
refreshToken: "new-refresh-token",
|
||||
},
|
||||
});
|
||||
}
|
||||
return jsonResponse(
|
||||
{ message: "expired" },
|
||||
{ status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
|
||||
);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const http = createHttpClient();
|
||||
await expect(http("/api/chat/history")).rejects.toMatchObject({
|
||||
message: "login token write failed",
|
||||
});
|
||||
|
||||
expect(fetchMock.mock.calls.map((call) => callUrl(call).pathname)).toEqual([
|
||||
"/api/chat/history",
|
||||
"/api/auth/refresh",
|
||||
]);
|
||||
});
|
||||
|
||||
it("propagates refresh-token read failures instead of treating them as missing", async () => {
|
||||
const authStorage = AuthStorage.getInstance();
|
||||
await authStorage.setLoginToken("old-login-token");
|
||||
vi.spyOn(authStorage, "getRefreshToken").mockResolvedValue(
|
||||
Result.err(new Error("refresh token read failed")),
|
||||
);
|
||||
const fetchMock = vi.fn<typeof fetch>(async () =>
|
||||
jsonResponse(
|
||||
{ message: "expired" },
|
||||
{ status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
|
||||
),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const http = createHttpClient();
|
||||
await expect(http("/api/chat/history")).rejects.toMatchObject({
|
||||
message: "refresh token read failed",
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("propagates login-state read failures without attempting a guest refresh", async () => {
|
||||
const authStorage = AuthStorage.getInstance();
|
||||
await authStorage.setLoginToken("old-login-token");
|
||||
vi.spyOn(authStorage, "hasLoginToken").mockResolvedValue(
|
||||
Result.err(new Error("login state read failed")),
|
||||
);
|
||||
const fetchMock = vi.fn<typeof fetch>(async () =>
|
||||
jsonResponse(
|
||||
{ message: "expired" },
|
||||
{ status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
|
||||
),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const http = createHttpClient();
|
||||
await expect(http("/api/chat/history")).rejects.toMatchObject({
|
||||
message: "login state read failed",
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rejects a guest refresh when the new guest token cannot be stored", async () => {
|
||||
const authStorage = AuthStorage.getInstance();
|
||||
await authStorage.setGuestToken("old-guest-token");
|
||||
vi.spyOn(deviceIdentifier, "getDeviceId").mockResolvedValue("device-1");
|
||||
vi.spyOn(authStorage, "setGuestToken").mockResolvedValue(
|
||||
Result.err(new Error("guest token write failed")),
|
||||
);
|
||||
const fetchMock = vi.fn<typeof fetch>(async (input) => {
|
||||
const url = new URL(input instanceof Request ? input.url : String(input));
|
||||
if (url.pathname === "/api/auth/guest") {
|
||||
return jsonResponse({
|
||||
success: true,
|
||||
data: { token: "new-guest-token", deviceId: "device-1" },
|
||||
});
|
||||
}
|
||||
return jsonResponse(
|
||||
{ message: "expired" },
|
||||
{ status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
|
||||
);
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const http = createHttpClient();
|
||||
await expect(http("/api/chat/history")).rejects.toMatchObject({
|
||||
message: "guest token write failed",
|
||||
});
|
||||
|
||||
expect(fetchMock.mock.calls.map((call) => callUrl(call).pathname)).toEqual([
|
||||
"/api/chat/history",
|
||||
"/api/auth/guest",
|
||||
]);
|
||||
await expect(authStorage.getGuestToken()).resolves.toEqual(Result.ok(null));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user