chore: merge latest dev into pr 208 fixes
This commit is contained in:
@@ -2,6 +2,47 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function extractFunctionBody(source, name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
for (let i = braceStart; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return source.slice(braceStart + 1, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`unterminated function ${name}`);
|
||||
}
|
||||
|
||||
test('icloud login helper distinguishes auth-required errors from transient context errors', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
@@ -107,3 +148,20 @@ test('icloud login helper distinguishes auth-required errors from transient cont
|
||||
'icloud auto-fetch should fallback to reusable aliases when create-new fails due transient session/context issues'
|
||||
);
|
||||
});
|
||||
|
||||
test('icloud login helper does not redeclare safeActionLabel in transient branch', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const body = extractFunctionBody(source, 'withIcloudLoginHelp');
|
||||
const declarations = body.match(/\bconst\s+safeActionLabel\b/g) || [];
|
||||
|
||||
assert.equal(
|
||||
declarations.length,
|
||||
1,
|
||||
'withIcloudLoginHelp should declare safeActionLabel once to avoid temporal-dead-zone crashes'
|
||||
);
|
||||
assert.match(
|
||||
body,
|
||||
/const transientError = new Error\(`iCloud:\$\{safeActionLabel\}受网络\/上下文波动影响,请稍后重试。`\);/,
|
||||
'transient context errors should use the already-initialized safeActionLabel'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,27 @@ test('background imports message router module', () => {
|
||||
assert.match(source, /background\/message-router\.js/);
|
||||
});
|
||||
|
||||
test('background defaults enable free phone reuse switches', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const defaultsStart = source.indexOf('const PERSISTED_SETTING_DEFAULTS = {');
|
||||
const defaultsEnd = source.indexOf('const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);');
|
||||
const defaultsBlock = source.slice(defaultsStart, defaultsEnd);
|
||||
|
||||
assert.match(defaultsBlock, /freePhoneReuseEnabled:\s*true/);
|
||||
assert.match(defaultsBlock, /freePhoneReuseAutoEnabled:\s*true/);
|
||||
});
|
||||
|
||||
test('background free reusable phone setter does not depend on module-scoped phone flow constants', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const setterStart = source.indexOf('async function setFreeReusablePhoneActivation');
|
||||
const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart);
|
||||
const setterBlock = source.slice(setterStart, setterEnd);
|
||||
|
||||
assert.ok(setterStart >= 0, 'expected setFreeReusablePhoneActivation to exist');
|
||||
assert.doesNotMatch(setterBlock, /DEFAULT_PHONE_NUMBER_MAX_USES/);
|
||||
assert.match(setterBlock, /maxUses:\s*Math\.max\(1,\s*Math\.floor\(Number\(record\.maxUses\)\s*\|\|\s*3\)\)/);
|
||||
});
|
||||
|
||||
test('message router module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = {};
|
||||
@@ -15,3 +36,56 @@ test('message router module exposes a factory', () => {
|
||||
|
||||
assert.equal(typeof api?.createMessageRouter, 'function');
|
||||
});
|
||||
|
||||
test('SAVE_SETTING broadcasts free phone reuse setting updates for realtime sidepanel sync', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
const broadcasts = [];
|
||||
let state = {
|
||||
freePhoneReuseEnabled: false,
|
||||
freePhoneReuseAutoEnabled: false,
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
};
|
||||
|
||||
const router = api.createMessageRouter({
|
||||
addLog: async () => {},
|
||||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||||
buildPersistentSettingsPayload: (input = {}) => {
|
||||
const updates = {};
|
||||
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseEnabled')) {
|
||||
updates.freePhoneReuseEnabled = Boolean(input.freePhoneReuseEnabled);
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(input, 'freePhoneReuseAutoEnabled')) {
|
||||
updates.freePhoneReuseAutoEnabled = Boolean(input.freePhoneReuseAutoEnabled);
|
||||
}
|
||||
return updates;
|
||||
},
|
||||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||||
getState: async () => ({ ...state }),
|
||||
setPersistentSettings: async () => {},
|
||||
setState: async (updates) => {
|
||||
state = { ...state, ...updates };
|
||||
},
|
||||
});
|
||||
|
||||
const response = await router.handleMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
payload: {
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.ok, true);
|
||||
assert.equal(state.freePhoneReuseEnabled, true);
|
||||
assert.equal(state.freePhoneReuseAutoEnabled, true);
|
||||
assert.ok(
|
||||
broadcasts.some((payload) => (
|
||||
payload.freePhoneReuseEnabled === true
|
||||
&& payload.freePhoneReuseAutoEnabled === true
|
||||
)),
|
||||
'expected SAVE_SETTING to broadcast free reuse switch updates'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -116,3 +116,49 @@ test('panel bridge can request cpa oauth url via management api', async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('panel bridge forwards SUB2API account priority when requesting oauth url', async () => {
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
const sentMessages = [];
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
|
||||
const bridge = api.createPanelBridge({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async () => ({ id: 72 }),
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'sub2api',
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
rememberSourceLastUrl: async () => {},
|
||||
sendToContentScript: async (sourceName, message, options) => {
|
||||
sentMessages.push({ sourceName, message, options });
|
||||
return {
|
||||
oauthUrl: 'https://auth.openai.com/authorize?state=oauth-state',
|
||||
sub2apiSessionId: 'session-123',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
};
|
||||
},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
waitForTabUrlFamily: async () => ({ id: 72 }),
|
||||
DEFAULT_SUB2API_GROUP_NAME: 'codex',
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
|
||||
});
|
||||
|
||||
await bridge.requestOAuthUrlFromPanel({
|
||||
panelMode: 'sub2api',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex',
|
||||
sub2apiAccountPriority: 3,
|
||||
}, { logLabel: '步骤 7' });
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 3);
|
||||
});
|
||||
|
||||
@@ -272,3 +272,34 @@ test('platform verify module sends Plus visible step 13 to SUB2API panel', async
|
||||
assert.equal(sentMessages[0].message.step, 13);
|
||||
});
|
||||
|
||||
test('platform verify module forwards SUB2API account priority to panel', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const sentMessages = [];
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps } = createDeps({
|
||||
getPanelMode: () => 'sub2api',
|
||||
getTabId: async () => 91,
|
||||
isTabAlive: async () => true,
|
||||
sendToContentScript: async (sourceName, message, options) => {
|
||||
sentMessages.push({ sourceName, message, options });
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'sub2api',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiAccountPriority: 2,
|
||||
});
|
||||
|
||||
assert.equal(sentMessages.length, 1);
|
||||
assert.equal(sentMessages[0].sourceName, 'sub2api-panel');
|
||||
assert.equal(sentMessages[0].message.payload.sub2apiAccountPriority, 2);
|
||||
});
|
||||
|
||||
@@ -294,3 +294,152 @@ return {
|
||||
|
||||
assert.equal(result.code, '556677');
|
||||
});
|
||||
|
||||
test('icloud step8 polling finds a visible first-row code immediately', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getThreadItemMetadata'),
|
||||
extractFunction('buildItemSignature'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
extractFunction('normalizePollSessionKey'),
|
||||
extractFunction('getOrCreatePollSessionBaseline'),
|
||||
extractFunction('persistPollSessionBaseline'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const ICLOUD_POLL_SESSION_CACHE = new Map();
|
||||
function log() {}
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {}
|
||||
async function waitForElement() { return true; }
|
||||
async function refreshInbox() { return true; }
|
||||
const currentThreadData = [
|
||||
{
|
||||
signature: 'visible-code',
|
||||
sender: 'noreply@tm.openai.com',
|
||||
subject: '你的 OpenAI 代码为 576773',
|
||||
preview: '输入此临时验证码以继续:576773',
|
||||
timestamp: '21:05',
|
||||
ariaLabel: 'visible-code',
|
||||
},
|
||||
{
|
||||
signature: 'older-code',
|
||||
sender: 'noreply@tm.openai.com',
|
||||
subject: '你的 OpenAI 代码为 697852',
|
||||
preview: '输入此临时验证码以继续:697852',
|
||||
timestamp: '21:04',
|
||||
ariaLabel: 'older-code',
|
||||
},
|
||||
];
|
||||
function collectThreadItems() {
|
||||
return currentThreadData.map((entry) => ({
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
|
||||
return '';
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === '.thread-participants') return { textContent: entry.sender };
|
||||
if (selector === '.thread-subject') return { textContent: entry.subject };
|
||||
if (selector === '.thread-preview') return { textContent: entry.preview };
|
||||
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
}
|
||||
async function openMailItemAndRead(item) {
|
||||
const meta = getThreadItemMetadata(item);
|
||||
return {
|
||||
sender: meta.sender,
|
||||
recipients: '',
|
||||
timestamp: meta.timestamp,
|
||||
bodyText: meta.preview,
|
||||
combinedText: meta.combinedText,
|
||||
};
|
||||
}
|
||||
${bundle}
|
||||
return { handlePollEmail };
|
||||
`)();
|
||||
|
||||
const result = await api.handlePollEmail(8, {
|
||||
senderFilters: ['openai', 'noreply', 'chatgpt'],
|
||||
subjectFilters: ['code', '验证码', 'login'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 10,
|
||||
excludeCodes: [],
|
||||
sessionKey: '8:visible-first',
|
||||
});
|
||||
|
||||
assert.equal(result.code, '576773');
|
||||
});
|
||||
|
||||
test('icloud step8 visible first-row code still respects excluded codes', async () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getThreadItemMetadata'),
|
||||
extractFunction('buildItemSignature'),
|
||||
extractFunction('extractVerificationCode'),
|
||||
extractFunction('normalizePollSessionKey'),
|
||||
extractFunction('getOrCreatePollSessionBaseline'),
|
||||
extractFunction('persistPollSessionBaseline'),
|
||||
extractFunction('handlePollEmail'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const ICLOUD_POLL_SESSION_CACHE = new Map();
|
||||
function log() {}
|
||||
function throwIfStopped() {}
|
||||
async function sleep() {}
|
||||
async function waitForElement() { return true; }
|
||||
async function refreshInbox() { return true; }
|
||||
const currentThreadData = [
|
||||
{
|
||||
signature: 'visible-code',
|
||||
sender: 'noreply@tm.openai.com',
|
||||
subject: '你的 OpenAI 代码为 576773',
|
||||
preview: '输入此临时验证码以继续:576773',
|
||||
timestamp: '21:05',
|
||||
ariaLabel: 'visible-code',
|
||||
},
|
||||
];
|
||||
function collectThreadItems() {
|
||||
return currentThreadData.map((entry) => ({
|
||||
getAttribute(name) {
|
||||
if (name === 'aria-label') return entry.ariaLabel || entry.signature;
|
||||
return '';
|
||||
},
|
||||
querySelector(selector) {
|
||||
if (selector === '.thread-participants') return { textContent: entry.sender };
|
||||
if (selector === '.thread-subject') return { textContent: entry.subject };
|
||||
if (selector === '.thread-preview') return { textContent: entry.preview };
|
||||
if (selector === '.thread-timestamp') return { textContent: entry.timestamp };
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
}
|
||||
async function openMailItemAndRead(item) {
|
||||
const meta = getThreadItemMetadata(item);
|
||||
return {
|
||||
sender: meta.sender,
|
||||
recipients: '',
|
||||
timestamp: meta.timestamp,
|
||||
bodyText: meta.preview,
|
||||
combinedText: meta.combinedText,
|
||||
};
|
||||
}
|
||||
${bundle}
|
||||
return { handlePollEmail };
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
() => api.handlePollEmail(8, {
|
||||
senderFilters: ['openai', 'noreply', 'chatgpt'],
|
||||
subjectFilters: ['code', '验证码', 'login'],
|
||||
maxAttempts: 1,
|
||||
intervalMs: 10,
|
||||
excludeCodes: ['576773'],
|
||||
sessionKey: '8:excluded-visible-first',
|
||||
}),
|
||||
/仍未在 iCloud 邮箱中找到新的匹配邮件/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -448,3 +448,52 @@ test('phone auth resend stops with PHONE_ROUTE_405_RECOVERY_FAILED instead of en
|
||||
global.window = originalWindow;
|
||||
}
|
||||
});
|
||||
|
||||
test('phone auth exposes resend page error checks for banned numbers', () => {
|
||||
const originalLocation = global.location;
|
||||
const originalDocument = global.document;
|
||||
try {
|
||||
global.location = {
|
||||
href: 'https://auth.openai.com/phone-verification',
|
||||
pathname: '/phone-verification',
|
||||
};
|
||||
global.document = {
|
||||
querySelector() {
|
||||
return {
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneAuthHelpers({
|
||||
fillInput: () => {},
|
||||
getActionText: () => '',
|
||||
getPageTextSnapshot: () => 'We cannot send a text message to this phone number. Please try another.',
|
||||
getVerificationErrorText: () => '',
|
||||
humanPause: async () => {},
|
||||
isActionEnabled: () => true,
|
||||
isAddPhonePageReady: () => false,
|
||||
isConsentReady: () => false,
|
||||
isPhoneVerificationPageReady: () => true,
|
||||
isVisibleElement: () => true,
|
||||
simulateClick: () => {},
|
||||
sleep: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
waitForElement: async () => null,
|
||||
});
|
||||
|
||||
const result = helpers.checkPhoneResendError();
|
||||
assert.equal(result.hasError, true);
|
||||
assert.equal(result.reason, 'resend_phone_banned');
|
||||
assert.equal(result.prefix, 'PHONE_RESEND_BANNED_NUMBER::');
|
||||
assert.match(result.message, /cannot send/i);
|
||||
} finally {
|
||||
global.location = originalLocation;
|
||||
global.document = originalDocument;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2794,6 +2794,28 @@ test('phone verification helper immediately replaces number when page says the p
|
||||
verificationResendCount: 1,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
phoneReusableActivationPool: [
|
||||
{
|
||||
activationId: 'pool-used-match',
|
||||
phoneNumber: '+66 95 000 0011',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
},
|
||||
],
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'free-used-match',
|
||||
phoneNumber: '+66 95 000 0011',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const numbers = [
|
||||
@@ -2903,6 +2925,11 @@ test('phone verification helper immediately replaces number when page says the p
|
||||
'SUBMIT_PHONE_NUMBER',
|
||||
'SUBMIT_PHONE_VERIFICATION_CODE',
|
||||
]);
|
||||
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||
assert.deepStrictEqual(
|
||||
currentState.phoneReusableActivationPool.filter((activation) => activation.phoneNumber === '+66 95 000 0011'),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper treats phone_max_usage_exceeded as used-number and rotates immediately', async () => {
|
||||
@@ -2913,6 +2940,28 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
|
||||
verificationResendCount: 1,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
phoneReusableActivationPool: [
|
||||
{
|
||||
activationId: 'pool-max-match',
|
||||
phoneNumber: '+66 95 000 1011',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
successfulUses: 1,
|
||||
maxUses: 3,
|
||||
},
|
||||
],
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'free-max-match',
|
||||
phoneNumber: '+66 95 000 1011',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const numbers = [
|
||||
@@ -3022,6 +3071,11 @@ test('phone verification helper treats phone_max_usage_exceeded as used-number a
|
||||
'SUBMIT_PHONE_NUMBER',
|
||||
'SUBMIT_PHONE_VERIFICATION_CODE',
|
||||
]);
|
||||
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||
assert.deepStrictEqual(
|
||||
currentState.phoneReusableActivationPool.filter((activation) => activation.phoneNumber === '+66 95 000 1011'),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper rotates number when submitPhoneVerificationCode throws phone_max_usage_exceeded', async () => {
|
||||
@@ -3418,6 +3472,771 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
|
||||
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
|
||||
});
|
||||
|
||||
test('phone verification helper gives automatic free reuse priority over paid reuse and new acquisition', async () => {
|
||||
const requests = [];
|
||||
let statusPollCount = 0;
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
heroSmsReuseEnabled: true,
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
reusablePhoneActivation: {
|
||||
activationId: 'paid-reuse',
|
||||
phoneNumber: '66950003333',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
},
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'free-priority',
|
||||
phoneNumber: '66950004444',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||
throw new Error(`${action} should not be called before automatic free reuse`);
|
||||
}
|
||||
if (action === 'setStatus' && id === 'free-priority') {
|
||||
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'free-priority') {
|
||||
statusPollCount += 1;
|
||||
return { ok: true, text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_CODE' : 'STATUS_OK:112233') };
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
requests.map((requestUrl) => requestUrl.searchParams.get('action')),
|
||||
['setStatus', 'getStatus', 'getStatus']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
requests
|
||||
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||
['3']
|
||||
);
|
||||
assert.equal(currentState.freeReusablePhoneActivation.successfulUses, 1);
|
||||
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'free-priority');
|
||||
assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse');
|
||||
});
|
||||
|
||||
test('phone verification helper accepts HeroSMS WAIT_RETRY as free-reuse ready without using its suffix as code', async () => {
|
||||
const requests = [];
|
||||
const events = [];
|
||||
const submittedCodes = [];
|
||||
let statusPollCount = 0;
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'retry-ready-free',
|
||||
phoneNumber: '6283184934060',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 6,
|
||||
countryLabel: 'Indonesia',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
const status = parsedUrl.searchParams.get('status');
|
||||
events.push(action === 'setStatus' ? `${action}:${status}` : action);
|
||||
if (action === 'setStatus' && id === 'retry-ready-free' && status === '3') {
|
||||
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'retry-ready-free') {
|
||||
statusPollCount += 1;
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RETRY:100001' : 'STATUS_OK:654321'),
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
events.push(message.type);
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
assert.equal(message.payload.phoneNumber, '6283184934060');
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
submittedCodes.push(message.payload.code);
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {
|
||||
events.push('sleep');
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.deepStrictEqual(submittedCodes, ['654321']);
|
||||
assert.equal(submittedCodes.includes('100001'), false);
|
||||
assert.deepStrictEqual(
|
||||
events.slice(0, 4),
|
||||
['setStatus:3', 'sleep', 'getStatus', 'SUBMIT_PHONE_NUMBER']
|
||||
);
|
||||
assert.equal(currentState.freeReusablePhoneActivation.successfulUses, 1);
|
||||
assert.equal(
|
||||
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper stops failed automatic free reuse without buying a new number', async () => {
|
||||
const requests = [];
|
||||
const messages = [];
|
||||
const stops = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'cancelled-free',
|
||||
phoneNumber: '66950007777',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
if (action === 'setStatus' && id === 'cancelled-free') {
|
||||
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'cancelled-free') {
|
||||
return { ok: true, text: async () => 'STATUS_CANCEL' };
|
||||
}
|
||||
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||
throw new Error(`${action} should not be called after automatic free reuse preparation fails`);
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
requestStop: async (payload) => {
|
||||
stops.push(payload);
|
||||
},
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
messages.push(message);
|
||||
throw new Error(`Auth page should not be touched after failed free reuse: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
}),
|
||||
/PHONE_AUTO_FREE_REUSE_PREPARE::/
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(messages, []);
|
||||
assert.equal(stops.length, 1);
|
||||
assert.match(stops[0].logMessage, /不购买新 HeroSMS 号码/);
|
||||
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||
assert.equal(
|
||||
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper stops never-ready automatic free reuse without buying or reactivating', async () => {
|
||||
const requests = [];
|
||||
const messages = [];
|
||||
const stops = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
heroSmsReuseEnabled: true,
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'never-ready-free',
|
||||
phoneNumber: '66950008888',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
if (action === 'setStatus' && id === 'never-ready-free') {
|
||||
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'never-ready-free') {
|
||||
return { ok: true, text: async () => 'STATUS_OK:445566' };
|
||||
}
|
||||
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||
throw new Error(`${action} should not be called after automatic free reuse never becomes ready`);
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
requestStop: async (payload) => {
|
||||
stops.push(payload);
|
||||
},
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
messages.push(message);
|
||||
throw new Error(`Auth page should not be touched when automatic free reuse is never ready: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
}),
|
||||
/PHONE_AUTO_FREE_REUSE_PREPARE::/
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(messages, []);
|
||||
assert.equal(stops.length, 1);
|
||||
assert.match(stops[0].logMessage, /不购买新 HeroSMS 号码/);
|
||||
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'never-ready-free');
|
||||
assert.equal(
|
||||
requests.some((requestUrl) => ['reactivate', 'getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
|
||||
false
|
||||
);
|
||||
assert.equal(requests.every((requestUrl) => requestUrl.searchParams.get('id') === 'never-ready-free'), true);
|
||||
});
|
||||
|
||||
test('phone verification helper accepts HeroSMS WAIT_RESEND as free-reuse ready before submit', async () => {
|
||||
const requests = [];
|
||||
const messages = [];
|
||||
let statusPollCount = 0;
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'not-waiting-free',
|
||||
phoneNumber: '66950005555',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
if (action === 'setStatus' && id === 'not-waiting-free') {
|
||||
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'not-waiting-free') {
|
||||
statusPollCount += 1;
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RESEND' : 'STATUS_OK:778899'),
|
||||
};
|
||||
}
|
||||
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||
throw new Error(`${action} should not be called for accepted saved free reuse waiting state`);
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
messages.push(message);
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
messages.map((message) => message.type),
|
||||
['SUBMIT_PHONE_NUMBER', 'SUBMIT_PHONE_VERIFICATION_CODE']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
requests
|
||||
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||
['3']
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper accepts HeroSMS WAIT_RESEND suffix as free-reuse ready before submit', async () => {
|
||||
const requests = [];
|
||||
const messages = [];
|
||||
let statusPollCount = 0;
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'resend-info-free',
|
||||
phoneNumber: '66950005656',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 0,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
if (action === 'setStatus' && id === 'resend-info-free') {
|
||||
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'resend-info-free') {
|
||||
statusPollCount += 1;
|
||||
return {
|
||||
ok: true,
|
||||
text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_RESEND:100001' : 'STATUS_OK:887766'),
|
||||
};
|
||||
}
|
||||
if (action === 'reactivate' || action === 'getPrices' || action === 'getNumber' || action === 'getNumberV2') {
|
||||
throw new Error(`${action} should not be called for accepted saved free reuse resend state`);
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
messages.push(message);
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
assert.equal(message.payload.phoneNumber, '66950005656');
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
assert.equal(message.payload.code, '887766');
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.deepStrictEqual(
|
||||
messages.map((message) => message.type),
|
||||
['SUBMIT_PHONE_NUMBER', 'SUBMIT_PHONE_VERIFICATION_CODE']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
requests
|
||||
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||
['3']
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper retires automatic free reuse record at max uses', async () => {
|
||||
const requests = [];
|
||||
let statusPollCount = 0;
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
freePhoneReuseEnabled: true,
|
||||
freePhoneReuseAutoEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: {
|
||||
activationId: 'free-max',
|
||||
phoneNumber: '66950009999',
|
||||
provider: 'hero-sms',
|
||||
serviceCode: 'dr',
|
||||
countryId: 52,
|
||||
countryLabel: 'Thailand',
|
||||
successfulUses: 2,
|
||||
maxUses: 3,
|
||||
source: 'free-manual-reuse',
|
||||
},
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
if (action === 'setStatus' && id === 'free-max') {
|
||||
return { ok: true, text: async () => 'ACCESS_RETRY_GET' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'free-max') {
|
||||
statusPollCount += 1;
|
||||
return { ok: true, text: async () => (statusPollCount === 1 ? 'STATUS_WAIT_CODE' : 'STATUS_OK:778899') };
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.equal(currentState.freeReusablePhoneActivation, null);
|
||||
assert.deepStrictEqual(
|
||||
requests.map((requestUrl) => requestUrl.searchParams.get('action')),
|
||||
['setStatus', 'getStatus', 'getStatus']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
requests
|
||||
.filter((requestUrl) => requestUrl.searchParams.get('action') === 'setStatus')
|
||||
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||
['3']
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper preserves coded free-reuse activation when code submission errors', async () => {
|
||||
const requests = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
heroSmsCountryId: 52,
|
||||
heroSmsCountryLabel: 'Thailand',
|
||||
freePhoneReuseEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
const status = parsedUrl.searchParams.get('status');
|
||||
if (action === 'getPrices') {
|
||||
return { ok: true, text: async () => buildHeroSmsPricesPayload() };
|
||||
}
|
||||
if (action === 'getNumber') {
|
||||
return { ok: true, text: async () => 'ACCESS_NUMBER:new-free-save:66950006666' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'new-free-save') {
|
||||
return { ok: true, text: async () => 'STATUS_OK:445566' };
|
||||
}
|
||||
if (action === 'setStatus' && id === 'new-free-save' && status === '8') {
|
||||
throw new Error('free-reuse activation should not be cancelled after SMS code was received');
|
||||
}
|
||||
if (action === 'setStatus' && id === 'new-free-save' && status === '6') {
|
||||
throw new Error('free-reuse activation should not be completed after SMS code was received');
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
throw new Error('simulated auth-page failure after code submit');
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
}),
|
||||
/simulated auth-page failure/
|
||||
);
|
||||
|
||||
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'new-free-save');
|
||||
assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '66950006666');
|
||||
assert.equal(
|
||||
requests.some((requestUrl) => (
|
||||
requestUrl.searchParams.get('action') === 'setStatus'
|
||||
&& requestUrl.searchParams.get('id') === 'new-free-save'
|
||||
&& ['6', '8'].includes(requestUrl.searchParams.get('status'))
|
||||
)),
|
||||
false
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
requests
|
||||
.filter((requestUrl) => (
|
||||
requestUrl.searchParams.get('action') === 'setStatus'
|
||||
&& requestUrl.searchParams.get('id') === 'new-free-save'
|
||||
))
|
||||
.map((requestUrl) => requestUrl.searchParams.get('status')),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper preserves newly saved free-reuse activation after OAuth success without source marker', async () => {
|
||||
const requests = [];
|
||||
let currentState = {
|
||||
heroSmsApiKey: 'demo-key',
|
||||
heroSmsCountryId: 52,
|
||||
heroSmsCountryLabel: 'Thailand',
|
||||
freePhoneReuseEnabled: true,
|
||||
verificationResendCount: 0,
|
||||
currentPhoneActivation: null,
|
||||
freeReusablePhoneActivation: null,
|
||||
reusablePhoneActivation: null,
|
||||
};
|
||||
|
||||
const helpers = api.createPhoneVerificationHelpers({
|
||||
addLog: async () => {},
|
||||
ensureStep8SignupPageReady: async () => {},
|
||||
fetchImpl: async (url) => {
|
||||
const parsedUrl = new URL(url);
|
||||
requests.push(parsedUrl);
|
||||
const action = parsedUrl.searchParams.get('action');
|
||||
const id = parsedUrl.searchParams.get('id');
|
||||
const status = parsedUrl.searchParams.get('status');
|
||||
if (action === 'getPrices') {
|
||||
return { ok: true, text: async () => buildHeroSmsPricesPayload() };
|
||||
}
|
||||
if (action === 'getNumber') {
|
||||
return { ok: true, text: async () => 'ACCESS_NUMBER:new-free-success:66950007777' };
|
||||
}
|
||||
if (action === 'getStatus' && id === 'new-free-success') {
|
||||
return { ok: true, text: async () => 'STATUS_OK:778899' };
|
||||
}
|
||||
if (action === 'setStatus' && id === 'new-free-success' && ['6', '8'].includes(status)) {
|
||||
throw new Error(`free-reuse activation should not receive terminal setStatus(${status}) after OAuth success`);
|
||||
}
|
||||
throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}:${status || ''}`);
|
||||
},
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
|
||||
getState: async () => ({ ...currentState }),
|
||||
sendToContentScriptResilient: async (_source, message) => {
|
||||
if (message.type === 'SUBMIT_PHONE_NUMBER') {
|
||||
return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' };
|
||||
}
|
||||
if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') {
|
||||
return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' };
|
||||
}
|
||||
throw new Error(`Unexpected content-script message: ${message.type}`);
|
||||
},
|
||||
setState: async (updates) => {
|
||||
currentState = { ...currentState, ...updates };
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const result = await helpers.completePhoneVerificationFlow(1, {
|
||||
addPhonePage: true,
|
||||
phoneVerificationPage: false,
|
||||
url: 'https://auth.openai.com/add-phone',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
success: true,
|
||||
consentReady: true,
|
||||
url: 'https://auth.openai.com/authorize',
|
||||
});
|
||||
assert.equal(currentState.freeReusablePhoneActivation.activationId, 'new-free-success');
|
||||
assert.equal(currentState.freeReusablePhoneActivation.phoneNumber, '66950007777');
|
||||
assert.equal(currentState.freeReusablePhoneActivation.source, 'free-manual-reuse');
|
||||
assert.equal(
|
||||
requests.some((requestUrl) => (
|
||||
requestUrl.searchParams.get('action') === 'setStatus'
|
||||
&& requestUrl.searchParams.get('id') === 'new-free-success'
|
||||
&& ['6', '8'].includes(requestUrl.searchParams.get('status'))
|
||||
)),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('phone verification helper replaces number immediately when resend is throttled and does not spam resend clicks', async () => {
|
||||
const requests = [];
|
||||
const messages = [];
|
||||
|
||||
@@ -932,6 +932,65 @@ test('GPC billing reads OTP from local SMS helper when enabled', async () => {
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('GPC billing can read WhatsApp OTP from local helper when enabled', async () => {
|
||||
const fetchCalls = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [],
|
||||
stateByFrame: {},
|
||||
fetchImpl: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
if (url.startsWith('http://127.0.0.1:18767/otp')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ ok: true, otp: '765432', message_id: 'wa-1' }),
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/api/gopay/otp')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ reference_id: 'ref_wa', challenge_id: 'challenge_wa' }),
|
||||
};
|
||||
}
|
||||
if (url.endsWith('/api/gopay/pin')) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ stage: 'gopay_complete' }),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({
|
||||
plusPaymentMethod: 'gpc-helper',
|
||||
plusCheckoutSource: 'gpc-helper',
|
||||
gopayHelperReferenceId: 'ref_wa',
|
||||
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
|
||||
gopayHelperPin: '654321',
|
||||
gopayHelperCardKey: 'card_wa',
|
||||
gopayHelperOtpChannel: 'whatsapp',
|
||||
gopayHelperLocalSmsHelperEnabled: true,
|
||||
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
|
||||
gopayHelperPhoneNumber: '+8613800138000',
|
||||
});
|
||||
|
||||
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
|
||||
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '765432'), true);
|
||||
const helperUrl = new URL(fetchCalls[0].url);
|
||||
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp');
|
||||
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_wa');
|
||||
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000');
|
||||
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
|
||||
reference_id: 'ref_wa',
|
||||
otp: '765432',
|
||||
card_key: 'card_wa',
|
||||
});
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => {
|
||||
const fetchCalls = [];
|
||||
let currentState = {
|
||||
|
||||
@@ -110,6 +110,7 @@ const inputAutoSkipFailures = { disabled: false };
|
||||
const autoContinueBar = { style: { display: '' } };
|
||||
|
||||
function setSettingsCardLocked() {}
|
||||
function setFreePhoneReuseControlsLocked() {}
|
||||
function getAutoRunLabel() { return ''; }
|
||||
function getLockedRunCountFromEmailPool() { return lockedRunCount; }
|
||||
function isCustomMailProvider() { return false; }
|
||||
@@ -198,6 +199,7 @@ const inputAutoSkipFailures = { disabled: false };
|
||||
const autoContinueBar = { style: { display: '' } };
|
||||
|
||||
function setSettingsCardLocked() {}
|
||||
function setFreePhoneReuseControlsLocked() {}
|
||||
function getAutoRunLabel() { return ''; }
|
||||
function getLockedRunCountFromEmailPool() { return 0; }
|
||||
function isCustomMailProvider() { return false; }
|
||||
|
||||
@@ -108,6 +108,18 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
|
||||
assert.match(html, /id="row-hero-sms-current-code"/);
|
||||
assert.match(html, /id="row-hero-sms-preferred-activation"/);
|
||||
assert.match(html, /id="select-hero-sms-preferred-activation"/);
|
||||
assert.match(html, /id="row-free-phone-reuse-enabled"/);
|
||||
assert.match(html, /id="input-free-phone-reuse-enabled"/);
|
||||
assert.match(html, /id="row-free-phone-reuse-auto-enabled"/);
|
||||
assert.match(html, /id="input-free-phone-reuse-auto-enabled"/);
|
||||
assert.match(html, /id="row-free-reusable-phone"/);
|
||||
assert.match(html, /id="display-free-reusable-phone"/);
|
||||
assert.match(html, /id="display-free-reusable-phone-country"/);
|
||||
assert.match(html, /id="input-free-reusable-phone"/);
|
||||
assert.match(html, /id="btn-save-free-reusable-phone"/);
|
||||
assert.match(html, /id="btn-clear-free-reusable-phone"/);
|
||||
assert.match(html, /白嫖复用/);
|
||||
assert.match(html, /自动白嫖复用/);
|
||||
assert.match(html, /id="row-phone-replacement-limit"/);
|
||||
assert.match(html, /id="row-phone-verification-resend-count"/);
|
||||
assert.match(html, /id="row-phone-code-wait-seconds"/);
|
||||
@@ -134,6 +146,49 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
|
||||
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
|
||||
});
|
||||
|
||||
test('sidepanel source wires free reusable phone save and clear actions to runtime messages', () => {
|
||||
assert.match(sidepanelSource, /const inputFreePhoneReuseEnabled = document\.getElementById\('input-free-phone-reuse-enabled'\);/);
|
||||
assert.match(sidepanelSource, /const inputFreePhoneReuseAutoEnabled = document\.getElementById\('input-free-phone-reuse-auto-enabled'\);/);
|
||||
assert.match(sidepanelSource, /const displayFreeReusablePhone = document\.getElementById\('display-free-reusable-phone'\);/);
|
||||
assert.match(sidepanelSource, /const inputFreeReusablePhone = document\.getElementById\('input-free-reusable-phone'\);/);
|
||||
assert.match(sidepanelSource, /const btnSaveFreeReusablePhone = document\.getElementById\('btn-save-free-reusable-phone'\);/);
|
||||
assert.match(sidepanelSource, /const btnClearFreeReusablePhone = document\.getElementById\('btn-clear-free-reusable-phone'\);/);
|
||||
assert.match(sidepanelSource, /type:\s*'SET_FREE_REUSABLE_PHONE'/);
|
||||
assert.match(sidepanelSource, /payload:\s*\{\s*phoneNumber\s*\}/s);
|
||||
assert.match(sidepanelSource, /type:\s*'CLEAR_FREE_REUSABLE_PHONE'/);
|
||||
});
|
||||
|
||||
test('sidepanel keeps free reuse switches realtime and locks them during auto run', () => {
|
||||
assert.match(
|
||||
sidepanelSource,
|
||||
/message\.payload\.freePhoneReuseEnabled !== undefined[\s\S]*updatePhoneVerificationSettingsUI\(\);/
|
||||
);
|
||||
assert.match(
|
||||
sidepanelSource,
|
||||
/message\.payload\.freePhoneReuseAutoEnabled !== undefined[\s\S]*updatePhoneVerificationSettingsUI\(\);/
|
||||
);
|
||||
assert.match(sidepanelSource, /setFreePhoneReuseControlsLocked\(settingsCardLocked\);/);
|
||||
assert.match(
|
||||
sidepanelSource,
|
||||
/inputFreePhoneReuseEnabled\.disabled = locked;[\s\S]*inputFreePhoneReuseAutoEnabled\.disabled = locked/
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel free reusable phone paths avoid stale identifiers and empty-save errors', () => {
|
||||
assert.doesNotMatch(
|
||||
sidepanelSource,
|
||||
/applyHeroSmsFallbackSelection\(\s*\[\.\.\.nextPrimaryCountries,\s*\.\.\.nextFallback\]/
|
||||
);
|
||||
assert.match(
|
||||
sidepanelSource,
|
||||
/applyHeroSmsFallbackSelection\(\s*\[\s*nextPrimary,\s*\.\.\.nextFallback\]/
|
||||
);
|
||||
assert.match(
|
||||
sidepanelSource,
|
||||
/if \(!phoneNumber\) \{[\s\S]*请先填写白嫖复用手机号[\s\S]*return;[\s\S]*chrome\.runtime\.sendMessage\(\{\s*type:\s*'SET_FREE_REUSABLE_PHONE'/
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel source wires runtime signup phone field to background sync messages', () => {
|
||||
assert.match(sidepanelSource, /function getRuntimeSignupPhoneValue\(state = latestState\)/);
|
||||
assert.match(sidepanelSource, /function shouldExecuteStep3WithSignupPhoneIdentity\(state = latestState\)/);
|
||||
@@ -494,6 +549,9 @@ function updateSignupMethodUI() {
|
||||
function syncSignupPhoneInputFromState() {
|
||||
rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none';
|
||||
}
|
||||
function setFreePhoneReuseControlsLocked() {}
|
||||
function isAutoRunLockedPhase() { return false; }
|
||||
function isAutoRunScheduledPhase() { return false; }
|
||||
|
||||
${extractFunction('updatePhoneVerificationSettingsUI')}
|
||||
|
||||
@@ -686,6 +744,8 @@ const inputAutoDelayEnabled = { checked: false };
|
||||
const inputAutoDelayMinutes = { value: '30' };
|
||||
const inputAutoStepDelaySeconds = { value: '' };
|
||||
const inputPhoneVerificationEnabled = { checked: true };
|
||||
const inputFreePhoneReuseEnabled = { checked: true };
|
||||
const inputFreePhoneReuseAutoEnabled = { checked: true };
|
||||
const selectPhoneSmsProvider = { value: 'hero-sms' };
|
||||
const inputVerificationResendCount = { value: '4' };
|
||||
const inputHeroSmsApiKey = { value: 'demo-key' };
|
||||
@@ -832,6 +892,8 @@ return { collectSettingsPayload };
|
||||
assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]);
|
||||
assert.equal(payload.nexSmsServiceCode, 'ot');
|
||||
assert.equal(payload.heroSmsReuseEnabled, true);
|
||||
assert.equal(payload.freePhoneReuseEnabled, true);
|
||||
assert.equal(payload.freePhoneReuseAutoEnabled, true);
|
||||
assert.equal(payload.heroSmsAcquirePriority, 'price');
|
||||
assert.equal(payload.heroSmsMaxPrice, '0.12');
|
||||
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
|
||||
|
||||
@@ -257,24 +257,20 @@ return {
|
||||
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
|
||||
assert.match(api.plusPaymentMethodCaption.textContent, /GPC/);
|
||||
|
||||
api.selectGpcHelperOtpChannel.value = 'sms';
|
||||
api.updatePlusModeUI();
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
|
||||
|
||||
api.inputGpcHelperLocalSmsEnabled.checked = true;
|
||||
api.updatePlusModeUI();
|
||||
assert.equal(api.selectGpcHelperOtpChannel.value, 'whatsapp');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
|
||||
|
||||
api.selectGpcHelperOtpChannel.value = 'whatsapp';
|
||||
api.selectGpcHelperOtpChannel.value = 'sms';
|
||||
api.updatePlusModeUI();
|
||||
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, false);
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
|
||||
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, true);
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
|
||||
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
|
||||
|
||||
api.selectPlusPaymentMethod.value = 'gopay';
|
||||
api.updatePlusModeUI();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
test('sidepanel exposes SUB2API account priority below group setting', () => {
|
||||
assert.match(html, /id="row-sub2api-account-priority"/);
|
||||
assert.match(html, /id="input-sub2api-account-priority"/);
|
||||
assert.match(html, /<span class="data-label">优先级<\/span>/);
|
||||
const inputTag = html.match(/<input[^>]*id="input-sub2api-account-priority"[^>]*>/)?.[0] || '';
|
||||
assert.match(inputTag, /type="number"/);
|
||||
assert.match(inputTag, /min="1"/);
|
||||
assert.match(inputTag, /step="1"/);
|
||||
assert.ok(
|
||||
html.indexOf('id="row-sub2api-account-priority"') > html.indexOf('id="row-sub2api-group"'),
|
||||
'priority row should be placed below the SUB2API group row'
|
||||
);
|
||||
assert.ok(
|
||||
html.indexOf('id="row-sub2api-account-priority"') < html.indexOf('id="row-sub2api-default-proxy"'),
|
||||
'priority row should remain above the SUB2API default proxy row'
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel persists and locks SUB2API account priority setting', () => {
|
||||
assert.match(
|
||||
source,
|
||||
/const rowSub2ApiAccountPriority = document\.getElementById\('row-sub2api-account-priority'\);/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const inputSub2ApiAccountPriority = document\.getElementById\('input-sub2api-account-priority'\);/
|
||||
);
|
||||
assert.match(source, /function normalizeSub2ApiAccountPriorityValue\(/);
|
||||
assert.match(source, /const sub2apiAccountPriorityNormalizer = typeof normalizeSub2ApiAccountPriorityValue === 'function'/);
|
||||
assert.match(source, /sub2apiAccountPriority: sub2apiAccountPriorityNormalizer\(/);
|
||||
assert.match(
|
||||
source,
|
||||
/inputSub2ApiAccountPriority\.value = String\(normalizeSub2ApiAccountPriorityValue\(state\?\.sub2apiAccountPriority\)\);/
|
||||
);
|
||||
assert.match(source, /rowSub2ApiAccountPriority\.style\.display = useSub2Api \? '' : 'none';/);
|
||||
assert.match(source, /inputSub2ApiAccountPriority\.disabled = locked;/);
|
||||
assert.match(
|
||||
source,
|
||||
/inputSub2ApiAccountPriority\.addEventListener\('input', \(\) => \{[\s\S]*scheduleSettingsAutoSave\(\);[\s\S]*\}\);/
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/inputSub2ApiAccountPriority\.addEventListener\('blur', \(\) => \{[\s\S]*saveSettings\(\{ silent: true \}\)/
|
||||
);
|
||||
});
|
||||
@@ -277,3 +277,49 @@ test('SUB2API step 10 omits proxy_id when no proxy is configured', async () => {
|
||||
assert.equal(Object.hasOwn(exchangeCall.body, 'proxy_id'), false);
|
||||
assert.equal(Object.hasOwn(createCall.body, 'proxy_id'), false);
|
||||
});
|
||||
|
||||
test('SUB2API step 10 creates account with configured account priority', async () => {
|
||||
const fetchCalls = [];
|
||||
const context = createSub2ApiPanelContext(fetchCalls);
|
||||
|
||||
await vm.runInContext(`
|
||||
step9_submitOpenAiCallback({
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiGroupId: 5,
|
||||
sub2apiAccountPriority: 3
|
||||
})
|
||||
`, context);
|
||||
|
||||
const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
|
||||
assert.equal(createCall.body.priority, 3);
|
||||
});
|
||||
|
||||
test('SUB2API account priority must be an integer greater than or equal to 1', async () => {
|
||||
const fetchCalls = [];
|
||||
const context = createSub2ApiPanelContext(fetchCalls);
|
||||
|
||||
await assert.rejects(
|
||||
() => vm.runInContext(`
|
||||
step9_submitOpenAiCallback({
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
sub2apiUrl: 'https://sub.example/admin/accounts',
|
||||
sub2apiEmail: 'admin@example.com',
|
||||
sub2apiPassword: 'secret',
|
||||
sub2apiGroupName: 'codex',
|
||||
sub2apiSessionId: 'session-1',
|
||||
sub2apiOAuthState: 'oauth-state',
|
||||
sub2apiGroupId: 5,
|
||||
sub2apiAccountPriority: 0
|
||||
})
|
||||
`, context),
|
||||
/SUB2API 账号优先级必须是大于等于 1 的整数/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalls.some((call) => call.path === '/api/v1/admin/accounts'), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user