369 lines
12 KiB
JavaScript
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,
|
|
});
|
|
});
|