import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalAccountFeedbackRoutes, } from './portal-account-feedback-routes.mjs'; function createResponse() { return { statusCode: 200, body: undefined, ended: false, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; return this; }, end() { this.ended = true; return this; }, }; } function createSetup(overrides = {}) { const routes = []; const calls = []; let userAuth = { async getMe(token) { calls.push(['get-me', token]); return { id: 'user-1', username: 'john', }; }, async listPathGrants(userId) { calls.push(['paths', userId]); return ['/workspace']; }, async getUserById(userId) { calls.push(['get-user', userId]); return { id: userId }; }, async resolveUserCapabilities(row) { calls.push(['capabilities', row]); return { capabilities: ['chat'], grantedSkills: ['web'], unrestricted: false, }; }, async revoke(token) { calls.push(['user-revoke', token]); }, async listUsageRecords(options) { calls.push(['usage', options]); return [{ id: 'usage-1' }]; }, }; let legacyAuth = { revoke(token) { calls.push(['legacy-revoke', token]); }, }; let subscriptionService = { async getActiveSubscription(userId) { calls.push(['subscription', userId]); return { planType: 'pro', }; }, }; let feedbackService = { async submit(userId, payload) { calls.push([ 'feedback-submit', userId, payload, ]); return { id: 'feedback-1', ...payload, }; }, async listForUser(userId, options) { calls.push([ 'feedback-list-user', userId, options, ]); return [{ id: 'feedback-1' }]; }, async listAll(options) { calls.push(['feedback-list-all', options]); return { items: [{ id: 'feedback-1' }], page: options.page, limit: options.limit, }; }, async getById(feedbackId, userId) { calls.push([ 'feedback-detail', feedbackId, userId, ]); return { id: feedbackId, userId, }; }, }; const jsonBody = (_req, _res, next) => next(); const app = { get(path, ...handlers) { routes.push({ method: 'get', path, handlers, }); }, post(path, ...handlers) { routes.push({ method: 'post', path, handlers, }); }, }; const options = { app, jsonBody, userAuthReady: Promise.resolve(), getUserAuth: () => userAuth, getLegacyAuth: () => legacyAuth, getSubscriptionService: () => subscriptionService, getFeedbackService: () => feedbackService, userToken: () => 'request-user-token', legacySessionToken: () => 'request-legacy-token', clearUserLoginCookies() { calls.push(['clear-cookies']); }, async resolveSkillRuntimeForClient() { calls.push(['skill-runtime']); return { enabled: true, }; }, async resolveAgentCodeRunForClient(userId) { calls.push(['agent-code-run', userId]); return { enabled: true, userId, }; }, logger: { warn(...args) { calls.push(['warn', ...args]); }, }, ...overrides, }; attachPortalAccountFeedbackRoutes(options); return { routes, calls, jsonBody, setUserAuth(value) { userAuth = value; }, setLegacyAuth(value) { legacyAuth = value; }, setSubscriptionService(value) { subscriptionService = value; }, setFeedbackService(value) { feedbackService = value; }, route(method, path) { return routes.find( (route) => route.method === method && route.path === path, ); }, }; } async function invoke( setup, method, path, req = {}, ) { const route = setup.route(method, path); assert.ok( route, `${method.toUpperCase()} ${path}`, ); const res = createResponse(); await route.handlers.at(-1)( { body: {}, query: {}, params: {}, ip: '127.0.0.1', ...req, }, res, ); return res; } test('registers the account and feedback route inventory in order', () => { const setup = createSetup(); assert.deepEqual( setup.routes.map(({ method, path }) => [ method, path, ]), [ ['get', '/auth/me'], ['post', '/auth/logout'], ['get', '/auth/usage'], ['post', '/auth/feedback'], ['get', '/auth/feedback'], ['get', '/auth/feedback/board'], ['get', '/auth/feedback/:feedbackId'], ], ); assert.equal( setup.route( 'post', '/auth/feedback', ).handlers[0], setup.jsonBody, ); assert.equal( setup.route('post', '/auth/logout').handlers .length, 1, ); }); test('preserves account profile, subscription, capability, and skill projection', async () => { const setup = createSetup(); const res = await invoke( setup, 'get', '/auth/me', ); assert.deepEqual(res.body, { user: { id: 'user-1', username: 'john', subscription: { planType: 'pro', }, }, paths: ['/workspace'], capabilities: ['chat'], grantedSkills: ['web'], unrestricted: false, skillRuntime: { enabled: true, }, agentCodeRun: { enabled: true, userId: 'user-1', }, }); setup.setSubscriptionService(null); const noSubscription = await invoke( setup, 'get', '/auth/me', ); assert.equal( noSubscription.body.user.subscription, null, ); }); test('preserves account availability and authentication gates', async () => { const unavailable = createSetup(); unavailable.setUserAuth(null); const unavailableRes = await invoke( unavailable, 'get', '/auth/me', ); assert.equal(unavailableRes.statusCode, 503); const unauthorized = createSetup(); unauthorized.setUserAuth({ async getMe() { return null; }, }); const unauthorizedRes = await invoke( unauthorized, 'get', '/auth/me', ); assert.equal(unauthorizedRes.statusCode, 401); }); test('preserves user and legacy logout revocation and cookie clearing', async () => { const setup = createSetup(); const res = await invoke( setup, 'post', '/auth/logout', ); assert.equal(res.statusCode, 204); assert.equal(res.ended, true); assert.deepEqual( setup.calls.filter(([name]) => [ 'user-revoke', 'legacy-revoke', 'clear-cookies', ].includes(name), ), [ ['user-revoke', 'request-user-token'], [ 'legacy-revoke', 'request-legacy-token', ], ['clear-cookies'], ], ); const disabled = createSetup(); disabled.setUserAuth(null); disabled.setLegacyAuth(null); const disabledRes = await invoke( disabled, 'post', '/auth/logout', ); assert.equal(disabledRes.statusCode, 204); assert.equal( disabled.calls.some( ([name]) => name === 'clear-cookies', ), false, ); }); test('preserves usage record scope and limit', async () => { const setup = createSetup(); const res = await invoke( setup, 'get', '/auth/usage', ); assert.deepEqual(res.body, { records: [{ id: 'usage-1' }], }); assert.ok( setup.calls.some( (call) => call[0] === 'usage' && call[1].userId === 'user-1' && call[1].limit === 30, ), ); }); test('preserves feedback submission payload, status, and error mapping', async () => { const setup = createSetup(); const body = { type: 'bug', title: '页面异常', description: '详情', contact: 'john@example.com', images: ['asset-1'], context: { path: '/chat' }, ignored: 'internal', }; const res = await invoke( setup, 'post', '/auth/feedback', { body }, ); assert.equal(res.statusCode, 201); assert.equal(res.body.feedback.id, 'feedback-1'); assert.deepEqual( setup.calls.find( ([name]) => name === 'feedback-submit', ), [ 'feedback-submit', 'user-1', { type: 'bug', title: '页面异常', description: '详情', contact: 'john@example.com', images: ['asset-1'], context: { path: '/chat' }, }, ], ); const invalid = createSetup(); invalid.setFeedbackService({ async submit() { const error = new Error('标题不能为空'); error.code = 'invalid_input'; throw error; }, }); const invalidRes = await invoke( invalid, 'post', '/auth/feedback', ); assert.equal(invalidRes.statusCode, 400); assert.deepEqual(invalidRes.body, { message: '标题不能为空', }); const failing = createSetup(); failing.setFeedbackService({ async submit() { throw new Error('storage down'); }, }); const failingRes = await invoke( failing, 'post', '/auth/feedback', ); assert.equal(failingRes.statusCode, 500); assert.ok( failing.calls.some( ([name]) => name === 'warn', ), ); }); test('preserves personal feedback limits and board pagination bounds', async () => { const setup = createSetup(); const personal = await invoke( setup, 'get', '/auth/feedback', { query: { limit: '1000' }, }, ); assert.deepEqual(personal.body, { items: [{ id: 'feedback-1' }], }); assert.ok( setup.calls.some( (call) => call[0] === 'feedback-list-user' && call[2].limit === 50, ), ); const board = await invoke( setup, 'get', '/auth/feedback/board', { query: { page: '-4', limit: '50', }, }, ); assert.deepEqual(board.body, { items: [{ id: 'feedback-1' }], page: 1, limit: 10, }); }); test('preserves feedback detail ownership and not-found mapping', async () => { const setup = createSetup(); const res = await invoke( setup, 'get', '/auth/feedback/:feedbackId', { params: { feedbackId: 'feedback-1', }, }, ); assert.deepEqual(res.body, { item: { id: 'feedback-1', userId: 'user-1', }, isMine: true, }); const missing = createSetup(); missing.setFeedbackService({ async getById() { const error = new Error('反馈不存在'); error.code = 'feedback_not_found'; throw error; }, }); const missingRes = await invoke( missing, 'get', '/auth/feedback/:feedbackId', { params: { feedbackId: 'missing', }, }, ); assert.equal(missingRes.statusCode, 404); assert.deepEqual(missingRes.body, { message: '反馈不存在', }); });