Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 11 additions & 10 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,17 @@ app/scripts/lib/smart-transaction @MetaMask/transactions
shared/lib/multichain/addresses/* @MetaMask/new-networks

# QA Team - E2E Framework
test/e2e/page-objects/ @MetaMask/qa
test/e2e/seeder/ @MetaMask/qa
test/e2e/default-fixture.js @MetaMask/qa
test/e2e/fixture-builder.js @MetaMask/qa
test/e2e/fixture-server.js @MetaMask/qa
test/e2e/helpers.js @MetaMask/qa
test/e2e/mock-e2e-allowlist.js @MetaMask/qa
test/e2e/mock-e2e.js @MetaMask/qa
test/e2e/tests/settings/state-logs-helpers.ts @MetaMask/qa
test/e2e/tests/settings/state-logs.json @MetaMask/qa @MetaMask/extension-privacy-reviewers
test/e2e/page-objects/ @MetaMask/qa
test/e2e/seeder/ @MetaMask/qa
test/e2e/default-fixture.js @MetaMask/qa
test/e2e/fixture-builder.js @MetaMask/qa
test/e2e/fixture-server.js @MetaMask/qa
test/e2e/helpers.js @MetaMask/qa
test/e2e/mock-e2e-allowlist.js @MetaMask/qa
test/e2e/mock-e2e.js @MetaMask/qa
test/e2e/tests/settings/state-logs-helpers.ts @MetaMask/qa
test/e2e/tests/settings/state-logs.json @MetaMask/qa @MetaMask/extension-privacy-reviewers
test/e2e/tests/privacy/privacy-max-allowlist-onboarding.json @MetaMask/qa @MetaMask/extension-privacy-reviewers

# Wallet Integrations
app/scripts/lib/rpc-method-middleware @MetaMask/wallet-integrations
Expand Down
278 changes: 278 additions & 0 deletions test/e2e/tests/privacy/onboarding-maximum-privacy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/**
* This test verifies that with maximum privacy settings during onboarding enabled,
* only allowlisted network calls are made during the onboarding process.
*
* It checks two critical phases during onboarding:
* 1. During onboarding (before clicking "Done" on complete onboarding screen)
* 2. After completing onboarding (on homepage without any user action)
*
* The allowlist is defined in privacy-max-allowlist-onboarding.json
*/
import { strict as assert } from 'assert';
import { promises as fs } from 'fs';
import path from 'path';
import { Mockttp, MockedEndpoint } from 'mockttp';
import { withFixtures, veryLargeDelayMs } from '../../helpers';
import { FEATURE_FLAGS_API_MOCK_RESULT } from '../../../data/mock-data';
import FixtureBuilder from '../../fixtures/fixture-builder';
import HomePage from '../../page-objects/pages/home/homepage';
import OnboardingCompletePage from '../../page-objects/pages/onboarding/onboarding-complete-page';
import OnboardingPrivacySettingsPage from '../../page-objects/pages/onboarding/onboarding-privacy-settings-page';
import {
importSRPOnboardingFlow,
createNewWalletOnboardingFlow,
handleSidepanelPostOnboarding,
} from '../../page-objects/flows/onboarding.flow';

const ALLOWLIST_FILE_NAME = 'privacy-max-allowlist-onboarding.json';

async function mockApis(mockServer: Mockttp): Promise<MockedEndpoint[]> {
return [
// Mock Infura RPC endpoints
await mockServer
.forPost(/https:\/\/.*\.infura\.io\/v3\/.*/u)
.thenCallback(() => ({
statusCode: 200,
json: {
jsonrpc: '2.0',
id: '1',
result: '0x1',
},
})),
await mockServer
.forGet('https://bridge.api.cx.metamask.io/featureFlags')
.thenCallback(() => ({
statusCode: 200,
json: FEATURE_FLAGS_API_MOCK_RESULT,
})),
];
}

type OnboardingAllowlist = {
duringOnboarding: string[];
untilOnboardingComplete: string[];
};

async function loadAllowlist(): Promise<OnboardingAllowlist> {
const allowlistPath = path.join(__dirname, ALLOWLIST_FILE_NAME);
const allowlistRaw = await fs.readFile(allowlistPath, 'utf8');
const allowlist: OnboardingAllowlist = JSON.parse(allowlistRaw);
console.log('Privacy maximum onboarding allowlist loaded');
return allowlist;
}

