Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions app/util/test/network-store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import axios from 'axios';
import { Platform } from 'react-native';
import { getFixturesServerPortInApp } from './utils';
import {
appendE2EDiagnosticsToUrl,
getE2ETestConfigDiagnostics,
getFixturesServerPortInApp,
} from './utils';

const FETCH_TIMEOUT = 40000; // Timeout in milliseconds

Expand Down Expand Up @@ -90,15 +94,21 @@ class ReadOnlyNetworkStore {
// 1. localhost with actual port (works on iOS, might work on Android with adb reverse)
// 2. 10.0.2.2 with actual port (Android emulator host, direct access without adb reverse!)
// 3. bs-local.com (BrowserStack)
const urls = [`http://localhost:${port}/state.json`];
const diagnostics = getE2ETestConfigDiagnostics({
source: 'network-store',
platform: Platform.OS,
});
const appendDiagnostics = (url) =>
appendE2EDiagnosticsToUrl(url, diagnostics);
const urls = [appendDiagnostics(`http://localhost:${port}/state.json`)];

// Android emulator can access host via 10.0.2.2 without adb reverse
if (isAndroid) {
urls.push(`http://10.0.2.2:${port}/state.json`);
urls.push(appendDiagnostics(`http://10.0.2.2:${port}/state.json`));
}

// BrowserStack uses bs-local.com
urls.push(`http://bs-local.com:${port}/state.json`);
urls.push(appendDiagnostics(`http://bs-local.com:${port}/state.json`));

try {
for (const url of urls) {
Expand Down
51 changes: 49 additions & 2 deletions app/util/test/utils.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
/* global globalThis */

export const flushPromises = () => new Promise(setImmediate);

// Fallback ports - used in fixture data and when LaunchArgs are unavailable
// Android: These are mapped to actual PortManager-allocated ports via adb reverse
// iOS: These are overridden by LaunchArgs at runtime
export const FALLBACK_FIXTURE_SERVER_PORT = 12345;
export const FALLBACK_COMMAND_QUEUE_SERVER_PORT = 2446;
export const E2E_TEST_CONFIG_GLOBAL_KEY = '__METAMASK_E2E_TEST_CONFIG__';
export const E2E_DIAGNOSTICS_ENDPOINT = '/e2e-diagnostics';

// E2E test configuration required in app
export const testConfig = {};
// E2E test configuration required in app. Metro/RN can evaluate this module
// through more than one path during startup, so keep the backing object on
// globalThis and make every module instance share the same runtime config.
if (!globalThis[E2E_TEST_CONFIG_GLOBAL_KEY]) {
globalThis[E2E_TEST_CONFIG_GLOBAL_KEY] = {};
}
export const testConfig = globalThis[E2E_TEST_CONFIG_GLOBAL_KEY];

// SEGMENT TRACK URL for E2E tests - this is not a real URL and is used for testing purposes only
export const E2E_METAMETRICS_TRACK_URL = 'https://metametrics.test/track';
Expand Down Expand Up @@ -43,4 +52,42 @@
export const getCommandQueueServerPortInApp = () =>
testConfig.commandQueueServerPort ?? FALLBACK_COMMAND_QUEUE_SERVER_PORT;

const formatE2EDiagnosticValue = (value) => {
if (value === undefined) {
return 'missing';
}
if (value === null) {
return 'null';
}
return String(value);
};

export const getE2ETestConfigDiagnostics = (extra = {}) => ({
source: extra.source ?? 'unknown',
phase: extra.phase ?? 'unknown',
platform: extra.platform ?? 'unknown',
fixtureServerPortInApp: getFixturesServerPortInApp(),
commandQueueServerPortInApp: getCommandQueueServerPortInApp(),
fixtureServerPortRaw: formatE2EDiagnosticValue(testConfig.fixtureServerPort),
commandQueueServerPortRaw: formatE2EDiagnosticValue(
testConfig.commandQueueServerPort,
),
mockServerPortRaw: formatE2EDiagnosticValue(testConfig.mockServerPort),
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
hasGlobalE2EConfig:
globalThis[E2E_TEST_CONFIG_GLOBAL_KEY] === testConfig ? 'true' : 'false',
configKeys: Object.keys(testConfig).sort().join(',') || 'none',

Check failure on line 78 in app/util/test/utils.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Provide a compare function that depends on "String.localeCompare", to reliably sort elements alphabetically.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ4riYWihHNRY-AfPp6Q&open=AZ4riYWihHNRY-AfPp6Q&pullRequest=30240
launchArgumentKeys: Array.isArray(testConfig.launchArgumentKeys)
? testConfig.launchArgumentKeys.join(',')
: 'missing',
...extra,
});

export const appendE2EDiagnosticsToUrl = (url, diagnostics) => {
const nextUrl = new URL(url);
Object.entries(diagnostics).forEach(([key, value]) => {
nextUrl.searchParams.set(`e2e_${key}`, formatE2EDiagnosticValue(value));
});
return nextUrl.toString();
};

export const isRc = process.env.METAMASK_ENVIRONMENT === 'rc';
45 changes: 45 additions & 0 deletions shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import {
} from 'react-native-quick-crypto';
import { LaunchArguments } from 'react-native-launch-arguments';
import {
E2E_DIAGNOSTICS_ENDPOINT,
FALLBACK_FIXTURE_SERVER_PORT,
FALLBACK_COMMAND_QUEUE_SERVER_PORT,
getE2ETestConfigDiagnostics,
isE2E,
isTest,
enableApiCallLogs,
Expand All @@ -45,6 +47,16 @@ import { defaultMockPort } from './tests/api-mocking/mock-config/mockUrlCollecti

import './shimPerf';

const getLaunchArgumentKeys = (raw) =>
raw && typeof raw === 'object' ? Object.keys(raw).sort() : [];

const recordLaunchArgumentDiagnostics = (raw) => {
testConfig.launchArgumentKeys = getLaunchArgumentKeys(raw);
testConfig.rawFixtureServerPort = raw?.fixtureServerPort ?? null;
testConfig.rawCommandQueueServerPort = raw?.commandQueueServerPort ?? null;
testConfig.rawMockServerPort = raw?.mockServerPort ?? null;
};

// Needed to polyfill random number generation
import 'react-native-get-random-values';

Expand Down Expand Up @@ -108,12 +120,14 @@ if (isE2E) {
// See FixtureHelper.ts for the port mapping implementation.
if (isTest) {
const raw = LaunchArguments.value();
recordLaunchArgumentDiagnostics(raw);
testConfig.fixtureServerPort = raw?.fixtureServerPort
? raw.fixtureServerPort
: FALLBACK_FIXTURE_SERVER_PORT;
testConfig.commandQueueServerPort = raw?.commandQueueServerPort
? raw.commandQueueServerPort
: FALLBACK_COMMAND_QUEUE_SERVER_PORT;
testConfig.mockServerPort = raw?.mockServerPort ?? defaultMockPort;
}

// Fix for https://github.com/facebook/react-native/issues/5667
Expand Down Expand Up @@ -269,7 +283,9 @@ if (typeof localStorage !== 'undefined') {
if (enableApiCallLogs || isTest) {
(async () => {
const raw = LaunchArguments.value();
recordLaunchArgumentDiagnostics(raw);
const mockServerPort = raw?.mockServerPort ?? defaultMockPort;
testConfig.mockServerPort = mockServerPort;
const { fetch: originalFetch } = global;

// eslint-disable-next-line no-console
Expand Down Expand Up @@ -315,6 +331,35 @@ if (enableApiCallLogs || isTest) {
}
}

const postE2EDiagnostics = async (phase) => {
if (!isMockServerAvailable || !MOCKTTP_URL) {
return;
}

const diagnostics = getE2ETestConfigDiagnostics({
source: 'shim',
phase,
platform: Platform.OS,
mockServerUrl: MOCKTTP_URL,
mockServerAvailable: String(isMockServerAvailable),
launchArgFixtureServerPort: raw?.fixtureServerPort ?? 'missing',
launchArgCommandQueueServerPort:
raw?.commandQueueServerPort ?? 'missing',
launchArgMockServerPort: raw?.mockServerPort ?? 'missing',
});

await originalFetch(`${MOCKTTP_URL}${E2E_DIAGNOSTICS_ENDPOINT}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(diagnostics),
}).catch((error) => {
// eslint-disable-next-line no-console
console.warn(`[E2E SHIM] Failed to post diagnostics: ${error.message}`);
});
};

await postE2EDiagnostics('mock-server-connected');

// if mockServer is off we route to original destination
global.fetch = async (url, options) => {
// Extract URL string from Request or URL objects
Expand Down
70 changes: 70 additions & 0 deletions tests/api-mocking/MockServerE2E.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,45 @@ interface LiveRequest {
timestamp: string;
}

interface E2EDiagnostic {
timestamp: string;
payload: unknown;
}

export interface InternalMockServer extends Mockttp {
_liveRequests?: LiveRequest[];
_e2eDiagnostics?: E2EDiagnostic[];
}

const MAX_E2E_DIAGNOSTICS = 20;
const MAX_E2E_DIAGNOSTICS_IN_ERROR = 3;

const stringifyE2EDiagnostic = (payload: unknown): string => {
try {
return JSON.stringify(payload, null, 2);
} catch {
return String(payload);
}
};

const formatE2EDiagnostics = (diagnostics?: E2EDiagnostic[]): string => {
if (!diagnostics || diagnostics.length === 0) {
return 'E2E app diagnostics: none received.';
}

const latestDiagnostics = diagnostics.slice(-MAX_E2E_DIAGNOSTICS_IN_ERROR);
const diagnosticsSummary = latestDiagnostics
.map(
(diagnostic, index) =>
`${index + 1}. ${diagnostic.timestamp}\n${stringifyE2EDiagnostic(
diagnostic.payload,
)}`,
)
.join('\n');

return `E2E app diagnostics (latest ${latestDiagnostics.length}/${diagnostics.length}):\n${diagnosticsSummary}`;
};

/**
* Translates fallback ports to actual allocated ports in URLs.
* This allows the MockServer (running on host) to forward requests to dynamically allocated local resources.
Expand Down Expand Up @@ -278,6 +313,7 @@ export default class MockServerE2E implements Resource {

this._server = getLocal() as InternalMockServer;
this._server._liveRequests = [];
this._server._e2eDiagnostics = [];
await this._server.start(this._serverPort);

logger.info(
Expand All @@ -287,6 +323,38 @@ export default class MockServerE2E implements Resource {
await this._server
.forGet('/health-check')
.thenReply(200, 'Mock server is running');
await this._server
.forPost('/e2e-diagnostics')
.thenCallback(async (request) => {
const bodyText = await safeGetBodyText(request);
let payload: unknown = bodyText;

if (bodyText) {
try {
payload = JSON.parse(bodyText);
} catch {
payload = bodyText;
}
}

const diagnostic = {
timestamp: new Date().toISOString(),
payload,
};
this._server?._e2eDiagnostics?.push(diagnostic);

if (
this._server?._e2eDiagnostics &&
this._server._e2eDiagnostics.length > MAX_E2E_DIAGNOSTICS
) {
this._server._e2eDiagnostics.shift();
}

logger.debug(
`Received E2E app diagnostics: ${stringifyE2EDiagnostic(payload)}`,
);
return { statusCode: 204, body: '' };
});
await this._server
.forGet(
/^http:\/\/(localhost|127\.0\.0\.1|10\.0\.2\.2)(:\\d+)?\/favicon\.ico$/,
Expand Down Expand Up @@ -854,8 +922,10 @@ export default class MockServerE2E implements Resource {

const totalCount = mockServer._liveRequests.length;
const uniqueCount = uniqueRequests.length;
const diagnosticsSummary = formatE2EDiagnostics(mockServer._e2eDiagnostics);
const message =
`Test made ${totalCount} unmocked request(s) (${uniqueCount} unique):\n${requestsSummary}\n\n` +
`${diagnosticsSummary}\n\n` +
"Check your test-specific mocks or add them to the default mocks.\n You can also add the URL to the allowlist if it's a known live request.";
logger.error(message);
throw new Error(message);
Expand Down
Loading