]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame_incremental - shared/extra-utils/videos/videos.ts
Increase tests waits
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / videos / videos.ts
... / ...
CommitLineData
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { pathExists, readdir, readFile } from 'fs-extra'
5import * as parseTorrent from 'parse-torrent'
6import { extname, join } from 'path'
7import * as request from 'supertest'
8import { v4 as uuidv4 } from 'uuid'
9import validator from 'validator'
10import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
11import { VideoDetails, VideoPrivacy } from '../../models/videos'
12import {
13 buildAbsoluteFixturePath,
14 buildServerDirectory,
15 dateIsValid,
16 immutableAssign,
17 root,
18 testImage,
19 webtorrentAdd
20} from '../miscs/miscs'
21import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
22import { waitJobs } from '../server/jobs'
23import { ServerInfo } from '../server/servers'
24import { getMyUserInformation } from '../users/users'
25
26loadLanguages()
27
28type VideoAttributes = {
29 name?: string
30 category?: number
31 licence?: number
32 language?: string
33 nsfw?: boolean
34 commentsEnabled?: boolean
35 downloadEnabled?: boolean
36 waitTranscoding?: boolean
37 description?: string
38 originallyPublishedAt?: string
39 tags?: string[]
40 channelId?: number
41 privacy?: VideoPrivacy
42 fixture?: string
43 thumbnailfile?: string
44 previewfile?: string
45 scheduleUpdate?: {
46 updateAt: string
47 privacy?: VideoPrivacy
48 }
49}
50
51function getVideoCategories (url: string) {
52 const path = '/api/v1/videos/categories'
53
54 return makeGetRequest({
55 url,
56 path,
57 statusCodeExpected: 200
58 })
59}
60
61function getVideoLicences (url: string) {
62 const path = '/api/v1/videos/licences'
63
64 return makeGetRequest({
65 url,
66 path,
67 statusCodeExpected: 200
68 })
69}
70
71function getVideoLanguages (url: string) {
72 const path = '/api/v1/videos/languages'
73
74 return makeGetRequest({
75 url,
76 path,
77 statusCodeExpected: 200
78 })
79}
80
81function getVideoPrivacies (url: string) {
82 const path = '/api/v1/videos/privacies'
83
84 return makeGetRequest({
85 url,
86 path,
87 statusCodeExpected: 200
88 })
89}
90
91function getVideo (url: string, id: number | string, expectedStatus = 200) {
92 const path = '/api/v1/videos/' + id
93
94 return request(url)
95 .get(path)
96 .set('Accept', 'application/json')
97 .expect(expectedStatus)
98}
99
100async function getVideoIdFromUUID (url: string, uuid: string) {
101 const res = await getVideo(url, uuid)
102
103 return res.body.id
104}
105
106function getVideoFileMetadataUrl (url: string) {
107 return request(url)
108 .get('/')
109 .set('Accept', 'application/json')
110 .expect(200)
111 .expect('Content-Type', /json/)
112}
113
114function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) {
115 const path = '/api/v1/videos/' + id + '/views'
116
117 const req = request(url)
118 .post(path)
119 .set('Accept', 'application/json')
120
121 if (xForwardedFor) {
122 req.set('X-Forwarded-For', xForwardedFor)
123 }
124
125 return req.expect(expectedStatus)
126}
127
128function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) {
129 const path = '/api/v1/videos/' + id
130
131 return request(url)
132 .get(path)
133 .set('Authorization', 'Bearer ' + token)
134 .set('Accept', 'application/json')
135 .expect(expectedStatus)
136}
137
138function getVideoDescription (url: string, descriptionPath: string) {
139 return request(url)
140 .get(descriptionPath)
141 .set('Accept', 'application/json')
142 .expect(200)
143 .expect('Content-Type', /json/)
144}
145
146function getVideosList (url: string) {
147 const path = '/api/v1/videos'
148
149 return request(url)
150 .get(path)
151 .query({ sort: 'name' })
152 .set('Accept', 'application/json')
153 .expect(200)
154 .expect('Content-Type', /json/)
155}
156
157function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
158 const path = '/api/v1/videos'
159
160 return request(url)
161 .get(path)
162 .set('Authorization', 'Bearer ' + token)
163 .query(immutableAssign(query, { sort: 'name' }))
164 .set('Accept', 'application/json')
165 .expect(200)
166 .expect('Content-Type', /json/)
167}
168
169function getLocalVideos (url: string) {
170 const path = '/api/v1/videos'
171
172 return request(url)
173 .get(path)
174 .query({ sort: 'name', filter: 'local' })
175 .set('Accept', 'application/json')
176 .expect(200)
177 .expect('Content-Type', /json/)
178}
179
180function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
181 const path = '/api/v1/users/me/videos'
182
183 const req = request(url)
184 .get(path)
185 .query({ start: start })
186 .query({ count: count })
187 .query({ search: search })
188
189 if (sort) req.query({ sort })
190
191 return req.set('Accept', 'application/json')
192 .set('Authorization', 'Bearer ' + accessToken)
193 .expect(200)
194 .expect('Content-Type', /json/)
195}
196
197function getAccountVideos (
198 url: string,
199 accessToken: string,
200 accountName: string,
201 start: number,
202 count: number,
203 sort?: string,
204 query: { nsfw?: boolean } = {}
205) {
206 const path = '/api/v1/accounts/' + accountName + '/videos'
207
208 return makeGetRequest({
209 url,
210 path,
211 query: immutableAssign(query, {
212 start,
213 count,
214 sort
215 }),
216 token: accessToken,
217 statusCodeExpected: 200
218 })
219}
220
221function getVideoChannelVideos (
222 url: string,
223 accessToken: string,
224 videoChannelName: string,
225 start: number,
226 count: number,
227 sort?: string,
228 query: { nsfw?: boolean } = {}
229) {
230 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
231
232 return makeGetRequest({
233 url,
234 path,
235 query: immutableAssign(query, {
236 start,
237 count,
238 sort
239 }),
240 token: accessToken,
241 statusCodeExpected: 200
242 })
243}
244
245function getPlaylistVideos (
246 url: string,
247 accessToken: string,
248 playlistId: number | string,
249 start: number,
250 count: number,
251 query: { nsfw?: boolean } = {}
252) {
253 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
254
255 return makeGetRequest({
256 url,
257 path,
258 query: immutableAssign(query, {
259 start,
260 count
261 }),
262 token: accessToken,
263 statusCodeExpected: 200
264 })
265}
266
267function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
268 const path = '/api/v1/videos'
269
270 const req = request(url)
271 .get(path)
272 .query({ start: start })
273 .query({ count: count })
274
275 if (sort) req.query({ sort })
276 if (skipCount) req.query({ skipCount })
277
278 return req.set('Accept', 'application/json')
279 .expect(200)
280 .expect('Content-Type', /json/)
281}
282
283function getVideosListSort (url: string, sort: string) {
284 const path = '/api/v1/videos'
285
286 return request(url)
287 .get(path)
288 .query({ sort: sort })
289 .set('Accept', 'application/json')
290 .expect(200)
291 .expect('Content-Type', /json/)
292}
293
294function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
295 const path = '/api/v1/videos'
296
297 return request(url)
298 .get(path)
299 .query(query)
300 .set('Accept', 'application/json')
301 .expect(200)
302 .expect('Content-Type', /json/)
303}
304
305function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
306 const path = '/api/v1/videos'
307
308 return request(url)
309 .delete(path + '/' + id)
310 .set('Accept', 'application/json')
311 .set('Authorization', 'Bearer ' + token)
312 .expect(expectedStatus)
313}
314
315async function removeAllVideos (server: ServerInfo) {
316 const resVideos = await getVideosList(server.url)
317
318 for (const v of resVideos.body.data) {
319 await removeVideo(server.url, server.accessToken, v.id)
320 }
321}
322
323async function checkVideoFilesWereRemoved (
324 videoUUID: string,
325 serverNumber: number,
326 directories = [
327 'redundancy',
328 'videos',
329 'thumbnails',
330 'torrents',
331 'previews',
332 'captions',
333 join('playlists', 'hls'),
334 join('redundancy', 'hls')
335 ]
336) {
337 for (const directory of directories) {
338 const directoryPath = buildServerDirectory(serverNumber, directory)
339
340 const directoryExists = await pathExists(directoryPath)
341 if (directoryExists === false) continue
342
343 const files = await readdir(directoryPath)
344 for (const file of files) {
345 expect(file).to.not.contain(videoUUID)
346 }
347 }
348}
349
350async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
351 const path = '/api/v1/videos/upload'
352 let defaultChannelId = '1'
353
354 try {
355 const res = await getMyUserInformation(url, accessToken)
356 defaultChannelId = res.body.videoChannels[0].id
357 } catch (e) { /* empty */ }
358
359 // Override default attributes
360 const attributes = Object.assign({
361 name: 'my super video',
362 category: 5,
363 licence: 4,
364 language: 'zh',
365 channelId: defaultChannelId,
366 nsfw: true,
367 waitTranscoding: false,
368 description: 'my super description',
369 support: 'my super support text',
370 tags: [ 'tag' ],
371 privacy: VideoPrivacy.PUBLIC,
372 commentsEnabled: true,
373 downloadEnabled: true,
374 fixture: 'video_short.webm'
375 }, videoAttributesArg)
376
377 const req = request(url)
378 .post(path)
379 .set('Accept', 'application/json')
380 .set('Authorization', 'Bearer ' + accessToken)
381 .field('name', attributes.name)
382 .field('nsfw', JSON.stringify(attributes.nsfw))
383 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
384 .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
385 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
386 .field('privacy', attributes.privacy.toString())
387 .field('channelId', attributes.channelId)
388
389 if (attributes.support !== undefined) {
390 req.field('support', attributes.support)
391 }
392
393 if (attributes.description !== undefined) {
394 req.field('description', attributes.description)
395 }
396 if (attributes.language !== undefined) {
397 req.field('language', attributes.language.toString())
398 }
399 if (attributes.category !== undefined) {
400 req.field('category', attributes.category.toString())
401 }
402 if (attributes.licence !== undefined) {
403 req.field('licence', attributes.licence.toString())
404 }
405
406 const tags = attributes.tags || []
407 for (let i = 0; i < tags.length; i++) {
408 req.field('tags[' + i + ']', attributes.tags[i])
409 }
410
411 if (attributes.thumbnailfile !== undefined) {
412 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
413 }
414 if (attributes.previewfile !== undefined) {
415 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
416 }
417
418 if (attributes.scheduleUpdate) {
419 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
420
421 if (attributes.scheduleUpdate.privacy) {
422 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
423 }
424 }
425
426 if (attributes.originallyPublishedAt !== undefined) {
427 req.field('originallyPublishedAt', attributes.originallyPublishedAt)
428 }
429
430 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
431 .expect(specialStatus)
432}
433
434function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
435 const path = '/api/v1/videos/' + id
436 const body = {}
437
438 if (attributes.name) body['name'] = attributes.name
439 if (attributes.category) body['category'] = attributes.category
440 if (attributes.licence) body['licence'] = attributes.licence
441 if (attributes.language) body['language'] = attributes.language
442 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
443 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
444 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
445 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
446 if (attributes.description) body['description'] = attributes.description
447 if (attributes.tags) body['tags'] = attributes.tags
448 if (attributes.privacy) body['privacy'] = attributes.privacy
449 if (attributes.channelId) body['channelId'] = attributes.channelId
450 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
451
452 // Upload request
453 if (attributes.thumbnailfile || attributes.previewfile) {
454 const attaches: any = {}
455 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
456 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
457
458 return makeUploadRequest({
459 url,
460 method: 'PUT',
461 path,
462 token: accessToken,
463 fields: body,
464 attaches,
465 statusCodeExpected
466 })
467 }
468
469 return makePutBodyRequest({
470 url,
471 path,
472 fields: body,
473 token: accessToken,
474 statusCodeExpected
475 })
476}
477
478function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
479 const path = '/api/v1/videos/' + id + '/rate'
480
481 return request(url)
482 .put(path)
483 .set('Accept', 'application/json')
484 .set('Authorization', 'Bearer ' + accessToken)
485 .send({ rating })
486 .expect(specialStatus)
487}
488
489function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
490 return new Promise<any>((res, rej) => {
491 const torrentName = videoUUID + '-' + resolution + '.torrent'
492 const torrentPath = join(root(), 'test' + server.internalServerNumber, 'torrents', torrentName)
493 readFile(torrentPath, (err, data) => {
494 if (err) return rej(err)
495
496 return res(parseTorrent(data))
497 })
498 })
499}
500
501async function completeVideoCheck (
502 url: string,
503 video: any,
504 attributes: {
505 name: string
506 category: number
507 licence: number
508 language: string
509 nsfw: boolean
510 commentsEnabled: boolean
511 downloadEnabled: boolean
512 description: string
513 publishedAt?: string
514 support: string
515 originallyPublishedAt?: string
516 account: {
517 name: string
518 host: string
519 }
520 isLocal: boolean
521 tags: string[]
522 privacy: number
523 likes?: number
524 dislikes?: number
525 duration: number
526 channel: {
527 displayName: string
528 name: string
529 description
530 isLocal: boolean
531 }
532 fixture: string
533 files: {
534 resolution: number
535 size: number
536 }[]
537 thumbnailfile?: string
538 previewfile?: string
539 }
540) {
541 if (!attributes.likes) attributes.likes = 0
542 if (!attributes.dislikes) attributes.dislikes = 0
543
544 expect(video.name).to.equal(attributes.name)
545 expect(video.category.id).to.equal(attributes.category)
546 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
547 expect(video.licence.id).to.equal(attributes.licence)
548 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
549 expect(video.language.id).to.equal(attributes.language)
550 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
551 expect(video.privacy.id).to.deep.equal(attributes.privacy)
552 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
553 expect(video.nsfw).to.equal(attributes.nsfw)
554 expect(video.description).to.equal(attributes.description)
555 expect(video.account.id).to.be.a('number')
556 expect(video.account.host).to.equal(attributes.account.host)
557 expect(video.account.name).to.equal(attributes.account.name)
558 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
559 expect(video.channel.name).to.equal(attributes.channel.name)
560 expect(video.likes).to.equal(attributes.likes)
561 expect(video.dislikes).to.equal(attributes.dislikes)
562 expect(video.isLocal).to.equal(attributes.isLocal)
563 expect(video.duration).to.equal(attributes.duration)
564 expect(dateIsValid(video.createdAt)).to.be.true
565 expect(dateIsValid(video.publishedAt)).to.be.true
566 expect(dateIsValid(video.updatedAt)).to.be.true
567
568 if (attributes.publishedAt) {
569 expect(video.publishedAt).to.equal(attributes.publishedAt)
570 }
571
572 if (attributes.originallyPublishedAt) {
573 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
574 } else {
575 expect(video.originallyPublishedAt).to.be.null
576 }
577
578 const res = await getVideo(url, video.uuid)
579 const videoDetails: VideoDetails = res.body
580
581 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
582 expect(videoDetails.tags).to.deep.equal(attributes.tags)
583 expect(videoDetails.account.name).to.equal(attributes.account.name)
584 expect(videoDetails.account.host).to.equal(attributes.account.host)
585 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
586 expect(video.channel.name).to.equal(attributes.channel.name)
587 expect(videoDetails.channel.host).to.equal(attributes.account.host)
588 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
589 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
590 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
591 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
592 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
593
594 for (const attributeFile of attributes.files) {
595 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
596 expect(file).not.to.be.undefined
597
598 let extension = extname(attributes.fixture)
599 // Transcoding enabled: extension will always be .mp4
600 if (attributes.files.length > 1) extension = '.mp4'
601
602 expect(file.magnetUri).to.have.lengthOf.above(2)
603 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
604 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
605 expect(file.resolution.id).to.equal(attributeFile.resolution)
606 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
607
608 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
609 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
610 expect(
611 file.size,
612 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
613 ).to.be.above(minSize).and.below(maxSize)
614
615 const torrent = await webtorrentAdd(file.magnetUri, true)
616 expect(torrent.files).to.be.an('array')
617 expect(torrent.files.length).to.equal(1)
618 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
619 }
620
621 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
622
623 if (attributes.previewfile) {
624 await testImage(url, attributes.previewfile, videoDetails.previewPath)
625 }
626}
627
628async function videoUUIDToId (url: string, id: number | string) {
629 if (validator.isUUID('' + id) === false) return id
630
631 const res = await getVideo(url, id)
632 return res.body.id
633}
634
635async function uploadVideoAndGetId (options: {
636 server: ServerInfo
637 videoName: string
638 nsfw?: boolean
639 privacy?: VideoPrivacy
640 token?: string
641}) {
642 const videoAttrs: any = { name: options.videoName }
643 if (options.nsfw) videoAttrs.nsfw = options.nsfw
644 if (options.privacy) videoAttrs.privacy = options.privacy
645
646 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
647
648 return { id: res.body.video.id, uuid: res.body.video.uuid }
649}
650
651async function getLocalIdByUUID (url: string, uuid: string) {
652 const res = await getVideo(url, uuid)
653
654 return res.body.id
655}
656
657// serverNumber starts from 1
658async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) {
659 const server = servers.find(s => s.serverNumber === serverNumber)
660 const res = await uploadRandomVideo(server, false, additionalParams)
661
662 await waitJobs(servers)
663
664 return res
665}
666
667async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
668 const prefixName = additionalParams.prefixName || ''
669 const name = prefixName + uuidv4()
670
671 const data = Object.assign({ name }, additionalParams)
672 const res = await uploadVideo(server.url, server.accessToken, data)
673
674 if (wait) await waitJobs([ server ])
675
676 return { uuid: res.body.video.uuid, name }
677}
678
679// ---------------------------------------------------------------------------
680
681export {
682 getVideoDescription,
683 getVideoCategories,
684 uploadRandomVideo,
685 getVideoLicences,
686 videoUUIDToId,
687 getVideoPrivacies,
688 getVideoLanguages,
689 getMyVideos,
690 getAccountVideos,
691 getVideoChannelVideos,
692 getVideo,
693 getVideoFileMetadataUrl,
694 getVideoWithToken,
695 getVideosList,
696 removeAllVideos,
697 getVideosListPagination,
698 getVideosListSort,
699 removeVideo,
700 getVideosListWithToken,
701 uploadVideo,
702 getVideosWithFilters,
703 uploadRandomVideoOnServers,
704 updateVideo,
705 rateVideo,
706 viewVideo,
707 parseTorrentVideo,
708 getLocalVideos,
709 completeVideoCheck,
710 checkVideoFilesWereRemoved,
711 getPlaylistVideos,
712 uploadVideoAndGetId,
713 getLocalIdByUUID,
714 getVideoIdFromUUID
715}