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