import { afterEach, describe, expect, it, spyOn } from 'bun:test' import { handleOpenTargetsApi } from '../api/open-targets.js' import { openTargetService } from '../services/openTargetService.js' let listTargetsSpy: ReturnType | undefined let openTargetSpy: ReturnType | undefined let getTargetIconSpy: ReturnType | undefined function makeRequest( method: string, urlStr: string, body?: Record | string, ): { req: Request; url: URL; segments: string[] } { const url = new URL(urlStr, 'http://localhost:3456') const init: RequestInit = { method } if (body !== undefined) { init.headers = { 'Content-Type': 'application/json' } init.body = typeof body === 'string' ? body : JSON.stringify(body) } const req = new Request(url.toString(), init) return { req, url, segments: url.pathname.split('/').filter(Boolean), } } describe('open-targets API', () => { afterEach(() => { listTargetsSpy?.mockRestore() listTargetsSpy = undefined openTargetSpy?.mockRestore() openTargetSpy = undefined getTargetIconSpy?.mockRestore() getTargetIconSpy = undefined }) it('returns detected targets from GET /api/open-targets', async () => { listTargetsSpy = spyOn(openTargetService, 'listTargets').mockResolvedValue({ platform: 'darwin', targets: [ { id: 'vscode', kind: 'ide', label: 'VS Code', icon: 'vscode', platform: 'darwin' }, ], primaryTargetId: 'vscode', cachedAt: 123, ttlMs: 1_000, }) const { req, url, segments } = makeRequest('GET', '/api/open-targets') const res = await handleOpenTargetsApi(req, url, segments) expect(res.status).toBe(200) expect(listTargetsSpy).toHaveBeenCalledTimes(1) await expect(res.json()).resolves.toMatchObject({ platform: 'darwin', primaryTargetId: 'vscode', targets: [{ id: 'vscode', kind: 'ide' }], }) }) it('opens an allowed target from POST /api/open-targets/open', async () => { openTargetSpy = spyOn(openTargetService, 'openTarget').mockResolvedValue({ ok: true, targetId: 'vscode', path: '/Users/nanmi/project', }) const { req, url, segments } = makeRequest('POST', '/api/open-targets/open', { targetId: 'vscode', path: '/Users/nanmi/project', }) const res = await handleOpenTargetsApi(req, url, segments) expect(res.status).toBe(200) expect(openTargetSpy).toHaveBeenCalledWith({ targetId: 'vscode', path: '/Users/nanmi/project', }) await expect(res.json()).resolves.toMatchObject({ ok: true, targetId: 'vscode', path: '/Users/nanmi/project', }) }) it('rejects invalid request bodies before opening', async () => { openTargetSpy = spyOn(openTargetService, 'openTarget') const { req, url, segments } = makeRequest('POST', '/api/open-targets/open', { targetId: 'vscode' }) const res = await handleOpenTargetsApi(req, url, segments) expect(res.status).toBe(400) expect(openTargetSpy).not.toHaveBeenCalled() await expect(res.json()).resolves.toMatchObject({ error: 'BAD_REQUEST', message: 'Missing or invalid "path" in request body', }) }) it('rejects invalid JSON bodies', async () => { const { req, url, segments } = makeRequest('POST', '/api/open-targets/open', '{not json') const res = await handleOpenTargetsApi(req, url, segments) expect(res.status).toBe(400) await expect(res.json()).resolves.toMatchObject({ error: 'BAD_REQUEST', message: 'Invalid JSON body', }) }) it('returns a target icon as cacheable PNG', async () => { getTargetIconSpy = spyOn(openTargetService, 'getTargetIcon').mockResolvedValue({ contentType: 'image/png', data: new Uint8Array([1, 2, 3]), }) const { req, url, segments } = makeRequest('GET', '/api/open-targets/icons/vscode') const res = await handleOpenTargetsApi(req, url, segments) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/png') expect(res.headers.get('Cache-Control')).toBe('private, max-age=86400') expect(getTargetIconSpy).toHaveBeenCalledWith('vscode') expect(Array.from(new Uint8Array(await res.arrayBuffer()))).toEqual([1, 2, 3]) }) })