| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import crypto from 'node:crypto';
- import { createApplication } from '../src/core.mjs';
- import { attachBoardActionHandlers } from '../src/board-actions.mjs';
- const data = JSON.parse(fs.readFileSync(new URL('../references/runtime-data.json', import.meta.url)));
- function fixture() {
- let rows = {}, seq = 0;
- const clone = (x) => structuredClone(x);
- const store = {
- async transaction(fn) { const before = clone(rows); try { return await fn(store); } catch (error) { rows = before; throw error; } },
- async lock() {},
- async get(name, id) { return clone(rows[name]?.find((row) => row.objectId === id) || null); },
- async list(name, { where = {} } = {}) { return clone((rows[name] || []).filter((row) => Object.entries(where).every(([key, value]) => row[key] === value))); },
- async create(name, value) { const row = { ...clone(value), objectId: 'test' + ++seq, updatedAt: new Date(1800000000000 + seq).toISOString() }; (rows[name] ||= []).push(row); return clone(row); },
- async update(name, id, changes) { const row = rows[name].find((row) => row.objectId === id); Object.assign(row, clone(changes), { updatedAt: new Date(1800000000000 + ++seq).toISOString() }); return clone(row); },
- };
- const app = createApplication({ store, crypto, Parse: {}, data, settings: { signingSecret: 'test-only' }, files: {} });
- attachBoardActionHandlers(app, { store });
- return { store, app };
- }
- async function card(f, extra = {}) { return f.store.create('KfOpportunity', { title: 'test', status: 'candidate', externalKey: 'workshop:opportunity:test', schemaVersion: 'ksf.v0.7', linkedProject: 'kept-project', linkedArtifacts: ['kept-report'], ...extra }); }
- const params = (row, extra = {}) => ({ requestId: 'request-' + crypto.randomUUID(), opportunityId: row.objectId, expectedUpdatedAt: row.updatedAt, by: '自报姓名', ...extra });
- test('匿名三状态持久化,保留互链与旧决策,不产生项目或企业批准', async () => {
- const f = fixture(); let row = await card(f);
- for (const status of ['approved', 'rejected', 'candidate']) {
- const result = (await f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status }), null)).data;
- assert.equal(result.opportunity.status, status); assert.equal(result.identityVerified, false); assert.equal(result.projectCreated, false); assert.equal(result.enterpriseApproval, false);
- assert.equal(result.opportunity.decision.via, 'public_board'); assert.equal(result.opportunity.decision.identityVerified, false);
- row = await f.store.get('KfOpportunity', row.objectId); assert.equal(row.status, status);
- assert.equal(row.linkedProject, 'kept-project'); assert.deepEqual(row.linkedArtifacts, ['kept-report']);
- }
- assert.equal(row.decisionLog.length, 3); assert.equal((await f.store.list('KfProject')).length, 0);
- assert.equal((await f.store.list('KsfOperation')).length, 3);
- });
- test('重复请求复用回执,改内容复用请求键、过期版本均不写入', async () => {
- const f = fixture(), row = await card(f), p = params(row, { status: 'approved' });
- const first = await f.app.dispatch('ksfSetPublicOpportunityStatus', p, null);
- assert.deepEqual((await f.app.dispatch('ksfSetPublicOpportunityStatus', p, null)).data, first.data);
- await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', { ...p, status: 'rejected' }, null), { code: 'IDEMPOTENCY_CONFLICT' });
- await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status: 'rejected' }), null), { code: 'VERSION_CONFLICT' });
- assert.equal((await f.store.list('KsfOperation')).length, 1);
- assert.equal((await f.store.get('KfOpportunity', row.objectId)).decisionLog.length, 1);
- });
- test('私有/归档/未知记录、越权字段与坏状态被拒绝,回执不泄露私有字段', async () => {
- const f = fixture();
- for (const extra of [{ externalKey: 'private:test' }, { recordState: 'archived' }]) {
- const row = await card(f, extra);
- await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status: 'approved' }), null), { code: 'REFERENCE_NOT_FOUND' });
- }
- const row = await card(f, { privateInfo: 'must-not-leak' });
- for (const extra of [{ status: 'completed' }, { status: 'approved', linkedProject: 'hack' }, { status: 'approved', expectedUpdatedAt: null }]) {
- await assert.rejects(() => f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, extra), null), { code: 'VALIDATION_FAILED' });
- }
- const result = await f.app.dispatch('ksfSetPublicOpportunityStatus', params(row, { status: 'approved', by: '' }), null);
- assert.equal(result.data.opportunity.privateInfo, undefined); assert.equal(result.data.opportunity.decision.by, '匿名访客');
- });
- test('匿名评分校验与同署名覆盖,保留其他评分', async () => {
- const f = fixture(); let row = await card(f, { scores: [{ user: '其他人', score: 60 }] });
- for (const score of [70, 80]) {
- await f.app.dispatch('ksfRatePublicOpportunity', params(row, { score, comment: 'test' }), null);
- row = await f.store.get('KfOpportunity', row.objectId);
- }
- assert.equal(row.scores.length, 2); assert.equal(row.scores.find((s) => s.user === '自报姓名').score, 80);
- for (const extra of [{ score: 101 }, { score: '80' }, { score: 50, by: '' }]) await assert.rejects(() => f.app.dispatch('ksfRatePublicOpportunity', params(row, extra), null), { code: 'VALIDATION_FAILED' });
- });
|