]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/videos.ts
Add check constraints live tests
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / videos / videos.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3 import { expect } from 'chai'
4 import { pathExists, readdir, readFile } from 'fs-extra'
5 import * as parseTorrent from 'parse-torrent'
6 import { extname, join } from 'path'
7 import * as request from 'supertest'
8 import { v4 as uuidv4 } from 'uuid'
9 import validator from 'validator'
10 import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
11 import { VideoDetails, VideoPrivacy } from '../../models/videos'
12 import {
13 buildAbsoluteFixturePath,
14 buildServerDirectory,
15 dateIsValid,
16 immutableAssign,
17 root,
18 testImage,
19 webtorrentAdd
20 } from '../miscs/miscs'
21 import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
22 import { waitJobs } from '../server/jobs'
23 import { ServerInfo } from '../server/servers'
24 import { getMyUserInformation } from '../users/users'
25
26 loadLanguages()
27
28 type 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
51 function getVideoCategories (url: string) {
52 const path = '/api/v1/videos/categories'
53
54 return makeGetRequest({
55 url,
56 path,
57 statusCodeExpected: 200
58 })
59 }
60
61 function getVideoLicences (url: string) {
62 const path = '/api/v1/videos/licences'
63
64 return makeGetRequest({
65 url,
66 path,
67 statusCodeExpected: 200
68 })
69 }
70
71 function getVideoLanguages (url: string) {
72 const path = '/api/v1/videos/languages'
73
74 return makeGetRequest({
75 url,
76 path,
77 statusCodeExpected: 200
78 })
79 }
80
81 function getVideoPrivacies (url: string) {
82 const path = '/api/v1/videos/privacies'
83
84 return makeGetRequest({
85 url,
86 path,
87 statusCodeExpected: 200
88 })
89 }
90
91 function 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
100 async function getVideoIdFromUUID (url: string, uuid: string) {
101 const res = await getVideo(url, uuid)
102
103 return res.body.id
104 }
105
106 function getVideoFileMetadataUrl (url: string) {
107 return request(url)
108 .get('/')
109 .set('Accept', 'application/json')
110 .expect(200)
111 .expect('Content-Type', /json/)
112 }
113
114 function 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
128 function 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
138 function 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
146 function 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
157 function 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
169 function 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
180 function 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
197 function 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
221 function 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
245 function 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
267 function 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
283 function 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
294 function 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
305 function 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
315 async function checkVideoFilesWereRemoved (
316 videoUUID: string,
317 serverNumber: number,
318 directories = [
319 'redundancy',
320 'videos',
321 'thumbnails',
322 'torrents',
323 'previews',
324 'captions',
325 join('playlists', 'hls'),
326 join('redundancy', 'hls')
327 ]
328 ) {
329 for (const directory of directories) {
330 const directoryPath = buildServerDirectory(serverNumber, directory)
331
332 const directoryExists = await pathExists(directoryPath)
333 if (directoryExists === false) continue
334
335 const files = await readdir(directoryPath)
336 for (const file of files) {
337 expect(file).to.not.contain(videoUUID)
338 }
339 }
340 }
341
342 async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
343 const path = '/api/v1/videos/upload'
344 let defaultChannelId = '1'
345
346 try {
347 const res = await getMyUserInformation(url, accessToken)
348 defaultChannelId = res.body.videoChannels[0].id
349 } catch (e) { /* empty */ }
350
351 // Override default attributes
352 const attributes = Object.assign({
353 name: 'my super video',
354 category: 5,
355 licence: 4,
356 language: 'zh',
357 channelId: defaultChannelId,
358 nsfw: true,
359 waitTranscoding: false,
360 description: 'my super description',
361 support: 'my super support text',
362 tags: [ 'tag' ],
363 privacy: VideoPrivacy.PUBLIC,
364 commentsEnabled: true,
365 downloadEnabled: true,
366 fixture: 'video_short.webm'
367 }, videoAttributesArg)
368
369 const req = request(url)
370 .post(path)
371 .set('Accept', 'application/json')
372 .set('Authorization', 'Bearer ' + accessToken)
373 .field('name', attributes.name)
374 .field('nsfw', JSON.stringify(attributes.nsfw))
375 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
376 .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
377 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
378 .field('privacy', attributes.privacy.toString())
379 .field('channelId', attributes.channelId)
380
381 if (attributes.support !== undefined) {
382 req.field('support', attributes.support)
383 }
384
385 if (attributes.description !== undefined) {
386 req.field('description', attributes.description)
387 }
388 if (attributes.language !== undefined) {
389 req.field('language', attributes.language.toString())
390 }
391 if (attributes.category !== undefined) {
392 req.field('category', attributes.category.toString())
393 }
394 if (attributes.licence !== undefined) {
395 req.field('licence', attributes.licence.toString())
396 }
397
398 const tags = attributes.tags || []
399 for (let i = 0; i < tags.length; i++) {
400 req.field('tags[' + i + ']', attributes.tags[i])
401 }
402
403 if (attributes.thumbnailfile !== undefined) {
404 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
405 }
406 if (attributes.previewfile !== undefined) {
407 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
408 }
409
410 if (attributes.scheduleUpdate) {
411 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
412
413 if (attributes.scheduleUpdate.privacy) {
414 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
415 }
416 }
417
418 if (attributes.originallyPublishedAt !== undefined) {
419 req.field('originallyPublishedAt', attributes.originallyPublishedAt)
420 }
421
422 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
423 .expect(specialStatus)
424 }
425
426 function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
427 const path = '/api/v1/videos/' + id
428 const body = {}
429
430 if (attributes.name) body['name'] = attributes.name
431 if (attributes.category) body['category'] = attributes.category
432 if (attributes.licence) body['licence'] = attributes.licence
433 if (attributes.language) body['language'] = attributes.language
434 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
435 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
436 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
437 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
438 if (attributes.description) body['description'] = attributes.description
439 if (attributes.tags) body['tags'] = attributes.tags
440 if (attributes.privacy) body['privacy'] = attributes.privacy
441 if (attributes.channelId) body['channelId'] = attributes.channelId
442 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
443
444 // Upload request
445 if (attributes.thumbnailfile || attributes.previewfile) {
446 const attaches: any = {}
447 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
448 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
449
450 return makeUploadRequest({
451 url,
452 method: 'PUT',
453 path,
454 token: accessToken,
455 fields: body,
456 attaches,
457 statusCodeExpected
458 })
459 }
460
461 return makePutBodyRequest({
462 url,
463 path,
464 fields: body,
465 token: accessToken,
466 statusCodeExpected
467 })
468 }
469
470 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
471 const path = '/api/v1/videos/' + id + '/rate'
472
473 return request(url)
474 .put(path)
475 .set('Accept', 'application/json')
476 .set('Authorization', 'Bearer ' + accessToken)
477 .send({ rating })
478 .expect(specialStatus)
479 }
480
481 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
482 return new Promise<any>((res, rej) => {
483 const torrentName = videoUUID + '-' + resolution + '.torrent'
484 const torrentPath = join(root(), 'test' + server.internalServerNumber, 'torrents', torrentName)
485 readFile(torrentPath, (err, data) => {
486 if (err) return rej(err)
487
488 return res(parseTorrent(data))
489 })
490 })
491 }
492
493 async function completeVideoCheck (
494 url: string,
495 video: any,
496 attributes: {
497 name: string
498 category: number
499 licence: number
500 language: string
501 nsfw: boolean
502 commentsEnabled: boolean
503 downloadEnabled: boolean
504 description: string
505 publishedAt?: string
506 support: string
507 originallyPublishedAt?: string
508 account: {
509 name: string
510 host: string
511 }
512 isLocal: boolean
513 tags: string[]
514 privacy: number
515 likes?: number
516 dislikes?: number
517 duration: number
518 channel: {
519 displayName: string
520 name: string
521 description
522 isLocal: boolean
523 }
524 fixture: string
525 files: {
526 resolution: number
527 size: number
528 }[]
529 thumbnailfile?: string
530 previewfile?: string
531 }
532 ) {
533 if (!attributes.likes) attributes.likes = 0
534 if (!attributes.dislikes) attributes.dislikes = 0
535
536 expect(video.name).to.equal(attributes.name)
537 expect(video.category.id).to.equal(attributes.category)
538 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
539 expect(video.licence.id).to.equal(attributes.licence)
540 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
541 expect(video.language.id).to.equal(attributes.language)
542 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
543 expect(video.privacy.id).to.deep.equal(attributes.privacy)
544 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
545 expect(video.nsfw).to.equal(attributes.nsfw)
546 expect(video.description).to.equal(attributes.description)
547 expect(video.account.id).to.be.a('number')
548 expect(video.account.host).to.equal(attributes.account.host)
549 expect(video.account.name).to.equal(attributes.account.name)
550 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
551 expect(video.channel.name).to.equal(attributes.channel.name)
552 expect(video.likes).to.equal(attributes.likes)
553 expect(video.dislikes).to.equal(attributes.dislikes)
554 expect(video.isLocal).to.equal(attributes.isLocal)
555 expect(video.duration).to.equal(attributes.duration)
556 expect(dateIsValid(video.createdAt)).to.be.true
557 expect(dateIsValid(video.publishedAt)).to.be.true
558 expect(dateIsValid(video.updatedAt)).to.be.true
559
560 if (attributes.publishedAt) {
561 expect(video.publishedAt).to.equal(attributes.publishedAt)
562 }
563
564 if (attributes.originallyPublishedAt) {
565 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
566 } else {
567 expect(video.originallyPublishedAt).to.be.null
568 }
569
570 const res = await getVideo(url, video.uuid)
571 const videoDetails: VideoDetails = res.body
572
573 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
574 expect(videoDetails.tags).to.deep.equal(attributes.tags)
575 expect(videoDetails.account.name).to.equal(attributes.account.name)
576 expect(videoDetails.account.host).to.equal(attributes.account.host)
577 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
578 expect(video.channel.name).to.equal(attributes.channel.name)
579 expect(videoDetails.channel.host).to.equal(attributes.account.host)
580 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
581 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
582 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
583 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
584 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
585
586 for (const attributeFile of attributes.files) {
587 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
588 expect(file).not.to.be.undefined
589
590 let extension = extname(attributes.fixture)
591 // Transcoding enabled: extension will always be .mp4
592 if (attributes.files.length > 1) extension = '.mp4'
593
594 expect(file.magnetUri).to.have.lengthOf.above(2)
595 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
596 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
597 expect(file.resolution.id).to.equal(attributeFile.resolution)
598 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
599
600 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
601 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
602 expect(
603 file.size,
604 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
605 ).to.be.above(minSize).and.below(maxSize)
606
607 const torrent = await webtorrentAdd(file.magnetUri, true)
608 expect(torrent.files).to.be.an('array')
609 expect(torrent.files.length).to.equal(1)
610 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
611 }
612
613 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
614
615 if (attributes.previewfile) {
616 await testImage(url, attributes.previewfile, videoDetails.previewPath)
617 }
618 }
619
620 async function videoUUIDToId (url: string, id: number | string) {
621 if (validator.isUUID('' + id) === false) return id
622
623 const res = await getVideo(url, id)
624 return res.body.id
625 }
626
627 async function uploadVideoAndGetId (options: {
628 server: ServerInfo
629 videoName: string
630 nsfw?: boolean
631 privacy?: VideoPrivacy
632 token?: string
633 }) {
634 const videoAttrs: any = { name: options.videoName }
635 if (options.nsfw) videoAttrs.nsfw = options.nsfw
636 if (options.privacy) videoAttrs.privacy = options.privacy
637
638 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
639
640 return { id: res.body.video.id, uuid: res.body.video.uuid }
641 }
642
643 async function getLocalIdByUUID (url: string, uuid: string) {
644 const res = await getVideo(url, uuid)
645
646 return res.body.id
647 }
648
649 // serverNumber starts from 1
650 async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) {
651 const server = servers.find(s => s.serverNumber === serverNumber)
652 const res = await uploadRandomVideo(server, false, additionalParams)
653
654 await waitJobs(servers)
655
656 return res
657 }
658
659 async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
660 const prefixName = additionalParams.prefixName || ''
661 const name = prefixName + uuidv4()
662
663 const data = Object.assign({ name }, additionalParams)
664 const res = await uploadVideo(server.url, server.accessToken, data)
665
666 if (wait) await waitJobs([ server ])
667
668 return { uuid: res.body.video.uuid, name }
669 }
670
671 // ---------------------------------------------------------------------------
672
673 export {
674 getVideoDescription,
675 getVideoCategories,
676 uploadRandomVideo,
677 getVideoLicences,
678 videoUUIDToId,
679 getVideoPrivacies,
680 getVideoLanguages,
681 getMyVideos,
682 getAccountVideos,
683 getVideoChannelVideos,
684 getVideo,
685 getVideoFileMetadataUrl,
686 getVideoWithToken,
687 getVideosList,
688 getVideosListPagination,
689 getVideosListSort,
690 removeVideo,
691 getVideosListWithToken,
692 uploadVideo,
693 getVideosWithFilters,
694 uploadRandomVideoOnServers,
695 updateVideo,
696 rateVideo,
697 viewVideo,
698 parseTorrentVideo,
699 getLocalVideos,
700 completeVideoCheck,
701 checkVideoFilesWereRemoved,
702 getPlaylistVideos,
703 uploadVideoAndGetId,
704 getLocalIdByUUID,
705 getVideoIdFromUUID
706 }