@@ -2,6 +2,42 @@
|
2 | 2 | import { describe, expect, it, vi } from "vitest"; |
3 | 3 | import { loginChutes } from "./oauth.js"; |
4 | 4 | |
| 5 | +function boundedErrorResponse(body: string, status = 500): { |
| 6 | +response: Response; |
| 7 | +cancel: ReturnType<typeof vi.fn>; |
| 8 | +releaseLock: ReturnType<typeof vi.fn>; |
| 9 | +text: ReturnType<typeof vi.fn>; |
| 10 | +} { |
| 11 | +const encoded = new TextEncoder().encode(body); |
| 12 | +let read = false; |
| 13 | +const cancel = vi.fn(async () => undefined); |
| 14 | +const releaseLock = vi.fn(); |
| 15 | +const text = vi.fn(async () => { |
| 16 | +throw new Error("response.text() should not be called"); |
| 17 | +}); |
| 18 | +const response = { |
| 19 | +ok: false, |
| 20 | + status, |
| 21 | +headers: new Headers(), |
| 22 | +body: { |
| 23 | +getReader: () => ({ |
| 24 | +read: async () => { |
| 25 | +if (read) { |
| 26 | +return { done: true, value: undefined }; |
| 27 | +} |
| 28 | +read = true; |
| 29 | +return { done: false, value: encoded }; |
| 30 | +}, |
| 31 | + cancel, |
| 32 | + releaseLock, |
| 33 | +}), |
| 34 | +}, |
| 35 | + text, |
| 36 | +} as unknown as Response; |
| 37 | + |
| 38 | +return { response, cancel, releaseLock, text }; |
| 39 | +} |
| 40 | + |
5 | 41 | describe("chutes plugin OAuth", () => { |
6 | 42 | it("rejects unsafe token lifetimes before storing credentials", async () => { |
7 | 43 | const fetchFn = vi.fn(async (input: RequestInfo | URL) => { |
@@ -33,4 +69,47 @@ describe("chutes plugin OAuth", () => {
|
33 | 69 | }), |
34 | 70 | ).rejects.toThrow("Chutes token exchange returned invalid expires_in"); |
35 | 71 | }); |
| 72 | + |
| 73 | +it("bounds token exchange error bodies without requiring response.text()", async () => { |
| 74 | +const errorResponse = boundedErrorResponse( |
| 75 | +`${"chutes token unavailable ".repeat(1024)}tail-marker`, |
| 76 | +502, |
| 77 | +); |
| 78 | +const fetchFn = vi.fn(async (input: RequestInfo | URL) => { |
| 79 | +const url = |
| 80 | +typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; |
| 81 | +if (url === "https://api.chutes.ai/idp/token") { |
| 82 | +return errorResponse.response; |
| 83 | +} |
| 84 | +return new Response("not found", { status: 404 }); |
| 85 | +}); |
| 86 | + |
| 87 | +let error: unknown; |
| 88 | +try { |
| 89 | +await loginChutes({ |
| 90 | +app: { |
| 91 | +clientId: "cid_test", |
| 92 | +redirectUri: "http://127.0.0.1:1456/oauth-callback", |
| 93 | +scopes: ["openid"], |
| 94 | +}, |
| 95 | +manual: true, |
| 96 | +createState: () => "state_test", |
| 97 | +onAuth: vi.fn(async () => {}), |
| 98 | +onPrompt: vi.fn( |
| 99 | +async () => "http://127.0.0.1:1456/oauth-callback?code=code_test&state=state_test", |
| 100 | +), |
| 101 | + fetchFn, |
| 102 | +}); |
| 103 | +} catch (caught) { |
| 104 | +error = caught; |
| 105 | +} |
| 106 | + |
| 107 | +expect(error).toBeInstanceOf(Error); |
| 108 | +const message = (error as Error).message; |
| 109 | +expect(message).toContain("Chutes token exchange failed: chutes token unavailable"); |
| 110 | +expect(message).not.toContain("tail-marker"); |
| 111 | +expect(errorResponse.text).not.toHaveBeenCalled(); |
| 112 | +expect(errorResponse.cancel).toHaveBeenCalledTimes(1); |
| 113 | +expect(errorResponse.releaseLock).toHaveBeenCalledTimes(1); |
| 114 | +}); |
36 | 115 | }); |