@@ -21,6 +21,33 @@ function mockGuardedTokenResponse(body: BodyInit, init?: ResponseInit): ReturnTy
|
21 | 21 | return release; |
22 | 22 | } |
23 | 23 | |
| 24 | +function cancelTrackedResponse( |
| 25 | +text: string, |
| 26 | +init: ResponseInit, |
| 27 | +): { |
| 28 | +release: ReturnType<typeof vi.fn>; |
| 29 | +response: Response; |
| 30 | +wasCanceled: () => boolean; |
| 31 | +} { |
| 32 | +let canceled = false; |
| 33 | +const stream = new ReadableStream<Uint8Array>({ |
| 34 | +start(controller) { |
| 35 | +controller.enqueue(new TextEncoder().encode(text)); |
| 36 | +}, |
| 37 | +cancel() { |
| 38 | +canceled = true; |
| 39 | +}, |
| 40 | +}); |
| 41 | +const release = vi.fn(async () => {}); |
| 42 | +const response = new Response(stream, init); |
| 43 | +fetchWithSsrFGuardMock.mockResolvedValueOnce({ response, release }); |
| 44 | +return { |
| 45 | + release, |
| 46 | + response, |
| 47 | +wasCanceled: () => canceled, |
| 48 | +}; |
| 49 | +} |
| 50 | + |
24 | 51 | describe("QQBot token manager", () => { |
25 | 52 | beforeEach(() => { |
26 | 53 | fetchWithSsrFGuardMock.mockReset(); |
@@ -59,6 +86,25 @@ describe("QQBot token manager", () => {
|
59 | 86 | expect(release).toHaveBeenCalledTimes(1); |
60 | 87 | }); |
61 | 88 | |
| 89 | +it("bounds access token responses without using response.text()", async () => { |
| 90 | +const logger = { debug: vi.fn(), info: vi.fn(), error: vi.fn() }; |
| 91 | +const tracked = cancelTrackedResponse(`${"qqbot token unavailable ".repeat(1024)}tail`, { |
| 92 | +status: 503, |
| 93 | +headers: { "content-type": "text/plain" }, |
| 94 | +}); |
| 95 | +const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); |
| 96 | + |
| 97 | +await expect(new TokenManager({ logger }).getAccessToken("app-id", "secret")).rejects.toThrow( |
| 98 | +"QQBot access_token response was malformed JSON", |
| 99 | +); |
| 100 | + |
| 101 | +expect(tracked.wasCanceled()).toBe(true); |
| 102 | +expect(textSpy).not.toHaveBeenCalled(); |
| 103 | +expect(tracked.release).toHaveBeenCalledTimes(1); |
| 104 | +expect(logger.debug.mock.calls.join("\n")).toContain("qqbot token unavailable"); |
| 105 | +expect(logger.debug.mock.calls.join("\n")).not.toContain("tail"); |
| 106 | +}); |
| 107 | + |
62 | 108 | it("passes the RFC2544 SSRF allowance to the token fetch (regression for #88984)", async () => { |
63 | 109 | mockGuardedTokenResponse('{"access_token":"token-1","expires_in":7200}', { |
64 | 110 | status: 200, |
|