93 lines
6.0 KiB
JavaScript
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));
|
|
});
|