Fix add-email preserve identity email state flow

This commit is contained in:
QLHazyCoder
2026-05-12 03:49:22 +08:00
parent f81cc74c29
commit c3423b21ab
12 changed files with 670 additions and 26 deletions
+129 -1
View File
@@ -212,6 +212,71 @@ test('generated email helper prefers current UI email over preserved runtime bas
});
});
test('generated email helper preserves phone identity through the shared persistence helper during Duck add-email generation', async () => {
const api = loadGeneratedEmailHelpersApi();
const persistCalls = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build alias');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getRegistrationEmailBaseline: () => '',
getState: async () => ({}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate 2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
persistRegistrationEmailState: async (state, email, options) => {
persistCalls.push({ state, email, options });
},
reuseOrCreateTab: async () => {},
sendToContentScript: async () => ({ email: 'fresh@duck.com', generated: true }),
setEmailState: async () => {
throw new Error('preserveAccountIdentity should use shared persistence helper');
},
throwIfStopped: () => {},
});
const state = {
email: '',
emailGenerator: 'duck',
mailProvider: 'gmail',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
signupPhoneCompletedActivation: {
activationId: 'done-1',
phoneNumber: '+447780579093',
},
};
const email = await helpers.fetchGeneratedEmail(state, {
generator: 'duck',
preserveAccountIdentity: true,
});
assert.equal(email, 'fresh@duck.com');
assert.equal(persistCalls.length, 1);
assert.equal(persistCalls[0].email, 'fresh@duck.com');
assert.equal(persistCalls[0].options.source, 'generated:duck');
assert.equal(persistCalls[0].options.preserveAccountIdentity, true);
assert.equal(persistCalls[0].state.accountIdentifierType, 'phone');
assert.equal(persistCalls[0].state.signupPhoneNumber, '+447780579093');
});
test('generated email helper can read the requested address from custom email pool', async () => {
const api = loadGeneratedEmailHelpersApi();
const events = [];
@@ -545,5 +610,68 @@ test('generated email helper honors iCloud always-new fetch mode', async () => {
});
assert.equal(email, 'fresh@icloud.example.com');
assert.deepEqual(icloudOptions, [{ generateNew: true }]);
assert.equal(icloudOptions.length, 1);
assert.equal(icloudOptions[0].generateNew, true);
assert.equal(icloudOptions[0].preserveAccountIdentity, false);
assert.equal(icloudOptions[0].source, 'generated:icloud');
assert.equal(icloudOptions[0].state.emailGenerator, 'icloud');
});
test('generated email helper forwards preserve identity context to the iCloud generator', async () => {
const api = loadGeneratedEmailHelpersApi();
const icloudOptions = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async (options) => {
icloudOptions.push(options);
return 'fresh@icloud.example.com';
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getState: async () => ({}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async () => {},
throwIfStopped: () => {},
});
const state = {
emailGenerator: 'icloud',
icloudFetchMode: 'always_new',
mailProvider: 'gmail',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
};
const email = await helpers.fetchGeneratedEmail(state, {
generator: 'icloud',
preserveAccountIdentity: true,
});
assert.equal(email, 'fresh@icloud.example.com');
assert.equal(icloudOptions.length, 1);
assert.equal(icloudOptions[0].generateNew, true);
assert.equal(icloudOptions[0].preserveAccountIdentity, true);
assert.equal(icloudOptions[0].source, 'generated:icloud');
assert.equal(icloudOptions[0].state.accountIdentifierType, 'phone');
assert.equal(icloudOptions[0].state.signupPhoneNumber, '+447780579093');
assert.equal(icloudOptions[0].state.emailGenerator, 'icloud');
});
+136
View File
@@ -0,0 +1,136 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function extractFunction(name) {
const source = fs.readFileSync('background.js', 'utf8');
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`Unable to find function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let i = start; i < source.length; i += 1) {
const char = source[i];
if (char === '(') {
parenDepth += 1;
} else if (char === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (char === '{' && signatureEnded) {
braceStart = i;
break;
}
}
if (braceStart < 0) {
throw new Error(`Unable to extract function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const char = source[end];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(start, end + 1);
}
}
}
throw new Error(`Unable to extract function ${name}`);
}
test('fetchIcloudHideMyEmail uses shared persistence helper for preserve-account-identity reuse flow', async () => {
const bundle = extractFunction('fetchIcloudHideMyEmail');
const api = new Function(`
const persistCalls = [];
const broadcasts = [];
const ICLOUD_REQUEST_TIMEOUT_MS = 15000;
const ICLOUD_WRITE_MAX_ATTEMPTS = 2;
function withIcloudLoginHelp(_label, action) {
return action();
}
function throwIfStopped() {}
async function addLog() {}
async function resolveIcloudPremiumMailService() {
return {
serviceUrl: 'https://p67-maildomainws.icloud.com',
setupUrl: 'https://setup.icloud.com/setup/ws/1',
};
}
function getErrorMessage(error) {
return String(typeof error === 'string' ? error : error?.message || '');
}
async function listIcloudAliases() {
return [
{ email: 'reuse@icloud.com', active: true, used: false, preserved: false },
];
}
function pickReusableIcloudAlias(aliases) {
return aliases[0] || null;
}
async function icloudRequest() {
throw new Error('should not create a new alias');
}
function getIcloudAliasLabel() {
return 'MultiPage 2026-04-26';
}
async function persistRegistrationEmailState(state, email, options) {
persistCalls.push({ state, email, options });
}
async function setEmailState() {
throw new Error('preserve flow should use shared persistence helper');
}
function broadcastIcloudAliasesChanged(payload) {
broadcasts.push(payload);
}
function findIcloudAliasByEmail() {
return null;
}
function shouldStopIcloudAutoFetchRetries() {
return true;
}
${bundle}
return {
fetchIcloudHideMyEmail,
readPersistCalls: () => persistCalls,
readBroadcasts: () => broadcasts,
};
`)();
const state = {
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
};
const email = await api.fetchIcloudHideMyEmail({
generateNew: false,
preserveAccountIdentity: true,
source: 'generated:icloud',
state,
});
assert.equal(email, 'reuse@icloud.com');
assert.deepStrictEqual(api.readPersistCalls(), [
{
state,
email: 'reuse@icloud.com',
options: {
source: 'generated:icloud',
preserveAccountIdentity: true,
},
},
]);
assert.deepStrictEqual(api.readBroadcasts(), [{ reason: 'selected', email: 'reuse@icloud.com' }]);
});
@@ -56,3 +56,80 @@ test('registration email baseline prefers the current UI email over preserved ru
assert.equal(baseline, 'visible@duck.com');
});
test('flow registration email state can preserve phone identity while updating the runtime email', () => {
const api = loadRegistrationEmailStateApi();
const helpers = api.createRegistrationEmailStateHelpers();
const updates = helpers.buildFlowRegistrationEmailStateUpdates({
email: '',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
signupPhoneActivation: {
activationId: 'active-1',
phoneNumber: '+447780579093',
},
signupPhoneCompletedActivation: {
activationId: 'done-1',
phoneNumber: '+447780579093',
},
signupPhoneVerificationRequestedAt: 12345,
signupPhoneVerificationPurpose: 'login',
}, {
currentEmail: 'fresh@example.com',
preserveAccountIdentity: true,
source: 'flow',
});
assert.deepStrictEqual(updates, {
email: 'fresh@example.com',
registrationEmailState: {
current: 'fresh@example.com',
previous: 'fresh@example.com',
source: 'flow',
updatedAt: updates.registrationEmailState.updatedAt,
},
phoneNumber: '',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
signupPhoneActivation: {
activationId: 'active-1',
phoneNumber: '+447780579093',
},
signupPhoneCompletedActivation: {
activationId: 'done-1',
phoneNumber: '+447780579093',
},
signupPhoneVerificationRequestedAt: 12345,
signupPhoneVerificationPurpose: 'login',
});
assert.ok(updates.registrationEmailState.updatedAt > 0);
});
test('flow registration email state falls back to email identity updates when no phone identity exists', () => {
const api = loadRegistrationEmailStateApi();
const helpers = api.createRegistrationEmailStateHelpers();
const updates = helpers.buildFlowRegistrationEmailStateUpdates({
email: '',
accountIdentifierType: 'email',
accountIdentifier: 'old@example.com',
}, {
currentEmail: 'fresh@example.com',
preserveAccountIdentity: true,
source: 'flow',
});
assert.deepStrictEqual(updates, {
email: 'fresh@example.com',
registrationEmailState: {
current: 'fresh@example.com',
previous: 'fresh@example.com',
source: 'flow',
updatedAt: updates.registrationEmailState.updatedAt,
},
});
assert.ok(updates.registrationEmailState.updatedAt > 0);
});
@@ -789,6 +789,72 @@ test('signup flow helper can generate an email on demand when add-email starts f
]);
});
test('signup flow helper delegates preserved phone identity email sync to the shared persistence helper when reusing an existing email', async () => {
const persistCalls = [];
let setEmailCalls = 0;
let setStateCalls = 0;
const helpers = signupFlowApi.createSignupFlowHelpers({
buildGeneratedAliasEmail: () => 'demo+saved@gmail.com',
chrome: { tabs: { get: async () => ({ id: 21, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureLuckmailPurchaseForFlow: async () => ({}),
fetchGeneratedEmail: async () => {
throw new Error('should not generate a new email');
},
isGeneratedAliasProvider: () => true,
isReusableGeneratedAliasEmail: (_state, email) => email === 'demo+saved@gmail.com',
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
persistRegistrationEmailState: async (state, email, options) => {
persistCalls.push({ state, email, options });
},
reuseOrCreateTab: async () => 21,
sendToContentScriptResilient: async () => ({}),
setEmailState: async () => {
setEmailCalls += 1;
},
setState: async () => {
setStateCalls += 1;
},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabUrlMatch: async () => null,
});
const state = {
mailProvider: 'gmail',
email: 'demo+saved@gmail.com',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
signupPhoneCompletedActivation: {
activationId: 'signup-completed',
phoneNumber: '+447780579093',
},
};
const email = await helpers.resolveSignupEmailForFlow(state, {
preserveAccountIdentity: true,
});
assert.equal(email, 'demo+saved@gmail.com');
assert.equal(setEmailCalls, 0);
assert.equal(setStateCalls, 0);
assert.deepStrictEqual(persistCalls, [
{
state,
email: 'demo+saved@gmail.com',
options: {
source: 'flow',
preserveAccountIdentity: true,
},
},
]);
});
test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => {
let ensureCalls = 0;
const messages = [];
+74
View File
@@ -154,3 +154,77 @@ test('pollCloudMailVerificationCode ignores stale receive mailbox when generator
assert.equal(result.code, '246810');
assert.deepEqual(api.snapshot().listCalls, ['generated@example.com']);
});
test('fetchCloudMailAddress preserves phone identity through the shared persistence helper', async () => {
const persistCalls = [];
const setEmailCalls = [];
const api = globalThis.MultiPageBackgroundCloudMailProvider.createCloudMailProvider({
addLog: async () => {},
buildCloudMailHeaders: () => ({}),
CLOUD_MAIL_DEFAULT_PAGE_SIZE: 20,
CLOUD_MAIL_GENERATOR: 'cloudmail',
CLOUD_MAIL_PROVIDER: 'cloudmail',
fetchImpl: async (url) => {
if (String(url).includes('/api/public/addUser')) {
return {
ok: true,
text: async () => JSON.stringify({ code: 200 }),
};
}
return {
ok: true,
text: async () => JSON.stringify({ code: 200, data: { token: 'token' } }),
};
},
getCloudMailTokenFromResponse: () => 'token',
getState: async () => ({}),
joinCloudMailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudMailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeCloudMailBaseUrl: (value) => String(value || '').trim(),
normalizeCloudMailDomain: (value) => String(value || '').trim(),
normalizeCloudMailDomains: (values) => values || [],
normalizeCloudMailMailApiMessages: () => [],
persistRegistrationEmailState: async (state, email, options) => {
persistCalls.push({ state, email, options });
},
pickVerificationMessageWithTimeFallback: () => ({
match: null,
usedRelaxedFilters: false,
usedTimeFallback: false,
}),
setEmailState: async (email) => {
setEmailCalls.push(email);
},
setPersistentSettings: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
const state = {
cloudMailBaseUrl: 'https://mail.example.com',
cloudMailAdminEmail: 'admin@example.com',
cloudMailAdminPassword: 'secret',
cloudMailToken: 'token',
cloudMailDomain: 'example.com',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
};
const email = await api.fetchCloudMailAddress(state, {
localPart: 'fresh',
preserveAccountIdentity: true,
});
assert.equal(email, 'fresh@example.com');
assert.deepStrictEqual(setEmailCalls, []);
assert.deepStrictEqual(persistCalls, [
{
state,
email: 'fresh@example.com',
options: {
source: 'generated:cloudmail',
preserveAccountIdentity: true,
},
},
]);
});