aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-04-21 15:00:01 +0200
committerChocobozzz <chocobozzz@cpy.re>2023-05-09 08:57:34 +0200
commitd102de1b38f2877463529c3b27bd35ffef4fd8bf (patch)
tree31fa0bdf26ad7a2ee46d600d804a6f03260266c8 /server/tests/shared
parent2fe978744e5b74eb824e4d79c1bb9b840169f125 (diff)
downloadPeerTube-d102de1b38f2877463529c3b27bd35ffef4fd8bf.tar.gz
PeerTube-d102de1b38f2877463529c3b27bd35ffef4fd8bf.tar.zst
PeerTube-d102de1b38f2877463529c3b27bd35ffef4fd8bf.zip
Add runner server tests
Diffstat (limited to 'server/tests/shared')
-rw-r--r--server/tests/shared/checks.ts4
-rw-r--r--server/tests/shared/generate.ts2
-rw-r--r--server/tests/shared/index.ts3
-rw-r--r--server/tests/shared/live.ts10
-rw-r--r--server/tests/shared/peertube-runner-process.ts87
-rw-r--r--server/tests/shared/sql-command.ts150
-rw-r--r--server/tests/shared/streaming-playlists.ts134
-rw-r--r--server/tests/shared/videos.ts187
-rw-r--r--server/tests/shared/webtorrent.ts58
9 files changed, 520 insertions, 115 deletions
diff --git a/server/tests/shared/checks.ts b/server/tests/shared/checks.ts
index c0098b293..d7eb25bb5 100644
--- a/server/tests/shared/checks.ts
+++ b/server/tests/shared/checks.ts
@@ -11,7 +11,7 @@ import { HttpStatusCode } from '@shared/models'
11import { makeGetRequest, PeerTubeServer } from '@shared/server-commands' 11import { makeGetRequest, PeerTubeServer } from '@shared/server-commands'
12 12
13// Default interval -> 5 minutes 13// Default interval -> 5 minutes
14function dateIsValid (dateString: string, interval = 300000) { 14function dateIsValid (dateString: string | Date, interval = 300000) {
15 const dateToCheck = new Date(dateString) 15 const dateToCheck = new Date(dateString)
16 const now = new Date() 16 const now = new Date()
17 17
@@ -90,6 +90,8 @@ async function testFileExistsOrNot (server: PeerTubeServer, directory: string, f
90 expect(await pathExists(join(base, filePath))).to.equal(exist) 90 expect(await pathExists(join(base, filePath))).to.equal(exist)
91} 91}
92 92
93// ---------------------------------------------------------------------------
94
93function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { 95function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
94 return makeGetRequest({ 96 return makeGetRequest({
95 url, 97 url,
diff --git a/server/tests/shared/generate.ts b/server/tests/shared/generate.ts
index 9a57084e4..b0c8dba66 100644
--- a/server/tests/shared/generate.ts
+++ b/server/tests/shared/generate.ts
@@ -3,7 +3,7 @@ import ffmpeg from 'fluent-ffmpeg'
3import { ensureDir, pathExists } from 'fs-extra' 3import { ensureDir, pathExists } from 'fs-extra'
4import { dirname } from 'path' 4import { dirname } from 'path'
5import { buildAbsoluteFixturePath, getMaxBitrate } from '@shared/core-utils' 5import { buildAbsoluteFixturePath, getMaxBitrate } from '@shared/core-utils'
6import { getVideoStreamBitrate, getVideoStreamFPS, getVideoStreamDimensionsInfo } from '@shared/extra-utils' 6import { getVideoStreamBitrate, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@shared/ffmpeg'
7 7
8async function ensureHasTooBigBitrate (fixturePath: string) { 8async function ensureHasTooBigBitrate (fixturePath: string) {
9 const bitrate = await getVideoStreamBitrate(fixturePath) 9 const bitrate = await getVideoStreamBitrate(fixturePath)
diff --git a/server/tests/shared/index.ts b/server/tests/shared/index.ts
index 963ef8fe6..eda24adb5 100644
--- a/server/tests/shared/index.ts
+++ b/server/tests/shared/index.ts
@@ -6,11 +6,14 @@ export * from './directories'
6export * from './generate' 6export * from './generate'
7export * from './live' 7export * from './live'
8export * from './notifications' 8export * from './notifications'
9export * from './peertube-runner-process'
9export * from './video-playlists' 10export * from './video-playlists'
10export * from './plugins' 11export * from './plugins'
11export * from './requests' 12export * from './requests'
13export * from './sql-command'
12export * from './streaming-playlists' 14export * from './streaming-playlists'
13export * from './tests' 15export * from './tests'
14export * from './tracker' 16export * from './tracker'
15export * from './videos' 17export * from './videos'
16export * from './views' 18export * from './views'
19export * from './webtorrent'
diff --git a/server/tests/shared/live.ts b/server/tests/shared/live.ts
index ff0b2f226..31f92ef19 100644
--- a/server/tests/shared/live.ts
+++ b/server/tests/shared/live.ts
@@ -6,6 +6,7 @@ import { join } from 'path'
6import { sha1 } from '@shared/extra-utils' 6import { sha1 } from '@shared/extra-utils'
7import { LiveVideo, VideoStreamingPlaylistType } from '@shared/models' 7import { LiveVideo, VideoStreamingPlaylistType } from '@shared/models'
8import { ObjectStorageCommand, PeerTubeServer } from '@shared/server-commands' 8import { ObjectStorageCommand, PeerTubeServer } from '@shared/server-commands'
9import { SQLCommand } from './sql-command'
9import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist } from './streaming-playlists' 10import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist } from './streaming-playlists'
10 11
11async function checkLiveCleanup (options: { 12async function checkLiveCleanup (options: {
@@ -36,8 +37,10 @@ async function checkLiveCleanup (options: {
36 37
37// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
38 39
39async function testVideoResolutions (options: { 40async function testLiveVideoResolutions (options: {
41 sqlCommand: SQLCommand
40 originServer: PeerTubeServer 42 originServer: PeerTubeServer
43
41 servers: PeerTubeServer[] 44 servers: PeerTubeServer[]
42 liveVideoId: string 45 liveVideoId: string
43 resolutions: number[] 46 resolutions: number[]
@@ -48,6 +51,7 @@ async function testVideoResolutions (options: {
48}) { 51}) {
49 const { 52 const {
50 originServer, 53 originServer,
54 sqlCommand,
51 servers, 55 servers,
52 liveVideoId, 56 liveVideoId,
53 resolutions, 57 resolutions,
@@ -116,7 +120,7 @@ async function testVideoResolutions (options: {
116 120
117 if (originServer.internalServerNumber === server.internalServerNumber) { 121 if (originServer.internalServerNumber === server.internalServerNumber) {
118 const infohash = sha1(`${2 + hlsPlaylist.playlistUrl}+V${i}`) 122 const infohash = sha1(`${2 + hlsPlaylist.playlistUrl}+V${i}`)
119 const dbInfohashes = await originServer.sql.getPlaylistInfohash(hlsPlaylist.id) 123 const dbInfohashes = await sqlCommand.getPlaylistInfohash(hlsPlaylist.id)
120 124
121 expect(dbInfohashes).to.include(infohash) 125 expect(dbInfohashes).to.include(infohash)
122 } 126 }
@@ -128,7 +132,7 @@ async function testVideoResolutions (options: {
128 132
129export { 133export {
130 checkLiveCleanup, 134 checkLiveCleanup,
131 testVideoResolutions 135 testLiveVideoResolutions
132} 136}
133 137
134// --------------------------------------------------------------------------- 138// ---------------------------------------------------------------------------
diff --git a/server/tests/shared/peertube-runner-process.ts b/server/tests/shared/peertube-runner-process.ts
new file mode 100644
index 000000000..84e2dc6df
--- /dev/null
+++ b/server/tests/shared/peertube-runner-process.ts
@@ -0,0 +1,87 @@
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 runServer (options: {
11 hideLogs?: boolean // default true
12 } = {}) {
13 const { hideLogs = true } = options
14
15 return new Promise<void>((res, rej) => {
16 const args = [ 'server', '--verbose', '--id', 'test' ]
17
18 const forkOptions = {
19 detached: false,
20 silent: true
21 }
22 this.app = fork(this.getRunnerPath(), args, forkOptions)
23
24 this.app.stdout.on('data', data => {
25 const str = data.toString() as string
26
27 if (!hideLogs) {
28 console.log(str)
29 }
30 })
31
32 res()
33 })
34 }
35
36 registerPeerTubeInstance (options: {
37 server: PeerTubeServer
38 registrationToken: string
39 runnerName: string
40 runnerDescription?: string
41 }) {
42 const { server, registrationToken, runnerName, runnerDescription } = options
43
44 const args = [
45 'register',
46 '--url', server.url,
47 '--registration-token', registrationToken,
48 '--runner-name', runnerName,
49 '--id', 'test'
50 ]
51
52 if (runnerDescription) {
53 args.push('--runner-description')
54 args.push(runnerDescription)
55 }
56
57 return execa.node(this.getRunnerPath(), args)
58 }
59
60 unregisterPeerTubeInstance (options: {
61 server: PeerTubeServer
62 }) {
63 const { server } = options
64
65 const args = [ 'unregister', '--url', server.url, '--id', 'test' ]
66 return execa.node(this.getRunnerPath(), args)
67 }
68
69 async listRegisteredPeerTubeInstances () {
70 const args = [ 'list-registered', '--id', 'test' ]
71 const { stdout } = await execa.node(this.getRunnerPath(), args)
72
73 return stdout
74 }
75
76 kill () {
77 if (!this.app) return
78
79 process.kill(this.app.pid)
80
81 this.app = null
82 }
83
84 private getRunnerPath () {
85 return join(root(), 'packages', 'peertube-runner', 'dist', 'peertube-runner.js')
86 }
87}
diff --git a/server/tests/shared/sql-command.ts b/server/tests/shared/sql-command.ts
new file mode 100644
index 000000000..5c53a8ac6
--- /dev/null
+++ b/server/tests/shared/sql-command.ts
@@ -0,0 +1,150 @@
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
index 1c38cb512..acfb2b408 100644
--- a/server/tests/shared/streaming-playlists.ts
+++ b/server/tests/shared/streaming-playlists.ts
@@ -4,10 +4,11 @@ import { expect } from 'chai'
4import { basename, dirname, join } from 'path' 4import { basename, dirname, join } from 'path'
5import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' 5import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils'
6import { sha256 } from '@shared/extra-utils' 6import { sha256 } from '@shared/extra-utils'
7import { HttpStatusCode, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models' 7import { HttpStatusCode, VideoPrivacy, VideoResolution, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models'
8import { makeRawRequest, PeerTubeServer, webtorrentAdd } from '@shared/server-commands' 8import { makeRawRequest, PeerTubeServer } from '@shared/server-commands'
9import { expectStartWith } from './checks' 9import { expectStartWith } from './checks'
10import { hlsInfohashExist } from './tracker' 10import { hlsInfohashExist } from './tracker'
11import { checkWebTorrentWorks } from './webtorrent'
11 12
12async function checkSegmentHash (options: { 13async function checkSegmentHash (options: {
13 server: PeerTubeServer 14 server: PeerTubeServer
@@ -15,14 +16,15 @@ async function checkSegmentHash (options: {
15 baseUrlSegment: string 16 baseUrlSegment: string
16 resolution: number 17 resolution: number
17 hlsPlaylist: VideoStreamingPlaylist 18 hlsPlaylist: VideoStreamingPlaylist
19 token?: string
18}) { 20}) {
19 const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options 21 const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist, token } = options
20 const command = server.streamingPlaylists 22 const command = server.streamingPlaylists
21 23
22 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) 24 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
23 const videoName = basename(file.fileUrl) 25 const videoName = basename(file.fileUrl)
24 26
25 const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` }) 27 const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8`, token })
26 28
27 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) 29 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
28 30
@@ -33,11 +35,12 @@ async function checkSegmentHash (options: {
33 const segmentBody = await command.getFragmentedSegment({ 35 const segmentBody = await command.getFragmentedSegment({
34 url: `${baseUrlSegment}/${videoName}`, 36 url: `${baseUrlSegment}/${videoName}`,
35 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206, 37 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
36 range: `bytes=${range}` 38 range: `bytes=${range}`,
39 token
37 }) 40 })
38 41
39 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) 42 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, token })
40 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range]) 43 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range], `Invalid sha256 result for ${videoName} range ${range}`)
41} 44}
42 45
43// --------------------------------------------------------------------------- 46// ---------------------------------------------------------------------------
@@ -64,19 +67,24 @@ async function checkResolutionsInMasterPlaylist (options: {
64 server: PeerTubeServer 67 server: PeerTubeServer
65 playlistUrl: string 68 playlistUrl: string
66 resolutions: number[] 69 resolutions: number[]
70 token?: string
67 transcoded?: boolean // default true 71 transcoded?: boolean // default true
68 withRetry?: boolean // default false 72 withRetry?: boolean // default false
69}) { 73}) {
70 const { server, playlistUrl, resolutions, withRetry = false, transcoded = true } = options 74 const { server, playlistUrl, resolutions, token, withRetry = false, transcoded = true } = options
71 75
72 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, withRetry }) 76 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, token, withRetry })
73 77
74 for (const resolution of resolutions) { 78 for (const resolution of resolutions) {
75 const reg = transcoded 79 const base = '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution
76 ? new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"') 80
77 : new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + '') 81 if (resolution === VideoResolution.H_NOVIDEO) {
78 82 expect(masterPlaylist).to.match(new RegExp(`${base},CODECS="mp4a.40.2"`))
79 expect(masterPlaylist).to.match(reg) 83 } else if (transcoded) {
84 expect(masterPlaylist).to.match(new RegExp(`${base},(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"`))
85 } else {
86 expect(masterPlaylist).to.match(new RegExp(`${base}`))
87 }
80 } 88 }
81 89
82 const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH=')) 90 const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH='))
@@ -89,14 +97,23 @@ async function completeCheckHlsPlaylist (options: {
89 hlsOnly: boolean 97 hlsOnly: boolean
90 98
91 resolutions?: number[] 99 resolutions?: number[]
92 objectStorageBaseUrl: string 100 objectStorageBaseUrl?: string
93}) { 101}) {
94 const { videoUUID, hlsOnly, objectStorageBaseUrl } = options 102 const { videoUUID, hlsOnly, objectStorageBaseUrl } = options
95 103
96 const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ] 104 const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ]
97 105
98 for (const server of options.servers) { 106 for (const server of options.servers) {
99 const videoDetails = await server.videos.get({ id: videoUUID }) 107 const videoDetails = await server.videos.getWithToken({ id: videoUUID })
108 const requiresAuth = videoDetails.privacy.id === VideoPrivacy.PRIVATE || videoDetails.privacy.id === VideoPrivacy.INTERNAL
109
110 const privatePath = requiresAuth
111 ? 'private/'
112 : ''
113 const token = requiresAuth
114 ? server.accessToken
115 : undefined
116
100 const baseUrl = `http://${videoDetails.account.host}` 117 const baseUrl = `http://${videoDetails.account.host}`
101 118
102 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1) 119 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
@@ -115,35 +132,55 @@ async function completeCheckHlsPlaylist (options: {
115 const file = hlsFiles.find(f => f.resolution.id === resolution) 132 const file = hlsFiles.find(f => f.resolution.id === resolution)
116 expect(file).to.not.be.undefined 133 expect(file).to.not.be.undefined
117 134
118 expect(file.magnetUri).to.have.lengthOf.above(2) 135 if (file.resolution.id === VideoResolution.H_NOVIDEO) {
119 expect(file.torrentUrl).to.match( 136 expect(file.resolution.label).to.equal('Audio')
120 new RegExp(`${server.url}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}-hls.torrent`)
121 )
122
123 if (objectStorageBaseUrl) {
124 expectStartWith(file.fileUrl, objectStorageBaseUrl)
125 } else { 137 } else {
126 expect(file.fileUrl).to.match( 138 expect(file.resolution.label).to.equal(resolution + 'p')
127 new RegExp(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${uuidRegex}-${file.resolution.id}-fragmented.mp4`)
128 )
129 } 139 }
130 140
131 expect(file.resolution.label).to.equal(resolution + 'p') 141 expect(file.magnetUri).to.have.lengthOf.above(2)
132 142 await checkWebTorrentWorks(file.magnetUri)
133 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }) 143
134 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 }) 144 {
145 const nameReg = `${uuidRegex}-${file.resolution.id}`
146
147 expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}-hls.torrent`))
148
149 if (objectStorageBaseUrl && requiresAuth) {
150 // eslint-disable-next-line max-len
151 expect(file.fileUrl).to.match(new RegExp(`${server.url}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`))
152 } else if (objectStorageBaseUrl) {
153 expectStartWith(file.fileUrl, objectStorageBaseUrl)
154 } else {
155 expect(file.fileUrl).to.match(
156 new RegExp(`${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`)
157 )
158 }
159 }
135 160
136 const torrent = await webtorrentAdd(file.magnetUri, true) 161 {
137 expect(torrent.files).to.be.an('array') 162 await Promise.all([
138 expect(torrent.files.length).to.equal(1) 163 makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
139 expect(torrent.files[0].path).to.exist.and.to.not.equal('') 164 makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
165 makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
166 makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
167
168 makeRawRequest({
169 url: file.fileDownloadUrl,
170 token,
171 expectedStatus: objectStorageBaseUrl
172 ? HttpStatusCode.FOUND_302
173 : HttpStatusCode.OK_200
174 })
175 ])
176 }
140 } 177 }
141 178
142 // Check master playlist 179 // Check master playlist
143 { 180 {
144 await checkResolutionsInMasterPlaylist({ server, playlistUrl: hlsPlaylist.playlistUrl, resolutions }) 181 await checkResolutionsInMasterPlaylist({ server, token, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
145 182
146 const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl }) 183 const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl, token })
147 184
148 let i = 0 185 let i = 0
149 for (const resolution of resolutions) { 186 for (const resolution of resolutions) {
@@ -163,11 +200,16 @@ async function completeCheckHlsPlaylist (options: {
163 const file = hlsFiles.find(f => f.resolution.id === resolution) 200 const file = hlsFiles.find(f => f.resolution.id === resolution)
164 const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8' 201 const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8'
165 202
166 const url = objectStorageBaseUrl 203 let url: string
167 ? `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}` 204 if (objectStorageBaseUrl && requiresAuth) {
168 : `${baseUrl}/static/streaming-playlists/hls/${videoUUID}/${playlistName}` 205 url = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
206 } else if (objectStorageBaseUrl) {
207 url = `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}`
208 } else {
209 url = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
210 }
169 211
170 const subPlaylist = await server.streamingPlaylists.get({ url }) 212 const subPlaylist = await server.streamingPlaylists.get({ url, token })
171 213
172 expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`)) 214 expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`))
173 expect(subPlaylist).to.contain(basename(file.fileUrl)) 215 expect(subPlaylist).to.contain(basename(file.fileUrl))
@@ -175,13 +217,19 @@ async function completeCheckHlsPlaylist (options: {
175 } 217 }
176 218
177 { 219 {
178 const baseUrlAndPath = objectStorageBaseUrl 220 let baseUrlAndPath: string
179 ? objectStorageBaseUrl + 'hls/' + videoUUID 221 if (objectStorageBaseUrl && requiresAuth) {
180 : baseUrl + '/static/streaming-playlists/hls/' + videoUUID 222 baseUrlAndPath = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}`
223 } else if (objectStorageBaseUrl) {
224 baseUrlAndPath = `${objectStorageBaseUrl}hls/${videoUUID}`
225 } else {
226 baseUrlAndPath = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}`
227 }
181 228
182 for (const resolution of resolutions) { 229 for (const resolution of resolutions) {
183 await checkSegmentHash({ 230 await checkSegmentHash({
184 server, 231 server,
232 token,
185 baseUrlPlaylist: baseUrlAndPath, 233 baseUrlPlaylist: baseUrlAndPath,
186 baseUrlSegment: baseUrlAndPath, 234 baseUrlSegment: baseUrlAndPath,
187 resolution, 235 resolution,
diff --git a/server/tests/shared/videos.ts b/server/tests/shared/videos.ts
index f8ec65752..856fabd11 100644
--- a/server/tests/shared/videos.ts
+++ b/server/tests/shared/videos.ts
@@ -4,16 +4,106 @@ import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra' 4import { pathExists, readdir } from 'fs-extra'
5import { basename, join } from 'path' 5import { basename, join } from 'path'
6import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '@server/initializers/constants' 6import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '@server/initializers/constants'
7import { getLowercaseExtension, uuidRegex } from '@shared/core-utils' 7import { getLowercaseExtension, pick, uuidRegex } from '@shared/core-utils'
8import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models' 8import { HttpStatusCode, VideoCaption, VideoDetails, VideoPrivacy, VideoResolution } from '@shared/models'
9import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs, webtorrentAdd } from '@shared/server-commands' 9import { makeRawRequest, PeerTubeServer, VideoEdit, waitJobs } from '@shared/server-commands'
10import { dateIsValid, testImage } from './checks' 10import { dateIsValid, expectStartWith, testImage } from './checks'
11import { checkWebTorrentWorks } from './webtorrent'
11 12
12loadLanguages() 13loadLanguages()
13 14
14async function completeVideoCheck ( 15async function completeWebVideoFilesCheck (options: {
15 server: PeerTubeServer, 16 server: PeerTubeServer
16 video: any, 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.webtorrent.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 expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/object-storage-proxy/webseed/${privatePath}${nameReg}${extension}`))
55 } else if (objectStorageBaseUrl) {
56 expectStartWith(file.fileUrl, objectStorageBaseUrl)
57 } else {
58 expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/webseed/${privatePath}${nameReg}${extension}`))
59 }
60
61 expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`))
62 }
63
64 {
65 const token = requiresAuth
66 ? server.accessToken
67 : undefined
68
69 await Promise.all([
70 makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
71 makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
72 makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
73 makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
74 makeRawRequest({
75 url: file.fileDownloadUrl,
76 token,
77 expectedStatus: objectStorageBaseUrl ? HttpStatusCode.FOUND_302 : HttpStatusCode.OK_200
78 })
79 ])
80 }
81
82 expect(file.resolution.id).to.equal(attributeFile.resolution)
83
84 if (file.resolution.id === VideoResolution.H_NOVIDEO) {
85 expect(file.resolution.label).to.equal('Audio')
86 } else {
87 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
88 }
89
90 if (attributeFile.size) {
91 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
92 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
93 expect(
94 file.size,
95 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
96 ).to.be.above(minSize).and.below(maxSize)
97 }
98
99 await checkWebTorrentWorks(file.magnetUri)
100 }
101}
102
103async function completeVideoCheck (options: {
104 server: PeerTubeServer
105 originServer: PeerTubeServer
106 videoUUID: string
17 attributes: { 107 attributes: {
18 name: string 108 name: string
19 category: number 109 category: number
@@ -50,13 +140,14 @@ async function completeVideoCheck (
50 thumbnailfile?: string 140 thumbnailfile?: string
51 previewfile?: string 141 previewfile?: string
52 } 142 }
53) { 143}) {
144 const { attributes, originServer, server, videoUUID } = options
145
146 const video = await server.videos.get({ id: videoUUID })
147
54 if (!attributes.likes) attributes.likes = 0 148 if (!attributes.likes) attributes.likes = 0
55 if (!attributes.dislikes) attributes.dislikes = 0 149 if (!attributes.dislikes) attributes.dislikes = 0
56 150
57 const host = new URL(server.url).host
58 const originHost = attributes.account.host
59
60 expect(video.name).to.equal(attributes.name) 151 expect(video.name).to.equal(attributes.name)
61 expect(video.category.id).to.equal(attributes.category) 152 expect(video.category.id).to.equal(attributes.category)
62 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Unknown') 153 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Unknown')
@@ -77,7 +168,7 @@ async function completeVideoCheck (
77 expect(video.dislikes).to.equal(attributes.dislikes) 168 expect(video.dislikes).to.equal(attributes.dislikes)
78 expect(video.isLocal).to.equal(attributes.isLocal) 169 expect(video.isLocal).to.equal(attributes.isLocal)
79 expect(video.duration).to.equal(attributes.duration) 170 expect(video.duration).to.equal(attributes.duration)
80 expect(video.url).to.contain(originHost) 171 expect(video.url).to.contain(originServer.host)
81 expect(dateIsValid(video.createdAt)).to.be.true 172 expect(dateIsValid(video.createdAt)).to.be.true
82 expect(dateIsValid(video.publishedAt)).to.be.true 173 expect(dateIsValid(video.publishedAt)).to.be.true
83 expect(dateIsValid(video.updatedAt)).to.be.true 174 expect(dateIsValid(video.updatedAt)).to.be.true
@@ -92,67 +183,28 @@ async function completeVideoCheck (
92 expect(video.originallyPublishedAt).to.be.null 183 expect(video.originallyPublishedAt).to.be.null
93 } 184 }
94 185
95 const videoDetails = await server.videos.get({ id: video.uuid }) 186 expect(video.files).to.have.lengthOf(attributes.files.length)
96 187 expect(video.tags).to.deep.equal(attributes.tags)
97 expect(videoDetails.files).to.have.lengthOf(attributes.files.length) 188 expect(video.account.name).to.equal(attributes.account.name)
98 expect(videoDetails.tags).to.deep.equal(attributes.tags) 189 expect(video.account.host).to.equal(attributes.account.host)
99 expect(videoDetails.account.name).to.equal(attributes.account.name)
100 expect(videoDetails.account.host).to.equal(attributes.account.host)
101 expect(video.channel.displayName).to.equal(attributes.channel.displayName) 190 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
102 expect(video.channel.name).to.equal(attributes.channel.name) 191 expect(video.channel.name).to.equal(attributes.channel.name)
103 expect(videoDetails.channel.host).to.equal(attributes.account.host) 192 expect(video.channel.host).to.equal(attributes.account.host)
104 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal) 193 expect(video.channel.isLocal).to.equal(attributes.channel.isLocal)
105 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true 194 expect(dateIsValid(video.channel.createdAt.toString())).to.be.true
106 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true 195 expect(dateIsValid(video.channel.updatedAt.toString())).to.be.true
107 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) 196 expect(video.commentsEnabled).to.equal(attributes.commentsEnabled)
108 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled) 197 expect(video.downloadEnabled).to.equal(attributes.downloadEnabled)
109
110 for (const attributeFile of attributes.files) {
111 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
112 expect(file).not.to.be.undefined
113
114 let extension = getLowercaseExtension(attributes.fixture)
115 // Transcoding enabled: extension will always be .mp4
116 if (attributes.files.length > 1) extension = '.mp4'
117
118 expect(file.id).to.exist
119 expect(file.magnetUri).to.have.lengthOf.above(2)
120
121 expect(file.torrentDownloadUrl).to.match(new RegExp(`http://${host}/download/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
122 expect(file.torrentUrl).to.match(new RegExp(`http://${host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
123
124 expect(file.fileUrl).to.match(new RegExp(`http://${originHost}/static/webseed/${uuidRegex}-${file.resolution.id}${extension}`))
125 expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`))
126 198
127 await Promise.all([ 199 expect(video.thumbnailPath).to.exist
128 makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 }), 200 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath)
129 makeRawRequest({ url: file.torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }),
130 makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.OK_200 })
131 ])
132
133 expect(file.resolution.id).to.equal(attributeFile.resolution)
134 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
135
136 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
137 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
138 expect(
139 file.size,
140 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
141 ).to.be.above(minSize).and.below(maxSize)
142
143 const torrent = await webtorrentAdd(file.magnetUri, true)
144 expect(torrent.files).to.be.an('array')
145 expect(torrent.files.length).to.equal(1)
146 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
147 }
148
149 expect(videoDetails.thumbnailPath).to.exist
150 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
151 201
152 if (attributes.previewfile) { 202 if (attributes.previewfile) {
153 expect(videoDetails.previewPath).to.exist 203 expect(video.previewPath).to.exist
154 await testImage(server.url, attributes.previewfile, videoDetails.previewPath) 204 await testImage(server.url, attributes.previewfile, video.previewPath)
155 } 205 }
206
207 await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) })
156} 208}
157 209
158async function checkVideoFilesWereRemoved (options: { 210async function checkVideoFilesWereRemoved (options: {
@@ -245,6 +297,7 @@ async function uploadRandomVideoOnServers (
245 297
246export { 298export {
247 completeVideoCheck, 299 completeVideoCheck,
300 completeWebVideoFilesCheck,
248 checkUploadVideoParam, 301 checkUploadVideoParam,
249 uploadRandomVideoOnServers, 302 uploadRandomVideoOnServers,
250 checkVideoFilesWereRemoved, 303 checkVideoFilesWereRemoved,
diff --git a/server/tests/shared/webtorrent.ts b/server/tests/shared/webtorrent.ts
new file mode 100644
index 000000000..d5bd86500
--- /dev/null
+++ b/server/tests/shared/webtorrent.ts
@@ -0,0 +1,58 @@
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}