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