diff options
author | Chocobozzz <me@florianbigard.com> | 2022-03-24 13:36:47 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2022-04-15 09:49:35 +0200 |
commit | b211106695bb82f6c32e53306081b5262c3d109d (patch) | |
tree | fa187de1c33b0956665f5362e29af6b0f6d8bb57 /server/tests | |
parent | 69d48ee30c9d47cddf0c3c047dc99a99dcb6e894 (diff) | |
download | PeerTube-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')
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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { processViewersStats } from '@server/tests/shared' | ||
6 | import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@shared/models' | ||
5 | import { | 7 | import { |
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' |
14 | import { HttpStatusCode, VideoPlaylistPrivacy } from '@shared/models' | ||
15 | 16 | ||
16 | const expect = chai.expect | 17 | const 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' | |||
33 | import './video-files' | 33 | import './video-files' |
34 | import './videos-history' | 34 | import './videos-history' |
35 | import './videos-overviews' | 35 | import './videos-overviews' |
36 | import './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 { | |||
17 | describe('Test videos history API validator', function () { | 17 | describe('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 | |||
3 | import 'mocha' | ||
4 | import { HttpStatusCode, VideoPrivacy } from '@shared/models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultVideoChannel | ||
12 | } from '@shared/server-commands' | ||
13 | |||
14 | describe('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' | |||
3 | import './live-permanent' | 3 | import './live-permanent' |
4 | import './live-rtmps' | 4 | import './live-rtmps' |
5 | import './live-save-replay' | 5 | import './live-save-replay' |
6 | import './live-views' | ||
7 | import './live' | 6 | import './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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
6 | import { wait } from '@shared/core-utils' | ||
7 | import { VideoPrivacy } from '@shared/models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | setDefaultVideoChannel, | ||
15 | stopFfmpeg, | ||
16 | waitJobs, | ||
17 | waitUntilLivePublishedOnAllServers | ||
18 | } from '@shared/server-commands' | ||
19 | |||
20 | const expect = chai.expect | ||
21 | |||
22 | describe('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' | |||
16 | import './videos-common-filters' | 16 | import './videos-common-filters' |
17 | import './videos-history' | 17 | import './videos-history' |
18 | import './videos-overview' | 18 | import './videos-overview' |
19 | import './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 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { wait } from '@shared/core-utils' | 5 | import { wait } from '@shared/core-utils' |
6 | import { HttpStatusCode, Video } from '@shared/models' | 6 | import { Video } from '@shared/models' |
7 | import { | 7 | import { 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 | ||
16 | const expect = chai.expect | 9 | const 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 @@ | |||
1 | export * from './video-views-counter' | ||
2 | export * from './video-views-overall-stats' | ||
3 | export * from './video-views-retention-stats' | ||
4 | export * from './video-views-timeserie-stats' | ||
5 | export * 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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
6 | import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@server/tests/shared' | ||
7 | import { wait } from '@shared/core-utils' | ||
8 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@shared/server-commands' | ||
9 | |||
10 | const expect = chai.expect | ||
11 | |||
12 | describe('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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
6 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@server/tests/shared' | ||
7 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@shared/server-commands' | ||
8 | |||
9 | const expect = chai.expect | ||
10 | |||
11 | describe('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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@server/tests/shared' | ||
6 | import { cleanupTests, PeerTubeServer } from '@shared/server-commands' | ||
7 | |||
8 | const expect = chai.expect | ||
9 | |||
10 | describe('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 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
6 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@server/tests/shared' | ||
7 | import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models' | ||
8 | import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@shared/server-commands' | ||
9 | |||
10 | const expect = chai.expect | ||
11 | |||
12 | describe('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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | ||
4 | import { | 5 | import { |
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' |
13 | import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' | ||
14 | 14 | ||
15 | describe('Test plugin action hooks', function () { | 15 | describe('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' | |||
13 | export * from './tests' | 13 | export * from './tests' |
14 | export * from './tracker' | 14 | export * from './tracker' |
15 | export * from './videos' | 15 | export * from './videos' |
16 | export * 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 @@ | |||
1 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
2 | import { wait } from '@shared/core-utils' | ||
3 | import { VideoCreateResult, VideoPrivacy } from '@shared/models' | ||
4 | import { | ||
5 | createMultipleServers, | ||
6 | doubleFollow, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers, | ||
9 | setDefaultVideoChannel, | ||
10 | waitJobs, | ||
11 | waitUntilLivePublishedOnAllServers | ||
12 | } from '@shared/server-commands' | ||
13 | |||
14 | async 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 | |||
25 | async 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 | |||
33 | async 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 | |||
55 | async 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 | |||
88 | export { | ||
89 | processViewersStats, | ||
90 | prepareViewsServers, | ||
91 | processViewsBuffer, | ||
92 | prepareViewsVideos | ||
93 | } | ||