Files
memind/page-data-ops.test.mjs
T
john 6b0c633a75 feat(page-data): complete Phase 4-5, ops UI, and publish integration
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index,
Turnstile captcha, browser client SDK, publish-panel dataset binding,
acceptance tests, and usage documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 14:52:49 +08:00

227 lines
8.0 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import express from 'express';
import { attachPageDataRoutes } from './page-data-routes.mjs';
import { createPageDataService } from './page-data-service.mjs';
import { createPageDataPublicService } from './page-data-public-service.mjs';
import { createPageDataSessionStore } from './page-data-session-store.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
import { writePageAccessPolicy } from './page-data-policy-store.mjs';
import { publicationInternals } from './mindspace-publications.mjs';
const PAGE_ID = 'page-ops-1';
const OWNER_ID = 'user-ops-1';
const OLD_PASSWORD = 'old-pass-123';
const NEW_PASSWORD = 'new-pass-456';
function createPool({ accessMode = 'password', password = OLD_PASSWORD, onUpdate } = {}) {
const passwordHash = password ? publicationInternals.hashPassword(password) : null;
const state = {
id: 'pub-ops-1',
user_id: OWNER_ID,
page_id: PAGE_ID,
access_mode: accessMode,
password_hash: passwordHash,
status: 'online',
expires_at: null,
};
return {
async query(sql, params) {
if (sql.includes('FROM h5_publish_records') && sql.includes('WHERE pr.page_id')) {
return [[{ ...state }]];
}
if (sql.startsWith('UPDATE h5_publish_records SET password_hash')) {
state.password_hash = params[0];
onUpdate?.(params);
return [{ affectedRows: 1 }];
}
return [[]];
},
};
}
async function setupWorkspace(workspaceRoot) {
const service = createUserDataSpaceService({ workspaceRoot });
await service.executeSql(`CREATE TABLE signups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT,
created_at TEXT
);`);
await service.upsertDataset({
name: 'signups',
table: 'signups',
actions: ['read', 'insert'],
columns: {
read: ['id', 'name', 'phone', 'created_at'],
insert: ['name', 'phone'],
},
});
writePageAccessPolicy(workspaceRoot, {
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
accessMode: 'password',
datasets: {
signups: {
read: true,
insert: true,
columns: {
read: ['id', 'name', 'phone', 'created_at'],
insert: ['name', 'phone'],
},
},
},
});
return service;
}
function buildApp(workspaceRoot, pool, sessionStore) {
const pageDataPublicService = createPageDataPublicService({
getPool: () => pool,
resolveWorkspaceRootForOwner: () => workspaceRoot,
sessionStore,
});
const api = express.Router();
api.use(express.json());
api.use((req, _res, next) => {
if (req.headers['x-test-user']) {
req.currentUser = { id: OWNER_ID, workspaceRoot };
}
next();
});
attachPageDataRoutes(api, {
sendData: (res, _req, data, status = 200) => res.status(status).json({ data }),
sendError: (res, _req, status, code, message) => res.status(status).json({ error: { code, message } }),
getPageDataService: () =>
createPageDataService({
resolveWorkspaceRoot: async () => workspaceRoot,
}),
getPageDataPublicService: () => pageDataPublicService,
});
const app = express();
app.use('/api', api);
return app;
}
async function request(app, method, url, { body, headers } = {}) {
const server = app.listen(0);
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}${url}`, {
method,
headers: {
...(body ? { 'content-type': 'application/json' } : {}),
...headers,
},
body: body ? JSON.stringify(body) : undefined,
});
return { status: response.status, body: await response.json() };
} finally {
await new Promise((resolve) => server.close(resolve));
}
}
test('ops overview returns datasets stats logs and active sessions', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-ops-overview-'));
const ownerService = await setupWorkspace(workspaceRoot);
await ownerService.insertDatasetRow('signups', { name: '概览项', phone: '13800000000' });
const sessionStore = createPageDataSessionStore();
sessionStore.issue({
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
publicationId: 'pub-ops-1',
accessMode: 'password',
});
const app = buildApp(workspaceRoot, createPool(), sessionStore);
await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/signups/rows`, {
body: { name: '公开提交', phone: '13800000001' },
headers: { 'x-page-data-token': (await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, { body: { password: OLD_PASSWORD } })).body.data.token },
});
const overview = await request(app, 'GET', `/api/page-data/policies/${PAGE_ID}/ops`, {
headers: { 'x-test-user': '1' },
});
assert.equal(overview.status, 200);
assert.equal(overview.body.data.datasets.length, 1);
assert.equal(overview.body.data.datasets[0].stats.total, 2);
assert.ok(overview.body.data.activeSessionCount >= 1);
assert.ok(overview.body.data.recentLogs.length >= 1);
});
test('owner can revoke all tokens and reset password', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-ops-reset-'));
await setupWorkspace(workspaceRoot);
const sessionStore = createPageDataSessionStore();
const auth1 = sessionStore.issue({
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
publicationId: 'pub-ops-1',
accessMode: 'password',
});
const auth2 = sessionStore.issue({
pageId: PAGE_ID,
ownerUserId: OWNER_ID,
publicationId: 'pub-ops-1',
accessMode: 'password',
});
let updatedPasswordHash = null;
const app = buildApp(workspaceRoot, createPool({ onUpdate: ([hash]) => { updatedPasswordHash = hash; } }), sessionStore);
const revoked = await request(app, 'POST', `/api/page-data/policies/${PAGE_ID}/tokens/revoke`, {
headers: { 'x-test-user': '1' },
body: { revokeAll: true },
});
assert.equal(revoked.status, 200);
assert.equal(revoked.body.data.revokedCount, 2);
assert.equal(sessionStore.verify(auth1.token), null);
assert.equal(sessionStore.verify(auth2.token), null);
const reset = await request(app, 'POST', `/api/page-data/policies/${PAGE_ID}/password/reset`, {
headers: { 'x-test-user': '1' },
body: { password: NEW_PASSWORD },
});
assert.equal(reset.status, 200);
assert.equal(reset.body.data.passwordReset, true);
assert.ok(updatedPasswordHash);
assert.ok(publicationInternals.verifyPassword(NEW_PASSWORD, updatedPasswordHash));
const oldAuthDenied = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, {
body: { password: OLD_PASSWORD },
});
assert.equal(oldAuthDenied.status, 401);
const newAuth = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, {
body: { password: NEW_PASSWORD },
});
assert.equal(newAuth.status, 200);
assert.ok(newAuth.body.data.token);
});
test('owner can close dataset and public insert is denied afterwards', async () => {
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-ops-close-'));
await setupWorkspace(workspaceRoot);
const sessionStore = createPageDataSessionStore();
const app = buildApp(workspaceRoot, createPool(), sessionStore);
const auth = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, {
body: { password: OLD_PASSWORD },
});
const closed = await request(app, 'POST', `/api/page-data/policies/${PAGE_ID}/datasets/signups/close`, {
headers: { 'x-test-user': '1' },
});
assert.equal(closed.status, 200);
assert.equal(closed.body.data.closed, true);
assert.equal(closed.body.data.policyDataset.closed, true);
const denied = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/signups/rows`, {
headers: { 'x-page-data-token': auth.body.data.token },
body: { name: '关闭后提交', phone: '13800000002' },
});
assert.equal(denied.status, 403);
assert.equal(denied.body.error.code, 'action_not_allowed');
});