smoke-test.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #!/usr/bin/env node
  2. const assert = require('assert');
  3. const http = require('http');
  4. const { loadCatalog, searchEndpoints, findEndpoint } = require('../mcp/src/core/api-catalog');
  5. const { searchQiweiApis, getQiweiApiDoc, callQiweiApi } = require('../mcp/src/tools/qiwei-api-catalog-run');
  6. const {
  7. qiweiLoginStatus,
  8. qiweiLoginStart,
  9. qiweiLoginCheck,
  10. qiweiLoginVerify
  11. } = require('../mcp/src/tools/qiwei-login-run');
  12. const {
  13. qiweiSubscriptionStatus,
  14. qiweiSubscribe,
  15. qiweiSubscriptionAutoRenew
  16. } = require('../mcp/src/tools/qiwei-subscription-run');
  17. const PRIVATE_PROVIDER_NAME = ['private', 'provider'].join('-');
  18. const PRIVATE_PROVIDER_HOST = ['private', 'provider', 'invalid'].join('.');
  19. const PRIVATE_CREDENTIAL_KEY = ['token', 'Id'].join('');
  20. function assertProviderHidden(value) {
  21. const output = JSON.stringify(value);
  22. assert(!output.includes(PRIVATE_PROVIDER_NAME));
  23. assert(!output.includes(PRIVATE_PROVIDER_HOST));
  24. assert(!output.includes('server-only-guid'));
  25. assert(!output.includes('server-only-token'));
  26. }
  27. function readBody(req) {
  28. return new Promise(resolve => {
  29. let body = '';
  30. req.setEncoding('utf8');
  31. req.on('data', chunk => {
  32. body += chunk;
  33. });
  34. req.on('end', () => resolve(body ? JSON.parse(body) : undefined));
  35. });
  36. }
  37. async function startMockGateway() {
  38. const requests = [];
  39. const server = http.createServer(async (req, res) => {
  40. const body = await readBody(req);
  41. const url = new URL(req.url, 'http://localhost');
  42. requests.push({
  43. method: req.method,
  44. path: url.pathname,
  45. query: Object.fromEntries(url.searchParams.entries()),
  46. authorization: req.headers.authorization,
  47. body
  48. });
  49. let data;
  50. if (url.pathname === '/doApi') {
  51. data = {
  52. nickname: '测试账号',
  53. echoedMethod: body.method,
  54. guid: 'server-only-guid',
  55. [PRIVATE_CREDENTIAL_KEY]: 'server-only-token',
  56. provider: PRIVATE_PROVIDER_NAME,
  57. providerName: PRIVATE_PROVIDER_NAME,
  58. providerHost: PRIVATE_PROVIDER_HOST,
  59. supportUrl: `https://${PRIVATE_PROVIDER_HOST}/help`,
  60. diagnostic: `See https://${PRIVATE_PROVIDER_HOST}/diagnostic`
  61. };
  62. } else if (url.pathname === '/login/status') {
  63. data = { configured: true, online: true, statusCode: 2 };
  64. } else if (url.pathname === '/login/start') {
  65. data = { uid: body.uid, guid: 'server-only-guid', loginQrcodeBase64Data: Buffer.from('mock-png').toString('base64') };
  66. } else if (url.pathname === '/login/check') {
  67. data = { uid: body.uid, guid: 'server-only-guid', status: 2, detail: { nickname: '测试账号', userId: 'u-1' } };
  68. } else if (url.pathname === '/login/verify') {
  69. data = { accepted: true };
  70. } else if (url.pathname === '/subscribe/status') {
  71. data = {
  72. subscribed: true,
  73. seats: 2,
  74. usedSeats: 1,
  75. expireAt: '2026-08-10T00:00:00.000Z',
  76. autoRenew: true,
  77. balance: 1000
  78. };
  79. } else if (url.pathname === '/subscribe') {
  80. data = { seats: body.seats || 1, expireAt: '2026-08-10T00:00:00.000Z', autoRenew: true };
  81. } else if (url.pathname === '/subscribe/autoRenew') {
  82. data = { autoRenew: body.autoRenew };
  83. } else {
  84. res.writeHead(404, { 'Content-Type': 'application/json' });
  85. res.end(JSON.stringify({ code: 404, mess: 'not found' }));
  86. return;
  87. }
  88. res.writeHead(200, { 'Content-Type': 'application/json' });
  89. res.end(JSON.stringify({ code: 200, data }));
  90. });
  91. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  92. const address = server.address();
  93. return {
  94. requests,
  95. apiBase: `http://127.0.0.1:${address.port}`,
  96. close: () => new Promise(resolve => server.close(resolve))
  97. };
  98. }
  99. async function main() {
  100. const catalog = loadCatalog();
  101. assert(catalog.endpoints.length >= 100, 'catalog should contain 100+ endpoints');
  102. assert(catalog.gateway.baseUrl.includes('server.fmode.cn/api/qiwei'));
  103. assert(catalog.endpoints.every(endpoint => !Object.hasOwn(endpoint, 'docUrl')));
  104. assertProviderHidden(catalog);
  105. console.log(`[ok] catalog loaded: ${catalog.endpoints.length} endpoints`);
  106. const hits = searchEndpoints({ query: '发送 文本' });
  107. assert(hits.length > 0 && hits[0].module === 'msg');
  108. assert(findEndpoint('/login/getLoginQrcode'));
  109. const searchResult = await searchQiweiApis({ query: '朋友圈' });
  110. assert(searchResult.status === 'ok' && searchResult.data.endpoints.length > 0);
  111. assertProviderHidden(searchResult);
  112. const doc = await getQiweiApiDoc({ id: 'user.getProfile' });
  113. assert(doc.status === 'ok' && doc.data.callTemplate);
  114. assertProviderHidden(doc);
  115. assert(!JSON.stringify(doc).includes('"guid"'));
  116. console.log('[ok] catalog search and docs');
  117. const gateway = await startMockGateway();
  118. const common = {
  119. authToken: 'sk-test-relay-token',
  120. uid: 'qiwei-smoke-device',
  121. apiBase: gateway.apiBase,
  122. persistConfig: false
  123. };
  124. try {
  125. const call = await callQiweiApi({ ...common, id: 'user.getProfile', params: {} });
  126. assert.strictEqual(call.status, 'ok');
  127. const apiRequest = gateway.requests.at(-1);
  128. assert.strictEqual(apiRequest.path, '/doApi');
  129. assert.strictEqual(apiRequest.authorization, 'Bearer sk-test-relay-token');
  130. assert.deepStrictEqual(apiRequest.body, {
  131. uid: 'qiwei-smoke-device',
  132. method: '/user/getProfile',
  133. params: {}
  134. });
  135. assert(!JSON.stringify(apiRequest.body).includes('tokenId'));
  136. assert(!JSON.stringify(apiRequest.body).includes('guid'));
  137. assertProviderHidden(call);
  138. console.log('[ok] qiwei_api_call uses the Fmode gateway envelope without private credentials');
  139. const blocked = await callQiweiApi({ ...common, rawMethod: '/login/manualLogin', params: {} });
  140. assert.strictEqual(blocked.status, 'error');
  141. assert.strictEqual(gateway.requests.at(-1), apiRequest);
  142. const subscription = await qiweiSubscriptionStatus(common);
  143. assert.strictEqual(subscription.status, 'ok');
  144. assert.strictEqual(subscription.summary.subscribed, true);
  145. const subscribed = await qiweiSubscribe({ ...common, seats: 3 });
  146. assert.strictEqual(subscribed.status, 'ok');
  147. assert.strictEqual(gateway.requests.at(-1).body.seats, 3);
  148. const autoRenew = await qiweiSubscriptionAutoRenew({ ...common, autoRenew: false });
  149. assert.strictEqual(autoRenew.status, 'ok');
  150. assert.strictEqual(gateway.requests.at(-1).body.autoRenew, false);
  151. console.log('[ok] subscription tools use Fmode gateway routes');
  152. const status = await qiweiLoginStatus(common);
  153. assert.strictEqual(status.status, 'ok');
  154. assert.strictEqual(gateway.requests.at(-1).query.uid, common.uid);
  155. const start = await qiweiLoginStart({ ...common, openBrowser: false });
  156. assert.strictEqual(start.status, 'ok');
  157. assertProviderHidden(start);
  158. const check = await qiweiLoginCheck(common);
  159. assert.strictEqual(check.status, 'ok');
  160. assert.strictEqual(check.summary.loggedIn, true);
  161. assertProviderHidden(check);
  162. const verify = await qiweiLoginVerify({ ...common, code: '123456' });
  163. assert.strictEqual(verify.status, 'ok');
  164. assert.deepStrictEqual(gateway.requests.at(-1).body, { uid: common.uid, code: '123456' });
  165. console.log('[ok] login tools use uid-based Fmode gateway routes and hide internal identifiers');
  166. for (const request of gateway.requests) {
  167. assert.strictEqual(request.authorization, 'Bearer sk-test-relay-token');
  168. }
  169. console.log(`[ok] ${gateway.requests.length} mock Fmode gateway requests authenticated`);
  170. } finally {
  171. await gateway.close();
  172. }
  173. console.log('\nAll smoke tests passed.');
  174. }
  175. main().catch(error => {
  176. console.error('smoke test failed:', error);
  177. process.exit(1);
  178. });