Fix add-email preserve identity email state flow
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user