aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils
diff options
context:
space:
mode:
Diffstat (limited to 'shared/extra-utils')
-rw-r--r--shared/extra-utils/cli/cli.ts24
-rw-r--r--shared/extra-utils/feeds/feeds.ts32
-rw-r--r--shared/extra-utils/index.ts26
-rw-r--r--shared/extra-utils/logs/logs.ts18
-rw-r--r--shared/extra-utils/miscs/email-child-process.js27
-rw-r--r--shared/extra-utils/miscs/email.ts68
-rw-r--r--shared/extra-utils/miscs/miscs.ts101
-rw-r--r--shared/extra-utils/miscs/sql.ts80
-rw-r--r--shared/extra-utils/miscs/stubs.ts14
-rw-r--r--shared/extra-utils/overviews/overviews.ts18
-rw-r--r--shared/extra-utils/requests/activitypub.ts43
-rw-r--r--shared/extra-utils/requests/check-api-params.ts40
-rw-r--r--shared/extra-utils/requests/requests.ts184
-rw-r--r--shared/extra-utils/search/video-channels.ts22
-rw-r--r--shared/extra-utils/search/videos.ts77
-rw-r--r--shared/extra-utils/server/activitypub.ts14
-rw-r--r--shared/extra-utils/server/clients.ts19
-rw-r--r--shared/extra-utils/server/config.ts156
-rw-r--r--shared/extra-utils/server/contact-form.ts28
-rw-r--r--shared/extra-utils/server/follows.ts111
-rw-r--r--shared/extra-utils/server/jobs.ts82
-rw-r--r--shared/extra-utils/server/redundancy.ts17
-rw-r--r--shared/extra-utils/server/servers.ts313
-rw-r--r--shared/extra-utils/server/stats.ts22
-rw-r--r--shared/extra-utils/socket/socket-io.ts13
-rw-r--r--shared/extra-utils/users/accounts.ts80
-rw-r--r--shared/extra-utils/users/blocklist.ts197
-rw-r--r--shared/extra-utils/users/login.ts62
-rw-r--r--shared/extra-utils/users/user-notifications.ts496
-rw-r--r--shared/extra-utils/users/user-subscriptions.ts82
-rw-r--r--shared/extra-utils/users/users.ts330
-rw-r--r--shared/extra-utils/videos/services.ts23
-rw-r--r--shared/extra-utils/videos/video-abuses.ts65
-rw-r--r--shared/extra-utils/videos/video-blacklist.ts72
-rw-r--r--shared/extra-utils/videos/video-captions.ts71
-rw-r--r--shared/extra-utils/videos/video-change-ownership.ts54
-rw-r--r--shared/extra-utils/videos/video-channels.ts134
-rw-r--r--shared/extra-utils/videos/video-comments.ts87
-rw-r--r--shared/extra-utils/videos/video-history.ts39
-rw-r--r--shared/extra-utils/videos/video-imports.ts57
-rw-r--r--shared/extra-utils/videos/video-playlists.ts318
-rw-r--r--shared/extra-utils/videos/video-streaming-playlists.ts51
-rw-r--r--shared/extra-utils/videos/videos.ts648
43 files changed, 4415 insertions, 0 deletions
diff --git a/shared/extra-utils/cli/cli.ts b/shared/extra-utils/cli/cli.ts
new file mode 100644
index 000000000..54d05e9c6
--- /dev/null
+++ b/shared/extra-utils/cli/cli.ts
@@ -0,0 +1,24 @@
1import { exec } from 'child_process'
2
3import { ServerInfo } from '../server/servers'
4
5function getEnvCli (server?: ServerInfo) {
6 return `NODE_ENV=test NODE_APP_INSTANCE=${server.serverNumber}`
7}
8
9async function execCLI (command: string) {
10 return new Promise<string>((res, rej) => {
11 exec(command, (err, stdout, stderr) => {
12 if (err) return rej(err)
13
14 return res(stdout)
15 })
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 execCLI,
23 getEnvCli
24}
diff --git a/shared/extra-utils/feeds/feeds.ts b/shared/extra-utils/feeds/feeds.ts
new file mode 100644
index 000000000..af6df2b20
--- /dev/null
+++ b/shared/extra-utils/feeds/feeds.ts
@@ -0,0 +1,32 @@
1import * as request from 'supertest'
2
3type FeedType = 'videos' | 'video-comments'
4
5function getXMLfeed (url: string, feed: FeedType, format?: string) {
6 const path = '/feeds/' + feed + '.xml'
7
8 return request(url)
9 .get(path)
10 .query((format) ? { format: format } : {})
11 .set('Accept', 'application/xml')
12 .expect(200)
13 .expect('Content-Type', /xml/)
14}
15
16function getJSONfeed (url: string, feed: FeedType, query: any = {}) {
17 const path = '/feeds/' + feed + '.json'
18
19 return request(url)
20 .get(path)
21 .query(query)
22 .set('Accept', 'application/json')
23 .expect(200)
24 .expect('Content-Type', /json/)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 getXMLfeed,
31 getJSONfeed
32}
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
new file mode 100644
index 000000000..9d0bbaa38
--- /dev/null
+++ b/shared/extra-utils/index.ts
@@ -0,0 +1,26 @@
1export * from './server/activitypub'
2export * from './cli/cli'
3export * from './server/clients'
4export * from './server/config'
5export * from './server/jobs'
6export * from './users/login'
7export * from './miscs/miscs'
8export * from './miscs/stubs'
9export * from './miscs/sql'
10export * from './server/follows'
11export * from './requests/requests'
12export * from './requests/check-api-params'
13export * from './server/servers'
14export * from './videos/services'
15export * from './videos/video-playlists'
16export * from './users/users'
17export * from './users/accounts'
18export * from './videos/video-abuses'
19export * from './videos/video-blacklist'
20export * from './videos/video-channels'
21export * from './videos/video-comments'
22export * from './videos/video-streaming-playlists'
23export * from './videos/videos'
24export * from './videos/video-change-ownership'
25export * from './feeds/feeds'
26export * from './search/videos'
diff --git a/shared/extra-utils/logs/logs.ts b/shared/extra-utils/logs/logs.ts
new file mode 100644
index 000000000..cbb1afb93
--- /dev/null
+++ b/shared/extra-utils/logs/logs.ts
@@ -0,0 +1,18 @@
1import { makeGetRequest } from '../requests/requests'
2import { LogLevel } from '../../models/server/log-level.type'
3
4function getLogs (url: string, accessToken: string, startDate: Date, endDate?: Date, level?: LogLevel) {
5 const path = '/api/v1/server/logs'
6
7 return makeGetRequest({
8 url,
9 path,
10 token: accessToken,
11 query: { startDate, endDate, level },
12 statusCodeExpected: 200
13 })
14}
15
16export {
17 getLogs
18}
diff --git a/shared/extra-utils/miscs/email-child-process.js b/shared/extra-utils/miscs/email-child-process.js
new file mode 100644
index 000000000..088a5a08c
--- /dev/null
+++ b/shared/extra-utils/miscs/email-child-process.js
@@ -0,0 +1,27 @@
1const MailDev = require('maildev')
2
3// must run maildev as forked ChildProcess
4// failed instantiation stops main process with exit code 0
5process.on('message', (msg) => {
6 if (msg.start) {
7 const maildev = new MailDev({
8 ip: '127.0.0.1',
9 smtp: msg.port,
10 disableWeb: true,
11 silent: true
12 })
13
14 maildev.on('new', email => {
15 process.send({ email })
16 })
17
18 maildev.listen(err => {
19 if (err) {
20 // cannot send as Error object
21 return process.send({ err: err.message })
22 }
23
24 return process.send({ err: null })
25 })
26 }
27})
diff --git a/shared/extra-utils/miscs/email.ts b/shared/extra-utils/miscs/email.ts
new file mode 100644
index 000000000..b2a1093da
--- /dev/null
+++ b/shared/extra-utils/miscs/email.ts
@@ -0,0 +1,68 @@
1import { ChildProcess, fork } from 'child_process'
2import { randomInt } from '../../core-utils/miscs/miscs'
3import { parallelTests } from '../server/servers'
4
5class MockSmtpServer {
6
7 private static instance: MockSmtpServer
8 private started = false
9 private emailChildProcess: ChildProcess
10 private emails: object[]
11
12 private constructor () {
13 this.emailChildProcess = fork(`${__dirname}/email-child-process`, [])
14
15 this.emailChildProcess.on('message', (msg) => {
16 if (msg.email) {
17 return this.emails.push(msg.email)
18 }
19 })
20
21 process.on('exit', () => this.kill())
22 }
23
24 collectEmails (emailsCollection: object[]) {
25 return new Promise<number>((res, rej) => {
26 const port = parallelTests() ? randomInt(1000, 2000) : 1025
27
28 if (this.started) {
29 this.emails = emailsCollection
30 return res()
31 }
32
33 // ensure maildev isn't started until
34 // unexpected exit can be reported to test runner
35 this.emailChildProcess.send({ start: true, port })
36 this.emailChildProcess.on('exit', () => {
37 return rej(new Error('maildev exited unexpectedly, confirm port not in use'))
38 })
39 this.emailChildProcess.on('message', (msg) => {
40 if (msg.err) {
41 return rej(new Error(msg.err))
42 }
43 this.started = true
44 this.emails = emailsCollection
45 return res(port)
46 })
47 })
48 }
49
50 kill () {
51 if (!this.emailChildProcess) return
52
53 process.kill(this.emailChildProcess.pid)
54
55 this.emailChildProcess = null
56 MockSmtpServer.instance = null
57 }
58
59 static get Instance () {
60 return this.instance || (this.instance = new this())
61 }
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 MockSmtpServer
68}
diff --git a/shared/extra-utils/miscs/miscs.ts b/shared/extra-utils/miscs/miscs.ts
new file mode 100644
index 000000000..d1ffb7be4
--- /dev/null
+++ b/shared/extra-utils/miscs/miscs.ts
@@ -0,0 +1,101 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import { isAbsolute, join } from 'path'
5import * as request from 'supertest'
6import * as WebTorrent from 'webtorrent'
7import { pathExists, readFile } from 'fs-extra'
8import * as ffmpeg from 'fluent-ffmpeg'
9
10const expect = chai.expect
11let webtorrent = new WebTorrent()
12
13function immutableAssign <T, U> (target: T, source: U) {
14 return Object.assign<{}, T, U>({}, target, source)
15}
16
17 // Default interval -> 5 minutes
18function dateIsValid (dateString: string, interval = 300000) {
19 const dateToCheck = new Date(dateString)
20 const now = new Date()
21
22 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
23}
24
25function wait (milliseconds: number) {
26 return new Promise(resolve => setTimeout(resolve, milliseconds))
27}
28
29function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
30 if (refreshWebTorrent === true) webtorrent = new WebTorrent()
31
32 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
33}
34
35function root () {
36 // We are in /miscs
37 return join(__dirname, '..', '..', '..')
38}
39
40async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
41 const res = await request(url)
42 .get(imagePath)
43 .expect(200)
44
45 const body = res.body
46
47 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
48 const minLength = body.length - ((20 * body.length) / 100)
49 const maxLength = body.length + ((20 * body.length) / 100)
50
51 expect(data.length).to.be.above(minLength)
52 expect(data.length).to.be.below(maxLength)
53}
54
55function buildAbsoluteFixturePath (path: string, customTravisPath = false) {
56 if (isAbsolute(path)) {
57 return path
58 }
59
60 if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path)
61
62 return join(root(), 'server', 'tests', 'fixtures', path)
63}
64
65async function generateHighBitrateVideo () {
66 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
67
68 const exists = await pathExists(tempFixturePath)
69 if (!exists) {
70
71 // Generate a random, high bitrate video on the fly, so we don't have to include
72 // a large file in the repo. The video needs to have a certain minimum length so
73 // that FFmpeg properly applies bitrate limits.
74 // https://stackoverflow.com/a/15795112
75 return new Promise<string>(async (res, rej) => {
76 ffmpeg()
77 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
78 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
79 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
80 .output(tempFixturePath)
81 .on('error', rej)
82 .on('end', () => res(tempFixturePath))
83 .run()
84 })
85 }
86
87 return tempFixturePath
88}
89
90// ---------------------------------------------------------------------------
91
92export {
93 dateIsValid,
94 wait,
95 webtorrentAdd,
96 immutableAssign,
97 testImage,
98 buildAbsoluteFixturePath,
99 root,
100 generateHighBitrateVideo
101}
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts
new file mode 100644
index 000000000..3cfae5c23
--- /dev/null
+++ b/shared/extra-utils/miscs/sql.ts
@@ -0,0 +1,80 @@
1import { QueryTypes, Sequelize } from 'sequelize'
2
3let sequelizes: { [ id: number ]: Sequelize } = {}
4
5function getSequelize (serverNumber: number) {
6 if (sequelizes[serverNumber]) return sequelizes[serverNumber]
7
8 const dbname = 'peertube_test' + serverNumber
9 const username = 'peertube'
10 const password = 'peertube'
11 const host = 'localhost'
12 const port = 5432
13
14 const seq = new Sequelize(dbname, username, password, {
15 dialect: 'postgres',
16 host,
17 port,
18 logging: false
19 })
20
21 sequelizes[serverNumber] = seq
22
23 return seq
24}
25
26function setActorField (serverNumber: number, to: string, field: string, value: string) {
27 const seq = getSequelize(serverNumber)
28
29 const options = { type: QueryTypes.UPDATE }
30
31 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
32}
33
34function setVideoField (serverNumber: number, uuid: string, field: string, value: string) {
35 const seq = getSequelize(serverNumber)
36
37 const options = { type: QueryTypes.UPDATE }
38
39 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
40}
41
42function setPlaylistField (serverNumber: number, uuid: string, field: string, value: string) {
43 const seq = getSequelize(serverNumber)
44
45 const options = { type: QueryTypes.UPDATE }
46
47 return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
48}
49
50async function countVideoViewsOf (serverNumber: number, uuid: string) {
51 const seq = getSequelize(serverNumber)
52
53 // tslint:disable
54 const query = `SELECT SUM("videoView"."views") AS "total" FROM "videoView" INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
55
56 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
57 const [ { total } ] = await seq.query<{ total: number }>(query, options)
58
59 if (!total) return 0
60
61 // FIXME: check if we really need parseInt
62 return parseInt(total + '', 10)
63}
64
65async function closeAllSequelize (servers: any[]) {
66 for (let i = 1; i <= servers.length; i++) {
67 if (sequelizes[ i ]) {
68 await sequelizes[ i ].close()
69 delete sequelizes[ i ]
70 }
71 }
72}
73
74export {
75 setVideoField,
76 setPlaylistField,
77 setActorField,
78 countVideoViewsOf,
79 closeAllSequelize
80}
diff --git a/shared/extra-utils/miscs/stubs.ts b/shared/extra-utils/miscs/stubs.ts
new file mode 100644
index 000000000..d1eb0e3b2
--- /dev/null
+++ b/shared/extra-utils/miscs/stubs.ts
@@ -0,0 +1,14 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
12 buildResponseStub,
13 buildRequestStub
14}
diff --git a/shared/extra-utils/overviews/overviews.ts b/shared/extra-utils/overviews/overviews.ts
new file mode 100644
index 000000000..23e3ceb1e
--- /dev/null
+++ b/shared/extra-utils/overviews/overviews.ts
@@ -0,0 +1,18 @@
1import { makeGetRequest } from '../requests/requests'
2
3function getVideosOverview (url: string, useCache = false) {
4 const path = '/api/v1/overviews/videos'
5
6 const query = {
7 t: useCache ? undefined : new Date().getTime()
8 }
9
10 return makeGetRequest({
11 url,
12 path,
13 query,
14 statusCodeExpected: 200
15 })
16}
17
18export { getVideosOverview }
diff --git a/shared/extra-utils/requests/activitypub.ts b/shared/extra-utils/requests/activitypub.ts
new file mode 100644
index 000000000..4762a8665
--- /dev/null
+++ b/shared/extra-utils/requests/activitypub.ts
@@ -0,0 +1,43 @@
1import { doRequest } from '../../../server/helpers/requests'
2import { HTTP_SIGNATURE } from '../../../server/initializers/constants'
3import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../server/helpers/activitypub'
5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = {
8 method: 'POST',
9 uri: url,
10 json: body,
11 httpSignature,
12 headers
13 }
14
15 return doRequest(options)
16}
17
18async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
19 const follow = {
20 type: 'Follow',
21 id: by.url + '/toto',
22 actor: by.url,
23 object: to.url
24 }
25
26 const body = activityPubContextify(follow)
27
28 const httpSignature = {
29 algorithm: HTTP_SIGNATURE.ALGORITHM,
30 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
31 keyId: by.url,
32 key: by.privateKey,
33 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
34 }
35 const headers = buildGlobalHeaders(body)
36
37 return makePOSTAPRequest(to.url, body, httpSignature, headers)
38}
39
40export {
41 makePOSTAPRequest,
42 makeFollowRequest
43}
diff --git a/shared/extra-utils/requests/check-api-params.ts b/shared/extra-utils/requests/check-api-params.ts
new file mode 100644
index 000000000..a2a549682
--- /dev/null
+++ b/shared/extra-utils/requests/check-api-params.ts
@@ -0,0 +1,40 @@
1import { makeGetRequest } from './requests'
2import { immutableAssign } from '../miscs/miscs'
3
4function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
5 return makeGetRequest({
6 url,
7 path,
8 token,
9 query: immutableAssign(query, { start: 'hello' }),
10 statusCodeExpected: 400
11 })
12}
13
14function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
15 return makeGetRequest({
16 url,
17 path,
18 token,
19 query: immutableAssign(query, { count: 'hello' }),
20 statusCodeExpected: 400
21 })
22}
23
24function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
25 return makeGetRequest({
26 url,
27 path,
28 token,
29 query: immutableAssign(query, { sort: 'hello' }),
30 statusCodeExpected: 400
31 })
32}
33
34// ---------------------------------------------------------------------------
35
36export {
37 checkBadStartPagination,
38 checkBadCountPagination,
39 checkBadSortPagination
40}
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts
new file mode 100644
index 000000000..3532fb429
--- /dev/null
+++ b/shared/extra-utils/requests/requests.ts
@@ -0,0 +1,184 @@
1import * as request from 'supertest'
2import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
3import { isAbsolute, join } from 'path'
4import { parse } from 'url'
5
6function get4KFileUrl () {
7 return 'https://download.cpy.re/peertube/4k_file.txt'
8}
9
10function makeRawRequest (url: string, statusCodeExpected?: number, range?: string) {
11 const { host, protocol, pathname } = parse(url)
12
13 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range })
14}
15
16function makeGetRequest (options: {
17 url: string,
18 path?: string,
19 query?: any,
20 token?: string,
21 statusCodeExpected?: number,
22 contentType?: string,
23 range?: string
24}) {
25 if (!options.statusCodeExpected) options.statusCodeExpected = 400
26 if (options.contentType === undefined) options.contentType = 'application/json'
27
28 const req = request(options.url).get(options.path)
29
30 if (options.contentType) req.set('Accept', options.contentType)
31 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
32 if (options.query) req.query(options.query)
33 if (options.range) req.set('Range', options.range)
34
35 return req.expect(options.statusCodeExpected)
36}
37
38function makeDeleteRequest (options: {
39 url: string,
40 path: string,
41 token?: string,
42 statusCodeExpected?: number
43}) {
44 if (!options.statusCodeExpected) options.statusCodeExpected = 400
45
46 const req = request(options.url)
47 .delete(options.path)
48 .set('Accept', 'application/json')
49
50 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
51
52 return req.expect(options.statusCodeExpected)
53}
54
55function makeUploadRequest (options: {
56 url: string,
57 method?: 'POST' | 'PUT',
58 path: string,
59 token?: string,
60 fields: { [ fieldName: string ]: any },
61 attaches: { [ attachName: string ]: any | any[] },
62 statusCodeExpected?: number
63}) {
64 if (!options.statusCodeExpected) options.statusCodeExpected = 400
65
66 let req: request.Test
67 if (options.method === 'PUT') {
68 req = request(options.url).put(options.path)
69 } else {
70 req = request(options.url).post(options.path)
71 }
72
73 req.set('Accept', 'application/json')
74
75 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
76
77 Object.keys(options.fields).forEach(field => {
78 const value = options.fields[field]
79
80 if (value === undefined) return
81
82 if (Array.isArray(value)) {
83 for (let i = 0; i < value.length; i++) {
84 req.field(field + '[' + i + ']', value[i])
85 }
86 } else {
87 req.field(field, value)
88 }
89 })
90
91 Object.keys(options.attaches).forEach(attach => {
92 const value = options.attaches[attach]
93 if (Array.isArray(value)) {
94 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
95 } else {
96 req.attach(attach, buildAbsoluteFixturePath(value))
97 }
98 })
99
100 return req.expect(options.statusCodeExpected)
101}
102
103function makePostBodyRequest (options: {
104 url: string,
105 path: string,
106 token?: string,
107 fields?: { [ fieldName: string ]: any },
108 statusCodeExpected?: number
109}) {
110 if (!options.fields) options.fields = {}
111 if (!options.statusCodeExpected) options.statusCodeExpected = 400
112
113 const req = request(options.url)
114 .post(options.path)
115 .set('Accept', 'application/json')
116
117 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
118
119 return req.send(options.fields)
120 .expect(options.statusCodeExpected)
121}
122
123function makePutBodyRequest (options: {
124 url: string,
125 path: string,
126 token?: string,
127 fields: { [ fieldName: string ]: any },
128 statusCodeExpected?: number
129}) {
130 if (!options.statusCodeExpected) options.statusCodeExpected = 400
131
132 const req = request(options.url)
133 .put(options.path)
134 .set('Accept', 'application/json')
135
136 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
137
138 return req.send(options.fields)
139 .expect(options.statusCodeExpected)
140}
141
142function makeHTMLRequest (url: string, path: string) {
143 return request(url)
144 .get(path)
145 .set('Accept', 'text/html')
146 .expect(200)
147}
148
149function updateAvatarRequest (options: {
150 url: string,
151 path: string,
152 accessToken: string,
153 fixture: string
154}) {
155 let filePath = ''
156 if (isAbsolute(options.fixture)) {
157 filePath = options.fixture
158 } else {
159 filePath = join(root(), 'server', 'tests', 'fixtures', options.fixture)
160 }
161
162 return makeUploadRequest({
163 url: options.url,
164 path: options.path,
165 token: options.accessToken,
166 fields: {},
167 attaches: { avatarfile: filePath },
168 statusCodeExpected: 200
169 })
170}
171
172// ---------------------------------------------------------------------------
173
174export {
175 get4KFileUrl,
176 makeHTMLRequest,
177 makeGetRequest,
178 makeUploadRequest,
179 makePostBodyRequest,
180 makePutBodyRequest,
181 makeDeleteRequest,
182 makeRawRequest,
183 updateAvatarRequest
184}
diff --git a/shared/extra-utils/search/video-channels.ts b/shared/extra-utils/search/video-channels.ts
new file mode 100644
index 000000000..0532134ae
--- /dev/null
+++ b/shared/extra-utils/search/video-channels.ts
@@ -0,0 +1,22 @@
1import { makeGetRequest } from '../requests/requests'
2
3function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = 200) {
4 const path = '/api/v1/search/video-channels'
5
6 return makeGetRequest({
7 url,
8 path,
9 query: {
10 sort: '-createdAt',
11 search
12 },
13 token,
14 statusCodeExpected
15 })
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 searchVideoChannel
22}
diff --git a/shared/extra-utils/search/videos.ts b/shared/extra-utils/search/videos.ts
new file mode 100644
index 000000000..ba4627017
--- /dev/null
+++ b/shared/extra-utils/search/videos.ts
@@ -0,0 +1,77 @@
1/* tslint:disable:no-unused-expression */
2
3import * as request from 'supertest'
4import { VideosSearchQuery } from '../../models/search'
5import { immutableAssign } from '../miscs/miscs'
6
7function searchVideo (url: string, search: string) {
8 const path = '/api/v1/search/videos'
9 const req = request(url)
10 .get(path)
11 .query({ sort: '-publishedAt', search })
12 .set('Accept', 'application/json')
13
14 return req.expect(200)
15 .expect('Content-Type', /json/)
16}
17
18function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
19 const path = '/api/v1/search/videos'
20 const req = request(url)
21 .get(path)
22 .set('Authorization', 'Bearer ' + token)
23 .query(immutableAssign(query, { sort: '-publishedAt', search }))
24 .set('Accept', 'application/json')
25
26 return req.expect(200)
27 .expect('Content-Type', /json/)
28}
29
30function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
31 const path = '/api/v1/search/videos'
32
33 const req = request(url)
34 .get(path)
35 .query({ start })
36 .query({ search })
37 .query({ count })
38
39 if (sort) req.query({ sort })
40
41 return req.set('Accept', 'application/json')
42 .expect(200)
43 .expect('Content-Type', /json/)
44}
45
46function searchVideoWithSort (url: string, search: string, sort: string) {
47 const path = '/api/v1/search/videos'
48
49 return request(url)
50 .get(path)
51 .query({ search })
52 .query({ sort })
53 .set('Accept', 'application/json')
54 .expect(200)
55 .expect('Content-Type', /json/)
56}
57
58function advancedVideosSearch (url: string, options: VideosSearchQuery) {
59 const path = '/api/v1/search/videos'
60
61 return request(url)
62 .get(path)
63 .query(options)
64 .set('Accept', 'application/json')
65 .expect(200)
66 .expect('Content-Type', /json/)
67}
68
69// ---------------------------------------------------------------------------
70
71export {
72 searchVideo,
73 advancedVideosSearch,
74 searchVideoWithToken,
75 searchVideoWithPagination,
76 searchVideoWithSort
77}
diff --git a/shared/extra-utils/server/activitypub.ts b/shared/extra-utils/server/activitypub.ts
new file mode 100644
index 000000000..eccb198ca
--- /dev/null
+++ b/shared/extra-utils/server/activitypub.ts
@@ -0,0 +1,14 @@
1import * as request from 'supertest'
2
3function makeActivityPubGetRequest (url: string, path: string, expectedStatus = 200) {
4 return request(url)
5 .get(path)
6 .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8')
7 .expect(expectedStatus)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 makeActivityPubGetRequest
14}
diff --git a/shared/extra-utils/server/clients.ts b/shared/extra-utils/server/clients.ts
new file mode 100644
index 000000000..273aac747
--- /dev/null
+++ b/shared/extra-utils/server/clients.ts
@@ -0,0 +1,19 @@
1import * as request from 'supertest'
2import * as urlUtil from 'url'
3
4function getClient (url: string) {
5 const path = '/api/v1/oauth-clients/local'
6
7 return request(url)
8 .get(path)
9 .set('Host', urlUtil.parse(url).host)
10 .set('Accept', 'application/json')
11 .expect(200)
12 .expect('Content-Type', /json/)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 getClient
19}
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
new file mode 100644
index 000000000..deb77e9c0
--- /dev/null
+++ b/shared/extra-utils/server/config.ts
@@ -0,0 +1,156 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../models/server/custom-config.model'
3
4function getConfig (url: string) {
5 const path = '/api/v1/config'
6
7 return makeGetRequest({
8 url,
9 path,
10 statusCodeExpected: 200
11 })
12}
13
14function getAbout (url: string) {
15 const path = '/api/v1/config/about'
16
17 return makeGetRequest({
18 url,
19 path,
20 statusCodeExpected: 200
21 })
22}
23
24function getCustomConfig (url: string, token: string, statusCodeExpected = 200) {
25 const path = '/api/v1/config/custom'
26
27 return makeGetRequest({
28 url,
29 token,
30 path,
31 statusCodeExpected
32 })
33}
34
35function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = 200) {
36 const path = '/api/v1/config/custom'
37
38 return makePutBodyRequest({
39 url,
40 token,
41 path,
42 fields: newCustomConfig,
43 statusCodeExpected
44 })
45}
46
47function updateCustomSubConfig (url: string, token: string, newConfig: any) {
48 const updateParams: CustomConfig = {
49 instance: {
50 name: 'PeerTube updated',
51 shortDescription: 'my short description',
52 description: 'my super description',
53 terms: 'my super terms',
54 defaultClientRoute: '/videos/recently-added',
55 isNSFW: true,
56 defaultNSFWPolicy: 'blur',
57 customizations: {
58 javascript: 'alert("coucou")',
59 css: 'body { background-color: red; }'
60 }
61 },
62 services: {
63 twitter: {
64 username: '@MySuperUsername',
65 whitelisted: true
66 }
67 },
68 cache: {
69 previews: {
70 size: 2
71 },
72 captions: {
73 size: 3
74 }
75 },
76 signup: {
77 enabled: false,
78 limit: 5,
79 requiresEmailVerification: false
80 },
81 admin: {
82 email: 'superadmin1@example.com'
83 },
84 contactForm: {
85 enabled: true
86 },
87 user: {
88 videoQuota: 5242881,
89 videoQuotaDaily: 318742
90 },
91 transcoding: {
92 enabled: true,
93 allowAdditionalExtensions: true,
94 threads: 1,
95 resolutions: {
96 '240p': false,
97 '360p': true,
98 '480p': true,
99 '720p': false,
100 '1080p': false
101 },
102 hls: {
103 enabled: false
104 }
105 },
106 import: {
107 videos: {
108 http: {
109 enabled: false
110 },
111 torrent: {
112 enabled: false
113 }
114 }
115 },
116 autoBlacklist: {
117 videos: {
118 ofUsers: {
119 enabled: false
120 }
121 }
122 },
123 followers: {
124 instance: {
125 enabled: true,
126 manualApproval: false
127 }
128 }
129 }
130
131 Object.assign(updateParams, newConfig)
132
133 return updateCustomConfig(url, token, updateParams)
134}
135
136function deleteCustomConfig (url: string, token: string, statusCodeExpected = 200) {
137 const path = '/api/v1/config/custom'
138
139 return makeDeleteRequest({
140 url,
141 token,
142 path,
143 statusCodeExpected
144 })
145}
146
147// ---------------------------------------------------------------------------
148
149export {
150 getConfig,
151 getCustomConfig,
152 updateCustomConfig,
153 getAbout,
154 deleteCustomConfig,
155 updateCustomSubConfig
156}
diff --git a/shared/extra-utils/server/contact-form.ts b/shared/extra-utils/server/contact-form.ts
new file mode 100644
index 000000000..80394cf99
--- /dev/null
+++ b/shared/extra-utils/server/contact-form.ts
@@ -0,0 +1,28 @@
1import * as request from 'supertest'
2import { ContactForm } from '../../models/server'
3
4function sendContactForm (options: {
5 url: string,
6 fromEmail: string,
7 fromName: string,
8 body: string,
9 expectedStatus?: number
10}) {
11 const path = '/api/v1/server/contact'
12
13 const body: ContactForm = {
14 fromEmail: options.fromEmail,
15 fromName: options.fromName,
16 body: options.body
17 }
18 return request(options.url)
19 .post(path)
20 .send(body)
21 .expect(options.expectedStatus || 204)
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 sendContactForm
28}
diff --git a/shared/extra-utils/server/follows.ts b/shared/extra-utils/server/follows.ts
new file mode 100644
index 000000000..1505804de
--- /dev/null
+++ b/shared/extra-utils/server/follows.ts
@@ -0,0 +1,111 @@
1import * as request from 'supertest'
2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs'
4import { makeGetRequest, makePostBodyRequest } from '..'
5
6function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
7 const path = '/api/v1/server/followers'
8
9 return request(url)
10 .get(path)
11 .query({ start })
12 .query({ count })
13 .query({ sort })
14 .query({ search })
15 .set('Accept', 'application/json')
16 .expect(200)
17 .expect('Content-Type', /json/)
18}
19
20function acceptFollower (url: string, token: string, follower: string, statusCodeExpected = 204) {
21 const path = '/api/v1/server/followers/' + follower + '/accept'
22
23 return makePostBodyRequest({
24 url,
25 token,
26 path,
27 statusCodeExpected
28 })
29}
30
31function rejectFollower (url: string, token: string, follower: string, statusCodeExpected = 204) {
32 const path = '/api/v1/server/followers/' + follower + '/reject'
33
34 return makePostBodyRequest({
35 url,
36 token,
37 path,
38 statusCodeExpected
39 })
40}
41
42function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
43 const path = '/api/v1/server/following'
44
45 return request(url)
46 .get(path)
47 .query({ start })
48 .query({ count })
49 .query({ sort })
50 .query({ search })
51 .set('Accept', 'application/json')
52 .expect(200)
53 .expect('Content-Type', /json/)
54}
55
56function follow (follower: string, following: string[], accessToken: string, expectedStatus = 204) {
57 const path = '/api/v1/server/following'
58
59 const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
60 return request(follower)
61 .post(path)
62 .set('Accept', 'application/json')
63 .set('Authorization', 'Bearer ' + accessToken)
64 .send({ 'hosts': followingHosts })
65 .expect(expectedStatus)
66}
67
68async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
69 const path = '/api/v1/server/following/' + target.host
70
71 return request(url)
72 .delete(path)
73 .set('Accept', 'application/json')
74 .set('Authorization', 'Bearer ' + accessToken)
75 .expect(expectedStatus)
76}
77
78function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = 204) {
79 const path = '/api/v1/server/followers/peertube@' + follower.host
80
81 return request(url)
82 .delete(path)
83 .set('Accept', 'application/json')
84 .set('Authorization', 'Bearer ' + accessToken)
85 .expect(expectedStatus)
86}
87
88async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
89 await Promise.all([
90 follow(server1.url, [ server2.url ], server1.accessToken),
91 follow(server2.url, [ server1.url ], server2.accessToken)
92 ])
93
94 // Wait request propagation
95 await waitJobs([ server1, server2 ])
96
97 return true
98}
99
100// ---------------------------------------------------------------------------
101
102export {
103 getFollowersListPaginationAndSort,
104 getFollowingListPaginationAndSort,
105 unfollow,
106 removeFollower,
107 follow,
108 doubleFollow,
109 acceptFollower,
110 rejectFollower
111}
diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts
new file mode 100644
index 000000000..692b5e24d
--- /dev/null
+++ b/shared/extra-utils/server/jobs.ts
@@ -0,0 +1,82 @@
1import * as request from 'supertest'
2import { Job, JobState } from '../../models'
3import { wait } from '../miscs/miscs'
4import { ServerInfo } from './servers'
5
6function getJobsList (url: string, accessToken: string, state: JobState) {
7 const path = '/api/v1/jobs/' + state
8
9 return request(url)
10 .get(path)
11 .set('Accept', 'application/json')
12 .set('Authorization', 'Bearer ' + accessToken)
13 .expect(200)
14 .expect('Content-Type', /json/)
15}
16
17function getJobsListPaginationAndSort (url: string, accessToken: string, state: JobState, start: number, count: number, sort: string) {
18 const path = '/api/v1/jobs/' + state
19
20 return request(url)
21 .get(path)
22 .query({ start })
23 .query({ count })
24 .query({ sort })
25 .set('Accept', 'application/json')
26 .set('Authorization', 'Bearer ' + accessToken)
27 .expect(200)
28 .expect('Content-Type', /json/)
29}
30
31async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
32 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) : 2000
33 let servers: ServerInfo[]
34
35 if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ]
36 else servers = serversArg as ServerInfo[]
37
38 const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
39 let pendingRequests = false
40
41 function tasksBuilder () {
42 const tasks: Promise<any>[] = []
43 pendingRequests = false
44
45 // Check if each server has pending request
46 for (const server of servers) {
47 for (const state of states) {
48 const p = getJobsListPaginationAndSort(server.url, server.accessToken, state, 0, 10, '-createdAt')
49 .then(res => res.body.data)
50 .then((jobs: Job[]) => jobs.filter(j => j.type !== 'videos-views'))
51 .then(jobs => {
52 if (jobs.length !== 0) pendingRequests = true
53 })
54 tasks.push(p)
55 }
56 }
57
58 return tasks
59 }
60
61 do {
62 await Promise.all(tasksBuilder())
63
64 // Retry, in case of new jobs were created
65 if (pendingRequests === false) {
66 await wait(pendingJobWait)
67 await Promise.all(tasksBuilder())
68 }
69
70 if (pendingRequests) {
71 await wait(1000)
72 }
73 } while (pendingRequests)
74}
75
76// ---------------------------------------------------------------------------
77
78export {
79 getJobsList,
80 waitJobs,
81 getJobsListPaginationAndSort
82}
diff --git a/shared/extra-utils/server/redundancy.ts b/shared/extra-utils/server/redundancy.ts
new file mode 100644
index 000000000..c39ff2c8b
--- /dev/null
+++ b/shared/extra-utils/server/redundancy.ts
@@ -0,0 +1,17 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3async function updateRedundancy (url: string, accessToken: string, host: string, redundancyAllowed: boolean, expectedStatus = 204) {
4 const path = '/api/v1/server/redundancy/' + host
5
6 return makePutBodyRequest({
7 url,
8 path,
9 token: accessToken,
10 fields: { redundancyAllowed },
11 statusCodeExpected: expectedStatus
12 })
13}
14
15export {
16 updateRedundancy
17}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
new file mode 100644
index 000000000..ed41bfa48
--- /dev/null
+++ b/shared/extra-utils/server/servers.ts
@@ -0,0 +1,313 @@
1/* tslint:disable:no-unused-expression */
2
3import { ChildProcess, exec, fork } from 'child_process'
4import { join } from 'path'
5import { root, wait } from '../miscs/miscs'
6import { copy, readdir, readFile, remove } from 'fs-extra'
7import { existsSync } from 'fs'
8import { expect } from 'chai'
9import { VideoChannel } from '../../models/videos'
10import { randomInt } from '../../core-utils/miscs/miscs'
11
12interface ServerInfo {
13 app: ChildProcess,
14 url: string
15 host: string
16
17 port: number
18 parallel: boolean
19 internalServerNumber: number
20 serverNumber: number
21
22 client: {
23 id: string,
24 secret: string
25 }
26
27 user: {
28 username: string,
29 password: string,
30 email?: string
31 }
32
33 customConfigFile?: string
34
35 accessToken?: string
36 videoChannel?: VideoChannel
37
38 video?: {
39 id: number
40 uuid: string
41 name: string
42 account: {
43 name: string
44 }
45 }
46
47 remoteVideo?: {
48 id: number
49 uuid: string
50 }
51
52 videos?: { id: number, uuid: string }[]
53}
54
55function parallelTests () {
56 return process.env.MOCHA_PARALLEL === 'true'
57}
58
59function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
60 let apps = []
61 let i = 0
62
63 return new Promise<ServerInfo[]>(res => {
64 function anotherServerDone (serverNumber, app) {
65 apps[serverNumber - 1] = app
66 i++
67 if (i === totalServers) {
68 return res(apps)
69 }
70 }
71
72 for (let j = 1; j <= totalServers; j++) {
73 flushAndRunServer(j, configOverride).then(app => anotherServerDone(j, app))
74 }
75 })
76}
77
78function flushTests (serverNumber?: number) {
79 return new Promise<void>((res, rej) => {
80 const suffix = serverNumber ? ` -- ${serverNumber}` : ''
81
82 return exec('npm run clean:server:test' + suffix, err => {
83 if (err) return rej(err)
84
85 return res()
86 })
87 })
88}
89
90function randomServer () {
91 const low = 10
92 const high = 10000
93
94 return randomInt(low, high)
95}
96
97async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = []) {
98 const parallel = parallelTests()
99
100 const internalServerNumber = parallel ? randomServer() : serverNumber
101 const port = 9000 + internalServerNumber
102
103 await flushTests(internalServerNumber)
104
105 const server: ServerInfo = {
106 app: null,
107 port,
108 internalServerNumber,
109 parallel,
110 serverNumber,
111 url: `http://localhost:${port}`,
112 host: `localhost:${port}`,
113 client: {
114 id: null,
115 secret: null
116 },
117 user: {
118 username: null,
119 password: null
120 }
121 }
122
123 return runServer(server, configOverride, args)
124}
125
126async function runServer (server: ServerInfo, configOverrideArg?: any, args = []) {
127 // These actions are async so we need to be sure that they have both been done
128 const serverRunString = {
129 'Server listening': false
130 }
131 const key = 'Database peertube_test' + server.internalServerNumber + ' is ready'
132 serverRunString[key] = false
133
134 const regexps = {
135 client_id: 'Client id: (.+)',
136 client_secret: 'Client secret: (.+)',
137 user_username: 'Username: (.+)',
138 user_password: 'User password: (.+)'
139 }
140
141 if (server.internalServerNumber !== server.serverNumber) {
142 const basePath = join(root(), 'config')
143
144 const tmpConfigFile = join(basePath, `test-${server.internalServerNumber}.yaml`)
145 await copy(join(basePath, `test-${server.serverNumber}.yaml`), tmpConfigFile)
146
147 server.customConfigFile = tmpConfigFile
148 }
149
150 const configOverride: any = {}
151
152 if (server.parallel) {
153 Object.assign(configOverride, {
154 listen: {
155 port: server.port
156 },
157 webserver: {
158 port: server.port
159 },
160 database: {
161 suffix: '_test' + server.internalServerNumber
162 },
163 storage: {
164 tmp: `test${server.internalServerNumber}/tmp/`,
165 avatars: `test${server.internalServerNumber}/avatars/`,
166 videos: `test${server.internalServerNumber}/videos/`,
167 streaming_playlists: `test${server.internalServerNumber}/streaming-playlists/`,
168 redundancy: `test${server.internalServerNumber}/redundancy/`,
169 logs: `test${server.internalServerNumber}/logs/`,
170 previews: `test${server.internalServerNumber}/previews/`,
171 thumbnails: `test${server.internalServerNumber}/thumbnails/`,
172 torrents: `test${server.internalServerNumber}/torrents/`,
173 captions: `test${server.internalServerNumber}/captions/`,
174 cache: `test${server.internalServerNumber}/cache/`
175 },
176 admin: {
177 email: `admin${server.internalServerNumber}@example.com`
178 }
179 })
180 }
181
182 if (configOverrideArg !== undefined) {
183 Object.assign(configOverride, configOverrideArg)
184 }
185
186 // Share the environment
187 const env = Object.create(process.env)
188 env['NODE_ENV'] = 'test'
189 env['NODE_APP_INSTANCE'] = server.internalServerNumber.toString()
190 env['NODE_CONFIG'] = JSON.stringify(configOverride)
191
192 const options = {
193 silent: true,
194 env,
195 detached: true
196 }
197
198 return new Promise<ServerInfo>(res => {
199 server.app = fork(join(root(), 'dist', 'server.js'), args, options)
200 server.app.stdout.on('data', function onStdout (data) {
201 let dontContinue = false
202
203 // Capture things if we want to
204 for (const key of Object.keys(regexps)) {
205 const regexp = regexps[ key ]
206 const matches = data.toString().match(regexp)
207 if (matches !== null) {
208 if (key === 'client_id') server.client.id = matches[ 1 ]
209 else if (key === 'client_secret') server.client.secret = matches[ 1 ]
210 else if (key === 'user_username') server.user.username = matches[ 1 ]
211 else if (key === 'user_password') server.user.password = matches[ 1 ]
212 }
213 }
214
215 // Check if all required sentences are here
216 for (const key of Object.keys(serverRunString)) {
217 if (data.toString().indexOf(key) !== -1) serverRunString[ key ] = true
218 if (serverRunString[ key ] === false) dontContinue = true
219 }
220
221 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
222 if (dontContinue === true) return
223
224 server.app.stdout.removeListener('data', onStdout)
225
226 process.on('exit', () => {
227 try {
228 process.kill(server.app.pid)
229 } catch { /* empty */ }
230 })
231
232 res(server)
233 })
234 })
235}
236
237async function reRunServer (server: ServerInfo, configOverride?: any) {
238 const newServer = await runServer(server, configOverride)
239 server.app = newServer.app
240
241 return server
242}
243
244async function checkTmpIsEmpty (server: ServerInfo) {
245 return checkDirectoryIsEmpty(server, 'tmp')
246}
247
248async function checkDirectoryIsEmpty (server: ServerInfo, directory: string) {
249 const testDirectory = 'test' + server.serverNumber
250
251 const directoryPath = join(root(), testDirectory, directory)
252
253 const directoryExists = existsSync(directoryPath)
254 expect(directoryExists).to.be.true
255
256 const files = await readdir(directoryPath)
257 expect(files).to.have.lengthOf(0)
258}
259
260function killallServers (servers: ServerInfo[]) {
261 for (const server of servers) {
262 if (!server.app) continue
263
264 process.kill(-server.app.pid)
265 server.app = null
266 }
267}
268
269function cleanupTests (servers: ServerInfo[]) {
270 killallServers(servers)
271
272 const p: Promise<any>[] = []
273 for (const server of servers) {
274 if (server.parallel) {
275 p.push(flushTests(server.internalServerNumber))
276 }
277
278 if (server.customConfigFile) {
279 p.push(remove(server.customConfigFile))
280 }
281 }
282
283 return Promise.all(p)
284}
285
286async function waitUntilLog (server: ServerInfo, str: string, count = 1) {
287 const logfile = join(root(), 'test' + server.serverNumber, 'logs/peertube.log')
288
289 while (true) {
290 const buf = await readFile(logfile)
291
292 const matches = buf.toString().match(new RegExp(str, 'g'))
293 if (matches && matches.length === count) return
294
295 await wait(1000)
296 }
297}
298
299// ---------------------------------------------------------------------------
300
301export {
302 checkDirectoryIsEmpty,
303 checkTmpIsEmpty,
304 ServerInfo,
305 parallelTests,
306 cleanupTests,
307 flushAndRunMultipleServers,
308 flushTests,
309 flushAndRunServer,
310 killallServers,
311 reRunServer,
312 waitUntilLog
313}
diff --git a/shared/extra-utils/server/stats.ts b/shared/extra-utils/server/stats.ts
new file mode 100644
index 000000000..6f079ad18
--- /dev/null
+++ b/shared/extra-utils/server/stats.ts
@@ -0,0 +1,22 @@
1import { makeGetRequest } from '../requests/requests'
2
3function getStats (url: string, useCache = false) {
4 const path = '/api/v1/server/stats'
5
6 const query = {
7 t: useCache ? undefined : new Date().getTime()
8 }
9
10 return makeGetRequest({
11 url,
12 path,
13 query,
14 statusCodeExpected: 200
15 })
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 getStats
22}
diff --git a/shared/extra-utils/socket/socket-io.ts b/shared/extra-utils/socket/socket-io.ts
new file mode 100644
index 000000000..854ab71af
--- /dev/null
+++ b/shared/extra-utils/socket/socket-io.ts
@@ -0,0 +1,13 @@
1import * as io from 'socket.io-client'
2
3function getUserNotificationSocket (serverUrl: string, accessToken: string) {
4 return io(serverUrl + '/user-notifications', {
5 query: { accessToken }
6 })
7}
8
9// ---------------------------------------------------------------------------
10
11export {
12 getUserNotificationSocket
13}
diff --git a/shared/extra-utils/users/accounts.ts b/shared/extra-utils/users/accounts.ts
new file mode 100644
index 000000000..f64a2dbad
--- /dev/null
+++ b/shared/extra-utils/users/accounts.ts
@@ -0,0 +1,80 @@
1/* tslint:disable:no-unused-expression */
2
3import * as request from 'supertest'
4import { expect } from 'chai'
5import { existsSync, readdir } from 'fs-extra'
6import { join } from 'path'
7import { Account } from '../../models/actors'
8import { root } from '../miscs/miscs'
9import { makeGetRequest } from '../requests/requests'
10import { VideoRateType } from '../../models/videos'
11
12function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = 200) {
13 const path = '/api/v1/accounts'
14
15 return makeGetRequest({
16 url,
17 query: { sort },
18 path,
19 statusCodeExpected
20 })
21}
22
23function getAccount (url: string, accountName: string, statusCodeExpected = 200) {
24 const path = '/api/v1/accounts/' + accountName
25
26 return makeGetRequest({
27 url,
28 path,
29 statusCodeExpected
30 })
31}
32
33async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) {
34 const res = await getAccountsList(url)
35 const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain)
36
37 const message = `${nameWithDomain} on ${url}`
38 expect(account.followersCount).to.equal(followersCount, message)
39 expect(account.followingCount).to.equal(followingCount, message)
40}
41
42async function checkActorFilesWereRemoved (actorUUID: string, serverNumber: number) {
43 const testDirectory = 'test' + serverNumber
44
45 for (const directory of [ 'avatars' ]) {
46 const directoryPath = join(root(), testDirectory, directory)
47
48 const directoryExists = existsSync(directoryPath)
49 expect(directoryExists).to.be.true
50
51 const files = await readdir(directoryPath)
52 for (const file of files) {
53 expect(file).to.not.contain(actorUUID)
54 }
55 }
56}
57
58function getAccountRatings (url: string, accountName: string, accessToken: string, rating?: VideoRateType, statusCodeExpected = 200) {
59 const path = '/api/v1/accounts/' + accountName + '/ratings'
60
61 const query = rating ? { rating } : {}
62
63 return request(url)
64 .get(path)
65 .query(query)
66 .set('Accept', 'application/json')
67 .set('Authorization', 'Bearer ' + accessToken)
68 .expect(statusCodeExpected)
69 .expect('Content-Type', /json/)
70}
71
72// ---------------------------------------------------------------------------
73
74export {
75 getAccount,
76 expectAccountFollows,
77 getAccountsList,
78 checkActorFilesWereRemoved,
79 getAccountRatings
80}
diff --git a/shared/extra-utils/users/blocklist.ts b/shared/extra-utils/users/blocklist.ts
new file mode 100644
index 000000000..5feb84179
--- /dev/null
+++ b/shared/extra-utils/users/blocklist.ts
@@ -0,0 +1,197 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests'
4
5function getAccountBlocklistByAccount (
6 url: string,
7 token: string,
8 start: number,
9 count: number,
10 sort = '-createdAt',
11 statusCodeExpected = 200
12) {
13 const path = '/api/v1/users/me/blocklist/accounts'
14
15 return makeGetRequest({
16 url,
17 token,
18 query: { start, count, sort },
19 path,
20 statusCodeExpected
21 })
22}
23
24function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
25 const path = '/api/v1/users/me/blocklist/accounts'
26
27 return makePostBodyRequest({
28 url,
29 path,
30 token,
31 fields: {
32 accountName: accountToBlock
33 },
34 statusCodeExpected
35 })
36}
37
38function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
39 const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
40
41 return makeDeleteRequest({
42 url,
43 path,
44 token,
45 statusCodeExpected
46 })
47}
48
49function getServerBlocklistByAccount (
50 url: string,
51 token: string,
52 start: number,
53 count: number,
54 sort = '-createdAt',
55 statusCodeExpected = 200
56) {
57 const path = '/api/v1/users/me/blocklist/servers'
58
59 return makeGetRequest({
60 url,
61 token,
62 query: { start, count, sort },
63 path,
64 statusCodeExpected
65 })
66}
67
68function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
69 const path = '/api/v1/users/me/blocklist/servers'
70
71 return makePostBodyRequest({
72 url,
73 path,
74 token,
75 fields: {
76 host: serverToBlock
77 },
78 statusCodeExpected
79 })
80}
81
82function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
83 const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
84
85 return makeDeleteRequest({
86 url,
87 path,
88 token,
89 statusCodeExpected
90 })
91}
92
93function getAccountBlocklistByServer (
94 url: string,
95 token: string,
96 start: number,
97 count: number,
98 sort = '-createdAt',
99 statusCodeExpected = 200
100) {
101 const path = '/api/v1/server/blocklist/accounts'
102
103 return makeGetRequest({
104 url,
105 token,
106 query: { start, count, sort },
107 path,
108 statusCodeExpected
109 })
110}
111
112function addAccountToServerBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
113 const path = '/api/v1/server/blocklist/accounts'
114
115 return makePostBodyRequest({
116 url,
117 path,
118 token,
119 fields: {
120 accountName: accountToBlock
121 },
122 statusCodeExpected
123 })
124}
125
126function removeAccountFromServerBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
127 const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock
128
129 return makeDeleteRequest({
130 url,
131 path,
132 token,
133 statusCodeExpected
134 })
135}
136
137function getServerBlocklistByServer (
138 url: string,
139 token: string,
140 start: number,
141 count: number,
142 sort = '-createdAt',
143 statusCodeExpected = 200
144) {
145 const path = '/api/v1/server/blocklist/servers'
146
147 return makeGetRequest({
148 url,
149 token,
150 query: { start, count, sort },
151 path,
152 statusCodeExpected
153 })
154}
155
156function addServerToServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
157 const path = '/api/v1/server/blocklist/servers'
158
159 return makePostBodyRequest({
160 url,
161 path,
162 token,
163 fields: {
164 host: serverToBlock
165 },
166 statusCodeExpected
167 })
168}
169
170function removeServerFromServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
171 const path = '/api/v1/server/blocklist/servers/' + serverToBlock
172
173 return makeDeleteRequest({
174 url,
175 path,
176 token,
177 statusCodeExpected
178 })
179}
180
181// ---------------------------------------------------------------------------
182
183export {
184 getAccountBlocklistByAccount,
185 addAccountToAccountBlocklist,
186 removeAccountFromAccountBlocklist,
187 getServerBlocklistByAccount,
188 addServerToAccountBlocklist,
189 removeServerFromAccountBlocklist,
190
191 getAccountBlocklistByServer,
192 addAccountToServerBlocklist,
193 removeAccountFromServerBlocklist,
194 getServerBlocklistByServer,
195 addServerToServerBlocklist,
196 removeServerFromServerBlocklist
197}
diff --git a/shared/extra-utils/users/login.ts b/shared/extra-utils/users/login.ts
new file mode 100644
index 000000000..ddeb9df2a
--- /dev/null
+++ b/shared/extra-utils/users/login.ts
@@ -0,0 +1,62 @@
1import * as request from 'supertest'
2
3import { ServerInfo } from '../server/servers'
4
5type Client = { id: string, secret: string }
6type User = { username: string, password: string }
7type Server = { url: string, client: Client, user: User }
8
9function login (url: string, client: Client, user: User, expectedStatus = 200) {
10 const path = '/api/v1/users/token'
11
12 const body = {
13 client_id: client.id,
14 client_secret: client.secret,
15 username: user.username,
16 password: user.password,
17 response_type: 'code',
18 grant_type: 'password',
19 scope: 'upload'
20 }
21
22 return request(url)
23 .post(path)
24 .type('form')
25 .send(body)
26 .expect(expectedStatus)
27}
28
29async function serverLogin (server: Server) {
30 const res = await login(server.url, server.client, server.user, 200)
31
32 return res.body.access_token as string
33}
34
35async function userLogin (server: Server, user: User, expectedStatus = 200) {
36 const res = await login(server.url, server.client, user, expectedStatus)
37
38 return res.body.access_token as string
39}
40
41function setAccessTokensToServers (servers: ServerInfo[]) {
42 const tasks: Promise<any>[] = []
43
44 for (const server of servers) {
45 const p = serverLogin(server).then(t => server.accessToken = t)
46 tasks.push(p)
47 }
48
49 return Promise.all(tasks)
50}
51
52// ---------------------------------------------------------------------------
53
54export {
55 login,
56 serverLogin,
57 userLogin,
58 setAccessTokensToServers,
59 Server,
60 Client,
61 User
62}
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts
new file mode 100644
index 000000000..495ff80d9
--- /dev/null
+++ b/shared/extra-utils/users/user-notifications.ts
@@ -0,0 +1,496 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
4import { UserNotification, UserNotificationSetting, UserNotificationType } from '../../models/users'
5import { ServerInfo } from '..'
6import { expect } from 'chai'
7import { inspect } from 'util'
8
9function updateMyNotificationSettings (url: string, token: string, settings: UserNotificationSetting, statusCodeExpected = 204) {
10 const path = '/api/v1/users/me/notification-settings'
11
12 return makePutBodyRequest({
13 url,
14 path,
15 token,
16 fields: settings,
17 statusCodeExpected
18 })
19}
20
21async function getUserNotifications (
22 url: string,
23 token: string,
24 start: number,
25 count: number,
26 unread?: boolean,
27 sort = '-createdAt',
28 statusCodeExpected = 200
29) {
30 const path = '/api/v1/users/me/notifications'
31
32 return makeGetRequest({
33 url,
34 path,
35 token,
36 query: {
37 start,
38 count,
39 sort,
40 unread
41 },
42 statusCodeExpected
43 })
44}
45
46function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = 204) {
47 const path = '/api/v1/users/me/notifications/read'
48
49 return makePostBodyRequest({
50 url,
51 path,
52 token,
53 fields: { ids },
54 statusCodeExpected
55 })
56}
57function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = 204) {
58 const path = '/api/v1/users/me/notifications/read-all'
59
60 return makePostBodyRequest({
61 url,
62 path,
63 token,
64 statusCodeExpected
65 })
66}
67
68async function getLastNotification (serverUrl: string, accessToken: string) {
69 const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
70
71 if (res.body.total === 0) return undefined
72
73 return res.body.data[0] as UserNotification
74}
75
76type CheckerBaseParams = {
77 server: ServerInfo
78 emails: object[]
79 socketNotifications: UserNotification[]
80 token: string,
81 check?: { web: boolean, mail: boolean }
82}
83
84type CheckerType = 'presence' | 'absence'
85
86async function checkNotification (
87 base: CheckerBaseParams,
88 notificationChecker: (notification: UserNotification, type: CheckerType) => void,
89 emailNotificationFinder: (email: object) => boolean,
90 checkType: CheckerType
91) {
92 const check = base.check || { web: true, mail: true }
93
94 if (check.web) {
95 const notification = await getLastNotification(base.server.url, base.token)
96
97 if (notification || checkType !== 'absence') {
98 notificationChecker(notification, checkType)
99 }
100
101 const socketNotification = base.socketNotifications.find(n => {
102 try {
103 notificationChecker(n, 'presence')
104 return true
105 } catch {
106 return false
107 }
108 })
109
110 if (checkType === 'presence') {
111 const obj = inspect(base.socketNotifications, { depth: 5 })
112 expect(socketNotification, 'The socket notification is absent. ' + obj).to.not.be.undefined
113 } else {
114 const obj = inspect(socketNotification, { depth: 5 })
115 expect(socketNotification, 'The socket notification is present. ' + obj).to.be.undefined
116 }
117 }
118
119 if (check.mail) {
120 // Last email
121 const email = base.emails
122 .slice()
123 .reverse()
124 .find(e => emailNotificationFinder(e))
125
126 if (checkType === 'presence') {
127 expect(email, 'The email is absent. ' + inspect(base.emails)).to.not.be.undefined
128 } else {
129 expect(email, 'The email is present. ' + inspect(email)).to.be.undefined
130 }
131 }
132}
133
134function checkVideo (video: any, videoName?: string, videoUUID?: string) {
135 expect(video.name).to.be.a('string')
136 expect(video.name).to.not.be.empty
137 if (videoName) expect(video.name).to.equal(videoName)
138
139 expect(video.uuid).to.be.a('string')
140 expect(video.uuid).to.not.be.empty
141 if (videoUUID) expect(video.uuid).to.equal(videoUUID)
142
143 expect(video.id).to.be.a('number')
144}
145
146function checkActor (actor: any) {
147 expect(actor.displayName).to.be.a('string')
148 expect(actor.displayName).to.not.be.empty
149 expect(actor.host).to.not.be.undefined
150}
151
152function checkComment (comment: any, commentId: number, threadId: number) {
153 expect(comment.id).to.equal(commentId)
154 expect(comment.threadId).to.equal(threadId)
155}
156
157async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
158 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
159
160 function notificationChecker (notification: UserNotification, type: CheckerType) {
161 if (type === 'presence') {
162 expect(notification).to.not.be.undefined
163 expect(notification.type).to.equal(notificationType)
164
165 checkVideo(notification.video, videoName, videoUUID)
166 checkActor(notification.video.channel)
167 } else {
168 expect(notification).to.satisfy((n: UserNotification) => {
169 return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
170 })
171 }
172 }
173
174 function emailFinder (email: object) {
175 const text = email[ 'text' ]
176 return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1
177 }
178
179 await checkNotification(base, notificationChecker, emailFinder, type)
180}
181
182async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
183 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
184
185 function notificationChecker (notification: UserNotification, type: CheckerType) {
186 if (type === 'presence') {
187 expect(notification).to.not.be.undefined
188 expect(notification.type).to.equal(notificationType)
189
190 checkVideo(notification.video, videoName, videoUUID)
191 checkActor(notification.video.channel)
192 } else {
193 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
194 }
195 }
196
197 function emailFinder (email: object) {
198 const text: string = email[ 'text' ]
199 return text.includes(videoUUID) && text.includes('Your video')
200 }
201
202 await checkNotification(base, notificationChecker, emailFinder, type)
203}
204
205async function checkMyVideoImportIsFinished (
206 base: CheckerBaseParams,
207 videoName: string,
208 videoUUID: string,
209 url: string,
210 success: boolean,
211 type: CheckerType
212) {
213 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
214
215 function notificationChecker (notification: UserNotification, type: CheckerType) {
216 if (type === 'presence') {
217 expect(notification).to.not.be.undefined
218 expect(notification.type).to.equal(notificationType)
219
220 expect(notification.videoImport.targetUrl).to.equal(url)
221
222 if (success) checkVideo(notification.videoImport.video, videoName, videoUUID)
223 } else {
224 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
225 }
226 }
227
228 function emailFinder (email: object) {
229 const text: string = email[ 'text' ]
230 const toFind = success ? ' finished' : ' error'
231
232 return text.includes(url) && text.includes(toFind)
233 }
234
235 await checkNotification(base, notificationChecker, emailFinder, type)
236}
237
238async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) {
239 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
240
241 function notificationChecker (notification: UserNotification, type: CheckerType) {
242 if (type === 'presence') {
243 expect(notification).to.not.be.undefined
244 expect(notification.type).to.equal(notificationType)
245
246 checkActor(notification.account)
247 expect(notification.account.name).to.equal(username)
248 } else {
249 expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
250 }
251 }
252
253 function emailFinder (email: object) {
254 const text: string = email[ 'text' ]
255
256 return text.includes(' registered ') && text.includes(username)
257 }
258
259 await checkNotification(base, notificationChecker, emailFinder, type)
260}
261
262async function checkNewActorFollow (
263 base: CheckerBaseParams,
264 followType: 'channel' | 'account',
265 followerName: string,
266 followerDisplayName: string,
267 followingDisplayName: string,
268 type: CheckerType
269) {
270 const notificationType = UserNotificationType.NEW_FOLLOW
271
272 function notificationChecker (notification: UserNotification, type: CheckerType) {
273 if (type === 'presence') {
274 expect(notification).to.not.be.undefined
275 expect(notification.type).to.equal(notificationType)
276
277 checkActor(notification.actorFollow.follower)
278 expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
279 expect(notification.actorFollow.follower.name).to.equal(followerName)
280 expect(notification.actorFollow.follower.host).to.not.be.undefined
281
282 expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName)
283 expect(notification.actorFollow.following.type).to.equal(followType)
284 } else {
285 expect(notification).to.satisfy(n => {
286 return n.type !== notificationType ||
287 (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
288 })
289 }
290 }
291
292 function emailFinder (email: object) {
293 const text: string = email[ 'text' ]
294
295 return text.includes('Your ' + followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
296 }
297
298 await checkNotification(base, notificationChecker, emailFinder, type)
299}
300
301async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost: string, type: CheckerType) {
302 const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
303
304 function notificationChecker (notification: UserNotification, type: CheckerType) {
305 if (type === 'presence') {
306 expect(notification).to.not.be.undefined
307 expect(notification.type).to.equal(notificationType)
308
309 checkActor(notification.actorFollow.follower)
310 expect(notification.actorFollow.follower.name).to.equal('peertube')
311 expect(notification.actorFollow.follower.host).to.equal(followerHost)
312
313 expect(notification.actorFollow.following.name).to.equal('peertube')
314 } else {
315 expect(notification).to.satisfy(n => {
316 return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
317 })
318 }
319 }
320
321 function emailFinder (email: object) {
322 const text: string = email[ 'text' ]
323
324 return text.includes('instance has a new follower') && text.includes(followerHost)
325 }
326
327 await checkNotification(base, notificationChecker, emailFinder, type)
328}
329
330async function checkCommentMention (
331 base: CheckerBaseParams,
332 uuid: string,
333 commentId: number,
334 threadId: number,
335 byAccountDisplayName: string,
336 type: CheckerType
337) {
338 const notificationType = UserNotificationType.COMMENT_MENTION
339
340 function notificationChecker (notification: UserNotification, type: CheckerType) {
341 if (type === 'presence') {
342 expect(notification).to.not.be.undefined
343 expect(notification.type).to.equal(notificationType)
344
345 checkComment(notification.comment, commentId, threadId)
346 checkActor(notification.comment.account)
347 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
348
349 checkVideo(notification.comment.video, undefined, uuid)
350 } else {
351 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
352 }
353 }
354
355 function emailFinder (email: object) {
356 const text: string = email[ 'text' ]
357
358 return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName)
359 }
360
361 await checkNotification(base, notificationChecker, emailFinder, type)
362}
363
364let lastEmailCount = 0
365async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
366 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
367
368 function notificationChecker (notification: UserNotification, type: CheckerType) {
369 if (type === 'presence') {
370 expect(notification).to.not.be.undefined
371 expect(notification.type).to.equal(notificationType)
372
373 checkComment(notification.comment, commentId, threadId)
374 checkActor(notification.comment.account)
375 checkVideo(notification.comment.video, undefined, uuid)
376 } else {
377 expect(notification).to.satisfy((n: UserNotification) => {
378 return n === undefined || n.comment === undefined || n.comment.id !== commentId
379 })
380 }
381 }
382
383 const commentUrl = `http://localhost:9001/videos/watch/${uuid};threadId=${threadId}`
384 function emailFinder (email: object) {
385 return email[ 'text' ].indexOf(commentUrl) !== -1
386 }
387
388 await checkNotification(base, notificationChecker, emailFinder, type)
389
390 if (type === 'presence') {
391 // We cannot detect email duplicates, so check we received another email
392 expect(base.emails).to.have.length.above(lastEmailCount)
393 lastEmailCount = base.emails.length
394 }
395}
396
397async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
398 const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS
399
400 function notificationChecker (notification: UserNotification, type: CheckerType) {
401 if (type === 'presence') {
402 expect(notification).to.not.be.undefined
403 expect(notification.type).to.equal(notificationType)
404
405 expect(notification.videoAbuse.id).to.be.a('number')
406 checkVideo(notification.videoAbuse.video, videoName, videoUUID)
407 } else {
408 expect(notification).to.satisfy((n: UserNotification) => {
409 return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID
410 })
411 }
412 }
413
414 function emailFinder (email: object) {
415 const text = email[ 'text' ]
416 return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
417 }
418
419 await checkNotification(base, notificationChecker, emailFinder, type)
420}
421
422async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
423 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
424
425 function notificationChecker (notification: UserNotification, type: CheckerType) {
426 if (type === 'presence') {
427 expect(notification).to.not.be.undefined
428 expect(notification.type).to.equal(notificationType)
429
430 expect(notification.video.id).to.be.a('number')
431 checkVideo(notification.video, videoName, videoUUID)
432 } else {
433 expect(notification).to.satisfy((n: UserNotification) => {
434 return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
435 })
436 }
437 }
438
439 function emailFinder (email: object) {
440 const text = email[ 'text' ]
441 return text.indexOf(videoUUID) !== -1 && email[ 'text' ].indexOf('video-auto-blacklist/list') !== -1
442 }
443
444 await checkNotification(base, notificationChecker, emailFinder, type)
445}
446
447async function checkNewBlacklistOnMyVideo (
448 base: CheckerBaseParams,
449 videoUUID: string,
450 videoName: string,
451 blacklistType: 'blacklist' | 'unblacklist'
452) {
453 const notificationType = blacklistType === 'blacklist'
454 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
455 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
456
457 function notificationChecker (notification: UserNotification) {
458 expect(notification).to.not.be.undefined
459 expect(notification.type).to.equal(notificationType)
460
461 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
462
463 checkVideo(video, videoName, videoUUID)
464 }
465
466 function emailFinder (email: object) {
467 const text = email[ 'text' ]
468 return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1
469 }
470
471 await checkNotification(base, notificationChecker, emailFinder, 'presence')
472}
473
474// ---------------------------------------------------------------------------
475
476export {
477 CheckerBaseParams,
478 CheckerType,
479 checkNotification,
480 markAsReadAllNotifications,
481 checkMyVideoImportIsFinished,
482 checkUserRegistered,
483 checkVideoIsPublished,
484 checkNewVideoFromSubscription,
485 checkNewActorFollow,
486 checkNewCommentOnMyVideo,
487 checkNewBlacklistOnMyVideo,
488 checkCommentMention,
489 updateMyNotificationSettings,
490 checkNewVideoAbuseForModerators,
491 checkVideoAutoBlacklistForModerators,
492 getUserNotifications,
493 markAsReadNotifications,
494 getLastNotification,
495 checkNewInstanceFollower
496}
diff --git a/shared/extra-utils/users/user-subscriptions.ts b/shared/extra-utils/users/user-subscriptions.ts
new file mode 100644
index 000000000..7148fbfca
--- /dev/null
+++ b/shared/extra-utils/users/user-subscriptions.ts
@@ -0,0 +1,82 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests'
2
3function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = 204) {
4 const path = '/api/v1/users/me/subscriptions'
5
6 return makePostBodyRequest({
7 url,
8 path,
9 token,
10 statusCodeExpected,
11 fields: { uri: targetUri }
12 })
13}
14
15function listUserSubscriptions (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
16 const path = '/api/v1/users/me/subscriptions'
17
18 return makeGetRequest({
19 url,
20 path,
21 token,
22 statusCodeExpected,
23 query: { sort }
24 })
25}
26
27function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
28 const path = '/api/v1/users/me/subscriptions/videos'
29
30 return makeGetRequest({
31 url,
32 path,
33 token,
34 statusCodeExpected,
35 query: { sort }
36 })
37}
38
39function getUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 200) {
40 const path = '/api/v1/users/me/subscriptions/' + uri
41
42 return makeGetRequest({
43 url,
44 path,
45 token,
46 statusCodeExpected
47 })
48}
49
50function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 204) {
51 const path = '/api/v1/users/me/subscriptions/' + uri
52
53 return makeDeleteRequest({
54 url,
55 path,
56 token,
57 statusCodeExpected
58 })
59}
60
61function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = 200) {
62 const path = '/api/v1/users/me/subscriptions/exist'
63
64 return makeGetRequest({
65 url,
66 path,
67 query: { 'uris[]': uris },
68 token,
69 statusCodeExpected
70 })
71}
72
73// ---------------------------------------------------------------------------
74
75export {
76 areSubscriptionsExist,
77 addUserSubscription,
78 listUserSubscriptions,
79 getUserSubscription,
80 listUserSubscriptionVideos,
81 removeUserSubscription
82}
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
new file mode 100644
index 000000000..2bd37b8be
--- /dev/null
+++ b/shared/extra-utils/users/users.ts
@@ -0,0 +1,330 @@
1import * as request from 'supertest'
2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
3
4import { UserRole } from '../../index'
5import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
6import { ServerInfo, userLogin } from '..'
7import { UserAdminFlag } from '../../models/users/user-flag.model'
8
9type CreateUserArgs = { url: string,
10 accessToken: string,
11 username: string,
12 password: string,
13 videoQuota?: number,
14 videoQuotaDaily?: number,
15 role?: UserRole,
16 adminFlags?: UserAdminFlag,
17 specialStatus?: number
18}
19function createUser (parameters: CreateUserArgs) {
20 const {
21 url,
22 accessToken,
23 username,
24 adminFlags,
25 password = 'password',
26 videoQuota = 1000000,
27 videoQuotaDaily = -1,
28 role = UserRole.USER,
29 specialStatus = 200
30 } = parameters
31
32 const path = '/api/v1/users'
33 const body = {
34 username,
35 password,
36 role,
37 adminFlags,
38 email: username + '@example.com',
39 videoQuota,
40 videoQuotaDaily
41 }
42
43 return request(url)
44 .post(path)
45 .set('Accept', 'application/json')
46 .set('Authorization', 'Bearer ' + accessToken)
47 .send(body)
48 .expect(specialStatus)
49}
50
51async function generateUserAccessToken (server: ServerInfo, username: string) {
52 const password = 'my super password'
53 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
54
55 return userLogin(server, { username, password })
56}
57
58function registerUser (url: string, username: string, password: string, specialStatus = 204) {
59 const path = '/api/v1/users/register'
60 const body = {
61 username,
62 password,
63 email: username + '@example.com'
64 }
65
66 return request(url)
67 .post(path)
68 .set('Accept', 'application/json')
69 .send(body)
70 .expect(specialStatus)
71}
72
73function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) {
74 const path = '/api/v1/users/me'
75
76 return request(url)
77 .get(path)
78 .set('Accept', 'application/json')
79 .set('Authorization', 'Bearer ' + accessToken)
80 .expect(specialStatus)
81 .expect('Content-Type', /json/)
82}
83
84function deleteMe (url: string, accessToken: string, specialStatus = 204) {
85 const path = '/api/v1/users/me'
86
87 return request(url)
88 .delete(path)
89 .set('Accept', 'application/json')
90 .set('Authorization', 'Bearer ' + accessToken)
91 .expect(specialStatus)
92}
93
94function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) {
95 const path = '/api/v1/users/me/video-quota-used'
96
97 return request(url)
98 .get(path)
99 .set('Accept', 'application/json')
100 .set('Authorization', 'Bearer ' + accessToken)
101 .expect(specialStatus)
102 .expect('Content-Type', /json/)
103}
104
105function getUserInformation (url: string, accessToken: string, userId: number) {
106 const path = '/api/v1/users/' + userId
107
108 return request(url)
109 .get(path)
110 .set('Accept', 'application/json')
111 .set('Authorization', 'Bearer ' + accessToken)
112 .expect(200)
113 .expect('Content-Type', /json/)
114}
115
116function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = 200) {
117 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
118
119 return request(url)
120 .get(path)
121 .set('Accept', 'application/json')
122 .set('Authorization', 'Bearer ' + accessToken)
123 .expect(specialStatus)
124 .expect('Content-Type', /json/)
125}
126
127function getUsersList (url: string, accessToken: string) {
128 const path = '/api/v1/users'
129
130 return request(url)
131 .get(path)
132 .set('Accept', 'application/json')
133 .set('Authorization', 'Bearer ' + accessToken)
134 .expect(200)
135 .expect('Content-Type', /json/)
136}
137
138function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) {
139 const path = '/api/v1/users'
140
141 return request(url)
142 .get(path)
143 .query({ start })
144 .query({ count })
145 .query({ sort })
146 .query({ search })
147 .set('Accept', 'application/json')
148 .set('Authorization', 'Bearer ' + accessToken)
149 .expect(200)
150 .expect('Content-Type', /json/)
151}
152
153function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
154 const path = '/api/v1/users'
155
156 return request(url)
157 .delete(path + '/' + userId)
158 .set('Accept', 'application/json')
159 .set('Authorization', 'Bearer ' + accessToken)
160 .expect(expectedStatus)
161}
162
163function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) {
164 const path = '/api/v1/users'
165 let body: any
166 if (reason) body = { reason }
167
168 return request(url)
169 .post(path + '/' + userId + '/block')
170 .send(body)
171 .set('Accept', 'application/json')
172 .set('Authorization', 'Bearer ' + accessToken)
173 .expect(expectedStatus)
174}
175
176function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
177 const path = '/api/v1/users'
178
179 return request(url)
180 .post(path + '/' + userId + '/unblock')
181 .set('Accept', 'application/json')
182 .set('Authorization', 'Bearer ' + accessToken)
183 .expect(expectedStatus)
184}
185
186function updateMyUser (options: {
187 url: string
188 accessToken: string
189 currentPassword?: string
190 newPassword?: string
191 nsfwPolicy?: NSFWPolicyType
192 email?: string
193 autoPlayVideo?: boolean
194 displayName?: string
195 description?: string
196 videosHistoryEnabled?: boolean
197}) {
198 const path = '/api/v1/users/me'
199
200 const toSend = {}
201 if (options.currentPassword !== undefined && options.currentPassword !== null) toSend['currentPassword'] = options.currentPassword
202 if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword
203 if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy
204 if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo
205 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
206 if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
207 if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
208 if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
209 toSend['videosHistoryEnabled'] = options.videosHistoryEnabled
210 }
211
212 return makePutBodyRequest({
213 url: options.url,
214 path,
215 token: options.accessToken,
216 fields: toSend,
217 statusCodeExpected: 204
218 })
219}
220
221function updateMyAvatar (options: {
222 url: string,
223 accessToken: string,
224 fixture: string
225}) {
226 const path = '/api/v1/users/me/avatar/pick'
227
228 return updateAvatarRequest(Object.assign(options, { path }))
229}
230
231function updateUser (options: {
232 url: string
233 userId: number,
234 accessToken: string,
235 email?: string,
236 emailVerified?: boolean,
237 videoQuota?: number,
238 videoQuotaDaily?: number,
239 password?: string,
240 adminFlags?: UserAdminFlag,
241 role?: UserRole
242}) {
243 const path = '/api/v1/users/' + options.userId
244
245 const toSend = {}
246 if (options.password !== undefined && options.password !== null) toSend['password'] = options.password
247 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
248 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
249 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
250 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
251 if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
252 if (options.adminFlags !== undefined && options.adminFlags !== null) toSend['adminFlags'] = options.adminFlags
253
254 return makePutBodyRequest({
255 url: options.url,
256 path,
257 token: options.accessToken,
258 fields: toSend,
259 statusCodeExpected: 204
260 })
261}
262
263function askResetPassword (url: string, email: string) {
264 const path = '/api/v1/users/ask-reset-password'
265
266 return makePostBodyRequest({
267 url,
268 path,
269 fields: { email },
270 statusCodeExpected: 204
271 })
272}
273
274function resetPassword (url: string, userId: number, verificationString: string, password: string, statusCodeExpected = 204) {
275 const path = '/api/v1/users/' + userId + '/reset-password'
276
277 return makePostBodyRequest({
278 url,
279 path,
280 fields: { password, verificationString },
281 statusCodeExpected
282 })
283}
284
285function askSendVerifyEmail (url: string, email: string) {
286 const path = '/api/v1/users/ask-send-verify-email'
287
288 return makePostBodyRequest({
289 url,
290 path,
291 fields: { email },
292 statusCodeExpected: 204
293 })
294}
295
296function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) {
297 const path = '/api/v1/users/' + userId + '/verify-email'
298
299 return makePostBodyRequest({
300 url,
301 path,
302 fields: { verificationString },
303 statusCodeExpected
304 })
305}
306
307// ---------------------------------------------------------------------------
308
309export {
310 createUser,
311 registerUser,
312 getMyUserInformation,
313 getMyUserVideoRating,
314 deleteMe,
315 getMyUserVideoQuotaUsed,
316 getUsersList,
317 getUsersListPaginationAndSort,
318 removeUser,
319 updateUser,
320 updateMyUser,
321 getUserInformation,
322 blockUser,
323 unblockUser,
324 askResetPassword,
325 resetPassword,
326 updateMyAvatar,
327 askSendVerifyEmail,
328 generateUserAccessToken,
329 verifyEmail
330}
diff --git a/shared/extra-utils/videos/services.ts b/shared/extra-utils/videos/services.ts
new file mode 100644
index 000000000..1a53dd4cf
--- /dev/null
+++ b/shared/extra-utils/videos/services.ts
@@ -0,0 +1,23 @@
1import * as request from 'supertest'
2
3function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
4 const path = '/services/oembed'
5 const query = {
6 url: oembedUrl,
7 format,
8 maxheight: maxHeight,
9 maxwidth: maxWidth
10 }
11
12 return request(url)
13 .get(path)
14 .query(query)
15 .set('Accept', 'application/json')
16 .expect(200)
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 getOEmbed
23}
diff --git a/shared/extra-utils/videos/video-abuses.ts b/shared/extra-utils/videos/video-abuses.ts
new file mode 100644
index 000000000..7f011ec0f
--- /dev/null
+++ b/shared/extra-utils/videos/video-abuses.ts
@@ -0,0 +1,65 @@
1import * as request from 'supertest'
2import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
3import { makeDeleteRequest, makePutBodyRequest } from '../requests/requests'
4
5function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) {
6 const path = '/api/v1/videos/' + videoId + '/abuse'
7
8 return request(url)
9 .post(path)
10 .set('Accept', 'application/json')
11 .set('Authorization', 'Bearer ' + token)
12 .send({ reason })
13 .expect(specialStatus)
14}
15
16function getVideoAbusesList (url: string, token: string) {
17 const path = '/api/v1/videos/abuse'
18
19 return request(url)
20 .get(path)
21 .query({ sort: 'createdAt' })
22 .set('Accept', 'application/json')
23 .set('Authorization', 'Bearer ' + token)
24 .expect(200)
25 .expect('Content-Type', /json/)
26}
27
28function updateVideoAbuse (
29 url: string,
30 token: string,
31 videoId: string | number,
32 videoAbuseId: number,
33 body: VideoAbuseUpdate,
34 statusCodeExpected = 204
35) {
36 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
37
38 return makePutBodyRequest({
39 url,
40 token,
41 path,
42 fields: body,
43 statusCodeExpected
44 })
45}
46
47function deleteVideoAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
48 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
49
50 return makeDeleteRequest({
51 url,
52 token,
53 path,
54 statusCodeExpected
55 })
56}
57
58// ---------------------------------------------------------------------------
59
60export {
61 reportVideoAbuse,
62 getVideoAbusesList,
63 updateVideoAbuse,
64 deleteVideoAbuse
65}
diff --git a/shared/extra-utils/videos/video-blacklist.ts b/shared/extra-utils/videos/video-blacklist.ts
new file mode 100644
index 000000000..e25a292fc
--- /dev/null
+++ b/shared/extra-utils/videos/video-blacklist.ts
@@ -0,0 +1,72 @@
1import * as request from 'supertest'
2import { VideoBlacklistType } from '../../models/videos'
3import { makeGetRequest } from '..'
4
5function addVideoToBlacklist (
6 url: string,
7 token: string,
8 videoId: number | string,
9 reason?: string,
10 unfederate?: boolean,
11 specialStatus = 204
12) {
13 const path = '/api/v1/videos/' + videoId + '/blacklist'
14
15 return request(url)
16 .post(path)
17 .send({ reason, unfederate })
18 .set('Accept', 'application/json')
19 .set('Authorization', 'Bearer ' + token)
20 .expect(specialStatus)
21}
22
23function updateVideoBlacklist (url: string, token: string, videoId: number, reason?: string, specialStatus = 204) {
24 const path = '/api/v1/videos/' + videoId + '/blacklist'
25
26 return request(url)
27 .put(path)
28 .send({ reason })
29 .set('Accept', 'application/json')
30 .set('Authorization', 'Bearer ' + token)
31 .expect(specialStatus)
32}
33
34function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
35 const path = '/api/v1/videos/' + videoId + '/blacklist'
36
37 return request(url)
38 .delete(path)
39 .set('Accept', 'application/json')
40 .set('Authorization', 'Bearer ' + token)
41 .expect(specialStatus)
42}
43
44function getBlacklistedVideosList (parameters: {
45 url: string,
46 token: string,
47 sort?: string,
48 type?: VideoBlacklistType,
49 specialStatus?: number
50}) {
51 let { url, token, sort, type, specialStatus = 200 } = parameters
52 const path = '/api/v1/videos/blacklist/'
53
54 const query = { sort, type }
55
56 return makeGetRequest({
57 url,
58 path,
59 query,
60 token,
61 statusCodeExpected: specialStatus
62 })
63}
64
65// ---------------------------------------------------------------------------
66
67export {
68 addVideoToBlacklist,
69 removeVideoFromBlacklist,
70 getBlacklistedVideosList,
71 updateVideoBlacklist
72}
diff --git a/shared/extra-utils/videos/video-captions.ts b/shared/extra-utils/videos/video-captions.ts
new file mode 100644
index 000000000..8d67f617b
--- /dev/null
+++ b/shared/extra-utils/videos/video-captions.ts
@@ -0,0 +1,71 @@
1import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
2import * as request from 'supertest'
3import * as chai from 'chai'
4import { buildAbsoluteFixturePath } from '../miscs/miscs'
5
6const expect = chai.expect
7
8function createVideoCaption (args: {
9 url: string,
10 accessToken: string
11 videoId: string | number
12 language: string
13 fixture: string,
14 mimeType?: string,
15 statusCodeExpected?: number
16}) {
17 const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language
18
19 const captionfile = buildAbsoluteFixturePath(args.fixture)
20 const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile
21
22 return makeUploadRequest({
23 method: 'PUT',
24 url: args.url,
25 path,
26 token: args.accessToken,
27 fields: {},
28 attaches: {
29 captionfile: captionfileAttach
30 },
31 statusCodeExpected: args.statusCodeExpected || 204
32 })
33}
34
35function listVideoCaptions (url: string, videoId: string | number) {
36 const path = '/api/v1/videos/' + videoId + '/captions'
37
38 return makeGetRequest({
39 url,
40 path,
41 statusCodeExpected: 200
42 })
43}
44
45function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) {
46 const path = '/api/v1/videos/' + videoId + '/captions/' + language
47
48 return makeDeleteRequest({
49 url,
50 token,
51 path,
52 statusCodeExpected: 204
53 })
54}
55
56async function testCaptionFile (url: string, captionPath: string, containsString: string) {
57 const res = await request(url)
58 .get(captionPath)
59 .expect(200)
60
61 expect(res.text).to.contain(containsString)
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 createVideoCaption,
68 listVideoCaptions,
69 testCaptionFile,
70 deleteVideoCaption
71}
diff --git a/shared/extra-utils/videos/video-change-ownership.ts b/shared/extra-utils/videos/video-change-ownership.ts
new file mode 100644
index 000000000..371d02000
--- /dev/null
+++ b/shared/extra-utils/videos/video-change-ownership.ts
@@ -0,0 +1,54 @@
1import * as request from 'supertest'
2
3function changeVideoOwnership (url: string, token: string, videoId: number | string, username, expectedStatus = 204) {
4 const path = '/api/v1/videos/' + videoId + '/give-ownership'
5
6 return request(url)
7 .post(path)
8 .set('Accept', 'application/json')
9 .set('Authorization', 'Bearer ' + token)
10 .send({ username })
11 .expect(expectedStatus)
12}
13
14function getVideoChangeOwnershipList (url: string, token: string) {
15 const path = '/api/v1/videos/ownership'
16
17 return request(url)
18 .get(path)
19 .query({ sort: '-createdAt' })
20 .set('Accept', 'application/json')
21 .set('Authorization', 'Bearer ' + token)
22 .expect(200)
23 .expect('Content-Type', /json/)
24}
25
26function acceptChangeOwnership (url: string, token: string, ownershipId: string, channelId: number, expectedStatus = 204) {
27 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
28
29 return request(url)
30 .post(path)
31 .set('Accept', 'application/json')
32 .set('Authorization', 'Bearer ' + token)
33 .send({ channelId })
34 .expect(expectedStatus)
35}
36
37function refuseChangeOwnership (url: string, token: string, ownershipId: string, expectedStatus = 204) {
38 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
39
40 return request(url)
41 .post(path)
42 .set('Accept', 'application/json')
43 .set('Authorization', 'Bearer ' + token)
44 .expect(expectedStatus)
45}
46
47// ---------------------------------------------------------------------------
48
49export {
50 changeVideoOwnership,
51 getVideoChangeOwnershipList,
52 acceptChangeOwnership,
53 refuseChangeOwnership
54}
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts
new file mode 100644
index 000000000..93a257bf9
--- /dev/null
+++ b/shared/extra-utils/videos/video-channels.ts
@@ -0,0 +1,134 @@
1import * as request from 'supertest'
2import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos'
3import { updateAvatarRequest } from '../requests/requests'
4import { getMyUserInformation, ServerInfo } from '..'
5import { User } from '../..'
6
7function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
8 const path = '/api/v1/video-channels'
9
10 const req = request(url)
11 .get(path)
12 .query({ start: start })
13 .query({ count: count })
14
15 if (sort) req.query({ sort })
16
17 return req.set('Accept', 'application/json')
18 .expect(200)
19 .expect('Content-Type', /json/)
20}
21
22function getAccountVideoChannelsList (url: string, accountName: string, specialStatus = 200) {
23 const path = '/api/v1/accounts/' + accountName + '/video-channels'
24
25 return request(url)
26 .get(path)
27 .set('Accept', 'application/json')
28 .expect(specialStatus)
29 .expect('Content-Type', /json/)
30}
31
32function addVideoChannel (
33 url: string,
34 token: string,
35 videoChannelAttributesArg: VideoChannelCreate,
36 expectedStatus = 200
37) {
38 const path = '/api/v1/video-channels/'
39
40 // Default attributes
41 let attributes = {
42 displayName: 'my super video channel',
43 description: 'my super channel description',
44 support: 'my super channel support'
45 }
46 attributes = Object.assign(attributes, videoChannelAttributesArg)
47
48 return request(url)
49 .post(path)
50 .send(attributes)
51 .set('Accept', 'application/json')
52 .set('Authorization', 'Bearer ' + token)
53 .expect(expectedStatus)
54}
55
56function updateVideoChannel (
57 url: string,
58 token: string,
59 channelName: string,
60 attributes: VideoChannelUpdate,
61 expectedStatus = 204
62) {
63 const body = {}
64 const path = '/api/v1/video-channels/' + channelName
65
66 if (attributes.displayName) body['displayName'] = attributes.displayName
67 if (attributes.description) body['description'] = attributes.description
68 if (attributes.support) body['support'] = attributes.support
69
70 return request(url)
71 .put(path)
72 .send(body)
73 .set('Accept', 'application/json')
74 .set('Authorization', 'Bearer ' + token)
75 .expect(expectedStatus)
76}
77
78function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = 204) {
79 const path = '/api/v1/video-channels/' + channelName
80
81 return request(url)
82 .delete(path)
83 .set('Accept', 'application/json')
84 .set('Authorization', 'Bearer ' + token)
85 .expect(expectedStatus)
86}
87
88function getVideoChannel (url: string, channelName: string) {
89 const path = '/api/v1/video-channels/' + channelName
90
91 return request(url)
92 .get(path)
93 .set('Accept', 'application/json')
94 .expect(200)
95 .expect('Content-Type', /json/)
96}
97
98function updateVideoChannelAvatar (options: {
99 url: string,
100 accessToken: string,
101 fixture: string,
102 videoChannelName: string | number
103}) {
104
105 const path = '/api/v1/video-channels/' + options.videoChannelName + '/avatar/pick'
106
107 return updateAvatarRequest(Object.assign(options, { path }))
108}
109
110function setDefaultVideoChannel (servers: ServerInfo[]) {
111 const tasks: Promise<any>[] = []
112
113 for (const server of servers) {
114 const p = getMyUserInformation(server.url, server.accessToken)
115 .then(res => server.videoChannel = (res.body as User).videoChannels[0])
116
117 tasks.push(p)
118 }
119
120 return Promise.all(tasks)
121}
122
123// ---------------------------------------------------------------------------
124
125export {
126 updateVideoChannelAvatar,
127 getVideoChannelsList,
128 getAccountVideoChannelsList,
129 addVideoChannel,
130 updateVideoChannel,
131 deleteVideoChannel,
132 getVideoChannel,
133 setDefaultVideoChannel
134}
diff --git a/shared/extra-utils/videos/video-comments.ts b/shared/extra-utils/videos/video-comments.ts
new file mode 100644
index 000000000..0ebf69ced
--- /dev/null
+++ b/shared/extra-utils/videos/video-comments.ts
@@ -0,0 +1,87 @@
1import * as request from 'supertest'
2import { makeDeleteRequest } from '../requests/requests'
3
4function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
5 const path = '/api/v1/videos/' + videoId + '/comment-threads'
6
7 const req = request(url)
8 .get(path)
9 .query({ start: start })
10 .query({ count: count })
11
12 if (sort) req.query({ sort })
13 if (token) req.set('Authorization', 'Bearer ' + token)
14
15 return req.set('Accept', 'application/json')
16 .expect(200)
17 .expect('Content-Type', /json/)
18}
19
20function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
21 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
22
23 const req = request(url)
24 .get(path)
25 .set('Accept', 'application/json')
26
27 if (token) req.set('Authorization', 'Bearer ' + token)
28
29 return req.expect(200)
30 .expect('Content-Type', /json/)
31}
32
33function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) {
34 const path = '/api/v1/videos/' + videoId + '/comment-threads'
35
36 return request(url)
37 .post(path)
38 .send({ text })
39 .set('Accept', 'application/json')
40 .set('Authorization', 'Bearer ' + token)
41 .expect(expectedStatus)
42}
43
44function addVideoCommentReply (
45 url: string,
46 token: string,
47 videoId: number | string,
48 inReplyToCommentId: number,
49 text: string,
50 expectedStatus = 200
51) {
52 const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
53
54 return request(url)
55 .post(path)
56 .send({ text })
57 .set('Accept', 'application/json')
58 .set('Authorization', 'Bearer ' + token)
59 .expect(expectedStatus)
60}
61
62function deleteVideoComment (
63 url: string,
64 token: string,
65 videoId: number | string,
66 commentId: number,
67 statusCodeExpected = 204
68) {
69 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
70
71 return makeDeleteRequest({
72 url,
73 path,
74 token,
75 statusCodeExpected
76 })
77}
78
79// ---------------------------------------------------------------------------
80
81export {
82 getVideoCommentThreads,
83 getVideoThreadComments,
84 addVideoCommentThread,
85 addVideoCommentReply,
86 deleteVideoComment
87}
diff --git a/shared/extra-utils/videos/video-history.ts b/shared/extra-utils/videos/video-history.ts
new file mode 100644
index 000000000..dc7095b4d
--- /dev/null
+++ b/shared/extra-utils/videos/video-history.ts
@@ -0,0 +1,39 @@
1import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2
3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number, statusCodeExpected = 204) {
4 const path = '/api/v1/videos/' + videoId + '/watching'
5 const fields = { currentTime }
6
7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
8}
9
10function listMyVideosHistory (url: string, token: string) {
11 const path = '/api/v1/users/me/history/videos'
12
13 return makeGetRequest({
14 url,
15 path,
16 token,
17 statusCodeExpected: 200
18 })
19}
20
21function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
22 const path = '/api/v1/users/me/history/videos/remove'
23
24 return makePostBodyRequest({
25 url,
26 path,
27 token,
28 fields: beforeDate ? { beforeDate } : {},
29 statusCodeExpected: 204
30 })
31}
32
33// ---------------------------------------------------------------------------
34
35export {
36 userWatchVideo,
37 listMyVideosHistory,
38 removeMyVideosHistory
39}
diff --git a/shared/extra-utils/videos/video-imports.ts b/shared/extra-utils/videos/video-imports.ts
new file mode 100644
index 000000000..ec77cdcda
--- /dev/null
+++ b/shared/extra-utils/videos/video-imports.ts
@@ -0,0 +1,57 @@
1
2import { VideoImportCreate } from '../../models/videos'
3import { makeGetRequest, makeUploadRequest } from '../requests/requests'
4
5function getYoutubeVideoUrl () {
6 return 'https://youtu.be/msX3jv1XdvM'
7}
8
9function getMagnetURI () {
10 // tslint:disable:max-line-length
11 return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4'
12}
13
14function getBadVideoUrl () {
15 return 'https://download.cpy.re/peertube/bad_video.mp4'
16}
17
18function importVideo (url: string, token: string, attributes: VideoImportCreate) {
19 const path = '/api/v1/videos/imports'
20
21 let attaches: any = {}
22 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
23
24 return makeUploadRequest({
25 url,
26 path,
27 token,
28 attaches,
29 fields: attributes,
30 statusCodeExpected: 200
31 })
32}
33
34function getMyVideoImports (url: string, token: string, sort?: string) {
35 const path = '/api/v1/users/me/videos/imports'
36
37 const query = {}
38 if (sort) query['sort'] = sort
39
40 return makeGetRequest({
41 url,
42 query,
43 path,
44 token,
45 statusCodeExpected: 200
46 })
47}
48
49// ---------------------------------------------------------------------------
50
51export {
52 getBadVideoUrl,
53 getYoutubeVideoUrl,
54 importVideo,
55 getMagnetURI,
56 getMyVideoImports
57}
diff --git a/shared/extra-utils/videos/video-playlists.ts b/shared/extra-utils/videos/video-playlists.ts
new file mode 100644
index 000000000..4d110a131
--- /dev/null
+++ b/shared/extra-utils/videos/video-playlists.ts
@@ -0,0 +1,318 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
2import { VideoPlaylistCreate } from '../../models/videos/playlist/video-playlist-create.model'
3import { omit } from 'lodash'
4import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
5import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
6import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
7import { videoUUIDToId } from './videos'
8import { join } from 'path'
9import { root } from '..'
10import { readdir } from 'fs-extra'
11import { expect } from 'chai'
12import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
13
14function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
15 const path = '/api/v1/video-playlists'
16
17 const query = {
18 start,
19 count,
20 sort
21 }
22
23 return makeGetRequest({
24 url,
25 path,
26 query,
27 statusCodeExpected: 200
28 })
29}
30
31function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
32 const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
33
34 const query = {
35 start,
36 count,
37 sort
38 }
39
40 return makeGetRequest({
41 url,
42 path,
43 query,
44 statusCodeExpected: 200
45 })
46}
47
48function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string) {
49 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
50
51 const query = {
52 start,
53 count,
54 sort
55 }
56
57 return makeGetRequest({
58 url,
59 path,
60 query,
61 statusCodeExpected: 200
62 })
63}
64
65function getAccountPlaylistsListWithToken (
66 url: string,
67 token: string,
68 accountName: string,
69 start: number,
70 count: number,
71 playlistType?: VideoPlaylistType,
72 sort?: string
73) {
74 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
75
76 const query = {
77 start,
78 count,
79 playlistType,
80 sort
81 }
82
83 return makeGetRequest({
84 url,
85 token,
86 path,
87 query,
88 statusCodeExpected: 200
89 })
90}
91
92function getVideoPlaylist (url: string, playlistId: number | string, statusCodeExpected = 200) {
93 const path = '/api/v1/video-playlists/' + playlistId
94
95 return makeGetRequest({
96 url,
97 path,
98 statusCodeExpected
99 })
100}
101
102function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = 200) {
103 const path = '/api/v1/video-playlists/' + playlistId
104
105 return makeGetRequest({
106 url,
107 token,
108 path,
109 statusCodeExpected
110 })
111}
112
113function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = 204) {
114 const path = '/api/v1/video-playlists/' + playlistId
115
116 return makeDeleteRequest({
117 url,
118 path,
119 token,
120 statusCodeExpected
121 })
122}
123
124function createVideoPlaylist (options: {
125 url: string,
126 token: string,
127 playlistAttrs: VideoPlaylistCreate,
128 expectedStatus?: number
129}) {
130 const path = '/api/v1/video-playlists'
131
132 const fields = omit(options.playlistAttrs, 'thumbnailfile')
133
134 const attaches = options.playlistAttrs.thumbnailfile
135 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
136 : {}
137
138 return makeUploadRequest({
139 method: 'POST',
140 url: options.url,
141 path,
142 token: options.token,
143 fields,
144 attaches,
145 statusCodeExpected: options.expectedStatus || 200
146 })
147}
148
149function updateVideoPlaylist (options: {
150 url: string,
151 token: string,
152 playlistAttrs: VideoPlaylistUpdate,
153 playlistId: number | string,
154 expectedStatus?: number
155}) {
156 const path = '/api/v1/video-playlists/' + options.playlistId
157
158 const fields = omit(options.playlistAttrs, 'thumbnailfile')
159
160 const attaches = options.playlistAttrs.thumbnailfile
161 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
162 : {}
163
164 return makeUploadRequest({
165 method: 'PUT',
166 url: options.url,
167 path,
168 token: options.token,
169 fields,
170 attaches,
171 statusCodeExpected: options.expectedStatus || 204
172 })
173}
174
175async function addVideoInPlaylist (options: {
176 url: string,
177 token: string,
178 playlistId: number | string,
179 elementAttrs: VideoPlaylistElementCreate | { videoId: string }
180 expectedStatus?: number
181}) {
182 options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
183
184 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
185
186 return makePostBodyRequest({
187 url: options.url,
188 path,
189 token: options.token,
190 fields: options.elementAttrs,
191 statusCodeExpected: options.expectedStatus || 200
192 })
193}
194
195function updateVideoPlaylistElement (options: {
196 url: string,
197 token: string,
198 playlistId: number | string,
199 videoId: number | string,
200 elementAttrs: VideoPlaylistElementUpdate,
201 expectedStatus?: number
202}) {
203 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
204
205 return makePutBodyRequest({
206 url: options.url,
207 path,
208 token: options.token,
209 fields: options.elementAttrs,
210 statusCodeExpected: options.expectedStatus || 204
211 })
212}
213
214function removeVideoFromPlaylist (options: {
215 url: string,
216 token: string,
217 playlistId: number | string,
218 videoId: number | string,
219 expectedStatus?: number
220}) {
221 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
222
223 return makeDeleteRequest({
224 url: options.url,
225 path,
226 token: options.token,
227 statusCodeExpected: options.expectedStatus || 204
228 })
229}
230
231function reorderVideosPlaylist (options: {
232 url: string,
233 token: string,
234 playlistId: number | string,
235 elementAttrs: {
236 startPosition: number,
237 insertAfterPosition: number,
238 reorderLength?: number
239 },
240 expectedStatus?: number
241}) {
242 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
243
244 return makePostBodyRequest({
245 url: options.url,
246 path,
247 token: options.token,
248 fields: options.elementAttrs,
249 statusCodeExpected: options.expectedStatus || 204
250 })
251}
252
253async function checkPlaylistFilesWereRemoved (
254 playlistUUID: string,
255 serverNumber: number,
256 directories = [ 'thumbnails' ]
257) {
258 const testDirectory = 'test' + serverNumber
259
260 for (const directory of directories) {
261 const directoryPath = join(root(), testDirectory, directory)
262
263 const files = await readdir(directoryPath)
264 for (const file of files) {
265 expect(file).to.not.contain(playlistUUID)
266 }
267 }
268}
269
270function getVideoPlaylistPrivacies (url: string) {
271 const path = '/api/v1/video-playlists/privacies'
272
273 return makeGetRequest({
274 url,
275 path,
276 statusCodeExpected: 200
277 })
278}
279
280function doVideosExistInMyPlaylist (url: string, token: string, videoIds: number[]) {
281 const path = '/api/v1/users/me/video-playlists/videos-exist'
282
283 return makeGetRequest({
284 url,
285 token,
286 path,
287 query: { videoIds },
288 statusCodeExpected: 200
289 })
290}
291
292// ---------------------------------------------------------------------------
293
294export {
295 getVideoPlaylistPrivacies,
296
297 getVideoPlaylistsList,
298 getVideoChannelPlaylistsList,
299 getAccountPlaylistsList,
300 getAccountPlaylistsListWithToken,
301
302 getVideoPlaylist,
303 getVideoPlaylistWithToken,
304
305 createVideoPlaylist,
306 updateVideoPlaylist,
307 deleteVideoPlaylist,
308
309 addVideoInPlaylist,
310 updateVideoPlaylistElement,
311 removeVideoFromPlaylist,
312
313 reorderVideosPlaylist,
314
315 checkPlaylistFilesWereRemoved,
316
317 doVideosExistInMyPlaylist
318}
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
new file mode 100644
index 000000000..eb25011cb
--- /dev/null
+++ b/shared/extra-utils/videos/video-streaming-playlists.ts
@@ -0,0 +1,51 @@
1import { makeRawRequest } from '../requests/requests'
2import { sha256 } from '../../../server/helpers/core-utils'
3import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
4import { expect } from 'chai'
5
6function getPlaylist (url: string, statusCodeExpected = 200) {
7 return makeRawRequest(url, statusCodeExpected)
8}
9
10function getSegment (url: string, statusCodeExpected = 200, range?: string) {
11 return makeRawRequest(url, statusCodeExpected, range)
12}
13
14function getSegmentSha256 (url: string, statusCodeExpected = 200) {
15 return makeRawRequest(url, statusCodeExpected)
16}
17
18async function checkSegmentHash (
19 baseUrlPlaylist: string,
20 baseUrlSegment: string,
21 videoUUID: string,
22 resolution: number,
23 hlsPlaylist: VideoStreamingPlaylist
24) {
25 const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
26 const playlist = res.text
27
28 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
29
30 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
31
32 const length = parseInt(matches[1], 10)
33 const offset = parseInt(matches[2], 10)
34 const range = `${offset}-${offset + length - 1}`
35
36 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, 206, `bytes=${range}`)
37
38 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
39
40 const sha256Server = resSha.body[ videoName ][range]
41 expect(sha256(res2.body)).to.equal(sha256Server)
42}
43
44// ---------------------------------------------------------------------------
45
46export {
47 getPlaylist,
48 getSegment,
49 getSegmentSha256,
50 checkSegmentHash
51}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
new file mode 100644
index 000000000..b5a07b792
--- /dev/null
+++ b/shared/extra-utils/videos/videos.ts
@@ -0,0 +1,648 @@
1/* tslint:disable:no-unused-expression */
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 {
9 buildAbsoluteFixturePath,
10 getMyUserInformation,
11 immutableAssign,
12 makeGetRequest,
13 makePutBodyRequest,
14 makeUploadRequest,
15 root,
16 ServerInfo,
17 testImage
18} from '../'
19import * as validator from 'validator'
20import { VideoDetails, VideoPrivacy } from '../../models/videos'
21import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, loadLanguages, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
22import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
23
24loadLanguages()
25
26type 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
49function getVideoCategories (url: string) {
50 const path = '/api/v1/videos/categories'
51
52 return makeGetRequest({
53 url,
54 path,
55 statusCodeExpected: 200
56 })
57}
58
59function getVideoLicences (url: string) {
60 const path = '/api/v1/videos/licences'
61
62 return makeGetRequest({
63 url,
64 path,
65 statusCodeExpected: 200
66 })
67}
68
69function getVideoLanguages (url: string) {
70 const path = '/api/v1/videos/languages'
71
72 return makeGetRequest({
73 url,
74 path,
75 statusCodeExpected: 200
76 })
77}
78
79function getVideoPrivacies (url: string) {
80 const path = '/api/v1/videos/privacies'
81
82 return makeGetRequest({
83 url,
84 path,
85 statusCodeExpected: 200
86 })
87}
88
89function 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
98function 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
112function 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
122function 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
130function 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
141function 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
153function 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
164function 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
180function 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
204function 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
228function 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
250function 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
265function 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
276function 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
287function 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
297async 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
326async 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
405function 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
449function 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
460function 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
472async 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 on server 2, extension will always be .mp4
572 if (attributes.account.host === 'localhost:9002') 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
602async 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
609async 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
620export {
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}