fix: auth refresh token issue

This commit is contained in:
Stanley Cheung 2026-02-11 15:34:59 +08:00
parent 0ef5b9b1ea
commit 4f983387ac
4 changed files with 104 additions and 4 deletions

View File

@ -17,7 +17,7 @@ test("login loads backend projects list on /projects", async ({ context, page })
},
]);
await page.route("**/api/auth/refresh", async (route) => {
await page.route("**/api/auth/user/refresh", async (route) => {
await route.fulfill({
status: 401,
contentType: "application/json",

View File

@ -57,7 +57,7 @@ async function mockAuthAndCloudEditorApis({
page: Page;
withPutRoute?: boolean;
}) {
await page.route("**/api/auth/refresh", async (route: Route) => {
await page.route("**/api/auth/user/refresh", async (route: Route) => {
await route.fulfill({
status: 200,
contentType: "application/json",

View File

@ -0,0 +1,80 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { BackendAuthError, backendAuthApi } from "./api-client";
const originalFetch = globalThis.fetch;
const originalBaseUrl = process.env.NEXT_PUBLIC_AUTH_API_BASE_URL;
describe("backend auth api client", () => {
beforeEach(() => {
process.env.NEXT_PUBLIC_AUTH_API_BASE_URL = "http://localhost:3001/api";
});
afterEach(() => {
globalThis.fetch = originalFetch;
if (typeof originalBaseUrl === "string") {
process.env.NEXT_PUBLIC_AUTH_API_BASE_URL = originalBaseUrl;
} else {
delete process.env.NEXT_PUBLIC_AUTH_API_BASE_URL;
}
});
test("refresh uses POST /auth/user/refresh and unwraps data payload", async () => {
let requestUrl = "";
let requestMethod = "";
let requestCredentials = "";
globalThis.fetch = (async (input, init) => {
requestUrl = String(input);
requestMethod = init?.method ?? "GET";
requestCredentials = String(init?.credentials ?? "");
return new Response(
JSON.stringify({
status: "success",
data: {
token: "jwt-access-token",
user: {
id: "1",
email: "user@example.com",
},
},
}),
{
status: 201,
headers: { "Content-Type": "application/json" },
},
);
}) as typeof fetch;
const response = await backendAuthApi.refresh();
expect(requestUrl).toBe("http://localhost:3001/api/auth/user/refresh");
expect(requestMethod).toBe("POST");
expect(requestCredentials).toBe("include");
expect(response.token).toBe("jwt-access-token");
expect(response.user.email).toBe("user@example.com");
});
test("throws BackendAuthError when backend returns wrapped error in 2xx", async () => {
globalThis.fetch = (async () =>
new Response(
JSON.stringify({
status: "error",
message: "Invalid refresh token",
statusCode: 401,
}),
{
status: 201,
headers: { "Content-Type": "application/json" },
},
)) as typeof fetch;
const refreshPromise = backendAuthApi.refresh();
await expect(refreshPromise).rejects.toBeInstanceOf(BackendAuthError);
await expect(refreshPromise).rejects.toMatchObject({
message: "Invalid refresh token",
statusCode: 401,
});
});
});

View File

@ -44,6 +44,17 @@ export class BackendAuthError extends Error {
}
}
const isWrappedErrorPayload = (
payload: unknown,
): payload is { status: "error"; message?: string; statusCode?: number } => {
if (!payload || typeof payload !== "object") {
return false;
}
const candidate = payload as { status?: unknown };
return candidate.status === "error";
};
const unwrapSuccessPayload = <T,>(payload: TAuthSuccessPayload<T>): T => {
if (payload && typeof payload === "object" && "data" in payload) {
return payload.data;
@ -100,6 +111,15 @@ async function request<T>({
payload = null;
}
if (isWrappedErrorPayload(payload)) {
const { message, statusCode } = extractError({
payload,
defaultMessage: "Request failed",
defaultStatusCode: response.status,
});
throw new BackendAuthError({ message, statusCode });
}
if (!response.ok) {
const { message, statusCode } = extractError({
payload,
@ -148,8 +168,8 @@ export const backendAuthApi = {
}),
refresh: () =>
request<{ token: string; user: TBackendUser }>({
path: "/auth/refresh",
method: "GET",
path: "/auth/user/refresh",
method: "POST",
}),
logout: () =>
request<{ message?: string }>({