describe('Onboarding with Maximum Privacy Settings', function () {
it('should only make allowlisted network calls during and after import wallet onboarding', async function () {
const allowlist = await loadAllowlist();
const capturedCalls = new Set<string>();

await withFixtures(
{
fixtures: new FixtureBuilder({ onboarding: true }).build(),
title: this.test?.fullTitle(),
testSpecificMock: mockApis,
},
async ({ driver, mockServer }) => {
// Listen to all network requests and capture them
mockServer.on(
'request-initiated',
(request: { headers: { host: string }; url: string }) => {
const { host } = request.headers;
if (host) {
capturedCalls.add(host);
}
},
);

// Complete onboarding up to the complete page
await importSRPOnboardingFlow({ driver });

const onboardingCompletePage = new OnboardingCompletePage(driver);
await onboardingCompletePage.checkPageIsLoaded();
await onboardingCompletePage.checkWalletReadyMessageIsDisplayed();

// Navigate to privacy settings and toggle them OFF (maximum privacy)
await onboardingCompletePage.navigateToDefaultPrivacySettings();

const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage(
driver,
);
await onboardingPrivacySettingsPage.checkPageIsLoaded();
await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings();
await onboardingPrivacySettingsPage.toggleAssetsSettings();
await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage();

await onboardingCompletePage.checkPageIsLoaded();

// Intended delay to ensure we cover at least 1 polling loop for network requests
await driver.delay(veryLargeDelayMs);

// Check Phase 1: All calls until now should be in duringOnboarding allowlist
const unexpectedCallsDuringOnboarding = [...capturedCalls].filter(
(host) => !allowlist.duringOnboarding.includes(host),
);

console.log('Checking calls until complete onboarding screen');
console.log('Total calls made during onboarding:', capturedCalls.size);
console.log(
'Calls made during onboarding:',
JSON.stringify([...capturedCalls].sort(), null, 2),
);

// Assert no unexpected hosts during onboarding
assert.equal(
unexpectedCallsDuringOnboarding.length,
0,
`Unexpected network calls during onboarding:\n${unexpectedCallsDuringOnboarding
.map((host) => ` - ${host}`)
.join(
'\n',
)}\n\nThese hosts are NOT in the duringOnboarding allowlist.\nIf these are expected, add them to privacy-max-allowlist-onboarding.json`,
);

// Complete onboarding and go to homepage
await onboardingCompletePage.completeOnboarding();
await handleSidepanelPostOnboarding(driver);

const homePage = new HomePage(driver);
await homePage.checkPageIsLoaded();

// Intended delay to ensure we cover at least 1 polling loop for network requests
await driver.delay(veryLargeDelayMs);

// Check Phase 2: All calls (including previous ones) should be in untilOnboardingComplete allowlist
const unexpectedCallsAfterOnboardingComplete = [
...capturedCalls,
].filter((host) => !allowlist.untilOnboardingComplete.includes(host));

console.log('Checking calls until homepage');
console.log(
'Total calls made until landing on homepage:',
capturedCalls.size,
);
console.log(
'Calls made until landing on homepage:',
JSON.stringify([...capturedCalls].sort(), null, 2),
);

// Assert no unexpected hosts after onboarding complete
assert.equal(
unexpectedCallsAfterOnboardingComplete.length,
0,
`Unexpected network calls after onboarding complete:\n${unexpectedCallsAfterOnboardingComplete
.map((host) => ` - ${host}`)
.join(
'\n',
)}\n\nThese hosts are NOT in the untilOnboardingComplete allowlist.\nIf these are expected, add them to privacy-max-allowlist-onboarding.json`,
);
},
);
});

it('should only make allowlisted network calls during and after create new wallet onboarding', async function () {
const allowlist = await loadAllowlist();
const capturedCalls = new Set<string>();

await withFixtures(
{
fixtures: new FixtureBuilder({ onboarding: true }).build(),
title: this.test?.fullTitle(),
testSpecificMock: mockApis,
},
async ({ driver, mockServer }) => {
// Listen to all network requests and capture them
mockServer.on(
'request-initiated',
(request: { headers: { host: string }; url: string }) => {
const { host } = request.headers;
if (host) {
capturedCalls.add(host);
}
},
);

// Complete onboarding up to the complete page
await createNewWalletOnboardingFlow({ driver });

const onboardingCompletePage = new OnboardingCompletePage(driver);
await onboardingCompletePage.checkPageIsLoaded();
await onboardingCompletePage.checkWalletReadyMessageIsDisplayed();

// Navigate to privacy settings and toggle them OFF (maximum privacy)
await onboardingCompletePage.navigateToDefaultPrivacySettings();

const onboardingPrivacySettingsPage = new OnboardingPrivacySettingsPage(
driver,
);
await onboardingPrivacySettingsPage.checkPageIsLoaded();
await onboardingPrivacySettingsPage.toggleBasicFunctionalitySettings();
await onboardingPrivacySettingsPage.toggleAssetsSettings();
await onboardingPrivacySettingsPage.navigateBackToOnboardingCompletePage();

await onboardingCompletePage.checkPageIsLoaded();

// Intended delay to ensure we cover at least 1 polling loop for network requests
await driver.delay(veryLargeDelayMs);

// Check Phase 1: All calls until now should be in duringOnboarding allowlist
const unexpectedCallsDuringOnboarding = [...capturedCalls].filter(
(host) => !allowlist.duringOnboarding.includes(host),
);

console.log('Checking calls until complete onboarding screen');
console.log('Total calls made during onboarding:', capturedCalls.size);
console.log(
'Calls made during onboarding:',
JSON.stringify([...capturedCalls].sort(), null, 2),
);

// Assert no unexpected hosts during onboarding
assert.equal(
unexpectedCallsDuringOnboarding.length,
0,
`Unexpected network calls during onboarding:\n${unexpectedCallsDuringOnboarding
.map((host) => ` - ${host}`)
.join(
'\n',
)}\n\nThese hosts are NOT in the duringOnboarding allowlist.\nIf these are expected, add them to privacy-max-allowlist-onboarding.json`,
);

// Complete onboarding and go to homepage
await onboardingCompletePage.completeOnboarding();
await handleSidepanelPostOnboarding(driver);

const homePage = new HomePage(driver);
await homePage.checkPageIsLoaded();

// Intended delay to ensure we cover at least 1 polling loop for network requests
await driver.delay(veryLargeDelayMs);

// Check Phase 2: All calls (including previous ones) should be in untilOnboardingComplete allowlist
const unexpectedCallsAfterOnboardingComplete = [
...capturedCalls,
].filter((host) => !allowlist.untilOnboardingComplete.includes(host));

console.log('Checking calls until homepage');
console.log(
'Total calls made until landing on homepage:',
capturedCalls.size,
);
console.log(
'Calls made until landing on homepage:',
JSON.stringify([...capturedCalls].sort(), null, 2),
);

// Assert no unexpected hosts after onboarding complete
assert.equal(
unexpectedCallsAfterOnboardingComplete.length,
0,
`Unexpected network calls after onboarding complete:\n${unexpectedCallsAfterOnboardingComplete
.map((host) => ` - ${host}`)
.join(
'\n',
)}\n\nThese hosts are NOT in the untilOnboardingComplete allowlist.\nIf these are expected, add them to privacy-max-allowlist-onboarding.json`,
);
},
);
});
});
27 changes: 27 additions & 0 deletions test/e2e/tests/privacy/privacy-max-allowlist-onboarding.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"duringOnboarding": [
"accounts.google.com",
"firefox.settings.services.mozilla.com",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

firefox browser-level call, only exists on firefox, not initiated by extension

"metamask.github.io",
"sentry.io"
],
"toBeRemoved": {
Copy link
Contributor Author

@chloeYue chloeYue Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this toBeRemoved list is a reminder, we need to remove the 2 hosts from allowlist once they are addressed

"accounts.google.com": "Should be removed - web3auth team - will only call during social login",
"bridge.api.cx.metamask.io": "Should be removed - STX team - when move to remote feature flag"
},
"untilOnboardingComplete": [
"accounts.google.com",
"arbitrum-mainnet.infura.io",
"base-mainnet.infura.io",
"bridge.api.cx.metamask.io",
"bsc-mainnet.infura.io",
"firefox.settings.services.mozilla.com",
"linea-mainnet.infura.io",
"mainnet.infura.io",
"metamask.github.io",
"optimism-mainnet.infura.io",
"polygon-mainnet.infura.io",
"sei-mainnet.infura.io",
"sentry.io"
]
}
Loading