From 738fa5b8940ee2eea313b4978758cb36c1d57f07 Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Tue, 5 May 2026 13:02:14 -0700 Subject: [PATCH] test(mobile): verify platform lifecycle clients --- mobile/src/lib/app-metadata.test.ts | 11 ++ mobile/src/lib/platform.test.ts | 186 ++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 mobile/src/lib/app-metadata.test.ts create mode 100644 mobile/src/lib/platform.test.ts diff --git a/mobile/src/lib/app-metadata.test.ts b/mobile/src/lib/app-metadata.test.ts new file mode 100644 index 0000000..61a85f8 --- /dev/null +++ b/mobile/src/lib/app-metadata.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; +import { APP_PLATFORM, APP_VERSION, BUILD_NUMBER, OS_VERSION } from './app-metadata'; + +describe('mobile app metadata', () => { + it('derives platform and release metadata for shared platform clients', () => { + expect(APP_PLATFORM).toBe('ios'); + expect(APP_VERSION).toBe('0.1.0'); + expect(BUILD_NUMBER).toBe('1'); + expect(OS_VERSION).toBe('17.0'); + }); +}); diff --git a/mobile/src/lib/platform.test.ts b/mobile/src/lib/platform.test.ts new file mode 100644 index 0000000..932aeb7 --- /dev/null +++ b/mobile/src/lib/platform.test.ts @@ -0,0 +1,186 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { API_CONFIG, PRODUCT_ID } from '../api/config'; +import { mmkvStorage } from '../store/mmkv-storage'; +import { APP_VERSION, BUILD_NUMBER, OS_VERSION } from './app-metadata'; + +const { + blobClientMock, + createBlobClientMock, + createFeatureFlagClientMock, + createKillSwitchClientMock, + createTelemetryClientMock, + diagnosticsGetInstanceMock, + featureFlagClientMock, + getAccessTokenMock, + killSwitchClientMock, + telemetryClientMock, +} = vi.hoisted(() => { + const telemetryClient = { + flush: vi.fn(), + init: vi.fn(), + trackEvent: vi.fn(), + }; + const featureFlagClient = { + getValue: vi.fn(), + init: vi.fn(), + isEnabled: vi.fn(), + }; + const killSwitchClient = { + check: vi.fn(), + }; + const blobClient = { + upload: vi.fn(), + }; + + return { + telemetryClientMock: telemetryClient, + featureFlagClientMock: featureFlagClient, + killSwitchClientMock: killSwitchClient, + blobClientMock: blobClient, + createTelemetryClientMock: vi.fn(() => telemetryClient), + createFeatureFlagClientMock: vi.fn(() => featureFlagClient), + createKillSwitchClientMock: vi.fn(() => killSwitchClient), + createBlobClientMock: vi.fn(() => blobClient), + diagnosticsGetInstanceMock: vi.fn(() => ({ capture: vi.fn() })), + getAccessTokenMock: vi.fn(), + }; +}); + +vi.mock('@bytelyst/telemetry-client', () => ({ + createTelemetryClient: createTelemetryClientMock, +})); + +vi.mock('@bytelyst/feature-flag-client', () => ({ + createFeatureFlagClient: createFeatureFlagClientMock, +})); + +vi.mock('@bytelyst/kill-switch-client', () => ({ + createKillSwitchClient: createKillSwitchClientMock, +})); + +vi.mock('@bytelyst/blob-client', () => ({ + createBlobClient: createBlobClientMock, +})); + +vi.mock('@bytelyst/diagnostics-client', () => ({ + DiagnosticsClient: { + getInstance: diagnosticsGetInstanceMock, + }, +})); + +vi.mock('./auth-helpers', () => ({ + getAccessToken: getAccessTokenMock, +})); + +async function loadPlatform() { + vi.resetModules(); + return import('./platform'); +} + +describe('mobile platform lifecycle clients', () => { + beforeEach(() => { + vi.clearAllMocks(); + mmkvStorage.removeItem(`${PRODUCT_ID}_install_id`); + getAccessTokenMock.mockReturnValue('mobile-token'); + featureFlagClientMock.init.mockResolvedValue(undefined); + featureFlagClientMock.isEnabled.mockReturnValue(true); + featureFlagClientMock.getValue.mockImplementation((_key: string, fallback: unknown) => fallback); + killSwitchClientMock.check.mockResolvedValue({ disabled: false, message: null }); + }); + + it('configures telemetry, flags, kill switch, and blob clients with product metadata', async () => { + const platform = await loadPlatform(); + + expect(createTelemetryClientMock).toHaveBeenCalledWith({ + productId: PRODUCT_ID, + baseUrl: API_CONFIG.platformBaseUrl, + endpoint: '/telemetry/events', + platform: 'mobile', + channel: 'notelett_mobile', + transport: 'fetch', + appVersion: APP_VERSION, + buildNumber: BUILD_NUMBER, + releaseChannel: 'dev', + osFamily: OS_VERSION, + }); + expect(createFeatureFlagClientMock).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: API_CONFIG.platformBaseUrl, + productId: PRODUCT_ID, + platform: 'mobile', + pollIntervalMs: 5 * 60 * 1000, + getAccessToken: getAccessTokenMock, + }), + ); + expect(createKillSwitchClientMock).toHaveBeenCalledWith({ + baseUrl: API_CONFIG.platformBaseUrl, + productId: PRODUCT_ID, + platform: 'mobile', + }); + expect(createBlobClientMock).toHaveBeenCalledWith({ + baseUrl: API_CONFIG.platformBaseUrl, + productId: PRODUCT_ID, + getAccessToken: getAccessTokenMock, + }); + expect(platform.blobClient).toBe(blobClientMock); + }); + + it('initializes telemetry and feature flags once', async () => { + const { initPlatform } = await loadPlatform(); + + await initPlatform(); + await initPlatform(); + + expect(telemetryClientMock.init).toHaveBeenCalledTimes(1); + expect(telemetryClientMock.trackEvent).toHaveBeenCalledWith('info', 'app_shell', 'mobile_app_initialized'); + expect(featureFlagClientMock.init).toHaveBeenCalledTimes(1); + }); + + it('keeps platform initialization best-effort when feature flags fail', async () => { + const { initPlatform } = await loadPlatform(); + featureFlagClientMock.init.mockRejectedValueOnce(new Error('flags down')); + + await expect(initPlatform()).resolves.toBeUndefined(); + + expect(telemetryClientMock.init).toHaveBeenCalled(); + expect(telemetryClientMock.trackEvent).toHaveBeenCalledWith('info', 'app_shell', 'mobile_app_initialized'); + }); + + it('delegates feature checks, kill switch checks, and telemetry flushes', async () => { + const { checkKillSwitch, flushTelemetry, getFeatureValue, isFeatureEnabled } = await loadPlatform(); + + expect(isFeatureEnabled('mobile.capture')).toBe(true); + expect(getFeatureValue('mobile.limit', 10)).toBe(10); + await expect(checkKillSwitch()).resolves.toEqual({ disabled: false, message: null }); + flushTelemetry(); + + expect(featureFlagClientMock.isEnabled).toHaveBeenCalledWith('mobile.capture'); + expect(featureFlagClientMock.getValue).toHaveBeenCalledWith('mobile.limit', 10); + expect(killSwitchClientMock.check).toHaveBeenCalled(); + expect(telemetryClientMock.flush).toHaveBeenCalled(); + }); + + it('configures diagnostics with install id, app metadata, and auth token access', async () => { + const { getDiagnosticsClient } = await loadPlatform(); + const { mmkvStorage: platformStorage } = await import('../store/mmkv-storage'); + platformStorage.setItem(`${PRODUCT_ID}_install_id`, 'install-1'); + + getDiagnosticsClient(); + + expect(diagnosticsGetInstanceMock).toHaveBeenCalledWith({ + productId: PRODUCT_ID, + serverUrl: API_CONFIG.platformBaseUrl, + platform: 'mobile', + channel: 'notelett_mobile', + anonymousInstallId: 'install-1', + osFamily: OS_VERSION, + appVersion: APP_VERSION, + buildNumber: BUILD_NUMBER, + releaseChannel: 'dev', + getAuthToken: expect.any(Function), + }); + const calls = diagnosticsGetInstanceMock.mock.calls as unknown as Array<[{ getAuthToken: () => string }]>; + const options = calls[0][0]; + expect(options.getAuthToken()).toBe('mobile-token'); + }); +});