Files
stack/packages/library/tests/supporting.test.mjs

93 lines
6.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { inspect } from 'node:util';
import { LibraryError, supportingPath } from '../src/index.mjs';
import { mapping } from './helpers.mjs';
// C2-01d (M12): the path checks an agent write passes. The tree is a listTree array built by hand,
// with a symlink, a gitlink, case twins and an NFC name, so no git is needed.
const m = mapping(e => {
e.universes.harbor.playground = ['Book One/Playground/', 'Book One/Sketches/'];
e.universes.harbor.supporting = ['Book One/Playground/', 'Book One/Notes/', 'Harbor Notes/'];
e.universes.second.supporting = ['Second Notes/'];
e.universes.idea = { kind: 'draft', name: 'New World', playground: ['Drafts/New World/'], supporting: ['Drafts/New World Notes/'] };
return e;
});
const blob = (path, mode = '100644') => ({ mode, type: mode === '160000' ? 'commit' : 'blob', oid: '0'.repeat(40), size: 1, path });
const tree = [
blob('Book One/Characters/Mara Vell.md'),
blob('Book One/Notes/Existing.md'),
blob('Book One/Notes/Linked', '120000'),
blob('Book One/Notes/Vendor', '160000'),
blob('Book One/Notes/Pointer.md', '120000'),
blob('Book One/Notes/Case/Twin.md'),
blob('Book One/Notes/Café.md'),
blob('Harbor Notes/Places/Island.md'),
];
const refuses = (universe, path, pattern) => assert.throws(() => supportingPath(m, universe, path, tree),
e => e instanceof LibraryError && e.code === 'library-write-story-path' && pattern.test(e.message), inspect(path));
test('C2-01d: a path passes at book level, universe level, in a draft and in a folder in both lists', () => {
for (const [universe, path] of [['harbor', 'Book One/Notes/Characters/New.md'], ['harbor', 'Book One/Notes/Existing.md'],
['harbor', 'Harbor Notes/Places/Island.md'], ['harbor', 'Harbor Notes/Loose.md'], ['idea', 'Drafts/New World Notes/Ana.md'],
['harbor', 'Book One/Playground/What If.md'], ['second', 'Second Notes/A.md'], ['harbor', 'Book One/Notes/Café.md']])
assert.equal(supportingPath(m, universe, path, tree), path);
assert.equal(supportingPath(m, 'harbor', 'Book One/Notes/x.md', []), 'Book One/Notes/x.md');
});
test('C2-01d: a path of exactly 1024 bytes passes, and 1025 bytes made of multi-byte characters refuses', () => {
const prefix = 'Harbor Notes/';
const exact = `${prefix}${'a'.repeat(1024 - prefix.length - 3)}.md`;
assert.equal(Buffer.byteLength(exact), 1024);
assert.equal(supportingPath(m, 'harbor', exact, tree), exact);
// é is two bytes: 1025 bytes but far fewer characters.
const name = 'é'.repeat((1025 - prefix.length - 3 - 1) / 2);
const long = `${prefix}${name}a.md`;
assert.equal(Buffer.byteLength(long), 1025);
assert.ok(long.length < 1024);
refuses('harbor', long, /is longer than 1024 bytes/);
});
test('C2-01d: a path outside the universe\'s supporting folders refuses', () => {
const outside = /is not under a supporting folder of universe/;
for (const path of ['Inbox Note.md', 'Book One/Notes/Board.canvas', 'Book One/Notes/Table.base', 'Book One/Notes/cover.png',
'Book One/Characters/Mara Vell.md', 'Book One/Sketches/Idea.md', 'Drafts/New World/Ana.md', 'Second Notes/A.md', 'Book One/Notes.md'])
refuses('harbor', path, outside);
refuses('idea', 'Book One/Notes/A.md', /universe "idea"/);
refuses('nowhere', 'Book One/Notes/A.md', /universe "nowhere"/);
});
test('C2-01d: malformed paths refuse by class, in order', () => {
refuses('harbor', '/Book One/Notes/A.md', /"\/Book One\/Notes\/A.md" is absolute/);
for (const path of ['Book One/Notes/../Notes/A.md', 'Book One/Notes/./A.md', 'Book One//Notes/A.md', 'Book One/Notes/A/', 'Book One/Notes/..'])
refuses('harbor', path, /has an empty, \. or \.\. segment/);
refuses('harbor', 'Book One/Notes/A\\B.md', /holds a backslash/);
for (const c of ['\u0000', '\u0001', '\n', '\u001f', '\u007f', '\u0085', '\u009f'])
assert.throws(() => supportingPath(m, 'harbor', `Book One/Notes/A${c}.md`, tree),
e => e.code === 'library-write-story-path' && e.message === 'library-write-story-path: the path holds a control character');
for (const path of [7, null, undefined, 1n, ['Book One/Notes/A.md']]) refuses('harbor', path, /the path must be a string/);
// A backslash in a path that is also absolute names the backslash: the order is fixed.
refuses('harbor', '/a\\b.md', /holds a backslash/);
assert.throws(() => supportingPath(m, 'harbor', 'Book One/Notes/A.md', { 'Book One/Notes/A.md': '100644' }), TypeError);
assert.throws(() => supportingPath(m, 'harbor', 'Book One/Notes/A.md'), TypeError);
});
test('C2-01d: a symlink or gitlink on the way, or at the path itself, refuses', () => {
refuses('harbor', 'Book One/Notes/Linked/A.md', /goes through the symlink or gitlink "Book One\/Notes\/Linked"/);
refuses('harbor', 'Book One/Notes/Vendor/Deep/A.md', /goes through the symlink or gitlink "Book One\/Notes\/Vendor"/);
refuses('harbor', 'Book One/Notes/Pointer.md', /goes through the symlink or gitlink "Book One\/Notes\/Pointer.md"/);
});
test('C2-01d: a name that differs from a tree name only in case or Unicode form refuses, at the file or any folder', () => {
refuses('harbor', 'Book One/Notes/existing.md', /differs only in case or Unicode form from "Book One\/Notes\/Existing.md"/);
refuses('harbor', 'Book One/Notes/case/New.md', /differs only in case or Unicode form from "Book One\/Notes\/Case"/);
refuses('harbor', 'Book One/Notes/CASE/Twin.md', /from "Book One\/Notes\/Case"/);
refuses('harbor', 'Harbor notes/A.md', /is not under a supporting folder/);
// NFD e + combining acute against the NFC name in the tree: one file on APFS.
refuses('harbor', 'Book One/Notes/Café.md', /differs only in case or Unicode form from "Book One\/Notes\/Café.md"/);
// A tree that already holds case twins refuses a write to either (R7).
const twins = [...tree, blob('Book One/Notes/existing.md')];
for (const path of ['Book One/Notes/Existing.md', 'Book One/Notes/existing.md'])
assert.throws(() => supportingPath(m, 'harbor', path, twins), e => e.code === 'library-write-story-path' && /differs only in case/.test(e.message));
});