1 /* tslint:disable:no-unused-expression */
3 import * as chai from 'chai'
5 import { VideoDetails } from '../../../../shared/models/videos'
8 checkVideoFilesWereRemoved,
10 flushAndRunMultipleServers,
11 getFollowingListPaginationAndSort,
21 setAccessTokensToServers,
27 } from '../../../../shared/utils'
28 import { waitJobs } from '../../../../shared/utils/server/jobs'
30 import * as magnetUtil from 'magnet-uri'
31 import { updateRedundancy } from '../../../../shared/utils/server/redundancy'
32 import { ActorFollow } from '../../../../shared/models/actors'
33 import { readdir } from 'fs-extra'
34 import { join } from 'path'
35 import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy'
36 import { getStats } from '../../../../shared/utils/server/stats'
37 import { ServerStats } from '../../../../shared/models/server/server-stats.model'
39 const expect = chai.expect
41 let servers: ServerInfo[] = []
42 let video1Server2UUID: string
44 function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) {
45 const parsed = magnetUtil.decode(file.magnetUri)
47 for (const ws of baseWebseeds) {
48 const found = parsed.urlList.find(url => url === `${ws}-${file.resolution.id}.mp4`)
49 expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
52 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
55 async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) {
64 check_interval: '5 seconds',
67 min_lifetime: '1 hour',
75 servers = await flushAndRunMultipleServers(3, config)
77 // Get the access tokens
78 await setAccessTokensToServers(servers)
81 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' })
82 video1Server2UUID = res.body.video.uuid
84 await viewVideo(servers[ 1 ].url, video1Server2UUID)
87 await waitJobs(servers)
89 // Server 1 and server 2 follow each other
90 await doubleFollow(servers[ 0 ], servers[ 1 ])
91 // Server 1 and server 3 follow each other
92 await doubleFollow(servers[ 0 ], servers[ 2 ])
93 // Server 2 and server 3 follow each other
94 await doubleFollow(servers[ 1 ], servers[ 2 ])
96 await waitJobs(servers)
99 async function check1WebSeed (videoUUID?: string) {
100 if (!videoUUID) videoUUID = video1Server2UUID
103 'http://localhost:9002/static/webseed/' + videoUUID
106 for (const server of servers) {
107 // With token to avoid issues with video follow constraints
108 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
110 const video: VideoDetails = res.body
111 for (const f of video.files) {
112 checkMagnetWebseeds(f, webseeds, server)
117 async function check2Webseeds (videoUUID?: string) {
118 if (!videoUUID) videoUUID = video1Server2UUID
121 'http://localhost:9001/static/redundancy/' + videoUUID,
122 'http://localhost:9002/static/webseed/' + videoUUID
125 for (const server of servers) {
126 const res = await getVideo(server.url, videoUUID)
128 const video: VideoDetails = res.body
130 for (const file of video.files) {
131 checkMagnetWebseeds(file, webseeds, server)
133 await makeGetRequest({
135 statusCodeExpected: 200,
136 path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`,
139 await makeGetRequest({
141 statusCodeExpected: 200,
142 path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`,
148 for (const directory of [ 'test1/redundancy', 'test2/videos' ]) {
149 const files = await readdir(join(root(), directory))
150 expect(files).to.have.length.at.least(4)
152 for (const resolution of [ 240, 360, 480, 720 ]) {
153 expect(files.find(f => f === `${videoUUID}-${resolution}.mp4`)).to.not.be.undefined
158 async function check0PlaylistRedundancies (videoUUID?: string) {
159 if (!videoUUID) videoUUID = video1Server2UUID
161 for (const server of servers) {
162 // With token to avoid issues with video follow constraints
163 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
164 const video: VideoDetails = res.body
166 expect(video.streamingPlaylists).to.be.an('array')
167 expect(video.streamingPlaylists).to.have.lengthOf(1)
168 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
172 async function check1PlaylistRedundancies (videoUUID?: string) {
173 if (!videoUUID) videoUUID = video1Server2UUID
175 for (const server of servers) {
176 const res = await getVideo(server.url, videoUUID)
177 const video: VideoDetails = res.body
179 expect(video.streamingPlaylists).to.have.lengthOf(1)
180 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
182 const redundancy = video.streamingPlaylists[0].redundancies[0]
184 expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
187 const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls'
188 const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
190 const res = await getVideo(servers[0].url, videoUUID)
191 const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
193 for (const resolution of [ 240, 360, 480, 720 ]) {
194 await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
197 for (const directory of [ 'test1/redundancy/hls', 'test2/streaming-playlists/hls' ]) {
198 const files = await readdir(join(root(), directory, videoUUID))
199 expect(files).to.have.length.at.least(4)
201 for (const resolution of [ 240, 360, 480, 720 ]) {
202 const filename = `${videoUUID}-${resolution}-fragmented.mp4`
204 expect(files.find(f => f === filename)).to.not.be.undefined
209 async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
210 const res = await getStats(servers[0].url)
211 const data: ServerStats = res.body
213 expect(data.videosRedundancy).to.have.lengthOf(1)
214 const stat = data.videosRedundancy[0]
216 expect(stat.strategy).to.equal(strategy)
217 expect(stat.totalSize).to.equal(204800)
218 expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
219 expect(stat.totalVideoFiles).to.equal(4)
220 expect(stat.totalVideos).to.equal(1)
223 async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
224 const res = await getStats(servers[0].url)
225 const data: ServerStats = res.body
227 expect(data.videosRedundancy).to.have.lengthOf(1)
229 const stat = data.videosRedundancy[0]
230 expect(stat.strategy).to.equal(strategy)
231 expect(stat.totalSize).to.equal(204800)
232 expect(stat.totalUsed).to.equal(0)
233 expect(stat.totalVideoFiles).to.equal(0)
234 expect(stat.totalVideos).to.equal(0)
237 async function enableRedundancyOnServer1 () {
238 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true)
240 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
241 const follows: ActorFollow[] = res.body.data
242 const server2 = follows.find(f => f.following.host === 'localhost:9002')
243 const server3 = follows.find(f => f.following.host === 'localhost:9003')
245 expect(server3).to.not.be.undefined
246 expect(server3.following.hostRedundancyAllowed).to.be.false
248 expect(server2).to.not.be.undefined
249 expect(server2.following.hostRedundancyAllowed).to.be.true
252 async function disableRedundancyOnServer1 () {
253 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false)
255 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, '-createdAt')
256 const follows: ActorFollow[] = res.body.data
257 const server2 = follows.find(f => f.following.host === 'localhost:9002')
258 const server3 = follows.find(f => f.following.host === 'localhost:9003')
260 expect(server3).to.not.be.undefined
261 expect(server3.following.hostRedundancyAllowed).to.be.false
263 expect(server2).to.not.be.undefined
264 expect(server2.following.hostRedundancyAllowed).to.be.false
267 async function cleanServers () {
268 killallServers(servers)
271 describe('Test videos redundancy', function () {
273 describe('With most-views strategy', function () {
274 const strategy = 'most-views'
279 return runServers(strategy)
282 it('Should have 1 webseed on the first video', async function () {
283 await check1WebSeed()
284 await check0PlaylistRedundancies()
285 await checkStatsWith1Webseed(strategy)
288 it('Should enable redundancy on server 1', function () {
289 return enableRedundancyOnServer1()
292 it('Should have 2 webseeds on the first video', async function () {
295 await waitJobs(servers)
296 await waitUntilLog(servers[0], 'Duplicated ', 5)
297 await waitJobs(servers)
299 await check2Webseeds()
300 await check1PlaylistRedundancies()
301 await checkStatsWith2Webseed(strategy)
304 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
307 await disableRedundancyOnServer1()
309 await waitJobs(servers)
312 await check1WebSeed()
313 await check0PlaylistRedundancies()
315 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos', join('playlists', 'hls') ])
319 return cleanServers()
323 describe('With trending strategy', function () {
324 const strategy = 'trending'
329 return runServers(strategy)
332 it('Should have 1 webseed on the first video', async function () {
333 await check1WebSeed()
334 await check0PlaylistRedundancies()
335 await checkStatsWith1Webseed(strategy)
338 it('Should enable redundancy on server 1', function () {
339 return enableRedundancyOnServer1()
342 it('Should have 2 webseeds on the first video', async function () {
345 await waitJobs(servers)
346 await waitUntilLog(servers[0], 'Duplicated ', 5)
347 await waitJobs(servers)
349 await check2Webseeds()
350 await check1PlaylistRedundancies()
351 await checkStatsWith2Webseed(strategy)
354 it('Should unfollow on server 1 and remove duplicated videos', async function () {
357 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
359 await waitJobs(servers)
362 await check1WebSeed()
363 await check0PlaylistRedundancies()
365 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
369 return cleanServers()
373 describe('With recently added strategy', function () {
374 const strategy = 'recently-added'
379 return runServers(strategy, { min_views: 3 })
382 it('Should have 1 webseed on the first video', async function () {
383 await check1WebSeed()
384 await check0PlaylistRedundancies()
385 await checkStatsWith1Webseed(strategy)
388 it('Should enable redundancy on server 1', function () {
389 return enableRedundancyOnServer1()
392 it('Should still have 1 webseed on the first video', async function () {
395 await waitJobs(servers)
397 await waitJobs(servers)
399 await check1WebSeed()
400 await check0PlaylistRedundancies()
401 await checkStatsWith1Webseed(strategy)
404 it('Should view 2 times the first video to have > min_views config', async function () {
407 await viewVideo(servers[ 0 ].url, video1Server2UUID)
408 await viewVideo(servers[ 2 ].url, video1Server2UUID)
411 await waitJobs(servers)
414 it('Should have 2 webseeds on the first video', async function () {
417 await waitJobs(servers)
418 await waitUntilLog(servers[0], 'Duplicated ', 5)
419 await waitJobs(servers)
421 await check2Webseeds()
422 await check1PlaylistRedundancies()
423 await checkStatsWith2Webseed(strategy)
426 it('Should remove the video and the redundancy files', async function () {
429 await removeVideo(servers[1].url, servers[1].accessToken, video1Server2UUID)
431 await waitJobs(servers)
433 for (const server of servers) {
434 await checkVideoFilesWereRemoved(video1Server2UUID, server.serverNumber)
439 return cleanServers()
443 describe('Test expiration', function () {
444 const strategy = 'recently-added'
446 async function checkContains (servers: ServerInfo[], str: string) {
447 for (const server of servers) {
448 const res = await getVideo(server.url, video1Server2UUID)
449 const video: VideoDetails = res.body
451 for (const f of video.files) {
452 expect(f.magnetUri).to.contain(str)
457 async function checkNotContains (servers: ServerInfo[], str: string) {
458 for (const server of servers) {
459 const res = await getVideo(server.url, video1Server2UUID)
460 const video: VideoDetails = res.body
462 for (const f of video.files) {
463 expect(f.magnetUri).to.not.contain(str)
468 before(async function () {
471 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
473 await enableRedundancyOnServer1()
476 it('Should still have 2 webseeds after 10 seconds', async function () {
482 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
484 // Maybe a server deleted a redundancy in the scheduler
487 await checkContains(servers, 'http%3A%2F%2Flocalhost%3A9001')
491 it('Should stop server 1 and expire video redundancy', async function () {
494 killallServers([ servers[0] ])
498 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
502 return killallServers([ servers[1], servers[2] ])
506 describe('Test file replacement', function () {
507 let video2Server2UUID: string
508 const strategy = 'recently-added'
510 before(async function () {
513 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
515 await enableRedundancyOnServer1()
517 await waitJobs(servers)
518 await waitUntilLog(servers[0], 'Duplicated ', 5)
519 await waitJobs(servers)
521 await check2Webseeds()
522 await check1PlaylistRedundancies()
523 await checkStatsWith2Webseed(strategy)
525 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' })
526 video2Server2UUID = res.body.video.uuid
529 it('Should cache video 2 webseeds on the first video', async function () {
532 await waitJobs(servers)
536 while (checked === false) {
540 await check1WebSeed(video1Server2UUID)
541 await check0PlaylistRedundancies(video1Server2UUID)
542 await check2Webseeds(video2Server2UUID)
543 await check1PlaylistRedundancies(video2Server2UUID)
552 it('Should disable strategy and remove redundancies', async function () {
555 await waitJobs(servers)
557 killallServers([ servers[ 0 ] ])
558 await reRunServer(servers[ 0 ], {
561 check_interval: '1 second',
567 await waitJobs(servers)
569 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ join('redundancy', 'hls') ])
573 return cleanServers()