@@ -24,6 +24,42 @@ function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
24 | 24 | }); |
25 | 25 | } |
26 | 26 | |
| 27 | +function boundedTextErrorResponse(body: string, status = 502): { |
| 28 | +response: Response; |
| 29 | +cancel: ReturnType<typeof vi.fn>; |
| 30 | +releaseLock: ReturnType<typeof vi.fn>; |
| 31 | +text: ReturnType<typeof vi.fn>; |
| 32 | +} { |
| 33 | +const encoded = new TextEncoder().encode(body); |
| 34 | +let read = false; |
| 35 | +const cancel = vi.fn(async () => undefined); |
| 36 | +const releaseLock = vi.fn(); |
| 37 | +const text = vi.fn(async () => { |
| 38 | +throw new Error("response.text() should not be called"); |
| 39 | +}); |
| 40 | +const response = { |
| 41 | +ok: false, |
| 42 | + status, |
| 43 | +headers: new Headers(), |
| 44 | +body: { |
| 45 | +getReader: () => ({ |
| 46 | +read: async () => { |
| 47 | +if (read) { |
| 48 | +return { done: true, value: undefined }; |
| 49 | +} |
| 50 | +read = true; |
| 51 | +return { done: false, value: encoded }; |
| 52 | +}, |
| 53 | + cancel, |
| 54 | + releaseLock, |
| 55 | +}), |
| 56 | +}, |
| 57 | + text, |
| 58 | +} as unknown as Response; |
| 59 | + |
| 60 | +return { response, cancel, releaseLock, text }; |
| 61 | +} |
| 62 | + |
27 | 63 | function requestUrl(input: RequestInfo | URL): string { |
28 | 64 | if (typeof input === "string") { |
29 | 65 | return input; |
@@ -180,6 +216,33 @@ describe("OpenRouter OAuth", () => {
|
180 | 216 | ).rejects.toThrow("OpenRouter OAuth key exchange failed (400): Invalid code"); |
181 | 217 | }); |
182 | 218 | |
| 219 | +it("bounds OpenRouter OAuth exchange error bodies without requiring response.text()", async () => { |
| 220 | +const errorResponse = boundedTextErrorResponse( |
| 221 | +`${"openrouter denied ".repeat(1024)}tail-marker`, |
| 222 | +502, |
| 223 | +); |
| 224 | +const fetchImpl = vi.fn<typeof fetch>(async () => errorResponse.response); |
| 225 | + |
| 226 | +let error: unknown; |
| 227 | +try { |
| 228 | +await exchangeOpenRouterOAuthCode({ |
| 229 | +code: "bad-code", |
| 230 | +codeVerifier: "bad-verifier", |
| 231 | + fetchImpl, |
| 232 | +}); |
| 233 | +} catch (caught) { |
| 234 | +error = caught; |
| 235 | +} |
| 236 | + |
| 237 | +expect(error).toBeInstanceOf(Error); |
| 238 | +const message = (error as Error).message; |
| 239 | +expect(message).toContain("OpenRouter OAuth key exchange failed (502): openrouter denied"); |
| 240 | +expect(message).not.toContain("tail-marker"); |
| 241 | +expect(errorResponse.text).not.toHaveBeenCalled(); |
| 242 | +expect(errorResponse.cancel).toHaveBeenCalledTimes(1); |
| 243 | +expect(errorResponse.releaseLock).toHaveBeenCalledTimes(1); |
| 244 | +}); |
| 245 | + |
183 | 246 | it("stores a browser OAuth result as the default OpenRouter API-key profile", async () => { |
184 | 247 | const fetchImpl = vi.fn<typeof fetch>(async () => |
185 | 248 | jsonResponse({ key: "sk-or-v1-test", user_id: "user-1" }), |
|