mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Escape project names before building hand-test patterns, compare transport identifiers as literal tokens, preserve boundaries when extracting prose and XML facts, and require canonical containment for smoke static assets. Add regression coverage for metacharacters, dollar identifiers, markup joining, traversal, symlinks, and malformed URL encoding. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
65 lines
3.1 KiB
JavaScript
65 lines
3.1 KiB
JavaScript
import { afterEach, expect, it } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { Writable } from 'node:stream';
|
|
import { literalPattern } from './literal-pattern.mjs';
|
|
import { serveStaticAsset } from './smoke-static-assets.mjs';
|
|
|
|
const temporary = [];
|
|
afterEach(() => { for (const path of temporary.splice(0)) rmSync(path, { recursive: true, force: true }); });
|
|
|
|
it.each(['a.b', 'a+b', '(a+)+$', '[project]', 'a\\b'])('treats a project name as literal text: %s', name => {
|
|
const pattern = literalPattern(name, { start: true, end: true });
|
|
expect(pattern.test(name)).toBe(true);
|
|
expect(pattern.test(`prefix${name}`)).toBe(false);
|
|
expect(pattern.test(`${name}suffix`)).toBe(false);
|
|
});
|
|
|
|
it('does not expand project-name wildcards', () => {
|
|
expect(literalPattern('a.b', { start: true, end: true }).test('axb')).toBe(false);
|
|
});
|
|
|
|
function fixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'cbm-static-assets-')); temporary.push(root);
|
|
mkdirSync(join(root, 'spike')); mkdirSync(join(root, 'node_modules')); mkdirSync(join(root, 'spike-extra'));
|
|
writeFileSync(join(root, 'spike', 'index.html'), 'fixture');
|
|
writeFileSync(join(root, 'outside.txt'), 'outside-root');
|
|
writeFileSync(join(root, 'spike-extra', 'outside.txt'), 'sibling-root');
|
|
return root;
|
|
}
|
|
function request(root, url) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = []; let status;
|
|
const response = new Writable({ write(chunk, encoding, done) { chunks.push(Buffer.from(chunk)); done(); } });
|
|
response.writeHead = code => { status = code; };
|
|
response.on('error', reject);
|
|
response.on('finish', () => resolve({ status, body: Buffer.concat(chunks).toString() }));
|
|
try { serveStaticAsset(root, { url }, response); } catch (error) { reject(error); }
|
|
});
|
|
}
|
|
|
|
it('serves an allowed static fixture and rejects traversal and sibling prefixes', async () => {
|
|
const root = fixture();
|
|
expect(await request(root, '/spike/index.html')).toEqual({ status: 200, body: 'fixture' });
|
|
expect((await request(root, '/spike/../outside.txt')).status).toBe(404);
|
|
expect((await request(root, '/spike/%2e%2e/outside.txt')).status).toBe(404);
|
|
expect((await request(root, '/spike-extra/outside.txt')).status).toBe(404);
|
|
});
|
|
|
|
it('rejects a symlink escaping an allowed static root', async () => {
|
|
const root = fixture();
|
|
symlinkSync(join(root, 'outside.txt'), join(root, 'spike', 'escape.txt'));
|
|
expect((await request(root, '/spike/escape.txt')).status).toBe(404);
|
|
});
|
|
|
|
it('returns not found for malformed URL encoding instead of throwing', async () => {
|
|
expect((await request(fixture(), '/spike/%zz')).status).toBe(404);
|
|
});
|
|
|
|
it('allows a symlink whose real target stays inside the static root', async () => {
|
|
const root = fixture();
|
|
symlinkSync(join(root, 'spike', 'index.html'), join(root, 'spike', 'alias.html'));
|
|
expect(await request(root, '/spike/alias.html')).toEqual({ status: 200, body: 'fixture' });
|
|
});
|