]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/extra-utils/videos/videos.ts
Fix multiple server tests
[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.support !== undefined) {
366 req.field('support', attributes.support)
367 }
368
369 if (attributes.description !== undefined) {
370 req.field('description', attributes.description)
371 }
372 if (attributes.language !== undefined) {
373 req.field('language', attributes.language.toString())
374 }
375 if (attributes.category !== undefined) {
376 req.field('category', attributes.category.toString())
377 }
378 if (attributes.licence !== undefined) {
379 req.field('licence', attributes.licence.toString())
380 }
381
382 const tags = attributes.tags || []
383 for (let i = 0; i < tags.length; i++) {
384 req.field('tags[' + i + ']', attributes.tags[i])
385 }
386
387 if (attributes.thumbnailfile !== undefined) {
388 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
389 }
390 if (attributes.previewfile !== undefined) {
391 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
392 }
393
394 if (attributes.scheduleUpdate) {
395 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
396
397 if (attributes.scheduleUpdate.privacy) {
398 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
399 }
400 }
401
402 if (attributes.originallyPublishedAt !== undefined) {
403 req.field('originallyPublishedAt', attributes.originallyPublishedAt)
404 }
405
406 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
407 .expect(specialStatus)
408 }
409
410 function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
411 const path = '/api/v1/videos/' + id
412 const body = {}
413
414 if (attributes.name) body['name'] = attributes.name
415 if (attributes.category) body['category'] = attributes.category
416 if (attributes.licence) body['licence'] = attributes.licence
417 if (attributes.language) body['language'] = attributes.language
418 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
419 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
420 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
421 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
422 if (attributes.description) body['description'] = attributes.description
423 if (attributes.tags) body['tags'] = attributes.tags
424 if (attributes.privacy) body['privacy'] = attributes.privacy
425 if (attributes.channelId) body['channelId'] = attributes.channelId
426 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
427
428 // Upload request
429 if (attributes.thumbnailfile || attributes.previewfile) {
430 const attaches: any = {}
431 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
432 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
433
434 return makeUploadRequest({
435 url,
436 method: 'PUT',
437 path,
438 token: accessToken,
439 fields: body,
440 attaches,
441 statusCodeExpected
442 })
443 }
444
445 return makePutBodyRequest({
446 url,
447 path,
448 fields: body,
449 token: accessToken,
450 statusCodeExpected
451 })
452 }
453
454 function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
455 const path = '/api/v1/videos/' + id + '/rate'
456
457 return request(url)
458 .put(path)
459 .set('Accept', 'application/json')
460 .set('Authorization', 'Bearer ' + accessToken)
461 .send({ rating })
462 .expect(specialStatus)
463 }
464
465 function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
466 return new Promise<any>((res, rej) => {
467 const torrentName = videoUUID + '-' + resolution + '.torrent'
468 const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
469 readFile(torrentPath, (err, data) => {
470 if (err) return rej(err)
471
472 return res(parseTorrent(data))
473 })
474 })
475 }
476
477 async function completeVideoCheck (
478 url: string,
479 video: any,
480 attributes: {
481 name: string
482 category: number
483 licence: number
484 language: string
485 nsfw: boolean
486 commentsEnabled: boolean
487 downloadEnabled: boolean
488 description: string
489 publishedAt?: string
490 support: string
491 originallyPublishedAt?: string,
492 account: {
493 name: string
494 host: string
495 }
496 isLocal: boolean
497 tags: string[]
498 privacy: number
499 likes?: number
500 dislikes?: number
501 duration: number
502 channel: {
503 displayName: string
504 name: string
505 description
506 isLocal: boolean
507 }
508 fixture: string
509 files: {
510 resolution: number
511 size: number
512 }[],
513 thumbnailfile?: string
514 previewfile?: string
515 }
516 ) {
517 if (!attributes.likes) attributes.likes = 0
518 if (!attributes.dislikes) attributes.dislikes = 0
519
520 expect(video.name).to.equal(attributes.name)
521 expect(video.category.id).to.equal(attributes.category)
522 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
523 expect(video.licence.id).to.equal(attributes.licence)
524 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
525 expect(video.language.id).to.equal(attributes.language)
526 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
527 expect(video.privacy.id).to.deep.equal(attributes.privacy)
528 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
529 expect(video.nsfw).to.equal(attributes.nsfw)
530 expect(video.description).to.equal(attributes.description)
531 expect(video.account.id).to.be.a('number')
532 expect(video.account.host).to.equal(attributes.account.host)
533 expect(video.account.name).to.equal(attributes.account.name)
534 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
535 expect(video.channel.name).to.equal(attributes.channel.name)
536 expect(video.likes).to.equal(attributes.likes)
537 expect(video.dislikes).to.equal(attributes.dislikes)
538 expect(video.isLocal).to.equal(attributes.isLocal)
539 expect(video.duration).to.equal(attributes.duration)
540 expect(dateIsValid(video.createdAt)).to.be.true
541 expect(dateIsValid(video.publishedAt)).to.be.true
542 expect(dateIsValid(video.updatedAt)).to.be.true
543
544 if (attributes.publishedAt) {
545 expect(video.publishedAt).to.equal(attributes.publishedAt)
546 }
547
548 if (attributes.originallyPublishedAt) {
549 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
550 } else {
551 expect(video.originallyPublishedAt).to.be.null
552 }
553
554 const res = await getVideo(url, video.uuid)
555 const videoDetails: VideoDetails = res.body
556
557 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
558 expect(videoDetails.tags).to.deep.equal(attributes.tags)
559 expect(videoDetails.account.name).to.equal(attributes.account.name)
560 expect(videoDetails.account.host).to.equal(attributes.account.host)
561 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
562 expect(video.channel.name).to.equal(attributes.channel.name)
563 expect(videoDetails.channel.host).to.equal(attributes.account.host)
564 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
565 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
566 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
567 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
568 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
569
570 for (const attributeFile of attributes.files) {
571 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
572 expect(file).not.to.be.undefined
573
574 let extension = extname(attributes.fixture)
575 // Transcoding enabled: extension will always be .mp4
576 if (attributes.files.length > 1) extension = '.mp4'
577
578 const magnetUri = file.magnetUri
579 expect(file.magnetUri).to.have.lengthOf.above(2)
580 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
581 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
582 expect(file.resolution.id).to.equal(attributeFile.resolution)
583 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
584
585 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
586 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
587 expect(file.size,
588 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
589 .to.be.above(minSize).and.below(maxSize)
590
591 {
592 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
593 }
594
595 if (attributes.previewfile) {
596 await testImage(url, attributes.previewfile, videoDetails.previewPath)
597 }
598
599 const torrent = await webtorrentAdd(magnetUri, true)
600 expect(torrent.files).to.be.an('array')
601 expect(torrent.files.length).to.equal(1)
602 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
603 }
604 }
605
606 async function videoUUIDToId (url: string, id: number | string) {
607 if (validator.isUUID('' + id) === false) return id
608
609 const res = await getVideo(url, id)
610 return res.body.id
611 }
612
613 async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) {
614 const videoAttrs: any = { name: options.videoName }
615 if (options.nsfw) videoAttrs.nsfw = options.nsfw
616
617 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
618
619 return { id: res.body.video.id, uuid: res.body.video.uuid }
620 }
621
622 // ---------------------------------------------------------------------------
623
624 export {
625 getVideoDescription,
626 getVideoCategories,
627 getVideoLicences,
628 videoUUIDToId,
629 getVideoPrivacies,
630 getVideoLanguages,
631 getMyVideos,
632 getAccountVideos,
633 getVideoChannelVideos,
634 getVideo,
635 getVideoWithToken,
636 getVideosList,
637 getVideosListPagination,
638 getVideosListSort,
639 removeVideo,
640 getVideosListWithToken,
641 uploadVideo,
642 getVideosWithFilters,
643 updateVideo,
644 rateVideo,
645 viewVideo,
646 parseTorrentVideo,
647 getLocalVideos,
648 completeVideoCheck,
649 checkVideoFilesWereRemoved,
650 getPlaylistVideos,
651 uploadVideoAndGetId
652 }