login-seat-flow-smoke-test.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. const assert = require('assert');
  2. const http = require('http');
  3. const {
  4. startLoginFlowServer,
  5. stopLoginFlowServer
  6. } = require('../mcp/src/core/login-flow-server');
  7. async function body(req) {
  8. const chunks = [];
  9. for await (const chunk of req) chunks.push(chunk);
  10. if (!chunks.length) return {};
  11. return JSON.parse(Buffer.concat(chunks).toString('utf8'));
  12. }
  13. async function startGateway() {
  14. const requests = [];
  15. const server = http.createServer(async (req, res) => {
  16. const url = new URL(req.url, 'http://localhost');
  17. const input = await body(req);
  18. requests.push({ path: url.pathname, method: req.method, input, authorization: req.headers.authorization });
  19. let data;
  20. if (url.pathname === '/subscribe/status') {
  21. const quotedSeats = Number(url.searchParams.get('seats'));
  22. data = quotedSeats
  23. ? { subscribed: true, seats: 1, usedSeats: 1, quote: { seats: quotedSeats, amount: 125 } }
  24. : { subscribed: true, seats: 1, usedSeats: 1, price: 500 };
  25. } else if (url.pathname === '/login/accounts') {
  26. data = { seats: 1, usedSeats: 1, accounts: [{ id: `account_${'a'.repeat(40)}`, nickname: '原账号', corpName: '测试企业', online: false }] };
  27. } else if (url.pathname === '/login/status') {
  28. data = { configured: true, online: false, statusCode: -1 };
  29. } else if (url.pathname === '/login/select') {
  30. data = { selected: true, uid: input.uid, accountId: input.accountId, online: false };
  31. } else if (url.pathname === '/login/start') {
  32. data = { uid: input.newAccount ? 'qiwei-temporary-login' : input.uid, loginQrcodeBase64Data: Buffer.from('mock-png').toString('base64') };
  33. } else if (url.pathname === '/login/check') {
  34. data = {
  35. uid: input.uid,
  36. status: 2,
  37. detail: { corpId: 'corp-new', userId: 'user-new', nickname: '新账号' },
  38. decisionRequired: true,
  39. seats: 1,
  40. usedSeats: 1,
  41. account: { id: `account_${'b'.repeat(40)}`, nickname: '新账号' },
  42. accounts: [{ id: `account_${'a'.repeat(40)}`, nickname: '原账号' }],
  43. actions: ['purchase', 'replace', 'cancel']
  44. };
  45. } else if (url.pathname === '/login/resolve') {
  46. data = { resolved: true, action: input.action, cancelled: input.action === 'cancel' };
  47. } else if (url.pathname === '/subscribe') {
  48. data = { seats: input.seats, amount: input.expectedAmount, state: 'active' };
  49. } else {
  50. res.writeHead(404, { 'Content-Type': 'application/json' });
  51. res.end(JSON.stringify({ code: 404, mess: 'not found' }));
  52. return;
  53. }
  54. res.writeHead(200, { 'Content-Type': 'application/json' });
  55. res.end(JSON.stringify({ code: 200, data }));
  56. });
  57. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  58. return {
  59. apiBase: `http://127.0.0.1:${server.address().port}`,
  60. requests,
  61. close: () => new Promise(resolve => server.close(resolve))
  62. };
  63. }
  64. async function main() {
  65. const gateway = await startGateway();
  66. const port = 4397;
  67. try {
  68. const { url } = await startLoginFlowServer({
  69. token: 'sk-login-seat-smoke',
  70. apiBase: gateway.apiBase,
  71. uid: 'qiwei-client-one',
  72. port
  73. });
  74. const page = await fetch(url).then(res => res.text());
  75. const checkoutToken = JSON.parse(page.match(/const CHECKOUT_TOKEN = ("[^"]+")/)[1]);
  76. const origin = `http://127.0.0.1:${port}`;
  77. const state = await fetch(`${origin}/flow/state`).then(res => res.json());
  78. assert.strictEqual(state.subscribed, true);
  79. assert.strictEqual(state.accounts.length, 1);
  80. const select = await fetch(`${origin}/flow/select-account`, {
  81. method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
  82. body: JSON.stringify({ accountId: state.accounts[0].id })
  83. }).then(res => res.json());
  84. assert.strictEqual(select.selected, true);
  85. await fetch(`${origin}/flow/start-login`, {
  86. method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
  87. body: JSON.stringify({ newAccount: true })
  88. });
  89. const decision = await fetch(`${origin}/flow/check`).then(res => res.json());
  90. assert.strictEqual(decision.decisionRequired, true);
  91. assert.deepStrictEqual(decision.actions, ['purchase', 'replace', 'cancel']);
  92. const quote = await fetch(`${origin}/flow/upgrade-quote`).then(res => res.json());
  93. assert.strictEqual(quote.targetSeats, 2);
  94. assert.strictEqual(quote.quote.amount, 125);
  95. const cancel = await fetch(`${origin}/flow/resolve`, {
  96. method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
  97. body: JSON.stringify({ checkoutToken, action: 'cancel' })
  98. }).then(res => res.json());
  99. assert.strictEqual(cancel.cancelled, true);
  100. const refreshedPage = await fetch(origin).then(res => res.text());
  101. const upgradeToken = JSON.parse(refreshedPage.match(/const CHECKOUT_TOKEN = ("[^"]+")/)[1]);
  102. await fetch(`${origin}/flow/start-login`, {
  103. method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
  104. body: JSON.stringify({ newAccount: true })
  105. });
  106. await fetch(`${origin}/flow/check`);
  107. const upgraded = await fetch(`${origin}/flow/upgrade`, {
  108. method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
  109. body: JSON.stringify({ checkoutToken: upgradeToken, idempotencyKey: 'seat-upgrade-smoke-001' })
  110. }).then(res => res.json());
  111. assert.strictEqual(upgraded.resolved, true);
  112. assert(gateway.requests.some(request => request.path === '/subscribe' && request.input.expectedAmount === 125));
  113. assert(gateway.requests.some(request => request.path === '/login/resolve' && request.input.action === 'purchase'));
  114. const replacePage = await fetch(origin).then(res => res.text());
  115. const replaceToken = JSON.parse(replacePage.match(/const CHECKOUT_TOKEN = ("[^"]+")/)[1]);
  116. await fetch(`${origin}/flow/start-login`, {
  117. method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
  118. body: JSON.stringify({ newAccount: true })
  119. });
  120. await fetch(`${origin}/flow/check`);
  121. const replaced = await fetch(`${origin}/flow/resolve`, {
  122. method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
  123. body: JSON.stringify({ checkoutToken: replaceToken, action: 'replace', replaceAccountId: state.accounts[0].id })
  124. }).then(res => res.json());
  125. assert.strictEqual(replaced.resolved, true);
  126. assert(gateway.requests.some(request => request.path === '/login/resolve' && request.input.action === 'replace'));
  127. assert(gateway.requests.every(request => request.authorization === 'Bearer sk-login-seat-smoke'));
  128. console.log('[ok] account selection and purchase, replace, cancel seat decisions');
  129. } finally {
  130. stopLoginFlowServer();
  131. await gateway.close();
  132. }
  133. }
  134. main().catch(error => {
  135. console.error(error);
  136. process.exitCode = 1;
  137. });