jevstrudel.git / worker / src / screen.test.ts
1import { readdirSync, readFileSync } from 'node:fs';
2import { describe, expect, it } from 'vitest';
3import { codeOnly, SCREEN_CRITERIA, screenRequest, verdictOf, whyNotCode } from './screen';
4import { whyNot } from './relay';
5
6const SONGS = new URL('../../songs/', import.meta.url);
7const siteSongs = () =>
8  readdirSync(SONGS, { withFileTypes: true })
9    .filter((d) => d.isDirectory())
10    .flatMap((t) =>
11      readdirSync(new URL(`${t.name}/`, SONGS), { withFileTypes: true })
12        .filter((d) => d.isDirectory())
13        .map((d) => `${t.name}/${d.name}`),
14    )
15    .flatMap((id) => {
16      try {
17        return [{ id, code: readFileSync(new URL(`${id}/song.js`, SONGS), 'utf8') }];
18      } catch {
19        return [];
20      }
21    });
22
23describe('the screening question', () => {
24  it('is a choice the relay would forward, with fine, spam and abuse', () => {
25    const body = screenRequest({ kind: 'comment', body: 'love the drop', song: 'jev/dial-up' });
26    expect(whyNot(body)).toBeNull();
27    expect(Object.keys(body.questions.verdict.criteria)).toEqual(['fine', 'spam', 'abuse']);
28    expect(body.state).toEqual({ item: 'a comment on a song', body: 'love the drop', song: 'jev/dial-up' });
29  });
30
31  it('reads only a verdict that is one of its options', () => {
32    expect(verdictOf({ verdict: { choice: 'spam', confidence: 0.9 } })).toEqual({ verdict: 'spam', confidence: 0.9 });
33    expect(verdictOf({ verdict: { choice: 'fine' } })).toEqual({ verdict: 'fine', confidence: null });
34    for (const bad of [null, {}, { verdict: {} }, { verdict: { choice: 'toString' } }, { verdict: { choice: 'ok' } }]) {
35      expect(verdictOf(bad)).toBeNull();
36    }
37    expect(Object.keys(SCREEN_CRITERIA)).toEqual(['fine', 'spam', 'abuse']);
38  });
39});
40
41describe('whyNotCode: a song only makes music', () => {
42  it("passes every one of the site's own songs", () => {
43    const songs = siteSongs();
44    expect(songs.length).toBeGreaterThan(5);
45    for (const { id, code } of songs) expect(whyNotCode(code), id).toBeNull();
46  });
47
48  it('refuses code that reaches the network, storage, the document or the global object', () => {
49    for (const code of [
50      'fetch("/jev/listeners/pitches", { method: "POST" })',
51      's("bd").gain(1); window.x = 1',
52      'document.cookie',
53      "globalThis['fe' + 'tch']",
54      '[].at.constructor("return 1")()',
55      'self.fetch',
56      'top["location"]',
57      '(function () { return this })()',
58      'setTimeout("x()", 1)',
59      'import("https://example.com/x.js")',
60      'note(`c3 ${eval("1")}`)',
61    ]) {
62      expect(whyNotCode(code), code).toMatch(/only make music/);
63    }
64  });
65
66  it('reads what the code does, not what its comments and strings say', () => {
67    expect(whyNotCode('// open the window\nnote("c3") // fetch me a beer')).toBeNull();
68    expect(whyNotCode('const lyric = \'Take this. Close the window.\'; s("bd")')).toBeNull();
69    expect(whyNotCode('const top = 3; n(top)')).toBeNull();
70    expect(codeOnly('a("x" + `y${b}z`) /* c */ // d')).toBe('a("" + `${b}`)   ');
71  });
72});