// jetbrains-fetch.test.mjs — Integration tests for `/security ide-scan ` // with a JetBrains Marketplace URL. Mocks `globalThis.fetch` so we never hit // real plugins.jetbrains.com endpoints. `useSandbox: false` is required because // mocks do not cross process boundaries — this mirrors the VSIX test strategy. // // Covers: // 1. Spawned worker emits well-formed JSON when fed a bogus URL (sub-process // path — no mock, we just assert the IPC contract holds). // 2. End-to-end `scan()` on a `/plugin/-` URL resolves // numericId → xmlId via metadata, then downloads + extracts. // 3. End-to-end `scan()` on a `/plugin/download?pluginId=` URL // skips the metadata round-trip and downloads directly. // 4. Network failure / malformed archive bubble up as warnings. // 5. URL kind discriminator (`meta.source.kind === 'jetbrains'`) distinguishes // JetBrains plugins from VS Code extensions in the envelope. // // See: plan step 12 (`ultraplan-2026-04-17-jetbrains-ide-scan.md`). import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { scan } from '../../scanners/ide-extension-scanner.mjs'; import { createZip } from '../helpers/zip-writer.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const JB_WORKER_PATH = join( __dirname, '..', '..', 'scanners', 'lib', 'jetbrains-fetch-worker.mjs', ); const realFetch = globalThis.fetch; function mockBufferResponse(buffer, { status = 200 } = {}) { const stream = new ReadableStream({ start(controller) { controller.enqueue(buffer); controller.close(); }, }); return new Response(stream, { status, headers: { 'content-type': 'application/zip' }, }); } function jsonResponse(obj, { status = 200 } = {}) { return new Response(JSON.stringify(obj), { status, headers: { 'content-type': 'application/json' }, }); } function installFetchRouter(routes) { globalThis.fetch = async (url) => { const handler = routes(String(url)); if (!handler) throw new Error(`unrouted fetch: ${url}`); return handler; }; } // Build a synthetic JetBrains plugin archive with the layout // /lib/.jar → containing META-INF/plugin.xml. // The outer archive is what plugins.jetbrains.com ships; the inner jar is what // parseIntelliJPlugin walks for the manifest. function buildBenignJetBrainsArchive() { const pluginXml = ` com.example.benign Benign 1.0.0 Example `; const innerJar = createZip([ { name: 'META-INF/plugin.xml', data: pluginXml }, { name: 'META-INF/MANIFEST.MF', data: 'Manifest-Version: 1.0\n' }, ]); return createZip([ { name: 'com.example.benign/lib/main.jar', data: innerJar }, ]); } // --------------------------------------------------------------------------- // 1. Worker IPC contract // --------------------------------------------------------------------------- describe('jetbrains-fetch-worker — IPC contract', () => { it('emits ok:false JSON on missing args and exits 1', async () => { const child = spawn('node', [JB_WORKER_PATH], { stdio: ['ignore', 'pipe', 'pipe'] }); let out = ''; child.stdout.on('data', (c) => { out += c.toString('utf8'); }); const code = await new Promise((resolve) => child.on('close', resolve)); assert.equal(code, 1); const parsed = JSON.parse(out.trim()); assert.equal(parsed.ok, false); assert.match(parsed.error, /missing --url or --tmpdir/); }); it('emits ok:false JSON when given a non-JetBrains URL', async () => { // Reject non-JetBrains URLs at the worker level — defense-in-depth in case // orchestrator routes a wrong URL to the JB worker. const child = spawn( 'node', [JB_WORKER_PATH, '--url', 'https://example.com/x.vsix', '--tmpdir', '/tmp'], { stdio: ['ignore', 'pipe', 'pipe'] }, ); let out = ''; child.stdout.on('data', (c) => { out += c.toString('utf8'); }); const code = await new Promise((resolve) => child.on('close', resolve)); assert.equal(code, 1); const parsed = JSON.parse(out.trim()); assert.equal(parsed.ok, false); assert.match(parsed.error, /expected JetBrains URL/); }); }); // --------------------------------------------------------------------------- // 2. End-to-end scan() with mocked fetch // --------------------------------------------------------------------------- describe('ide-extension-scanner — JetBrains URL mode', () => { before(() => resetCounter()); after(() => { globalThis.fetch = realFetch; }); it('resolves numericId → xmlId via metadata, then downloads + scans', async () => { const archive = buildBenignJetBrainsArchive(); const calls = []; installFetchRouter((url) => { calls.push(url); if (/\/api\/plugins\/7973$/.test(url)) { return jsonResponse({ xmlId: 'com.example.benign' }); } if (/\/plugin\/download\?pluginId=com\.example\.benign/.test(url)) { return mockBufferResponse(archive); } return null; }); const env = await scan( 'https://plugins.jetbrains.com/plugin/7973-benign', { useSandbox: false }, ); // Two fetches: metadata + download. assert.equal(calls.length, 2, `calls: ${calls.join(', ')}`); assert.match(calls[0], /\/api\/plugins\/7973/); assert.match(calls[1], /\/plugin\/download\?pluginId=com\.example\.benign/); // Envelope shape. assert.ok(env.meta.source, 'expected meta.source to be set'); assert.equal(env.meta.source.type, 'url'); assert.equal(env.meta.source.kind, 'jetbrains'); assert.equal(env.meta.source.xmlId, 'com.example.benign'); assert.equal(env.meta.source.numericId, '7973'); assert.match(env.meta.source.sha256, /^[a-f0-9]{64}$/); assert.equal(env.meta.source.sandbox, 'in-process'); assert.equal(env.meta.target, 'https://plugins.jetbrains.com/plugin/7973-benign'); // Scanner parsed the inner plugin.xml and produced exactly one JB extension. assert.equal(env.extensions.length, 1); assert.equal(env.extensions[0].type, 'jetbrains'); assert.equal(env.extensions[0].id, 'com.example.benign'); assert.equal(env.extensions[0].version, '1.0.0'); }); it('downloads by xmlId directly (no metadata round-trip)', async () => { const archive = buildBenignJetBrainsArchive(); let metaCalled = false; let downloadCalled = false; installFetchRouter((url) => { if (/\/api\/plugins\//.test(url)) { metaCalled = true; return jsonResponse({ xmlId: 'should.not.be.used' }); } if (/\/plugin\/download\?pluginId=com\.example\.benign/.test(url)) { downloadCalled = true; return mockBufferResponse(archive); } return null; }); const env = await scan( 'https://plugins.jetbrains.com/plugin/download?pluginId=com.example.benign', { useSandbox: false }, ); assert.equal(metaCalled, false, 'metadata should not be fetched when xmlId is explicit'); assert.equal(downloadCalled, true); assert.equal(env.meta.source.kind, 'jetbrains'); assert.equal(env.meta.source.xmlId, 'com.example.benign'); assert.equal(env.extensions.length, 1); assert.equal(env.extensions[0].type, 'jetbrains'); }); it('passes version query through unchanged', async () => { const archive = buildBenignJetBrainsArchive(); const calls = []; installFetchRouter((url) => { calls.push(url); if (/\/plugin\/download\?pluginId=com\.example\.benign/.test(url)) { return mockBufferResponse(archive); } return null; }); await scan( 'https://plugins.jetbrains.com/plugin/download?pluginId=com.example.benign&version=2.3.4', { useSandbox: false }, ); assert.equal(calls.length, 1); assert.match(calls[0], /version=2\.3\.4/); }); it('reports fetch network failure as a warning, no extensions scanned', async () => { installFetchRouter(() => { throw new Error('ECONNREFUSED'); }); const env = await scan( 'https://plugins.jetbrains.com/plugin/download?pluginId=com.example.benign', { useSandbox: false }, ); assert.equal(env.extensions.length, 0); assert.ok( env.meta.warnings.some((w) => /URL fetch\/extract failed/.test(w)), `warnings: ${env.meta.warnings.join(' | ')}`, ); }); it('reports malformed archive as a warning, no extensions scanned', async () => { installFetchRouter((url) => { if (/\/plugin\/download/.test(url)) { return mockBufferResponse(Buffer.from('not a zip at all')); } return null; }); const env = await scan( 'https://plugins.jetbrains.com/plugin/download?pluginId=com.example.benign', { useSandbox: false }, ); assert.equal(env.extensions.length, 0); assert.ok( env.meta.warnings.some((w) => /malformed plugin archive|URL fetch\/extract failed/.test(w)), `warnings: ${env.meta.warnings.join(' | ')}`, ); }); it('cannot reach JetBrains path via the VS Code-only toggle', async () => { // Sanity: --vscode-only should not short-circuit URL fetches, but also // shouldn't gate on extension type (URL scan fetches regardless of toggle). const archive = buildBenignJetBrainsArchive(); installFetchRouter((url) => { if (/\/plugin\/download/.test(url)) return mockBufferResponse(archive); return null; }); const env = await scan( 'https://plugins.jetbrains.com/plugin/download?pluginId=com.example.benign', { useSandbox: false, vscodeOnly: true }, ); // The URL was explicitly JB; we still scan it. assert.equal(env.meta.source.kind, 'jetbrains'); assert.equal(env.extensions.length, 1); assert.equal(env.extensions[0].type, 'jetbrains'); }); });