// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. /* eslint-disable max-lines */ import {DeviceEventEmitter} from 'react-native'; import LocalConfig from '@assets/config.json'; import {Events} from '@constants'; import NetworkPerformanceManager from '@managers/network_performance_manager'; import test_helper from '@test/test_helper'; import * as ClientConstants from './constants'; import ClientError from './error'; import ClientTracking, {testExports} from './tracking'; import type {APIClientInterface, ClientResponseMetrics} from '@mattermost/react-native-network-client'; type ParallelGroup = typeof testExports.ParallelGroup; jest.mock('react-native', () => ({ DeviceEventEmitter: { emit: jest.fn(), }, Platform: { OS: 'ios', select: jest.fn(({ios, default: def}: any) => { return ios ?? def; }), }, })); jest.mock('@mattermost/rnutils', () => ({ getIOSAppGroupDetails: jest.fn().mockRejectedValue(''), isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}), })); jest.mock('expo-crypto', () => ({ randomUUID: jest.fn(() => '12345678-1234-1234-1234-1234567890ab'), })); jest.mock('@i18n', () => ({ t: jest.fn((key) => key), })); jest.mock('@utils/file', () => ({ getFormattedFileSize: jest.fn((size) => `${size} bytes`), })); jest.mock('@utils/log', () => ({ logDebug: jest.fn(), logInfo: jest.fn(), })); jest.mock('@utils/server', () => ({ semverFromServerVersion: jest.fn((version) => version), })); jest.mock('@init/credentials', () => ({ setServerCredentials: jest.fn(), })); jest.mock('@managers/performance_metrics_manager', () => ({ collectNetworkRequestData: jest.fn(), })); jest.mock('@managers/network_performance_manager', () => ({ __esModule: true, default: { startRequestTracking: jest.fn(() => 'mock-request-id-123'), completeRequestTracking: jest.fn(), cancelRequestTracking: jest.fn(), }, })); describe('ClientTracking', () => { const mockedNPM = jest.mocked(NetworkPerformanceManager); const apiClientMock = { baseUrl: 'https://example.com', get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn(), }; let client: ClientTracking; beforeAll(() => { LocalConfig.CollectNetworkMetrics = true; }); beforeEach(() => { client = new ClientTracking(apiClientMock as unknown as APIClientInterface); }); afterEach(() => { jest.clearAllMocks(); }); afterAll(() => { LocalConfig.CollectNetworkMetrics = false; }); it('should set bearer token', () => { const token = 'testToken'; client.setClientCredentials(token); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} ${token}`); expect(require('@init/credentials').setServerCredentials).toHaveBeenCalledWith(apiClientMock.baseUrl, token, undefined); }); it('should set CSRF token', () => { const token = 'csrfToken'; client.setCSRFToken(token); expect(client.csrfToken).toBe(token); }); it('should get request headers', () => { client.setCSRFToken('csrfToken'); client.setClientCredentials('testToken'); const headers = client.getRequestHeaders('POST'); expect(headers[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} testToken`); expect(headers[ClientConstants.HEADER_X_CSRF_TOKEN]).toBe('csrfToken'); }); it('should initialize and track group data', () => { client.initTrackGroup('Cold Start'); expect(client.requestGroups.has('Cold Start')).toBe(true); client.trackRequest('Cold Start', 'https://example.com/api', {size: 100, compressedSize: 50} as ClientResponseMetrics); const group = client.requestGroups.get('Cold Start')!; expect(group.totalSize).toBe(100); expect(group.totalCompressedSize).toBe(50); expect(group.urls['https://example.com/api'].count).toBe(1); }); it('should increment and decrement request count', () => { client.initTrackGroup('Cold Start'); client.incrementRequestCount('Cold Start'); let group = client.requestGroups.get('Cold Start')!; expect(group.activeCount).toBe(1); client.decrementRequestCount('Cold Start'); group = client.requestGroups.get('Cold Start')!; expect(group.activeCount).toBe(0); }); it('should handle request completion', () => { client.initTrackGroup('Cold Start'); client.handleRequestCompletion('Cold Start'); expect(require('@utils/log').logDebug).toHaveBeenCalledWith('Group "Cold Start" completed.'); }); it('should clear completion timer', () => { client.initTrackGroup('Cold Start'); const group = client.requestGroups.get('Cold Start')!; group.completionTimer = setTimeout(() => {}, 100); client.clearCompletionTimer('Cold Start'); expect(group.completionTimer).toBeUndefined(); }); it('should check if all requests are completed', () => { client.initTrackGroup('Cold Start'); const result = client.allRequestsCompleted('Cold Start'); expect(result).toBe(true); }); it('should send telemetry event', () => { client.initTrackGroup('Cold Start'); const group = client.requestGroups.get('Cold Start')!; group.urls = { 'https://example.com/api': { count: 2, metrics: { latency: 200, startTime: Date.now(), endTime: Date.now() + 300, speedInMbps: 1, networkType: 'Wi-Fi', tlsCipherSuite: 'none', tlsVersion: 'none', isCached: false, httpVersion: 'h2', compressedSize: 10 * 1024, size: 6 * 1024 * 1024, connectionTime: 0, }, }, }; client.sendTelemetryEvent('Cold Start', group, 1000); expect(require('@utils/log').logInfo).toHaveBeenCalled(); }); it('should build request options', () => { const options = { body: {key: 'value'}, method: 'POST', noRetry: true, timeoutInterval: 5000, headers: {custom: 'header'}, }; const result = client.buildRequestOptions(options); expect(result.headers?.custom).toBe('header'); expect(result.retryPolicyConfiguration?.retryLimit).toBe(0); expect(result.timeoutInterval).toBe(5000); }); it('should perform fetch with tracking', async () => { apiClientMock.get.mockResolvedValue({ ok: true, data: {success: true}, headers: {}, }); const options = { method: 'GET', groupLabel: 'Cold Start' as RequestGroupLabel, }; const result = await client.doFetchWithTracking('https://example.com/api', options); expect(result).toEqual({success: true}); }); it('should handle fetch errors', async () => { apiClientMock.get.mockRejectedValue(new Error('Request failed')); const options = { method: 'GET', groupLabel: 'Cold Start' as RequestGroupLabel, }; await expect(client.doFetchWithTracking('https://example.com/api', options)).rejects.toThrow('Received invalid response from the server.'); }); it('should handle non-ok response with error details', async () => { apiClientMock.get.mockResolvedValue({ ok: false, data: { message: 'Custom error message', id: 'error_id_123', }, code: 400, }); const options = { method: 'GET', groupLabel: 'Cold Start' as RequestGroupLabel, }; try { await client.doFetchWithTracking('https://example.com/api', options); } catch (error: unknown) { expect(error).toBeInstanceOf(ClientError); const clientError = error as ClientError; expect((clientError as {message: string}).message).toBe('Custom error message'); expect((clientError as {server_error_id: string}).server_error_id).toBe('error_id_123'); expect((clientError as {status_code: number}).status_code).toBe(400); } }); it('should handle non-ok response without error details', async () => { apiClientMock.get.mockResolvedValue({ ok: false, data: {}, code: 500, }); const options = { method: 'GET', groupLabel: 'Cold Start' as RequestGroupLabel, }; try { await client.doFetchWithTracking('https://example.com/api', options); fail('Expected error to be thrown'); } catch (error: unknown) { const clientError = error as ClientError; expect((clientError as {message: string}).message).toBe('Response with status code 500'); expect((clientError as {status_code: number}).status_code).toBe(500); } }); it('should handle response with bearer token header', async () => { apiClientMock.get.mockResolvedValue({ ok: true, data: {success: true}, headers: { Token: 'new_bearer_token', }, }); const options = { method: 'GET', groupLabel: 'Cold Start' as RequestGroupLabel, }; await client.doFetchWithTracking('https://example.com/api', options); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new_bearer_token`); }); it('should handle response with lowercase bearer token header', async () => { apiClientMock.get.mockResolvedValue({ ok: true, data: {success: true}, headers: { token: 'new_lowercase_token', }, }); const options = { method: 'GET', groupLabel: 'Cold Start' as RequestGroupLabel, }; await client.doFetchWithTracking('https://example.com/api', options); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new_lowercase_token`); }); it('should call increment and decrement the same number of times as doFetchWithTracking, and handleRequestCompletion only once', async () => { const incrementSpy = jest.spyOn(client, 'incrementRequestCount'); const decrementSpy = jest.spyOn(client, 'decrementRequestCount'); const handleRequestCompletionSpy = jest.spyOn(client, 'handleRequestCompletion'); apiClientMock.get.mockImplementation(() => { return new Promise((resolve) => { setTimeout(() => { resolve({ ok: true, data: {success: true}, headers: {}, metrics: {latency: 200, size: 500, compressedSize: 300, startTime: Date.now(), endTime: Date.now() + 100, speedInMbps: 1}, }); }, 200); // Simulate a 100ms network delay }); }); client.initTrackGroup('Cold Start'); // Simulate multiple fetch calls const promises = [ client.doFetchWithTracking('https://example.com/api1', {method: 'GET', groupLabel: 'Cold Start'}), client.doFetchWithTracking('https://example.com/api2', {method: 'GET', groupLabel: 'Cold Start'}), client.doFetchWithTracking('https://example.com/api3', {method: 'GET', groupLabel: 'Cold Start'}), ]; // Wait for all fetches to resolve await Promise.all(promises); const group = client.requestGroups.get('Cold Start')!; expect(group.activeCount).toBe(0); await test_helper.wait(300); expect(handleRequestCompletionSpy).toHaveBeenCalledTimes(1); expect(incrementSpy).toHaveBeenCalledTimes(3); expect(decrementSpy).toHaveBeenCalledTimes(3); }); it('should handle invalid HTTP method', async () => { const options = { method: 'INVALID' as string, groupLabel: 'Cold Start' as RequestGroupLabel, }; const result = await client.doFetchWithTracking('https://example.com/api', options) as unknown as {error: string}; expect(result.error).toEqual(new Error('Invalid request method')); }); it('should handle server version changes without cache control', async () => { const emitSpy = jest.spyOn(DeviceEventEmitter, 'emit'); apiClientMock.get.mockResolvedValue({ ok: true, data: {success: true}, headers: { 'x-version-id': '5.30.0', }, }); await client.doFetchWithTracking('https://example.com/api', { method: 'GET', groupLabel: 'Cold Start', }); expect(client.serverVersion).toBe('5.30.0'); expect(emitSpy).toHaveBeenCalledWith( Events.SERVER_VERSION_CHANGED, {serverUrl: 'https://example.com', serverVersion: '5.30.0'}, ); }); it('should not update server version when cache control present', async () => { client.serverVersion = '5.20.0'; apiClientMock.get.mockResolvedValue({ ok: true, data: {success: true}, headers: { 'x-version-id': '5.31.0', 'Cache-Control': 'no-cache', }, }); await client.doFetchWithTracking('https://example.com/api', { method: 'GET', groupLabel: 'Cold Start', }); expect(client.serverVersion).toBe('5.20.0'); }); it('should track duplicate requests correctly and log duplicate details', async () => { const logDebugSpy = jest.spyOn(require('@utils/log'), 'logDebug'); apiClientMock.get.mockResolvedValue({ ok: true, data: {success: true}, headers: {}, metrics: {latency: 150, size: 200, compressedSize: 100, startTime: Date.now(), endTime: Date.now() + 100, speedInMbps: 1}, }); client.initTrackGroup('Cold Start'); // Simulate multiple fetch calls to the same URL const promises = [ client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}), client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}), client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}), ]; // Wait for all fetches to resolve await Promise.all(promises); // Verify that the group tracked duplicates correctly const group = client.requestGroups.get('Cold Start')!; expect(group.urls['https://example.com/api'].count).toBe(3); // URL was requested 3 times expect(group.totalSize).toBe(600); // Total size = 3 * 200 expect(group.totalCompressedSize).toBe(300); // Total compressed size = 3 * 100 await test_helper.wait(300); // Verify duplicate logging expect(logDebugSpy).toHaveBeenCalledWith( expect.stringContaining('Duplicate URLs:\n1 - https://example.com/api'), ); }); it('should return true when checking allRequestsCompleted for non-existent group', () => { // Try to check completion status for a non-existent group const result = client.allRequestsCompleted('Non Existent Group' as RequestGroupLabel); // Should return true since there are no pending requests expect(result).toBe(true); }); describe('Request Categorization and Speed Calculations', () => { it('should create multiple parallel groups for non-overlapping requests', () => { client.initTrackGroup('Cold Start'); const group = client.requestGroups.get('Cold Start')!; const baseTime = Date.now(); // First parallel group (2 concurrent requests) group.urls = { 'https://example.com/api1': { count: 1, metrics: { latency: 100, startTime: baseTime, endTime: baseTime + 200, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, 'https://example.com/api2': { count: 1, metrics: { latency: 150, startTime: baseTime + 50, endTime: baseTime + 250, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, // Second parallel group (3 concurrent requests) 'https://example.com/api3': { count: 1, metrics: { latency: 120, startTime: baseTime + 300, endTime: baseTime + 500, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, 'https://example.com/api4': { count: 1, metrics: { latency: 180, startTime: baseTime + 320, endTime: baseTime + 520, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, 'https://example.com/api5': { count: 1, metrics: { latency: 160, startTime: baseTime + 340, endTime: baseTime + 540, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, }; const result = client.categorizeRequests('Cold Start'); // Verify parallel groups expect(result.parallelGroups).toHaveLength(2); expect(result.maxConcurrency).toBe(3); // First group should have 2 requests expect(result.parallelGroups[0].requests).toHaveLength(2); expect(result.parallelGroups[0].latency).toBe(150); // Max latency of first group // Second group should have 3 requests expect(result.parallelGroups[1].requests).toHaveLength(3); expect(result.parallelGroups[1].latency).toBe(180); // Max latency of second group }); it('should handle overlapping parallel groups correctly', () => { client.initTrackGroup('Cold Start'); const group = client.requestGroups.get('Cold Start')!; const baseTime = Date.now(); group.urls = { // First request starts early and ends late 'https://example.com/api1': { count: 1, metrics: { latency: 500, startTime: baseTime, endTime: baseTime + 1000, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, // These requests start during the first request 'https://example.com/api2': { count: 1, metrics: { latency: 200, startTime: baseTime + 100, endTime: baseTime + 300, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, 'https://example.com/api3': { count: 1, metrics: { latency: 200, startTime: baseTime + 200, endTime: baseTime + 400, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, }; const result = client.categorizeRequests('Cold Start'); // Should create a single parallel group since all requests overlap expect(result.parallelGroups).toHaveLength(1); expect(result.maxConcurrency).toBe(3); // The group should contain all 3 requests expect(result.parallelGroups[0].requests).toHaveLength(3); expect(result.parallelGroups[0].latency).toBe(500); // Should take the max latency }); it('should handle requests with latency extending beyond group end time', () => { client.initTrackGroup('Cold Start'); const group = client.requestGroups.get('Cold Start')!; const baseTime = Date.now(); const dataTransferTime = 200; group.urls = { // First request starts and ends quickly 'https://example.com/api1': { count: 1, metrics: { latency: 100, startTime: baseTime, endTime: baseTime + dataTransferTime + 100, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, // Second request starts at same time but has much longer latency 'https://example.com/api2': { count: 1, metrics: { latency: 500, // Long latency startTime: baseTime, endTime: baseTime + dataTransferTime + 500, // Ends much later size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, }; const result = client.categorizeRequests('Cold Start'); // Should create a single parallel group expect(result.parallelGroups).toHaveLength(1); expect(result.maxConcurrency).toBe(2); const group1 = result.parallelGroups[0]; // Group end time should be the earliest end time expect(group1.endTime).toBe(baseTime + dataTransferTime + 100); // But latency should be from the longest request expect(group1.latency).toBe(500); }); it('should handle requests with latency extending beyond group end time and separate parallel groups', () => { client.initTrackGroup('Cold Start'); const group = client.requestGroups.get('Cold Start')!; const baseTime = Date.now(); const dataTransferTime = 200; const firstGroupMinEndTime = baseTime + dataTransferTime + 100; group.urls = { // First parallel group 'https://example.com/api1': { count: 1, metrics: { latency: 100, startTime: baseTime, endTime: firstGroupMinEndTime, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, 'https://example.com/api2': { count: 1, metrics: { latency: 500, startTime: baseTime, endTime: baseTime + dataTransferTime + 500, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, // Second parallel group - starts after first group has ended 'https://example.com/api3': { count: 1, metrics: { latency: 300, startTime: firstGroupMinEndTime + 1, // Starts after first group endTime: firstGroupMinEndTime + dataTransferTime + 300, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, 'https://example.com/api4': { count: 1, metrics: { latency: 600, startTime: firstGroupMinEndTime + 1, // Starts with api3 endTime: firstGroupMinEndTime + 100 + 600, size: 1000, compressedSize: 500, } as ClientResponseMetrics, }, }; const result = client.categorizeRequests('Cold Start'); // Should create two parallel groups expect(result.parallelGroups).toHaveLength(2); expect(result.maxConcurrency).toBe(2); const group1 = result.parallelGroups[0]; const group2 = result.parallelGroups[1]; // First group assertions expect(group1.endTime).toBe(baseTime + dataTransferTime + 100); // longest latency on the first group is 500, but the first group ends after 300ms, so the effective latency is only 300ms expect(group1.latency).toBe(300); expect(group1.requests).toHaveLength(2); // Second group assertions expect(group2.endTime).toBe(firstGroupMinEndTime + dataTransferTime + 300); // second group min endTime after 500ms, but it's the last group to end, so the effective latency for the group // is 600ms (longest latency) expect(group2.latency).toBe(600); expect(group2.requests).toHaveLength(2); }); it('should calculate average speed and effective latency for multiple parallel groups', () => { const baseTime = Date.now(); // Create test parallel groups with known data sizes and timings const parallelGroups: ParallelGroup[] = [ { startTime: baseTime, endTime: baseTime + 1000, latency: 200, requests: [ { latency: 200, startTime: baseTime, endTime: baseTime + 1000, compressedSize: 1000000, // 1MB } as ClientResponseMetrics, { latency: 150, startTime: baseTime + 100, endTime: baseTime + 900, compressedSize: 500000, // 0.5MB } as ClientResponseMetrics, ], }, { startTime: baseTime + 1100, endTime: baseTime + 2000, latency: 300, requests: [ { latency: 300, startTime: baseTime + 1100, endTime: baseTime + 2000, compressedSize: 2000000, // 2MB } as ClientResponseMetrics, { latency: 250, startTime: baseTime + 1200, endTime: baseTime + 1900, compressedSize: 1500000, // 1.5MB } as ClientResponseMetrics, ], }, ]; // Total compressed size: 5MB (5,000,000 bytes) // Total elapsed time: 2 seconds // Total effective latency: 500ms (200ms + 300ms) const result = client.calculateAverageSpeedWithCategories(parallelGroups, 2); // Expected average speed: // Total bits = 5,000,000 * 8 = 40,000,000 bits // Data transfer time = 2 seconds - (500ms / 1000) = 1.5 seconds // Speed = 40,000,000 / 1.5 = 26,666,666.67 bps = 26.67 Mbps expect(result.averageSpeedMbps).toBeCloseTo(26.67, 1); expect(result.effectiveLatency).toBe(500); // Sum of max latencies from each group }); it('should handle zero transfer time in speed calculation', () => { client.initTrackGroup('Cold Start'); const group = client.requestGroups.get('Cold Start')!; const now = Date.now(); // Set up metrics where transfer time would be zero group.urls = { 'https://example.com/api': { count: 1, metrics: { latency: 1000, startTime: now, endTime: now + 1000, speedInMbps: 0, networkType: 'Wi-Fi', tlsCipherSuite: 'none', tlsVersion: 'none', isCached: false, httpVersion: 'h2', size: 1000, compressedSize: 500, connectionTime: 0, } as ClientResponseMetrics, }, }; const parallelGroups: ParallelGroup[] = [{ startTime: now, endTime: now + 1000, latency: 1000, requests: [group.urls['https://example.com/api'].metrics!], }]; const result = client.calculateAverageSpeedWithCategories( parallelGroups, 1, // 1 second elapsed ); expect(result.averageSpeedMbps).toBe(0); // Should be 0 since data transfer time is 0/negative expect(result.effectiveLatency).toBe(1000); }); it('should return empty result when groupData is not found', () => { // Try to categorize requests for a non-existent group const result = client.categorizeRequests('Non Existent Group' as RequestGroupLabel); // Verify empty result structure expect(result.parallelGroups).toEqual([]); expect(result.maxConcurrency).toBe(0); }); it('should return default latency when groupData is not found', () => { // Try to get average latency for a non-existent group const result = client.getAverageLatency('Non Existent Group' as RequestGroupLabel); // Should return default latency of 100ms expect(result).toBe(100); }); }); describe('setClientCredentials', () => { it('should set shared password header when provided', () => { client.setClientCredentials('bearer-token', 'shared-password'); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer-token`); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('shared-password'); }); it('should remove shared password header when undefined', () => { // First set a shared password client.setClientCredentials('bearer-token', 'shared-password'); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('shared-password'); // Then remove it by setting undefined client.setClientCredentials('bearer-token', undefined); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBeUndefined(); }); it('should remove shared password header when empty string', () => { // First set a shared password client.setClientCredentials('bearer-token', 'shared-password'); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('shared-password'); // Then remove it by setting empty string client.setClientCredentials('bearer-token', ''); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBeUndefined(); }); it('should always set bearer token correctly', () => { client.setClientCredentials('test-bearer', 'shared-password'); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} test-bearer`); client.setClientCredentials('new-bearer', undefined); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new-bearer`); }); it('should handle multiple header updates correctly', () => { // Set initial credentials client.setClientCredentials('bearer1', 'password1'); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer1`); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('password1'); // Update to new password client.setClientCredentials('bearer2', 'password2'); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer2`); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('password2'); // Remove password client.setClientCredentials('bearer3', undefined); expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer3`); expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBeUndefined(); }); }); describe('Network Performance Tracking', () => { const createMockMetrics = (overrides = {}) => ({ latency: 500, size: 1000, compressedSize: 500, startTime: Date.now(), endTime: Date.now() + 500, speedInMbps: 1, networkType: 'Wi-Fi', tlsCipherSuite: 'none', tlsVersion: 'none', isCached: false, httpVersion: 'h2', connectionTime: 0, ...overrides, }); const mockSuccessResponse = (metrics = createMockMetrics()) => ({ ok: true, data: {success: true}, headers: {}, metrics, }); const requestOptions = { method: 'GET', groupLabel: 'Cold Start' as RequestGroupLabel, }; beforeEach(() => { jest.clearAllMocks(); }); it('should call full tracking lifecycle', async () => { const mockMetrics = createMockMetrics(); apiClientMock.get.mockResolvedValue(mockSuccessResponse(mockMetrics)); await client.doFetchWithTracking('https://example.com/api', requestOptions); expect(mockedNPM.startRequestTracking).toHaveBeenCalledWith('https://example.com', 'https://example.com/api'); expect(mockedNPM.completeRequestTracking).toHaveBeenCalledWith('https://example.com', 'mock-request-id-123', mockMetrics); }); it('should pass correct server URL to NetworkPerformanceManager', async () => { const customBaseUrl = 'https://custom-server.com'; const customApiClient = {...apiClientMock, baseUrl: customBaseUrl}; const customClient = new ClientTracking(customApiClient as unknown as APIClientInterface); const mockMetrics = createMockMetrics({latency: 300, size: 2000, compressedSize: 1000, speedInMbps: 2}); customApiClient.get.mockResolvedValue(mockSuccessResponse(mockMetrics)); await customClient.doFetchWithTracking('https://custom-server.com/api', requestOptions); expect(mockedNPM.startRequestTracking).toHaveBeenCalledWith(customBaseUrl, 'https://custom-server.com/api'); expect(mockedNPM.completeRequestTracking).toHaveBeenCalledWith(customBaseUrl, 'mock-request-id-123', mockMetrics); }); it('should call cancelRequestTracking when request fails', async () => { apiClientMock.get.mockRejectedValue(new Error('Request failed')); await expect(client.doFetchWithTracking('https://example.com/api', requestOptions)).rejects.toThrow('Received invalid response from the server.'); expect(mockedNPM.startRequestTracking).toHaveBeenCalledWith('https://example.com', 'https://example.com/api'); expect(mockedNPM.cancelRequestTracking).toHaveBeenCalledWith('https://example.com', 'mock-request-id-123'); expect(mockedNPM.completeRequestTracking).not.toHaveBeenCalled(); }); }); }); /* eslint-enable max-lines */