import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createHash } from 'node:crypto'; import { startSpotify, finishSpotify, spotifyStatus, searchSpotify } from '../server/spotify.mjs'; test('PKCE state is single use and token persists privately; exact artist ranks first', async () => { const dir = mkdtempSync(join(tmpdir(), 'spotify-test-')); process.env.DATA_DIR = dir; process.env.SPOTIFY_CLIENT_ID = 'test-client'; const original = globalThis.fetch; let verifier; globalThis.fetch = async (url, options) => { if (String(url).includes('/api/token')) { verifier = options.body.get('code_verifier'); return Response.json({ access_token: 'access', refresh_token: 'refresh', expires_in: 3600 }); } return Response.json({ tracks: { items: [{ uri: 'spotify:track:1', name: 'Song', type: 'track' }] }, artists: { items: [{ uri: 'spotify:artist:1', name: 'Aphex Twin', type: 'artist' }] } }); }; try { const url = new URL(startSpotify()), state = url.searchParams.get('state'); await assert.rejects(finishSpotify('wrong', 'code')); await finishSpotify(state, 'code'); assert.equal(createHash('sha256').update(verifier).digest('base64url'), url.searchParams.get('code_challenge')); await assert.rejects(finishSpotify(state, 'code')); assert.equal(statSync(join(dir, 'spotify.json')).mode & 0o777, 0o600); assert.equal(JSON.parse(readFileSync(join(dir, 'spotify.json'))).refresh_token, 'refresh'); assert.deepEqual(spotifyStatus(), { configured: true, connected: true }); assert.equal((await searchSpotify('aphex twin'))[0].type, 'artist'); } finally { globalThis.fetch = original; rmSync(dir, { recursive: true, force: true }); } });