aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/tests/shared
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/tests/shared')
-rw-r--r--server/tests/shared/actors.ts69
-rw-r--r--server/tests/shared/captions.ts21
-rw-r--r--server/tests/shared/checks.ts174
-rw-r--r--server/tests/shared/directories.ts43
-rw-r--r--server/tests/shared/generate.ts74
-rw-r--r--server/tests/shared/index.ts19
-rw-r--r--server/tests/shared/live.ts185
-rw-r--r--server/tests/shared/mock-servers/index.ts8
-rw-r--r--server/tests/shared/mock-servers/mock-429.ts33
-rw-r--r--server/tests/shared/mock-servers/mock-email.ts61
-rw-r--r--server/tests/shared/mock-servers/mock-http.ts23
-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.ts889
-rw-r--r--server/tests/shared/peertube-runner-process.ts98
-rw-r--r--server/tests/shared/plugins.ts18
-rw-r--r--server/tests/shared/requests.ts12
-rw-r--r--server/tests/shared/sql-command.ts150
-rw-r--r--server/tests/shared/streaming-playlists.ts296
-rw-r--r--server/tests/shared/tests.ts40
-rw-r--r--server/tests/shared/tracker.ts27
-rw-r--r--server/tests/shared/video-playlists.ts22
-rw-r--r--server/tests/shared/videos.ts315
-rw-r--r--server/tests/shared/views.ts93
-rw-r--r--server/tests/shared/webtorrent.ts58
29 files changed, 0 insertions, 2942 deletions
diff --git a/server/tests/shared/actors.ts b/server/tests/shared/actors.ts
deleted file mode 100644
index 41fd72e89..000000000
--- a/server/tests/shared/actors.ts
+++ /dev/null
@@ -1,69 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { Account, VideoChannel } from '@shared/models'
6import { PeerTubeServer } from '@shared/server-commands'
7
8async function expectChannelsFollows (options: {
9 server: PeerTubeServer
10 handle: string
11 followers: number
12 following: number
13}) {
14 const { server } = options
15 const { data } = await server.channels.list()
16
17 return expectActorFollow({ ...options, data })
18}
19
20async function expectAccountFollows (options: {
21 server: PeerTubeServer
22 handle: string
23 followers: number
24 following: number
25}) {
26 const { server } = options
27 const { data } = await server.accounts.list()
28
29 return expectActorFollow({ ...options, data })
30}
31
32async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) {
33 for (const directory of [ 'avatars' ]) {
34 const directoryPath = server.getDirectoryPath(directory)
35
36 const directoryExists = await pathExists(directoryPath)
37 expect(directoryExists).to.be.true
38
39 const files = await readdir(directoryPath)
40 for (const file of files) {
41 expect(file).to.not.contain(filename)
42 }
43 }
44}
45
46export {
47 expectAccountFollows,
48 expectChannelsFollows,
49 checkActorFilesWereRemoved
50}
51
52// ---------------------------------------------------------------------------
53
54function expectActorFollow (options: {
55 server: PeerTubeServer
56 data: (Account | VideoChannel)[]
57 handle: string
58 followers: number
59 following: number
60}) {
61 const { server, data, handle, followers, following } = options
62
63 const actor = data.find(a => a.name + '@' + a.host === handle)
64 const message = `${handle} on ${server.url}`
65
66 expect(actor, message).to.exist
67 expect(actor.followersCount).to.equal(followers, message)
68 expect(actor.followingCount).to.equal(following, message)
69}
diff --git a/server/tests/shared/captions.ts b/server/tests/shared/captions.ts
deleted file mode 100644
index 35e722408..000000000
--- a/server/tests/shared/captions.ts
+++ /dev/null
@@ -1,21 +0,0 @@
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
deleted file mode 100644
index 90179c6ac..000000000
--- a/server/tests/shared/checks.ts
+++ /dev/null
@@ -1,174 +0,0 @@
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 JPEG from 'jpeg-js'
6import { join } from 'path'
7import pixelmatch from 'pixelmatch'
8import { PNG } from 'pngjs'
9import { root } from '@shared/core-utils'
10import { HttpStatusCode } from '@shared/models'
11import { makeGetRequest, PeerTubeServer } from '@shared/server-commands'
12
13// Default interval -> 5 minutes
14function dateIsValid (dateString: string | Date, interval = 300000) {
15 const dateToCheck = new Date(dateString)
16 const now = new Date()
17
18 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
19}
20
21function expectStartWith (str: string, start: string) {
22 expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true
23}
24
25function expectNotStartWith (str: string, start: string) {
26 expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.false
27}
28
29function expectEndWith (str: string, end: string) {
30 expect(str.endsWith(end), `${str} does not end with ${end}`).to.be.true
31}
32
33// ---------------------------------------------------------------------------
34
35async function expectLogDoesNotContain (server: PeerTubeServer, str: string) {
36 const content = await server.servers.getLogContent()
37
38 expect(content.toString()).to.not.contain(str)
39}
40
41async function expectLogContain (server: PeerTubeServer, str: string) {
42 const content = await server.servers.getLogContent()
43
44 expect(content.toString()).to.contain(str)
45}
46
47async function testImageSize (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') {
48 const res = await makeGetRequest({
49 url,
50 path: imageHTTPPath,
51 expectedStatus: HttpStatusCode.OK_200
52 })
53
54 const body = res.body
55
56 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
57 const minLength = data.length - ((40 * data.length) / 100)
58 const maxLength = data.length + ((40 * data.length) / 100)
59
60 expect(body.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
61 expect(body.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
62}
63
64async function testImageGeneratedByFFmpeg (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') {
65 if (process.env.ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS !== 'true') {
66 console.log(
67 'Pixel comparison of image generated by ffmpeg is disabled. ' +
68 'You can enable it using `ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS=true env variable')
69 }
70
71 return testImage(url, imageName, imageHTTPPath, extension)
72}
73
74async function testImage (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') {
75 const res = await makeGetRequest({
76 url,
77 path: imageHTTPPath,
78 expectedStatus: HttpStatusCode.OK_200
79 })
80
81 const body = res.body
82 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
83
84 const img1 = imageHTTPPath.endsWith('.png')
85 ? PNG.sync.read(body)
86 : JPEG.decode(body)
87
88 const img2 = extension === '.png'
89 ? PNG.sync.read(data)
90 : JPEG.decode(data)
91
92 const result = pixelmatch(img1.data, img2.data, null, img1.width, img1.height, { threshold: 0.1 })
93
94 expect(result).to.equal(0, `${imageHTTPPath} image is not the same as ${imageName}${extension}`)
95}
96
97async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
98 const base = server.servers.buildDirectory(directory)
99
100 expect(await pathExists(join(base, filePath))).to.equal(exist)
101}
102
103// ---------------------------------------------------------------------------
104
105function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
106 return makeGetRequest({
107 url,
108 path,
109 token,
110 query: { ...query, start: 'hello' },
111 expectedStatus: HttpStatusCode.BAD_REQUEST_400
112 })
113}
114
115async function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
116 await makeGetRequest({
117 url,
118 path,
119 token,
120 query: { ...query, count: 'hello' },
121 expectedStatus: HttpStatusCode.BAD_REQUEST_400
122 })
123
124 await makeGetRequest({
125 url,
126 path,
127 token,
128 query: { ...query, count: 2000 },
129 expectedStatus: HttpStatusCode.BAD_REQUEST_400
130 })
131}
132
133function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
134 return makeGetRequest({
135 url,
136 path,
137 token,
138 query: { ...query, sort: 'hello' },
139 expectedStatus: HttpStatusCode.BAD_REQUEST_400
140 })
141}
142
143// ---------------------------------------------------------------------------
144
145async function checkVideoDuration (server: PeerTubeServer, videoUUID: string, duration: number) {
146 const video = await server.videos.get({ id: videoUUID })
147
148 expect(video.duration).to.be.approximately(duration, 1)
149
150 for (const file of video.files) {
151 const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
152
153 for (const stream of metadata.streams) {
154 expect(Math.round(stream.duration)).to.be.approximately(duration, 1)
155 }
156 }
157}
158
159export {
160 dateIsValid,
161 testImageGeneratedByFFmpeg,
162 testImageSize,
163 testImage,
164 expectLogDoesNotContain,
165 testFileExistsOrNot,
166 expectStartWith,
167 expectNotStartWith,
168 expectEndWith,
169 checkBadStartPagination,
170 checkBadCountPagination,
171 checkBadSortPagination,
172 checkVideoDuration,
173 expectLogContain
174}
diff --git a/server/tests/shared/directories.ts b/server/tests/shared/directories.ts
deleted file mode 100644
index 5ad12d78a..000000000
--- a/server/tests/shared/directories.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { homedir } from 'os'
6import { join } from 'path'
7import { PeerTubeServer } from '@shared/server-commands'
8import { PeerTubeRunnerProcess } from './peertube-runner-process'
9
10export async function checkTmpIsEmpty (server: PeerTubeServer) {
11 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
12
13 if (await pathExists(server.getDirectoryPath('tmp/hls'))) {
14 await checkDirectoryIsEmpty(server, 'tmp/hls')
15 }
16}
17
18export async function checkPersistentTmpIsEmpty (server: PeerTubeServer) {
19 await checkDirectoryIsEmpty(server, 'tmp-persistent')
20}
21
22export async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
23 const directoryPath = server.getDirectoryPath(directory)
24
25 const directoryExists = await pathExists(directoryPath)
26 expect(directoryExists).to.be.true
27
28 const files = await readdir(directoryPath)
29 const filtered = files.filter(f => exceptions.includes(f) === false)
30
31 expect(filtered).to.have.lengthOf(0)
32}
33
34export async function checkPeerTubeRunnerCacheIsEmpty (runner: PeerTubeRunnerProcess) {
35 const directoryPath = join(homedir(), '.cache', 'peertube-runner-nodejs', runner.getId(), 'transcoding')
36
37 const directoryExists = await pathExists(directoryPath)
38 expect(directoryExists).to.be.true
39
40 const files = await readdir(directoryPath)
41
42 expect(files, 'Directory content: ' + files.join(', ')).to.have.lengthOf(0)
43}
diff --git a/server/tests/shared/generate.ts b/server/tests/shared/generate.ts
deleted file mode 100644
index 3788b049f..000000000
--- a/server/tests/shared/generate.ts
+++ /dev/null
@@ -1,74 +0,0 @@
1import { expect } from 'chai'
2import ffmpeg from 'fluent-ffmpeg'
3import { ensureDir, pathExists } from 'fs-extra'
4import { dirname } from 'path'
5import { buildAbsoluteFixturePath, getMaxTheoreticalBitrate } from '@shared/core-utils'
6import { getVideoStreamBitrate, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg'
7
8async function ensureHasTooBigBitrate (fixturePath: string) {
9 const bitrate = await getVideoStreamBitrate(fixturePath)
10 const dataResolution = await getVideoStreamDimensionsInfo(fixturePath)
11 const fps = await getVideoStreamFPS(fixturePath)
12
13 const maxBitrate = getMaxTheoreticalBitrate({ ...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
deleted file mode 100644
index eda24adb5..000000000
--- a/server/tests/shared/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
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 './peertube-runner-process'
10export * from './video-playlists'
11export * from './plugins'
12export * from './requests'
13export * from './sql-command'
14export * from './streaming-playlists'
15export * from './tests'
16export * from './tracker'
17export * from './videos'
18export * from './views'
19export * from './webtorrent'
diff --git a/server/tests/shared/live.ts b/server/tests/shared/live.ts
deleted file mode 100644
index 9d8c1d941..000000000
--- a/server/tests/shared/live.ts
+++ /dev/null
@@ -1,185 +0,0 @@
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 { sha1 } from '@shared/extra-utils'
7import { LiveVideo, VideoStreamingPlaylistType } from '@shared/models'
8import { ObjectStorageCommand, PeerTubeServer } from '@shared/server-commands'
9import { SQLCommand } from './sql-command'
10import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist } from './streaming-playlists'
11
12async function checkLiveCleanup (options: {
13 server: PeerTubeServer
14 videoUUID: string
15 permanent: boolean
16 savedResolutions?: number[]
17}) {
18 const { server, videoUUID, permanent, savedResolutions = [] } = options
19
20 const basePath = server.servers.buildDirectory('streaming-playlists')
21 const hlsPath = join(basePath, 'hls', videoUUID)
22
23 if (permanent) {
24 if (!await pathExists(hlsPath)) return
25
26 const files = await readdir(hlsPath)
27 expect(files).to.have.lengthOf(0)
28 return
29 }
30
31 if (savedResolutions.length === 0) {
32 return checkUnsavedLiveCleanup(server, videoUUID, hlsPath)
33 }
34
35 return checkSavedLiveCleanup(hlsPath, savedResolutions)
36}
37
38// ---------------------------------------------------------------------------
39
40async function testLiveVideoResolutions (options: {
41 sqlCommand: SQLCommand
42 originServer: PeerTubeServer
43
44 servers: PeerTubeServer[]
45 liveVideoId: string
46 resolutions: number[]
47 transcoded: boolean
48
49 objectStorage?: ObjectStorageCommand
50 objectStorageBaseUrl?: string
51}) {
52 const {
53 originServer,
54 sqlCommand,
55 servers,
56 liveVideoId,
57 resolutions,
58 transcoded,
59 objectStorage,
60 objectStorageBaseUrl = objectStorage?.getMockPlaylistBaseUrl()
61 } = options
62
63 for (const server of servers) {
64 const { data } = await server.videos.list()
65 expect(data.find(v => v.uuid === liveVideoId)).to.exist
66
67 const video = await server.videos.get({ id: liveVideoId })
68 expect(video.streamingPlaylists).to.have.lengthOf(1)
69
70 const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
71 expect(hlsPlaylist).to.exist
72 expect(hlsPlaylist.files).to.have.lengthOf(0) // Only fragmented mp4 files are displayed
73
74 await checkResolutionsInMasterPlaylist({
75 server,
76 playlistUrl: hlsPlaylist.playlistUrl,
77 resolutions,
78 transcoded,
79 withRetry: !!objectStorage
80 })
81
82 if (objectStorage) {
83 expect(hlsPlaylist.playlistUrl).to.contain(objectStorageBaseUrl)
84 }
85
86 for (let i = 0; i < resolutions.length; i++) {
87 const segmentNum = 3
88 const segmentName = `${i}-00000${segmentNum}.ts`
89 await originServer.live.waitUntilSegmentGeneration({
90 server: originServer,
91 videoUUID: video.uuid,
92 playlistNumber: i,
93 segment: segmentNum,
94 objectStorage,
95 objectStorageBaseUrl
96 })
97
98 const baseUrl = objectStorage
99 ? join(objectStorageBaseUrl, 'hls')
100 : originServer.url + '/static/streaming-playlists/hls'
101
102 if (objectStorage) {
103 expect(hlsPlaylist.segmentsSha256Url).to.contain(objectStorageBaseUrl)
104 }
105
106 const subPlaylist = await originServer.streamingPlaylists.get({
107 url: `${baseUrl}/${video.uuid}/${i}.m3u8`,
108 withRetry: !!objectStorage // With object storage, the request may fail because of inconsistent data in S3
109 })
110
111 expect(subPlaylist).to.contain(segmentName)
112
113 await checkLiveSegmentHash({
114 server,
115 baseUrlSegment: baseUrl,
116 videoUUID: video.uuid,
117 segmentName,
118 hlsPlaylist,
119 withRetry: !!objectStorage // With object storage, the request may fail because of inconsistent data in S3
120 })
121
122 if (originServer.internalServerNumber === server.internalServerNumber) {
123 const infohash = sha1(`${2 + hlsPlaylist.playlistUrl}+V${i}`)
124 const dbInfohashes = await sqlCommand.getPlaylistInfohash(hlsPlaylist.id)
125
126 expect(dbInfohashes).to.include(infohash)
127 }
128 }
129 }
130}
131
132// ---------------------------------------------------------------------------
133
134export {
135 checkLiveCleanup,
136 testLiveVideoResolutions
137}
138
139// ---------------------------------------------------------------------------
140
141async function checkSavedLiveCleanup (hlsPath: string, savedResolutions: number[] = []) {
142 const files = await readdir(hlsPath)
143
144 // fragmented file and playlist per resolution + master playlist + segments sha256 json file
145 expect(files, `Directory content: ${files.join(', ')}`).to.have.lengthOf(savedResolutions.length * 2 + 2)
146
147 for (const resolution of savedResolutions) {
148 const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
149 expect(fragmentedFile).to.exist
150
151 const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
152 expect(playlistFile).to.exist
153 }
154
155 const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
156 expect(masterPlaylistFile).to.exist
157
158 const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
159 expect(shaFile).to.exist
160}
161
162async function checkUnsavedLiveCleanup (server: PeerTubeServer, videoUUID: string, hlsPath: string) {
163 let live: LiveVideo
164
165 try {
166 live = await server.live.get({ videoId: videoUUID })
167 } catch {}
168
169 if (live?.permanentLive) {
170 expect(await pathExists(hlsPath)).to.be.true
171
172 const hlsFiles = await readdir(hlsPath)
173 expect(hlsFiles).to.have.lengthOf(1) // Only replays directory
174
175 const replayDir = join(hlsPath, 'replay')
176 expect(await pathExists(replayDir)).to.be.true
177
178 const replayFiles = await readdir(join(hlsPath, 'replay'))
179 expect(replayFiles).to.have.lengthOf(0)
180
181 return
182 }
183
184 expect(await pathExists(hlsPath)).to.be.false
185}
diff --git a/server/tests/shared/mock-servers/index.ts b/server/tests/shared/mock-servers/index.ts
deleted file mode 100644
index 1fa983116..000000000
--- a/server/tests/shared/mock-servers/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
1export * from './mock-429'
2export * from './mock-email'
3export * from './mock-http'
4export * from './mock-instances-index'
5export * from './mock-joinpeertube-versions'
6export * from './mock-object-storage'
7export * from './mock-plugin-blocklist'
8export * from './mock-proxy'
diff --git a/server/tests/shared/mock-servers/mock-429.ts b/server/tests/shared/mock-servers/mock-429.ts
deleted file mode 100644
index 1fc20b079..000000000
--- a/server/tests/shared/mock-servers/mock-429.ts
+++ /dev/null
@@ -1,33 +0,0 @@
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
deleted file mode 100644
index 6eda2dfda..000000000
--- a/server/tests/shared/mock-servers/mock-email.ts
+++ /dev/null
@@ -1,61 +0,0 @@
1import MailDev from '@peertube/maildev'
2import { parallelTests, randomInt } from '@shared/core-utils'
3
4class MockSmtpServer {
5
6 private static instance: MockSmtpServer
7 private started = false
8 private maildev: any
9 private emails: object[]
10
11 private constructor () { }
12
13 collectEmails (emailsCollection: object[]) {
14 return new Promise<number>((res, rej) => {
15 const port = parallelTests() ? randomInt(1025, 2000) : 1025
16 this.emails = emailsCollection
17
18 if (this.started) {
19 return res(undefined)
20 }
21
22 this.maildev = new MailDev({
23 ip: '127.0.0.1',
24 smtp: port,
25 disableWeb: true,
26 silent: true
27 })
28
29 this.maildev.on('new', email => {
30 this.emails.push(email)
31 })
32
33 this.maildev.listen(err => {
34 if (err) return rej(err)
35
36 this.started = true
37
38 return res(port)
39 })
40 })
41 }
42
43 kill () {
44 if (!this.maildev) return
45
46 this.maildev.close()
47
48 this.maildev = null
49 MockSmtpServer.instance = null
50 }
51
52 static get Instance () {
53 return this.instance || (this.instance = new this())
54 }
55}
56
57// ---------------------------------------------------------------------------
58
59export {
60 MockSmtpServer
61}
diff --git a/server/tests/shared/mock-servers/mock-http.ts b/server/tests/shared/mock-servers/mock-http.ts
deleted file mode 100644
index b7a019e07..000000000
--- a/server/tests/shared/mock-servers/mock-http.ts
+++ /dev/null
@@ -1,23 +0,0 @@
1import express from 'express'
2import { Server } from 'http'
3import { getPort, randomListen, terminateServer } from './shared'
4
5export class MockHTTP {
6 private server: Server
7
8 async initialize () {
9 const app = express()
10
11 app.get('/*', (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 return res.sendStatus(200)
13 })
14
15 this.server = await randomListen(app)
16
17 return getPort(this.server)
18 }
19
20 terminate () {
21 return terminateServer(this.server)
22 }
23}
diff --git a/server/tests/shared/mock-servers/mock-instances-index.ts b/server/tests/shared/mock-servers/mock-instances-index.ts
deleted file mode 100644
index 598b007f1..000000000
--- a/server/tests/shared/mock-servers/mock-instances-index.ts
+++ /dev/null
@@ -1,46 +0,0 @@
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
deleted file mode 100644
index 502f4e2f5..000000000
--- a/server/tests/shared/mock-servers/mock-joinpeertube-versions.ts
+++ /dev/null
@@ -1,34 +0,0 @@
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
deleted file mode 100644
index ae76c4f3f..000000000
--- a/server/tests/shared/mock-servers/mock-object-storage.ts
+++ /dev/null
@@ -1,41 +0,0 @@
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 MockObjectStorageProxy {
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.getMockEndpointHost()}/${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
deleted file mode 100644
index 5d6e01816..000000000
--- a/server/tests/shared/mock-servers/mock-plugin-blocklist.ts
+++ /dev/null
@@ -1,36 +0,0 @@
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
deleted file mode 100644
index e741d6735..000000000
--- a/server/tests/shared/mock-servers/mock-proxy.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import { createServer, Server } from 'http'
2import { createProxy } 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 = createProxy(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
deleted file mode 100644
index 235642439..000000000
--- a/server/tests/shared/mock-servers/shared.ts
+++ /dev/null
@@ -1,33 +0,0 @@
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
deleted file mode 100644
index 6c0688d5a..000000000
--- a/server/tests/shared/notifications.ts
+++ /dev/null
@@ -1,889 +0,0 @@
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 {
14 ConfigCommand,
15 createMultipleServers,
16 doubleFollow,
17 PeerTubeServer,
18 setAccessTokensToServers,
19 setDefaultAccountAvatar,
20 setDefaultChannelAvatar,
21 setDefaultVideoChannel
22} from '@shared/server-commands'
23import { MockSmtpServer } from './mock-servers'
24
25type CheckerBaseParams = {
26 server: PeerTubeServer
27 emails: any[]
28 socketNotifications: UserNotification[]
29 token: string
30 check?: { web: boolean, mail: boolean }
31}
32
33type CheckerType = 'presence' | 'absence'
34
35function getAllNotificationsSettings (): UserNotificationSetting {
36 return {
37 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
38 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
39 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
40 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
41 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
42 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
43 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
44 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
45 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
46 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
47 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
48 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
49 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
50 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
51 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
52 myVideoStudioEditionFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
53 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
54 }
55}
56
57async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
58 videoName: string
59 shortUUID: string
60 checkType: CheckerType
61}) {
62 const { videoName, shortUUID } = options
63 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
64
65 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
66 if (checkType === 'presence') {
67 expect(notification).to.not.be.undefined
68 expect(notification.type).to.equal(notificationType)
69
70 checkVideo(notification.video, videoName, shortUUID)
71 checkActor(notification.video.channel)
72 } else {
73 expect(notification).to.satisfy((n: UserNotification) => {
74 return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
75 })
76 }
77 }
78
79 function emailNotificationFinder (email: object) {
80 const text = email['text']
81 return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
82 }
83
84 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
85}
86
87async function checkVideoIsPublished (options: CheckerBaseParams & {
88 videoName: string
89 shortUUID: string
90 checkType: CheckerType
91}) {
92 const { videoName, shortUUID } = options
93 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
94
95 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
96 if (checkType === 'presence') {
97 expect(notification).to.not.be.undefined
98 expect(notification.type).to.equal(notificationType)
99
100 checkVideo(notification.video, videoName, shortUUID)
101 checkActor(notification.video.channel)
102 } else {
103 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
104 }
105 }
106
107 function emailNotificationFinder (email: object) {
108 const text: string = email['text']
109 return text.includes(shortUUID) && text.includes('Your video')
110 }
111
112 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
113}
114
115async function checkVideoStudioEditionIsFinished (options: CheckerBaseParams & {
116 videoName: string
117 shortUUID: string
118 checkType: CheckerType
119}) {
120 const { videoName, shortUUID } = options
121 const notificationType = UserNotificationType.MY_VIDEO_STUDIO_EDITION_FINISHED
122
123 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
124 if (checkType === 'presence') {
125 expect(notification).to.not.be.undefined
126 expect(notification.type).to.equal(notificationType)
127
128 checkVideo(notification.video, videoName, shortUUID)
129 checkActor(notification.video.channel)
130 } else {
131 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
132 }
133 }
134
135 function emailNotificationFinder (email: object) {
136 const text: string = email['text']
137 return text.includes(shortUUID) && text.includes('Edition of your video')
138 }
139
140 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
141}
142
143async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
144 videoName: string
145 shortUUID: string
146 url: string
147 success: boolean
148 checkType: CheckerType
149}) {
150 const { videoName, shortUUID, url, success } = options
151
152 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
153
154 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
155 if (checkType === 'presence') {
156 expect(notification).to.not.be.undefined
157 expect(notification.type).to.equal(notificationType)
158
159 expect(notification.videoImport.targetUrl).to.equal(url)
160
161 if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
162 } else {
163 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
164 }
165 }
166
167 function emailNotificationFinder (email: object) {
168 const text: string = email['text']
169 const toFind = success ? ' finished' : ' error'
170
171 return text.includes(url) && text.includes(toFind)
172 }
173
174 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
175}
176
177// ---------------------------------------------------------------------------
178
179async function checkUserRegistered (options: CheckerBaseParams & {
180 username: string
181 checkType: CheckerType
182}) {
183 const { username } = options
184 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
185
186 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
187 if (checkType === 'presence') {
188 expect(notification).to.not.be.undefined
189 expect(notification.type).to.equal(notificationType)
190
191 checkActor(notification.account, { withAvatar: false })
192 expect(notification.account.name).to.equal(username)
193 } else {
194 expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
195 }
196 }
197
198 function emailNotificationFinder (email: object) {
199 const text: string = email['text']
200
201 return text.includes(' registered.') && text.includes(username)
202 }
203
204 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
205}
206
207async function checkRegistrationRequest (options: CheckerBaseParams & {
208 username: string
209 registrationReason: string
210 checkType: CheckerType
211}) {
212 const { username, registrationReason } = options
213 const notificationType = UserNotificationType.NEW_USER_REGISTRATION_REQUEST
214
215 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
216 if (checkType === 'presence') {
217 expect(notification).to.not.be.undefined
218 expect(notification.type).to.equal(notificationType)
219
220 expect(notification.registration.username).to.equal(username)
221 } else {
222 expect(notification).to.satisfy(n => n.type !== notificationType || n.registration.username !== username)
223 }
224 }
225
226 function emailNotificationFinder (email: object) {
227 const text: string = email['text']
228
229 return text.includes(' wants to register ') && text.includes(username) && text.includes(registrationReason)
230 }
231
232 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
233}
234
235// ---------------------------------------------------------------------------
236
237async function checkNewActorFollow (options: CheckerBaseParams & {
238 followType: 'channel' | 'account'
239 followerName: string
240 followerDisplayName: string
241 followingDisplayName: string
242 checkType: CheckerType
243}) {
244 const { followType, followerName, followerDisplayName, followingDisplayName } = options
245 const notificationType = UserNotificationType.NEW_FOLLOW
246
247 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
248 if (checkType === 'presence') {
249 expect(notification).to.not.be.undefined
250 expect(notification.type).to.equal(notificationType)
251
252 checkActor(notification.actorFollow.follower)
253 expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
254 expect(notification.actorFollow.follower.name).to.equal(followerName)
255 expect(notification.actorFollow.follower.host).to.not.be.undefined
256
257 const following = notification.actorFollow.following
258 expect(following.displayName).to.equal(followingDisplayName)
259 expect(following.type).to.equal(followType)
260 } else {
261 expect(notification).to.satisfy(n => {
262 return n.type !== notificationType ||
263 (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
264 })
265 }
266 }
267
268 function emailNotificationFinder (email: object) {
269 const text: string = email['text']
270
271 return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
272 }
273
274 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
275}
276
277async function checkNewInstanceFollower (options: CheckerBaseParams & {
278 followerHost: string
279 checkType: CheckerType
280}) {
281 const { followerHost } = options
282 const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
283
284 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
285 if (checkType === 'presence') {
286 expect(notification).to.not.be.undefined
287 expect(notification.type).to.equal(notificationType)
288
289 checkActor(notification.actorFollow.follower, { withAvatar: false })
290 expect(notification.actorFollow.follower.name).to.equal('peertube')
291 expect(notification.actorFollow.follower.host).to.equal(followerHost)
292
293 expect(notification.actorFollow.following.name).to.equal('peertube')
294 } else {
295 expect(notification).to.satisfy(n => {
296 return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
297 })
298 }
299 }
300
301 function emailNotificationFinder (email: object) {
302 const text: string = email['text']
303
304 return text.includes('instance has a new follower') && text.includes(followerHost)
305 }
306
307 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
308}
309
310async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
311 followerHost: string
312 followingHost: string
313 checkType: CheckerType
314}) {
315 const { followerHost, followingHost } = options
316 const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
317
318 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
319 if (checkType === 'presence') {
320 expect(notification).to.not.be.undefined
321 expect(notification.type).to.equal(notificationType)
322
323 const following = notification.actorFollow.following
324
325 checkActor(following, { withAvatar: false })
326 expect(following.name).to.equal('peertube')
327 expect(following.host).to.equal(followingHost)
328
329 expect(notification.actorFollow.follower.name).to.equal('peertube')
330 expect(notification.actorFollow.follower.host).to.equal(followerHost)
331 } else {
332 expect(notification).to.satisfy(n => {
333 return n.type !== notificationType || n.actorFollow.following.host !== followingHost
334 })
335 }
336 }
337
338 function emailNotificationFinder (email: object) {
339 const text: string = email['text']
340
341 return text.includes(' automatically followed a new instance') && text.includes(followingHost)
342 }
343
344 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
345}
346
347async function checkCommentMention (options: CheckerBaseParams & {
348 shortUUID: string
349 commentId: number
350 threadId: number
351 byAccountDisplayName: string
352 checkType: CheckerType
353}) {
354 const { shortUUID, commentId, threadId, byAccountDisplayName } = options
355 const notificationType = UserNotificationType.COMMENT_MENTION
356
357 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
358 if (checkType === 'presence') {
359 expect(notification).to.not.be.undefined
360 expect(notification.type).to.equal(notificationType)
361
362 checkComment(notification.comment, commentId, threadId)
363 checkActor(notification.comment.account)
364 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
365
366 checkVideo(notification.comment.video, undefined, shortUUID)
367 } else {
368 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
369 }
370 }
371
372 function emailNotificationFinder (email: object) {
373 const text: string = email['text']
374
375 return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
376 }
377
378 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
379}
380
381let lastEmailCount = 0
382
383async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
384 shortUUID: string
385 commentId: number
386 threadId: number
387 checkType: CheckerType
388}) {
389 const { server, shortUUID, commentId, threadId, checkType, emails } = options
390 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
391
392 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
393 if (checkType === 'presence') {
394 expect(notification).to.not.be.undefined
395 expect(notification.type).to.equal(notificationType)
396
397 checkComment(notification.comment, commentId, threadId)
398 checkActor(notification.comment.account)
399 checkVideo(notification.comment.video, undefined, shortUUID)
400 } else {
401 expect(notification).to.satisfy((n: UserNotification) => {
402 return n === undefined || n.comment === undefined || n.comment.id !== commentId
403 })
404 }
405 }
406
407 const commentUrl = `${server.url}/w/${shortUUID};threadId=${threadId}`
408
409 function emailNotificationFinder (email: object) {
410 return email['text'].indexOf(commentUrl) !== -1
411 }
412
413 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
414
415 if (checkType === 'presence') {
416 // We cannot detect email duplicates, so check we received another email
417 expect(emails).to.have.length.above(lastEmailCount)
418 lastEmailCount = emails.length
419 }
420}
421
422async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
423 shortUUID: string
424 videoName: string
425 checkType: CheckerType
426}) {
427 const { shortUUID, videoName } = options
428 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
429
430 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
431 if (checkType === 'presence') {
432 expect(notification).to.not.be.undefined
433 expect(notification.type).to.equal(notificationType)
434
435 expect(notification.abuse.id).to.be.a('number')
436 checkVideo(notification.abuse.video, videoName, shortUUID)
437 } else {
438 expect(notification).to.satisfy((n: UserNotification) => {
439 return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
440 })
441 }
442 }
443
444 function emailNotificationFinder (email: object) {
445 const text = email['text']
446 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
447 }
448
449 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
450}
451
452async function checkNewAbuseMessage (options: CheckerBaseParams & {
453 abuseId: number
454 message: string
455 toEmail: string
456 checkType: CheckerType
457}) {
458 const { abuseId, message, toEmail } = options
459 const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
460
461 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
462 if (checkType === 'presence') {
463 expect(notification).to.not.be.undefined
464 expect(notification.type).to.equal(notificationType)
465
466 expect(notification.abuse.id).to.equal(abuseId)
467 } else {
468 expect(notification).to.satisfy((n: UserNotification) => {
469 return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId
470 })
471 }
472 }
473
474 function emailNotificationFinder (email: object) {
475 const text = email['text']
476 const to = email['to'].filter(t => t.address === toEmail)
477
478 return text.indexOf(message) !== -1 && to.length !== 0
479 }
480
481 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
482}
483
484async function checkAbuseStateChange (options: CheckerBaseParams & {
485 abuseId: number
486 state: AbuseState
487 checkType: CheckerType
488}) {
489 const { abuseId, state } = options
490 const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
491
492 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
493 if (checkType === 'presence') {
494 expect(notification).to.not.be.undefined
495 expect(notification.type).to.equal(notificationType)
496
497 expect(notification.abuse.id).to.equal(abuseId)
498 expect(notification.abuse.state).to.equal(state)
499 } else {
500 expect(notification).to.satisfy((n: UserNotification) => {
501 return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId
502 })
503 }
504 }
505
506 function emailNotificationFinder (email: object) {
507 const text = email['text']
508
509 const contains = state === AbuseState.ACCEPTED
510 ? ' accepted'
511 : ' rejected'
512
513 return text.indexOf(contains) !== -1
514 }
515
516 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
517}
518
519async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
520 shortUUID: string
521 videoName: string
522 checkType: CheckerType
523}) {
524 const { shortUUID, videoName } = options
525 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
526
527 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
528 if (checkType === 'presence') {
529 expect(notification).to.not.be.undefined
530 expect(notification.type).to.equal(notificationType)
531
532 expect(notification.abuse.id).to.be.a('number')
533 checkVideo(notification.abuse.comment.video, videoName, shortUUID)
534 } else {
535 expect(notification).to.satisfy((n: UserNotification) => {
536 return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
537 })
538 }
539 }
540
541 function emailNotificationFinder (email: object) {
542 const text = email['text']
543 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
544 }
545
546 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
547}
548
549async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
550 displayName: string
551 checkType: CheckerType
552}) {
553 const { displayName } = options
554 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
555
556 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
557 if (checkType === 'presence') {
558 expect(notification).to.not.be.undefined
559 expect(notification.type).to.equal(notificationType)
560
561 expect(notification.abuse.id).to.be.a('number')
562 expect(notification.abuse.account.displayName).to.equal(displayName)
563 } else {
564 expect(notification).to.satisfy((n: UserNotification) => {
565 return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName
566 })
567 }
568 }
569
570 function emailNotificationFinder (email: object) {
571 const text = email['text']
572 return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
573 }
574
575 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
576}
577
578async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
579 shortUUID: string
580 videoName: string
581 checkType: CheckerType
582}) {
583 const { shortUUID, videoName } = options
584 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
585
586 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
587 if (checkType === 'presence') {
588 expect(notification).to.not.be.undefined
589 expect(notification.type).to.equal(notificationType)
590
591 expect(notification.videoBlacklist.video.id).to.be.a('number')
592 checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
593 } else {
594 expect(notification).to.satisfy((n: UserNotification) => {
595 return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID
596 })
597 }
598 }
599
600 function emailNotificationFinder (email: object) {
601 const text = email['text']
602 return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
603 }
604
605 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
606}
607
608async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
609 shortUUID: string
610 videoName: string
611 blacklistType: 'blacklist' | 'unblacklist'
612}) {
613 const { videoName, shortUUID, blacklistType } = options
614 const notificationType = blacklistType === 'blacklist'
615 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
616 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
617
618 function notificationChecker (notification: UserNotification) {
619 expect(notification).to.not.be.undefined
620 expect(notification.type).to.equal(notificationType)
621
622 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
623
624 checkVideo(video, videoName, shortUUID)
625 }
626
627 function emailNotificationFinder (email: object) {
628 const text = email['text']
629 const blacklistText = blacklistType === 'blacklist'
630 ? 'blacklisted'
631 : 'unblacklisted'
632
633 return text.includes(shortUUID) && text.includes(blacklistText)
634 }
635
636 await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
637}
638
639async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
640 latestVersion: string
641 checkType: CheckerType
642}) {
643 const { latestVersion } = options
644 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
645
646 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
647 if (checkType === 'presence') {
648 expect(notification).to.not.be.undefined
649 expect(notification.type).to.equal(notificationType)
650
651 expect(notification.peertube).to.exist
652 expect(notification.peertube.latestVersion).to.equal(latestVersion)
653 } else {
654 expect(notification).to.satisfy((n: UserNotification) => {
655 return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
656 })
657 }
658 }
659
660 function emailNotificationFinder (email: object) {
661 const text = email['text']
662
663 return text.includes(latestVersion)
664 }
665
666 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
667}
668
669async function checkNewPluginVersion (options: CheckerBaseParams & {
670 pluginType: PluginType
671 pluginName: string
672 checkType: CheckerType
673}) {
674 const { pluginName, pluginType } = options
675 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
676
677 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
678 if (checkType === 'presence') {
679 expect(notification).to.not.be.undefined
680 expect(notification.type).to.equal(notificationType)
681
682 expect(notification.plugin.name).to.equal(pluginName)
683 expect(notification.plugin.type).to.equal(pluginType)
684 } else {
685 expect(notification).to.satisfy((n: UserNotification) => {
686 return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
687 })
688 }
689 }
690
691 function emailNotificationFinder (email: object) {
692 const text = email['text']
693
694 return text.includes(pluginName)
695 }
696
697 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
698}
699
700async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
701 const userNotifications: UserNotification[] = []
702 const adminNotifications: UserNotification[] = []
703 const adminNotificationsServer2: UserNotification[] = []
704 const emails: object[] = []
705
706 const port = await MockSmtpServer.Instance.collectEmails(emails)
707
708 const overrideConfig = {
709 ...ConfigCommand.getEmailOverrideConfig(port),
710
711 signup: {
712 limit: 20
713 }
714 }
715 const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
716
717 await setAccessTokensToServers(servers)
718 await setDefaultVideoChannel(servers)
719 await setDefaultChannelAvatar(servers)
720 await setDefaultAccountAvatar(servers)
721
722 if (servers[1]) {
723 await servers[1].config.enableStudio()
724 await servers[1].config.enableLive({ allowReplay: true, transcoding: false })
725 }
726
727 if (serversCount > 1) {
728 await doubleFollow(servers[0], servers[1])
729 }
730
731 const user = { username: 'user_1', password: 'super password' }
732 await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
733 const userAccessToken = await servers[0].login.getAccessToken(user)
734
735 await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
736 await servers[0].users.updateMyAvatar({ token: userAccessToken, fixture: 'avatar.png' })
737 await servers[0].channels.updateImage({ channelName: 'user_1_channel', token: userAccessToken, fixture: 'avatar.png', type: 'avatar' })
738
739 await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
740
741 if (serversCount > 1) {
742 await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
743 }
744
745 {
746 const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
747 socket.on('new-notification', n => userNotifications.push(n))
748 }
749 {
750 const socket = servers[0].socketIO.getUserNotificationSocket()
751 socket.on('new-notification', n => adminNotifications.push(n))
752 }
753
754 if (serversCount > 1) {
755 const socket = servers[1].socketIO.getUserNotificationSocket()
756 socket.on('new-notification', n => adminNotificationsServer2.push(n))
757 }
758
759 const { videoChannels } = await servers[0].users.getMyInfo()
760 const channelId = videoChannels[0].id
761
762 return {
763 userNotifications,
764 adminNotifications,
765 adminNotificationsServer2,
766 userAccessToken,
767 emails,
768 servers,
769 channelId,
770 baseOverrideConfig: overrideConfig
771 }
772}
773
774// ---------------------------------------------------------------------------
775
776export {
777 getAllNotificationsSettings,
778
779 CheckerBaseParams,
780 CheckerType,
781 checkMyVideoImportIsFinished,
782 checkUserRegistered,
783 checkAutoInstanceFollowing,
784 checkVideoIsPublished,
785 checkNewVideoFromSubscription,
786 checkNewActorFollow,
787 checkNewCommentOnMyVideo,
788 checkNewBlacklistOnMyVideo,
789 checkCommentMention,
790 checkNewVideoAbuseForModerators,
791 checkVideoAutoBlacklistForModerators,
792 checkNewAbuseMessage,
793 checkAbuseStateChange,
794 checkNewInstanceFollower,
795 prepareNotificationsTest,
796 checkNewCommentAbuseForModerators,
797 checkNewAccountAbuseForModerators,
798 checkNewPeerTubeVersion,
799 checkNewPluginVersion,
800 checkVideoStudioEditionIsFinished,
801 checkRegistrationRequest
802}
803
804// ---------------------------------------------------------------------------
805
806async function checkNotification (options: CheckerBaseParams & {
807 notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
808 emailNotificationFinder: (email: object) => boolean
809 checkType: CheckerType
810}) {
811 const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
812
813 const check = options.check || { web: true, mail: true }
814
815 if (check.web) {
816 const notification = await server.notifications.getLatest({ token })
817
818 if (notification || checkType !== 'absence') {
819 notificationChecker(notification, checkType)
820 }
821
822 const socketNotification = socketNotifications.find(n => {
823 try {
824 notificationChecker(n, 'presence')
825 return true
826 } catch {
827 return false
828 }
829 })
830
831 if (checkType === 'presence') {
832 const obj = inspect(socketNotifications, { depth: 5 })
833 expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
834 } else {
835 const obj = inspect(socketNotification, { depth: 5 })
836 expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
837 }
838 }
839
840 if (check.mail) {
841 // Last email
842 const email = emails
843 .slice()
844 .reverse()
845 .find(e => emailNotificationFinder(e))
846
847 if (checkType === 'presence') {
848 const texts = emails.map(e => e.text)
849 expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined
850 } else {
851 expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
852 }
853 }
854}
855
856function checkVideo (video: any, videoName?: string, shortUUID?: string) {
857 if (videoName) {
858 expect(video.name).to.be.a('string')
859 expect(video.name).to.not.be.empty
860 expect(video.name).to.equal(videoName)
861 }
862
863 if (shortUUID) {
864 expect(video.shortUUID).to.be.a('string')
865 expect(video.shortUUID).to.not.be.empty
866 expect(video.shortUUID).to.equal(shortUUID)
867 }
868
869 expect(video.id).to.be.a('number')
870}
871
872function checkActor (actor: any, options: { withAvatar?: boolean } = {}) {
873 const { withAvatar = true } = options
874
875 expect(actor.displayName).to.be.a('string')
876 expect(actor.displayName).to.not.be.empty
877 expect(actor.host).to.not.be.undefined
878
879 if (withAvatar) {
880 expect(actor.avatars).to.be.an('array')
881 expect(actor.avatars).to.have.lengthOf(2)
882 expect(actor.avatars[0].path).to.exist.and.not.empty
883 }
884}
885
886function checkComment (comment: any, commentId: number, threadId: number) {
887 expect(comment.id).to.equal(commentId)
888 expect(comment.threadId).to.equal(threadId)
889}
diff --git a/server/tests/shared/peertube-runner-process.ts b/server/tests/shared/peertube-runner-process.ts
deleted file mode 100644
index 9304ebcc8..000000000
--- a/server/tests/shared/peertube-runner-process.ts
+++ /dev/null
@@ -1,98 +0,0 @@
1import { ChildProcess, fork } from 'child_process'
2import execa from 'execa'
3import { join } from 'path'
4import { root } from '@shared/core-utils'
5import { PeerTubeServer } from '@shared/server-commands'
6
7export class PeerTubeRunnerProcess {
8 private app?: ChildProcess
9
10 constructor (private readonly server: PeerTubeServer) {
11
12 }
13
14 runServer (options: {
15 hideLogs?: boolean // default true
16 } = {}) {
17 const { hideLogs = true } = options
18
19 return new Promise<void>((res, rej) => {
20 const args = [ 'server', '--verbose', ...this.buildIdArg() ]
21
22 const forkOptions = {
23 detached: false,
24 silent: true
25 }
26 this.app = fork(this.getRunnerPath(), args, forkOptions)
27
28 this.app.stdout.on('data', data => {
29 const str = data.toString() as string
30
31 if (!hideLogs) {
32 console.log(str)
33 }
34 })
35
36 res()
37 })
38 }
39
40 registerPeerTubeInstance (options: {
41 registrationToken: string
42 runnerName: string
43 runnerDescription?: string
44 }) {
45 const { registrationToken, runnerName, runnerDescription } = options
46
47 const args = [
48 'register',
49 '--url', this.server.url,
50 '--registration-token', registrationToken,
51 '--runner-name', runnerName,
52 ...this.buildIdArg()
53 ]
54
55 if (runnerDescription) {
56 args.push('--runner-description')
57 args.push(runnerDescription)
58 }
59
60 return execa.node(this.getRunnerPath(), args)
61 }
62
63 unregisterPeerTubeInstance (options: {
64 runnerName: string
65 }) {
66 const { runnerName } = options
67
68 const args = [ 'unregister', '--url', this.server.url, '--runner-name', runnerName, ...this.buildIdArg() ]
69 return execa.node(this.getRunnerPath(), args)
70 }
71
72 async listRegisteredPeerTubeInstances () {
73 const args = [ 'list-registered', ...this.buildIdArg() ]
74 const { stdout } = await execa.node(this.getRunnerPath(), args)
75
76 return stdout
77 }
78
79 kill () {
80 if (!this.app) return
81
82 process.kill(this.app.pid)
83
84 this.app = null
85 }
86
87 getId () {
88 return 'test-' + this.server.internalServerNumber
89 }
90
91 private getRunnerPath () {
92 return join(root(), 'packages', 'peertube-runner', 'dist', 'peertube-runner.js')
93 }
94
95 private buildIdArg () {
96 return [ '--id', this.getId() ]
97 }
98}
diff --git a/server/tests/shared/plugins.ts b/server/tests/shared/plugins.ts
deleted file mode 100644
index 036fce2ff..000000000
--- a/server/tests/shared/plugins.ts
+++ /dev/null
@@ -1,18 +0,0 @@
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
deleted file mode 100644
index 0cfeab7b2..000000000
--- a/server/tests/shared/requests.ts
+++ /dev/null
@@ -1,12 +0,0 @@
1import { doRequest } from '@server/helpers/requests'
2
3export function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
4 const options = {
5 method: 'POST' as 'POST',
6 json: body,
7 httpSignature,
8 headers
9 }
10
11 return doRequest(url, options)
12}
diff --git a/server/tests/shared/sql-command.ts b/server/tests/shared/sql-command.ts
deleted file mode 100644
index 5c53a8ac6..000000000
--- a/server/tests/shared/sql-command.ts
+++ /dev/null
@@ -1,150 +0,0 @@
1import { QueryTypes, Sequelize } from 'sequelize'
2import { forceNumber } from '@shared/core-utils'
3import { PeerTubeServer } from '@shared/server-commands'
4
5export class SQLCommand {
6 private sequelize: Sequelize
7
8 constructor (private readonly server: PeerTubeServer) {
9
10 }
11
12 deleteAll (table: string) {
13 const seq = this.getSequelize()
14
15 const options = { type: QueryTypes.DELETE }
16
17 return seq.query(`DELETE FROM "${table}"`, options)
18 }
19
20 async getVideoShareCount () {
21 const [ { total } ] = await this.selectQuery<{ total: string }>(`SELECT COUNT(*) as total FROM "videoShare"`)
22 if (total === null) return 0
23
24 return parseInt(total, 10)
25 }
26
27 async getInternalFileUrl (fileId: number) {
28 return this.selectQuery<{ fileUrl: string }>(`SELECT "fileUrl" FROM "videoFile" WHERE id = :fileId`, { fileId })
29 .then(rows => rows[0].fileUrl)
30 }
31
32 setActorField (to: string, field: string, value: string) {
33 return this.updateQuery(`UPDATE actor SET ${this.escapeColumnName(field)} = :value WHERE url = :to`, { value, to })
34 }
35
36 setVideoField (uuid: string, field: string, value: string) {
37 return this.updateQuery(`UPDATE video SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid })
38 }
39
40 setPlaylistField (uuid: string, field: string, value: string) {
41 return this.updateQuery(`UPDATE "videoPlaylist" SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid })
42 }
43
44 async countVideoViewsOf (uuid: string) {
45 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
46 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = :uuid`
47
48 const [ { total } ] = await this.selectQuery<{ total: number }>(query, { uuid })
49 if (!total) return 0
50
51 return forceNumber(total)
52 }
53
54 getActorImage (filename: string) {
55 return this.selectQuery<{ width: number, height: number }>(`SELECT * FROM "actorImage" WHERE filename = :filename`, { filename })
56 .then(rows => rows[0])
57 }
58
59 // ---------------------------------------------------------------------------
60
61 setPluginVersion (pluginName: string, newVersion: string) {
62 return this.setPluginField(pluginName, 'version', newVersion)
63 }
64
65 setPluginLatestVersion (pluginName: string, newVersion: string) {
66 return this.setPluginField(pluginName, 'latestVersion', newVersion)
67 }
68
69 setPluginField (pluginName: string, field: string, value: string) {
70 return this.updateQuery(
71 `UPDATE "plugin" SET ${this.escapeColumnName(field)} = :value WHERE "name" = :pluginName`,
72 { pluginName, value }
73 )
74 }
75
76 // ---------------------------------------------------------------------------
77
78 selectQuery <T extends object> (query: string, replacements: { [id: string]: string | number } = {}) {
79 const seq = this.getSequelize()
80 const options = {
81 type: QueryTypes.SELECT as QueryTypes.SELECT,
82 replacements
83 }
84
85 return seq.query<T>(query, options)
86 }
87
88 updateQuery (query: string, replacements: { [id: string]: string | number } = {}) {
89 const seq = this.getSequelize()
90 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE, replacements }
91
92 return seq.query(query, options)
93 }
94
95 // ---------------------------------------------------------------------------
96
97 async getPlaylistInfohash (playlistId: number) {
98 const query = 'SELECT "p2pMediaLoaderInfohashes" FROM "videoStreamingPlaylist" WHERE id = :playlistId'
99
100 const result = await this.selectQuery<{ p2pMediaLoaderInfohashes: string }>(query, { playlistId })
101 if (!result || result.length === 0) return []
102
103 return result[0].p2pMediaLoaderInfohashes
104 }
105
106 // ---------------------------------------------------------------------------
107
108 setActorFollowScores (newScore: number) {
109 return this.updateQuery(`UPDATE "actorFollow" SET "score" = :newScore`, { newScore })
110 }
111
112 setTokenField (accessToken: string, field: string, value: string) {
113 return this.updateQuery(
114 `UPDATE "oAuthToken" SET ${this.escapeColumnName(field)} = :value WHERE "accessToken" = :accessToken`,
115 { value, accessToken }
116 )
117 }
118
119 async cleanup () {
120 if (!this.sequelize) return
121
122 await this.sequelize.close()
123 this.sequelize = undefined
124 }
125
126 private getSequelize () {
127 if (this.sequelize) return this.sequelize
128
129 const dbname = 'peertube_test' + this.server.internalServerNumber
130 const username = 'peertube'
131 const password = 'peertube'
132 const host = '127.0.0.1'
133 const port = 5432
134
135 this.sequelize = new Sequelize(dbname, username, password, {
136 dialect: 'postgres',
137 host,
138 port,
139 logging: false
140 })
141
142 return this.sequelize
143 }
144
145 private escapeColumnName (columnName: string) {
146 return this.getSequelize().escape(columnName)
147 .replace(/^'/, '"')
148 .replace(/'$/, '"')
149 }
150}
diff --git a/server/tests/shared/streaming-playlists.ts b/server/tests/shared/streaming-playlists.ts
deleted file mode 100644
index e4f88bc25..000000000
--- a/server/tests/shared/streaming-playlists.ts
+++ /dev/null
@@ -1,296 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { basename, dirname, join } from 'path'
5import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils'
6import { sha256 } from '@shared/extra-utils'
7import { HttpStatusCode, VideoPrivacy, VideoResolution, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models'
8import { makeRawRequest, PeerTubeServer } from '@shared/server-commands'
9import { expectStartWith } from './checks'
10import { hlsInfohashExist } from './tracker'
11import { checkWebTorrentWorks } from './webtorrent'
12
13async function checkSegmentHash (options: {
14 server: PeerTubeServer
15 baseUrlPlaylist: string
16 baseUrlSegment: string
17 resolution: number
18 hlsPlaylist: VideoStreamingPlaylist
19 token?: string
20}) {
21 const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist, token } = options
22 const command = server.streamingPlaylists
23
24 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
25 const videoName = basename(file.fileUrl)
26
27 const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8`, token })
28
29 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
30
31 const length = parseInt(matches[1], 10)
32 const offset = parseInt(matches[2], 10)
33 const range = `${offset}-${offset + length - 1}`
34
35 const segmentBody = await command.getFragmentedSegment({
36 url: `${baseUrlSegment}/${videoName}`,
37 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
38 range: `bytes=${range}`,
39 token
40 })
41
42 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, token })
43 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range], `Invalid sha256 result for ${videoName} range ${range}`)
44}
45
46// ---------------------------------------------------------------------------
47
48async function checkLiveSegmentHash (options: {
49 server: PeerTubeServer
50 baseUrlSegment: string
51 videoUUID: string
52 segmentName: string
53 hlsPlaylist: VideoStreamingPlaylist
54 withRetry?: boolean
55}) {
56 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist, withRetry = false } = options
57 const command = server.streamingPlaylists
58
59 const segmentBody = await command.getFragmentedSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}`, withRetry })
60 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry })
61
62 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
63}
64
65// ---------------------------------------------------------------------------
66
67async function checkResolutionsInMasterPlaylist (options: {
68 server: PeerTubeServer
69 playlistUrl: string
70 resolutions: number[]
71 token?: string
72 transcoded?: boolean // default true
73 withRetry?: boolean // default false
74}) {
75 const { server, playlistUrl, resolutions, token, withRetry = false, transcoded = true } = options
76
77 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, token, withRetry })
78
79 for (const resolution of resolutions) {
80 const base = '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution
81
82 if (resolution === VideoResolution.H_NOVIDEO) {
83 expect(masterPlaylist).to.match(new RegExp(`${base},CODECS="mp4a.40.2"`))
84 } else if (transcoded) {
85 expect(masterPlaylist).to.match(new RegExp(`${base},(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"`))
86 } else {
87 expect(masterPlaylist).to.match(new RegExp(`${base}`))
88 }
89 }
90
91 const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH='))
92 expect(playlistsLength).to.have.lengthOf(resolutions.length)
93}
94
95async function completeCheckHlsPlaylist (options: {
96 servers: PeerTubeServer[]
97 videoUUID: string
98 hlsOnly: boolean
99
100 resolutions?: number[]
101 objectStorageBaseUrl?: string
102}) {
103 const { videoUUID, hlsOnly, objectStorageBaseUrl } = options
104
105 const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ]
106
107 for (const server of options.servers) {
108 const videoDetails = await server.videos.getWithToken({ id: videoUUID })
109 const requiresAuth = videoDetails.privacy.id === VideoPrivacy.PRIVATE || videoDetails.privacy.id === VideoPrivacy.INTERNAL
110
111 const privatePath = requiresAuth
112 ? 'private/'
113 : ''
114 const token = requiresAuth
115 ? server.accessToken
116 : undefined
117
118 const baseUrl = `http://${videoDetails.account.host}`
119
120 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
121
122 const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
123 expect(hlsPlaylist).to.not.be.undefined
124
125 const hlsFiles = hlsPlaylist.files
126 expect(hlsFiles).to.have.lengthOf(resolutions.length)
127
128 if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0)
129 else expect(videoDetails.files).to.have.lengthOf(resolutions.length)
130
131 // Check JSON files
132 for (const resolution of resolutions) {
133 const file = hlsFiles.find(f => f.resolution.id === resolution)
134 expect(file).to.not.be.undefined
135
136 if (file.resolution.id === VideoResolution.H_NOVIDEO) {
137 expect(file.resolution.label).to.equal('Audio')
138 } else {
139 expect(file.resolution.label).to.equal(resolution + 'p')
140 }
141
142 expect(file.magnetUri).to.have.lengthOf.above(2)
143 await checkWebTorrentWorks(file.magnetUri)
144
145 {
146 const nameReg = `${uuidRegex}-${file.resolution.id}`
147
148 expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}-hls.torrent`))
149
150 if (objectStorageBaseUrl && requiresAuth) {
151 // eslint-disable-next-line max-len
152 expect(file.fileUrl).to.match(new RegExp(`${server.url}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`))
153 } else if (objectStorageBaseUrl) {
154 expectStartWith(file.fileUrl, objectStorageBaseUrl)
155 } else {
156 expect(file.fileUrl).to.match(
157 new RegExp(`${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`)
158 )
159 }
160 }
161
162 {
163 await Promise.all([
164 makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
165 makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
166 makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
167 makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
168
169 makeRawRequest({
170 url: file.fileDownloadUrl,
171 token,
172 expectedStatus: objectStorageBaseUrl
173 ? HttpStatusCode.FOUND_302
174 : HttpStatusCode.OK_200
175 })
176 ])
177 }
178 }
179
180 // Check master playlist
181 {
182 await checkResolutionsInMasterPlaylist({ server, token, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
183
184 const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl, token })
185
186 let i = 0
187 for (const resolution of resolutions) {
188 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
189 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
190
191 const url = 'http://' + videoDetails.account.host
192 await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i)
193
194 i++
195 }
196 }
197
198 // Check resolution playlists
199 {
200 for (const resolution of resolutions) {
201 const file = hlsFiles.find(f => f.resolution.id === resolution)
202 const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8'
203
204 let url: string
205 if (objectStorageBaseUrl && requiresAuth) {
206 url = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
207 } else if (objectStorageBaseUrl) {
208 url = `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}`
209 } else {
210 url = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
211 }
212
213 const subPlaylist = await server.streamingPlaylists.get({ url, token })
214
215 expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`))
216 expect(subPlaylist).to.contain(basename(file.fileUrl))
217 }
218 }
219
220 {
221 let baseUrlAndPath: string
222 if (objectStorageBaseUrl && requiresAuth) {
223 baseUrlAndPath = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}`
224 } else if (objectStorageBaseUrl) {
225 baseUrlAndPath = `${objectStorageBaseUrl}hls/${videoUUID}`
226 } else {
227 baseUrlAndPath = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}`
228 }
229
230 for (const resolution of resolutions) {
231 await checkSegmentHash({
232 server,
233 token,
234 baseUrlPlaylist: baseUrlAndPath,
235 baseUrlSegment: baseUrlAndPath,
236 resolution,
237 hlsPlaylist
238 })
239 }
240 }
241 }
242}
243
244async function checkVideoFileTokenReinjection (options: {
245 server: PeerTubeServer
246 videoUUID: string
247 videoFileToken: string
248 resolutions: number[]
249 isLive: boolean
250}) {
251 const { server, resolutions, videoFileToken, videoUUID, isLive } = options
252
253 const video = await server.videos.getWithToken({ id: videoUUID })
254 const hls = video.streamingPlaylists[0]
255
256 const query = { videoFileToken, reinjectVideoFileToken: 'true' }
257 const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 })
258
259 for (let i = 0; i < resolutions.length; i++) {
260 const resolution = resolutions[i]
261
262 const suffix = isLive
263 ? i
264 : `-${resolution}`
265
266 expect(text).to.contain(`${suffix}.m3u8?videoFileToken=${videoFileToken}&reinjectVideoFileToken=true`)
267 }
268
269 const resolutionPlaylists = extractResolutionPlaylistUrls(hls.playlistUrl, text)
270 expect(resolutionPlaylists).to.have.lengthOf(resolutions.length)
271
272 for (const url of resolutionPlaylists) {
273 const { text } = await makeRawRequest({ url, query, expectedStatus: HttpStatusCode.OK_200 })
274
275 const extension = isLive
276 ? '.ts'
277 : '.mp4'
278
279 expect(text).to.contain(`${extension}?videoFileToken=${videoFileToken}`)
280 expect(text).not.to.contain(`reinjectVideoFileToken=true`)
281 }
282}
283
284function extractResolutionPlaylistUrls (masterPath: string, masterContent: string) {
285 return masterContent.match(/^([^.]+\.m3u8.*)/mg)
286 .map(filename => join(dirname(masterPath), filename))
287}
288
289export {
290 checkSegmentHash,
291 checkLiveSegmentHash,
292 checkResolutionsInMasterPlaylist,
293 completeCheckHlsPlaylist,
294 extractResolutionPlaylistUrls,
295 checkVideoFileTokenReinjection
296}
diff --git a/server/tests/shared/tests.ts b/server/tests/shared/tests.ts
deleted file mode 100644
index d2cb040fb..000000000
--- a/server/tests/shared/tests.ts
+++ /dev/null
@@ -1,40 +0,0 @@
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 youtubeChannel: 'https://youtube.com/channel/UCtnlZdXv3-xQzxiqfn6cjIA',
20 youtubePlaylist: 'https://youtube.com/playlist?list=PLRGXHPrcPd2yc2KdswlAWOxIJ8G3vgy4h',
21
22 // eslint-disable-next-line max-len
23 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',
24
25 badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
26 goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
27 goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4',
28
29 file4K: 'https://download.cpy.re/peertube/4k_file.txt'
30}
31
32function buildRequestStub (): any {
33 return { }
34}
35
36export {
37 FIXTURE_URLS,
38
39 buildRequestStub
40}
diff --git a/server/tests/shared/tracker.ts b/server/tests/shared/tracker.ts
deleted file mode 100644
index 9c1f0246a..000000000
--- a/server/tests/shared/tracker.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import { expect } from 'chai'
2import { sha1 } from '@shared/extra-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-playlists.ts b/server/tests/shared/video-playlists.ts
deleted file mode 100644
index 8db303fd8..000000000
--- a/server/tests/shared/video-playlists.ts
+++ /dev/null
@@ -1,22 +0,0 @@
1import { expect } from 'chai'
2import { readdir } from 'fs-extra'
3import { PeerTubeServer } from '@shared/server-commands'
4
5async function checkPlaylistFilesWereRemoved (
6 playlistUUID: string,
7 server: PeerTubeServer,
8 directories = [ 'thumbnails' ]
9) {
10 for (const directory of directories) {
11 const directoryPath = server.getDirectoryPath(directory)
12
13 const files = await readdir(directoryPath)
14 for (const file of files) {
15 expect(file).to.not.contain(playlistUUID)
16 }
17 }
18}
19
20export {
21 checkPlaylistFilesWereRemoved
22}
diff --git a/server/tests/shared/videos.ts b/server/tests/shared/videos.ts
deleted file mode 100644
index ac24bb173..000000000
--- a/server/tests/shared/videos.ts
+++ /dev/null
@@ -1,315 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { basename, join } from 'path'
6import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '@server/initializers/constants'
7import { getLowercaseExtension, pick, uuidRegex } from '@shared/core-utils'
8import { HttpStatusCode, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@shared/models'
9import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@shared/server-commands'
10import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks'
11import { checkWebTorrentWorks } from './webtorrent'
12
13loadLanguages()
14
15async function completeWebVideoFilesCheck (options: {
16 server: PeerTubeServer
17 originServer: PeerTubeServer
18 videoUUID: string
19 fixture: string
20 files: {
21 resolution: number
22 size?: number
23 }[]
24 objectStorageBaseUrl?: string
25}) {
26 const { originServer, server, videoUUID, files, fixture, objectStorageBaseUrl } = options
27 const video = await server.videos.getWithToken({ id: videoUUID })
28 const serverConfig = await originServer.config.getConfig()
29 const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL
30
31 const transcodingEnabled = serverConfig.transcoding.web_videos.enabled
32
33 for (const attributeFile of files) {
34 const file = video.files.find(f => f.resolution.id === attributeFile.resolution)
35 expect(file, `resolution ${attributeFile.resolution} does not exist`).not.to.be.undefined
36
37 let extension = getLowercaseExtension(fixture)
38 // Transcoding enabled: extension will always be .mp4
39 if (transcodingEnabled) extension = '.mp4'
40
41 expect(file.id).to.exist
42 expect(file.magnetUri).to.have.lengthOf.above(2)
43
44 {
45 const privatePath = requiresAuth
46 ? 'private/'
47 : ''
48 const nameReg = `${uuidRegex}-${file.resolution.id}`
49
50 expect(file.torrentDownloadUrl).to.match(new RegExp(`${server.url}/download/torrents/${nameReg}.torrent`))
51 expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`))
52
53 if (objectStorageBaseUrl && requiresAuth) {
54 const regexp = new RegExp(`${originServer.url}/object-storage-proxy/web-videos/${privatePath}${nameReg}${extension}`)
55 expect(file.fileUrl).to.match(regexp)
56 } else if (objectStorageBaseUrl) {
57 expectStartWith(file.fileUrl, objectStorageBaseUrl)
58 } else {
59 expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/web-videos/${privatePath}${nameReg}${extension}`))
60 }
61
62 expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`))
63 }
64
65 {
66 const token = requiresAuth
67 ? server.accessToken
68 : undefined
69
70 await Promise.all([
71 makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
72 makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
73 makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
74 makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
75 makeRawRequest({
76 url: file.fileDownloadUrl,
77 token,
78 expectedStatus: objectStorageBaseUrl ? HttpStatusCode.FOUND_302 : HttpStatusCode.OK_200
79 })
80 ])
81 }
82
83 expect(file.resolution.id).to.equal(attributeFile.resolution)
84
85 if (file.resolution.id === VideoResolution.H_NOVIDEO) {
86 expect(file.resolution.label).to.equal('Audio')
87 } else {
88 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
89 }
90
91 if (attributeFile.size) {
92 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
93 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
94 expect(
95 file.size,
96 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
97 ).to.be.above(minSize).and.below(maxSize)
98 }
99
100 await checkWebTorrentWorks(file.magnetUri)
101 }
102}
103
104async function completeVideoCheck (options: {
105 server: PeerTubeServer
106 originServer: PeerTubeServer
107 videoUUID: string
108 attributes: {
109 name: string
110 category: number
111 licence: number
112 language: string
113 nsfw: boolean
114 commentsEnabled: boolean
115 downloadEnabled: boolean
116 description: string
117 publishedAt?: string
118 support: string
119 originallyPublishedAt?: string
120 account: {
121 name: string
122 host: string
123 }
124 isLocal: boolean
125 tags: string[]
126 privacy: number
127 likes?: number
128 dislikes?: number
129 duration: number
130 channel: {
131 displayName: string
132 name: string
133 description: string
134 isLocal: boolean
135 }
136 fixture: string
137 files: {
138 resolution: number
139 size: number
140 }[]
141 thumbnailfile?: string
142 previewfile?: string
143 }
144}) {
145 const { attributes, originServer, server, videoUUID } = options
146
147 const video = await server.videos.get({ id: videoUUID })
148
149 if (!attributes.likes) attributes.likes = 0
150 if (!attributes.dislikes) attributes.dislikes = 0
151
152 expect(video.name).to.equal(attributes.name)
153 expect(video.category.id).to.equal(attributes.category)
154 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Unknown')
155 expect(video.licence.id).to.equal(attributes.licence)
156 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
157 expect(video.language.id).to.equal(attributes.language)
158 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
159 expect(video.privacy.id).to.deep.equal(attributes.privacy)
160 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
161 expect(video.nsfw).to.equal(attributes.nsfw)
162 expect(video.description).to.equal(attributes.description)
163 expect(video.account.id).to.be.a('number')
164 expect(video.account.host).to.equal(attributes.account.host)
165 expect(video.account.name).to.equal(attributes.account.name)
166 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
167 expect(video.channel.name).to.equal(attributes.channel.name)
168 expect(video.likes).to.equal(attributes.likes)
169 expect(video.dislikes).to.equal(attributes.dislikes)
170 expect(video.isLocal).to.equal(attributes.isLocal)
171 expect(video.duration).to.equal(attributes.duration)
172 expect(video.url).to.contain(originServer.host)
173 expect(dateIsValid(video.createdAt)).to.be.true
174 expect(dateIsValid(video.publishedAt)).to.be.true
175 expect(dateIsValid(video.updatedAt)).to.be.true
176
177 if (attributes.publishedAt) {
178 expect(video.publishedAt).to.equal(attributes.publishedAt)
179 }
180
181 if (attributes.originallyPublishedAt) {
182 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
183 } else {
184 expect(video.originallyPublishedAt).to.be.null
185 }
186
187 expect(video.files).to.have.lengthOf(attributes.files.length)
188 expect(video.tags).to.deep.equal(attributes.tags)
189 expect(video.account.name).to.equal(attributes.account.name)
190 expect(video.account.host).to.equal(attributes.account.host)
191 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
192 expect(video.channel.name).to.equal(attributes.channel.name)
193 expect(video.channel.host).to.equal(attributes.account.host)
194 expect(video.channel.isLocal).to.equal(attributes.channel.isLocal)
195 expect(dateIsValid(video.channel.createdAt.toString())).to.be.true
196 expect(dateIsValid(video.channel.updatedAt.toString())).to.be.true
197 expect(video.commentsEnabled).to.equal(attributes.commentsEnabled)
198 expect(video.downloadEnabled).to.equal(attributes.downloadEnabled)
199
200 expect(video.thumbnailPath).to.exist
201 await testImageGeneratedByFFmpeg(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath)
202
203 if (attributes.previewfile) {
204 expect(video.previewPath).to.exist
205 await testImageGeneratedByFFmpeg(server.url, attributes.previewfile, video.previewPath)
206 }
207
208 await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) })
209}
210
211async function checkVideoFilesWereRemoved (options: {
212 server: PeerTubeServer
213 video: VideoDetails
214 captions?: VideoCaption[]
215 onlyVideoFiles?: boolean // default false
216}) {
217 const { video, server, captions = [], onlyVideoFiles = false } = options
218
219 const webVideoFiles = video.files || []
220 const hlsFiles = video.streamingPlaylists[0]?.files || []
221
222 const thumbnailName = basename(video.thumbnailPath)
223 const previewName = basename(video.previewPath)
224
225 const torrentNames = webVideoFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
226
227 const captionNames = captions.map(c => basename(c.captionPath))
228
229 const webVideoFilenames = webVideoFiles.map(f => basename(f.fileUrl))
230 const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
231
232 let directories: { [ directory: string ]: string[] } = {
233 videos: webVideoFilenames,
234 redundancy: webVideoFilenames,
235 [join('playlists', 'hls')]: hlsFilenames,
236 [join('redundancy', 'hls')]: hlsFilenames
237 }
238
239 if (onlyVideoFiles !== true) {
240 directories = {
241 ...directories,
242
243 thumbnails: [ thumbnailName ],
244 previews: [ previewName ],
245 torrents: torrentNames,
246 captions: captionNames
247 }
248 }
249
250 for (const directory of Object.keys(directories)) {
251 const directoryPath = server.servers.buildDirectory(directory)
252
253 const directoryExists = await pathExists(directoryPath)
254 if (directoryExists === false) continue
255
256 const existingFiles = await readdir(directoryPath)
257 for (const existingFile of existingFiles) {
258 for (const shouldNotExist of directories[directory]) {
259 expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
260 }
261 }
262 }
263}
264
265async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
266 for (const server of servers) {
267 server.store.videoDetails = await server.videos.get({ id: uuid })
268 }
269}
270
271function checkUploadVideoParam (options: {
272 server: PeerTubeServer
273 token: string
274 attributes: Partial<VideoEdit>
275 expectedStatus?: HttpStatusCode
276 completedExpectedStatus?: HttpStatusCode
277 mode?: 'legacy' | 'resumable'
278}) {
279 const { server, token, attributes, completedExpectedStatus, expectedStatus, mode = 'legacy' } = options
280
281 return mode === 'legacy'
282 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus: expectedStatus || completedExpectedStatus })
283 : server.videos.buildResumeUpload({
284 token,
285 attributes,
286 expectedStatus,
287 completedExpectedStatus,
288 path: '/api/v1/videos/upload-resumable'
289 })
290}
291
292// serverNumber starts from 1
293async function uploadRandomVideoOnServers (
294 servers: PeerTubeServer[],
295 serverNumber: number,
296 additionalParams?: VideoEdit & { prefixName?: string }
297) {
298 const server = servers.find(s => s.serverNumber === serverNumber)
299 const res = await server.videos.randomUpload({ wait: false, additionalParams })
300
301 await waitJobs(servers)
302
303 return res
304}
305
306// ---------------------------------------------------------------------------
307
308export {
309 completeVideoCheck,
310 completeWebVideoFilesCheck,
311 checkUploadVideoParam,
312 uploadRandomVideoOnServers,
313 checkVideoFilesWereRemoved,
314 saveVideoInServers
315}
diff --git a/server/tests/shared/views.ts b/server/tests/shared/views.ts
deleted file mode 100644
index e6b289715..000000000
--- a/server/tests/shared/views.ts
+++ /dev/null
@@ -1,93 +0,0 @@
1import { FfmpegCommand } from 'fluent-ffmpeg'
2import { wait } from '@shared/core-utils'
3import { VideoCreateResult, VideoPrivacy } from '@shared/models'
4import {
5 createMultipleServers,
6 doubleFollow,
7 PeerTubeServer,
8 setAccessTokensToServers,
9 setDefaultVideoChannel,
10 waitJobs,
11 waitUntilLivePublishedOnAllServers
12} from '@shared/server-commands'
13
14async function processViewersStats (servers: PeerTubeServer[]) {
15 await wait(6000)
16
17 for (const server of servers) {
18 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
19 await server.debug.sendCommand({ body: { command: 'process-video-viewers' } })
20 }
21
22 await waitJobs(servers)
23}
24
25async function processViewsBuffer (servers: PeerTubeServer[]) {
26 for (const server of servers) {
27 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
28 }
29
30 await waitJobs(servers)
31}
32
33async function prepareViewsServers () {
34 const servers = await createMultipleServers(2)
35 await setAccessTokensToServers(servers)
36 await setDefaultVideoChannel(servers)
37
38 await servers[0].config.updateCustomSubConfig({
39 newConfig: {
40 live: {
41 enabled: true,
42 allowReplay: true,
43 transcoding: {
44 enabled: false
45 }
46 }
47 }
48 })
49
50 await doubleFollow(servers[0], servers[1])
51
52 return servers
53}
54
55async function prepareViewsVideos (options: {
56 servers: PeerTubeServer[]
57 live: boolean
58 vod: boolean
59}) {
60 const { servers } = options
61
62 const liveAttributes = {
63 name: 'live video',
64 channelId: servers[0].store.channel.id,
65 privacy: VideoPrivacy.PUBLIC
66 }
67
68 let ffmpegCommand: FfmpegCommand
69 let live: VideoCreateResult
70 let vod: VideoCreateResult
71
72 if (options.live) {
73 live = await servers[0].live.create({ fields: liveAttributes })
74
75 ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: live.uuid })
76 await waitUntilLivePublishedOnAllServers(servers, live.uuid)
77 }
78
79 if (options.vod) {
80 vod = await servers[0].videos.quickUpload({ name: 'video' })
81 }
82
83 await waitJobs(servers)
84
85 return { liveVideoId: live?.uuid, vodVideoId: vod?.uuid, ffmpegCommand }
86}
87
88export {
89 processViewersStats,
90 prepareViewsServers,
91 processViewsBuffer,
92 prepareViewsVideos
93}
diff --git a/server/tests/shared/webtorrent.ts b/server/tests/shared/webtorrent.ts
deleted file mode 100644
index d5bd86500..000000000
--- a/server/tests/shared/webtorrent.ts
+++ /dev/null
@@ -1,58 +0,0 @@
1import { expect } from 'chai'
2import { readFile } from 'fs-extra'
3import parseTorrent from 'parse-torrent'
4import { basename, join } from 'path'
5import * as WebTorrent from 'webtorrent'
6import { VideoFile } from '@shared/models'
7import { PeerTubeServer } from '@shared/server-commands'
8
9let webtorrent: WebTorrent.Instance
10
11export async function checkWebTorrentWorks (magnetUri: string, pathMatch?: RegExp) {
12 const torrent = await webtorrentAdd(magnetUri, true)
13
14 expect(torrent.files).to.be.an('array')
15 expect(torrent.files.length).to.equal(1)
16 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
17
18 if (pathMatch) {
19 expect(torrent.files[0].path).match(pathMatch)
20 }
21}
22
23export async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) {
24 const torrentName = basename(file.torrentUrl)
25 const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
26
27 const data = await readFile(torrentPath)
28
29 return parseTorrent(data)
30}
31
32// ---------------------------------------------------------------------------
33// Private
34// ---------------------------------------------------------------------------
35
36function webtorrentAdd (torrentId: string, refreshWebTorrent = false) {
37 const WebTorrent = require('webtorrent')
38
39 if (webtorrent && refreshWebTorrent) webtorrent.destroy()
40 if (!webtorrent || refreshWebTorrent) webtorrent = new WebTorrent()
41
42 webtorrent.on('error', err => console.error('Error in webtorrent', err))
43
44 return new Promise<WebTorrent.Torrent>(res => {
45 const torrent = webtorrent.add(torrentId, res)
46
47 torrent.on('error', err => console.error('Error in webtorrent torrent', err))
48 torrent.on('warning', warn => {
49 const msg = typeof warn === 'string'
50 ? warn
51 : warn.message
52
53 if (msg.includes('Unsupported')) return
54
55 console.error('Warning in webtorrent torrent', warn)
56 })
57 })
58}