222 lines
8.5 KiB
JavaScript
222 lines
8.5 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
function jwt(payload) {
|
|
const header = Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url');
|
|
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
return `${header}.${body}.signature`;
|
|
}
|
|
|
|
(async () => {
|
|
const appSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
|
|
const indexSource = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
|
|
const objectUrls = new Map();
|
|
const downloads = [];
|
|
let objectUrlIndex = 0;
|
|
|
|
class TestBlob {
|
|
constructor(parts, options = {}) {
|
|
this.parts = parts;
|
|
this.type = options.type || '';
|
|
}
|
|
}
|
|
|
|
const context = vm.createContext({
|
|
console, Set, Map, Date, JSON, Math, String, Number, Object, Array, RegExp, Promise, Uint8Array,
|
|
TextDecoder, setTimeout, clearTimeout,
|
|
atob: value => Buffer.from(value, 'base64').toString('binary'),
|
|
Blob: TestBlob,
|
|
URL: {
|
|
createObjectURL(blob) {
|
|
const url = `blob:test/${++objectUrlIndex}`;
|
|
objectUrls.set(url, blob);
|
|
return url;
|
|
},
|
|
revokeObjectURL() {},
|
|
},
|
|
navigator: { clipboard: { async writeText() {} } },
|
|
document: {
|
|
createElement(tag) {
|
|
if (tag === 'a') {
|
|
return {
|
|
href: '',
|
|
download: '',
|
|
click() {
|
|
downloads.push({ filename: this.download, blob: objectUrls.get(this.href) });
|
|
},
|
|
};
|
|
}
|
|
return { value: '', select() {} };
|
|
},
|
|
body: { appendChild() {}, removeChild() {} },
|
|
execCommand() { return true; },
|
|
},
|
|
});
|
|
vm.runInContext(`${appSource}\nthis.__geminiApp = geminiApp;`, context, { filename: 'public/app.js' });
|
|
|
|
const email = 'user@example.com';
|
|
const accessToken = jwt({
|
|
client_id: 'client-1',
|
|
exp: 1893456000,
|
|
'https://api.openai.com/profile': { email },
|
|
'https://api.openai.com/auth': {
|
|
chatgpt_account_id: 'account-1',
|
|
chatgpt_user_id: 'chat-user-1',
|
|
poid: 'organization-1',
|
|
chatgpt_plan_type: 'plus',
|
|
},
|
|
});
|
|
const idToken = jwt({
|
|
email,
|
|
'https://api.openai.com/auth': {
|
|
chatgpt_account_id: 'account-1',
|
|
chatgpt_user_id: 'chat-user-1',
|
|
chatgpt_plan_type: 'plus',
|
|
organizations: [{ id: 'organization-1', is_default: true }],
|
|
},
|
|
});
|
|
const authJson = {
|
|
access_token: accessToken,
|
|
refresh_token: 'refresh-1',
|
|
id_token: idToken,
|
|
email,
|
|
};
|
|
|
|
const app = context.__geminiApp();
|
|
assert.strictEqual(app.exportMode.sub2api_json, false, 'sub2api JSON导出默认应关闭');
|
|
assert.match(
|
|
indexSource,
|
|
/x-model="exportMode\.sub2api_json"[^>]*>\s*导出 sub2api JSON/,
|
|
'导出弹窗应显示sub2api JSON勾选项',
|
|
);
|
|
|
|
app.accounts = [{ email }];
|
|
app.selectedEmails = new Set([email]);
|
|
app.exportMode.sub2api_json = true;
|
|
app.exportSelectedAccounts();
|
|
assert.strictEqual(app.exportMode.sub2api_json, false, '重新打开导出弹窗应恢复sub2api默认状态');
|
|
|
|
const built = app.buildSub2ApiExport(
|
|
[{ email, openai_auth_json: JSON.stringify(authJson) }],
|
|
new Date('2026-08-21T15:33:41Z'),
|
|
);
|
|
const payload = JSON.parse(JSON.stringify(built.data));
|
|
assert.strictEqual(built.skippedCount, 0);
|
|
assert.deepStrictEqual(Object.keys(payload), ['exported_at', 'proxies', 'accounts']);
|
|
assert.strictEqual(payload.exported_at, '2026-08-21T15:33:41Z');
|
|
assert.deepStrictEqual(payload.proxies, []);
|
|
assert.strictEqual(payload.accounts.length, 1);
|
|
const exportedAccount = payload.accounts[0];
|
|
assert.strictEqual(exportedAccount.name, email);
|
|
assert.strictEqual(exportedAccount.platform, 'openai');
|
|
assert.strictEqual(exportedAccount.type, 'oauth');
|
|
assert.strictEqual(exportedAccount.concurrency, 100);
|
|
assert.strictEqual(exportedAccount.priority, 1);
|
|
assert.strictEqual(exportedAccount.rate_multiplier, 1);
|
|
assert.strictEqual(exportedAccount.auto_pause_on_expired, true);
|
|
assert.strictEqual(exportedAccount.credentials.access_token, accessToken);
|
|
assert.strictEqual(exportedAccount.credentials.refresh_token, 'refresh-1');
|
|
assert.strictEqual(exportedAccount.credentials.id_token, idToken);
|
|
assert.strictEqual(exportedAccount.credentials.client_id, 'client-1');
|
|
assert.strictEqual(exportedAccount.credentials.email, email);
|
|
assert.strictEqual(exportedAccount.credentials.expires_at, 1893456000);
|
|
assert.strictEqual(exportedAccount.credentials.chatgpt_account_id, 'account-1');
|
|
assert.strictEqual(exportedAccount.credentials.chatgpt_user_id, 'chat-user-1');
|
|
assert.strictEqual(exportedAccount.credentials.organization_id, 'organization-1');
|
|
assert.strictEqual(exportedAccount.credentials.plan_type, 'plus');
|
|
assert.deepStrictEqual(exportedAccount.extra, { email });
|
|
assert.strictEqual('subscription_expires_at' in exportedAccount.credentials, false, '不得伪造订阅到期时间');
|
|
assert.strictEqual('codex_usage_updated_at' in exportedAccount.extra, false, '不得伪造用量统计');
|
|
|
|
const mixed = app.buildSub2ApiExport([
|
|
{ email: 'broken-json@example.com', openai_auth_json: '{invalid' },
|
|
{ email: 'missing-refresh@example.com', openai_auth_json: JSON.stringify({ access_token: accessToken }) },
|
|
{ email, openai_auth_json: authJson },
|
|
]);
|
|
assert.strictEqual(mixed.skippedCount, 2, '损坏或缺少关键令牌的账号必须跳过');
|
|
assert.strictEqual(mixed.data.accounts.length, 1);
|
|
|
|
const numericExpiry = app.buildSub2ApiExport([{
|
|
email,
|
|
openai_auth_json: {
|
|
access_token: 'opaque-access-token',
|
|
refresh_token: 'opaque-refresh-token',
|
|
expires_at: 1893456000000,
|
|
},
|
|
}]);
|
|
assert.strictEqual(numericExpiry.data.accounts[0].credentials.expires_at, 1893456000);
|
|
assert.strictEqual(
|
|
'client_id' in numericExpiry.data.accounts[0].credentials,
|
|
false,
|
|
'授权数据没有client_id时不得写入固定值',
|
|
);
|
|
|
|
Object.keys(app.exportFields).forEach(key => { app.exportFields[key] = false; });
|
|
app._exportList = [{ email }];
|
|
app.exportMode = {
|
|
file: false, csv: false, sub2api_json: true, clipboard: false, staging: false,
|
|
};
|
|
app.showExportModal = true;
|
|
const logs = [];
|
|
app.addLog = (message, type) => logs.push({ message, type });
|
|
let authLoadCount = 0;
|
|
app.loadOpenAIAuthForExport = async accounts => {
|
|
authLoadCount++;
|
|
return {
|
|
accounts: accounts.map(account => ({ ...account, openai_auth_json: JSON.stringify(authJson) })),
|
|
missingCount: 0,
|
|
};
|
|
};
|
|
|
|
await app.confirmExport();
|
|
assert.strictEqual(authLoadCount, 1, '只选sub2api时应读取一次授权JSON');
|
|
assert.strictEqual(downloads.length, 1, '只选sub2api且未选字段时也应下载');
|
|
assert.match(downloads[0].filename, /^sub2api-account-\d{14}\.json$/);
|
|
assert.strictEqual(downloads[0].blob.type, 'application/json;charset=utf-8');
|
|
const downloaded = JSON.parse(downloads[0].blob.parts.join(''));
|
|
assert.strictEqual(downloaded.accounts.length, 1);
|
|
assert.strictEqual(downloaded.accounts[0].credentials.email, email);
|
|
assert.strictEqual(app.showExportModal, false);
|
|
assert.ok(logs.some(item => item.type === 'success' && /sub2api JSON/.test(item.message)));
|
|
|
|
downloads.length = 0;
|
|
authLoadCount = 0;
|
|
logs.length = 0;
|
|
app.exportFields.email = true;
|
|
app.showExportModal = true;
|
|
app.exportMode = {
|
|
file: false, csv: true, sub2api_json: true, clipboard: false, staging: false,
|
|
};
|
|
await app.confirmExport();
|
|
assert.strictEqual(authLoadCount, 1, 'CSV与sub2api同时导出时授权数据只读取一次');
|
|
assert.deepStrictEqual(
|
|
downloads.map(item => path.extname(item.filename)).sort(),
|
|
['.csv', '.json'],
|
|
);
|
|
|
|
downloads.length = 0;
|
|
logs.length = 0;
|
|
app.showExportModal = true;
|
|
Object.keys(app.exportFields).forEach(key => { app.exportFields[key] = false; });
|
|
app.exportMode = {
|
|
file: false, csv: false, sub2api_json: true, clipboard: false, staging: false,
|
|
};
|
|
app.loadOpenAIAuthForExport = async accounts => ({
|
|
accounts: accounts.map(account => ({ ...account, openai_auth_json: '' })),
|
|
missingCount: accounts.length,
|
|
});
|
|
await app.confirmExport();
|
|
assert.strictEqual(downloads.length, 0, '没有有效授权账号时不得生成空sub2api文件');
|
|
assert.strictEqual(app.showExportModal, true, '未生成文件时应保留弹窗供用户调整');
|
|
assert.ok(logs.some(item => item.type === 'error' && /未生成 sub2api 文件/.test(item.message)));
|
|
|
|
console.log('account_sub2api_export.test.js: PASS');
|
|
})().catch(error => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|