import Fastify from 'fastify'; import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../src/services/ytDlpRunner.js', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, runYtDlp: vi.fn() }; }); import { playlistRoutes } from '../src/routes/playlist.js'; import { YtDlpError, runYtDlp } from '../src/services/ytDlpRunner.js'; const validUrl = encodeURIComponent('https://www.youtube.com/playlist?list=PLKDZ1ig0uz-U'); beforeEach(() => { vi.mocked(runYtDlp).mockReset(); }); describe('GET /api/playlist', () => { it('retourne le titre et les vidéos d’une playlist valide', async () => { vi.mocked(runYtDlp).mockResolvedValue( JSON.stringify({ _type: 'playlist', title: 'Ma playlist', entries: [ { id: 'v1', title: 'Vidéo 1', url: 'https://www.youtube.com/watch?v=v1', duration: 90, thumbnails: [{ url: 'https://x/v1.jpg' }], }, ], }), ); const app = Fastify(); await app.register(playlistRoutes); const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` }); expect(response.statusCode).toBe(200); expect(response.json()).toEqual({ title: 'Ma playlist', videos: [ { id: 'v1', title: 'Vidéo 1', url: 'https://www.youtube.com/watch?v=v1', thumbnailUrl: 'https://x/v1.jpg', durationSeconds: 90, uploadDate: null, }, ], }); }); it.each([ ['un domaine étranger', 'https://evil.com/playlist?list=PLabc'], ['un lien de vidéo', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'], ['un lien de vidéo avec list', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLabc'], ['du texte quelconque', 'pas une url'], ])('renvoie 400 pour %s, sans appeler yt-dlp', async (_label, url) => { const app = Fastify(); await app.register(playlistRoutes); const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${encodeURIComponent(url)}`, }); expect(response.statusCode).toBe(400); expect(runYtDlp).not.toHaveBeenCalled(); }); it('renvoie 400 si le paramètre url est manquant (validation de schéma)', async () => { const app = Fastify(); await app.register(playlistRoutes); const response = await app.inject({ method: 'GET', url: '/api/playlist' }); expect(response.statusCode).toBe(400); }); it('renvoie 502 quand yt-dlp échoue (ex. playlist introuvable)', async () => { vi.mocked(runYtDlp).mockRejectedValue(new YtDlpError(1, 'The playlist does not exist.')); const app = Fastify(); await app.register(playlistRoutes); const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` }); expect(response.statusCode).toBe(502); }); it('renvoie 502 (pas 500) quand le binaire yt-dlp est introuvable (ENOENT)', async () => { const spawnError = Object.assign(new Error('spawn yt-dlp ENOENT'), { code: 'ENOENT' }); vi.mocked(runYtDlp).mockRejectedValue(spawnError); const app = Fastify(); await app.register(playlistRoutes); const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` }); expect(response.statusCode).toBe(502); }); it('renvoie 502 (pas 500) quand la sortie de yt-dlp est illisible', async () => { vi.mocked(runYtDlp).mockResolvedValue('pas du JSON'); const app = Fastify(); await app.register(playlistRoutes); const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` }); expect(response.statusCode).toBe(502); }); });