From efa20979fc81fe2d23ed1af1560c34343d84794e Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Tue, 5 May 2026 09:33:10 -0700 Subject: [PATCH] test(platform): verify client propagation --- mobile/src/api/client.test.ts | 47 +++++++++++++++++++++++ mobile/src/lib/platform-api.test.ts | 46 ++++++++++++++++++++++ web/src/lib/api-helpers.test.ts | 59 +++++++++++++++++++++++++++++ web/src/lib/platform.test.ts | 46 ++++++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 mobile/src/api/client.test.ts create mode 100644 mobile/src/lib/platform-api.test.ts create mode 100644 web/src/lib/api-helpers.test.ts create mode 100644 web/src/lib/platform.test.ts diff --git a/mobile/src/api/client.test.ts b/mobile/src/api/client.test.ts new file mode 100644 index 0000000..843ebb5 --- /dev/null +++ b/mobile/src/api/client.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { API_CONFIG } from './config'; +import { getApiClient } from './client'; + +const { getAccessTokenMock } = vi.hoisted(() => ({ + getAccessTokenMock: vi.fn(), +})); + +const fetchMock = vi.fn(); + +vi.mock('./auth', () => ({ + getAuthClient: () => ({ + getAccessToken: getAccessTokenMock, + }), +})); + +function jsonResponse(data: unknown, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + statusText: status === 200 ? 'OK' : 'Error', + json: () => Promise.resolve(data), + }; +} + +describe('mobile API client', () => { + beforeEach(() => { + fetchMock.mockReset(); + getAccessTokenMock.mockReset(); + vi.stubGlobal('fetch', fetchMock); + }); + + it('propagates product identity, auth token, and request id through the shared API client', async () => { + getAccessTokenMock.mockReturnValue('mobile-token'); + fetchMock.mockResolvedValue(jsonResponse({ ok: true })); + + await getApiClient().fetch('/workspaces'); + + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + const headers = init.headers as Record; + + expect(headers['x-product-id']).toBe(API_CONFIG.productId); + expect(headers.Authorization).toBe('Bearer mobile-token'); + expect(headers['x-request-id']).toBeTruthy(); + }); +}); diff --git a/mobile/src/lib/platform-api.test.ts b/mobile/src/lib/platform-api.test.ts new file mode 100644 index 0000000..ea577e9 --- /dev/null +++ b/mobile/src/lib/platform-api.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { API_CONFIG, PRODUCT_ID } from '../api/config'; + +const { createPlatformClientMock, getMock, getAccessTokenMock } = vi.hoisted(() => ({ + createPlatformClientMock: vi.fn(), + getMock: vi.fn(), + getAccessTokenMock: vi.fn(), +})); + +vi.mock('@bytelyst/platform-client', () => ({ + createPlatformClient: createPlatformClientMock, +})); + +vi.mock('./auth-helpers', () => ({ + getAccessToken: getAccessTokenMock, +})); + +import { getUserSettings } from './platform-api'; + +describe('mobile platform API wrapper', () => { + beforeEach(() => { + createPlatformClientMock.mockReset(); + getMock.mockReset(); + getAccessTokenMock.mockReset(); + getAccessTokenMock.mockReturnValue('mobile-platform-token'); + getMock.mockResolvedValue({ theme: 'dark' }); + createPlatformClientMock.mockReturnValue({ + get: getMock, + put: vi.fn(), + del: vi.fn(), + }); + }); + + it('configures the shared platform client with product identity and auth token access', async () => { + await getUserSettings(); + + expect(createPlatformClientMock).toHaveBeenCalledWith({ + baseUrl: API_CONFIG.platformBaseUrl, + productId: PRODUCT_ID, + getAccessToken: getAccessTokenMock, + }); + expect(createPlatformClientMock.mock.calls[0][0].getAccessToken()).toBe('mobile-platform-token'); + expect(getMock).toHaveBeenCalledWith('/settings'); + }); +}); diff --git a/web/src/lib/api-helpers.test.ts b/web/src/lib/api-helpers.test.ts new file mode 100644 index 0000000..22e96e6 --- /dev/null +++ b/web/src/lib/api-helpers.test.ts @@ -0,0 +1,59 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { createNotesApiClient, getAccessToken } from "@/lib/api-helpers"; +import { PRODUCT_ID } from "@/lib/product-config"; + +const fetchMock = vi.fn(); +const storage = new Map(); +const localStorageMock = { + getItem: vi.fn((key: string) => storage.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => { + storage.set(key, value); + }), + clear: vi.fn(() => { + storage.clear(); + }), +}; + +function jsonResponse(data: unknown, status = 200) { + return { + ok: status >= 200 && status < 300, + status, + statusText: status === 200 ? "OK" : "Error", + json: () => Promise.resolve(data), + }; +} + +describe("api-helpers", () => { + beforeEach(() => { + fetchMock.mockReset(); + vi.stubGlobal("fetch", fetchMock); + storage.clear(); + Object.defineProperty(window, "localStorage", { + configurable: true, + value: localStorageMock, + }); + vi.stubGlobal("localStorage", localStorageMock); + }); + + it("reads the product-scoped access token from localStorage", () => { + localStorage.setItem(`${PRODUCT_ID}_access_token`, "web-token"); + + expect(getAccessToken()).toBe("web-token"); + }); + + it("propagates product identity, auth token, and request id through the shared API client", async () => { + localStorage.setItem(`${PRODUCT_ID}_access_token`, "web-token"); + fetchMock.mockResolvedValue(jsonResponse({ ok: true })); + + const api = createNotesApiClient(); + await api.fetch("/workspaces"); + + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + const headers = init.headers as Record; + + expect(headers["x-product-id"]).toBe(PRODUCT_ID); + expect(headers.Authorization).toBe("Bearer web-token"); + expect(headers["x-request-id"]).toBeTruthy(); + }); +}); diff --git a/web/src/lib/platform.test.ts b/web/src/lib/platform.test.ts new file mode 100644 index 0000000..5d9492f --- /dev/null +++ b/web/src/lib/platform.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { PLATFORM_SERVICE_URL, PRODUCT_ID } from "@/lib/product-config"; + +const { createPlatformClientMock, getMock, getAccessTokenMock } = vi.hoisted(() => ({ + createPlatformClientMock: vi.fn(), + getMock: vi.fn(), + getAccessTokenMock: vi.fn(), +})); + +vi.mock("@bytelyst/platform-client", () => ({ + createPlatformClient: createPlatformClientMock, +})); + +vi.mock("@/lib/api-helpers", () => ({ + getAccessToken: getAccessTokenMock, +})); + +import { getUserSettings } from "@/lib/platform"; + +describe("platform client wrapper", () => { + beforeEach(() => { + createPlatformClientMock.mockReset(); + getMock.mockReset(); + getAccessTokenMock.mockReset(); + getAccessTokenMock.mockReturnValue("web-platform-token"); + getMock.mockResolvedValue({ theme: "dark" }); + createPlatformClientMock.mockReturnValue({ + get: getMock, + put: vi.fn(), + del: vi.fn(), + }); + }); + + it("configures the shared platform client with product identity and auth token access", async () => { + await getUserSettings(); + + expect(createPlatformClientMock).toHaveBeenCalledWith({ + baseUrl: PLATFORM_SERVICE_URL, + productId: PRODUCT_ID, + getAccessToken: getAccessTokenMock, + }); + expect(createPlatformClientMock.mock.calls[0][0].getAccessToken()).toBe("web-platform-token"); + expect(getMock).toHaveBeenCalledWith("/settings"); + }); +});