| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| import { |
| __resetActiveStoreForTests, |
| setActiveSelection, |
| setRegisteredBackends, |
| } from "#/api/backend-registry/active-store"; |
| import { callCloudProxy } from "#/api/cloud/proxy"; |
| import type { Backend } from "#/api/backend-registry/types"; |
|
|
| const cloudPersonal: Backend = { |
| id: "cloud-personal", |
| name: "Production - Personal", |
| host: "https://app.all-hands.dev", |
| apiKey: "personal-key", |
| kind: "cloud", |
| }; |
|
|
| const cloudAcme: Backend = { |
| id: "cloud-acme", |
| name: "Production - Acme", |
| host: "https://app.all-hands.dev", |
| apiKey: "acme-key", |
| kind: "cloud", |
| }; |
|
|
| const cookieCloud: Backend = { |
| id: "cloud-cookie", |
| name: "Production - Cookie", |
| host: "https://app.all-hands.dev", |
| apiKey: "", |
| kind: "cloud", |
| authMode: "cookie", |
| }; |
|
|
| const originalFetch = global.fetch; |
| const fetchMock = vi.fn(); |
|
|
| function mockJsonResponse(data: unknown, status = 200): Response { |
| return new Response(JSON.stringify(data), { |
| status, |
| headers: { "content-type": "application/json" }, |
| }); |
| } |
|
|
| beforeEach(() => { |
| window.localStorage.clear(); |
| __resetActiveStoreForTests(); |
| fetchMock.mockReset(); |
| fetchMock.mockResolvedValue(mockJsonResponse({})); |
| global.fetch = fetchMock as typeof fetch; |
| }); |
|
|
| afterEach(() => { |
| window.localStorage.clear(); |
| __resetActiveStoreForTests(); |
| fetchMock.mockReset(); |
| global.fetch = originalFetch; |
| }); |
|
|
| describe("callCloudProxy X-Org-Id injection", () => { |
| it("sends X-Org-Id when targeting the active cloud backend with a selected orgId", async () => { |
| |
| |
| |
| setRegisteredBackends([cloudPersonal]); |
| setActiveSelection({ |
| backendId: cloudPersonal.id, |
| orgId: "org-personal-uuid", |
| }); |
|
|
| |
| await callCloudProxy({ |
| backend: cloudPersonal, |
| method: "GET", |
| path: "/api/v1/app-conversations/search", |
| }); |
|
|
| |
| |
| |
| const [url, init] = fetchMock.mock.calls[0]!; |
| expect(url).toBe(`${cloudPersonal.host}/api/v1/app-conversations/search`); |
| expect(init).toMatchObject({ |
| method: "GET", |
| }); |
| expect( |
| (init as { headers: Record<string, string> }).headers["X-Org-Id"], |
| ).toBe("org-personal-uuid"); |
| }); |
|
|
| it("omits X-Org-Id when targeting a different cloud backend than the active one", async () => { |
| |
| |
| |
| |
| setRegisteredBackends([cloudPersonal, cloudAcme]); |
| setActiveSelection({ |
| backendId: cloudPersonal.id, |
| orgId: "org-personal-uuid", |
| }); |
|
|
| |
| await callCloudProxy({ |
| backend: cloudAcme, |
| method: "GET", |
| path: "/api/keys/current", |
| }); |
|
|
| |
| const [, init] = fetchMock.mock.calls[0]!; |
| expect( |
| (init as { headers: Record<string, string> }).headers, |
| ).not.toHaveProperty("X-Org-Id"); |
| }); |
| }); |
|
|
| describe("callCloudProxy cookie auth", () => { |
| it("omits bearer auth for same-origin cookie-backed Cloud requests", async () => { |
| setRegisteredBackends([cookieCloud]); |
| setActiveSelection({ backendId: cookieCloud.id, orgId: null }); |
|
|
| await callCloudProxy({ |
| backend: cookieCloud, |
| method: "GET", |
| path: "/api/v1/users/me", |
| }); |
|
|
| const [url, init] = fetchMock.mock.calls[0]!; |
| expect(url).toBe(`${cookieCloud.host}/api/v1/users/me`); |
| expect(init).toMatchObject({ method: "GET" }); |
| expect( |
| (init as { headers: Record<string, string> }).headers, |
| ).not.toHaveProperty("Authorization"); |
| }); |
|
|
| it("omits bearer auth in runtime proxy envelope requests", async () => { |
| setRegisteredBackends([cookieCloud]); |
| setActiveSelection({ backendId: cookieCloud.id, orgId: null }); |
|
|
| await callCloudProxy({ |
| backend: cookieCloud, |
| method: "GET", |
| path: "/api/bash/bash_events/search", |
| hostOverride: "https://abc123.prod-runtime.all-hands.dev", |
| }); |
|
|
| const [, init] = fetchMock.mock.calls[0]!; |
| const envelope = JSON.parse((init as { body: string }).body); |
| expect( |
| (envelope as { headers: Record<string, string> }).headers, |
| ).not.toHaveProperty("Authorization"); |
| }); |
| }); |
|
|
| describe("callCloudProxy automation direct routing", () => { |
| it("sends automation requests straight to the cloud host with the API key instead of the /api/cloud-proxy envelope", async () => { |
| |
| |
| |
| setRegisteredBackends([cloudPersonal]); |
| setActiveSelection({ backendId: cloudPersonal.id, orgId: null }); |
| const page = { automations: [], total: 0 }; |
| fetchMock.mockResolvedValueOnce(mockJsonResponse(page)); |
|
|
| |
| const result = await callCloudProxy({ |
| backend: cloudPersonal, |
| method: "GET", |
| path: "/api/automation/v1?limit=50&offset=0", |
| }); |
|
|
| |
| |
| |
| const [url, init] = fetchMock.mock.calls[0]!; |
| expect(url).toBe( |
| `${cloudPersonal.host}/api/automation/v1?limit=50&offset=0`, |
| ); |
| expect(init).toMatchObject({ |
| method: "GET", |
| }); |
| expect( |
| (init as { headers: Record<string, string> }).headers.Authorization, |
| ).toBe(`Bearer ${cloudPersonal.apiKey}`); |
| expect(result).toEqual(page); |
| }); |
|
|
| it("forwards the blob responseType and fail-fast timeout to the direct request", async () => { |
| |
| |
| |
| setRegisteredBackends([cloudPersonal]); |
| setActiveSelection({ backendId: cloudPersonal.id, orgId: null }); |
|
|
| |
| await callCloudProxy({ |
| backend: cloudPersonal, |
| method: "GET", |
| path: "/api/automation/v1/auto-1/tarball", |
| responseType: "blob", |
| timeoutSeconds: 5, |
| }); |
|
|
| |
| const [url, init] = fetchMock.mock.calls[0]!; |
| expect(url).toBe(`${cloudPersonal.host}/api/automation/v1/auto-1/tarball`); |
| expect(init).toMatchObject({ method: "GET" }); |
| }); |
| }); |
|
|
| describe("callCloudProxy hostOverride routing", () => { |
| const runtimeHost = "https://abc123.prod-runtime.all-hands.dev"; |
|
|
| it("routes through the local /api/cloud-proxy instead of the upstream host when hostOverride is set", async () => { |
| |
| |
| |
| setRegisteredBackends([cloudPersonal]); |
| setActiveSelection({ backendId: cloudPersonal.id, orgId: null }); |
| fetchMock.mockResolvedValueOnce(mockJsonResponse({ items: [] })); |
|
|
| |
| const result = await callCloudProxy({ |
| backend: cloudPersonal, |
| method: "GET", |
| path: "/api/bash/bash_events/search", |
| hostOverride: runtimeHost, |
| }); |
|
|
| |
| |
| |
| const [url, init] = fetchMock.mock.calls[0]!; |
| expect(url).toMatch(/\/api\/cloud-proxy$/); |
| const envelope = JSON.parse((init as { body: string }).body); |
| expect(envelope).toMatchObject({ |
| host: runtimeHost, |
| method: "GET", |
| path: "/api/bash/bash_events/search", |
| }); |
| expect(result).toEqual({ items: [] }); |
| }); |
|
|
| it("carries bearer auth and X-Org-Id inside the proxy envelope", async () => { |
| |
| |
| |
| setRegisteredBackends([cloudPersonal]); |
| setActiveSelection({ |
| backendId: cloudPersonal.id, |
| orgId: "org-personal-uuid", |
| }); |
|
|
| |
| await callCloudProxy({ |
| backend: cloudPersonal, |
| method: "GET", |
| path: "/api/bash/bash_events/search", |
| hostOverride: runtimeHost, |
| }); |
|
|
| |
| const [, init] = fetchMock.mock.calls[0]!; |
| const envelope = JSON.parse((init as { body: string }).body); |
| expect( |
| (envelope as { headers: Record<string, string> }).headers, |
| ).toMatchObject({ |
| Authorization: `Bearer ${cloudPersonal.apiKey}`, |
| "X-Org-Id": "org-personal-uuid", |
| }); |
| }); |
| }); |
|
|