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