Make SUB2API proxy defaults available across panel flows
Expose a configurable default proxy for SUB2API, resolve the proxy once, persist the proxy_id in state, and reuse it across auth URL generation, code exchange, and account creation so one OAuth flow stays on one proxy. Constraint: Upstream GitHub permission is READ, so publication must use a fork PR Rejected: Push to origin | current GitHub viewer permission does not allow upstream branch publication Rejected: Resolve proxy independently in later steps | could drift from the proxy chosen at auth URL generation Confidence: high Scope-risk: moderate Reversibility: clean Tested: npm test passed 183/183 Not-tested: Manual browser extension flow against a live SUB2API deployment
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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: 'shadowrocket',
|
||||
}),
|
||||
},
|
||||
},
|
||||
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(step, payload) {
|
||||
context.completed.push({ step, 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' }],
|
||||
});
|
||||
}
|
||||
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('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].step, 10);
|
||||
});
|
||||
Reference in New Issue
Block a user