Skip to content

Commit 8075324

Browse files
authored
fix: properly set new token and fix tests (#252)
Token now persists properly, _should_ solve an issue with chats not reconnecting. Added some tests. Fixed tests hanging. Removed and updated deprecated/old tests. Simplified some tests.
1 parent 4598363 commit 8075324

File tree

8 files changed

+296
-787
lines changed

8 files changed

+296
-787
lines changed

apps/array/src/api/fetcher.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { buildApiFetcher } from "./fetcher";
3+
4+
describe("buildApiFetcher", () => {
5+
const mockFetch = vi.fn();
6+
const mockInput = {
7+
method: "get" as const,
8+
url: new URL("https://api.example.com/test"),
9+
path: "/test",
10+
};
11+
const ok = (data = {}) => ({
12+
ok: true,
13+
status: 200,
14+
json: () => Promise.resolve(data),
15+
});
16+
const err = (status: number) => ({
17+
ok: false,
18+
status,
19+
json: () => Promise.resolve({ error: status }),
20+
});
21+
22+
beforeEach(() => {
23+
vi.resetAllMocks();
24+
vi.stubGlobal("fetch", mockFetch);
25+
});
26+
27+
it("makes request with bearer token", async () => {
28+
mockFetch.mockResolvedValueOnce(ok());
29+
const fetcher = buildApiFetcher({ apiToken: "my-token" });
30+
31+
await fetcher.fetch(mockInput);
32+
33+
expect(mockFetch.mock.calls[0][1].headers.get("Authorization")).toBe(
34+
"Bearer my-token",
35+
);
36+
});
37+
38+
it("retries with new token on 401", async () => {
39+
const onTokenRefresh = vi.fn().mockResolvedValue("new-token");
40+
mockFetch.mockResolvedValueOnce(err(401)).mockResolvedValueOnce(ok());
41+
42+
const fetcher = buildApiFetcher({ apiToken: "old-token", onTokenRefresh });
43+
const response = await fetcher.fetch(mockInput);
44+
45+
expect(response.ok).toBe(true);
46+
expect(onTokenRefresh).toHaveBeenCalledTimes(1);
47+
expect(mockFetch.mock.calls[1][1].headers.get("Authorization")).toBe(
48+
"Bearer new-token",
49+
);
50+
});
51+
52+
it("uses refreshed token for subsequent requests", async () => {
53+
const onTokenRefresh = vi.fn().mockResolvedValue("refreshed-token");
54+
mockFetch
55+
.mockResolvedValueOnce(err(401))
56+
.mockResolvedValueOnce(ok())
57+
.mockResolvedValueOnce(ok());
58+
59+
const fetcher = buildApiFetcher({
60+
apiToken: "initial-token",
61+
onTokenRefresh,
62+
});
63+
await fetcher.fetch(mockInput);
64+
await fetcher.fetch(mockInput);
65+
66+
expect(mockFetch.mock.calls[2][1].headers.get("Authorization")).toBe(
67+
"Bearer refreshed-token",
68+
);
69+
});
70+
71+
it("does not refresh on non-401 errors", async () => {
72+
const onTokenRefresh = vi.fn();
73+
mockFetch.mockResolvedValueOnce(err(403));
74+
75+
const fetcher = buildApiFetcher({ apiToken: "token", onTokenRefresh });
76+
77+
await expect(fetcher.fetch(mockInput)).rejects.toThrow("[403]");
78+
expect(onTokenRefresh).not.toHaveBeenCalled();
79+
});
80+
81+
it("throws on 401 without refresh callback", async () => {
82+
mockFetch.mockResolvedValueOnce(err(401));
83+
const fetcher = buildApiFetcher({ apiToken: "token" });
84+
85+
await expect(fetcher.fetch(mockInput)).rejects.toThrow("[401]");
86+
});
87+
88+
it("throws when refresh fails", async () => {
89+
const onTokenRefresh = vi.fn().mockRejectedValue(new Error("failed"));
90+
mockFetch.mockResolvedValueOnce(err(401));
91+
92+
const fetcher = buildApiFetcher({ apiToken: "token", onTokenRefresh });
93+
94+
await expect(fetcher.fetch(mockInput)).rejects.toThrow("[401]");
95+
});
96+
97+
it("handles network errors", async () => {
98+
mockFetch.mockRejectedValueOnce(new Error("Network failure"));
99+
const fetcher = buildApiFetcher({ apiToken: "token" });
100+
101+
await expect(fetcher.fetch(mockInput)).rejects.toThrow(
102+
"Network request failed",
103+
);
104+
});
105+
});

apps/array/src/api/fetcher.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const buildApiFetcher: (config: {
44
apiToken: string;
55
onTokenRefresh?: () => Promise<string>;
66
}) => Parameters<typeof createApiClient>[0] = (config) => {
7+
let currentToken = config.apiToken;
8+
79
const makeRequest = async (
810
input: Parameters<Parameters<typeof createApiClient>[0]["fetch"]>[0],
911
token: string,
@@ -51,13 +53,14 @@ export const buildApiFetcher: (config: {
5153

5254
return {
5355
fetch: async (input) => {
54-
let response = await makeRequest(input, config.apiToken);
56+
let response = await makeRequest(input, currentToken);
5557

5658
// Handle 401 with automatic token refresh
5759
if (!response.ok && response.status === 401 && config.onTokenRefresh) {
5860
try {
5961
const newToken = await config.onTokenRefresh();
60-
response = await makeRequest(input, newToken);
62+
currentToken = newToken;
63+
response = await makeRequest(input, currentToken);
6164
} catch {
6265
// Token refresh failed - throw the original 401 error
6366
const errorResponse = await response.json();

0 commit comments

Comments
 (0)