sub2api-panel-proxy.test.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. const test = require('node:test');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const vm = require('node:vm');
  5. function createJsonResponse(payload, status = 200) {
  6. return {
  7. ok: status >= 200 && status < 300,
  8. status,
  9. text: async () => JSON.stringify(payload),
  10. };
  11. }
  12. function createSub2ApiPanelContext(fetchCalls = []) {
  13. const storage = new Map();
  14. const documentElement = {
  15. attrs: new Map(),
  16. getAttribute(name) {
  17. return this.attrs.get(name) || null;
  18. },
  19. setAttribute(name, value) {
  20. this.attrs.set(name, String(value));
  21. },
  22. };
  23. const context = {
  24. URL,
  25. console: { log() {} },
  26. setTimeout() {},
  27. document: { documentElement },
  28. location: {
  29. href: 'https://sub.example/admin/accounts',
  30. origin: 'https://sub.example',
  31. pathname: '/admin/accounts',
  32. replace() {},
  33. },
  34. chrome: {
  35. runtime: {
  36. onMessage: { addListener() {} },
  37. sendMessage: async () => ({
  38. email: 'flow@example.com',
  39. sub2apiDefaultProxyName: 'shadowrocket',
  40. }),
  41. },
  42. },
  43. localStorage: {
  44. setItem(key, value) { storage.set(`local:${key}`, String(value)); },
  45. removeItem(key) { storage.delete(`local:${key}`); },
  46. },
  47. sessionStorage: {
  48. removeItem(key) { storage.delete(`session:${key}`); },
  49. },
  50. log() {},
  51. reportComplete(step, payload) {
  52. context.completed.push({ step, payload });
  53. },
  54. reportReady() {},
  55. reportError() {},
  56. resetStopState() {},
  57. throwIfStopped() {},
  58. isStopError() { return false; },
  59. completed: [],
  60. fetch: async (url, options = {}) => {
  61. const parsed = new URL(url);
  62. const body = options.body ? JSON.parse(options.body) : null;
  63. fetchCalls.push({ path: parsed.pathname, search: parsed.search, method: options.method || 'GET', body });
  64. if (parsed.pathname === '/api/v1/auth/login') {
  65. return createJsonResponse({
  66. code: 0,
  67. data: {
  68. access_token: 'admin-token',
  69. refresh_token: 'refresh-admin',
  70. expires_in: 3600,
  71. user: { id: 1 },
  72. },
  73. });
  74. }
  75. if (parsed.pathname === '/api/v1/admin/groups/all') {
  76. return createJsonResponse({
  77. code: 0,
  78. data: [{ id: 5, name: 'codex', platform: 'openai' }],
  79. });
  80. }
  81. if (parsed.pathname === '/api/v1/admin/proxies/all') {
  82. return createJsonResponse({
  83. code: 0,
  84. data: [{
  85. id: 7,
  86. name: 'shadowrocket',
  87. protocol: 'socks5',
  88. host: '127.0.0.1',
  89. port: 1080,
  90. status: 'active',
  91. }],
  92. });
  93. }
  94. if (parsed.pathname === '/api/v1/admin/openai/generate-auth-url') {
  95. return createJsonResponse({
  96. code: 0,
  97. data: {
  98. auth_url: 'https://auth.openai.com/oauth?state=oauth-state',
  99. session_id: 'session-1',
  100. state: 'oauth-state',
  101. },
  102. });
  103. }
  104. if (parsed.pathname === '/api/v1/admin/openai/exchange-code') {
  105. return createJsonResponse({
  106. code: 0,
  107. data: {
  108. access_token: 'openai-access',
  109. refresh_token: 'openai-refresh',
  110. expires_at: 1770000000,
  111. email: 'flow@example.com',
  112. },
  113. });
  114. }
  115. if (parsed.pathname === '/api/v1/admin/accounts') {
  116. return createJsonResponse({
  117. code: 0,
  118. data: { id: 11 },
  119. });
  120. }
  121. return createJsonResponse({ code: 1, message: `unexpected path ${parsed.pathname}` }, 404);
  122. },
  123. };
  124. vm.createContext(context);
  125. vm.runInContext(fs.readFileSync('content/sub2api-panel.js', 'utf8'), context);
  126. return context;
  127. }
  128. test('SUB2API step 1 selects the configured default proxy before generating OAuth URL', async () => {
  129. const fetchCalls = [];
  130. const context = createSub2ApiPanelContext(fetchCalls);
  131. const result = await vm.runInContext(`
  132. step1_generateOpenAiAuthUrl({
  133. sub2apiEmail: 'admin@example.com',
  134. sub2apiPassword: 'secret',
  135. sub2apiGroupName: 'codex',
  136. sub2apiDefaultProxyName: 'shadowrocket'
  137. }, { report: false })
  138. `, context);
  139. const generateCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/generate-auth-url');
  140. assert.equal(result.sub2apiProxyId, 7);
  141. assert.equal(generateCall.body.proxy_id, 7);
  142. });
  143. test('SUB2API step 10 uses the same proxy for code exchange and account creation', async () => {
  144. const fetchCalls = [];
  145. const context = createSub2ApiPanelContext(fetchCalls);
  146. await vm.runInContext(`
  147. step9_submitOpenAiCallback({
  148. localhostUrl: 'http://localhost:1455/auth/callback?code=callback-code&state=oauth-state',
  149. sub2apiUrl: 'https://sub.example/admin/accounts',
  150. sub2apiEmail: 'admin@example.com',
  151. sub2apiPassword: 'secret',
  152. sub2apiGroupName: 'codex',
  153. sub2apiSessionId: 'session-1',
  154. sub2apiOAuthState: 'oauth-state',
  155. sub2apiGroupId: 5,
  156. sub2apiProxyId: 7
  157. })
  158. `, context);
  159. const exchangeCall = fetchCalls.find((call) => call.path === '/api/v1/admin/openai/exchange-code');
  160. const createCall = fetchCalls.find((call) => call.path === '/api/v1/admin/accounts');
  161. assert.equal(exchangeCall.body.proxy_id, 7);
  162. assert.equal(createCall.body.proxy_id, 7);
  163. assert.equal(createCall.body.group_ids[0], 5);
  164. assert.equal(context.completed[0].step, 10);
  165. });