535 lines
11 KiB
JavaScript
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: '反馈不存在',
|
|
});
|
|
});
|