@@ -251,6 +251,90 @@ describe("POST /video/thumbnail", () => {
251251 } ) ;
252252} ) ;
253253
254+ describe ( "POST /video/convert" , ( ) => {
255+ beforeEach ( ( ) => {
256+ mock . restore ( ) ;
257+ } ) ;
258+
259+ test ( "returns 400 for missing videoUrl" , async ( ) => {
260+ const response = await app . fetch (
261+ new Request ( "http://localhost/video/convert" , {
262+ method : "POST" ,
263+ headers : { "Content-Type" : "application/json" } ,
264+ body : JSON . stringify ( { } ) ,
265+ } ) ,
266+ ) ;
267+
268+ expect ( response . status ) . toBe ( 400 ) ;
269+ const data = await response . json ( ) ;
270+ expect ( data . code ) . toBe ( "INVALID_REQUEST" ) ;
271+ } ) ;
272+
273+ test ( "returns mp4 when conversion succeeds" , async ( ) => {
274+ const fixturePath = new URL (
275+ "../fixtures/test-with-audio.mp4" ,
276+ import . meta. url ,
277+ ) . pathname ;
278+ const fixtureBytes = await Bun . file ( fixturePath ) . bytes ( ) ;
279+ const mockMetadata = {
280+ duration : 10.5 ,
281+ width : 1280 ,
282+ height : 720 ,
283+ fps : 30 ,
284+ videoCodec : "h264" ,
285+ audioCodec : "aac" ,
286+ audioChannels : 2 ,
287+ sampleRate : 48000 ,
288+ bitrate : 5000000 ,
289+ fileSize : fixtureBytes . length ,
290+ } ;
291+
292+ mock . module ( "../../lib/ffprobe" , ( ) => ( {
293+ probeVideo : ffprobe . probeVideo ,
294+ probeVideoFile : async ( ) => mockMetadata ,
295+ canAcceptNewProbeProcess : ffprobe . canAcceptNewProbeProcess ,
296+ getActiveProbeProcessCount : ffprobe . getActiveProbeProcessCount ,
297+ } ) ) ;
298+
299+ mock . module ( "../../lib/ffmpeg-video" , ( ) => ( {
300+ downloadVideoToTemp : async ( ) => ( {
301+ path : fixturePath ,
302+ cleanup : async ( ) => { } ,
303+ } ) ,
304+ processVideo : async ( ) => ( {
305+ path : fixturePath ,
306+ cleanup : async ( ) => { } ,
307+ } ) ,
308+ generateThumbnail : ffmpegVideo . generateThumbnail ,
309+ repairContainer : ffmpegVideo . repairContainer ,
310+ uploadToS3 : ffmpegVideo . uploadToS3 ,
311+ uploadFileToS3 : ffmpegVideo . uploadFileToS3 ,
312+ } ) ) ;
313+
314+ const { default : appWithMock } = await import ( "../../app" ) ;
315+
316+ const response = await appWithMock . fetch (
317+ new Request ( "http://localhost/video/convert" , {
318+ method : "POST" ,
319+ headers : { "Content-Type" : "application/json" } ,
320+ body : JSON . stringify ( {
321+ videoUrl : "https://example.com/video.m3u8" ,
322+ inputExtension : ".m3u8" ,
323+ } ) ,
324+ } ) ,
325+ ) ;
326+
327+ expect ( response . status ) . toBe ( 200 ) ;
328+ expect ( response . headers . get ( "Content-Type" ) ) . toBe ( "video/mp4" ) ;
329+ expect ( response . headers . get ( "Content-Length" ) ) . toBe (
330+ fixtureBytes . length . toString ( ) ,
331+ ) ;
332+
333+ const buffer = await response . arrayBuffer ( ) ;
334+ expect ( new Uint8Array ( buffer ) ) . toEqual ( fixtureBytes ) ;
335+ } ) ;
336+ } ) ;
337+
254338describe ( "POST /video/process" , ( ) => {
255339 beforeEach ( ( ) => {
256340 mock . restore ( ) ;
@@ -428,7 +512,11 @@ describe("GET /video/process/:jobId/status", () => {
428512 updateJob : ( ) => null ,
429513 deleteJob : ( ) => { } ,
430514 sendWebhook : async ( ) => { } ,
431- getJobProgress : ( job : any ) => ( {
515+ getJobProgress : ( job : {
516+ phase : string ;
517+ progress : number ;
518+ message ?: string ;
519+ } ) => ( {
432520 phase : job . phase ,
433521 progress : job . progress ,
434522 message : job . message ,
0 commit comments