const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); function createJsonResponse(payload, status = 200) { return { ok: status >= 200 && status < 300, status, text: async () => JSON.stringify(payload), }; } function createSub2ApiPanelContext(fetchCalls = []) { const storage = new Map(); const documentElement = { attrs: new Map(), getAttribute(name) { return this.attrs.get(name) || null; }, setAttribute(name, value) { this.attrs.set(name, String(value)); }, }; const context = { URL, console: { log() {} }, setTimeout() {}, document: { documentElement }, location: { href: 'https://sub.example/admin/accounts', origin: 'https://sub.example', pathname: '/admin/accounts', replace() {}, }, chrome: { runtime: { onMessage: { addListener() {} }, sendMessage: async () => ({ email: 'flow@example.com', sub2apiDefaultProxyName: '', }), }, }, localStorage: { setItem(key, value) { storage.set(`local:${key}`, String(value)); }, removeItem(key) { storage.delete(`local:${key}`); }, }, sessionStorage: { removeItem(key) { storage.delete(`session:${key}`); }, }, log() {}, reportComplete(nodeId, payload) { context.completed.push({ nodeId, payload }); }, reportReady() {}, reportError() {}, resetStopState() {}, throwIfStopped() {}, isStopError() { return false; }, completed: [], fetch: 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 || 'GET', body }); if (parsed.pathname === '/api/v1/auth/login') { return createJsonResponse({ code: 0, data: { access_token: 'admin-token', refresh_token: 'refresh-admin', expires_in: 3600, user: { id: 1 }, }, }); } if (parsed.pathname === '/api/v1/admin/groups/all') { return createJsonResponse({ code: 0, data: [ { id: 5, name: 'codex', platform: 'openai' }, { id: 9, name: 'codex-plus', platform: 'openai' }, ], }); } if (parsed.pathname === '/api/v1/admin/proxies/all') { return createJsonResponse({ code: 0, data: [{ id: 7, name: 'shadowrocket', protocol: 'socks5', host: '127.0.0.1', port: 1080, status: 'active', }], }); } if (parsed.pathname === '/api/v1/admin/openai/generate-auth-url') { return createJsonResponse({ code: 0, data: { auth_url: 'https://auth.openai.com/oauth?state=oauth-state', session_id: 'session-1', state: 'oauth-state', }, }); } if (parsed.pathname === '/api/v1/admin/openai/exchange-code') { return createJsonResponse({ code: 0, data: { access_token: 'openai-access', refresh_token: 'openai-refresh', expires_at: 1770000000, email: 'flow@example.com', }, }); } if (parsed.pathname === '/api/v1/admin/accounts') { return createJsonResponse({ code: 0, data: { id: 11 }, }); } return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404); }, }; vm.createContext(context); vm.runInContext(fs.readFileSync('flows/openai/content/sub2api-panel.js', 'utf8'), context); return context; } test('SUB2API step 1 selects the configured default proxy before generating OAuth URL', async () => { const fetchCalls = []; const context = createSub2ApiPanelContext(fetchCalls); const result = await vm.runInContext(` step1_generateOpenAiAuthUrl({ sub2apiEmail: 'admin@example.com', sub2apiPassword: 'secret', sub2apiGroupName: 'codex', sub2apiDefaultProxyName: 'shadowrocket' }, { report: false }) `, context); const generateCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/generate-auth-url'); assert.equal(result.sub2apiProxyId, 7); assert.equal(generateCall.body.proxy_id, 7); }); test('SUB2API step 10 uses the same proxy for code exchange and account creation', 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, sub2apiProxyId: 7 }) `, context); const exchangeCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/exchange-code'); const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts'); assert.equal(exchangeCall.body.proxy_id, 7); assert.equal(createCall.body.proxy_id, 7); assert.equal(createCall.body.group_ids[0], 5); assert.equal(context.completed[0].nodeId, 'platform-verify'); assert.equal(context.completed[0].payload.visibleStep, 10); }); test('SUB2API panel accepts Plus platform verify step 13', async () => { const fetchCalls = []; const context = createSub2ApiPanelContext(fetchCalls); await vm.runInContext(` handleStep(13, { 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 }) `, context); const exchangeCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/exchange-code'); const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts'); assert.equal(exchangeCall.body.code, 'callback-code'); assert.equal(createCall.body.group_ids[0], 5); assert.equal(context.completed[0].nodeId, 'platform-verify'); assert.equal(context.completed[0].payload.visibleStep, 13); }); test('SUB2API step 1 omits proxy_id when default proxy is empty', async () => { const fetchCalls = []; const context = createSub2ApiPanelContext(fetchCalls); const result = await vm.runInContext(` step1_generateOpenAiAuthUrl({ sub2apiEmail: 'admin@example.com', sub2apiPassword: 'secret', sub2apiGroupName: 'codex', sub2apiDefaultProxyName: ' ' }, { report: false }) `, context); const generateCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/generate-auth-url'); assert.equal(result.sub2apiProxyId, null); assert.equal(Object.hasOwn(generateCall.body, 'proxy_id'), false); }); test('SUB2API step 10 creates accounts in multiple configured groups', async () => { const fetchCalls = []; const context = createSub2ApiPanelContext(fetchCalls); const step1Result = await vm.runInContext(` step1_generateOpenAiAuthUrl({ sub2apiEmail: 'admin@example.com', sub2apiPassword: 'secret', sub2apiGroupName: 'codex, codex-plus' }, { report: false }) `, context); 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, codex-plus', sub2apiSessionId: 'session-1', sub2apiOAuthState: 'oauth-state', sub2apiGroupIds: ${JSON.stringify(step1Result.sub2apiGroupIds)} }) `, context); const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts'); assert.deepEqual(Array.from(step1Result.sub2apiGroupIds), [5, 9]); assert.deepEqual(createCall.body.group_ids, [5, 9]); }); test('SUB2API step 10 omits proxy_id when no proxy is configured', 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, sub2apiDefaultProxyName: '' }) `, context); const exchangeCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/exchange-code'); const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts'); 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); });