1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
3 import { HttpStatusCode, RunnerJob, RunnerJobState, RunnerJobSuccessPayload, RunnerJobUpdatePayload, VideoPrivacy } from '@shared/models'
10 setAccessTokensToServers,
11 setDefaultVideoChannel,
14 } from '@shared/server-commands'
16 const badUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
18 describe('Test managing runners', function () {
19 let server: PeerTubeServer
23 let registrationTokenId: number
24 let registrationToken: string
26 let runnerToken: string
27 let runnerToken2: string
29 let completedJobToken: string
30 let completedJobUUID: string
32 let cancelledJobUUID: string
34 before(async function () {
45 server = await createSingleServer(1, config)
46 await setAccessTokensToServers([ server ])
47 await setDefaultVideoChannel([ server ])
49 userToken = await server.users.generateUserAndToken('user1')
51 const { data } = await server.runnerRegistrationTokens.list()
52 registrationToken = data[0].registrationToken
53 registrationTokenId = data[0].id
55 await server.config.enableTranscoding(true, true)
56 await server.config.enableRemoteTranscoding()
57 runnerToken = await server.runners.autoRegisterRunner()
58 runnerToken2 = await server.runners.autoRegisterRunner()
61 await server.videos.quickUpload({ name: 'video 1' })
62 await server.videos.quickUpload({ name: 'video 2' })
64 await waitJobs([ server ])
67 const job = await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
68 completedJobToken = job.jobToken
69 completedJobUUID = job.uuid
73 const { job } = await server.runnerJobs.autoAccept({ runnerToken })
74 cancelledJobUUID = job.uuid
75 await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID })
80 describe('Managing runner registration tokens', function () {
82 describe('Common', function () {
84 it('Should fail to generate, list or delete runner registration token without oauth token', async function () {
85 const expectedStatus = HttpStatusCode.UNAUTHORIZED_401
87 await server.runnerRegistrationTokens.generate({ token: null, expectedStatus })
88 await server.runnerRegistrationTokens.list({ token: null, expectedStatus })
89 await server.runnerRegistrationTokens.delete({ token: null, id: registrationTokenId, expectedStatus })
92 it('Should fail to generate, list or delete runner registration token without admin rights', async function () {
93 const expectedStatus = HttpStatusCode.FORBIDDEN_403
95 await server.runnerRegistrationTokens.generate({ token: userToken, expectedStatus })
96 await server.runnerRegistrationTokens.list({ token: userToken, expectedStatus })
97 await server.runnerRegistrationTokens.delete({ token: userToken, id: registrationTokenId, expectedStatus })
101 describe('Delete', function () {
103 it('Should fail to delete with a bad id', async function () {
104 await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
108 describe('List', function () {
109 const path = '/api/v1/runners/registration-tokens'
111 it('Should fail to list with a bad start pagination', async function () {
112 await checkBadStartPagination(server.url, path, server.accessToken)
115 it('Should fail to list with a bad count pagination', async function () {
116 await checkBadCountPagination(server.url, path, server.accessToken)
119 it('Should fail to list with an incorrect sort', async function () {
120 await checkBadSortPagination(server.url, path, server.accessToken)
123 it('Should succeed to list with the correct params', async function () {
124 await server.runnerRegistrationTokens.list({ start: 0, count: 5, sort: '-createdAt' })
129 describe('Managing runners', function () {
130 let toDeleteId: number
132 describe('Register', function () {
133 const name = 'runner name'
135 it('Should fail with a bad registration token', async function () {
136 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
138 await server.runners.register({ name, registrationToken: 'a'.repeat(4000), expectedStatus })
139 await server.runners.register({ name, registrationToken: null, expectedStatus })
142 it('Should fail with an unknown registration token', async function () {
143 await server.runners.register({ name, registrationToken: 'aaa', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
146 it('Should fail with a bad name', async function () {
147 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
149 await server.runners.register({ name: '', registrationToken, expectedStatus })
150 await server.runners.register({ name: 'a'.repeat(200), registrationToken, expectedStatus })
153 it('Should fail with an invalid description', async function () {
154 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
156 await server.runners.register({ name, description: '', registrationToken, expectedStatus })
157 await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus })
160 it('Should succeed with the correct params', async function () {
161 const { id } = await server.runners.register({ name, description: 'super description', registrationToken })
167 describe('Delete', function () {
169 it('Should fail without oauth token', async function () {
170 await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
173 it('Should fail without admin rights', async function () {
174 await server.runners.delete({ token: userToken, id: toDeleteId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
177 it('Should fail with a bad id', async function () {
178 await server.runners.delete({ id: 'hi' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
181 it('Should fail with an unknown id', async function () {
182 await server.runners.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
185 it('Should succeed with the correct params', async function () {
186 await server.runners.delete({ id: toDeleteId })
190 describe('List', function () {
191 const path = '/api/v1/runners'
193 it('Should fail without oauth token', async function () {
194 await server.runners.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
197 it('Should fail without admin rights', async function () {
198 await server.runners.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
201 it('Should fail to list with a bad start pagination', async function () {
202 await checkBadStartPagination(server.url, path, server.accessToken)
205 it('Should fail to list with a bad count pagination', async function () {
206 await checkBadCountPagination(server.url, path, server.accessToken)
209 it('Should fail to list with an incorrect sort', async function () {
210 await checkBadSortPagination(server.url, path, server.accessToken)
213 it('Should succeed to list with the correct params', async function () {
214 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
220 describe('Runner jobs by admin', function () {
222 describe('Cancel', function () {
225 before(async function () {
228 await server.videos.quickUpload({ name: 'video' })
229 await waitJobs([ server ])
231 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
232 jobUUID = availableJobs[0].uuid
235 it('Should fail without oauth token', async function () {
236 await server.runnerJobs.cancelByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
239 it('Should fail without admin rights', async function () {
240 await server.runnerJobs.cancelByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
243 it('Should fail with a bad job uuid', async function () {
244 await server.runnerJobs.cancelByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
247 it('Should fail with an unknown job uuid', async function () {
248 const jobUUID = badUUID
249 await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
252 it('Should succeed with the correct params', async function () {
253 await server.runnerJobs.cancelByAdmin({ jobUUID })
257 describe('List', function () {
258 const path = '/api/v1/runners/jobs'
260 it('Should fail without oauth token', async function () {
261 await server.runnerJobs.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
264 it('Should fail without admin rights', async function () {
265 await server.runnerJobs.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
268 it('Should fail to list with a bad start pagination', async function () {
269 await checkBadStartPagination(server.url, path, server.accessToken)
272 it('Should fail to list with a bad count pagination', async function () {
273 await checkBadCountPagination(server.url, path, server.accessToken)
276 it('Should fail to list with an incorrect sort', async function () {
277 await checkBadSortPagination(server.url, path, server.accessToken)
280 it('Should succeed to list with the correct params', async function () {
281 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt' })
287 describe('Runner jobs by runners', function () {
290 let videoUUID: string
293 let jobToken2: string
295 let videoUUID2: string
297 let pendingUUID: string
299 let liveAcceptedJob: RunnerJob & { jobToken: string }
301 async function fetchFiles (options: {
306 expectedStatus: HttpStatusCode
308 const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken } = options
310 const basePath = '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID
311 const paths = [ `${basePath}/max-quality`, `${basePath}/previews/max-quality` ]
313 for (const path of paths) {
314 await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
318 before(async function () {
322 await server.runnerJobs.cancelAllJobs({ state: RunnerJobState.PENDING })
326 const { uuid } = await server.videos.quickUpload({ name: 'video' })
329 await waitJobs([ server ])
331 const { job } = await server.runnerJobs.autoAccept({ runnerToken })
333 jobToken = job.jobToken
337 const { uuid } = await server.videos.quickUpload({ name: 'video' })
340 await waitJobs([ server ])
342 const { job } = await server.runnerJobs.autoAccept({ runnerToken: runnerToken2 })
344 jobToken2 = job.jobToken
348 await server.videos.quickUpload({ name: 'video' })
349 await waitJobs([ server ])
351 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
352 pendingUUID = availableJobs[0].uuid
356 await server.config.enableLive({
362 const { live } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
364 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
365 await waitJobs([ server ])
367 await server.runnerJobs.requestLiveJob(runnerToken)
369 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
370 liveAcceptedJob = job
372 await stopFfmpeg(ffmpegCommand)
376 describe('Common runner tokens validations', function () {
378 async function testEndpoints (options: {
382 expectedStatus: HttpStatusCode
384 await fetchFiles({ ...options, videoUUID })
386 await server.runnerJobs.abort({ ...options, reason: 'reason' })
387 await server.runnerJobs.update({ ...options })
388 await server.runnerJobs.error({ ...options, message: 'message' })
389 await server.runnerJobs.success({ ...options, payload: { videoFile: 'video_short.mp4' } })
392 it('Should fail with an invalid job uuid', async function () {
393 await testEndpoints({ jobUUID: 'a', runnerToken, jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
396 it('Should fail with an unknown job uuid', async function () {
397 const jobUUID = badUUID
398 await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
401 it('Should fail with an invalid runner token', async function () {
402 await testEndpoints({ jobUUID, runnerToken: '', jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
405 it('Should fail with an unknown runner token', async function () {
406 const runnerToken = badUUID
407 await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
410 it('Should fail with an invalid job token job uuid', async function () {
411 await testEndpoints({ jobUUID, runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
414 it('Should fail with an unknown job token job uuid', async function () {
415 const jobToken = badUUID
416 await testEndpoints({ jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
419 it('Should fail with a runner token not associated to this job', async function () {
420 await testEndpoints({ jobUUID, runnerToken: runnerToken2, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
423 it('Should fail with a job uuid not associated to the job token', async function () {
424 await testEndpoints({ jobUUID: jobUUID2, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
425 await testEndpoints({ jobUUID, runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
429 describe('Unregister', function () {
431 it('Should fail without a runner token', async function () {
432 await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
435 it('Should fail with a bad a runner token', async function () {
436 await server.runners.unregister({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
439 it('Should fail with an unknown runner token', async function () {
440 await server.runners.unregister({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
444 describe('Request', function () {
446 it('Should fail without a runner token', async function () {
447 await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
450 it('Should fail with a bad a runner token', async function () {
451 await server.runnerJobs.request({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
454 it('Should fail with an unknown runner token', async function () {
455 await server.runnerJobs.request({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
459 describe('Accept', function () {
461 it('Should fail with a bad a job uuid', async function () {
462 await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
465 it('Should fail with an unknown job uuid', async function () {
466 await server.runnerJobs.accept({ jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
469 it('Should fail with a job not in pending state', async function () {
470 await server.runnerJobs.accept({ jobUUID: completedJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
471 await server.runnerJobs.accept({ jobUUID: cancelledJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
474 it('Should fail without a runner token', async function () {
475 await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
478 it('Should fail with a bad a runner token', async function () {
479 await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
482 it('Should fail with an unknown runner token', async function () {
483 await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
487 describe('Abort', function () {
489 it('Should fail without a reason', async function () {
490 await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
493 it('Should fail with a bad reason', async function () {
494 const reason = 'reason'.repeat(5000)
495 await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
498 it('Should fail with a job not in processing state', async function () {
499 await server.runnerJobs.abort({
500 jobUUID: completedJobUUID,
501 jobToken: completedJobToken,
504 expectedStatus: HttpStatusCode.BAD_REQUEST_400
509 describe('Update', function () {
511 describe('Common', function () {
513 it('Should fail with an invalid progress', async function () {
514 await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
517 it('Should fail with a job not in processing state', async function () {
518 await server.runnerJobs.update({
519 jobUUID: completedJobUUID,
520 jobToken: completedJobToken,
522 expectedStatus: HttpStatusCode.BAD_REQUEST_400
527 describe('Live RTMP to HLS', function () {
528 const base: RunnerJobUpdatePayload = {
529 masterPlaylistFile: 'live/master.m3u8',
530 resolutionPlaylistFilename: '0.m3u8',
531 resolutionPlaylistFile: 'live/1.m3u8',
533 videoChunkFile: 'live/1-000069.ts',
534 videoChunkFilename: '1-000068.ts'
537 function testUpdate (payload: RunnerJobUpdatePayload) {
538 return server.runnerJobs.update({
539 jobUUID: liveAcceptedJob.uuid,
540 jobToken: liveAcceptedJob.jobToken,
543 expectedStatus: HttpStatusCode.BAD_REQUEST_400
547 it('Should fail with an invalid resolutionPlaylistFilename', async function () {
548 await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
549 await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
550 await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
553 it('Should fail with an invalid videoChunkFilename', async function () {
554 await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
555 await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
556 await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
559 it('Should fail with an invalid type', async function () {
560 await testUpdate({ ...base, type: undefined })
561 await testUpdate({ ...base, type: 'toto' as any })
564 it('Should succeed with the correct params', async function () {
565 await server.runnerJobs.update({
566 jobUUID: liveAcceptedJob.uuid,
567 jobToken: liveAcceptedJob.jobToken,
572 await server.runnerJobs.update({
573 jobUUID: liveAcceptedJob.uuid,
574 jobToken: liveAcceptedJob.jobToken,
575 payload: { ...base, masterPlaylistFile: undefined },
582 describe('Error', function () {
584 it('Should fail with a missing error message', async function () {
585 await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
588 it('Should fail with an invalid error messgae', async function () {
589 const message = 'a'.repeat(6000)
590 await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
593 it('Should fail with a job not in processing state', async function () {
594 await server.runnerJobs.error({
595 jobUUID: completedJobUUID,
596 jobToken: completedJobToken,
597 message: 'my message',
599 expectedStatus: HttpStatusCode.BAD_REQUEST_400
604 describe('Success', function () {
605 let vodJobUUID: string
606 let vodJobToken: string
608 describe('Common', function () {
610 it('Should fail with a job not in processing state', async function () {
611 await server.runnerJobs.success({
612 jobUUID: completedJobUUID,
613 jobToken: completedJobToken,
614 payload: { videoFile: 'video_short.mp4' },
616 expectedStatus: HttpStatusCode.BAD_REQUEST_400
621 describe('VOD', function () {
623 it('Should fail with an invalid vod web video payload', async function () {
624 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' })
626 await server.runnerJobs.success({
628 jobToken: job.jobToken,
629 payload: { hello: 'video_short.mp4' } as any,
631 expectedStatus: HttpStatusCode.BAD_REQUEST_400
634 vodJobUUID = job.uuid
635 vodJobToken = job.jobToken
638 it('Should fail with an invalid vod hls payload', async function () {
639 // To create HLS jobs
640 const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' }
641 await server.runnerJobs.success({ runnerToken, jobUUID: vodJobUUID, jobToken: vodJobToken, payload })
643 await waitJobs([ server ])
645 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-hls-transcoding' })
647 await server.runnerJobs.success({
649 jobToken: job.jobToken,
650 payload: { videoFile: 'video_short.mp4' } as any,
652 expectedStatus: HttpStatusCode.BAD_REQUEST_400
656 it('Should fail with an invalid vod audio merge payload', async function () {
657 const attributes = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
658 await server.videos.upload({ attributes, mode: 'legacy' })
660 await waitJobs([ server ])
662 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-audio-merge-transcoding' })
664 await server.runnerJobs.success({
666 jobToken: job.jobToken,
667 payload: { hello: 'video_short.mp4' } as any,
669 expectedStatus: HttpStatusCode.BAD_REQUEST_400
675 describe('Job files', function () {
677 describe('Video files', function () {
679 it('Should fail with an invalid video id', async function () {
680 await fetchFiles({ videoUUID: 'a', jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
683 it('Should fail with an unknown video id', async function () {
684 const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
685 await fetchFiles({ videoUUID, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
688 it('Should fail with a video id not associated to this job', async function () {
689 await fetchFiles({ videoUUID: videoUUID2, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
692 it('Should succeed with the correct params', async function () {
693 await fetchFiles({ videoUUID, jobUUID, runnerToken, jobToken, expectedStatus: HttpStatusCode.OK_200 })
699 after(async function () {
700 await cleanupTests([ server ])