// vsix-fetch.test.mjs — Unit tests for URL detection + body capping. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { detectUrlType, __testing } from '../../scanners/lib/vsix-fetch.mjs'; const { isAllowedHost, readBodyCapped, MAX_VSIX_BYTES } = __testing; describe('detectUrlType', () => { it('detects VS Code Marketplace URL', () => { const out = detectUrlType('https://marketplace.visualstudio.com/items?itemName=ms-python.python'); assert.equal(out.type, 'marketplace'); assert.equal(out.publisher, 'ms-python'); assert.equal(out.name, 'python'); }); it('returns unknown for marketplace URL without itemName', () => { const out = detectUrlType('https://marketplace.visualstudio.com/items'); assert.equal(out.type, 'unknown'); }); it('returns unknown for marketplace itemName without dot', () => { const out = detectUrlType('https://marketplace.visualstudio.com/items?itemName=foobar'); assert.equal(out.type, 'unknown'); }); it('detects OpenVSX URL with version', () => { const out = detectUrlType('https://open-vsx.org/extension/anthropic/claude-code/1.2.3'); assert.equal(out.type, 'openvsx'); assert.equal(out.publisher, 'anthropic'); assert.equal(out.name, 'claude-code'); assert.equal(out.version, '1.2.3'); }); it('detects OpenVSX URL without version', () => { const out = detectUrlType('https://open-vsx.org/extension/anthropic/claude-code'); assert.equal(out.type, 'openvsx'); assert.equal(out.publisher, 'anthropic'); assert.equal(out.name, 'claude-code'); assert.equal(out.version, null); }); it('detects direct .vsix download', () => { const out = detectUrlType('https://example.com/path/extension.vsix'); assert.equal(out.type, 'vsix'); }); it('detects GitHub URL as github (unsupported)', () => { const out = detectUrlType('https://github.com/anthropic/claude-code'); assert.equal(out.type, 'github'); }); it('rejects plain HTTP', () => { const out = detectUrlType('http://marketplace.visualstudio.com/items?itemName=ms-python.python'); assert.equal(out.type, 'unknown'); }); it('returns unknown for malformed URL', () => { const out = detectUrlType('not a url'); assert.equal(out.type, 'unknown'); }); it('returns unknown for unrelated HTTPS URL', () => { const out = detectUrlType('https://example.com/somefile.zip'); assert.equal(out.type, 'unknown'); }); }); describe('isAllowedHost', () => { it('allows marketplace gallery cdn for marketplace fetches', () => { assert.equal(isAllowedHost('foo.gallerycdn.vsassets.io', 'marketplace'), true); assert.equal(isAllowedHost('marketplace.visualstudio.com', 'marketplace'), true); }); it('rejects unrelated host for marketplace fetches', () => { assert.equal(isAllowedHost('evil.example.com', 'marketplace'), false); }); it('allows openvsx blob storage', () => { assert.equal(isAllowedHost('open-vsx.org', 'openvsx'), true); assert.equal(isAllowedHost('openvsxorg.blob.core.windows.net', 'openvsx'), true); }); it('rejects unrelated host for openvsx fetches', () => { assert.equal(isAllowedHost('evil.example.com', 'openvsx'), false); }); }); describe('readBodyCapped', () => { function makeStreamResponse(chunks) { const stream = new ReadableStream({ start(controller) { for (const chunk of chunks) controller.enqueue(chunk); controller.close(); }, }); return new Response(stream); } it('reads small body fully and computes SHA-256', async () => { const data = new TextEncoder().encode('hello world'); const res = makeStreamResponse([data]); const ctrl = new AbortController(); const out = await readBodyCapped(res, ctrl); assert.equal(out.size, 11); assert.equal(out.buffer.toString('utf8'), 'hello world'); // sha256("hello world") assert.equal(out.sha256, 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'); }); it('aborts when body exceeds MAX_VSIX_BYTES', async () => { // Stream a small chunk repeated such that total > cap. const chunkSize = 1024 * 1024; const chunk = new Uint8Array(chunkSize); const totalChunks = Math.ceil(MAX_VSIX_BYTES / chunkSize) + 2; // overshoot const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < totalChunks; i++) controller.enqueue(chunk); controller.close(); }, }); const res = new Response(stream); const ctrl = new AbortController(); await assert.rejects(() => readBodyCapped(res, ctrl), /exceeds maximum size/); }); });