Add curator Movement links config with proof URLs and admin-gated delete votes.
Persist lineage in PostgreSQL, seed documented edges, and keep timeline/tree consumers API-backed with a static fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2b4cdc5a9f
commit
1b760faf0d
36 files changed
+2051
-43
No files matched your search
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Tests for movement lineage validation (run before / with the service module).
|
||||
* node --test server/__tests__/movement-lineage-service.test.js
|
||||
*/
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
isValidProofUrl,
|
||||
validateCreatePayload,
|
||||
validateDeleteVotePayload,
|
||||
DEFAULT_MOVEMENT_LINEAGE,
|
||||
} = require('../movement-lineage-service');
|
||||
|
||||
describe('isValidProofUrl', () => {
|
||||
it('accepts http and https URLs', () => {
|
||||
assert.equal(isValidProofUrl('https://www.metmuseum.org/toah/hd/byza/hd_byza.htm'), true);
|
||||
assert.equal(isValidProofUrl('http://example.com/essay'), true);
|
||||
});
|
||||
|
||||
it('rejects empty, non-http, and garbage', () => {
|
||||
assert.equal(isValidProofUrl(''), false);
|
||||
assert.equal(isValidProofUrl(' '), false);
|
||||
assert.equal(isValidProofUrl(null), false);
|
||||
assert.equal(isValidProofUrl('ftp://files.example/a'), false);
|
||||
assert.equal(isValidProofUrl('not-a-url'), false);
|
||||
assert.equal(isValidProofUrl('www.metmuseum.org/page'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCreatePayload', () => {
|
||||
it('requires source, target, and proof URL', () => {
|
||||
const r = validateCreatePayload({});
|
||||
assert.equal(r.ok, false);
|
||||
assert.match(r.error, /sourceMovementId/i);
|
||||
});
|
||||
|
||||
it('rejects same source and target', () => {
|
||||
const r = validateCreatePayload({
|
||||
sourceMovementId: 3,
|
||||
targetMovementId: 3,
|
||||
proofUrl: 'https://www.theartstory.org/movement/cubism/',
|
||||
});
|
||||
assert.equal(r.ok, false);
|
||||
assert.match(r.error, /different/i);
|
||||
});
|
||||
|
||||
it('requires a valid proof URL', () => {
|
||||
const r = validateCreatePayload({
|
||||
sourceMovementId: 1,
|
||||
targetMovementId: 2,
|
||||
proofUrl: 'see Wikipedia',
|
||||
});
|
||||
assert.equal(r.ok, false);
|
||||
assert.match(r.error, /proof/i);
|
||||
});
|
||||
|
||||
it('accepts a valid payload and normalizes fields', () => {
|
||||
const r = validateCreatePayload({
|
||||
sourceMovementId: '10',
|
||||
targetMovementId: 20,
|
||||
proofUrl: ' https://www.metmuseum.org/essays/cubism ',
|
||||
proofLabel: ' Met Cubism ',
|
||||
linkYearApprox: '1907',
|
||||
notes: ' Cézanne → Picasso ',
|
||||
});
|
||||
assert.equal(r.ok, true);
|
||||
assert.deepEqual(r.value, {
|
||||
sourceMovementId: 10,
|
||||
targetMovementId: 20,
|
||||
proofUrl: 'https://www.metmuseum.org/essays/cubism',
|
||||
proofLabel: 'Met Cubism',
|
||||
linkYearApprox: 1907,
|
||||
notes: 'Cézanne → Picasso',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows null optional year and blank optional label/notes', () => {
|
||||
const r = validateCreatePayload({
|
||||
sourceMovementId: 1,
|
||||
targetMovementId: 2,
|
||||
proofUrl: 'https://example.org/a',
|
||||
proofLabel: '',
|
||||
linkYearApprox: '',
|
||||
notes: null,
|
||||
});
|
||||
assert.equal(r.ok, true);
|
||||
assert.equal(r.value.proofLabel, null);
|
||||
assert.equal(r.value.linkYearApprox, null);
|
||||
assert.equal(r.value.notes, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDeleteVotePayload', () => {
|
||||
it('requires a non-empty reason', () => {
|
||||
assert.equal(validateDeleteVotePayload({}).ok, false);
|
||||
assert.equal(validateDeleteVotePayload({ reason: ' ' }).ok, false);
|
||||
assert.match(validateDeleteVotePayload({ reason: '' }).error, /reason/i);
|
||||
});
|
||||
|
||||
it('rejects reasons that are too short', () => {
|
||||
const r = validateDeleteVotePayload({ reason: 'no' });
|
||||
assert.equal(r.ok, false);
|
||||
assert.match(r.error, /reason/i);
|
||||
});
|
||||
|
||||
it('accepts and trims a valid reason', () => {
|
||||
const r = validateDeleteVotePayload({
|
||||
reason: ' Incorrect lineage: Cubism did not derive from Fauvism. ',
|
||||
});
|
||||
assert.equal(r.ok, true);
|
||||
assert.equal(r.value.reason, 'Incorrect lineage: Cubism did not derive from Fauvism.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_MOVEMENT_LINEAGE', () => {
|
||||
it('has unique source→target pairs with proof URLs', () => {
|
||||
assert.ok(DEFAULT_MOVEMENT_LINEAGE.length >= 20);
|
||||
const keys = new Set();
|
||||
for (const row of DEFAULT_MOVEMENT_LINEAGE) {
|
||||
assert.ok(row.sourceName);
|
||||
assert.ok(row.targetName);
|
||||
assert.notEqual(row.sourceName, row.targetName);
|
||||
assert.equal(isValidProofUrl(row.proofUrl), true, row.proofUrl);
|
||||
const key = `${row.sourceName}→${row.targetName}`;
|
||||
assert.equal(keys.has(key), false, `duplicate ${key}`);
|
||||
keys.add(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user