// ide-extension-url.test.mjs — Integration tests for `/security ide-scan `. // Mocks global.fetch so we never hit real Marketplace / OpenVSX endpoints. import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { scan } from '../../scanners/ide-extension-scanner.mjs'; import { buildZip } from '../lib/build-zip.mjs'; const realFetch = globalThis.fetch; function mockResponse(buffer, { status = 200 } = {}) { const stream = new ReadableStream({ start(controller) { controller.enqueue(buffer); controller.close(); }, }); return new Response(stream, { status, headers: { 'content-type': 'application/octet-stream' } }); } function jsonResponse(obj) { return new Response(JSON.stringify(obj), { status: 200, headers: { 'content-type': 'application/json' }, }); } function buildBenignVsix() { const pkg = JSON.stringify({ publisher: 'anthropic', name: 'claude-code', version: '1.0.0', engines: { vscode: '^1.80.0' }, main: './extension.js', activationEvents: ['onCommand:claude.hello'], categories: ['Other'], }); return buildZip([ { name: 'extension.vsixmanifest', data: '' }, { name: 'extension/package.json', data: pkg }, { name: 'extension/extension.js', data: 'module.exports = { activate(){} };' }, ]); } function installFetchRouter(routes) { globalThis.fetch = async (url) => { const handler = routes(url); if (!handler) throw new Error(`unrouted fetch: ${url}`); return handler; }; } describe('ide-extension-scanner — URL mode', () => { before(() => resetCounter()); after(() => { globalThis.fetch = realFetch; }); it('rejects unsupported URL with a warning, no extensions scanned', async () => { installFetchRouter(() => null); const env = await scan('https://example.com/random.zip', { vscodeOnly: true, useSandbox: false }); assert.equal(env.extensions.length, 0); assert.ok(env.meta.warnings.some(w => /unsupported URL/i.test(w))); assert.equal(env.meta.source, null); }); it('reports github URL as unsupported in v6.4.0', async () => { installFetchRouter(() => null); const env = await scan('https://github.com/anthropic/claude-code', { vscodeOnly: true, useSandbox: false }); assert.equal(env.extensions.length, 0); assert.ok(env.meta.warnings.some(w => /GitHub repo URLs/i.test(w))); }); it('fetches OpenVSX VSIX and scans the extracted extension', async () => { const vsix = buildBenignVsix(); let metaCalled = false; let downloadCalled = false; installFetchRouter((url) => { if (url.endsWith('/latest')) { metaCalled = true; return jsonResponse({ version: '1.0.0' }); } if (url.includes('/file/') && url.endsWith('.vsix')) { downloadCalled = true; return mockResponse(vsix); } return null; }); const env = await scan('https://open-vsx.org/extension/anthropic/claude-code', { vscodeOnly: true, useSandbox: false }); assert.ok(metaCalled, 'expected metadata fetch for latest version'); assert.ok(downloadCalled, 'expected VSIX download'); assert.equal(env.extensions.length, 1); assert.equal(env.extensions[0].id, 'anthropic.claude-code'); assert.equal(env.extensions[0].version, '1.0.0'); assert.ok(env.meta.source); assert.equal(env.meta.source.type, 'url'); assert.equal(env.meta.source.publisher, 'anthropic'); assert.equal(env.meta.source.name, 'claude-code'); assert.equal(env.meta.source.version, '1.0.0'); assert.match(env.meta.source.sha256, /^[a-f0-9]{64}$/); assert.equal(env.meta.target, 'https://open-vsx.org/extension/anthropic/claude-code'); }); it('fetches Marketplace VSIX directly without metadata round-trip', async () => { const vsix = buildBenignVsix(); let downloads = 0; installFetchRouter((url) => { if (url.includes('Microsoft.VisualStudio.Services.VSIXPackage')) { downloads++; return mockResponse(vsix); } return null; }); const env = await scan('https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code', { vscodeOnly: true, useSandbox: false }); assert.equal(downloads, 1); assert.equal(env.extensions.length, 1); assert.equal(env.extensions[0].id, 'anthropic.claude-code'); assert.equal(env.meta.source.type, 'url'); assert.equal(env.meta.source.requestedUrl?.includes('VSIXPackage'), true); }); it('cleans up temp dir even when extraction fails', async () => { // Return a non-zip body so extract throws. installFetchRouter(() => mockResponse(Buffer.from('not a zip at all'))); const env = await scan('https://example.com/bad.vsix', { vscodeOnly: true, useSandbox: false }); assert.equal(env.extensions.length, 0); assert.ok(env.meta.warnings.some(w => /malformed VSIX/.test(w))); }); it('rejects zip-slip VSIX as malformed', async () => { const evil = buildZip([ { name: 'extension/package.json', data: '{}' }, { name: '../escape.txt', data: 'pwned' }, ]); installFetchRouter(() => mockResponse(evil)); const env = await scan('https://example.com/evil.vsix', { vscodeOnly: true, useSandbox: false }); assert.equal(env.extensions.length, 0); assert.ok(env.meta.warnings.some(w => /malformed VSIX/.test(w) && /traversal/.test(w))); }); it('handles fetch network failure cleanly', async () => { installFetchRouter(() => { throw new Error('ECONNREFUSED'); }); const env = await scan('https://open-vsx.org/extension/foo/bar', { vscodeOnly: true, useSandbox: false }); assert.equal(env.extensions.length, 0); assert.ok(env.meta.warnings.some(w => /URL fetch\/extract failed/.test(w))); }); });