worldwide todo list: public todo list of 68 real, sourced problems (planet to street) at /projects/worldwide-todo. worldtodo schema (categories, tasks, change_requests), idempotent seed on deploy, public add/edit/delete requests with Turnstile + salted daily-rotating IP-hash rate limit, owner-only review queue (approve, edit & approve, reject) with live updates over /ws/worldtodo(-owner), JSON API, detail pages with OSM map and history; listed in projects, header, /start and sitemap
Some checks failed
Deploy production / deploy (push) Failing after 9s
Some checks failed
Deploy production / deploy (push) Failing after 9s
This commit is contained in:
126
src/lib/server/worldtodo/validate.test.ts
Normal file
126
src/lib/server/worldtodo/validate.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user