diff --git a/extensions/minimax/oauth.test.ts b/extensions/minimax/oauth.test.ts index 796ac15fab6f..c5bd82cab259 100644 --- a/extensions/minimax/oauth.test.ts +++ b/extensions/minimax/oauth.test.ts @@ -213,6 +213,68 @@ function expectAbortOrTimeoutError(error: unknown) { expect(error).toHaveProperty("name", expect.stringMatching(/^(AbortError|TimeoutError)$/)); } +type FetchResponder = + | Response + | ((input: RequestInfo | URL, init?: RequestInit) => Response | Promise); + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +function formBody(init?: RequestInit): URLSearchParams { + return init?.body instanceof URLSearchParams + ? init.body + : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); +} + +function authorizationResponse( + init?: RequestInit, + overrides: Record = {}, +): Response { + return jsonResponse({ + user_code: "CODE", + verification_uri: "https://example.com/device", + expired_in: Date.now() + 10_000, + state: formBody(init).get("state"), + ...overrides, + }); +} + +function tokenResponse(overrides: Record = {}): Response { + return jsonResponse({ + status: "success", + access_token: "access", + refresh_token: "refresh", + expired_in: 3600, + ...overrides, + }); +} + +function stubOAuthFetch(...responders: FetchResponder[]) { + let responseIndex = 0; + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const responder = responders[responseIndex++]; + if (!responder) { + throw new Error(`unexpected MiniMax OAuth fetch #${responseIndex}`); + } + return typeof responder === "function" ? await responder(input, init) : responder; + }); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +} + +function loginMiniMax(overrides: Partial[0]> = {}) { + return loginMiniMaxPortalOAuth({ + openUrl: vi.fn(async () => undefined), + note: vi.fn(async () => undefined), + progress: { update: vi.fn(), stop: vi.fn() }, + ...overrides, + }); +} + afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); @@ -226,83 +288,21 @@ describe("loginMiniMaxPortalOAuth", () => { [1_700_000_000_000, 1_700_000_000_000], ])("normalizes token expiry %s through the OAuth flow", async (expiredIn, expectedExpires) => { vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000); - let callCount = 0; - vi.stubGlobal( - "fetch", - vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - return new Response( - JSON.stringify( - callCount === 1 - ? { - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - state: body.get("state"), - } - : { - status: "success", - access_token: "access", - refresh_token: "refresh", - expired_in: expiredIn, - }, - ), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }), + stubOAuthFetch( + (_input, init) => authorizationResponse(init), + tokenResponse({ expired_in: expiredIn }), ); - await expect( - loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }), - ).resolves.toMatchObject({ expires: expectedExpires }); + await expect(loginMiniMax()).resolves.toMatchObject({ expires: expectedExpires }); }); it("rejects malformed token expiry through the OAuth flow", async () => { - let callCount = 0; - vi.stubGlobal( - "fetch", - vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - return new Response( - JSON.stringify( - callCount === 1 - ? { - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - state: body.get("state"), - } - : { - status: "success", - access_token: "access", - refresh_token: "refresh", - expired_in: "3600s", - }, - ), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }), + stubOAuthFetch( + (_input, init) => authorizationResponse(init), + tokenResponse({ expired_in: "3600s" }), ); - await expect( - loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }), - ).rejects.toThrow("invalid token expiry"); + await expect(loginMiniMax()).rejects.toThrow("invalid token expiry"); }); it("times out authorization code HTTP requests against a hanging loopback server", async () => { @@ -324,11 +324,7 @@ describe("loginMiniMaxPortalOAuth", () => { ); vi.stubGlobal("fetch", fetchMock); - loginPromise = loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }); + loginPromise = loginMiniMax(); loginPromise.catch(() => undefined); await server.waitForRequestCount(2); @@ -364,33 +360,12 @@ describe("loginMiniMaxPortalOAuth", () => { waitForRequest: () => server.waitForRequestCount(1), }); - let callCount = 0; - const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - if (callCount === 1) { - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return await realFetch(`${server.origin}/token`, init); - }); - vi.stubGlobal("fetch", fetchMock); + const fetchMock = stubOAuthFetch( + (_input, init) => authorizationResponse(init), + async (_input, init) => await realFetch(`${server.origin}/token`, init), + ); - loginPromise = loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }); + loginPromise = loginMiniMax(); loginPromise.catch(() => undefined); await server.waitForRequestCount(2); @@ -422,16 +397,9 @@ describe("loginMiniMaxPortalOAuth", () => { }, ); const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); - vi.stubGlobal( - "fetch", - vi.fn(async () => tracked.response), - ); + stubOAuthFetch(tracked.response); - const error = await loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }).catch((cause: unknown) => cause); + const error = await loginMiniMax().catch((cause: unknown) => cause); expect(error).toBeInstanceOf(Error); expect((error as Error).message).toMatch( @@ -448,33 +416,9 @@ describe("loginMiniMaxPortalOAuth", () => { headers: { "Content-Type": "text/plain" }, }); const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); - let callCount = 0; - const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - if (callCount === 1) { - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return tracked.response; - }); - vi.stubGlobal("fetch", fetchMock); + stubOAuthFetch((_input, init) => authorizationResponse(init), tracked.response); - const error = await loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }).catch((cause: unknown) => cause); + const error = await loginMiniMax().catch((cause: unknown) => cause); expect(error).toBeInstanceOf(Error); expect((error as Error).message).toContain("minimax token unavailable"); @@ -489,33 +433,9 @@ describe("loginMiniMaxPortalOAuth", () => { headers: { "Content-Type": "application/json" }, }); const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); - let callCount = 0; - const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - if (callCount === 1) { - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return tracked.response; - }); - vi.stubGlobal("fetch", fetchMock); + stubOAuthFetch((_input, init) => authorizationResponse(init), tracked.response); - const error = await loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }).catch((cause: unknown) => cause); + const error = await loginMiniMax().catch((cause: unknown) => cause); expect(error).toBeInstanceOf(Error); expect((error as Error).message).toBe("MiniMax OAuth failed to parse response."); @@ -540,122 +460,49 @@ describe("loginMiniMaxPortalOAuth", () => { ], ], ] as const) { - const requestedUrls: string[] = []; - const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { - requestedUrls.push(input instanceof Request ? input.url : String(input)); - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - if (requestedUrls.length === 1) { - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return new Response( - JSON.stringify({ - status: "success", - access_token: "access", - refresh_token: "refresh", - expired_in: 3600, - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }); - vi.stubGlobal("fetch", fetchMock); + const fetchMock = stubOAuthFetch( + (_input, init) => authorizationResponse(init), + tokenResponse(), + ); - await expect( - loginMiniMaxPortalOAuth({ - region, - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }), - ).resolves.toMatchObject({ access: "access", refresh: "refresh" }); - expect(requestedUrls).toEqual(expectedHosts); + await expect(loginMiniMax({ region })).resolves.toMatchObject({ + access: "access", + refresh: "refresh", + }); + expect( + fetchMock.mock.calls.map(([input]) => + input instanceof Request ? input.url : String(input), + ), + ).toEqual(expectedHosts); vi.unstubAllGlobals(); } }); it("rejects Date-invalid authorization expiries before formatting instructions", async () => { - const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: 8_700_000_000_000_000, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }); - vi.stubGlobal("fetch", fetchMock); + stubOAuthFetch((_input, init) => + authorizationResponse(init, { expired_in: 8_700_000_000_000_000 }), + ); const note = vi.fn(async () => undefined); - await expect( - loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note, - progress: { update: vi.fn(), stop: vi.fn() }, - }), - ).rejects.toThrow("invalid expired_in"); + await expect(loginMiniMax({ note })).rejects.toThrow("invalid expired_in"); expect(note).not.toHaveBeenCalled(); }); it("caps oversized authorization poll intervals before scheduling", async () => { vi.useFakeTimers(); const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); - let callCount = 0; - const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - if (callCount === 1) { - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + MAX_TIMER_TIMEOUT_MS + 10_000, - interval: Number.MAX_SAFE_INTEGER, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return new Response( - JSON.stringify( - callCount === 2 - ? { status: "pending" } - : { - status: "success", - access_token: "access", - refresh_token: "refresh", - expired_in: 3600, - }, - ), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }); - vi.stubGlobal("fetch", fetchMock); + stubOAuthFetch( + (_input, init) => + authorizationResponse(init, { + expired_in: Date.now() + MAX_TIMER_TIMEOUT_MS + 10_000, + interval: Number.MAX_SAFE_INTEGER, + }), + jsonResponse({ status: "pending" }), + tokenResponse(), + ); - const result = loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }); + const result = loginMiniMax(); await vi.waitFor(() => { expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS); @@ -668,37 +515,12 @@ describe("loginMiniMaxPortalOAuth", () => { it("does not sleep past the authorization expiry deadline", async () => { vi.useFakeTimers(); const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); - let callCount = 0; - const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - if (callCount === 1) { - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - interval: Number.MAX_SAFE_INTEGER, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return new Response(JSON.stringify({ status: "pending" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }); - vi.stubGlobal("fetch", fetchMock); + stubOAuthFetch( + (_input, init) => authorizationResponse(init, { interval: Number.MAX_SAFE_INTEGER }), + jsonResponse({ status: "pending" }), + ); - const result = loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }); + const result = loginMiniMax(); await vi.waitFor(() => { expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 10_000); @@ -712,46 +534,13 @@ describe("loginMiniMaxPortalOAuth", () => { it("keeps the default poll delay for zero authorization intervals", async () => { vi.useFakeTimers(); const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); - let callCount = 0; - const fetchMock = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { - callCount += 1; - const body = - init?.body instanceof URLSearchParams - ? init.body - : new URLSearchParams(typeof init?.body === "string" ? init.body : ""); - if (callCount === 1) { - return new Response( - JSON.stringify({ - user_code: "CODE", - verification_uri: "https://example.com/device", - expired_in: Date.now() + 10_000, - interval: 0, - state: body.get("state"), - }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - return new Response( - JSON.stringify( - callCount === 2 - ? { status: "pending" } - : { - status: "success", - access_token: "access", - refresh_token: "refresh", - expired_in: 3600, - }, - ), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - }); - vi.stubGlobal("fetch", fetchMock); + stubOAuthFetch( + (_input, init) => authorizationResponse(init, { interval: 0 }), + jsonResponse({ status: "pending" }), + tokenResponse(), + ); - const result = loginMiniMaxPortalOAuth({ - openUrl: vi.fn(async () => undefined), - note: vi.fn(async () => undefined), - progress: { update: vi.fn(), stop: vi.fn() }, - }); + const result = loginMiniMax(); await vi.waitFor(() => { expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 2_000);