import { describe, it, expect } from 'vitest' import { buildTestApp, setupAdmin, auth } from './helpers.js' import { discogsSearchFixture, discogsReleaseFixture } from './fixtures.js' function stubFetch(routes: (url: string) => Response): typeof fetch { return (async (input: any) => routes(String(input))) as typeof fetch } function discogsStub(): typeof fetch { return stubFetch((url) => { if (url.includes('/database/search')) { return new Response(JSON.stringify(discogsSearchFixture), { status: 200, headers: { 'content-type': 'application/json' }, }) } if (url.includes('/releases/1001')) { return new Response(JSON.stringify(discogsReleaseFixture), { status: 200, headers: { 'content-type': 'application/json' }, }) } return new Response('nope', { status: 404 }) }) } async function appWithToken(discogsFetch: typeof fetch) { const app = await buildTestApp(discogsFetch) const cookie = await setupAdmin(app) await app.inject({ method: 'PUT', url: '/api/settings', ...auth(cookie), payload: { discogsToken: 'testtoken' }, }) return { app, cookie } } describe('GET /api/lookup/barcode/:code', () => { it('returns candidates on match', async () => { const { app, cookie } = await appWithToken(discogsStub()) const res = await app.inject({ method: 'GET', url: '/api/lookup/barcode/5021592210629', ...auth(cookie) }) expect(res.statusCode).toBe(200) const body = res.json() expect(body.candidates).toHaveLength(2) expect(body.candidates[0]).toMatchObject({ id: 1001, artist: 'The Cinematic Orchestra', title: 'Motion', year: 1999, }) await app.close() }) it('returns 404 not_found when discogs has zero results', async () => { const empty = stubFetch((url) => url.includes('/database/search') ? new Response(JSON.stringify({ results: [] }), { status: 200, headers: { 'content-type': 'application/json' }, }) : new Response('nope', { status: 404 }) ) const { app, cookie } = await appWithToken(empty) const res = await app.inject({ method: 'GET', url: '/api/lookup/barcode/000', ...auth(cookie) }) expect(res.statusCode).toBe(404) expect(res.json()).toEqual({ error: 'not_found' }) await app.close() }) it('returns 409 no_discogs_token when token unset', async () => { const app = await buildTestApp() const cookie = await setupAdmin(app) const res = await app.inject({ method: 'GET', url: '/api/lookup/barcode/000', ...auth(cookie) }) expect(res.statusCode).toBe(409) expect(res.json()).toEqual({ error: 'no_discogs_token' }) await app.close() }) it('maps discogs auth failure to 502 discogs_auth', async () => { const unauthorized = stubFetch(() => new Response('bad token', { status: 401 })) const { app, cookie } = await appWithToken(unauthorized) const res = await app.inject({ method: 'GET', url: '/api/lookup/barcode/000', ...auth(cookie) }) expect(res.statusCode).toBe(502) expect(res.json()).toEqual({ error: 'discogs_auth' }) await app.close() }) }) describe('GET /api/lookup/search', () => { it('returns candidates for text query with format filter', async () => { const seen: string[] = [] const { app, cookie } = await appWithToken( stubFetch((url) => { seen.push(url) return new Response(JSON.stringify(discogsSearchFixture), { status: 200, headers: { 'content-type': 'application/json' }, }) }) ) const res = await app.inject({ method: 'GET', url: '/api/lookup/search?q=motion&format=Vinyl', ...auth(cookie) }) expect(res.statusCode).toBe(200) expect(seen[0]).toContain('q=motion') expect(seen[0]).toContain('format=Vinyl') expect(res.json().candidates).toHaveLength(2) await app.close() }) }) describe('GET /api/lookup/release/:id', () => { it('returns full release with duplicate and ripMatch', async () => { const { app, cookie } = await appWithToken(discogsStub()) const res = await app.inject({ method: 'GET', url: '/api/lookup/release/1001', ...auth(cookie) }) expect(res.statusCode).toBe(200) const body = res.json() expect(body.release).toMatchObject({ id: 1001, title: 'Motion', artist: 'The Cinematic Orchestra', barcodes: ['5021592210629'], }) expect(body.duplicate).toBe(false) expect(body.ripMatch).toBe('not_ripped') expect(body.matchCandidates).toEqual([]) await app.close() }) it('caches release payloads on disk (second lookup costs no discogs call)', async () => { let releaseCalls = 0 const { app, cookie } = await appWithToken( stubFetch((url) => { if (url.includes('/releases/1001')) { releaseCalls++ return new Response(JSON.stringify(discogsReleaseFixture), { status: 200, headers: { 'content-type': 'application/json' }, }) } return new Response('nope', { status: 404 }) }) ) await app.inject({ method: 'GET', url: '/api/lookup/release/1001', ...auth(cookie) }) await app.inject({ method: 'GET', url: '/api/lookup/release/1001', ...auth(cookie) }) expect(releaseCalls).toBe(1) await app.close() }) it('rejects non-integer release ids with 400', async () => { const { app, cookie } = await appWithToken(discogsStub()) const res = await app.inject({ method: 'GET', url: '/api/lookup/release/abc', ...auth(cookie) }) expect(res.statusCode).toBe(400) expect(res.json()).toEqual({ error: 'invalid_input' }) await app.close() }) it('reports duplicate when release already in collection', async () => { const { app, cookie } = await appWithToken(discogsStub()) await app.inject({ method: 'POST', url: '/api/collection', ...auth(cookie), payload: { releaseId: 1001 } }) const res = await app.inject({ method: 'GET', url: '/api/lookup/release/1001', ...auth(cookie) }) expect(res.json().duplicate).toBe(true) await app.close() }) it('reports ripped on confident match against digital albums', async () => { const { app, cookie } = await appWithToken(discogsStub()) await app.inject({ method: 'POST', url: '/api/library/albums/test-seed', ...auth(cookie), payload: { albums: [{ subsonicId: 'a1', title: 'Motion', artist: 'The Cinematic Orchestra' }] }, }) const res = await app.inject({ method: 'GET', url: '/api/lookup/release/1001', ...auth(cookie) }) expect(res.json().ripMatch).toBe('ripped') await app.close() }) it('reports ambiguous with matchCandidates on same-title different-artist', async () => { const { app, cookie } = await appWithToken(discogsStub()) await app.inject({ method: 'POST', url: '/api/library/albums/test-seed', ...auth(cookie), payload: { albums: [{ subsonicId: 'a1', title: 'Motion', artist: 'Somebody Else' }] }, }) const res = await app.inject({ method: 'GET', url: '/api/lookup/release/1001', ...auth(cookie) }) const body = res.json() expect(body.ripMatch).toBe('ambiguous') expect(body.matchCandidates).toHaveLength(1) expect(body.matchCandidates[0]).toMatchObject({ title: 'Motion', artist: 'Somebody Else' }) await app.close() }) })