billing-client-smoke.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. 'use strict';
  2. const assert = require('node:assert/strict');
  3. const { queryBilling } = require('../mcp/src/core/billing-client');
  4. const { requestPaymentQr } = require('../mcp/src/core/recharge-client');
  5. async function main() {
  6. const originalFetch = global.fetch;
  7. let billingCalls = 0;
  8. global.fetch = async (url, options) => {
  9. if (/\/api\/fmode\/billing\/query$/.test(String(url))) {
  10. billingCalls += 1;
  11. assert.equal(options.method, 'POST');
  12. assert.match(options.headers.Authorization, /^Bearer sk-test$/);
  13. return new Response(JSON.stringify({
  14. code: 200,
  15. data: {
  16. balance: { availableQuota: 1000 },
  17. // NewAPI-key authentication intentionally omits this Session-bound
  18. // link. The client must build it locally from the current Session.
  19. balanceUrl: null,
  20. estimate: {
  21. quota: 2000,
  22. amountCny: 1,
  23. shortfallQuota: 1000,
  24. suggestedRechargeCny: 1.1,
  25. sufficient: false,
  26. operations: []
  27. },
  28. catalog: []
  29. }
  30. }), { status: 200, headers: { 'content-type': 'application/json' } });
  31. }
  32. if (/\/api\/fmode\/recharge\/order$/.test(String(url))) {
  33. assert.equal(options.method, 'POST');
  34. assert.match(String(options.body || ''), /sessionToken/);
  35. return new Response(JSON.stringify({ code: 200, data: { tradeNo: 'trade-query' } }), { status: 200 });
  36. }
  37. assert.match(String(url), /\/parse\/functions\/pay_code2$/);
  38. assert.equal(options.method, 'POST');
  39. assert.match(options.headers['X-Parse-Session-Token'], /^r:/);
  40. return new Response(JSON.stringify({ result: { code_url: ['weixin://pay/query-test'], nonce_str: 'nonce-query' } }), { status: 200 });
  41. };
  42. const result = await queryBilling({
  43. newapiToken: 'sk-test',
  44. baseUrl: 'https://billing.example.test',
  45. operations: [{ model: 'voc-social', count: 1 }]
  46. });
  47. assert.equal(result.status, 'ok');
  48. if (result.recharge) {
  49. assert.equal(result.recharge.paymentMode, 'balance_url_and_payment_request');
  50. assert.equal(Object.prototype.hasOwnProperty.call(result.recharge.paymentRequest, 'headers'), false);
  51. assert.doesNotMatch(JSON.stringify(result), /r:[A-Za-z0-9_-]{8,}/);
  52. }
  53. const fallbackResult = await queryBilling({
  54. newapiToken: 'sk-test',
  55. sessionToken: 'r:session-test',
  56. baseUrl: 'https://billing.example.test',
  57. operations: [{ model: 'voc-social', count: 1 }]
  58. });
  59. assert.equal(fallbackResult.status, 'ok');
  60. assert.equal(fallbackResult.recharge.paymentMode, 'balance_url_and_payment_request');
  61. assert.match(fallbackResult.recharge.balanceUrl, /balance\/\?token=r%3Asession-test/);
  62. assert.match(fallbackResult.assistantMessage, /免登录余额页:https:\/\/app\.fmode\.cn\/dev\/studio\/balance/);
  63. assert.doesNotMatch(JSON.stringify(fallbackResult.recharge.paymentRequest), /r:[A-Za-z0-9_-]{8,}/);
  64. const qrResult = await queryBilling({
  65. newapiToken: 'sk-test',
  66. sessionToken: 'r:session-test',
  67. baseUrl: 'https://billing.example.test',
  68. createPaymentQr: true,
  69. operations: [{ model: 'voc-social', count: 1 }]
  70. });
  71. assert.equal(qrResult.status, 'ok');
  72. assert.equal(qrResult.recharge.paymentMode, 'qr_and_balance_url');
  73. assert.equal(qrResult.recharge.qrCodeUrl, 'weixin://pay/query-test');
  74. assert.equal(qrResult.recharge.paymentRequest.nonceStr, 'nonce-query');
  75. assert.doesNotMatch(JSON.stringify(qrResult.recharge.paymentRequest), /r:[A-Za-z0-9_-]{8,}/);
  76. const qr = await requestPaymentQr({
  77. amount: 2,
  78. sessionToken: 'r:session-test',
  79. fetchImpl: async (_url, options) => {
  80. if (String(_url).endsWith('/api/fmode/recharge/order')) {
  81. assert.equal(options.method, 'POST');
  82. return new Response(JSON.stringify({ code: 200, data: { tradeNo: 'trade-qr' } }), { status: 200 });
  83. }
  84. assert.match(options.headers['X-Parse-Session-Token'], /^r:/);
  85. assert.equal(options.headers['X-Parse-Application-Id'], 'ncloudmaster');
  86. return new Response(JSON.stringify({ result: { code_url: ['weixin://pay/test'], nonce_str: 'nonce' } }), { status: 200 });
  87. }
  88. });
  89. assert.equal(qr.qrCodeUrl, 'weixin://pay/test');
  90. assert.equal(qr.amountCny, 2);
  91. global.fetch = originalFetch;
  92. console.log('billing-client smoke passed');
  93. }
  94. main().catch(error => {
  95. console.error(error);
  96. process.exitCode = 1;
  97. });