aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/shared
diff options
context:
space:
mode:
Diffstat (limited to 'server/tests/shared')
-rw-r--r--server/tests/shared/actors.ts73
-rw-r--r--server/tests/shared/captions.ts21
-rw-r--r--server/tests/shared/checks.ts98
-rw-r--r--server/tests/shared/directories.ts34
-rw-r--r--server/tests/shared/generate.ts74
-rw-r--r--server/tests/shared/index.ts15
-rw-r--r--server/tests/shared/live.ts41
-rw-r--r--server/tests/shared/mock-servers/index.ts7
-rw-r--r--server/tests/shared/mock-servers/mock-429.ts33
-rw-r--r--server/tests/shared/mock-servers/mock-email.ts62
-rw-r--r--server/tests/shared/mock-servers/mock-instances-index.ts46
-rw-r--r--server/tests/shared/mock-servers/mock-joinpeertube-versions.ts34
-rw-r--r--server/tests/shared/mock-servers/mock-object-storage.ts41
-rw-r--r--server/tests/shared/mock-servers/mock-plugin-blocklist.ts36
-rw-r--r--server/tests/shared/mock-servers/mock-proxy.ts24
-rw-r--r--server/tests/shared/mock-servers/shared.ts33
-rw-r--r--server/tests/shared/notifications.ts798
-rw-r--r--server/tests/shared/playlists.ts25
-rw-r--r--server/tests/shared/plugins.ts18
-rw-r--r--server/tests/shared/requests.ts12
-rw-r--r--server/tests/shared/streaming-playlists.ts77
-rw-r--r--server/tests/shared/tests.ts37
-rw-r--r--server/tests/shared/tracker.ts27
-rw-r--r--server/tests/shared/videos.ts (renamed from server/tests/shared/video.ts)107
24 files changed, 1765 insertions, 8 deletions
diff --git a/server/tests/shared/actors.ts b/server/tests/shared/actors.ts
new file mode 100644
index 000000000..f8f4a5137
--- /dev/null
+++ b/server/tests/shared/actors.ts
@@ -0,0 +1,73 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@shared/core-utils'
7import { Account, VideoChannel } from '@shared/models'
8import { PeerTubeServer } from '@shared/server-commands'
9
10async function expectChannelsFollows (options: {
11 server: PeerTubeServer
12 handle: string
13 followers: number
14 following: number
15}) {
16 const { server } = options
17 const { data } = await server.channels.list()
18
19 return expectActorFollow({ ...options, data })
20}
21
22async function expectAccountFollows (options: {
23 server: PeerTubeServer
24 handle: string
25 followers: number
26 following: number
27}) {
28 const { server } = options
29 const { data } = await server.accounts.list()
30
31 return expectActorFollow({ ...options, data })
32}
33
34async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
35 const testDirectory = 'test' + serverNumber
36
37 for (const directory of [ 'avatars' ]) {
38 const directoryPath = join(root(), testDirectory, directory)
39
40 const directoryExists = await pathExists(directoryPath)
41 expect(directoryExists).to.be.true
42
43 const files = await readdir(directoryPath)
44 for (const file of files) {
45 expect(file).to.not.contain(filename)
46 }
47 }
48}
49
50export {
51 expectAccountFollows,
52 expectChannelsFollows,
53 checkActorFilesWereRemoved
54}
55
56// ---------------------------------------------------------------------------
57
58function expectActorFollow (options: {
59 server: PeerTubeServer
60 data: (Account | VideoChannel)[]
61 handle: string
62 followers: number
63 following: number
64}) {
65 const { server, data, handle, followers, following } = options
66
67 const actor = data.find(a => a.name + '@' + a.host === handle)
68 const message = `${handle} on ${server.url}`
69
70 expect(actor, message).to.exist
71 expect(actor.followersCount).to.equal(followers, message)
72 expect(actor.followingCount).to.equal(following, message)
73}
diff --git a/server/tests/shared/captions.ts b/server/tests/shared/captions.ts
new file mode 100644
index 000000000..35e722408
--- /dev/null
+++ b/server/tests/shared/captions.ts
@@ -0,0 +1,21 @@
1import { expect } from 'chai'
2import request from 'supertest'
3import { HttpStatusCode } from '@shared/models'
4
5async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) {
6 const res = await request(url)
7 .get(captionPath)
8 .expect(HttpStatusCode.OK_200)
9
10 if (toTest instanceof RegExp) {
11 expect(res.text).to.match(toTest)
12 } else {
13 expect(res.text).to.contain(toTest)
14 }
15}
16
17// ---------------------------------------------------------------------------
18
19export {
20 testCaptionFile
21}
diff --git a/server/tests/shared/checks.ts b/server/tests/shared/checks.ts
new file mode 100644
index 000000000..9ecc84b5d
--- /dev/null
+++ b/server/tests/shared/checks.ts
@@ -0,0 +1,98 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { pathExists, readFile } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@shared/core-utils'
7import { HttpStatusCode } from '@shared/models'
8import { makeGetRequest, PeerTubeServer } from '@shared/server-commands'
9
10// Default interval -> 5 minutes
11function dateIsValid (dateString: string, interval = 300000) {
12 const dateToCheck = new Date(dateString)
13 const now = new Date()
14
15 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
16}
17
18function expectStartWith (str: string, start: string) {
19 expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true
20}
21
22async function expectLogDoesNotContain (server: PeerTubeServer, str: string) {
23 const content = await server.servers.getLogContent()
24
25 expect(content.toString()).to.not.contain(str)
26}
27
28async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
29 const res = await makeGetRequest({
30 url,
31 path: imagePath,
32 expectedStatus: HttpStatusCode.OK_200
33 })
34
35 const body = res.body
36
37 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
38 const minLength = body.length - ((30 * body.length) / 100)
39 const maxLength = body.length + ((30 * body.length) / 100)
40
41 expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
42 expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
43}
44
45async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
46 const base = server.servers.buildDirectory(directory)
47
48 expect(await pathExists(join(base, filePath))).to.equal(exist)
49}
50
51function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
52 return makeGetRequest({
53 url,
54 path,
55 token,
56 query: { ...query, start: 'hello' },
57 expectedStatus: HttpStatusCode.BAD_REQUEST_400
58 })
59}
60
61async function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
62 await makeGetRequest({
63 url,
64 path,
65 token,
66 query: { ...query, count: 'hello' },
67 expectedStatus: HttpStatusCode.BAD_REQUEST_400
68 })
69
70 await makeGetRequest({
71 url,
72 path,
73 token,
74 query: { ...query, count: 2000 },
75 expectedStatus: HttpStatusCode.BAD_REQUEST_400
76 })
77}
78
79function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
80 return makeGetRequest({
81 url,
82 path,
83 token,
84 query: { ...query, sort: 'hello' },
85 expectedStatus: HttpStatusCode.BAD_REQUEST_400
86 })
87}
88
89export {
90 dateIsValid,
91 testImage,
92 expectLogDoesNotContain,
93 testFileExistsOrNot,
94 expectStartWith,
95 checkBadStartPagination,
96 checkBadCountPagination,
97 checkBadSortPagination
98}
diff --git a/server/tests/shared/directories.ts b/server/tests/shared/directories.ts
new file mode 100644
index 000000000..c7065a767
--- /dev/null
+++ b/server/tests/shared/directories.ts
@@ -0,0 +1,34 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@shared/core-utils'
7import { PeerTubeServer } from '@shared/server-commands'
8
9async function checkTmpIsEmpty (server: PeerTubeServer) {
10 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
11
12 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
13 await checkDirectoryIsEmpty(server, 'tmp/hls')
14 }
15}
16
17async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
18 const testDirectory = 'test' + server.internalServerNumber
19
20 const directoryPath = join(root(), testDirectory, directory)
21
22 const directoryExists = await pathExists(directoryPath)
23 expect(directoryExists).to.be.true
24
25 const files = await readdir(directoryPath)
26 const filtered = files.filter(f => exceptions.includes(f) === false)
27
28 expect(filtered).to.have.lengthOf(0)
29}
30
31export {
32 checkTmpIsEmpty,
33 checkDirectoryIsEmpty
34}
diff --git a/server/tests/shared/generate.ts b/server/tests/shared/generate.ts
new file mode 100644
index 000000000..f806df2f5
--- /dev/null
+++ b/server/tests/shared/generate.ts
@@ -0,0 +1,74 @@
1import { expect } from 'chai'
2import ffmpeg from 'fluent-ffmpeg'
3import { ensureDir, pathExists } from 'fs-extra'
4import { dirname } from 'path'
5import { buildAbsoluteFixturePath, getMaxBitrate } from '@shared/core-utils'
6import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '@shared/extra-utils'
7
8async function ensureHasTooBigBitrate (fixturePath: string) {
9 const bitrate = await getVideoFileBitrate(fixturePath)
10 const dataResolution = await getVideoFileResolution(fixturePath)
11 const fps = await getVideoFileFPS(fixturePath)
12
13 const maxBitrate = getMaxBitrate({ ...dataResolution, fps })
14 expect(bitrate).to.be.above(maxBitrate)
15}
16
17async function generateHighBitrateVideo () {
18 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
19
20 await ensureDir(dirname(tempFixturePath))
21
22 const exists = await pathExists(tempFixturePath)
23 if (!exists) {
24 console.log('Generating high bitrate video.')
25
26 // Generate a random, high bitrate video on the fly, so we don't have to include
27 // a large file in the repo. The video needs to have a certain minimum length so
28 // that FFmpeg properly applies bitrate limits.
29 // https://stackoverflow.com/a/15795112
30 return new Promise<string>((res, rej) => {
31 ffmpeg()
32 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
33 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
34 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
35 .output(tempFixturePath)
36 .on('error', rej)
37 .on('end', () => res(tempFixturePath))
38 .run()
39 })
40 }
41
42 await ensureHasTooBigBitrate(tempFixturePath)
43
44 return tempFixturePath
45}
46
47async function generateVideoWithFramerate (fps = 60) {
48 const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
49
50 await ensureDir(dirname(tempFixturePath))
51
52 const exists = await pathExists(tempFixturePath)
53 if (!exists) {
54 console.log('Generating video with framerate %d.', fps)
55
56 return new Promise<string>((res, rej) => {
57 ffmpeg()
58 .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
59 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
60 .outputOptions([ `-r ${fps}` ])
61 .output(tempFixturePath)
62 .on('error', rej)
63 .on('end', () => res(tempFixturePath))
64 .run()
65 })
66 }
67
68 return tempFixturePath
69}
70
71export {
72 generateHighBitrateVideo,
73 generateVideoWithFramerate
74}
diff --git a/server/tests/shared/index.ts b/server/tests/shared/index.ts
index 938817268..47019d6a8 100644
--- a/server/tests/shared/index.ts
+++ b/server/tests/shared/index.ts
@@ -1,2 +1,15 @@
1export * from './mock-servers'
2export * from './actors'
3export * from './captions'
4export * from './checks'
5export * from './directories'
6export * from './generate'
7export * from './live'
8export * from './notifications'
9export * from './playlists'
10export * from './plugins'
1export * from './requests' 11export * from './requests'
2export * from './video' 12export * from './streaming-playlists'
13export * from './tests'
14export * from './tracker'
15export * from './videos'
diff --git a/server/tests/shared/live.ts b/server/tests/shared/live.ts
new file mode 100644
index 000000000..72e3e27f6
--- /dev/null
+++ b/server/tests/shared/live.ts
@@ -0,0 +1,41 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { PeerTubeServer } from '@shared/server-commands'
7
8async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) {
9 const basePath = server.servers.buildDirectory('streaming-playlists')
10 const hlsPath = join(basePath, 'hls', videoUUID)
11
12 if (resolutions.length === 0) {
13 const result = await pathExists(hlsPath)
14 expect(result).to.be.false
15
16 return
17 }
18
19 const files = await readdir(hlsPath)
20
21 // fragmented file and playlist per resolution + master playlist + segments sha256 json file
22 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
23
24 for (const resolution of resolutions) {
25 const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
26 expect(fragmentedFile).to.exist
27
28 const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
29 expect(playlistFile).to.exist
30 }
31
32 const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
33 expect(masterPlaylistFile).to.exist
34
35 const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
36 expect(shaFile).to.exist
37}
38
39export {
40 checkLiveCleanupAfterSave
41}
diff --git a/server/tests/shared/mock-servers/index.ts b/server/tests/shared/mock-servers/index.ts
new file mode 100644
index 000000000..abf4a8203
--- /dev/null
+++ b/server/tests/shared/mock-servers/index.ts
@@ -0,0 +1,7 @@
1export * from './mock-429'
2export * from './mock-email'
3export * from './mock-instances-index'
4export * from './mock-joinpeertube-versions'
5export * from './mock-object-storage'
6export * from './mock-plugin-blocklist'
7export * from './mock-proxy'
diff --git a/server/tests/shared/mock-servers/mock-429.ts b/server/tests/shared/mock-servers/mock-429.ts
new file mode 100644
index 000000000..1fc20b079
--- /dev/null
+++ b/server/tests/shared/mock-servers/mock-429.ts
@@ -0,0 +1,33 @@
1import express from 'express'
2import { Server } from 'http'
3import { getPort, randomListen, terminateServer } from './shared'
4
5export class Mock429 {
6 private server: Server
7 private responseSent = false
8
9 async initialize () {
10 const app = express()
11
12 app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
13
14 if (!this.responseSent) {
15 this.responseSent = true
16
17 // Retry after 5 seconds
18 res.header('retry-after', '2')
19 return res.sendStatus(429)
20 }
21
22 return res.sendStatus(200)
23 })
24
25 this.server = await randomListen(app)
26
27 return getPort(this.server)
28 }
29
30 terminate () {
31 return terminateServer(this.server)
32 }
33}
diff --git a/server/tests/shared/mock-servers/mock-email.ts b/server/tests/shared/mock-servers/mock-email.ts
new file mode 100644
index 000000000..c518679c9
--- /dev/null
+++ b/server/tests/shared/mock-servers/mock-email.ts
@@ -0,0 +1,62 @@
1import { ChildProcess } from 'child_process'
2import MailDev from '@peertube/maildev'
3import { parallelTests, randomInt } from '@shared/core-utils'
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
14 collectEmails (emailsCollection: object[]) {
15 return new Promise<number>((res, rej) => {
16 const port = parallelTests() ? randomInt(1000, 2000) : 1025
17 this.emails = emailsCollection
18
19 if (this.started) {
20 return res(undefined)
21 }
22
23 const maildev = new MailDev({
24 ip: '127.0.0.1',
25 smtp: port,
26 disableWeb: true,
27 silent: true
28 })
29
30 maildev.on('new', email => {
31 this.emails.push(email)
32 })
33
34 maildev.listen(err => {
35 if (err) return rej(err)
36
37 this.started = true
38
39 return res(port)
40 })
41 })
42 }
43
44 kill () {
45 if (!this.emailChildProcess) return
46
47 process.kill(this.emailChildProcess.pid)
48
49 this.emailChildProcess = null
50 MockSmtpServer.instance = null
51 }
52
53 static get Instance () {
54 return this.instance || (this.instance = new this())
55 }
56}
57
58// ---------------------------------------------------------------------------
59
60export {
61 MockSmtpServer
62}
diff --git a/server/tests/shared/mock-servers/mock-instances-index.ts b/server/tests/shared/mock-servers/mock-instances-index.ts
new file mode 100644
index 000000000..598b007f1
--- /dev/null
+++ b/server/tests/shared/mock-servers/mock-instances-index.ts
@@ -0,0 +1,46 @@
1import express from 'express'
2import { Server } from 'http'
3import { getPort, randomListen, terminateServer } from './shared'
4
5export class MockInstancesIndex {
6 private server: Server
7
8 private readonly indexInstances: { host: string, createdAt: string }[] = []
9
10 async initialize () {
11 const app = express()
12
13 app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
15
16 return next()
17 })
18
19 app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => {
20 const since = req.query.since
21
22 const filtered = this.indexInstances.filter(i => {
23 if (!since) return true
24
25 return i.createdAt > since
26 })
27
28 return res.json({
29 total: filtered.length,
30 data: filtered
31 })
32 })
33
34 this.server = await randomListen(app)
35
36 return getPort(this.server)
37 }
38
39 addInstance (host: string) {
40 this.indexInstances.push({ host, createdAt: new Date().toISOString() })
41 }
42
43 terminate () {
44 return terminateServer(this.server)
45 }
46}
diff --git a/server/tests/shared/mock-servers/mock-joinpeertube-versions.ts b/server/tests/shared/mock-servers/mock-joinpeertube-versions.ts
new file mode 100644
index 000000000..502f4e2f5
--- /dev/null
+++ b/server/tests/shared/mock-servers/mock-joinpeertube-versions.ts
@@ -0,0 +1,34 @@
1import express from 'express'
2import { Server } from 'http'
3import { getPort, randomListen } from './shared'
4
5export class MockJoinPeerTubeVersions {
6 private server: Server
7 private latestVersion: string
8
9 async initialize () {
10 const app = express()
11
12 app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
13 if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
14
15 return next()
16 })
17
18 app.get('/versions.json', (req: express.Request, res: express.Response) => {
19 return res.json({
20 peertube: {
21 latestVersion: this.latestVersion
22 }
23 })
24 })
25
26 this.server = await randomListen(app)
27
28 return getPort(this.server)
29 }
30
31 setLatestVersion (latestVersion: string) {
32 this.latestVersion = latestVersion
33 }
34}
diff --git a/server/tests/shared/mock-servers/mock-object-storage.ts b/server/tests/shared/mock-servers/mock-object-storage.ts
new file mode 100644
index 000000000..99d68e014
--- /dev/null
+++ b/server/tests/shared/mock-servers/mock-object-storage.ts
@@ -0,0 +1,41 @@
1import express from 'express'
2import got, { RequestError } from 'got'
3import { Server } from 'http'
4import { pipeline } from 'stream'
5import { ObjectStorageCommand } from '@shared/server-commands'
6import { getPort, randomListen, terminateServer } from './shared'
7
8export class MockObjectStorage {
9 private server: Server
10
11 async initialize () {
12 const app = express()
13
14 app.get('/:bucketName/:path(*)', (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 const url = `http://${req.params.bucketName}.${ObjectStorageCommand.getEndpointHost()}/${req.params.path}`
16
17 if (process.env.DEBUG) {
18 console.log('Receiving request on mocked server %s.', req.url)
19 console.log('Proxifying request to %s', url)
20 }
21
22 return pipeline(
23 got.stream(url, { throwHttpErrors: false }),
24 res,
25 (err: RequestError) => {
26 if (!err) return
27
28 console.error('Pipeline failed.', err)
29 }
30 )
31 })
32
33 this.server = await randomListen(app)
34
35 return getPort(this.server)
36 }
37
38 terminate () {
39 return terminateServer(this.server)
40 }
41}
diff --git a/server/tests/shared/mock-servers/mock-plugin-blocklist.ts b/server/tests/shared/mock-servers/mock-plugin-blocklist.ts
new file mode 100644
index 000000000..5d6e01816
--- /dev/null
+++ b/server/tests/shared/mock-servers/mock-plugin-blocklist.ts
@@ -0,0 +1,36 @@
1import express, { Request, Response } from 'express'
2import { Server } from 'http'
3import { getPort, randomListen, terminateServer } from './shared'
4
5type BlocklistResponse = {
6 data: {
7 value: string
8 action?: 'add' | 'remove'
9 updatedAt?: string
10 }[]
11}
12
13export class MockBlocklist {
14 private body: BlocklistResponse
15 private server: Server
16
17 async initialize () {
18 const app = express()
19
20 app.get('/blocklist', (req: Request, res: Response) => {
21 return res.json(this.body)
22 })
23
24 this.server = await randomListen(app)
25
26 return getPort(this.server)
27 }
28
29 replace (body: BlocklistResponse) {
30 this.body = body
31 }
32
33 terminate () {
34 return terminateServer(this.server)
35 }
36}
diff --git a/server/tests/shared/mock-servers/mock-proxy.ts b/server/tests/shared/mock-servers/mock-proxy.ts
new file mode 100644
index 000000000..cbc7c4466
--- /dev/null
+++ b/server/tests/shared/mock-servers/mock-proxy.ts
@@ -0,0 +1,24 @@
1import { createServer, Server } from 'http'
2import proxy from 'proxy'
3import { getPort, terminateServer } from './shared'
4
5class MockProxy {
6 private server: Server
7
8 initialize () {
9 return new Promise<number>(res => {
10 this.server = proxy(createServer())
11 this.server.listen(0, () => res(getPort(this.server)))
12 })
13 }
14
15 terminate () {
16 return terminateServer(this.server)
17 }
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 MockProxy
24}
diff --git a/server/tests/shared/mock-servers/shared.ts b/server/tests/shared/mock-servers/shared.ts
new file mode 100644
index 000000000..235642439
--- /dev/null
+++ b/server/tests/shared/mock-servers/shared.ts
@@ -0,0 +1,33 @@
1import { Express } from 'express'
2import { Server } from 'http'
3import { AddressInfo } from 'net'
4
5function randomListen (app: Express) {
6 return new Promise<Server>(res => {
7 const server = app.listen(0, () => res(server))
8 })
9}
10
11function getPort (server: Server) {
12 const address = server.address() as AddressInfo
13
14 return address.port
15}
16
17function terminateServer (server: Server) {
18 if (!server) return Promise.resolve()
19
20 return new Promise<void>((res, rej) => {
21 server.close(err => {
22 if (err) return rej(err)
23
24 return res()
25 })
26 })
27}
28
29export {
30 randomListen,
31 getPort,
32 terminateServer
33}
diff --git a/server/tests/shared/notifications.ts b/server/tests/shared/notifications.ts
new file mode 100644
index 000000000..cdc21fdc8
--- /dev/null
+++ b/server/tests/shared/notifications.ts
@@ -0,0 +1,798 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { inspect } from 'util'
5import {
6 AbuseState,
7 PluginType,
8 UserNotification,
9 UserNotificationSetting,
10 UserNotificationSettingValue,
11 UserNotificationType
12} from '@shared/models'
13import { createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
14import { MockSmtpServer } from './mock-servers'
15
16type CheckerBaseParams = {
17 server: PeerTubeServer
18 emails: any[]
19 socketNotifications: UserNotification[]
20 token: string
21 check?: { web: boolean, mail: boolean }
22}
23
24type CheckerType = 'presence' | 'absence'
25
26function getAllNotificationsSettings (): UserNotificationSetting {
27 return {
28 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
29 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
30 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
31 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
32 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
33 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
34 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
35 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
36 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
37 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
38 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
39 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
40 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
41 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
42 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
43 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
44 }
45}
46
47async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
48 videoName: string
49 shortUUID: string
50 checkType: CheckerType
51}) {
52 const { videoName, shortUUID } = options
53 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
54
55 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
56 if (checkType === 'presence') {
57 expect(notification).to.not.be.undefined
58 expect(notification.type).to.equal(notificationType)
59
60 checkVideo(notification.video, videoName, shortUUID)
61 checkActor(notification.video.channel)
62 } else {
63 expect(notification).to.satisfy((n: UserNotification) => {
64 return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
65 })
66 }
67 }
68
69 function emailNotificationFinder (email: object) {
70 const text = email['text']
71 return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
72 }
73
74 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
75}
76
77async function checkVideoIsPublished (options: CheckerBaseParams & {
78 videoName: string
79 shortUUID: string
80 checkType: CheckerType
81}) {
82 const { videoName, shortUUID } = options
83 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
84
85 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
86 if (checkType === 'presence') {
87 expect(notification).to.not.be.undefined
88 expect(notification.type).to.equal(notificationType)
89
90 checkVideo(notification.video, videoName, shortUUID)
91 checkActor(notification.video.channel)
92 } else {
93 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
94 }
95 }
96
97 function emailNotificationFinder (email: object) {
98 const text: string = email['text']
99 return text.includes(shortUUID) && text.includes('Your video')
100 }
101
102 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
103}
104
105async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
106 videoName: string
107 shortUUID: string
108 url: string
109 success: boolean
110 checkType: CheckerType
111}) {
112 const { videoName, shortUUID, url, success } = options
113
114 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
115
116 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
117 if (checkType === 'presence') {
118 expect(notification).to.not.be.undefined
119 expect(notification.type).to.equal(notificationType)
120
121 expect(notification.videoImport.targetUrl).to.equal(url)
122
123 if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
124 } else {
125 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
126 }
127 }
128
129 function emailNotificationFinder (email: object) {
130 const text: string = email['text']
131 const toFind = success ? ' finished' : ' error'
132
133 return text.includes(url) && text.includes(toFind)
134 }
135
136 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
137}
138
139async function checkUserRegistered (options: CheckerBaseParams & {
140 username: string
141 checkType: CheckerType
142}) {
143 const { username } = options
144 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
145
146 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
147 if (checkType === 'presence') {
148 expect(notification).to.not.be.undefined
149 expect(notification.type).to.equal(notificationType)
150
151 checkActor(notification.account)
152 expect(notification.account.name).to.equal(username)
153 } else {
154 expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
155 }
156 }
157
158 function emailNotificationFinder (email: object) {
159 const text: string = email['text']
160
161 return text.includes(' registered.') && text.includes(username)
162 }
163
164 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
165}
166
167async function checkNewActorFollow (options: CheckerBaseParams & {
168 followType: 'channel' | 'account'
169 followerName: string
170 followerDisplayName: string
171 followingDisplayName: string
172 checkType: CheckerType
173}) {
174 const { followType, followerName, followerDisplayName, followingDisplayName } = options
175 const notificationType = UserNotificationType.NEW_FOLLOW
176
177 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
178 if (checkType === 'presence') {
179 expect(notification).to.not.be.undefined
180 expect(notification.type).to.equal(notificationType)
181
182 checkActor(notification.actorFollow.follower)
183 expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
184 expect(notification.actorFollow.follower.name).to.equal(followerName)
185 expect(notification.actorFollow.follower.host).to.not.be.undefined
186
187 const following = notification.actorFollow.following
188 expect(following.displayName).to.equal(followingDisplayName)
189 expect(following.type).to.equal(followType)
190 } else {
191 expect(notification).to.satisfy(n => {
192 return n.type !== notificationType ||
193 (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
194 })
195 }
196 }
197
198 function emailNotificationFinder (email: object) {
199 const text: string = email['text']
200
201 return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
202 }
203
204 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
205}
206
207async function checkNewInstanceFollower (options: CheckerBaseParams & {
208 followerHost: string
209 checkType: CheckerType
210}) {
211 const { followerHost } = options
212 const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
213
214 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
215 if (checkType === 'presence') {
216 expect(notification).to.not.be.undefined
217 expect(notification.type).to.equal(notificationType)
218
219 checkActor(notification.actorFollow.follower)
220 expect(notification.actorFollow.follower.name).to.equal('peertube')
221 expect(notification.actorFollow.follower.host).to.equal(followerHost)
222
223 expect(notification.actorFollow.following.name).to.equal('peertube')
224 } else {
225 expect(notification).to.satisfy(n => {
226 return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
227 })
228 }
229 }
230
231 function emailNotificationFinder (email: object) {
232 const text: string = email['text']
233
234 return text.includes('instance has a new follower') && text.includes(followerHost)
235 }
236
237 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
238}
239
240async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
241 followerHost: string
242 followingHost: string
243 checkType: CheckerType
244}) {
245 const { followerHost, followingHost } = options
246 const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
247
248 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
249 if (checkType === 'presence') {
250 expect(notification).to.not.be.undefined
251 expect(notification.type).to.equal(notificationType)
252
253 const following = notification.actorFollow.following
254 checkActor(following)
255 expect(following.name).to.equal('peertube')
256 expect(following.host).to.equal(followingHost)
257
258 expect(notification.actorFollow.follower.name).to.equal('peertube')
259 expect(notification.actorFollow.follower.host).to.equal(followerHost)
260 } else {
261 expect(notification).to.satisfy(n => {
262 return n.type !== notificationType || n.actorFollow.following.host !== followingHost
263 })
264 }
265 }
266
267 function emailNotificationFinder (email: object) {
268 const text: string = email['text']
269
270 return text.includes(' automatically followed a new instance') && text.includes(followingHost)
271 }
272
273 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
274}
275
276async function checkCommentMention (options: CheckerBaseParams & {
277 shortUUID: string
278 commentId: number
279 threadId: number
280 byAccountDisplayName: string
281 checkType: CheckerType
282}) {
283 const { shortUUID, commentId, threadId, byAccountDisplayName } = options
284 const notificationType = UserNotificationType.COMMENT_MENTION
285
286 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
287 if (checkType === 'presence') {
288 expect(notification).to.not.be.undefined
289 expect(notification.type).to.equal(notificationType)
290
291 checkComment(notification.comment, commentId, threadId)
292 checkActor(notification.comment.account)
293 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
294
295 checkVideo(notification.comment.video, undefined, shortUUID)
296 } else {
297 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
298 }
299 }
300
301 function emailNotificationFinder (email: object) {
302 const text: string = email['text']
303
304 return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
305 }
306
307 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
308}
309
310let lastEmailCount = 0
311
312async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
313 shortUUID: string
314 commentId: number
315 threadId: number
316 checkType: CheckerType
317}) {
318 const { server, shortUUID, commentId, threadId, checkType, emails } = options
319 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
320
321 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
322 if (checkType === 'presence') {
323 expect(notification).to.not.be.undefined
324 expect(notification.type).to.equal(notificationType)
325
326 checkComment(notification.comment, commentId, threadId)
327 checkActor(notification.comment.account)
328 checkVideo(notification.comment.video, undefined, shortUUID)
329 } else {
330 expect(notification).to.satisfy((n: UserNotification) => {
331 return n === undefined || n.comment === undefined || n.comment.id !== commentId
332 })
333 }
334 }
335
336 const commentUrl = `http://localhost:${server.port}/w/${shortUUID};threadId=${threadId}`
337
338 function emailNotificationFinder (email: object) {
339 return email['text'].indexOf(commentUrl) !== -1
340 }
341
342 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
343
344 if (checkType === 'presence') {
345 // We cannot detect email duplicates, so check we received another email
346 expect(emails).to.have.length.above(lastEmailCount)
347 lastEmailCount = emails.length
348 }
349}
350
351async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
352 shortUUID: string
353 videoName: string
354 checkType: CheckerType
355}) {
356 const { shortUUID, videoName } = options
357 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
358
359 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
360 if (checkType === 'presence') {
361 expect(notification).to.not.be.undefined
362 expect(notification.type).to.equal(notificationType)
363
364 expect(notification.abuse.id).to.be.a('number')
365 checkVideo(notification.abuse.video, videoName, shortUUID)
366 } else {
367 expect(notification).to.satisfy((n: UserNotification) => {
368 return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
369 })
370 }
371 }
372
373 function emailNotificationFinder (email: object) {
374 const text = email['text']
375 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
376 }
377
378 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
379}
380
381async function checkNewAbuseMessage (options: CheckerBaseParams & {
382 abuseId: number
383 message: string
384 toEmail: string
385 checkType: CheckerType
386}) {
387 const { abuseId, message, toEmail } = options
388 const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
389
390 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
391 if (checkType === 'presence') {
392 expect(notification).to.not.be.undefined
393 expect(notification.type).to.equal(notificationType)
394
395 expect(notification.abuse.id).to.equal(abuseId)
396 } else {
397 expect(notification).to.satisfy((n: UserNotification) => {
398 return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId
399 })
400 }
401 }
402
403 function emailNotificationFinder (email: object) {
404 const text = email['text']
405 const to = email['to'].filter(t => t.address === toEmail)
406
407 return text.indexOf(message) !== -1 && to.length !== 0
408 }
409
410 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
411}
412
413async function checkAbuseStateChange (options: CheckerBaseParams & {
414 abuseId: number
415 state: AbuseState
416 checkType: CheckerType
417}) {
418 const { abuseId, state } = options
419 const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
420
421 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
422 if (checkType === 'presence') {
423 expect(notification).to.not.be.undefined
424 expect(notification.type).to.equal(notificationType)
425
426 expect(notification.abuse.id).to.equal(abuseId)
427 expect(notification.abuse.state).to.equal(state)
428 } else {
429 expect(notification).to.satisfy((n: UserNotification) => {
430 return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId
431 })
432 }
433 }
434
435 function emailNotificationFinder (email: object) {
436 const text = email['text']
437
438 const contains = state === AbuseState.ACCEPTED
439 ? ' accepted'
440 : ' rejected'
441
442 return text.indexOf(contains) !== -1
443 }
444
445 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
446}
447
448async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
449 shortUUID: string
450 videoName: string
451 checkType: CheckerType
452}) {
453 const { shortUUID, videoName } = options
454 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
455
456 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
457 if (checkType === 'presence') {
458 expect(notification).to.not.be.undefined
459 expect(notification.type).to.equal(notificationType)
460
461 expect(notification.abuse.id).to.be.a('number')
462 checkVideo(notification.abuse.comment.video, videoName, shortUUID)
463 } else {
464 expect(notification).to.satisfy((n: UserNotification) => {
465 return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
466 })
467 }
468 }
469
470 function emailNotificationFinder (email: object) {
471 const text = email['text']
472 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
473 }
474
475 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
476}
477
478async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
479 displayName: string
480 checkType: CheckerType
481}) {
482 const { displayName } = options
483 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
484
485 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
486 if (checkType === 'presence') {
487 expect(notification).to.not.be.undefined
488 expect(notification.type).to.equal(notificationType)
489
490 expect(notification.abuse.id).to.be.a('number')
491 expect(notification.abuse.account.displayName).to.equal(displayName)
492 } else {
493 expect(notification).to.satisfy((n: UserNotification) => {
494 return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName
495 })
496 }
497 }
498
499 function emailNotificationFinder (email: object) {
500 const text = email['text']
501 return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
502 }
503
504 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
505}
506
507async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
508 shortUUID: string
509 videoName: string
510 checkType: CheckerType
511}) {
512 const { shortUUID, videoName } = options
513 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
514
515 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
516 if (checkType === 'presence') {
517 expect(notification).to.not.be.undefined
518 expect(notification.type).to.equal(notificationType)
519
520 expect(notification.videoBlacklist.video.id).to.be.a('number')
521 checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
522 } else {
523 expect(notification).to.satisfy((n: UserNotification) => {
524 return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID
525 })
526 }
527 }
528
529 function emailNotificationFinder (email: object) {
530 const text = email['text']
531 return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
532 }
533
534 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
535}
536
537async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
538 shortUUID: string
539 videoName: string
540 blacklistType: 'blacklist' | 'unblacklist'
541}) {
542 const { videoName, shortUUID, blacklistType } = options
543 const notificationType = blacklistType === 'blacklist'
544 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
545 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
546
547 function notificationChecker (notification: UserNotification) {
548 expect(notification).to.not.be.undefined
549 expect(notification.type).to.equal(notificationType)
550
551 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
552
553 checkVideo(video, videoName, shortUUID)
554 }
555
556 function emailNotificationFinder (email: object) {
557 const text = email['text']
558 const blacklistText = blacklistType === 'blacklist'
559 ? 'blacklisted'
560 : 'unblacklisted'
561
562 return text.includes(shortUUID) && text.includes(blacklistText)
563 }
564
565 await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
566}
567
568async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
569 latestVersion: string
570 checkType: CheckerType
571}) {
572 const { latestVersion } = options
573 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
574
575 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
576 if (checkType === 'presence') {
577 expect(notification).to.not.be.undefined
578 expect(notification.type).to.equal(notificationType)
579
580 expect(notification.peertube).to.exist
581 expect(notification.peertube.latestVersion).to.equal(latestVersion)
582 } else {
583 expect(notification).to.satisfy((n: UserNotification) => {
584 return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
585 })
586 }
587 }
588
589 function emailNotificationFinder (email: object) {
590 const text = email['text']
591
592 return text.includes(latestVersion)
593 }
594
595 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
596}
597
598async function checkNewPluginVersion (options: CheckerBaseParams & {
599 pluginType: PluginType
600 pluginName: string
601 checkType: CheckerType
602}) {
603 const { pluginName, pluginType } = options
604 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
605
606 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
607 if (checkType === 'presence') {
608 expect(notification).to.not.be.undefined
609 expect(notification.type).to.equal(notificationType)
610
611 expect(notification.plugin.name).to.equal(pluginName)
612 expect(notification.plugin.type).to.equal(pluginType)
613 } else {
614 expect(notification).to.satisfy((n: UserNotification) => {
615 return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
616 })
617 }
618 }
619
620 function emailNotificationFinder (email: object) {
621 const text = email['text']
622
623 return text.includes(pluginName)
624 }
625
626 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
627}
628
629async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
630 const userNotifications: UserNotification[] = []
631 const adminNotifications: UserNotification[] = []
632 const adminNotificationsServer2: UserNotification[] = []
633 const emails: object[] = []
634
635 const port = await MockSmtpServer.Instance.collectEmails(emails)
636
637 const overrideConfig = {
638 smtp: {
639 hostname: 'localhost',
640 port
641 },
642 signup: {
643 limit: 20
644 }
645 }
646 const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
647
648 await setAccessTokensToServers(servers)
649
650 if (serversCount > 1) {
651 await doubleFollow(servers[0], servers[1])
652 }
653
654 const user = { username: 'user_1', password: 'super password' }
655 await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
656 const userAccessToken = await servers[0].login.getAccessToken(user)
657
658 await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
659 await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
660
661 if (serversCount > 1) {
662 await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
663 }
664
665 {
666 const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
667 socket.on('new-notification', n => userNotifications.push(n))
668 }
669 {
670 const socket = servers[0].socketIO.getUserNotificationSocket()
671 socket.on('new-notification', n => adminNotifications.push(n))
672 }
673
674 if (serversCount > 1) {
675 const socket = servers[1].socketIO.getUserNotificationSocket()
676 socket.on('new-notification', n => adminNotificationsServer2.push(n))
677 }
678
679 const { videoChannels } = await servers[0].users.getMyInfo()
680 const channelId = videoChannels[0].id
681
682 return {
683 userNotifications,
684 adminNotifications,
685 adminNotificationsServer2,
686 userAccessToken,
687 emails,
688 servers,
689 channelId
690 }
691}
692
693// ---------------------------------------------------------------------------
694
695export {
696 getAllNotificationsSettings,
697
698 CheckerBaseParams,
699 CheckerType,
700 checkMyVideoImportIsFinished,
701 checkUserRegistered,
702 checkAutoInstanceFollowing,
703 checkVideoIsPublished,
704 checkNewVideoFromSubscription,
705 checkNewActorFollow,
706 checkNewCommentOnMyVideo,
707 checkNewBlacklistOnMyVideo,
708 checkCommentMention,
709 checkNewVideoAbuseForModerators,
710 checkVideoAutoBlacklistForModerators,
711 checkNewAbuseMessage,
712 checkAbuseStateChange,
713 checkNewInstanceFollower,
714 prepareNotificationsTest,
715 checkNewCommentAbuseForModerators,
716 checkNewAccountAbuseForModerators,
717 checkNewPeerTubeVersion,
718 checkNewPluginVersion
719}
720
721// ---------------------------------------------------------------------------
722
723async function checkNotification (options: CheckerBaseParams & {
724 notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
725 emailNotificationFinder: (email: object) => boolean
726 checkType: CheckerType
727}) {
728 const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
729
730 const check = options.check || { web: true, mail: true }
731
732 if (check.web) {
733 const notification = await server.notifications.getLatest({ token: token })
734
735 if (notification || checkType !== 'absence') {
736 notificationChecker(notification, checkType)
737 }
738
739 const socketNotification = socketNotifications.find(n => {
740 try {
741 notificationChecker(n, 'presence')
742 return true
743 } catch {
744 return false
745 }
746 })
747
748 if (checkType === 'presence') {
749 const obj = inspect(socketNotifications, { depth: 5 })
750 expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
751 } else {
752 const obj = inspect(socketNotification, { depth: 5 })
753 expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
754 }
755 }
756
757 if (check.mail) {
758 // Last email
759 const email = emails
760 .slice()
761 .reverse()
762 .find(e => emailNotificationFinder(e))
763
764 if (checkType === 'presence') {
765 const texts = emails.map(e => e.text)
766 expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined
767 } else {
768 expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
769 }
770 }
771}
772
773function checkVideo (video: any, videoName?: string, shortUUID?: string) {
774 if (videoName) {
775 expect(video.name).to.be.a('string')
776 expect(video.name).to.not.be.empty
777 expect(video.name).to.equal(videoName)
778 }
779
780 if (shortUUID) {
781 expect(video.shortUUID).to.be.a('string')
782 expect(video.shortUUID).to.not.be.empty
783 expect(video.shortUUID).to.equal(shortUUID)
784 }
785
786 expect(video.id).to.be.a('number')
787}
788
789function checkActor (actor: any) {
790 expect(actor.displayName).to.be.a('string')
791 expect(actor.displayName).to.not.be.empty
792 expect(actor.host).to.not.be.undefined
793}
794
795function checkComment (comment: any, commentId: number, threadId: number) {
796 expect(comment.id).to.equal(commentId)
797 expect(comment.threadId).to.equal(threadId)
798}
diff --git a/server/tests/shared/playlists.ts b/server/tests/shared/playlists.ts
new file mode 100644
index 000000000..fdd541d20
--- /dev/null
+++ b/server/tests/shared/playlists.ts
@@ -0,0 +1,25 @@
1import { expect } from 'chai'
2import { readdir } from 'fs-extra'
3import { join } from 'path'
4import { root } from '@shared/core-utils'
5
6async function checkPlaylistFilesWereRemoved (
7 playlistUUID: string,
8 internalServerNumber: number,
9 directories = [ 'thumbnails' ]
10) {
11 const testDirectory = 'test' + internalServerNumber
12
13 for (const directory of directories) {
14 const directoryPath = join(root(), testDirectory, directory)
15
16 const files = await readdir(directoryPath)
17 for (const file of files) {
18 expect(file).to.not.contain(playlistUUID)
19 }
20 }
21}
22
23export {
24 checkPlaylistFilesWereRemoved
25}
diff --git a/server/tests/shared/plugins.ts b/server/tests/shared/plugins.ts
new file mode 100644
index 000000000..036fce2ff
--- /dev/null
+++ b/server/tests/shared/plugins.ts
@@ -0,0 +1,18 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { PeerTubeServer } from '@shared/server-commands'
5
6async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
7 const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
8
9 const registeredSettings = body.registeredSettings
10 expect(registeredSettings).to.have.length.at.least(1)
11
12 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
13 expect(adminNameSettings).to.not.be.undefined
14}
15
16export {
17 testHelloWorldRegisteredSettings
18}
diff --git a/server/tests/shared/requests.ts b/server/tests/shared/requests.ts
index 9eb596029..7f1acc0e1 100644
--- a/server/tests/shared/requests.ts
+++ b/server/tests/shared/requests.ts
@@ -1,7 +1,7 @@
1import { doRequest } from '@server/helpers/requests'
2import { activityPubContextify } from '@server/helpers/activitypub' 1import { activityPubContextify } from '@server/helpers/activitypub'
3import { HTTP_SIGNATURE } from '@server/initializers/constants' 2import { buildDigest } from '@server/helpers/peertube-crypto'
4import { buildGlobalHeaders } from '@server/lib/job-queue/handlers/utils/activitypub-http-utils' 3import { doRequest } from '@server/helpers/requests'
4import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants'
5 5
6export function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { 6export function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = { 7 const options = {
@@ -31,7 +31,11 @@ export async function makeFollowRequest (to: { url: string }, by: { url: string,
31 key: by.privateKey, 31 key: by.privateKey,
32 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN 32 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
33 } 33 }
34 const headers = buildGlobalHeaders(body) 34 const headers = {
35 'digest': buildDigest(body),
36 'content-type': 'application/activity+json',
37 'accept': ACTIVITY_PUB.ACCEPT_HEADER
38 }
35 39
36 return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers) 40 return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
37} 41}
diff --git a/server/tests/shared/streaming-playlists.ts b/server/tests/shared/streaming-playlists.ts
new file mode 100644
index 000000000..738dc90e1
--- /dev/null
+++ b/server/tests/shared/streaming-playlists.ts
@@ -0,0 +1,77 @@
1import { expect } from 'chai'
2import { basename } from 'path'
3import { removeFragmentedMP4Ext } from '@shared/core-utils'
4import { sha256 } from '@shared/core-utils/common/crypto'
5import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models'
6import { PeerTubeServer } from '@shared/server-commands'
7
8async function checkSegmentHash (options: {
9 server: PeerTubeServer
10 baseUrlPlaylist: string
11 baseUrlSegment: string
12 resolution: number
13 hlsPlaylist: VideoStreamingPlaylist
14}) {
15 const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options
16 const command = server.streamingPlaylists
17
18 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
19 const videoName = basename(file.fileUrl)
20
21 const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` })
22
23 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
24
25 const length = parseInt(matches[1], 10)
26 const offset = parseInt(matches[2], 10)
27 const range = `${offset}-${offset + length - 1}`
28
29 const segmentBody = await command.getSegment({
30 url: `${baseUrlSegment}/${videoName}`,
31 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
32 range: `bytes=${range}`
33 })
34
35 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
36 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
37}
38
39async function checkLiveSegmentHash (options: {
40 server: PeerTubeServer
41 baseUrlSegment: string
42 videoUUID: string
43 segmentName: string
44 hlsPlaylist: VideoStreamingPlaylist
45}) {
46 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
47 const command = server.streamingPlaylists
48
49 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
50 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
51
52 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
53}
54
55async function checkResolutionsInMasterPlaylist (options: {
56 server: PeerTubeServer
57 playlistUrl: string
58 resolutions: number[]
59}) {
60 const { server, playlistUrl, resolutions } = options
61
62 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
63
64 for (const resolution of resolutions) {
65 const reg = new RegExp(
66 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
67 )
68
69 expect(masterPlaylist).to.match(reg)
70 }
71}
72
73export {
74 checkSegmentHash,
75 checkLiveSegmentHash,
76 checkResolutionsInMasterPlaylist
77}
diff --git a/server/tests/shared/tests.ts b/server/tests/shared/tests.ts
new file mode 100644
index 000000000..3abaf833d
--- /dev/null
+++ b/server/tests/shared/tests.ts
@@ -0,0 +1,37 @@
1const FIXTURE_URLS = {
2 peertube_long: 'https://peertube2.cpy.re/videos/watch/122d093a-1ede-43bd-bd34-59d2931ffc5e',
3 peertube_short: 'https://peertube2.cpy.re/w/3fbif9S3WmtTP8gGsC5HBd',
4
5 youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM',
6
7 /**
8 * The video is used to check format-selection correctness wrt. HDR,
9 * which brings its own set of oddities outside of a MediaSource.
10 *
11 * The video needs to have the following format_ids:
12 * (which you can check by using `youtube-dl <url> -F`):
13 * - (webm vp9)
14 * - (mp4 avc1)
15 * - (webm vp9.2 HDR)
16 */
17 youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4',
18
19 // eslint-disable-next-line max-len
20 magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4',
21
22 badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
23 goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
24 goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4',
25
26 file4K: 'https://download.cpy.re/peertube/4k_file.txt'
27}
28
29function buildRequestStub (): any {
30 return { }
31}
32
33export {
34 FIXTURE_URLS,
35
36 buildRequestStub
37}
diff --git a/server/tests/shared/tracker.ts b/server/tests/shared/tracker.ts
new file mode 100644
index 000000000..699895d5f
--- /dev/null
+++ b/server/tests/shared/tracker.ts
@@ -0,0 +1,27 @@
1import { expect } from 'chai'
2import { sha1 } from '@shared/core-utils'
3import { makeGetRequest } from '@shared/server-commands'
4
5async function hlsInfohashExist (serverUrl: string, masterPlaylistUrl: string, fileNumber: number) {
6 const path = '/tracker/announce'
7
8 const infohash = sha1(`2${masterPlaylistUrl}+V${fileNumber}`)
9
10 // From bittorrent-tracker
11 const infohashBinary = escape(Buffer.from(infohash, 'hex').toString('binary')).replace(/[@*/+]/g, function (char) {
12 return '%' + char.charCodeAt(0).toString(16).toUpperCase()
13 })
14
15 const res = await makeGetRequest({
16 url: serverUrl,
17 path,
18 rawQuery: `peer_id=-WW0105-NkvYO/egUAr4&info_hash=${infohashBinary}&port=42100`,
19 expectedStatus: 200
20 })
21
22 expect(res.text).to.not.contain('failure')
23}
24
25export {
26 hlsInfohashExist
27}
diff --git a/server/tests/shared/video.ts b/server/tests/shared/videos.ts
index cf923d4cd..6be094f2b 100644
--- a/server/tests/shared/video.ts
+++ b/server/tests/shared/videos.ts
@@ -1,12 +1,17 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2import { dateIsValid, makeRawRequest, PeerTubeServer, testImage, webtorrentAdd } from '@shared/server-commands' 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { basename, join } from 'path'
4import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '@server/initializers/constants' 6import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '@server/initializers/constants'
5import { getLowercaseExtension, uuidRegex } from '@shared/core-utils' 7import { getLowercaseExtension, uuidRegex } from '@shared/core-utils'
8import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models'
9import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs, webtorrentAdd } from '@shared/server-commands'
10import { dateIsValid, testImage } from './checks'
6 11
7loadLanguages() 12loadLanguages()
8 13
9export async function completeVideoCheck ( 14async function completeVideoCheck (
10 server: PeerTubeServer, 15 server: PeerTubeServer,
11 video: any, 16 video: any,
12 attributes: { 17 attributes: {
@@ -148,3 +153,99 @@ export async function completeVideoCheck (
148 await testImage(server.url, attributes.previewfile, videoDetails.previewPath) 153 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
149 } 154 }
150} 155}
156
157async function checkVideoFilesWereRemoved (options: {
158 server: PeerTubeServer
159 video: VideoDetails
160 captions?: VideoCaption[]
161 onlyVideoFiles?: boolean // default false
162}) {
163 const { video, server, captions = [], onlyVideoFiles = false } = options
164
165 const webtorrentFiles = video.files || []
166 const hlsFiles = video.streamingPlaylists[0]?.files || []
167
168 const thumbnailName = basename(video.thumbnailPath)
169 const previewName = basename(video.previewPath)
170
171 const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
172
173 const captionNames = captions.map(c => basename(c.captionPath))
174
175 const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl))
176 const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
177
178 let directories: { [ directory: string ]: string[] } = {
179 videos: webtorrentFilenames,
180 redundancy: webtorrentFilenames,
181 [join('playlists', 'hls')]: hlsFilenames,
182 [join('redundancy', 'hls')]: hlsFilenames
183 }
184
185 if (onlyVideoFiles !== true) {
186 directories = {
187 ...directories,
188
189 thumbnails: [ thumbnailName ],
190 previews: [ previewName ],
191 torrents: torrentNames,
192 captions: captionNames
193 }
194 }
195
196 for (const directory of Object.keys(directories)) {
197 const directoryPath = server.servers.buildDirectory(directory)
198
199 const directoryExists = await pathExists(directoryPath)
200 if (directoryExists === false) continue
201
202 const existingFiles = await readdir(directoryPath)
203 for (const existingFile of existingFiles) {
204 for (const shouldNotExist of directories[directory]) {
205 expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
206 }
207 }
208 }
209}
210
211async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
212 for (const server of servers) {
213 server.store.videoDetails = await server.videos.get({ id: uuid })
214 }
215}
216
217function checkUploadVideoParam (
218 server: PeerTubeServer,
219 token: string,
220 attributes: Partial<VideoEdit>,
221 expectedStatus = HttpStatusCode.OK_200,
222 mode: 'legacy' | 'resumable' = 'legacy'
223) {
224 return mode === 'legacy'
225 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
226 : server.videos.buildResumeUpload({ token, attributes, expectedStatus })
227}
228
229// serverNumber starts from 1
230async function uploadRandomVideoOnServers (
231 servers: PeerTubeServer[],
232 serverNumber: number,
233 additionalParams?: VideoEdit & { prefixName?: string }
234) {
235 const server = servers.find(s => s.serverNumber === serverNumber)
236 const res = await server.videos.randomUpload({ wait: false, additionalParams })
237
238 await waitJobs(servers)
239
240 return res
241}
242
243// ---------------------------------------------------------------------------
244
245export {
246 completeVideoCheck,
247 checkUploadVideoParam,
248 uploadRandomVideoOnServers,
249 checkVideoFilesWereRemoved,
250 saveVideoInServers
251}