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>
69 lines
2 KiB
TypeScript
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§ion=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);
|
|
});
|
|
});
|