aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/tests
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-24 13:36:47 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-04-15 09:49:35 +0200
commitb211106695bb82f6c32e53306081b5262c3d109d (patch)
treefa187de1c33b0956665f5362e29af6b0f6d8bb57 /server/tests
parent69d48ee30c9d47cddf0c3c047dc99a99dcb6e894 (diff)
downloadPeerTube-b211106695bb82f6c32e53306081b5262c3d109d.tar.gz
PeerTube-b211106695bb82f6c32e53306081b5262c3d109d.tar.zst
PeerTube-b211106695bb82f6c32e53306081b5262c3d109d.zip
Support video views/viewers stats in server
* Add "currentTime" and "event" body params to view endpoint * Merge watching and view endpoints * Introduce WatchAction AP activity * Add tables to store viewer information of local videos * Add endpoints to fetch video views/viewers stats of local videos * Refactor views/viewers handlers * Support "views" and "viewers" counters for both VOD and live videos
Diffstat (limited to 'server/tests')
-rw-r--r--server/tests/api/activitypub/client.ts20
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/videos-history.ts46
-rw-r--r--server/tests/api/check-params/views.ts157
-rw-r--r--server/tests/api/live/index.ts1
-rw-r--r--server/tests/api/live/live-socket-messages.ts4
-rw-r--r--server/tests/api/live/live-views.ts132
-rw-r--r--server/tests/api/redundancy/redundancy.ts10
-rw-r--r--server/tests/api/server/reverse-proxy.ts16
-rw-r--r--server/tests/api/server/stats.ts2
-rw-r--r--server/tests/api/videos/index.ts1
-rw-r--r--server/tests/api/videos/multiple-servers.ts40
-rw-r--r--server/tests/api/videos/single-server.ts17
-rw-r--r--server/tests/api/videos/video-channels.ts4
-rw-r--r--server/tests/api/videos/videos-history.ts73
-rw-r--r--server/tests/api/views/index.ts5
-rw-r--r--server/tests/api/views/video-views-counter.ts155
-rw-r--r--server/tests/api/views/video-views-overall-stats.ts291
-rw-r--r--server/tests/api/views/video-views-retention-stats.ts56
-rw-r--r--server/tests/api/views/video-views-timeserie-stats.ts109
-rw-r--r--server/tests/api/views/videos-views-cleaner.ts (renamed from server/tests/api/videos/videos-views-cleaner.ts)8
-rw-r--r--server/tests/plugins/action-hooks.ts4
-rw-r--r--server/tests/plugins/plugin-helpers.ts2
-rw-r--r--server/tests/shared/index.ts1
-rw-r--r--server/tests/shared/views.ts93
25 files changed, 976 insertions, 272 deletions
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
index e69ab3cb9..655fa30d0 100644
--- a/server/tests/api/activitypub/client.ts
+++ b/server/tests/api/activitypub/client.ts
@@ -2,6 +2,8 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { processViewersStats } from '@server/tests/shared'
6import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@shared/models'
5import { 7import {
6 cleanupTests, 8 cleanupTests,
7 createMultipleServers, 9 createMultipleServers,
@@ -11,7 +13,6 @@ import {
11 setAccessTokensToServers, 13 setAccessTokensToServers,
12 setDefaultVideoChannel 14 setDefaultVideoChannel
13} from '@shared/server-commands' 15} from '@shared/server-commands'
14import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models'
15 16
16const expect = chai.expect 17const expect = chai.expect
17 18
@@ -115,6 +116,23 @@ describe('Test activitypub', function () {
115 expect(res.header.location).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + video.uuid) 116 expect(res.header.location).to.equal('http://localhost:' + servers[0].port + '/videos/watch/' + video.uuid)
116 }) 117 })
117 118
119 it('Should return the watch action', async function () {
120 this.timeout(50000)
121
122 await servers[0].views.simulateViewer({ id: video.uuid, currentTimes: [ 0, 2 ] })
123 await processViewersStats(servers)
124
125 const res = await makeActivityPubGetRequest(servers[0].url, '/videos/local-viewer/1', HttpStatusCode.OK_200)
126
127 const object: WatchActionObject = res.body
128 expect(object.type).to.equal('WatchAction')
129 expect(object.duration).to.equal('PT2S')
130 expect(object.actionStatus).to.equal('CompletedActionStatus')
131 expect(object.watchSections).to.have.lengthOf(1)
132 expect(object.watchSections[0].startTimestamp).to.equal(0)
133 expect(object.watchSections[0].endTimestamp).to.equal(2)
134 })
135
118 after(async function () { 136 after(async function () {
119 await cleanupTests(servers) 137 await cleanupTests(servers)
120 }) 138 })
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index c9adeef4a..259d7e783 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -33,3 +33,4 @@ import './videos-common-filters'
33import './video-files' 33import './video-files'
34import './videos-history' 34import './videos-history'
35import './videos-overviews' 35import './videos-overviews'
36import './views'
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
index 82f38b7b4..c1b2d8bf3 100644
--- a/server/tests/api/check-params/videos-history.ts
+++ b/server/tests/api/check-params/videos-history.ts
@@ -17,7 +17,7 @@ import {
17describe('Test videos history API validator', function () { 17describe('Test videos history API validator', function () {
18 const myHistoryPath = '/api/v1/users/me/history/videos' 18 const myHistoryPath = '/api/v1/users/me/history/videos'
19 const myHistoryRemove = myHistoryPath + '/remove' 19 const myHistoryRemove = myHistoryPath + '/remove'
20 let watchingPath: string 20 let viewPath: string
21 let server: PeerTubeServer 21 let server: PeerTubeServer
22 let videoId: number 22 let videoId: number
23 23
@@ -31,51 +31,15 @@ describe('Test videos history API validator', function () {
31 await setAccessTokensToServers([ server ]) 31 await setAccessTokensToServers([ server ])
32 32
33 const { id, uuid } = await server.videos.upload() 33 const { id, uuid } = await server.videos.upload()
34 watchingPath = '/api/v1/videos/' + uuid + '/watching' 34 viewPath = '/api/v1/videos/' + uuid + '/views'
35 videoId = id 35 videoId = id
36 }) 36 })
37 37
38 describe('When notifying a user is watching a video', function () { 38 describe('When notifying a user is watching a video', function () {
39 39
40 it('Should fail with an unauthenticated user', async function () { 40 it('Should fail with a bad token', async function () {
41 const fields = { currentTime: 5 }
42 await makePutBodyRequest({ url: server.url, path: watchingPath, fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
43 })
44
45 it('Should fail with an incorrect video id', async function () {
46 const fields = { currentTime: 5 } 41 const fields = { currentTime: 5 }
47 const path = '/api/v1/videos/blabla/watching' 42 await makePutBodyRequest({ url: server.url, path: viewPath, fields, token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
48 await makePutBodyRequest({
49 url: server.url,
50 path,
51 fields,
52 token: server.accessToken,
53 expectedStatus: HttpStatusCode.BAD_REQUEST_400
54 })
55 })
56
57 it('Should fail with an unknown video', async function () {
58 const fields = { currentTime: 5 }
59 const path = '/api/v1/videos/d91fff41-c24d-4508-8e13-3bd5902c3b02/watching'
60
61 await makePutBodyRequest({
62 url: server.url,
63 path,
64 fields,
65 token: server.accessToken,
66 expectedStatus: HttpStatusCode.NOT_FOUND_404
67 })
68 })
69
70 it('Should fail with a bad current time', async function () {
71 const fields = { currentTime: 'hello' }
72 await makePutBodyRequest({
73 url: server.url,
74 path: watchingPath,
75 fields,
76 token: server.accessToken,
77 expectedStatus: HttpStatusCode.BAD_REQUEST_400
78 })
79 }) 43 })
80 44
81 it('Should succeed with the correct parameters', async function () { 45 it('Should succeed with the correct parameters', async function () {
@@ -83,7 +47,7 @@ describe('Test videos history API validator', function () {
83 47
84 await makePutBodyRequest({ 48 await makePutBodyRequest({
85 url: server.url, 49 url: server.url,
86 path: watchingPath, 50 path: viewPath,
87 fields, 51 fields,
88 token: server.accessToken, 52 token: server.accessToken,
89 expectedStatus: HttpStatusCode.NO_CONTENT_204 53 expectedStatus: HttpStatusCode.NO_CONTENT_204
diff --git a/server/tests/api/check-params/views.ts b/server/tests/api/check-params/views.ts
new file mode 100644
index 000000000..185b04af1
--- /dev/null
+++ b/server/tests/api/check-params/views.ts
@@ -0,0 +1,157 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { HttpStatusCode, VideoPrivacy } from '@shared/models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultVideoChannel
12} from '@shared/server-commands'
13
14describe('Test videos views', function () {
15 let servers: PeerTubeServer[]
16 let liveVideoId: string
17 let videoId: string
18 let remoteVideoId: string
19 let userAccessToken: string
20
21 before(async function () {
22 this.timeout(30000)
23
24 servers = await createMultipleServers(2)
25 await setAccessTokensToServers(servers)
26 await setDefaultVideoChannel(servers)
27
28 await servers[0].config.enableLive({ allowReplay: false, transcoding: false });
29
30 ({ uuid: videoId } = await servers[0].videos.quickUpload({ name: 'video' }));
31 ({ uuid: remoteVideoId } = await servers[1].videos.quickUpload({ name: 'video' }));
32 ({ uuid: liveVideoId } = await servers[0].live.create({
33 fields: {
34 name: 'live',
35 privacy: VideoPrivacy.PUBLIC,
36 channelId: servers[0].store.channel.id
37 }
38 }))
39
40 userAccessToken = await servers[0].users.generateUserAndToken('user')
41
42 await doubleFollow(servers[0], servers[1])
43 })
44
45 describe('When viewing a video', async function () {
46
47 // TODO: implement it when we'll remove backward compatibility in REST API
48 it('Should fail without current time')
49
50 it('Should fail with an invalid current time', async function () {
51 await servers[0].views.view({ id: videoId, currentTime: -1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
52 await servers[0].views.view({ id: videoId, currentTime: 10, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
53 })
54
55 it('Should succeed with correct parameters', async function () {
56 await servers[0].views.view({ id: videoId, currentTime: 1 })
57 })
58 })
59
60 describe('When getting overall stats', function () {
61
62 it('Should fail with a remote video', async function () {
63 await servers[0].videoStats.getOverallStats({ videoId: remoteVideoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
64 })
65
66 it('Should fail without token', async function () {
67 await servers[0].videoStats.getOverallStats({ videoId: videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
68 })
69
70 it('Should fail with another token', async function () {
71 await servers[0].videoStats.getOverallStats({
72 videoId: videoId,
73 token: userAccessToken,
74 expectedStatus: HttpStatusCode.FORBIDDEN_403
75 })
76 })
77
78 it('Should succeed with the correct parameters', async function () {
79 await servers[0].videoStats.getOverallStats({ videoId })
80 })
81 })
82
83 describe('When getting timeserie stats', function () {
84
85 it('Should fail with a remote video', async function () {
86 await servers[0].videoStats.getTimeserieStats({
87 videoId: remoteVideoId,
88 metric: 'viewers',
89 expectedStatus: HttpStatusCode.FORBIDDEN_403
90 })
91 })
92
93 it('Should fail without token', async function () {
94 await servers[0].videoStats.getTimeserieStats({
95 videoId: videoId,
96 token: null,
97 metric: 'viewers',
98 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
99 })
100 })
101
102 it('Should fail with another token', async function () {
103 await servers[0].videoStats.getTimeserieStats({
104 videoId: videoId,
105 token: userAccessToken,
106 metric: 'viewers',
107 expectedStatus: HttpStatusCode.FORBIDDEN_403
108 })
109 })
110
111 it('Should fail with an invalid metric', async function () {
112 await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
113 })
114
115 it('Should succeed with the correct parameters', async function () {
116 await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' })
117 })
118 })
119
120 describe('When getting retention stats', function () {
121
122 it('Should fail with a remote video', async function () {
123 await servers[0].videoStats.getRetentionStats({
124 videoId: remoteVideoId,
125 expectedStatus: HttpStatusCode.FORBIDDEN_403
126 })
127 })
128
129 it('Should fail without token', async function () {
130 await servers[0].videoStats.getRetentionStats({
131 videoId: videoId,
132 token: null,
133 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
134 })
135 })
136
137 it('Should fail with another token', async function () {
138 await servers[0].videoStats.getRetentionStats({
139 videoId: videoId,
140 token: userAccessToken,
141 expectedStatus: HttpStatusCode.FORBIDDEN_403
142 })
143 })
144
145 it('Should fail on live video', async function () {
146 await servers[0].videoStats.getRetentionStats({ videoId: liveVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
147 })
148
149 it('Should succeed with the correct parameters', async function () {
150 await servers[0].videoStats.getRetentionStats({ videoId })
151 })
152 })
153
154 after(async function () {
155 await cleanupTests(servers)
156 })
157})
diff --git a/server/tests/api/live/index.ts b/server/tests/api/live/index.ts
index 105416b8d..71bc150d8 100644
--- a/server/tests/api/live/index.ts
+++ b/server/tests/api/live/index.ts
@@ -3,5 +3,4 @@ import './live-socket-messages'
3import './live-permanent' 3import './live-permanent'
4import './live-rtmps' 4import './live-rtmps'
5import './live-save-replay' 5import './live-save-replay'
6import './live-views'
7import './live' 6import './live'
diff --git a/server/tests/api/live/live-socket-messages.ts b/server/tests/api/live/live-socket-messages.ts
index 50b16443e..7668ed5b9 100644
--- a/server/tests/api/live/live-socket-messages.ts
+++ b/server/tests/api/live/live-socket-messages.ts
@@ -140,8 +140,8 @@ describe('Test live', function () {
140 expect(localLastVideoViews).to.equal(0) 140 expect(localLastVideoViews).to.equal(0)
141 expect(remoteLastVideoViews).to.equal(0) 141 expect(remoteLastVideoViews).to.equal(0)
142 142
143 await servers[0].videos.view({ id: liveVideoUUID }) 143 await servers[0].views.simulateView({ id: liveVideoUUID })
144 await servers[1].videos.view({ id: liveVideoUUID }) 144 await servers[1].views.simulateView({ id: liveVideoUUID })
145 145
146 await waitJobs(servers) 146 await waitJobs(servers)
147 147
diff --git a/server/tests/api/live/live-views.ts b/server/tests/api/live/live-views.ts
deleted file mode 100644
index 446d0913c..000000000
--- a/server/tests/api/live/live-views.ts
+++ /dev/null
@@ -1,132 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { FfmpegCommand } from 'fluent-ffmpeg'
6import { wait } from '@shared/core-utils'
7import { VideoPrivacy } from '@shared/models'
8import {
9 cleanupTests,
10 createMultipleServers,
11 doubleFollow,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 setDefaultVideoChannel,
15 stopFfmpeg,
16 waitJobs,
17 waitUntilLivePublishedOnAllServers
18} from '@shared/server-commands'
19
20const expect = chai.expect
21
22describe('Live views', function () {
23 let servers: PeerTubeServer[] = []
24
25 before(async function () {
26 this.timeout(120000)
27
28 servers = await createMultipleServers(2)
29
30 // Get the access tokens
31 await setAccessTokensToServers(servers)
32 await setDefaultVideoChannel(servers)
33
34 await servers[0].config.updateCustomSubConfig({
35 newConfig: {
36 live: {
37 enabled: true,
38 allowReplay: true,
39 transcoding: {
40 enabled: false
41 }
42 }
43 }
44 })
45
46 // Server 1 and server 2 follow each other
47 await doubleFollow(servers[0], servers[1])
48 })
49
50 let liveVideoId: string
51 let command: FfmpegCommand
52
53 async function countViewers (expectedViewers: number) {
54 for (const server of servers) {
55 const video = await server.videos.get({ id: liveVideoId })
56 expect(video.viewers).to.equal(expectedViewers)
57 }
58 }
59
60 async function countViews (expectedViews: number) {
61 for (const server of servers) {
62 const video = await server.videos.get({ id: liveVideoId })
63 expect(video.views).to.equal(expectedViews)
64 }
65 }
66
67 before(async function () {
68 this.timeout(30000)
69
70 const liveAttributes = {
71 name: 'live video',
72 channelId: servers[0].store.channel.id,
73 privacy: VideoPrivacy.PUBLIC
74 }
75
76 const live = await servers[0].live.create({ fields: liveAttributes })
77 liveVideoId = live.uuid
78
79 command = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId })
80 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
81 await waitJobs(servers)
82 })
83
84 it('Should display no views and viewers for a live', async function () {
85 await countViews(0)
86 await countViewers(0)
87 })
88
89 it('Should view a live twice and display 1 view/viewer', async function () {
90 this.timeout(30000)
91
92 await servers[0].videos.view({ id: liveVideoId })
93 await servers[0].videos.view({ id: liveVideoId })
94
95 await waitJobs(servers)
96 await countViewers(1)
97
98 await wait(7000)
99 await countViews(1)
100 })
101
102 it('Should wait and display 0 viewers while still have 1 view', async function () {
103 this.timeout(30000)
104
105 await wait(12000)
106 await waitJobs(servers)
107
108 await countViews(1)
109 await countViewers(0)
110 })
111
112 it('Should view a live on a remote and on local and display 2 viewers and 3 views', async function () {
113 this.timeout(30000)
114
115 await servers[0].videos.view({ id: liveVideoId })
116 await servers[1].videos.view({ id: liveVideoId })
117 await servers[1].videos.view({ id: liveVideoId })
118 await waitJobs(servers)
119
120 await countViewers(2)
121
122 await wait(7000)
123 await waitJobs(servers)
124
125 await countViews(3)
126 })
127
128 after(async function () {
129 await stopFfmpeg(command)
130 await cleanupTests(servers)
131 })
132})
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 3f2286278..0f7ffcb4c 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -87,7 +87,7 @@ async function createServers (strategy: VideoRedundancyStrategy | null, addition
87 const { id } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } }) 87 const { id } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
88 video1Server2 = await servers[1].videos.get({ id }) 88 video1Server2 = await servers[1].videos.get({ id })
89 89
90 await servers[1].videos.view({ id }) 90 await servers[1].views.simulateView({ id })
91 } 91 }
92 92
93 await waitJobs(servers) 93 await waitJobs(servers)
@@ -447,8 +447,8 @@ describe('Test videos redundancy', function () {
447 it('Should view 2 times the first video to have > min_views config', async function () { 447 it('Should view 2 times the first video to have > min_views config', async function () {
448 this.timeout(80000) 448 this.timeout(80000)
449 449
450 await servers[0].videos.view({ id: video1Server2.uuid }) 450 await servers[0].views.simulateView({ id: video1Server2.uuid })
451 await servers[2].videos.view({ id: video1Server2.uuid }) 451 await servers[2].views.simulateView({ id: video1Server2.uuid })
452 452
453 await wait(10000) 453 await wait(10000)
454 await waitJobs(servers) 454 await waitJobs(servers)
@@ -516,8 +516,8 @@ describe('Test videos redundancy', function () {
516 it('Should have 1 redundancy on the first video', async function () { 516 it('Should have 1 redundancy on the first video', async function () {
517 this.timeout(160000) 517 this.timeout(160000)
518 518
519 await servers[0].videos.view({ id: video1Server2.uuid }) 519 await servers[0].views.simulateView({ id: video1Server2.uuid })
520 await servers[2].videos.view({ id: video1Server2.uuid }) 520 await servers[2].views.simulateView({ id: video1Server2.uuid })
521 521
522 await wait(10000) 522 await wait(10000)
523 await waitJobs(servers) 523 await waitJobs(servers)
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts
index 968d98e96..fa2063536 100644
--- a/server/tests/api/server/reverse-proxy.ts
+++ b/server/tests/api/server/reverse-proxy.ts
@@ -41,8 +41,8 @@ describe('Test application behind a reverse proxy', function () {
41 it('Should view a video only once with the same IP by default', async function () { 41 it('Should view a video only once with the same IP by default', async function () {
42 this.timeout(20000) 42 this.timeout(20000)
43 43
44 await server.videos.view({ id: videoId }) 44 await server.views.simulateView({ id: videoId })
45 await server.videos.view({ id: videoId }) 45 await server.views.simulateView({ id: videoId })
46 46
47 // Wait the repeatable job 47 // Wait the repeatable job
48 await wait(8000) 48 await wait(8000)
@@ -54,8 +54,8 @@ describe('Test application behind a reverse proxy', function () {
54 it('Should view a video 2 times with the X-Forwarded-For header set', async function () { 54 it('Should view a video 2 times with the X-Forwarded-For header set', async function () {
55 this.timeout(20000) 55 this.timeout(20000)
56 56
57 await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.1,127.0.0.1' }) 57 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.1,127.0.0.1' })
58 await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.2,127.0.0.1' }) 58 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.2,127.0.0.1' })
59 59
60 // Wait the repeatable job 60 // Wait the repeatable job
61 await wait(8000) 61 await wait(8000)
@@ -67,8 +67,8 @@ describe('Test application behind a reverse proxy', function () {
67 it('Should view a video only once with the same client IP in the X-Forwarded-For header', async function () { 67 it('Should view a video only once with the same client IP in the X-Forwarded-For header', async function () {
68 this.timeout(20000) 68 this.timeout(20000)
69 69
70 await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.4,0.0.0.3,::ffff:127.0.0.1' }) 70 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.4,0.0.0.3,::ffff:127.0.0.1' })
71 await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.5,0.0.0.3,127.0.0.1' }) 71 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.5,0.0.0.3,127.0.0.1' })
72 72
73 // Wait the repeatable job 73 // Wait the repeatable job
74 await wait(8000) 74 await wait(8000)
@@ -80,8 +80,8 @@ describe('Test application behind a reverse proxy', function () {
80 it('Should view a video two times with a different client IP in the X-Forwarded-For header', async function () { 80 it('Should view a video two times with a different client IP in the X-Forwarded-For header', async function () {
81 this.timeout(20000) 81 this.timeout(20000)
82 82
83 await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.6,127.0.0.1' }) 83 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.6,127.0.0.1' })
84 await server.videos.view({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.7,127.0.0.1' }) 84 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.7,127.0.0.1' })
85 85
86 // Wait the repeatable job 86 // Wait the repeatable job
87 await wait(8000) 87 await wait(8000)
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts
index 2296c0cb9..a9ae236fb 100644
--- a/server/tests/api/server/stats.ts
+++ b/server/tests/api/server/stats.ts
@@ -38,7 +38,7 @@ describe('Test stats (excluding redundancy)', function () {
38 38
39 await servers[0].comments.createThread({ videoId: uuid, text: 'comment' }) 39 await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
40 40
41 await servers[0].videos.view({ id: uuid }) 41 await servers[0].views.simulateView({ id: uuid })
42 42
43 // Wait the video views repeatable job 43 // Wait the video views repeatable job
44 await wait(8000) 44 await wait(8000)
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index 7dc826353..27b119f30 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -16,4 +16,3 @@ import './video-schedule-update'
16import './videos-common-filters' 16import './videos-common-filters'
17import './videos-history' 17import './videos-history'
18import './videos-overview' 18import './videos-overview'
19import './videos-views-cleaner'
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index a9df262dc..84c1515a3 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -504,21 +504,22 @@ describe('Test multiple servers', function () {
504 it('Should view multiple videos on owned servers', async function () { 504 it('Should view multiple videos on owned servers', async function () {
505 this.timeout(30000) 505 this.timeout(30000)
506 506
507 await servers[2].videos.view({ id: localVideosServer3[0] }) 507 await servers[2].views.simulateView({ id: localVideosServer3[0] })
508 await wait(1000) 508 await wait(1000)
509 509
510 await servers[2].videos.view({ id: localVideosServer3[0] }) 510 await servers[2].views.simulateView({ id: localVideosServer3[0] })
511 await servers[2].videos.view({ id: localVideosServer3[1] }) 511 await servers[2].views.simulateView({ id: localVideosServer3[1] })
512 512
513 await wait(1000) 513 await wait(1000)
514 514
515 await servers[2].videos.view({ id: localVideosServer3[0] }) 515 await servers[2].views.simulateView({ id: localVideosServer3[0] })
516 await servers[2].videos.view({ id: localVideosServer3[0] }) 516 await servers[2].views.simulateView({ id: localVideosServer3[0] })
517 517
518 await waitJobs(servers) 518 await waitJobs(servers)
519 519
520 // Wait the repeatable job 520 for (const server of servers) {
521 await wait(6000) 521 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
522 }
522 523
523 await waitJobs(servers) 524 await waitJobs(servers)
524 525
@@ -537,23 +538,24 @@ describe('Test multiple servers', function () {
537 this.timeout(45000) 538 this.timeout(45000)
538 539
539 const tasks: Promise<any>[] = [] 540 const tasks: Promise<any>[] = []
540 tasks.push(servers[0].videos.view({ id: remoteVideosServer1[0] })) 541 tasks.push(servers[0].views.simulateView({ id: remoteVideosServer1[0] }))
541 tasks.push(servers[1].videos.view({ id: remoteVideosServer2[0] })) 542 tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] }))
542 tasks.push(servers[1].videos.view({ id: remoteVideosServer2[0] })) 543 tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] }))
543 tasks.push(servers[2].videos.view({ id: remoteVideosServer3[0] })) 544 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[0] }))
544 tasks.push(servers[2].videos.view({ id: remoteVideosServer3[1] })) 545 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
545 tasks.push(servers[2].videos.view({ id: remoteVideosServer3[1] })) 546 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
546 tasks.push(servers[2].videos.view({ id: remoteVideosServer3[1] })) 547 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
547 tasks.push(servers[2].videos.view({ id: localVideosServer3[1] })) 548 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
548 tasks.push(servers[2].videos.view({ id: localVideosServer3[1] })) 549 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
549 tasks.push(servers[2].videos.view({ id: localVideosServer3[1] })) 550 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
550 551
551 await Promise.all(tasks) 552 await Promise.all(tasks)
552 553
553 await waitJobs(servers) 554 await waitJobs(servers)
554 555
555 // Wait the repeatable job 556 for (const server of servers) {
556 await wait(16000) 557 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
558 }
557 559
558 await waitJobs(servers) 560 await waitJobs(servers)
559 561
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index d37043aef..0e429fef7 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -179,22 +179,21 @@ describe('Test a single server', function () {
179 it('Should have the views updated', async function () { 179 it('Should have the views updated', async function () {
180 this.timeout(20000) 180 this.timeout(20000)
181 181
182 await server.videos.view({ id: videoId }) 182 await server.views.simulateView({ id: videoId })
183 await server.videos.view({ id: videoId }) 183 await server.views.simulateView({ id: videoId })
184 await server.videos.view({ id: videoId }) 184 await server.views.simulateView({ id: videoId })
185 185
186 await wait(1500) 186 await wait(1500)
187 187
188 await server.videos.view({ id: videoId }) 188 await server.views.simulateView({ id: videoId })
189 await server.videos.view({ id: videoId }) 189 await server.views.simulateView({ id: videoId })
190 190
191 await wait(1500) 191 await wait(1500)
192 192
193 await server.videos.view({ id: videoId }) 193 await server.views.simulateView({ id: videoId })
194 await server.videos.view({ id: videoId }) 194 await server.views.simulateView({ id: videoId })
195 195
196 // Wait the repeatable job 196 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
197 await wait(8000)
198 197
199 const video = await server.videos.get({ id: videoId }) 198 const video = await server.videos.get({ id: videoId })
200 expect(video.views).to.equal(3) 199 expect(video.views).to.equal(3)
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 09a4bfa70..6f495c42d 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -466,8 +466,8 @@ describe('Test video channels', function () {
466 466
467 { 467 {
468 // video has been posted on channel servers[0].store.videoChannel.id since last update 468 // video has been posted on channel servers[0].store.videoChannel.id since last update
469 await servers[0].videos.view({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1' }) 469 await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1' })
470 await servers[0].videos.view({ id: videoUUID, xForwardedFor: '0.0.0.2,127.0.0.1' }) 470 await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.2,127.0.0.1' })
471 471
472 // Wait the repeatable job 472 // Wait the repeatable job
473 await wait(8000) 473 await wait(8000)
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
index 8648c97f0..b1b3ff10a 100644
--- a/server/tests/api/videos/videos-history.ts
+++ b/server/tests/api/videos/videos-history.ts
@@ -3,15 +3,8 @@
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { wait } from '@shared/core-utils' 5import { wait } from '@shared/core-utils'
6import { HttpStatusCode, Video } from '@shared/models' 6import { Video } from '@shared/models'
7import { 7import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
8 cleanupTests,
9 createSingleServer,
10 HistoryCommand,
11 killallServers,
12 PeerTubeServer,
13 setAccessTokensToServers
14} from '@shared/server-commands'
15 8
16const expect = chai.expect 9const expect = chai.expect
17 10
@@ -23,7 +16,6 @@ describe('Test videos history', function () {
23 let video3UUID: string 16 let video3UUID: string
24 let video3WatchedDate: Date 17 let video3WatchedDate: Date
25 let userAccessToken: string 18 let userAccessToken: string
26 let command: HistoryCommand
27 19
28 before(async function () { 20 before(async function () {
29 this.timeout(30000) 21 this.timeout(30000)
@@ -32,30 +24,26 @@ describe('Test videos history', function () {
32 24
33 await setAccessTokensToServers([ server ]) 25 await setAccessTokensToServers([ server ])
34 26
35 command = server.history 27 // 10 seconds long
28 const fixture = 'video_59fps.mp4'
36 29
37 { 30 {
38 const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1' } }) 31 const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1', fixture } })
39 video1UUID = uuid 32 video1UUID = uuid
40 video1Id = id 33 video1Id = id
41 } 34 }
42 35
43 { 36 {
44 const { uuid } = await server.videos.upload({ attributes: { name: 'video 2' } }) 37 const { uuid } = await server.videos.upload({ attributes: { name: 'video 2', fixture } })
45 video2UUID = uuid 38 video2UUID = uuid
46 } 39 }
47 40
48 { 41 {
49 const { uuid } = await server.videos.upload({ attributes: { name: 'video 3' } }) 42 const { uuid } = await server.videos.upload({ attributes: { name: 'video 3', fixture } })
50 video3UUID = uuid 43 video3UUID = uuid
51 } 44 }
52 45
53 const user = { 46 userAccessToken = await server.users.generateUserAndToken('user_1')
54 username: 'user_1',
55 password: 'super password'
56 }
57 await server.users.create({ username: user.username, password: user.password })
58 userAccessToken = await server.login.getAccessToken(user)
59 }) 47 })
60 48
61 it('Should get videos, without watching history', async function () { 49 it('Should get videos, without watching history', async function () {
@@ -70,8 +58,8 @@ describe('Test videos history', function () {
70 }) 58 })
71 59
72 it('Should watch the first and second video', async function () { 60 it('Should watch the first and second video', async function () {
73 await command.watchVideo({ videoId: video2UUID, currentTime: 8 }) 61 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
74 await command.watchVideo({ videoId: video1UUID, currentTime: 3 }) 62 await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 3 })
75 }) 63 })
76 64
77 it('Should return the correct history when listing, searching and getting videos', async function () { 65 it('Should return the correct history when listing, searching and getting videos', async function () {
@@ -124,9 +112,9 @@ describe('Test videos history', function () {
124 112
125 it('Should have these videos when listing my history', async function () { 113 it('Should have these videos when listing my history', async function () {
126 video3WatchedDate = new Date() 114 video3WatchedDate = new Date()
127 await command.watchVideo({ videoId: video3UUID, currentTime: 2 }) 115 await server.views.view({ id: video3UUID, token: server.accessToken, currentTime: 2 })
128 116
129 const body = await command.list() 117 const body = await server.history.list()
130 118
131 expect(body.total).to.equal(3) 119 expect(body.total).to.equal(3)
132 120
@@ -137,14 +125,14 @@ describe('Test videos history', function () {
137 }) 125 })
138 126
139 it('Should not have videos history on another user', async function () { 127 it('Should not have videos history on another user', async function () {
140 const body = await command.list({ token: userAccessToken }) 128 const body = await server.history.list({ token: userAccessToken })
141 129
142 expect(body.total).to.equal(0) 130 expect(body.total).to.equal(0)
143 expect(body.data).to.have.lengthOf(0) 131 expect(body.data).to.have.lengthOf(0)
144 }) 132 })
145 133
146 it('Should be able to search through videos in my history', async function () { 134 it('Should be able to search through videos in my history', async function () {
147 const body = await command.list({ search: '2' }) 135 const body = await server.history.list({ search: '2' })
148 expect(body.total).to.equal(1) 136 expect(body.total).to.equal(1)
149 137
150 const videos = body.data 138 const videos = body.data
@@ -152,11 +140,11 @@ describe('Test videos history', function () {
152 }) 140 })
153 141
154 it('Should clear my history', async function () { 142 it('Should clear my history', async function () {
155 await command.removeAll({ beforeDate: video3WatchedDate.toISOString() }) 143 await server.history.removeAll({ beforeDate: video3WatchedDate.toISOString() })
156 }) 144 })
157 145
158 it('Should have my history cleared', async function () { 146 it('Should have my history cleared', async function () {
159 const body = await command.list() 147 const body = await server.history.list()
160 expect(body.total).to.equal(1) 148 expect(body.total).to.equal(1)
161 149
162 const videos = body.data 150 const videos = body.data
@@ -168,7 +156,10 @@ describe('Test videos history', function () {
168 videosHistoryEnabled: false 156 videosHistoryEnabled: false
169 }) 157 })
170 158
171 await command.watchVideo({ videoId: video2UUID, currentTime: 8, expectedStatus: HttpStatusCode.CONFLICT_409 }) 159 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
160
161 const { data } = await server.history.list()
162 expect(data[0].name).to.not.equal('video 2')
172 }) 163 })
173 164
174 it('Should re-enable videos history', async function () { 165 it('Should re-enable videos history', async function () {
@@ -176,14 +167,10 @@ describe('Test videos history', function () {
176 videosHistoryEnabled: true 167 videosHistoryEnabled: true
177 }) 168 })
178 169
179 await command.watchVideo({ videoId: video1UUID, currentTime: 8 }) 170 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
180 171
181 const body = await command.list() 172 const { data } = await server.history.list()
182 expect(body.total).to.equal(2) 173 expect(data[0].name).to.equal('video 2')
183
184 const videos = body.data
185 expect(videos[0].name).to.equal('video 1')
186 expect(videos[1].name).to.equal('video 3')
187 }) 174 })
188 175
189 it('Should not clean old history', async function () { 176 it('Should not clean old history', async function () {
@@ -197,7 +184,7 @@ describe('Test videos history', function () {
197 184
198 // Should still have history 185 // Should still have history
199 186
200 const body = await command.list() 187 const body = await server.history.list()
201 expect(body.total).to.equal(2) 188 expect(body.total).to.equal(2)
202 }) 189 })
203 190
@@ -210,25 +197,25 @@ describe('Test videos history', function () {
210 197
211 await wait(6000) 198 await wait(6000)
212 199
213 const body = await command.list() 200 const body = await server.history.list()
214 expect(body.total).to.equal(0) 201 expect(body.total).to.equal(0)
215 }) 202 })
216 203
217 it('Should delete a specific history element', async function () { 204 it('Should delete a specific history element', async function () {
218 { 205 {
219 await command.watchVideo({ videoId: video1UUID, currentTime: 4 }) 206 await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 4 })
220 await command.watchVideo({ videoId: video2UUID, currentTime: 8 }) 207 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
221 } 208 }
222 209
223 { 210 {
224 const body = await command.list() 211 const body = await server.history.list()
225 expect(body.total).to.equal(2) 212 expect(body.total).to.equal(2)
226 } 213 }
227 214
228 { 215 {
229 await command.removeElement({ videoId: video1Id }) 216 await server.history.removeElement({ videoId: video1Id })
230 217
231 const body = await command.list() 218 const body = await server.history.list()
232 expect(body.total).to.equal(1) 219 expect(body.total).to.equal(1)
233 expect(body.data[0].uuid).to.equal(video2UUID) 220 expect(body.data[0].uuid).to.equal(video2UUID)
234 } 221 }
diff --git a/server/tests/api/views/index.ts b/server/tests/api/views/index.ts
new file mode 100644
index 000000000..5e06b31fb
--- /dev/null
+++ b/server/tests/api/views/index.ts
@@ -0,0 +1,5 @@
1export * from './video-views-counter'
2export * from './video-views-overall-stats'
3export * from './video-views-retention-stats'
4export * from './video-views-timeserie-stats'
5export * from './videos-views-cleaner'
diff --git a/server/tests/api/views/video-views-counter.ts b/server/tests/api/views/video-views-counter.ts
new file mode 100644
index 000000000..b68aaa350
--- /dev/null
+++ b/server/tests/api/views/video-views-counter.ts
@@ -0,0 +1,155 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { FfmpegCommand } from 'fluent-ffmpeg'
6import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@server/tests/shared'
7import { wait } from '@shared/core-utils'
8import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@shared/server-commands'
9
10const expect = chai.expect
11
12describe('Test video views/viewers counters', function () {
13 let servers: PeerTubeServer[]
14
15 async function checkCounter (field: 'views' | 'viewers', id: string, expected: number) {
16 for (const server of servers) {
17 const video = await server.videos.get({ id })
18
19 const messageSuffix = video.isLive
20 ? 'live video'
21 : 'vod video'
22
23 expect(video[field]).to.equal(expected, `${field} not valid on server ${server.serverNumber} for ${messageSuffix} ${video.uuid}`)
24 }
25 }
26
27 before(async function () {
28 this.timeout(120000)
29
30 servers = await prepareViewsServers()
31 })
32
33 describe('Test views counter on VOD', function () {
34 let videoUUID: string
35
36 before(async function () {
37 this.timeout(30000)
38
39 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
40 videoUUID = uuid
41
42 await waitJobs(servers)
43 })
44
45 it('Should not view a video if watch time is below the threshold', async function () {
46 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 2 ] })
47 await processViewsBuffer(servers)
48
49 await checkCounter('views', videoUUID, 0)
50 })
51
52 it('Should view a video if watch time is above the threshold', async function () {
53 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
54 await processViewsBuffer(servers)
55
56 await checkCounter('views', videoUUID, 1)
57 })
58
59 it('Should not view again this video with the same IP', async function () {
60 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
61 await processViewsBuffer(servers)
62
63 await checkCounter('views', videoUUID, 1)
64 })
65
66 it('Should view the video from server 2 and send the event', async function () {
67 await servers[1].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
68 await waitJobs(servers)
69 await processViewsBuffer(servers)
70
71 await checkCounter('views', videoUUID, 2)
72 })
73 })
74
75 describe('Test views and viewers counters on live and VOD', function () {
76 let liveVideoId: string
77 let vodVideoId: string
78 let command: FfmpegCommand
79
80 before(async function () {
81 this.timeout(60000);
82
83 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
84 })
85
86 it('Should display no views and viewers', async function () {
87 await checkCounter('views', liveVideoId, 0)
88 await checkCounter('viewers', liveVideoId, 0)
89
90 await checkCounter('views', vodVideoId, 0)
91 await checkCounter('viewers', vodVideoId, 0)
92 })
93
94 it('Should view twice and display 1 view/viewer', async function () {
95 this.timeout(30000)
96
97 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
98 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
99 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
100 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
101
102 await waitJobs(servers)
103 await checkCounter('viewers', liveVideoId, 1)
104 await checkCounter('viewers', vodVideoId, 1)
105
106 await processViewsBuffer(servers)
107
108 await checkCounter('views', liveVideoId, 1)
109 await checkCounter('views', vodVideoId, 1)
110 })
111
112 it('Should wait and display 0 viewers but still have 1 view', async function () {
113 this.timeout(30000)
114
115 await wait(12000)
116 await waitJobs(servers)
117
118 await checkCounter('views', liveVideoId, 1)
119 await checkCounter('viewers', liveVideoId, 0)
120
121 await checkCounter('views', vodVideoId, 1)
122 await checkCounter('viewers', vodVideoId, 0)
123 })
124
125 it('Should view on a remote and on local and display 2 viewers and 3 views', async function () {
126 this.timeout(30000)
127
128 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
129 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
130 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
131
132 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
133 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
134 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
135
136 await waitJobs(servers)
137
138 await checkCounter('viewers', liveVideoId, 2)
139 await checkCounter('viewers', vodVideoId, 2)
140
141 await processViewsBuffer(servers)
142
143 await checkCounter('views', liveVideoId, 3)
144 await checkCounter('views', vodVideoId, 3)
145 })
146
147 after(async function () {
148 await stopFfmpeg(command)
149 })
150 })
151
152 after(async function () {
153 await cleanupTests(servers)
154 })
155})
diff --git a/server/tests/api/views/video-views-overall-stats.ts b/server/tests/api/views/video-views-overall-stats.ts
new file mode 100644
index 000000000..22761d6ec
--- /dev/null
+++ b/server/tests/api/views/video-views-overall-stats.ts
@@ -0,0 +1,291 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { FfmpegCommand } from 'fluent-ffmpeg'
6import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@server/tests/shared'
7import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@shared/server-commands'
8
9const expect = chai.expect
10
11describe('Test views overall stats', function () {
12 let servers: PeerTubeServer[]
13
14 before(async function () {
15 this.timeout(120000)
16
17 servers = await prepareViewsServers()
18 })
19
20 describe('Test rates and comments of local videos on VOD', function () {
21 let vodVideoId: string
22
23 before(async function () {
24 this.timeout(60000);
25
26 ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
27 })
28
29 it('Should have the appropriate likes', async function () {
30 this.timeout(60000)
31
32 await servers[0].videos.rate({ id: vodVideoId, rating: 'like' })
33 await servers[1].videos.rate({ id: vodVideoId, rating: 'like' })
34
35 await waitJobs(servers)
36
37 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
38
39 expect(stats.likes).to.equal(2)
40 expect(stats.dislikes).to.equal(0)
41 })
42
43 it('Should have the appropriate dislikes', async function () {
44 this.timeout(60000)
45
46 await servers[0].videos.rate({ id: vodVideoId, rating: 'dislike' })
47 await servers[1].videos.rate({ id: vodVideoId, rating: 'dislike' })
48
49 await waitJobs(servers)
50
51 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
52
53 expect(stats.likes).to.equal(0)
54 expect(stats.dislikes).to.equal(2)
55 })
56
57 it('Should have the appropriate comments', async function () {
58 this.timeout(60000)
59
60 await servers[0].comments.createThread({ videoId: vodVideoId, text: 'root' })
61 await servers[0].comments.addReplyToLastThread({ text: 'reply' })
62 await servers[1].comments.createThread({ videoId: vodVideoId, text: 'root' })
63
64 await waitJobs(servers)
65
66 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
67 expect(stats.comments).to.equal(3)
68 })
69 })
70
71 describe('Test watch time stats of local videos on live and VOD', function () {
72 let vodVideoId: string
73 let liveVideoId: string
74 let command: FfmpegCommand
75
76 before(async function () {
77 this.timeout(60000);
78
79 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
80 })
81
82 it('Should display overall stats of a video with no viewers', async function () {
83 for (const videoId of [ liveVideoId, vodVideoId ]) {
84 const stats = await servers[0].videoStats.getOverallStats({ videoId })
85
86 expect(stats.views).to.equal(0)
87 expect(stats.averageWatchTime).to.equal(0)
88 expect(stats.totalWatchTime).to.equal(0)
89 }
90 })
91
92 it('Should display overall stats with 1 viewer below the watch time limit', async function () {
93 this.timeout(60000)
94
95 for (const videoId of [ liveVideoId, vodVideoId ]) {
96 await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
97 }
98
99 await processViewersStats(servers)
100
101 for (const videoId of [ liveVideoId, vodVideoId ]) {
102 const stats = await servers[0].videoStats.getOverallStats({ videoId })
103
104 expect(stats.views).to.equal(0)
105 expect(stats.averageWatchTime).to.equal(1)
106 expect(stats.totalWatchTime).to.equal(1)
107 }
108 })
109
110 it('Should display overall stats with 2 viewers', async function () {
111 this.timeout(60000)
112
113 {
114 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
115 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35, 40 ] })
116
117 await processViewersStats(servers)
118
119 {
120 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
121 expect(stats.views).to.equal(1)
122 expect(stats.averageWatchTime).to.equal(2)
123 expect(stats.totalWatchTime).to.equal(4)
124 }
125
126 {
127 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
128 expect(stats.views).to.equal(1)
129 expect(stats.averageWatchTime).to.equal(21)
130 expect(stats.totalWatchTime).to.equal(41)
131 }
132 }
133 })
134
135 it('Should display overall stats with a remote viewer below the watch time limit', async function () {
136 this.timeout(60000)
137
138 for (const videoId of [ liveVideoId, vodVideoId ]) {
139 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 2 ] })
140 }
141
142 await processViewersStats(servers)
143
144 {
145 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
146
147 expect(stats.views).to.equal(1)
148 expect(stats.averageWatchTime).to.equal(2)
149 expect(stats.totalWatchTime).to.equal(6)
150 }
151
152 {
153 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
154
155 expect(stats.views).to.equal(1)
156 expect(stats.averageWatchTime).to.equal(14)
157 expect(stats.totalWatchTime).to.equal(43)
158 }
159 })
160
161 it('Should display overall stats with a remote viewer above the watch time limit', async function () {
162 this.timeout(60000)
163
164 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
165 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 45 ] })
166 await processViewersStats(servers)
167
168 {
169 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
170 expect(stats.views).to.equal(2)
171 expect(stats.averageWatchTime).to.equal(3)
172 expect(stats.totalWatchTime).to.equal(11)
173 }
174
175 {
176 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
177 expect(stats.views).to.equal(2)
178 expect(stats.averageWatchTime).to.equal(22)
179 expect(stats.totalWatchTime).to.equal(88)
180 }
181 })
182
183 after(async function () {
184 await stopFfmpeg(command)
185 })
186 })
187
188 describe('Test watchers peak stats of local videos on VOD', function () {
189 let videoUUID: string
190
191 before(async function () {
192 this.timeout(60000);
193
194 ({ vodVideoId: videoUUID } = await prepareViewsVideos({ servers, live: true, vod: true }))
195 })
196
197 it('Should not have watchers peak', async function () {
198 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
199
200 expect(stats.viewersPeak).to.equal(0)
201 expect(stats.viewersPeakDate).to.be.null
202 })
203
204 it('Should have watcher peak with 1 watcher', async function () {
205 this.timeout(60000)
206
207 const before = new Date()
208 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 0, 2 ] })
209 const after = new Date()
210
211 await processViewersStats(servers)
212
213 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
214
215 expect(stats.viewersPeak).to.equal(1)
216 expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after)
217 })
218
219 it('Should have watcher peak with 2 watchers', async function () {
220 this.timeout(60000)
221
222 const before = new Date()
223 await servers[0].views.view({ id: videoUUID, currentTime: 0 })
224 await servers[1].views.view({ id: videoUUID, currentTime: 0 })
225 await servers[0].views.view({ id: videoUUID, currentTime: 2 })
226 await servers[1].views.view({ id: videoUUID, currentTime: 2 })
227 const after = new Date()
228
229 await processViewersStats(servers)
230
231 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
232
233 expect(stats.viewersPeak).to.equal(2)
234 expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after)
235 })
236 })
237
238 describe('Test countries', function () {
239
240 it('Should not report countries if geoip is disabled', async function () {
241 this.timeout(60000)
242
243 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
244 await waitJobs(servers)
245
246 await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
247
248 await processViewersStats(servers)
249
250 const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
251 expect(stats.countries).to.have.lengthOf(0)
252 })
253
254 it('Should report countries if geoip is enabled', async function () {
255 this.timeout(60000)
256
257 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
258 await waitJobs(servers)
259
260 await Promise.all([
261 servers[0].kill(),
262 servers[1].kill()
263 ])
264
265 const config = { geo_ip: { enabled: true } }
266 await Promise.all([
267 servers[0].run(config),
268 servers[1].run(config)
269 ])
270
271 await servers[0].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
272 await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.4,127.0.0.1', currentTime: 3 })
273 await servers[1].views.view({ id: uuid, xForwardedFor: '80.67.169.12,127.0.0.1', currentTime: 2 })
274
275 await processViewersStats(servers)
276
277 const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
278 expect(stats.countries).to.have.lengthOf(2)
279
280 expect(stats.countries[0].isoCode).to.equal('US')
281 expect(stats.countries[0].viewers).to.equal(2)
282
283 expect(stats.countries[1].isoCode).to.equal('FR')
284 expect(stats.countries[1].viewers).to.equal(1)
285 })
286 })
287
288 after(async function () {
289 await cleanupTests(servers)
290 })
291})
diff --git a/server/tests/api/views/video-views-retention-stats.ts b/server/tests/api/views/video-views-retention-stats.ts
new file mode 100644
index 000000000..98be7bfdb
--- /dev/null
+++ b/server/tests/api/views/video-views-retention-stats.ts
@@ -0,0 +1,56 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@server/tests/shared'
6import { cleanupTests, PeerTubeServer } from '@shared/server-commands'
7
8const expect = chai.expect
9
10describe('Test views retention stats', function () {
11 let servers: PeerTubeServer[]
12
13 before(async function () {
14 this.timeout(120000)
15
16 servers = await prepareViewsServers()
17 })
18
19 describe('Test retention stats on VOD', function () {
20 let vodVideoId: string
21
22 before(async function () {
23 this.timeout(60000);
24
25 ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
26 })
27
28 it('Should display empty retention', async function () {
29 const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId })
30 expect(data).to.have.lengthOf(6)
31
32 for (let i = 0; i < 6; i++) {
33 expect(data[i].second).to.equal(i)
34 expect(data[i].retentionPercent).to.equal(0)
35 }
36 })
37
38 it('Should display appropriate retention metrics', async function () {
39 await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
40 await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 3 ] })
41 await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 4 ] })
42 await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
43
44 await processViewersStats(servers)
45
46 const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId })
47 expect(data).to.have.lengthOf(6)
48
49 expect(data.map(d => d.retentionPercent)).to.deep.equal([ 50, 75, 25, 25, 25, 0 ])
50 })
51 })
52
53 after(async function () {
54 await cleanupTests(servers)
55 })
56})
diff --git a/server/tests/api/views/video-views-timeserie-stats.ts b/server/tests/api/views/video-views-timeserie-stats.ts
new file mode 100644
index 000000000..98c041cdf
--- /dev/null
+++ b/server/tests/api/views/video-views-timeserie-stats.ts
@@ -0,0 +1,109 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import * as chai from 'chai'
5import { FfmpegCommand } from 'fluent-ffmpeg'
6import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@server/tests/shared'
7import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models'
8import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@shared/server-commands'
9
10const expect = chai.expect
11
12describe('Test views timeserie stats', function () {
13 const availableMetrics: VideoStatsTimeserieMetric[] = [ 'viewers' ]
14
15 let servers: PeerTubeServer[]
16
17 before(async function () {
18 this.timeout(120000)
19
20 servers = await prepareViewsServers()
21 })
22
23 describe('Common metric tests', function () {
24 let vodVideoId: string
25
26 before(async function () {
27 this.timeout(60000);
28
29 ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
30 })
31
32 it('Should display empty metric stats', async function () {
33 for (const metric of availableMetrics) {
34 const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric })
35
36 expect(data).to.have.lengthOf(30)
37
38 for (const d of data) {
39 expect(d.value).to.equal(0)
40 }
41 }
42 })
43 })
44
45 describe('Test viewer and watch time metrics on live and VOD', function () {
46 let vodVideoId: string
47 let liveVideoId: string
48 let command: FfmpegCommand
49
50 function expectTimeserieData (result: VideoStatsTimeserie, lastValue: number) {
51 const { data } = result
52 expect(data).to.have.lengthOf(30)
53
54 const last = data[data.length - 1]
55
56 const today = new Date().getDate()
57 expect(new Date(last.date).getDate()).to.equal(today)
58 expect(last.value).to.equal(lastValue)
59
60 for (let i = 0; i < data.length - 2; i++) {
61 expect(data[i].value).to.equal(0)
62 }
63 }
64
65 before(async function () {
66 this.timeout(60000);
67
68 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
69 })
70
71 it('Should display appropriate viewers metrics', async function () {
72 for (const videoId of [ vodVideoId, liveVideoId ]) {
73 await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 3 ] })
74 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 5 ] })
75 }
76
77 await processViewersStats(servers)
78
79 for (const videoId of [ vodVideoId, liveVideoId ]) {
80 const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' })
81 expectTimeserieData(result, 2)
82 }
83 })
84
85 it('Should display appropriate watch time metrics', async function () {
86 for (const videoId of [ vodVideoId, liveVideoId ]) {
87 const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'aggregateWatchTime' })
88 expectTimeserieData(result, 8)
89
90 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
91 }
92
93 await processViewersStats(servers)
94
95 for (const videoId of [ vodVideoId, liveVideoId ]) {
96 const result = await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'aggregateWatchTime' })
97 expectTimeserieData(result, 9)
98 }
99 })
100
101 after(async function () {
102 await stopFfmpeg(command)
103 })
104 })
105
106 after(async function () {
107 await cleanupTests(servers)
108 })
109})
diff --git a/server/tests/api/videos/videos-views-cleaner.ts b/server/tests/api/views/videos-views-cleaner.ts
index e6815a4a8..ef988837f 100644
--- a/server/tests/api/videos/videos-views-cleaner.ts
+++ b/server/tests/api/views/videos-views-cleaner.ts
@@ -34,10 +34,10 @@ describe('Test video views cleaner', function () {
34 34
35 await waitJobs(servers) 35 await waitJobs(servers)
36 36
37 await servers[0].videos.view({ id: videoIdServer1 }) 37 await servers[0].views.simulateView({ id: videoIdServer1 })
38 await servers[1].videos.view({ id: videoIdServer1 }) 38 await servers[1].views.simulateView({ id: videoIdServer1 })
39 await servers[0].videos.view({ id: videoIdServer2 }) 39 await servers[0].views.simulateView({ id: videoIdServer2 })
40 await servers[1].videos.view({ id: videoIdServer2 }) 40 await servers[1].views.simulateView({ id: videoIdServer2 })
41 41
42 await waitJobs(servers) 42 await waitJobs(servers)
43 }) 43 })
diff --git a/server/tests/plugins/action-hooks.ts b/server/tests/plugins/action-hooks.ts
index 8788a9644..57ede2701 100644
--- a/server/tests/plugins/action-hooks.ts
+++ b/server/tests/plugins/action-hooks.ts
@@ -1,6 +1,7 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
4import { 5import {
5 cleanupTests, 6 cleanupTests,
6 createMultipleServers, 7 createMultipleServers,
@@ -10,7 +11,6 @@ import {
10 setAccessTokensToServers, 11 setAccessTokensToServers,
11 setDefaultVideoChannel 12 setDefaultVideoChannel
12} from '@shared/server-commands' 13} from '@shared/server-commands'
13import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models'
14 14
15describe('Test plugin action hooks', function () { 15describe('Test plugin action hooks', function () {
16 let servers: PeerTubeServer[] 16 let servers: PeerTubeServer[]
@@ -61,7 +61,7 @@ describe('Test plugin action hooks', function () {
61 }) 61 })
62 62
63 it('Should run action:api.video.viewed', async function () { 63 it('Should run action:api.video.viewed', async function () {
64 await servers[0].videos.view({ id: videoUUID }) 64 await servers[0].views.simulateView({ id: videoUUID })
65 65
66 await checkHook('action:api.video.viewed') 66 await checkHook('action:api.video.viewed')
67 }) 67 })
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts
index 167429ef4..5e8d08dff 100644
--- a/server/tests/plugins/plugin-helpers.ts
+++ b/server/tests/plugins/plugin-helpers.ts
@@ -301,7 +301,7 @@ describe('Test plugin helpers', function () {
301 // Should not throw -> video exists 301 // Should not throw -> video exists
302 const video = await servers[0].videos.get({ id: videoUUID }) 302 const video = await servers[0].videos.get({ id: videoUUID })
303 // Should delete the video 303 // Should delete the video
304 await servers[0].videos.view({ id: videoUUID }) 304 await servers[0].views.simulateView({ id: videoUUID })
305 305
306 await servers[0].servers.waitUntilLog('Video deleted by plugin four.') 306 await servers[0].servers.waitUntilLog('Video deleted by plugin four.')
307 307
diff --git a/server/tests/shared/index.ts b/server/tests/shared/index.ts
index 47019d6a8..9f7ade53d 100644
--- a/server/tests/shared/index.ts
+++ b/server/tests/shared/index.ts
@@ -13,3 +13,4 @@ export * from './streaming-playlists'
13export * from './tests' 13export * from './tests'
14export * from './tracker' 14export * from './tracker'
15export * from './videos' 15export * from './videos'
16export * from './views'
diff --git a/server/tests/shared/views.ts b/server/tests/shared/views.ts
new file mode 100644
index 000000000..e6b289715
--- /dev/null
+++ b/server/tests/shared/views.ts
@@ -0,0 +1,93 @@
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}