581 lines
20 KiB
JavaScript
581 lines
20 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 { createPageDataPublicService, isPageDataPublicPath } from './page-data-public-service.mjs';
|
|
import { createUserDataSpaceService } from './user-data-space-service.mjs';
|
|
import { writePageAccessPolicy, readPageAccessPolicy } from './page-data-policy-store.mjs';
|
|
import { publicationInternals } from './mindspace-publications.mjs';
|
|
import { createPageDataSessionStore } from './page-data-session-store.mjs';
|
|
|
|
const PAGE_ID = 'page-public-1';
|
|
const OWNER_ID = 'user-owner-1';
|
|
|
|
function createPublicationPool({
|
|
accessMode = 'public',
|
|
password = null,
|
|
pageId = PAGE_ID,
|
|
expiresAt = null,
|
|
} = {}) {
|
|
const passwordHash = password ? publicationInternals.hashPassword(password) : null;
|
|
return {
|
|
async query(sql, params = []) {
|
|
if (sql.includes('FROM h5_publish_records')) {
|
|
if (params[0] !== pageId) return [[]];
|
|
return [
|
|
[
|
|
{
|
|
id: 'pub-record-1',
|
|
user_id: OWNER_ID,
|
|
page_id: pageId,
|
|
access_mode: accessMode,
|
|
password_hash: passwordHash,
|
|
status: 'online',
|
|
expires_at: expiresAt,
|
|
},
|
|
],
|
|
];
|
|
}
|
|
return [[]];
|
|
},
|
|
};
|
|
}
|
|
|
|
async function setupPublicWorkspace(workspaceRoot, { accessMode = 'public', withRead = false } = {}) {
|
|
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'],
|
|
},
|
|
});
|
|
writePageAccessPolicy(workspaceRoot, {
|
|
pageId: PAGE_ID,
|
|
ownerUserId: OWNER_ID,
|
|
accessMode,
|
|
datasets: {
|
|
signups: {
|
|
read: withRead,
|
|
insert: true,
|
|
update: withRead,
|
|
softDelete: withRead,
|
|
columns: {
|
|
read: ['id', 'name', 'phone', 'status', 'created_at'],
|
|
insert: ['name', 'phone', 'status'],
|
|
update: ['status'],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return service;
|
|
}
|
|
|
|
function createPublicService(workspaceRoot, poolOptions = {}, serviceOptions = {}) {
|
|
return createPageDataPublicService({
|
|
getPool: () => createPublicationPool(poolOptions),
|
|
resolveWorkspaceRootForOwner: () => workspaceRoot,
|
|
...serviceOptions,
|
|
});
|
|
}
|
|
|
|
test('public page insert works without login for public access mode', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-public-'));
|
|
await setupPublicWorkspace(workspaceRoot);
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
|
|
const result = await service.insertRow(PAGE_ID, 'signups', {
|
|
ip: '127.0.0.1',
|
|
headers: { 'user-agent': 'test' },
|
|
}, {
|
|
name: '匿名用户',
|
|
phone: '13900000000',
|
|
});
|
|
assert.equal(result.row.name, '匿名用户');
|
|
});
|
|
|
|
test('public page read is denied in public access mode by default', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-public-read-'));
|
|
await setupPublicWorkspace(workspaceRoot);
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
|
|
await assert.rejects(
|
|
() => service.listRows(PAGE_ID, 'signups', { headers: {} }),
|
|
(error) => error.code === 'action_not_allowed',
|
|
);
|
|
});
|
|
|
|
test('public page read works when policy explicitly enables read', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-public-read-ok-'));
|
|
const ownerService = await setupPublicWorkspace(workspaceRoot, { accessMode: 'public', withRead: true });
|
|
await ownerService.insertRowForDataset(ownerService.getDataset('signups'), {
|
|
name: '公开可读',
|
|
phone: '13900000002',
|
|
});
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
|
|
const rows = await service.listRows(PAGE_ID, 'signups', { headers: {} });
|
|
assert.equal(rows.rows.length, 1);
|
|
assert.equal(rows.rows[0].name, '公开可读');
|
|
});
|
|
|
|
test('login_required page read requires login and works for authenticated visitor', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-login-required-'));
|
|
const ownerService = await setupPublicWorkspace(workspaceRoot, {
|
|
accessMode: 'login_required',
|
|
withRead: true,
|
|
});
|
|
await ownerService.insertRowForDataset(ownerService.getDataset('signups'), {
|
|
name: '登录可见',
|
|
phone: '13800000002',
|
|
});
|
|
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'login_required' });
|
|
await assert.rejects(
|
|
() => service.listRows(PAGE_ID, 'signups', { headers: {} }),
|
|
(error) => error.code === 'auth_required',
|
|
);
|
|
|
|
const rows = await service.listRows(PAGE_ID, 'signups', {
|
|
currentUser: { id: 'visitor-1' },
|
|
headers: {},
|
|
});
|
|
assert.equal(rows.rows.length, 1);
|
|
assert.equal(rows.rows[0].name, '登录可见');
|
|
});
|
|
|
|
test('password page data-auth returns token and enables read', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-password-'));
|
|
const ownerService = await setupPublicWorkspace(workspaceRoot, {
|
|
accessMode: 'password',
|
|
withRead: true,
|
|
});
|
|
await ownerService.insertRowForDataset(ownerService.getDataset('signups'), {
|
|
name: '协作项',
|
|
phone: '13800000001',
|
|
});
|
|
|
|
const service = createPublicService(workspaceRoot, {
|
|
accessMode: 'password',
|
|
password: 'team-2026',
|
|
});
|
|
const auth = await service.authenticate(PAGE_ID, 'team-2026', { ip: '127.0.0.1' });
|
|
assert.ok(auth.token);
|
|
|
|
const rows = await service.listRows(PAGE_ID, 'signups', {
|
|
ip: '127.0.0.1',
|
|
headers: { 'x-page-data-token': auth.token },
|
|
});
|
|
assert.equal(rows.rows.length, 1);
|
|
assert.equal(rows.rows[0].name, '协作项');
|
|
});
|
|
|
|
test('password page update and soft delete require token', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-password-mut-'));
|
|
const ownerService = await setupPublicWorkspace(workspaceRoot, {
|
|
accessMode: 'password',
|
|
withRead: true,
|
|
});
|
|
const inserted = await ownerService.insertRowForDataset(ownerService.getDataset('signups'), {
|
|
name: '待更新',
|
|
status: 'pending',
|
|
});
|
|
|
|
const service = createPublicService(workspaceRoot, {
|
|
accessMode: 'password',
|
|
password: 'team-2026',
|
|
});
|
|
const auth = await service.authenticate(PAGE_ID, 'team-2026', { ip: '127.0.0.1' });
|
|
const req = {
|
|
ip: '127.0.0.1',
|
|
headers: { 'x-page-data-token': auth.token },
|
|
body: { updated_by_label: '测试员' },
|
|
};
|
|
|
|
const updated = await service.updateRow(PAGE_ID, 'signups', inserted.row.id, req, {
|
|
status: 'done',
|
|
});
|
|
assert.equal(updated.row.status, 'done');
|
|
|
|
const deleted = await service.softDeleteRow(PAGE_ID, 'signups', inserted.row.id, req);
|
|
assert.equal(deleted.deleted, true);
|
|
});
|
|
|
|
test('password page rejects missing, invalid, cross-page, and expired tokens', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-password-token-'));
|
|
await setupPublicWorkspace(workspaceRoot, {
|
|
accessMode: 'password',
|
|
withRead: true,
|
|
});
|
|
const sessionStore = createPageDataSessionStore({ ttlMs: 60_000 });
|
|
const service = createPublicService(
|
|
workspaceRoot,
|
|
{ accessMode: 'password', password: 'team-2026' },
|
|
{ sessionStore },
|
|
);
|
|
|
|
for (const headers of [{}, { 'x-page-data-token': 'invalid-token' }]) {
|
|
await assert.rejects(
|
|
() => service.listRows(PAGE_ID, 'signups', { headers }),
|
|
(error) => error.code === 'token_invalid' && error.status === 401,
|
|
);
|
|
}
|
|
|
|
const crossPage = sessionStore.issue({
|
|
pageId: 'page-other',
|
|
ownerUserId: OWNER_ID,
|
|
publicationId: 'pub-other',
|
|
accessMode: 'password',
|
|
});
|
|
await assert.rejects(
|
|
() =>
|
|
service.listRows(PAGE_ID, 'signups', {
|
|
headers: { 'x-page-data-token': crossPage.token },
|
|
}),
|
|
(error) => error.code === 'token_invalid' && error.status === 401,
|
|
);
|
|
|
|
const expiredStore = createPageDataSessionStore({ ttlMs: -1 });
|
|
const expired = expiredStore.issue({
|
|
pageId: PAGE_ID,
|
|
ownerUserId: OWNER_ID,
|
|
publicationId: 'pub-expired',
|
|
accessMode: 'password',
|
|
});
|
|
const expiredService = createPublicService(
|
|
workspaceRoot,
|
|
{ accessMode: 'password', password: 'team-2026' },
|
|
{ sessionStore: expiredStore },
|
|
);
|
|
await assert.rejects(
|
|
() =>
|
|
expiredService.listRows(PAGE_ID, 'signups', {
|
|
headers: { 'x-page-data-token': expired.token },
|
|
}),
|
|
(error) => error.code === 'token_invalid' && error.status === 401,
|
|
);
|
|
});
|
|
|
|
test('public access never allows update or delete even when policy contains those actions', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-public-mutation-'));
|
|
const ownerService = await setupPublicWorkspace(workspaceRoot, {
|
|
accessMode: 'public',
|
|
withRead: true,
|
|
});
|
|
const inserted = await ownerService.insertRowForDataset(ownerService.getDataset('signups'), {
|
|
name: '不可匿名修改',
|
|
status: 'pending',
|
|
});
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
|
|
const req = { ip: '127.0.0.1', headers: {}, body: {} };
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
service.updateRow(PAGE_ID, 'signups', inserted.row.id, req, {
|
|
status: 'done',
|
|
}),
|
|
(error) => error.code === 'action_not_allowed' && error.status === 403,
|
|
);
|
|
await assert.rejects(
|
|
() => service.deleteRow(PAGE_ID, 'signups', inserted.row.id, req),
|
|
(error) => error.code === 'action_not_allowed' && error.status === 403,
|
|
);
|
|
});
|
|
|
|
test('expired, unknown, and invalid-policy publications fail closed', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-public-fail-closed-'));
|
|
await setupPublicWorkspace(workspaceRoot, { accessMode: 'public', withRead: true });
|
|
|
|
const expiredService = createPublicService(workspaceRoot, {
|
|
accessMode: 'public',
|
|
expiresAt: Date.now() - 1,
|
|
});
|
|
await assert.rejects(
|
|
() => expiredService.listRows(PAGE_ID, 'signups', { headers: {} }),
|
|
(error) => error.code === 'publication_not_found' && error.status === 404,
|
|
);
|
|
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
|
|
await assert.rejects(
|
|
() => service.listRows('page-missing', 'signups', { headers: {} }),
|
|
(error) => error.code === 'publication_not_found' && error.status === 404,
|
|
);
|
|
|
|
writePageAccessPolicy(workspaceRoot, {
|
|
pageId: PAGE_ID,
|
|
ownerUserId: 'different-owner',
|
|
accessMode: 'public',
|
|
datasets: {
|
|
signups: {
|
|
read: true,
|
|
columns: { read: ['id', 'name'] },
|
|
},
|
|
},
|
|
});
|
|
await assert.rejects(
|
|
() => service.listRows(PAGE_ID, 'signups', { headers: {} }),
|
|
(error) => error.code === 'invalid_policy' && error.status === 400,
|
|
);
|
|
});
|
|
|
|
test('isPageDataPublicPath allows public page data routes without auth', () => {
|
|
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups/rows', 'POST'), true);
|
|
assert.equal(isPageDataPublicPath('/public/pages/page-1/data-auth', 'POST'), true);
|
|
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups', 'GET'), true);
|
|
assert.equal(isPageDataPublicPath('/page-data/signups', 'GET'), false);
|
|
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups', 'POST'), false);
|
|
assert.equal(isPageDataPublicPath('/public/pages/page-1/data/signups/rows', 'GET'), false);
|
|
assert.equal(isPageDataPublicPath('/public/pages/page-1/dataevil/signups', 'GET'), false);
|
|
assert.equal(isPageDataPublicPath('/public/pages/page-1/data-auth', 'GET'), false);
|
|
assert.equal(isPageDataPublicPath('/admin/page-data/signups', 'GET'), false);
|
|
});
|
|
|
|
async function setupCollaborationWorkspace(workspaceRoot) {
|
|
const service = createUserDataSpaceService({ workspaceRoot });
|
|
await service.executeSql(`CREATE TABLE notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
status TEXT DEFAULT 'open',
|
|
created_by_user_id TEXT,
|
|
created_at TEXT,
|
|
deleted_at TEXT,
|
|
deleted_by TEXT,
|
|
updated_at TEXT,
|
|
updated_by_label TEXT
|
|
);`);
|
|
await service.upsertDataset({
|
|
name: 'notes',
|
|
table: 'notes',
|
|
actions: ['read', 'insert', 'update', 'soft_delete'],
|
|
columns: {
|
|
read: ['id', 'title', 'status', 'created_by_user_id', 'created_at'],
|
|
insert: ['title'],
|
|
update: ['status'],
|
|
soft_delete: ['id'],
|
|
},
|
|
});
|
|
writePageAccessPolicy(workspaceRoot, {
|
|
pageId: PAGE_ID,
|
|
ownerUserId: OWNER_ID,
|
|
accessMode: 'login_required',
|
|
defaultVisitorRole: 'deny',
|
|
visitors: {
|
|
'editor-a': 'editor',
|
|
'viewer-a': 'viewer',
|
|
},
|
|
datasets: {
|
|
notes: {
|
|
read: true,
|
|
insert: true,
|
|
update: true,
|
|
softDelete: true,
|
|
rowPolicy: { scope: 'own_rows', ownerColumn: 'created_by_user_id' },
|
|
columns: {
|
|
read: ['id', 'title', 'status', 'created_by_user_id', 'created_at'],
|
|
insert: ['title'],
|
|
update: ['status'],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return service;
|
|
}
|
|
|
|
test('login_required editor can insert and update only own_rows', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-role-editor-'));
|
|
const ownerService = await setupCollaborationWorkspace(workspaceRoot);
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'login_required' });
|
|
|
|
const inserted = await service.insertRow(
|
|
PAGE_ID,
|
|
'notes',
|
|
{ currentUser: { id: 'editor-a' }, ip: '127.0.0.1', headers: {} },
|
|
{ title: '编辑者 A 的笔记' },
|
|
);
|
|
assert.equal(inserted.row.title, '编辑者 A 的笔记');
|
|
|
|
const insertedB = await ownerService.insertRowForDataset(ownerService.getDataset('notes'), {
|
|
title: '编辑者 B 的笔记',
|
|
});
|
|
await ownerService.executeSql(
|
|
`UPDATE notes SET created_by_user_id = 'editor-b' WHERE id = ${insertedB.row.id};`,
|
|
);
|
|
|
|
const rowsA = await service.listRows(PAGE_ID, 'notes', {
|
|
currentUser: { id: 'editor-a' },
|
|
headers: {},
|
|
});
|
|
assert.equal(rowsA.rows.length, 1);
|
|
assert.equal(rowsA.rows[0].title, '编辑者 A 的笔记');
|
|
|
|
const updated = await service.updateRow(
|
|
PAGE_ID,
|
|
'notes',
|
|
inserted.row.id,
|
|
{ currentUser: { id: 'editor-a' }, ip: '127.0.0.1', headers: {}, body: {} },
|
|
{ status: 'done' },
|
|
);
|
|
assert.equal(updated.row.status, 'done');
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
service.updateRow(
|
|
PAGE_ID,
|
|
'notes',
|
|
insertedB.row.id,
|
|
{ currentUser: { id: 'editor-a' }, ip: '127.0.0.1', headers: {}, body: {} },
|
|
{ status: 'hijacked' },
|
|
),
|
|
(error) => error.code === 'row_not_allowed',
|
|
);
|
|
});
|
|
|
|
test('login_required viewer can read but cannot insert', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-role-viewer-'));
|
|
const ownerService = await setupCollaborationWorkspace(workspaceRoot);
|
|
await ownerService.executeSql(
|
|
`INSERT INTO notes (title, created_by_user_id, created_at) VALUES ('owner seed', '${OWNER_ID}', datetime('now'));`,
|
|
);
|
|
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'login_required' });
|
|
const rows = await service.listRows(PAGE_ID, 'notes', {
|
|
currentUser: { id: 'viewer-a' },
|
|
headers: {},
|
|
});
|
|
assert.equal(rows.rows.length, 0);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
service.insertRow(
|
|
PAGE_ID,
|
|
'notes',
|
|
{ currentUser: { id: 'viewer-a' }, ip: '127.0.0.1', headers: {} },
|
|
{ title: 'viewer insert' },
|
|
),
|
|
(error) => error.code === 'action_not_allowed',
|
|
);
|
|
});
|
|
|
|
test('login_required denies visitors not listed when defaultVisitorRole is deny', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-role-deny-'));
|
|
await setupCollaborationWorkspace(workspaceRoot);
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'login_required' });
|
|
await assert.rejects(
|
|
() =>
|
|
service.listRows(PAGE_ID, 'notes', {
|
|
currentUser: { id: 'stranger-1' },
|
|
headers: {},
|
|
}),
|
|
(error) => error.code === 'forbidden',
|
|
);
|
|
});
|
|
|
|
test('public insert requires captcha when turnstile secret is configured', async () => {
|
|
const previousSecret = process.env.PAGE_DATA_TURNSTILE_SECRET;
|
|
process.env.PAGE_DATA_TURNSTILE_SECRET = 'test-secret';
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-captcha-required-'));
|
|
await setupPublicWorkspace(workspaceRoot);
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
|
|
try {
|
|
await assert.rejects(
|
|
() =>
|
|
service.insertRow(PAGE_ID, 'signups', { ip: '127.0.0.1', headers: {} }, {
|
|
name: '匿名用户',
|
|
phone: '13900000000',
|
|
}),
|
|
(error) => error.code === 'captcha_required',
|
|
);
|
|
} finally {
|
|
if (previousSecret == null) delete process.env.PAGE_DATA_TURNSTILE_SECRET;
|
|
else process.env.PAGE_DATA_TURNSTILE_SECRET = previousSecret;
|
|
}
|
|
});
|
|
|
|
test('public insert accepts verified captcha token', async () => {
|
|
const previousSecret = process.env.PAGE_DATA_TURNSTILE_SECRET;
|
|
const previousFetch = globalThis.fetch;
|
|
process.env.PAGE_DATA_TURNSTILE_SECRET = 'test-secret';
|
|
globalThis.fetch = async () => ({
|
|
ok: true,
|
|
async json() {
|
|
return { success: true };
|
|
},
|
|
});
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-captcha-ok-'));
|
|
await setupPublicWorkspace(workspaceRoot);
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'public' });
|
|
try {
|
|
const result = await service.insertRow(
|
|
PAGE_ID,
|
|
'signups',
|
|
{ ip: '127.0.0.1', headers: { 'x-page-data-captcha': 'turnstile-token' } },
|
|
{ name: '验证码用户', phone: '13900000001' },
|
|
);
|
|
assert.equal(result.row.name, '验证码用户');
|
|
} finally {
|
|
globalThis.fetch = previousFetch;
|
|
if (previousSecret == null) delete process.env.PAGE_DATA_TURNSTILE_SECRET;
|
|
else process.env.PAGE_DATA_TURNSTILE_SECRET = previousSecret;
|
|
}
|
|
});
|
|
|
|
test('saveOwnerPolicyFromPublish writes policy for published page', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-apply-publish-'));
|
|
await setupPublicWorkspace(workspaceRoot, { accessMode: 'password', withRead: true });
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'password' });
|
|
const user = { id: OWNER_ID, workspaceRoot };
|
|
const policy = await service.saveOwnerPolicyFromPublish(user, PAGE_ID, {
|
|
datasetName: 'signups',
|
|
capabilities: {
|
|
read: true,
|
|
insert: true,
|
|
update: false,
|
|
softDelete: false,
|
|
},
|
|
});
|
|
assert.equal(policy.pageId, PAGE_ID);
|
|
assert.equal(policy.accessMode, 'password');
|
|
assert.equal(policy.datasets.signups.read, true);
|
|
assert.equal(policy.datasets.signups.insert, true);
|
|
const saved = readPageAccessPolicy(workspaceRoot, PAGE_ID);
|
|
assert.equal(saved.datasets.signups.read, true);
|
|
});
|
|
|
|
test('saveOwnerPolicy rejects unregistered datasets without overwriting the current policy', async () => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'page-data-owner-policy-guard-'));
|
|
await setupPublicWorkspace(workspaceRoot, { accessMode: 'password', withRead: true });
|
|
const service = createPublicService(workspaceRoot, { accessMode: 'password' });
|
|
const user = { id: OWNER_ID, workspaceRoot };
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
service.saveOwnerPolicy(user, PAGE_ID, {
|
|
accessMode: 'password',
|
|
datasets: {
|
|
split_crud_uat_20260724: {
|
|
read: true,
|
|
insert: true,
|
|
columns: { read: ['id'], insert: ['title'] },
|
|
},
|
|
},
|
|
}),
|
|
(error) => error.code === 'dataset_not_found' && error.status === 404,
|
|
);
|
|
|
|
const saved = readPageAccessPolicy(workspaceRoot, PAGE_ID);
|
|
assert.deepEqual(Object.keys(saved.datasets), ['signups']);
|
|
});
|