youtubeDownloader/server/test/channelRoute.test.ts
Nicolas 0e78b44a55
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
feat(server): service de découverte de chaîne (GET /api/channel)
Première brique du pipeline : lister les vidéos/streams d'une chaîne
YouTube (yt-dlp --flat-playlist), filtrables par mots-clés sur le titre.

- ytDlpRunner.ts : exécution de yt-dlp via child_process.spawn (jamais de
  shell, arguments en tableau - exclut l'injection de commande)
- ChannelDiscoveryService.ts : validation stricte du handle/URL de chaîne
  + séparateur `--` avant l'URL (défense en profondeur contre l'injection
  d'options yt-dlp), parsing tolérant des lignes JSON, filtrage mots-clés
- routes/channel.ts : GET /api/channel (validation de schéma Fastify,
  400 si entrée invalide, 502 si yt-dlp échoue)
- 14 tests (unitaires services + intégration route via app.inject) - un
  test de route a révélé une regex de validation trop permissive qu'un
  test de service avait manquée, corrigée avant ce commit

La file d'attente et le téléchargement suivront dans une PR séparée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:05:29 +02:00

69 lines
2 KiB
TypeScript

import Fastify from 'fastify';
import { describe, expect, it, vi } from 'vitest';
vi.mock('../src/services/ytDlpRunner.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../src/services/ytDlpRunner.js')>();
return { ...actual, runYtDlp: vi.fn() };
});
import { channelRoutes } from '../src/routes/channel.js';
import { YtDlpError, runYtDlp } from '../src/services/ytDlpRunner.js';
describe('GET /api/channel', () => {
it('retourne les vidéos correspondantes pour une chaîne valide', async () => {
vi.mocked(runYtDlp).mockResolvedValue(
JSON.stringify({ id: 'v1', title: 'Test video', url: 'https://youtu.be/v1' }),
);
const app = Fastify();
await app.register(channelRoutes);
const response = await app.inject({
method: 'GET',
url: '/api/channel?channel=@test&section=videos',
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
videos: [
{
id: 'v1',
title: 'Test video',
url: 'https://youtu.be/v1',
thumbnailUrl: null,
durationSeconds: null,
uploadDate: null,
},
],
});
});
it('renvoie 400 pour un identifiant de chaîne invalide', async () => {
const app = Fastify();
await app.register(channelRoutes);
const response = await app.inject({ method: 'GET', url: '/api/channel?channel=--exec' });
expect(response.statusCode).toBe(400);
});
it('renvoie 400 si le paramètre channel est manquant (validation de schéma)', async () => {
const app = Fastify();
await app.register(channelRoutes);
const response = await app.inject({ method: 'GET', url: '/api/channel' });
expect(response.statusCode).toBe(400);
});
it('renvoie 502 quand yt-dlp échoue', async () => {
vi.mocked(runYtDlp).mockRejectedValue(new YtDlpError(1, 'network error'));
const app = Fastify();
await app.register(channelRoutes);
const response = await app.inject({ method: 'GET', url: '/api/channel?channel=@test' });
expect(response.statusCode).toBe(502);
});
});