79 lines
4.3 KiB
JavaScript
79 lines
4.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtemp, mkdir, writeFile, symlink, rm, truncate } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { readDocument } from './file-access.mjs';
|
|
import { resolveDocumentLink } from './file-links.mjs';
|
|
|
|
test('relative report/source links preserve document context and source lines', () => {
|
|
const from = 'docs/studies/14-engine-blueprint.md';
|
|
const cases = [
|
|
['09-unity-ux-extensibility-study.md', 'docs/studies/09-unity-ux-extensibility-study.md', null],
|
|
['../ARCHITECTURE.md', 'docs/ARCHITECTURE.md', null],
|
|
['../../../UnrealEngine/Engine/example.cpp:123', '../UnrealEngine/Engine/example.cpp', '123'],
|
|
['../../../godot/scene/main/node.cpp#L77', '../godot/scene/main/node.cpp', '77'],
|
|
['report.md:12', 'docs/studies/report.md', '12'],
|
|
['a%20b.md', 'docs/studies/a b.md', null],
|
|
];
|
|
for (const [href, expected, line] of cases) {
|
|
const result = new URL(resolveDocumentLink(href, from), 'http://localhost:4178');
|
|
assert.equal(result.searchParams.get('file'), expected);
|
|
assert.equal(result.searchParams.get('line'), line);
|
|
}
|
|
assert.equal(resolveDocumentLink('https://example.com/docs', from), 'https://example.com/docs');
|
|
assert.equal(resolveDocumentLink('#section', from), '#section');
|
|
for (const name of ['README.md', 'PLAN.md']) {
|
|
const result = new URL(resolveDocumentLink('../../../' + name, 'docs/studies/map/README.md'), 'http://localhost:4178');
|
|
assert.equal(result.searchParams.get('file'), name);
|
|
}
|
|
for (const href of ['javascript:alert(1)', 'data:text/plain,hello', '//example.com/file', '/etc/passwd', 'C:\\private.md']) assert.equal(resolveDocumentLink(href, from), null);
|
|
});
|
|
|
|
test('file access is restricted to documentation and available sibling checkouts', async t => {
|
|
const workspace = await mkdtemp(path.join(tmpdir(), 'faset-map-'));
|
|
t.after(() => rm(workspace, { recursive: true, force: true }));
|
|
const project = path.join(workspace, 'Faset_Engine');
|
|
const files = {
|
|
'Faset_Engine/README.md': 'project',
|
|
'Faset_Engine/PLAN.md': 'plan',
|
|
'Faset_Engine/docs/ARCHITECTURE.md': 'architecture',
|
|
'Faset_Engine/docs/studies/report.md': 'report',
|
|
'Faset_Engine/private.md': 'private',
|
|
'Faset_Engine/docs/.private.md': 'hidden',
|
|
'Faset_Engine/docs/node_modules/private.md': 'dependency',
|
|
'Faset_Engine/docs/program.exe': 'binary',
|
|
'outside.md': 'outside',
|
|
'UnrealEngine/Engine/example.cpp': 'source',
|
|
'UnrealEngine-other/example.cpp': 'wrong root',
|
|
};
|
|
for (const [name, text] of Object.entries(files)) {
|
|
const target = path.join(workspace, name);
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
await writeFile(target, text);
|
|
}
|
|
assert.equal((await readDocument(project, 'README.md')).text, 'project');
|
|
assert.equal((await readDocument(project, 'PLAN.md')).text, 'plan');
|
|
assert.equal((await readDocument(project, 'docs/studies/../ARCHITECTURE.md')).path, 'docs/ARCHITECTURE.md');
|
|
assert.equal((await readDocument(project, '../UnrealEngine/Engine/example.cpp')).path, '../UnrealEngine/Engine/example.cpp');
|
|
for (const requested of ['private.md', '../outside.md', '../UnrealEngine-other/example.cpp', 'docs/.private.md', 'docs/node_modules/private.md', 'docs/program.exe', path.join(project, 'README.md')]) {
|
|
await assert.rejects(readDocument(project, requested), { status: 403 });
|
|
}
|
|
await assert.rejects(readDocument(project, '../godot/missing.cpp'), { code: 'ENOENT' });
|
|
await writeFile(path.join(project, 'docs/large.txt'), '');
|
|
await truncate(path.join(project, 'docs/large.txt'), 8 * 1024 * 1024 + 1);
|
|
await assert.rejects(readDocument(project, 'docs/large.txt'), { status: 403 });
|
|
try {
|
|
await symlink(path.join(workspace, 'outside.md'), path.join(project, 'docs/escape.md'));
|
|
} catch (error) {
|
|
if (error.code === 'EPERM') { t.diagnostic('Symlink case skipped: creating symlinks is not permitted on this host.'); return; }
|
|
throw error;
|
|
}
|
|
await assert.rejects(readDocument(project, 'docs/escape.md'), { status: 403 });
|
|
for (const name of ['README.md', 'PLAN.md']) {
|
|
await rm(path.join(project, name));
|
|
await symlink(path.join(workspace, 'outside.md'), path.join(project, name));
|
|
await assert.rejects(readDocument(project, name), { status: 403 });
|
|
}
|
|
});
|