Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dev": "node bin/pi-pocket.js --cwd .",
"check": "tsc --noEmit -p .",
"test": "node --test test/*.test.ts",
"coverage": "node --test --experimental-test-coverage --test-coverage-lines=60 test/*.test.ts",
"site:publish": "git subtree push --prefix site origin gh-pages",
"format": "eslint --fix . && prettier --write .",
"format:check": "prettier --check . && eslint .",
Expand Down
48 changes: 48 additions & 0 deletions test/entry-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// The names stored entries and the web app depend on: the entry kinds and the markers written into message text. The
// first test is a contract, not a behavior check: it pins values that already-stored data cannot change. The other two
// run the code that reads those names back, so a changed value fails there too.
import assert from "node:assert/strict";
import { test } from "node:test";
import {
ATTACHMENTS_HEADING,
FILE_BLOCK,
FROM_PI_ENTRY,
FROM_PREFIX,
NOTE_ENTRY,
SHELL_ENTRY,
} from "../src/server/entry-format.ts";

test("the entry kinds stored data depends on are these names", () => {
assert.equal(NOTE_ENTRY, "pocket.note");
assert.equal(SHELL_ENTRY, "pocket.shell");
assert.equal(FROM_PI_ENTRY, "pocket.from-pi");
assert.equal(FILE_BLOCK, '<file name="');
assert.equal(ATTACHMENTS_HEADING, "\n\nAttached files (saved on the server):\n");
});

test("the [from: …] prefix reads a person's name from a message line", () => {
assert.equal(FROM_PREFIX.exec("[from: Ada] read the file")?.[1], "Ada");
assert.equal(FROM_PREFIX.exec("[from: Ada] read the file")?.[0], "[from: Ada] ");
assert.equal(FROM_PREFIX.exec("[from: Ada]read the file"), null);
assert.equal(FROM_PREFIX.exec("[from: ] Ada"), null);
assert.equal(FROM_PREFIX.exec("[from: A]da] hi"), null);
assert.equal(FROM_PREFIX.exec("[from: Ada]\nhi"), null);
assert.equal(FROM_PREFIX.exec("hello [from: Ada] there"), null);
assert.equal(FROM_PREFIX.exec(`[from: ${"a".repeat(60)}] hi`)?.[1], "a".repeat(60));
assert.equal(FROM_PREFIX.exec(`[from: ${"a".repeat(61)}] hi`), null);
assert.equal("[from: Ada] read the file".replace(FROM_PREFIX, ""), "read the file");
});

test("a message with attached files splits into what was written and the file list", () => {
const message = "hello\n\nAttached files (saved on the server):\nnotes.md";
const parts = message.split(ATTACHMENTS_HEADING);

assert.equal(parts[0], "hello");
assert.equal(parts[1], "notes.md");
assert.equal("hello".split(ATTACHMENTS_HEADING).length, 1);

const block = '<file name="notes.md">\nthe file\n</file>';

assert.equal(block.startsWith(FILE_BLOCK), true);
assert.equal(block.slice(FILE_BLOCK.length, block.indexOf('">')), "notes.md");
});
79 changes: 79 additions & 0 deletions test/fence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// The fence text from elsewhere goes between tags for Pi: every `<` with a `/` soon after it loses its `<`, whatever
// stands between them, and a `<` that is not a closing tag keeps its own. `quoted` gives one line that cannot close
// its quotes, and `attribute` gives a value that cannot end an attribute.
import assert from "node:assert/strict";
import { test } from "node:test";
import { attribute, defang, quoted } from "../src/server/fence.ts";

const ZERO_WIDTH = "\u200b";
const VARIATION_SELECTOR = "\uFE0F";
const FORMAT_CHARACTER = "\u00ad";

test("a closing tag loses its <, whatever stands between it and the /", () => {
assert.equal(defang("</x>"), "‹/x>");
assert.equal(defang("< /x>"), "‹ /x>");
assert.equal(defang(`<${ZERO_WIDTH}/x>`), `‹${ZERO_WIDTH}/x>`);
assert.equal(defang(`<${VARIATION_SELECTOR}/x>`), `‹${VARIATION_SELECTOR}/x>`);
assert.equal(defang(`<${FORMAT_CHARACTER}/x>`), `‹${FORMAT_CHARACTER}/x>`);
assert.equal(defang("<\t/x>"), "‹\t/x>");
assert.equal(defang("< \u200b \uFE0F /x>"), "‹ \u200b \uFE0F /x>");
});

test("a closing tag loses its < behind every category of invisible character the class names", () => {
// One representative per category, 81 of them, so the 80-character branch cannot defang it and only the class
// can: a control character, a format character, a lone surrogate, a private-use character, an unassigned code
// point, a variation selector supplement, the braille blank, the Hangul fillers, and the combining grapheme
// joiner. Narrowing the class fails here.
const hides = (character: string, what: string) => {
const invisible = character.repeat(81);

assert.equal(defang(`<${invisible}/x>`), `‹${invisible}/x>`, what);
};

hides("\u0007", "a control character");
hides("\u00ad", "a format character");
hides("\uD800", "a lone surrogate");
hides("\uE000", "a private-use character");
hides("\u0378", "an unassigned code point");
hides("\u{E0100}", "a variation selector supplement");
hides("\u2800", "the braille blank");
hides("\u3164", "the Hangul filler");
hides("\uFFA0", "the halfwidth Hangul filler");
hides("\u034F", "the combining grapheme joiner");
});

test("a closing tag with more than 80 characters between < and / is defanged only by the invisible run", () => {
const long = `<${ZERO_WIDTH.repeat(81)}/x>`;

assert.equal(defang(long), `‹${ZERO_WIDTH.repeat(81)}/x>`);
assert.equal(defang(`<${ZERO_WIDTH.repeat(80)}/x>`), `‹${ZERO_WIDTH.repeat(80)}/x>`);
});

test("a < that is not a closing tag keeps its <", () => {
assert.equal(defang("Vec<T> and <div> stay"), "Vec<T> and <div> stay");
assert.equal(defang("<b>bold</b>"), "<b>bold‹/b>");
});

test("a < with a / within 80 characters loses its <, harmlessly", () => {
assert.equal(defang("a < b/2"), "a ‹ b/2");
assert.equal(defang("x < y/z"), "x ‹ y/z");
assert.equal(defang("<"), "<");
assert.equal(defang(""), "");
});

test("quoted text is one line, without invisible characters, and cannot close its quotes", () => {
assert.equal(quoted("a\u200bb\u202e\u{e0041}c\nd\u2028e\u0085f"), "“abc d e f”");
assert.equal(quoted('say "hi" now'), '“say "hi" now”');
assert.equal(quoted("<b> and <c"), "“‹b> and ‹c”");
assert.equal(quoted(" spaced out "), "“spaced out”");
assert.equal(quoted("x".repeat(200), 120), `“${"x".repeat(119)}…”`);
assert.equal(quoted(""), "“”");
});

test("an attribute value is one line, without quotes or angle brackets", () => {
assert.equal(attribute('a"b<c>d'), "a b c d");
assert.equal(attribute("x\r\ny"), "x y");
assert.equal(attribute(" <a> "), "a");
assert.equal(attribute("plain"), "plain");
assert.equal(attribute(""), "");
});