Files
Art-gallery/server/__tests__/movement-lineage-service.test.js
Danila KhodjaefandCursor 1b760faf0d 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>
2026-09-08 14:37:20 +03:00

130 lines
4.2 KiB
JavaScript

/**
* 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);
}
});
});