Skip to content

Commit adcef54

Browse files
test(e2e): add device-level proxy support in fixtures
Co-authored-by: javiergarciavera <javiergarciavera@users.noreply.github.com>
1 parent 1fe4671 commit adcef54

File tree

3 files changed

+248
-0
lines changed

3 files changed

+248
-0
lines changed

tests/framework/fixtures/FixtureHelper.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
startResourceWithRetry,
1515
startMultiInstanceResourceWithRetry,
1616
cleanupAllAndroidPortForwarding,
17+
enableDeviceTrafficProxyForMockServer,
18+
disableDeviceTrafficProxy,
1719
} from './FixtureUtils';
1820
import Utilities from '../Utilities';
1921
import { dismissDevScreens } from '../../flows/general.flow';
@@ -488,6 +490,7 @@ export async function withFixtures(
488490
const {
489491
fixture: fixtureOption,
490492
restartDevice = false,
493+
enableDeviceNetworkProxy,
491494
smartContracts,
492495
disableLocalNodes = false,
493496
dapps,
@@ -507,6 +510,8 @@ export async function withFixtures(
507510
skipReactNativeReload = false,
508511
useCommandQueueServer = false,
509512
} = options;
513+
const shouldEnableDeviceNetworkProxy =
514+
enableDeviceNetworkProxy ?? process.env.METAMASK_BUILD_TYPE === 'flask';
510515

511516
// Clean up any stale port forwarding from previous failed tests
512517
// This ensures we start with a clean slate on Android
@@ -573,6 +578,11 @@ export async function withFixtures(
573578
const mockServerResult = await createMockAPIServer(testSpecificMock);
574579
mockServerInstance = mockServerResult.mockServerInstance;
575580
mockServerPort = mockServerResult.mockServerPort;
581+
582+
if (shouldEnableDeviceNetworkProxy) {
583+
await enableDeviceTrafficProxyForMockServer(mockServerPort);
584+
}
585+
576586
// Resolve fixture after local nodes are started so dynamic ports are known
577587
let resolvedFixture: FixtureBuilder | Fixture;
578588
if (typeof fixtureOption === 'function') {
@@ -744,6 +754,15 @@ export async function withFixtures(
744754
}
745755
}
746756

757+
if (shouldEnableDeviceNetworkProxy) {
758+
try {
759+
await disableDeviceTrafficProxy();
760+
} catch (cleanupError) {
761+
logger.error('Error disabling device network proxy:', cleanupError);
762+
cleanupErrors.push(cleanupError as Error);
763+
}
764+
}
765+
747766
// Remove the abort filter AFTER all cleanup is complete so late async
748767
// "Aborted" rejections from destroyed sockets are still caught.
749768
if (mockServerInstance) {

tests/framework/fixtures/FixtureUtils.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ const logger = createLogger({
2828
name: 'FixtureUtils',
2929
});
3030

31+
let androidDeviceProxyConfigured = false;
32+
let iosDeviceProxyConfigured = false;
33+
let iosProxyConfiguredViaNetworkSetup = false;
34+
let iosNetworkServicesWithProxy: string[] = [];
35+
3136
/**
3237
* Maps ResourceType to fallback port.
3338
* These ports are used in fixture data and are mapped to actual allocated ports
@@ -115,6 +120,228 @@ export async function cleanupAllAndroidPortForwarding(): Promise<void> {
115120
logger.debug('✓ Cleaned up test port forwarding');
116121
}
117122

123+
/**
124+
* Enables emulator/simulator HTTP(S) proxy so device-level traffic is captured by MockServer.
125+
* This is especially useful for network calls that don't go through the app JS bridge
126+
* (e.g., requests made inside WebViews).
127+
*
128+
* - Android: Uses global proxy settings pointing to fallback MockServer port (8000),
129+
* which is already mapped to the allocated host port via adb reverse.
130+
* - iOS: Tries simulator-local proxy config first (simctl + defaults). If unavailable,
131+
* falls back to host networkservice proxy configuration via networksetup.
132+
*/
133+
export async function enableDeviceTrafficProxyForMockServer(
134+
mockServerPort: number,
135+
): Promise<void> {
136+
if (isBrowserStack()) {
137+
logger.info(
138+
'BrowserStack mode: skipping device-level proxy configuration for MockServer',
139+
);
140+
return;
141+
}
142+
143+
if (await PlatformDetector.isAndroid()) {
144+
let deviceFlag = '';
145+
if (FrameworkDetector.isDetox()) {
146+
const deviceId = device.id || '';
147+
deviceFlag = deviceId ? `-s ${deviceId}` : '';
148+
}
149+
150+
const proxyHost = '127.0.0.1';
151+
const proxyPort = FALLBACK_MOCKSERVER_PORT;
152+
const exclusionList = 'localhost,127.0.0.1,10.0.2.2';
153+
154+
await execAsync(
155+
`adb ${deviceFlag} shell settings put global http_proxy ${proxyHost}:${proxyPort}`,
156+
);
157+
await execAsync(
158+
`adb ${deviceFlag} shell settings put global global_http_proxy_host ${proxyHost}`,
159+
);
160+
await execAsync(
161+
`adb ${deviceFlag} shell settings put global global_http_proxy_port ${proxyPort}`,
162+
);
163+
await execAsync(
164+
`adb ${deviceFlag} shell settings put global global_http_proxy_exclusion_list ${exclusionList}`,
165+
);
166+
167+
androidDeviceProxyConfigured = true;
168+
logger.info(
169+
`Enabled Android device proxy ${proxyHost}:${proxyPort} (MockServer allocated port: ${mockServerPort})`,
170+
);
171+
return;
172+
}
173+
174+
if (!(await PlatformDetector.isIOS())) {
175+
return;
176+
}
177+
178+
if (process.platform !== 'darwin') {
179+
logger.warn(
180+
'Skipping iOS simulator proxy setup because host platform is not macOS',
181+
);
182+
return;
183+
}
184+
185+
const proxyHost = '127.0.0.1';
186+
187+
// Prefer simulator-local proxy settings first to avoid mutating host network services.
188+
try {
189+
const targetSimulator =
190+
FrameworkDetector.isDetox() && device?.id ? device.id : 'booted';
191+
192+
await execAsync(
193+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPEnable -int 1`,
194+
);
195+
await execAsync(
196+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPProxy -string ${proxyHost}`,
197+
);
198+
await execAsync(
199+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPPort -int ${mockServerPort}`,
200+
);
201+
await execAsync(
202+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPSEnable -int 1`,
203+
);
204+
await execAsync(
205+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPSProxy -string ${proxyHost}`,
206+
);
207+
await execAsync(
208+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPSPort -int ${mockServerPort}`,
209+
);
210+
211+
iosDeviceProxyConfigured = true;
212+
iosProxyConfiguredViaNetworkSetup = false;
213+
logger.info(
214+
`Enabled iOS simulator proxy via simctl defaults (${proxyHost}:${mockServerPort})`,
215+
);
216+
return;
217+
} catch (simctlError) {
218+
logger.warn(
219+
'Failed to enable iOS simulator proxy via simctl defaults, trying networksetup fallback',
220+
simctlError,
221+
);
222+
}
223+
224+
// Fallback: mutate host network service proxy settings.
225+
const { stdout } = await execAsync('networksetup -listallnetworkservices');
226+
const services = stdout
227+
.split('\n')
228+
.map((line) => line.trim())
229+
.filter(
230+
(line) =>
231+
line &&
232+
!line.startsWith('An asterisk') &&
233+
!line.startsWith('*'),
234+
);
235+
236+
const configuredServices: string[] = [];
237+
for (const service of services) {
238+
try {
239+
await execAsync(
240+
`networksetup -setwebproxy "${service}" ${proxyHost} ${mockServerPort}`,
241+
);
242+
await execAsync(
243+
`networksetup -setsecurewebproxy "${service}" ${proxyHost} ${mockServerPort}`,
244+
);
245+
await execAsync(`networksetup -setwebproxystate "${service}" on`);
246+
await execAsync(`networksetup -setsecurewebproxystate "${service}" on`);
247+
await execAsync(
248+
`networksetup -setproxybypassdomains "${service}" localhost 127.0.0.1 ::1`,
249+
);
250+
configuredServices.push(service);
251+
} catch (serviceError) {
252+
logger.warn(
253+
`Failed enabling proxy on macOS network service "${service}"`,
254+
serviceError,
255+
);
256+
}
257+
}
258+
259+
if (configuredServices.length === 0) {
260+
throw new Error(
261+
'Could not enable iOS simulator proxy: no network service accepted proxy configuration',
262+
);
263+
}
264+
265+
iosDeviceProxyConfigured = true;
266+
iosProxyConfiguredViaNetworkSetup = true;
267+
iosNetworkServicesWithProxy = configuredServices;
268+
logger.info(
269+
`Enabled iOS simulator proxy via networksetup on ${configuredServices.length} service(s): ${configuredServices.join(', ')}`,
270+
);
271+
}
272+
273+
/**
274+
* Disables previously configured emulator/simulator proxy settings.
275+
* Safe to call multiple times.
276+
*/
277+
export async function disableDeviceTrafficProxy(): Promise<void> {
278+
if (isBrowserStack()) {
279+
return;
280+
}
281+
282+
if ((await PlatformDetector.isAndroid()) && androidDeviceProxyConfigured) {
283+
let deviceFlag = '';
284+
if (FrameworkDetector.isDetox()) {
285+
const deviceId = device.id || '';
286+
deviceFlag = deviceId ? `-s ${deviceId}` : '';
287+
}
288+
289+
await execAsync(`adb ${deviceFlag} shell settings put global http_proxy :0`);
290+
await execAsync(
291+
`adb ${deviceFlag} shell settings delete global global_http_proxy_host`,
292+
);
293+
await execAsync(
294+
`adb ${deviceFlag} shell settings delete global global_http_proxy_port`,
295+
);
296+
await execAsync(
297+
`adb ${deviceFlag} shell settings delete global global_http_proxy_exclusion_list`,
298+
);
299+
300+
androidDeviceProxyConfigured = false;
301+
logger.info('Disabled Android device proxy settings');
302+
return;
303+
}
304+
305+
if (!(await PlatformDetector.isIOS()) || !iosDeviceProxyConfigured) {
306+
return;
307+
}
308+
309+
if (process.platform !== 'darwin') {
310+
iosDeviceProxyConfigured = false;
311+
iosProxyConfiguredViaNetworkSetup = false;
312+
iosNetworkServicesWithProxy = [];
313+
return;
314+
}
315+
316+
if (iosProxyConfiguredViaNetworkSetup) {
317+
for (const service of iosNetworkServicesWithProxy) {
318+
try {
319+
await execAsync(`networksetup -setwebproxystate "${service}" off`);
320+
await execAsync(`networksetup -setsecurewebproxystate "${service}" off`);
321+
} catch (serviceError) {
322+
logger.warn(
323+
`Failed disabling proxy on macOS network service "${service}"`,
324+
serviceError,
325+
);
326+
}
327+
}
328+
} else {
329+
const targetSimulator =
330+
FrameworkDetector.isDetox() && device?.id ? device.id : 'booted';
331+
await execAsync(
332+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPEnable -int 0`,
333+
);
334+
await execAsync(
335+
`xcrun simctl spawn ${targetSimulator} defaults write com.apple.CFNetwork ProxiesHTTPSEnable -int 0`,
336+
);
337+
}
338+
339+
iosDeviceProxyConfigured = false;
340+
iosProxyConfiguredViaNetworkSetup = false;
341+
iosNetworkServicesWithProxy = [];
342+
logger.info('Disabled iOS simulator proxy settings');
343+
}
344+
118345
/**
119346
* Sets up adb reverse for Android to map fallback port to actual allocated port.
120347
*

tests/framework/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ export type TestSpecificMock = (mockServer: Mockttp) => Promise<void>;
315315
* The options for the withFixtures function.
316316
* @param {FixtureBuilder | ((ctx: { localNodes?: LocalNode[] }) => FixtureBuilder | Promise<FixtureBuilder>)} fixture - The state of the fixture to load or a function that returns a fixture builder.
317317
* @param {boolean} [restartDevice=false] - If true, restarts the app to apply the loaded fixture.
318+
* @param {boolean} [enableDeviceNetworkProxy] - If true, configures emulator/simulator HTTP(S) proxy to route device-level traffic through MockServer.
318319
* @param {string[]} [smartContracts] - The smart contracts to load for test. These will be deployed on the different {localNodeOptions}
319320
* @param {LocalNodeOptionsInput} [localNodeOptions] - The local node options to use for the test.
320321
* @param {boolean} [disableLocalNodes=false] - If true, disables the local nodes.
@@ -333,6 +334,7 @@ export interface WithFixturesOptions {
333334
localNodes?: LocalNode[];
334335
}) => FixtureBuilder | Fixture | Promise<FixtureBuilder | Fixture>);
335336
restartDevice?: boolean;
337+
enableDeviceNetworkProxy?: boolean;
336338
smartContracts?: string[];
337339
disableLocalNodes?: boolean;
338340
dapps?: DappOptions[];

0 commit comments

Comments
 (0)