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 { createUserDataSpaceService } from './user-data-space-service.mjs'; import { publicationInternals } from './mindspace-publications.mjs'; const PAGE_ID = 'page-integration-1'; const OWNER_ID = 'user-integration-1'; function createPool(accessMode = 'public') { return { async query(sql) { if (sql.includes('FROM h5_publish_records')) { return [ [ { id: 'pub-1', user_id: OWNER_ID, page_id: PAGE_ID, access_mode: accessMode, password_hash: null, status: 'online', expires_at: null, }, ], ]; } return [[]]; }, }; } async function setupWorkspace(workspaceRoot) { const service = createUserDataSpaceService({ workspaceRoot }); await service.executeSql(`CREATE TABLE entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, created_at TEXT );`); await service.upsertDataset({ name: 'entries', table: 'entries', actions: ['read', 'insert'], columns: { read: ['id', 'title', 'created_at'], insert: ['title'], }, }); return service; } async function setupPublicWorkspace(workspaceRoot) { const service = createUserDataSpaceService({ workspaceRoot }); await service.executeSql(`CREATE TABLE signups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, status TEXT DEFAULT 'pending', created_at TEXT, deleted_at TEXT, deleted_by TEXT, updated_at TEXT, updated_by_label TEXT );`); await service.upsertDataset({ name: 'signups', table: 'signups', actions: ['read', 'insert', 'update', 'soft_delete'], columns: { read: ['id', 'name', 'phone', 'status', 'created_at'], insert: ['name', 'phone', 'status'], update: ['status'], soft_delete: ['id'], }, }); return service; } function buildApp(workspaceRoot) { const pageDataPublicService = createPageDataPublicService({ getPool: () => createPool('public'), resolveWorkspaceRootForOwner: () => workspaceRoot, }); 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.set('trust proxy', true); 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('integration: owner private API and public insert coexist without breaking each other', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-integration-')); const service = await setupWorkspace(workspaceRoot); const app = buildApp(workspaceRoot); const ownerInsert = await request(app, 'POST', '/api/admin/page-data/entries/rows', { headers: { 'x-test-user': '1' }, body: { title: 'owner 写入' }, }); assert.equal(ownerInsert.status, 201); assert.equal(ownerInsert.body.data.row.title, 'owner 写入'); const ownerList = await request(app, 'GET', '/api/admin/page-data/entries?limit=10', { headers: { 'x-test-user': '1' }, }); assert.equal(ownerList.status, 200); assert.equal(ownerList.body.data.rows.length, 1); const policy = { pageId: PAGE_ID, ownerUserId: OWNER_ID, accessMode: 'public', datasets: { entries: { insert: true, columns: { insert: ['title'] }, }, }, }; const policyWrite = await request(app, 'PUT', `/api/admin/page-data/policies/${PAGE_ID}`, { headers: { 'x-test-user': '1' }, body: policy, }); assert.equal(policyWrite.status, 200); assert.equal(policyWrite.body.data.policy.pageId, PAGE_ID); const publicInsert = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/entries/rows`, { body: { title: 'public 写入' }, }); assert.equal(publicInsert.status, 201); assert.equal(publicInsert.body.data.row.title, 'public 写入'); const publicReadDenied = await request(app, 'GET', `/api/public/pages/${PAGE_ID}/data/entries`); assert.equal(publicReadDenied.status, 403); assert.equal(publicReadDenied.body.error.code, 'action_not_allowed'); const ownerListAfterPublic = await request(app, 'GET', '/api/admin/page-data/entries?limit=10', { headers: { 'x-test-user': '1' }, }); assert.equal(ownerListAfterPublic.status, 200); assert.equal(ownerListAfterPublic.body.data.rows.length, 2); const stats = service.getDatasetStats('entries'); assert.equal(stats.total, 2); }); test('integration: public insert rejects SQL injection style payload keys', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-integration-sql-')); await setupWorkspace(workspaceRoot); const app = buildApp(workspaceRoot); await request(app, 'PUT', `/api/admin/page-data/policies/${PAGE_ID}`, { headers: { 'x-test-user': '1' }, body: { pageId: PAGE_ID, ownerUserId: OWNER_ID, accessMode: 'public', datasets: { entries: { insert: true, columns: { insert: ['title'] } }, }, }, }); const denied = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/entries/rows`, { body: { title: 'ok', sql: 'DROP TABLE entries' }, }); assert.equal(denied.status, 403); assert.equal(denied.body.error.code, 'columns_not_allowed'); }); test('integration: password publication flow still works for read after data-auth', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-integration-password-')); const service = await setupWorkspace(workspaceRoot); await service.insertDatasetRow('entries', { title: '口令可见' }); 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(); }); const pageDataPublicService = createPageDataPublicService({ getPool: () => ({ async query(sql) { if (sql.includes('FROM h5_publish_records')) { return [ [ { id: 'pub-1', user_id: OWNER_ID, page_id: PAGE_ID, access_mode: 'password', password_hash: publicationInternals.hashPassword('secret-123'), status: 'online', expires_at: null, }, ], ]; } return [[]]; }, }), resolveWorkspaceRootForOwner: () => workspaceRoot, }); 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); await request(app, 'PUT', `/api/admin/page-data/policies/${PAGE_ID}`, { headers: { 'x-test-user': '1' }, body: { pageId: PAGE_ID, ownerUserId: OWNER_ID, accessMode: 'password', datasets: { entries: { read: true, columns: { read: ['id', 'title', 'created_at'] }, }, }, }, }); const auth = await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data-auth`, { body: { password: 'secret-123' }, }); assert.equal(auth.status, 200); assert.ok(auth.body.data.token); const listed = await request(app, 'GET', `/api/public/pages/${PAGE_ID}/data/entries`, { headers: { 'x-page-data-token': auth.body.data.token }, }); assert.equal(listed.status, 200); assert.equal(listed.body.data.rows[0].title, '口令可见'); }); test('integration: public insert writes operation logs for owner review', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-logs-int-')); await setupPublicWorkspace(workspaceRoot); const app = buildApp(workspaceRoot); await request(app, 'PUT', `/api/admin/page-data/policies/${PAGE_ID}`, { headers: { 'x-test-user': '1' }, body: { pageId: PAGE_ID, ownerUserId: OWNER_ID, accessMode: 'public', datasets: { signups: { insert: true, columns: { insert: ['name', 'phone', 'status'] } } }, }, }); await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/signups/rows`, { body: { name: '日志测试', phone: '13800000000' }, }); const logs = await request(app, 'GET', `/api/admin/page-data/policies/${PAGE_ID}/logs`, { headers: { 'x-test-user': '1' }, }); assert.equal(logs.status, 200); assert.equal(logs.body.data.logs.length, 1); assert.equal(logs.body.data.logs[0].action, 'insert'); assert.equal(logs.body.data.logs[0].dataset, 'signups'); }); test('integration: owner can export dataset and restore soft-deleted rows', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-owner-ops-')); const ownerService = await setupPublicWorkspace(workspaceRoot); const inserted = await ownerService.insertDatasetRow('signups', { name: '导出项', phone: '13800000001', }); const app = buildApp(workspaceRoot); const exported = await request(app, 'GET', '/api/admin/page-data/signups/export?format=json', { headers: { 'x-test-user': '1' }, }); assert.equal(exported.status, 200); assert.equal(exported.body.data.rowCount, 1); await ownerService.softDeleteRowForDataset(ownerService.getDataset('signups'), inserted.row.id, { deletedBy: 'test', }); const restored = await request(app, 'POST', `/api/admin/page-data/signups/rows/${inserted.row.id}/restore`, { headers: { 'x-test-user': '1' }, }); assert.equal(restored.status, 200); assert.equal(restored.body.data.restored, true); }); test('integration: apply-publish route binds dataset after publication', async () => { const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-apply-publish-route-')); await setupPublicWorkspace(workspaceRoot); const app = buildApp(workspaceRoot); const applied = await request(app, 'POST', `/api/admin/page-data/policies/${PAGE_ID}/apply-publish`, { headers: { 'x-test-user': '1' }, body: { datasetName: 'signups', capabilities: { read: true, insert: true, update: false, softDelete: false }, }, }); assert.equal(applied.status, 200); assert.equal(applied.body.data.policy.datasets.signups.read, true); const publicReadEmpty = await request(app, 'GET', `/api/public/pages/${PAGE_ID}/data/signups`); assert.equal(publicReadEmpty.status, 200); assert.equal(publicReadEmpty.body.data.rows.length, 0); await request(app, 'POST', `/api/public/pages/${PAGE_ID}/data/signups/rows`, { body: { name: '发布后公开读', phone: '13800000000' }, }); const publicRead = await request(app, 'GET', `/api/public/pages/${PAGE_ID}/data/signups`); assert.equal(publicRead.status, 200); assert.equal(publicRead.body.data.rows.length, 1); });