Files
memind/server/portal-mindspace-space-routes.test.mjs

369 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalMindSpaceSpaceRoutes } from './portal-mindspace-space-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
return {
routes,
get(path, handler) {
routes.set(`GET ${path}`, handler);
},
post(path, handler) {
routes.set(`POST ${path}`, handler);
},
};
}
function createResponseRecorder() {
return {
statusCode: 200,
body: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
};
}
function createRequest(overrides = {}) {
return {
body: {},
params: {},
query: {},
currentUser: {
id: 'user-1',
username: 'alice',
slug: 'alice-slug',
},
ip: '127.0.0.1',
...overrides,
};
}
function createDependencies(overrides = {}) {
const calls = {
data: [],
errors: [],
routeErrors: [],
enabled: [],
};
return {
calls,
dependencies: {
ensureMindSpaceEnabled(res, req) {
calls.enabled.push({ res, req });
return true;
},
sendData(res, req, data) {
calls.data.push({ res, req, data });
return res.json({ data });
},
sendError(res, req, status, code, message) {
calls.errors.push({ res, req, status, code, message });
return res.status(status).json({ error: { code, message } });
},
handleMindSpaceError(res, req, error) {
calls.routeErrors.push({ res, req, error });
return res.status(418).json({ mapped: error.message });
},
...overrides,
},
};
}
test('MindSpace space module preserves route inventory and order', () => {
const api = createRouterRecorder();
attachPortalMindSpaceSpaceRoutes(api);
assert.deepEqual([...api.routes.keys()], [
'GET /mindspace/v1/space',
'POST /mindspace/v1/schedule/reminders/:reminderId/ignore',
'POST /mindspace/v1/schedule/reminders/bulk-delete',
'GET /mindspace/v1/space/quota',
'GET /mindspace/v1/space/categories',
'GET /mindspace/v1/space/cleanup',
'POST /mindspace/v1/space/cleanup',
]);
});
test('GET /mindspace/v1/space preserves availability, missing, and success behavior', async () => {
const unavailableApi = createRouterRecorder();
const unavailable = createDependencies();
attachPortalMindSpaceSpaceRoutes(unavailableApi, unavailable.dependencies);
const unavailableRes = createResponseRecorder();
await unavailableApi.routes.get('GET /mindspace/v1/space')(
createRequest(),
unavailableRes,
);
assert.equal(unavailableRes.body, undefined);
assert.equal(unavailable.calls.enabled.length, 0);
const missingApi = createRouterRecorder();
const missing = createDependencies({
getMindSpace: () => ({ getSpace: async () => null }),
});
attachPortalMindSpaceSpaceRoutes(missingApi, missing.dependencies);
const missingReq = createRequest();
const missingRes = createResponseRecorder();
await missingApi.routes.get('GET /mindspace/v1/space')(missingReq, missingRes);
assert.equal(missingRes.statusCode, 404);
assert.equal(missing.calls.errors[0].code, 'resource_not_found');
const space = { id: 'space-1' };
const successApi = createRouterRecorder();
const success = createDependencies({
getMindSpace: () => ({
async getSpace(userId) {
assert.equal(userId, 'user-1');
return space;
},
}),
});
attachPortalMindSpaceSpaceRoutes(successApi, success.dependencies);
const successReq = createRequest();
const successRes = createResponseRecorder();
await successApi.routes.get('GET /mindspace/v1/space')(successReq, successRes);
assert.deepEqual(success.calls.data[0], {
res: successRes,
req: successReq,
data: space,
});
});
test('schedule ignore preserves feature gate, forwarding, and error mapping', async () => {
const unavailableApi = createRouterRecorder();
const unavailable = createDependencies({
ensureMindSpaceEnabled: () => false,
});
attachPortalMindSpaceSpaceRoutes(unavailableApi, unavailable.dependencies);
const unavailableRes = createResponseRecorder();
await unavailableApi.routes.get(
'POST /mindspace/v1/schedule/reminders/:reminderId/ignore',
)(createRequest(), unavailableRes);
assert.equal(unavailableRes.statusCode, 503);
assert.equal(unavailable.calls.errors[0].code, 'feature_disabled');
let received = null;
const successApi = createRouterRecorder();
const success = createDependencies({
getScheduleService: () => ({
async cancelReminder(input) {
received = input;
return { id: input.reminderId, status: 'cancelled' };
},
}),
});
attachPortalMindSpaceSpaceRoutes(successApi, success.dependencies);
const successRes = createResponseRecorder();
await successApi.routes.get(
'POST /mindspace/v1/schedule/reminders/:reminderId/ignore',
)(createRequest({ params: { reminderId: 'reminder-1' } }), successRes);
assert.deepEqual(received, { userId: 'user-1', reminderId: 'reminder-1' });
assert.equal(success.calls.data[0].data.status, 'cancelled');
const failureApi = createRouterRecorder();
const failure = createDependencies({
getScheduleService: () => ({
async cancelReminder() {
throw new Error('cannot cancel');
},
}),
});
attachPortalMindSpaceSpaceRoutes(failureApi, failure.dependencies);
const failureRes = createResponseRecorder();
await failureApi.routes.get(
'POST /mindspace/v1/schedule/reminders/:reminderId/ignore',
)(createRequest({ params: { reminderId: 'reminder-2' } }), failureRes);
assert.equal(failureRes.statusCode, 400);
assert.equal(failure.calls.errors[0].message, 'cannot cancel');
});
test('schedule bulk delete preserves id normalization and error mapping', async () => {
let received = null;
const api = createRouterRecorder();
const success = createDependencies({
getScheduleService: () => ({
async deleteReminders(input) {
received = input;
return 2;
},
}),
});
attachPortalMindSpaceSpaceRoutes(api, success.dependencies);
const res = createResponseRecorder();
await api.routes.get('POST /mindspace/v1/schedule/reminders/bulk-delete')(
createRequest({ body: { ids: [1, '2'] } }),
res,
);
assert.deepEqual(received, { userId: 'user-1', reminderIds: ['1', '2'] });
assert.deepEqual(success.calls.data[0].data, { deleted: 2 });
const failureApi = createRouterRecorder();
const failure = createDependencies({
getScheduleService: () => ({
async deleteReminders() {
throw new Error('cannot delete');
},
}),
});
attachPortalMindSpaceSpaceRoutes(failureApi, failure.dependencies);
const failureRes = createResponseRecorder();
await failureApi.routes.get('POST /mindspace/v1/schedule/reminders/bulk-delete')(
createRequest(),
failureRes,
);
assert.equal(failureRes.statusCode, 400);
assert.equal(failure.calls.errors[0].message, 'cannot delete');
});
test('quota and categories preserve unavailable, missing, and success envelopes', async () => {
for (const [path, methodName] of [
['GET /mindspace/v1/space/quota', 'getQuota'],
['GET /mindspace/v1/space/categories', 'listCategories'],
]) {
const unavailableApi = createRouterRecorder();
attachPortalMindSpaceSpaceRoutes(unavailableApi);
const unavailableRes = createResponseRecorder();
await unavailableApi.routes.get(path)(createRequest(), unavailableRes);
assert.equal(unavailableRes.statusCode, 503);
assert.deepEqual(unavailableRes.body, { message: 'MindSpace 未启用' });
const missingApi = createRouterRecorder();
attachPortalMindSpaceSpaceRoutes(missingApi, {
getMindSpace: () => ({ [methodName]: async () => null }),
});
const missingRes = createResponseRecorder();
await missingApi.routes.get(path)(createRequest(), missingRes);
assert.equal(missingRes.statusCode, 404);
assert.deepEqual(missingRes.body, { message: '用户空间不存在' });
const value = methodName === 'getQuota' ? { used: 1 } : [{ code: 'notes' }];
const successApi = createRouterRecorder();
attachPortalMindSpaceSpaceRoutes(successApi, {
getMindSpace: () => ({ [methodName]: async () => value }),
});
const successRes = createResponseRecorder();
await successApi.routes.get(path)(createRequest(), successRes);
assert.deepEqual(successRes.body, { data: value });
}
});
test('GET cleanup preserves username choice, byte total, and route error mapping', async () => {
let received = null;
const items = [{ id: 'a', sizeBytes: 10 }, { id: 'b', sizeBytes: 25 }];
const api = createRouterRecorder();
const success = createDependencies({
getMindSpaceCleanup: () => ({
async listCandidates(userId, username) {
received = { userId, username };
return items;
},
}),
});
attachPortalMindSpaceSpaceRoutes(api, success.dependencies);
const res = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/space/cleanup')(createRequest(), res);
assert.deepEqual(received, { userId: 'user-1', username: 'alice' });
assert.deepEqual(success.calls.data[0].data, { items, totalBytes: 35 });
const failureApi = createRouterRecorder();
const failureError = new Error('cleanup list failed');
const failure = createDependencies({
getMindSpaceCleanup: () => ({
async listCandidates() {
throw failureError;
},
}),
});
attachPortalMindSpaceSpaceRoutes(failureApi, failure.dependencies);
const failureReq = createRequest();
const failureRes = createResponseRecorder();
await failureApi.routes.get('GET /mindspace/v1/space/cleanup')(
failureReq,
failureRes,
);
assert.deepEqual(failure.calls.routeErrors[0], {
res: failureRes,
req: failureReq,
error: failureError,
});
});
test('POST cleanup preserves cleanup, quota, audit, response, and error flow', async () => {
const calls = [];
const api = createRouterRecorder();
const success = createDependencies({
getMindSpaceCleanup: () => ({
async runCleanup(userId, username, itemIds) {
calls.push({ kind: 'cleanup', userId, username, itemIds });
return { removedCount: 2, freedBytes: 64 };
},
}),
getMindSpace: () => ({
async getQuota(userId) {
calls.push({ kind: 'quota', userId });
return { usedBytes: 100 };
},
}),
getMindSpaceAudit: () => ({
async write(entry) {
calls.push({ kind: 'audit', entry });
},
}),
});
attachPortalMindSpaceSpaceRoutes(api, success.dependencies);
const req = createRequest({ body: { item_ids: ['a', 'b'] }, ip: '10.0.0.5' });
const res = createResponseRecorder();
await api.routes.get('POST /mindspace/v1/space/cleanup')(req, res);
assert.deepEqual(calls[0], {
kind: 'cleanup',
userId: 'user-1',
username: 'alice',
itemIds: ['a', 'b'],
});
assert.deepEqual(calls[1], { kind: 'quota', userId: 'user-1' });
assert.deepEqual(calls[2], {
kind: 'audit',
entry: {
userId: 'user-1',
action: 'space.cleanup',
objectType: 'space',
objectId: 'user-1',
ip: '10.0.0.5',
detail: { removedCount: 2, freedBytes: 64 },
},
});
assert.deepEqual(success.calls.data[0].data, {
removedCount: 2,
freedBytes: 64,
quota: { usedBytes: 100 },
});
const failureApi = createRouterRecorder();
const failureError = new Error('cleanup failed');
const failure = createDependencies({
getMindSpaceCleanup: () => ({
async runCleanup() {
throw failureError;
},
}),
});
attachPortalMindSpaceSpaceRoutes(failureApi, failure.dependencies);
const failureReq = createRequest();
const failureRes = createResponseRecorder();
await failureApi.routes.get('POST /mindspace/v1/space/cleanup')(
failureReq,
failureRes,
);
assert.deepEqual(failure.calls.routeErrors[0], {
res: failureRes,
req: failureReq,
error: failureError,
});
});