fix: resolve HeroSMS Plus dev sync conflicts
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background step-1 state plumbing persists and resets cpa oauth runtime keys', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
assert.match(source, /cpaOAuthState:\s*null/);
|
||||
assert.match(source, /cpaManagementOrigin:\s*null/);
|
||||
assert.match(source, /payload\.cpaOAuthState[^\n]*updates\.cpaOAuthState/);
|
||||
assert.match(source, /payload\.cpaManagementOrigin[^\n]*updates\.cpaManagementOrigin/);
|
||||
assert.match(source, /if \(step <= 1\) \{[\s\S]*cpaOAuthState:\s*null,[\s\S]*cpaManagementOrigin:\s*null,/);
|
||||
});
|
||||
|
||||
test('message router step-1 handler stores cpa oauth runtime keys', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
|
||||
const updates = [];
|
||||
const router = api.createMessageRouter({
|
||||
broadcastDataUpdate: () => {},
|
||||
setState: async (payload) => {
|
||||
updates.push(payload);
|
||||
},
|
||||
});
|
||||
|
||||
await router.handleStepData(1, {
|
||||
cpaOAuthState: 'oauth-state-1',
|
||||
cpaManagementOrigin: 'http://localhost:8317',
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(updates, [
|
||||
{
|
||||
cpaOAuthState: 'oauth-state-1',
|
||||
cpaManagementOrigin: 'http://localhost:8317',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
|
||||
test('message router appends success record on Plus final step instead of hard-coded step 10', async () => {
|
||||
const appendCalls = [];
|
||||
const router = api.createMessageRouter({
|
||||
addLog: async () => {},
|
||||
appendAccountRunRecord: async (...args) => {
|
||||
appendCalls.push(args);
|
||||
},
|
||||
batchUpdateLuckmailPurchases: async () => {},
|
||||
buildLocalhostCleanupPrefix: () => '',
|
||||
buildLuckmailSessionSettingsPayload: () => ({}),
|
||||
buildPersistentSettingsPayload: () => ({}),
|
||||
broadcastDataUpdate: () => {},
|
||||
cancelScheduledAutoRun: async () => {},
|
||||
checkIcloudSession: async () => {},
|
||||
clearAutoRunTimerAlarm: async () => {},
|
||||
clearLuckmailRuntimeState: async () => {},
|
||||
clearStopRequest: () => {},
|
||||
closeLocalhostCallbackTabs: async () => {},
|
||||
closeTabsByUrlPrefix: async () => {},
|
||||
deleteHotmailAccount: async () => {},
|
||||
deleteHotmailAccounts: async () => {},
|
||||
deleteIcloudAlias: async () => {},
|
||||
deleteUsedIcloudAliases: async () => {},
|
||||
disableUsedLuckmailPurchases: async () => {},
|
||||
doesStepUseCompletionSignal: () => false,
|
||||
ensureManualInteractionAllowed: async () => ({}),
|
||||
executeStep: async () => {},
|
||||
executeStepViaCompletionSignal: async () => {},
|
||||
exportSettingsBundle: async () => ({}),
|
||||
fetchGeneratedEmail: async () => '',
|
||||
finalizeStep3Completion: async () => {},
|
||||
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
|
||||
findHotmailAccount: async () => null,
|
||||
flushCommand: async () => {},
|
||||
getCurrentLuckmailPurchase: () => null,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getSourceLabel: () => '',
|
||||
getState: async () => ({ plusModeEnabled: true, stepStatuses: { 13: 'pending' } }),
|
||||
getLastStepIdForState: () => 13,
|
||||
getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'oauth-login' : 'platform-verify' }),
|
||||
getStepIdsForState: () => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
||||
getTabId: async () => null,
|
||||
getStopRequested: () => false,
|
||||
handleAutoRunLoopUnhandledError: async () => {},
|
||||
handleCloudflareSecurityBlocked: async () => '',
|
||||
importSettingsBundle: async () => {},
|
||||
invalidateDownstreamAfterStepRestart: async () => {},
|
||||
isCloudflareSecurityBlockedError: () => false,
|
||||
isAutoRunLockedState: () => false,
|
||||
isHotmailProvider: () => false,
|
||||
isLocalhostOAuthCallbackUrl: () => true,
|
||||
isLuckmailProvider: () => false,
|
||||
isStopError: () => false,
|
||||
isTabAlive: async () => false,
|
||||
launchAutoRunTimerPlan: async () => {},
|
||||
listIcloudAliases: async () => [],
|
||||
listLuckmailPurchasesForManagement: async () => [],
|
||||
normalizeHotmailAccounts: (items) => items,
|
||||
normalizeRunCount: (value) => value,
|
||||
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
|
||||
notifyStepComplete: () => {},
|
||||
notifyStepError: () => {},
|
||||
patchHotmailAccount: async () => {},
|
||||
patchMail2925Account: async () => {},
|
||||
registerTab: async () => {},
|
||||
requestStop: async () => {},
|
||||
resetState: async () => {},
|
||||
resumeAutoRun: async () => {},
|
||||
scheduleAutoRun: async () => {},
|
||||
selectLuckmailPurchase: async () => {},
|
||||
setCurrentHotmailAccount: async () => {},
|
||||
setCurrentMail2925Account: async () => {},
|
||||
setContributionMode: async () => {},
|
||||
setEmailState: async () => {},
|
||||
setEmailStateSilently: async () => {},
|
||||
setIcloudAliasPreservedState: async () => {},
|
||||
setIcloudAliasUsedState: async () => {},
|
||||
setLuckmailPurchaseDisabledState: async () => {},
|
||||
setLuckmailPurchasePreservedState: async () => {},
|
||||
setLuckmailPurchaseUsedState: async () => {},
|
||||
setPersistentSettings: async () => {},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
skipAutoRunCountdown: async () => false,
|
||||
skipStep: async () => {},
|
||||
startAutoRunLoop: async () => {},
|
||||
syncHotmailAccounts: async () => {},
|
||||
testHotmailAccountMailAccess: async () => {},
|
||||
upsertHotmailAccount: async () => {},
|
||||
verifyHotmailAccount: async () => {},
|
||||
});
|
||||
|
||||
await router.handleMessage({ type: 'STEP_COMPLETE', step: 13, payload: {} }, {});
|
||||
|
||||
assert.equal(appendCalls.length, 1);
|
||||
assert.equal(appendCalls[0][0], 'success');
|
||||
});
|
||||
@@ -65,6 +65,7 @@ function createRouter(overrides = {}) {
|
||||
getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
|
||||
getStepDefinitionForState: overrides.getStepDefinitionForState,
|
||||
getStepIdsForState: overrides.getStepIdsForState,
|
||||
getLastStepIdForState: overrides.getLastStepIdForState,
|
||||
getTabId: overrides.getTabId || (async () => null),
|
||||
getStopRequested: () => false,
|
||||
handleAutoRunLoopUnhandledError: async () => {},
|
||||
|
||||
@@ -71,3 +71,48 @@ test('panel bridge can request codex2api oauth url via protocol', async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('panel bridge can request cpa oauth url via management api', async () => {
|
||||
const source = fs.readFileSync('background/panel-bridge.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:8317/v0/management/codex-auth-url');
|
||||
assert.equal(options.method, 'GET');
|
||||
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
|
||||
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
status: 'ok',
|
||||
url: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
|
||||
state: 'cpa-oauth-state',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundPanelBridge;`)({});
|
||||
const bridge = api.createPanelBridge({
|
||||
addLog: async () => {},
|
||||
getPanelMode: () => 'cpa',
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
DEFAULT_SUB2API_GROUP_NAME: 'codex',
|
||||
SUB2API_STEP1_RESPONSE_TIMEOUT_MS: 90000,
|
||||
});
|
||||
|
||||
const result = await bridge.requestOAuthUrlFromPanel({
|
||||
panelMode: 'cpa',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
}, { logLabel: '步骤 7' });
|
||||
|
||||
assert.deepStrictEqual(result, {
|
||||
oauthUrl: 'https://auth.openai.com/authorize?state=cpa-oauth-state',
|
||||
cpaOAuthState: 'cpa-oauth-state',
|
||||
cpaManagementOrigin: 'http://localhost:8317',
|
||||
});
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const test = require('node:test');
|
||||
|
||||
function createDeps(overrides = {}) {
|
||||
const logs = [];
|
||||
const completed = [];
|
||||
const uiCalls = [];
|
||||
|
||||
const deps = {
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
closeConflictingTabsForSource: async () => {},
|
||||
completeStepFromBackground: async (step, payload) => {
|
||||
completed.push({ step, payload });
|
||||
},
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
getPanelMode: () => 'cpa',
|
||||
getTabId: async () => 0,
|
||||
isLocalhostOAuthCallbackUrl: (value) => String(value || '').includes('/auth/callback?code='),
|
||||
isTabAlive: async () => false,
|
||||
normalizeCodex2ApiUrl: (value) => value,
|
||||
normalizeSub2ApiUrl: (value) => value,
|
||||
rememberSourceLastUrl: async () => {},
|
||||
reuseOrCreateTab: async () => 91,
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToContentScriptResilient: async (source, message, options) => {
|
||||
uiCalls.push({ source, message, options });
|
||||
return {};
|
||||
},
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
SUB2API_STEP9_RESPONSE_TIMEOUT_MS: 120000,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return { deps, logs, completed, uiCalls };
|
||||
}
|
||||
|
||||
test('platform verify module submits CPA callback via management API first', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
let uiCalled = false;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:8317/v0/management/oauth-callback');
|
||||
assert.equal(options.method, 'POST');
|
||||
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
|
||||
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
|
||||
assert.deepStrictEqual(JSON.parse(options.body), {
|
||||
provider: 'codex',
|
||||
redirect_url: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
message: 'CPA API 回调提交成功',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, logs, completed } = createDeps({
|
||||
sendToContentScriptResilient: async () => {
|
||||
uiCalled = true;
|
||||
return {};
|
||||
},
|
||||
});
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
});
|
||||
|
||||
assert.equal(uiCalled, false);
|
||||
assert.deepStrictEqual(completed, [
|
||||
{
|
||||
step: 10,
|
||||
payload: {
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
verifiedStatus: 'CPA API 回调提交成功',
|
||||
},
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(logs, [
|
||||
{ message: '步骤 10:正在通过 CPA 管理接口提交回调地址...', level: 'info' },
|
||||
{ message: '步骤 10:CPA API 回调提交成功', level: 'ok' },
|
||||
]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module prefers cpaManagementOrigin when provided', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
assert.equal(url, 'http://localhost:9999/v0/management/oauth-callback');
|
||||
assert.equal(options.method, 'POST');
|
||||
assert.equal(options.headers.Authorization, 'Bearer cpa-key');
|
||||
assert.equal(options.headers['X-Management-Key'], 'cpa-key');
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
message: 'CPA API 回调提交成功',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, completed } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
cpaManagementOrigin: 'http://localhost:9999',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
});
|
||||
|
||||
assert.equal(completed.length, 1);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module fails fast when CPA API submit fails', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => ({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: async () => ({ message: 'failed to persist oauth callback' }),
|
||||
});
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, logs, completed, uiCalls } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
}),
|
||||
/failed to persist oauth callback/
|
||||
);
|
||||
|
||||
assert.equal(uiCalls.length, 0);
|
||||
assert.equal(completed.length, 0);
|
||||
assert.equal(logs[0].message, '步骤 10:正在通过 CPA 管理接口提交回调地址...');
|
||||
assert.match(logs[1].message, /步骤 10:CPA 接口提交失败:failed to persist oauth callback/);
|
||||
assert.equal(logs[1].level, 'error');
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module requires management key for CPA API-only flow', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalled = true;
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps, logs, completed, uiCalls } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: ' ',
|
||||
}),
|
||||
/尚未配置 CPA 管理密钥/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalled, false);
|
||||
assert.equal(uiCalls.length, 0);
|
||||
assert.equal(completed.length, 0);
|
||||
assert.equal(logs.length, 0);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('platform verify module rejects callback when cpa oauth state mismatches', async () => {
|
||||
const source = fs.readFileSync('background/steps/platform-verify.js', 'utf8');
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalled = true;
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({ message: 'should not happen' }),
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep10;`)({});
|
||||
const { deps } = createDeps();
|
||||
const executor = api.createStep10Executor(deps);
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executeStep10({
|
||||
panelMode: 'cpa',
|
||||
localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=callback-state',
|
||||
cpaOAuthState: 'expected-state',
|
||||
vpsUrl: 'http://localhost:8317/admin/oauth',
|
||||
vpsPassword: 'cpa-key',
|
||||
}),
|
||||
/CPA 回调 state 与当前授权会话不匹配/
|
||||
);
|
||||
|
||||
assert.equal(fetchCalled, false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user