Files
memind/server/portal-account-feedback-routes.test.mjs
T

535 lines
11 KiB
JavaScript

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: '反馈不存在',
});
});