| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- const assert = require('assert');
- const fs = require('fs');
- const http = require('http');
- const os = require('os');
- const path = require('path');
- const vm = require('vm');
- const {
- startLoginFlowServer,
- stopLoginFlowServer,
- subscribeErrorStatus,
- subscribeErrorBody
- } = require('../mcp/src/core/login-flow-server');
- const { classifySubscribeError } = require('../mcp/src/core/subscribe-page');
- async function body(req) {
- const chunks = [];
- for await (const chunk of req) chunks.push(chunk);
- if (!chunks.length) return {};
- return JSON.parse(Buffer.concat(chunks).toString('utf8'));
- }
- async function startGateway() {
- const requests = [];
- let failSubscribe = false;
- const server = http.createServer(async (req, res) => {
- const url = new URL(req.url, 'http://localhost');
- const input = await body(req);
- requests.push({ path: url.pathname, method: req.method, input, authorization: req.headers.authorization });
- let data;
- if (url.pathname === '/subscribe/status') {
- const quotedSeats = Number(url.searchParams.get('seats'));
- data = quotedSeats
- ? { subscribed: true, seats: 1, usedSeats: 1, quote: { seats: quotedSeats, amount: 125 } }
- : { subscribed: true, seats: 1, usedSeats: 1, price: 500 };
- } else if (url.pathname === '/login/accounts') {
- data = { seats: 1, usedSeats: 1, accounts: [{ id: `account_${'a'.repeat(40)}`, nickname: '原账号', corpName: '测试企业', online: false }] };
- } else if (url.pathname === '/login/status') {
- data = { configured: true, online: false, statusCode: -1 };
- } else if (url.pathname === '/login/select') {
- data = { selected: true, uid: input.uid, accountId: input.accountId, online: false };
- } else if (url.pathname === '/login/start') {
- data = {
- uid: input.newAccount ? 'qiwei-temporary-login' : input.uid,
- guid: input.newAccount ? 'GUID-NEW-DEVICE' : 'GUID-EXISTING-DEVICE',
- loginQrcodeBase64Data: Buffer.from('mock-png').toString('base64')
- };
- } else if (url.pathname === '/login/check') {
- data = {
- uid: input.uid,
- status: 2,
- detail: { corpId: 'corp-new', userId: 'user-new', nickname: '新账号' },
- decisionRequired: true,
- seats: 1,
- usedSeats: 1,
- account: { id: `account_${'b'.repeat(40)}`, nickname: '新账号' },
- accounts: [{ id: `account_${'a'.repeat(40)}`, nickname: '原账号' }],
- actions: ['purchase', 'replace', 'cancel']
- };
- } else if (url.pathname === '/login/resolve') {
- data = { resolved: true, action: input.action, cancelled: input.action === 'cancel' };
- } else if (url.pathname === '/subscribe') {
- if (failSubscribe) {
- res.writeHead(200, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ code: 402, mess: '余额不足:当前 0 元,本次订阅需 500 元' }));
- return;
- }
- data = { seats: input.seats, amount: input.expectedAmount, state: 'active' };
- } else {
- res.writeHead(404, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ code: 404, mess: 'not found' }));
- return;
- }
- res.writeHead(200, { 'Content-Type': 'application/json' });
- res.end(JSON.stringify({ code: 200, data }));
- });
- await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
- return {
- apiBase: `http://127.0.0.1:${server.address().port}`,
- requests,
- setSubscribeFailure(value) { failSubscribe = Boolean(value); },
- close: () => new Promise(resolve => server.close(resolve))
- };
- }
- async function main() {
- assert.equal(
- classifySubscribeError(409, '上游账号当前没有可用企微登录数量,请稍后重试'),
- 'QW-UP-409',
- 'upstream login capacity errors must not be shown as generic 502'
- );
- assert.strictEqual(
- subscribeErrorStatus({ httpStatus: 200, bizCode: 402 }),
- 402,
- 'business billing code must survive an HTTP 200 gateway response'
- );
- assert.strictEqual(
- subscribeErrorBody(402, '余额不足').errorCode,
- 'QW-PAY-402',
- 'billing errors must use the payment error code'
- );
- const gateway = await startGateway();
- const port = 4397;
- const envRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-guid-persist-'));
- fs.writeFileSync(path.join(envRoot, '.env.local'), 'QIWEI_UID=qiwei-client-one\nQIWEI_GUID=\n', 'utf8');
- const previousRoots = {
- package: process.env.QIWEI_PACKAGE_ROOT,
- workspace: process.env.QIWEI_WORKSPACE_ROOT,
- };
- process.env.QIWEI_PACKAGE_ROOT = envRoot;
- process.env.QIWEI_WORKSPACE_ROOT = envRoot;
- try {
- const { url } = await startLoginFlowServer({
- token: 'sk-login-seat-smoke',
- apiBase: gateway.apiBase,
- uid: 'qiwei-client-one',
- port
- });
- const page = await fetch(url).then(res => res.text());
- const inlineScript = page.match(/<script>([\s\S]*?)<\/script>/);
- assert(inlineScript, 'login flow page must include its client script');
- assert.doesNotThrow(() => new vm.Script(inlineScript[1]), 'login flow client script must be valid JavaScript');
- const checkoutToken = JSON.parse(page.match(/const CHECKOUT_TOKEN = ("[^"]+")/)[1]);
- const origin = `http://127.0.0.1:${port}`;
- gateway.setSubscribeFailure(true);
- const failedSubscribe = await fetch(`${origin}/flow/subscribe`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ seats: 1, months: 1, idempotencyKey: 'subscribe-error-smoke-001', checkoutToken })
- });
- const failedBody = await failedSubscribe.json();
- assert.strictEqual(failedSubscribe.status, 402);
- assert.strictEqual(failedBody.errorCode, 'QW-PAY-402');
- gateway.setSubscribeFailure(false);
- const state = await fetch(`${origin}/flow/state`).then(res => res.json());
- assert.strictEqual(state.subscribed, true);
- assert.strictEqual(state.accounts.length, 1);
- const select = await fetch(`${origin}/flow/select-account`, {
- method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ accountId: state.accounts[0].id })
- }).then(res => res.json());
- assert.strictEqual(select.selected, true);
- const started = await fetch(`${origin}/flow/start-login`, {
- method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ newAccount: true })
- });
- assert.strictEqual(started.status, 200);
- assert.match(fs.readFileSync(path.join(envRoot, '.env.local'), 'utf8'), /^QIWEI_GUID=GUID-NEW-DEVICE$/m, 'new device GUID must be written immediately after QR creation');
- const decision = await fetch(`${origin}/flow/check`).then(res => res.json());
- assert.strictEqual(decision.decisionRequired, true);
- assert.deepStrictEqual(decision.actions, ['purchase', 'replace', 'cancel']);
- const quote = await fetch(`${origin}/flow/upgrade-quote`).then(res => res.json());
- assert.strictEqual(quote.targetSeats, 2);
- assert.strictEqual(quote.quote.amount, 125);
- const cancel = await fetch(`${origin}/flow/resolve`, {
- method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ checkoutToken, action: 'cancel' })
- }).then(res => res.json());
- assert.strictEqual(cancel.cancelled, true);
- const refreshedPage = await fetch(origin).then(res => res.text());
- const upgradeToken = JSON.parse(refreshedPage.match(/const CHECKOUT_TOKEN = ("[^"]+")/)[1]);
- await fetch(`${origin}/flow/start-login`, {
- method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ newAccount: true })
- });
- await fetch(`${origin}/flow/check`);
- const upgraded = await fetch(`${origin}/flow/upgrade`, {
- method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ checkoutToken: upgradeToken, idempotencyKey: 'seat-upgrade-smoke-001' })
- }).then(res => res.json());
- assert.strictEqual(upgraded.resolved, true);
- assert(gateway.requests.some(request => request.path === '/subscribe' && request.input.expectedAmount === 125));
- assert(gateway.requests.some(request => request.path === '/login/resolve' && request.input.action === 'purchase'));
- const replacePage = await fetch(origin).then(res => res.text());
- const replaceToken = JSON.parse(replacePage.match(/const CHECKOUT_TOKEN = ("[^"]+")/)[1]);
- await fetch(`${origin}/flow/start-login`, {
- method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ newAccount: true })
- });
- await fetch(`${origin}/flow/check`);
- const replaced = await fetch(`${origin}/flow/resolve`, {
- method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin },
- body: JSON.stringify({ checkoutToken: replaceToken, action: 'replace', replaceAccountId: state.accounts[0].id })
- }).then(res => res.json());
- assert.strictEqual(replaced.resolved, true);
- assert(gateway.requests.some(request => request.path === '/login/resolve' && request.input.action === 'replace'));
- assert(gateway.requests.every(request => request.authorization === 'Bearer sk-login-seat-smoke'));
- console.log('[ok] account selection and purchase, replace, cancel seat decisions');
- } finally {
- stopLoginFlowServer();
- await gateway.close();
- if (previousRoots.package === undefined) delete process.env.QIWEI_PACKAGE_ROOT;
- else process.env.QIWEI_PACKAGE_ROOT = previousRoots.package;
- if (previousRoots.workspace === undefined) delete process.env.QIWEI_WORKSPACE_ROOT;
- else process.env.QIWEI_WORKSPACE_ROOT = previousRoots.workspace;
- fs.rmSync(envRoot, { recursive: true, force: true });
- }
- }
- main().catch(error => {
- console.error(error);
- process.exitCode = 1;
- });
|