Files
FlowPilot/tests/background-cpa-api.test.js
T
2026-05-19 16:56:08 +08:00

185 lines
6.0 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function createJsonResponse(payload, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: async () => payload,
};
}
function loadCpaApiModule() {
const source = fs.readFileSync('background/cpa-api.js', 'utf8');
return new Function('self', `${source}; return self.MultiPageBackgroundCpaApi;`)({});
}
function encodeBase64UrlJson(value) {
return Buffer.from(JSON.stringify(value)).toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
}
function createJwtToken(payload = {}) {
return [
encodeBase64UrlJson({ alg: 'HS256', typ: 'JWT' }),
encodeBase64UrlJson(payload),
'signature',
].join('.');
}
test('cpa api imports current ChatGPT session through management auth-files endpoint', async () => {
const apiModule = loadCpaApiModule();
const logs = [];
const fetchCalls = [];
const expiresAt = '2026-05-20T12:34:56.000Z';
const accessToken = createJwtToken({
exp: Math.floor(Date.parse(expiresAt) / 1000),
email: 'jwt@example.com',
'https://api.openai.com/auth': {
chatgpt_account_id: 'acct_123',
chatgpt_plan_type: 'plus',
chatgpt_user_id: 'user_123',
},
'https://api.openai.com/profile': {
email: 'profile@example.com',
},
});
const api = apiModule.createCpaApi({
addLog: async (message, level = 'info', options = {}) => {
logs.push({ message, level, step: options.step, stepKey: options.stepKey });
},
fetchImpl: async (url, options = {}) => {
const parsed = new URL(url);
const body = options.body ? JSON.parse(options.body) : null;
fetchCalls.push({
path: parsed.pathname,
search: parsed.search,
method: options.method || 'POST',
headers: options.headers || {},
body,
});
if (parsed.pathname === '/v0/management/auth-files') {
return createJsonResponse({});
}
return createJsonResponse({ message: `unexpected path ${parsed.pathname}` }, 404);
},
});
const result = await api.importCurrentChatGptSession({
vpsUrl: 'https://cpa.example.com/management.html#/oauth',
vpsPassword: 'management-key',
session: {
accessToken,
expires: expiresAt,
user: {
email: 'flow@example.com',
},
account: {
id: 'acct_123',
planType: 'plus',
},
},
accessToken,
email: 'registration@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'identifier@example.com',
}, {
logLabel: '步骤 10',
logOptions: { step: 10, stepKey: 'cpa-session-import' },
});
const importCall = fetchCalls.find((call) => call.path === '/v0/management/auth-files');
assert.ok(importCall, 'expected CPA auth-files import call');
assert.equal(importCall.method, 'POST');
assert.equal(importCall.headers.Authorization, 'Bearer management-key');
assert.equal(importCall.headers['X-Management-Key'], 'management-key');
assert.equal(
decodeURIComponent(new URLSearchParams(importCall.search).get('name')),
'codex-flow@example.com-plus.json'
);
assert.equal(importCall.body.type, 'codex');
assert.equal(importCall.body.account_id, 'acct_123');
assert.equal(importCall.body.chatgpt_account_id, 'acct_123');
assert.equal(importCall.body.email, 'flow@example.com');
assert.equal(importCall.body.plan_type, 'plus');
assert.equal(importCall.body.chatgpt_plan_type, 'plus');
assert.equal(importCall.body.access_token, accessToken);
assert.equal(importCall.body.id_token_synthetic, true);
assert.match(String(importCall.body.id_token || ''), /\.synthetic$/);
assert.equal(importCall.body.expired, expiresAt);
assert.equal(Object.prototype.hasOwnProperty.call(importCall.body, 'refresh_token'), false);
assert.equal(result.verifiedStatus, 'CPA 会话导入完成:flow@example.com');
assert.equal(
logs.some((entry) => entry.level === 'warn' && /refresh_token/.test(entry.message)),
true
);
});
test('cpa api falls back to registration email when session has no readable email', () => {
const apiModule = loadCpaApiModule();
const accessToken = createJwtToken({
exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
'https://api.openai.com/auth': {
chatgpt_account_id: 'acct_456',
chatgpt_plan_type: 'plus',
chatgpt_user_id: 'user_456',
},
});
const api = apiModule.createCpaApi();
const result = api.buildCpaSessionAuthJson({
session: {
accessToken,
user: {
name: 'Flow User',
},
},
accessToken,
email: 'registration@example.com',
accountIdentifierType: 'email',
accountIdentifier: 'identifier@example.com',
});
assert.equal(result.email, 'registration@example.com');
assert.equal(result.authJson.email, 'registration@example.com');
assert.equal(result.fileName, 'codex-registration@example.com-plus.json');
});
test('cpa api preserves provided id_token and refresh_token when available', () => {
const apiModule = loadCpaApiModule();
const accessToken = createJwtToken({
exp: Math.floor(Date.parse('2026-05-20T12:34:56.000Z') / 1000),
'https://api.openai.com/auth': {
chatgpt_account_id: 'acct_789',
chatgpt_plan_type: 'plus',
chatgpt_user_id: 'user_789',
},
});
const api = apiModule.createCpaApi();
const result = api.buildCpaSessionAuthJson({
session: {
accessToken,
user: {
email: 'session@example.com',
},
},
accessToken,
id_token: 'provided-id-token',
refresh_token: 'refresh-token-1',
session_token: 'session-token-1',
});
assert.equal(result.authJson.id_token, 'provided-id-token');
assert.equal(Object.prototype.hasOwnProperty.call(result.authJson, 'id_token_synthetic'), false);
assert.equal(result.authJson.refresh_token, 'refresh-token-1');
assert.equal(result.authJson.session_token, 'session-token-1');
assert.equal(result.hasRefreshToken, true);
});