mattermost-mobile/app/client/rest/tracking.test.ts
Rahim Rahman abe7a24be5
feat(MM-65145): low connectivity manager (#9283)
* feat(MM-65145): low connectivity manager
* forgot localization
* connection banner showing limited network connectivity
* network_performance_manager test
* adding some test related to network performance monitoring
* add comment about count-based sliding window
* component and hook tests
* clean up some code based on review.
* changes based on CP review
2025-12-04 10:44:33 -07:00

960 lines
37 KiB
TypeScript

// 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 */