127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { looksLikeBot, normaliseUrl, parseRequest, plain } from './validate';
|
|
|
|
const add = {
|
|
kind: 'add',
|
|
title: 'Fix the <b>broken</b> streetlights on Main Street',
|
|
description: 'Half the lights have been out since August.\n\n<script>x</script>Please fix.',
|
|
scale: 'street',
|
|
category: 'safety',
|
|
location_name: 'Main Street, Gouda',
|
|
country_code: 'nl',
|
|
lat: '52.01',
|
|
lon: '4.71',
|
|
status: 'open',
|
|
source_urls: ['https://example.org/news', '', 'https://example.org/news'],
|
|
reason: 'I walk here every evening and it is dark.',
|
|
display_name: ''
|
|
};
|
|
|
|
describe('worldtodo request validation', () => {
|
|
it('accepts a valid add, strips HTML and normalises', () => {
|
|
const r = parseRequest(add);
|
|
expect(r.ok).toBe(true);
|
|
if (!r.ok) return;
|
|
expect(r.value.fields?.title).toBe('Fix the broken streetlights on Main Street');
|
|
expect(r.value.fields?.description).not.toContain('<');
|
|
expect(r.value.fields?.country_code).toBe('NL');
|
|
expect(r.value.fields?.lat).toBe(52.01);
|
|
expect(r.value.fields?.source_urls).toEqual(['https://example.org/news']);
|
|
expect(r.value.displayName).toBeNull();
|
|
});
|
|
|
|
it('requires sources for add and edit, and a reason of 10+ chars', () => {
|
|
const r = parseRequest({ ...add, source_urls: [], reason: 'short' });
|
|
expect(r.ok).toBe(false);
|
|
if (r.ok) return;
|
|
expect(r.errors.source_urls).toBeTruthy();
|
|
expect(r.errors.reason).toBeTruthy();
|
|
});
|
|
|
|
it('rejects non-http URLs, too many URLs and overlong URLs', () => {
|
|
expect(normaliseUrl('javascript:alert(1)')).toBeNull();
|
|
expect(normaliseUrl('ftp://example.org/x')).toBeNull();
|
|
expect(normaliseUrl(`https://example.org/${'a'.repeat(500)}`)).toBeNull();
|
|
expect(normaliseUrl('https://example.org/ok')).toBe('https://example.org/ok');
|
|
const many = Array.from({ length: 6 }, (_, i) => `https://e${i}.org/`);
|
|
const r = parseRequest({ ...add, source_urls: many });
|
|
expect(r.ok).toBe(false);
|
|
});
|
|
|
|
it('needs both coordinates or neither, in range', () => {
|
|
expect(parseRequest({ ...add, lon: '' }).ok).toBe(false);
|
|
expect(parseRequest({ ...add, lat: '91' }).ok).toBe(false);
|
|
expect(parseRequest({ ...add, lat: '', lon: '' }).ok).toBe(true);
|
|
});
|
|
|
|
it('delete needs only a task and a reason', () => {
|
|
const r = parseRequest({
|
|
kind: 'delete',
|
|
task: 'some-task',
|
|
reason: 'It was fixed last week.'
|
|
});
|
|
expect(r.ok).toBe(true);
|
|
if (r.ok) expect(r.value.fields).toBeNull();
|
|
expect(parseRequest({ kind: 'delete', reason: 'It was fixed last week.' }).ok).toBe(false);
|
|
});
|
|
|
|
it('caps the display name at 40 characters', () => {
|
|
expect(parseRequest({ ...add, display_name: 'x'.repeat(41) }).ok).toBe(false);
|
|
const r = parseRequest({ ...add, display_name: ' <i>Anna</i> ' });
|
|
expect(r.ok && r.value.displayName).toBe('Anna');
|
|
});
|
|
|
|
it('flags the honeypot and too-fast submissions', () => {
|
|
const now = 1_000_000;
|
|
expect(looksLikeBot('', now - 10_000, now)).toBe(false);
|
|
expect(looksLikeBot('http://spam', now - 10_000, now)).toBe(true);
|
|
expect(looksLikeBot('', now - 1000, now)).toBe(true);
|
|
expect(looksLikeBot('', 'garbage', now)).toBe(true);
|
|
});
|
|
|
|
it('plain() keeps paragraphs but drops control characters', () => {
|
|
expect(plain('a\u0000b\r\n\r\n\r\nc', true)).toBe('a b\n\nc');
|
|
expect(plain(' one\ntwo ')).toBe('one two');
|
|
});
|
|
|
|
it('add without the optional keys defaults them (JSON API)', () => {
|
|
const { country_code, lat, lon, status, display_name, ...minimal } = add;
|
|
void [country_code, lat, lon, status, display_name];
|
|
const r = parseRequest(minimal);
|
|
expect(r.ok).toBe(true);
|
|
if (!r.ok) return;
|
|
expect(r.value.fields).toMatchObject({
|
|
country_code: null,
|
|
lat: null,
|
|
lon: null,
|
|
status: 'open'
|
|
});
|
|
});
|
|
|
|
it('a partial edit is validated on top of the current task', () => {
|
|
const base = {
|
|
title: 'Fix the broken streetlights on Main Street',
|
|
description: 'Out since August.',
|
|
scale: 'street',
|
|
category: 'safety',
|
|
location_name: 'Main Street, Gouda',
|
|
country_code: 'NL',
|
|
lat: 52.01,
|
|
lon: 4.71,
|
|
status: 'open',
|
|
source_urls: ['https://example.org/news']
|
|
};
|
|
const r = parseRequest(
|
|
{
|
|
kind: 'edit',
|
|
task: 'main-street',
|
|
status: 'in_progress',
|
|
reason: 'Works started on Monday.'
|
|
},
|
|
base
|
|
);
|
|
expect(r.ok).toBe(true);
|
|
if (r.ok) expect(r.value.fields).toMatchObject({ ...base, status: 'in_progress' });
|
|
});
|
|
});
|