aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/tests/src/api
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/tests/src/api
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'packages/tests/src/api')
-rw-r--r--packages/tests/src/api/activitypub/cleaner.ts342
-rw-r--r--packages/tests/src/api/activitypub/client.ts136
-rw-r--r--packages/tests/src/api/activitypub/fetch.ts82
-rw-r--r--packages/tests/src/api/activitypub/index.ts5
-rw-r--r--packages/tests/src/api/activitypub/refresher.ts157
-rw-r--r--packages/tests/src/api/activitypub/security.ts331
-rw-r--r--packages/tests/src/api/check-params/abuses.ts438
-rw-r--r--packages/tests/src/api/check-params/accounts.ts43
-rw-r--r--packages/tests/src/api/check-params/blocklist.ts556
-rw-r--r--packages/tests/src/api/check-params/bulk.ts86
-rw-r--r--packages/tests/src/api/check-params/channel-import-videos.ts209
-rw-r--r--packages/tests/src/api/check-params/config.ts428
-rw-r--r--packages/tests/src/api/check-params/contact-form.ts86
-rw-r--r--packages/tests/src/api/check-params/custom-pages.ts79
-rw-r--r--packages/tests/src/api/check-params/debug.ts67
-rw-r--r--packages/tests/src/api/check-params/follows.ts369
-rw-r--r--packages/tests/src/api/check-params/index.ts45
-rw-r--r--packages/tests/src/api/check-params/jobs.ts125
-rw-r--r--packages/tests/src/api/check-params/live.ts590
-rw-r--r--packages/tests/src/api/check-params/logs.ts163
-rw-r--r--packages/tests/src/api/check-params/metrics.ts214
-rw-r--r--packages/tests/src/api/check-params/my-user.ts492
-rw-r--r--packages/tests/src/api/check-params/plugins.ts490
-rw-r--r--packages/tests/src/api/check-params/redundancy.ts240
-rw-r--r--packages/tests/src/api/check-params/registrations.ts446
-rw-r--r--packages/tests/src/api/check-params/runners.ts911
-rw-r--r--packages/tests/src/api/check-params/search.ts278
-rw-r--r--packages/tests/src/api/check-params/services.ts207
-rw-r--r--packages/tests/src/api/check-params/transcoding.ts112
-rw-r--r--packages/tests/src/api/check-params/two-factor.ts294
-rw-r--r--packages/tests/src/api/check-params/upload-quota.ts134
-rw-r--r--packages/tests/src/api/check-params/user-notifications.ts290
-rw-r--r--packages/tests/src/api/check-params/user-subscriptions.ts298
-rw-r--r--packages/tests/src/api/check-params/users-admin.ts457
-rw-r--r--packages/tests/src/api/check-params/users-emails.ts122
-rw-r--r--packages/tests/src/api/check-params/video-blacklist.ts292
-rw-r--r--packages/tests/src/api/check-params/video-captions.ts307
-rw-r--r--packages/tests/src/api/check-params/video-channel-syncs.ts319
-rw-r--r--packages/tests/src/api/check-params/video-channels.ts379
-rw-r--r--packages/tests/src/api/check-params/video-comments.ts484
-rw-r--r--packages/tests/src/api/check-params/video-files.ts195
-rw-r--r--packages/tests/src/api/check-params/video-imports.ts433
-rw-r--r--packages/tests/src/api/check-params/video-passwords.ts604
-rw-r--r--packages/tests/src/api/check-params/video-playlists.ts695
-rw-r--r--packages/tests/src/api/check-params/video-source.ts154
-rw-r--r--packages/tests/src/api/check-params/video-storyboards.ts45
-rw-r--r--packages/tests/src/api/check-params/video-studio.ts392
-rw-r--r--packages/tests/src/api/check-params/video-token.ts70
-rw-r--r--packages/tests/src/api/check-params/videos-common-filters.ts171
-rw-r--r--packages/tests/src/api/check-params/videos-history.ts145
-rw-r--r--packages/tests/src/api/check-params/videos-overviews.ts31
-rw-r--r--packages/tests/src/api/check-params/videos.ts883
-rw-r--r--packages/tests/src/api/check-params/views.ts227
-rw-r--r--packages/tests/src/api/live/index.ts7
-rw-r--r--packages/tests/src/api/live/live-constraints.ts237
-rw-r--r--packages/tests/src/api/live/live-fast-restream.ts153
-rw-r--r--packages/tests/src/api/live/live-permanent.ts204
-rw-r--r--packages/tests/src/api/live/live-rtmps.ts143
-rw-r--r--packages/tests/src/api/live/live-save-replay.ts583
-rw-r--r--packages/tests/src/api/live/live-socket-messages.ts186
-rw-r--r--packages/tests/src/api/live/live.ts766
-rw-r--r--packages/tests/src/api/moderation/abuses.ts887
-rw-r--r--packages/tests/src/api/moderation/blocklist-notification.ts231
-rw-r--r--packages/tests/src/api/moderation/blocklist.ts902
-rw-r--r--packages/tests/src/api/moderation/index.ts4
-rw-r--r--packages/tests/src/api/moderation/video-blacklist.ts414
-rw-r--r--packages/tests/src/api/notifications/admin-notifications.ts154
-rw-r--r--packages/tests/src/api/notifications/comments-notifications.ts300
-rw-r--r--packages/tests/src/api/notifications/index.ts6
-rw-r--r--packages/tests/src/api/notifications/moderation-notifications.ts609
-rw-r--r--packages/tests/src/api/notifications/notifications-api.ts206
-rw-r--r--packages/tests/src/api/notifications/registrations-notifications.ts83
-rw-r--r--packages/tests/src/api/notifications/user-notifications.ts574
-rw-r--r--packages/tests/src/api/object-storage/index.ts4
-rw-r--r--packages/tests/src/api/object-storage/live.ts314
-rw-r--r--packages/tests/src/api/object-storage/video-imports.ts112
-rw-r--r--packages/tests/src/api/object-storage/video-static-file-privacy.ts573
-rw-r--r--packages/tests/src/api/object-storage/videos.ts434
-rw-r--r--packages/tests/src/api/redundancy/index.ts3
-rw-r--r--packages/tests/src/api/redundancy/manage-redundancy.ts324
-rw-r--r--packages/tests/src/api/redundancy/redundancy-constraints.ts191
-rw-r--r--packages/tests/src/api/redundancy/redundancy.ts743
-rw-r--r--packages/tests/src/api/runners/index.ts5
-rw-r--r--packages/tests/src/api/runners/runner-common.ts744
-rw-r--r--packages/tests/src/api/runners/runner-live-transcoding.ts332
-rw-r--r--packages/tests/src/api/runners/runner-socket.ts120
-rw-r--r--packages/tests/src/api/runners/runner-studio-transcoding.ts169
-rw-r--r--packages/tests/src/api/runners/runner-vod-transcoding.ts545
-rw-r--r--packages/tests/src/api/search/index.ts7
-rw-r--r--packages/tests/src/api/search/search-activitypub-video-channels.ts255
-rw-r--r--packages/tests/src/api/search/search-activitypub-video-playlists.ts214
-rw-r--r--packages/tests/src/api/search/search-activitypub-videos.ts196
-rw-r--r--packages/tests/src/api/search/search-channels.ts159
-rw-r--r--packages/tests/src/api/search/search-index.ts438
-rw-r--r--packages/tests/src/api/search/search-playlists.ts180
-rw-r--r--packages/tests/src/api/search/search-videos.ts568
-rw-r--r--packages/tests/src/api/server/auto-follows.ts189
-rw-r--r--packages/tests/src/api/server/bulk.ts185
-rw-r--r--packages/tests/src/api/server/config-defaults.ts294
-rw-r--r--packages/tests/src/api/server/config.ts645
-rw-r--r--packages/tests/src/api/server/contact-form.ts101
-rw-r--r--packages/tests/src/api/server/email.ts371
-rw-r--r--packages/tests/src/api/server/follow-constraints.ts321
-rw-r--r--packages/tests/src/api/server/follows-moderation.ts364
-rw-r--r--packages/tests/src/api/server/follows.ts644
-rw-r--r--packages/tests/src/api/server/handle-down.ts339
-rw-r--r--packages/tests/src/api/server/homepage.ts81
-rw-r--r--packages/tests/src/api/server/index.ts22
-rw-r--r--packages/tests/src/api/server/jobs.ts128
-rw-r--r--packages/tests/src/api/server/logs.ts265
-rw-r--r--packages/tests/src/api/server/no-client.ts24
-rw-r--r--packages/tests/src/api/server/open-telemetry.ts193
-rw-r--r--packages/tests/src/api/server/plugins.ts410
-rw-r--r--packages/tests/src/api/server/proxy.ts173
-rw-r--r--packages/tests/src/api/server/reverse-proxy.ts156
-rw-r--r--packages/tests/src/api/server/services.ts143
-rw-r--r--packages/tests/src/api/server/slow-follows.ts85
-rw-r--r--packages/tests/src/api/server/stats.ts279
-rw-r--r--packages/tests/src/api/server/tracker.ts110
-rw-r--r--packages/tests/src/api/transcoding/audio-only.ts104
-rw-r--r--packages/tests/src/api/transcoding/create-transcoding.ts267
-rw-r--r--packages/tests/src/api/transcoding/hls.ts176
-rw-r--r--packages/tests/src/api/transcoding/index.ts6
-rw-r--r--packages/tests/src/api/transcoding/transcoder.ts802
-rw-r--r--packages/tests/src/api/transcoding/update-while-transcoding.ts161
-rw-r--r--packages/tests/src/api/transcoding/video-studio.ts379
-rw-r--r--packages/tests/src/api/users/index.ts8
-rw-r--r--packages/tests/src/api/users/oauth.ts203
-rw-r--r--packages/tests/src/api/users/registrations.ts415
-rw-r--r--packages/tests/src/api/users/two-factor.ts206
-rw-r--r--packages/tests/src/api/users/user-subscriptions.ts614
-rw-r--r--packages/tests/src/api/users/user-videos.ts219
-rw-r--r--packages/tests/src/api/users/users-email-verification.ts165
-rw-r--r--packages/tests/src/api/users/users-multiple-servers.ts213
-rw-r--r--packages/tests/src/api/users/users.ts529
-rw-r--r--packages/tests/src/api/videos/channel-import-videos.ts161
-rw-r--r--packages/tests/src/api/videos/index.ts23
-rw-r--r--packages/tests/src/api/videos/multiple-servers.ts1095
-rw-r--r--packages/tests/src/api/videos/resumable-upload.ts316
-rw-r--r--packages/tests/src/api/videos/single-server.ts461
-rw-r--r--packages/tests/src/api/videos/video-captions.ts189
-rw-r--r--packages/tests/src/api/videos/video-change-ownership.ts314
-rw-r--r--packages/tests/src/api/videos/video-channel-syncs.ts321
-rw-r--r--packages/tests/src/api/videos/video-channels.ts556
-rw-r--r--packages/tests/src/api/videos/video-comments.ts335
-rw-r--r--packages/tests/src/api/videos/video-description.ts103
-rw-r--r--packages/tests/src/api/videos/video-files.ts202
-rw-r--r--packages/tests/src/api/videos/video-imports.ts634
-rw-r--r--packages/tests/src/api/videos/video-nsfw.ts227
-rw-r--r--packages/tests/src/api/videos/video-passwords.ts97
-rw-r--r--packages/tests/src/api/videos/video-playlist-thumbnails.ts234
-rw-r--r--packages/tests/src/api/videos/video-playlists.ts1210
-rw-r--r--packages/tests/src/api/videos/video-privacy.ts294
-rw-r--r--packages/tests/src/api/videos/video-schedule-update.ts155
-rw-r--r--packages/tests/src/api/videos/video-source.ts448
-rw-r--r--packages/tests/src/api/videos/video-static-file-privacy.ts602
-rw-r--r--packages/tests/src/api/videos/video-storyboard.ts213
-rw-r--r--packages/tests/src/api/videos/videos-common-filters.ts499
-rw-r--r--packages/tests/src/api/videos/videos-history.ts230
-rw-r--r--packages/tests/src/api/videos/videos-overview.ts129
-rw-r--r--packages/tests/src/api/views/index.ts5
-rw-r--r--packages/tests/src/api/views/video-views-counter.ts153
-rw-r--r--packages/tests/src/api/views/video-views-overall-stats.ts368
-rw-r--r--packages/tests/src/api/views/video-views-retention-stats.ts53
-rw-r--r--packages/tests/src/api/views/video-views-timeserie-stats.ts253
-rw-r--r--packages/tests/src/api/views/videos-views-cleaner.ts98
166 files changed, 48394 insertions, 0 deletions
diff --git a/packages/tests/src/api/activitypub/cleaner.ts b/packages/tests/src/api/activitypub/cleaner.ts
new file mode 100644
index 000000000..4476aea85
--- /dev/null
+++ b/packages/tests/src/api/activitypub/cleaner.ts
@@ -0,0 +1,342 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { SQLCommand } from '@tests/shared/sql-command.js'
5import { wait } from '@peertube/peertube-core-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test AP cleaner', function () {
16 let servers: PeerTubeServer[] = []
17 const sqlCommands: SQLCommand[] = []
18
19 let videoUUID1: string
20 let videoUUID2: string
21 let videoUUID3: string
22
23 let videoUUIDs: string[]
24
25 before(async function () {
26 this.timeout(120000)
27
28 const config = {
29 federation: {
30 videos: { cleanup_remote_interactions: true }
31 }
32 }
33 servers = await createMultipleServers(3, config)
34
35 // Get the access tokens
36 await setAccessTokensToServers(servers)
37
38 await Promise.all([
39 doubleFollow(servers[0], servers[1]),
40 doubleFollow(servers[1], servers[2]),
41 doubleFollow(servers[0], servers[2])
42 ])
43
44 // Update 1 local share, check 6 shares
45
46 // Create 1 comment per video
47 // Update 1 remote URL and 1 local URL on
48
49 videoUUID1 = (await servers[0].videos.quickUpload({ name: 'server 1' })).uuid
50 videoUUID2 = (await servers[1].videos.quickUpload({ name: 'server 2' })).uuid
51 videoUUID3 = (await servers[2].videos.quickUpload({ name: 'server 3' })).uuid
52
53 videoUUIDs = [ videoUUID1, videoUUID2, videoUUID3 ]
54
55 await waitJobs(servers)
56
57 for (const server of servers) {
58 for (const uuid of videoUUIDs) {
59 await server.videos.rate({ id: uuid, rating: 'like' })
60 await server.comments.createThread({ videoId: uuid, text: 'comment' })
61 }
62
63 sqlCommands.push(new SQLCommand(server))
64 }
65
66 await waitJobs(servers)
67 })
68
69 it('Should have the correct likes', async function () {
70 for (const server of servers) {
71 for (const uuid of videoUUIDs) {
72 const video = await server.videos.get({ id: uuid })
73
74 expect(video.likes).to.equal(3)
75 expect(video.dislikes).to.equal(0)
76 }
77 }
78 })
79
80 it('Should destroy server 3 internal likes and correctly clean them', async function () {
81 this.timeout(20000)
82
83 await sqlCommands[2].deleteAll('accountVideoRate')
84 for (const uuid of videoUUIDs) {
85 await sqlCommands[2].setVideoField(uuid, 'likes', '0')
86 }
87
88 await wait(5000)
89 await waitJobs(servers)
90
91 // Updated rates of my video
92 {
93 const video = await servers[0].videos.get({ id: videoUUID1 })
94 expect(video.likes).to.equal(2)
95 expect(video.dislikes).to.equal(0)
96 }
97
98 // Did not update rates of a remote video
99 {
100 const video = await servers[0].videos.get({ id: videoUUID2 })
101 expect(video.likes).to.equal(3)
102 expect(video.dislikes).to.equal(0)
103 }
104 })
105
106 it('Should update rates to dislikes', async function () {
107 this.timeout(20000)
108
109 for (const server of servers) {
110 for (const uuid of videoUUIDs) {
111 await server.videos.rate({ id: uuid, rating: 'dislike' })
112 }
113 }
114
115 await waitJobs(servers)
116
117 for (const server of servers) {
118 for (const uuid of videoUUIDs) {
119 const video = await server.videos.get({ id: uuid })
120 expect(video.likes).to.equal(0)
121 expect(video.dislikes).to.equal(3)
122 }
123 }
124 })
125
126 it('Should destroy server 3 internal dislikes and correctly clean them', async function () {
127 this.timeout(20000)
128
129 await sqlCommands[2].deleteAll('accountVideoRate')
130
131 for (const uuid of videoUUIDs) {
132 await sqlCommands[2].setVideoField(uuid, 'dislikes', '0')
133 }
134
135 await wait(5000)
136 await waitJobs(servers)
137
138 // Updated rates of my video
139 {
140 const video = await servers[0].videos.get({ id: videoUUID1 })
141 expect(video.likes).to.equal(0)
142 expect(video.dislikes).to.equal(2)
143 }
144
145 // Did not update rates of a remote video
146 {
147 const video = await servers[0].videos.get({ id: videoUUID2 })
148 expect(video.likes).to.equal(0)
149 expect(video.dislikes).to.equal(3)
150 }
151 })
152
153 it('Should destroy server 3 internal shares and correctly clean them', async function () {
154 this.timeout(20000)
155
156 const preCount = await sqlCommands[0].getVideoShareCount()
157 expect(preCount).to.equal(6)
158
159 await sqlCommands[2].deleteAll('videoShare')
160 await wait(5000)
161 await waitJobs(servers)
162
163 // Still 6 because we don't have remote shares on local videos
164 const postCount = await sqlCommands[0].getVideoShareCount()
165 expect(postCount).to.equal(6)
166 })
167
168 it('Should destroy server 3 internal comments and correctly clean them', async function () {
169 this.timeout(20000)
170
171 {
172 const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
173 expect(total).to.equal(3)
174 }
175
176 await sqlCommands[2].deleteAll('videoComment')
177
178 await wait(5000)
179 await waitJobs(servers)
180
181 {
182 const { total } = await servers[0].comments.listThreads({ videoId: videoUUID1 })
183 expect(total).to.equal(2)
184 }
185 })
186
187 it('Should correctly update rate URLs', async function () {
188 this.timeout(30000)
189
190 async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
191 const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` +
192 `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'`
193 const res = await sqlCommands[0].selectQuery<{ url: string }>(query)
194
195 for (const rate of res) {
196 const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`)
197 expect(rate.url).to.match(matcher)
198 }
199 }
200
201 async function checkLocal () {
202 const startsWith = 'http://' + servers[0].host + '%'
203 // On local videos
204 await check(startsWith, servers[0].url, '', 'false')
205 // On remote videos
206 await check(startsWith, servers[0].url, '', 'true')
207 }
208
209 async function checkRemote (suffix: string) {
210 const startsWith = 'http://' + servers[1].host + '%'
211 // On local videos
212 await check(startsWith, servers[1].url, suffix, 'false')
213 // On remote videos, we should not update URLs so no suffix
214 await check(startsWith, servers[1].url, '', 'true')
215 }
216
217 await checkLocal()
218 await checkRemote('')
219
220 {
221 const query = `UPDATE "accountVideoRate" SET url = url || 'stan'`
222 await sqlCommands[1].updateQuery(query)
223
224 await wait(5000)
225 await waitJobs(servers)
226 }
227
228 await checkLocal()
229 await checkRemote('stan')
230 })
231
232 it('Should correctly update comment URLs', async function () {
233 this.timeout(30000)
234
235 async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') {
236 const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` +
237 `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'`
238
239 const res = await sqlCommands[0].selectQuery<{ url: string, videoUUID: string }>(query)
240
241 for (const comment of res) {
242 const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`)
243 expect(comment.url).to.match(matcher)
244 }
245 }
246
247 async function checkLocal () {
248 const startsWith = 'http://' + servers[0].host + '%'
249 // On local videos
250 await check(startsWith, servers[0].url, '', 'false')
251 // On remote videos
252 await check(startsWith, servers[0].url, '', 'true')
253 }
254
255 async function checkRemote (suffix: string) {
256 const startsWith = 'http://' + servers[1].host + '%'
257 // On local videos
258 await check(startsWith, servers[1].url, suffix, 'false')
259 // On remote videos, we should not update URLs so no suffix
260 await check(startsWith, servers[1].url, '', 'true')
261 }
262
263 {
264 const query = `UPDATE "videoComment" SET url = url || 'kyle'`
265 await sqlCommands[1].updateQuery(query)
266
267 await wait(5000)
268 await waitJobs(servers)
269 }
270
271 await checkLocal()
272 await checkRemote('kyle')
273 })
274
275 it('Should remove unavailable remote resources', async function () {
276 this.timeout(240000)
277
278 async function expectNotDeleted () {
279 {
280 const video = await servers[0].videos.get({ id: uuid })
281
282 expect(video.likes).to.equal(3)
283 expect(video.dislikes).to.equal(0)
284 }
285
286 {
287 const { total } = await servers[0].comments.listThreads({ videoId: uuid })
288 expect(total).to.equal(3)
289 }
290 }
291
292 async function expectDeleted () {
293 {
294 const video = await servers[0].videos.get({ id: uuid })
295
296 expect(video.likes).to.equal(2)
297 expect(video.dislikes).to.equal(0)
298 }
299
300 {
301 const { total } = await servers[0].comments.listThreads({ videoId: uuid })
302 expect(total).to.equal(2)
303 }
304 }
305
306 const uuid = (await servers[0].videos.quickUpload({ name: 'server 1 video 2' })).uuid
307
308 await waitJobs(servers)
309
310 for (const server of servers) {
311 await server.videos.rate({ id: uuid, rating: 'like' })
312 await server.comments.createThread({ videoId: uuid, text: 'comment' })
313 }
314
315 await waitJobs(servers)
316
317 await expectNotDeleted()
318
319 await servers[1].kill()
320
321 await wait(5000)
322 await expectNotDeleted()
323
324 let continueWhile = true
325
326 do {
327 try {
328 await expectDeleted()
329 continueWhile = false
330 } catch {
331 }
332 } while (continueWhile)
333 })
334
335 after(async function () {
336 for (const sql of sqlCommands) {
337 await sql.cleanup()
338 }
339
340 await cleanupTests(servers)
341 })
342})
diff --git a/packages/tests/src/api/activitypub/client.ts b/packages/tests/src/api/activitypub/client.ts
new file mode 100644
index 000000000..fb9575d31
--- /dev/null
+++ b/packages/tests/src/api/activitypub/client.ts
@@ -0,0 +1,136 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { processViewersStats } from '@tests/shared/views.js'
5import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 makeActivityPubGetRequest,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel
14} from '@peertube/peertube-server-commands'
15
16describe('Test activitypub', function () {
17 let servers: PeerTubeServer[] = []
18 let video: { id: number, uuid: string, shortUUID: string }
19 let playlist: { id: number, uuid: string, shortUUID: string }
20
21 async function testAccount (path: string) {
22 const res = await makeActivityPubGetRequest(servers[0].url, path)
23 const object = res.body
24
25 expect(object.type).to.equal('Person')
26 expect(object.id).to.equal(servers[0].url + '/accounts/root')
27 expect(object.name).to.equal('root')
28 expect(object.preferredUsername).to.equal('root')
29 }
30
31 async function testChannel (path: string) {
32 const res = await makeActivityPubGetRequest(servers[0].url, path)
33 const object = res.body
34
35 expect(object.type).to.equal('Group')
36 expect(object.id).to.equal(servers[0].url + '/video-channels/root_channel')
37 expect(object.name).to.equal('Main root channel')
38 expect(object.preferredUsername).to.equal('root_channel')
39 }
40
41 async function testVideo (path: string) {
42 const res = await makeActivityPubGetRequest(servers[0].url, path)
43 const object = res.body
44
45 expect(object.type).to.equal('Video')
46 expect(object.id).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
47 expect(object.name).to.equal('video')
48 }
49
50 async function testPlaylist (path: string) {
51 const res = await makeActivityPubGetRequest(servers[0].url, path)
52 const object = res.body
53
54 expect(object.type).to.equal('Playlist')
55 expect(object.id).to.equal(servers[0].url + '/video-playlists/' + playlist.uuid)
56 expect(object.name).to.equal('playlist')
57 }
58
59 before(async function () {
60 this.timeout(30000)
61
62 servers = await createMultipleServers(2)
63
64 await setAccessTokensToServers(servers)
65 await setDefaultVideoChannel(servers)
66
67 {
68 video = await servers[0].videos.quickUpload({ name: 'video' })
69 }
70
71 {
72 const attributes = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].store.channel.id }
73 playlist = await servers[0].playlists.create({ attributes })
74 }
75
76 await doubleFollow(servers[0], servers[1])
77 })
78
79 it('Should return the account object', async function () {
80 await testAccount('/accounts/root')
81 await testAccount('/a/root')
82 })
83
84 it('Should return the channel object', async function () {
85 await testChannel('/video-channels/root_channel')
86 await testChannel('/c/root_channel')
87 })
88
89 it('Should return the video object', async function () {
90 await testVideo('/videos/watch/' + video.id)
91 await testVideo('/videos/watch/' + video.uuid)
92 await testVideo('/videos/watch/' + video.shortUUID)
93 await testVideo('/w/' + video.id)
94 await testVideo('/w/' + video.uuid)
95 await testVideo('/w/' + video.shortUUID)
96 })
97
98 it('Should return the playlist object', async function () {
99 await testPlaylist('/video-playlists/' + playlist.id)
100 await testPlaylist('/video-playlists/' + playlist.uuid)
101 await testPlaylist('/video-playlists/' + playlist.shortUUID)
102 await testPlaylist('/w/p/' + playlist.id)
103 await testPlaylist('/w/p/' + playlist.uuid)
104 await testPlaylist('/w/p/' + playlist.shortUUID)
105 await testPlaylist('/videos/watch/playlist/' + playlist.id)
106 await testPlaylist('/videos/watch/playlist/' + playlist.uuid)
107 await testPlaylist('/videos/watch/playlist/' + playlist.shortUUID)
108 })
109
110 it('Should redirect to the origin video object', async function () {
111 const res = await makeActivityPubGetRequest(servers[1].url, '/videos/watch/' + video.uuid, HttpStatusCode.FOUND_302)
112
113 expect(res.header.location).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
114 })
115
116 it('Should return the watch action', async function () {
117 this.timeout(50000)
118
119 await servers[0].views.simulateViewer({ id: video.uuid, currentTimes: [ 0, 2 ] })
120 await processViewersStats(servers)
121
122 const res = await makeActivityPubGetRequest(servers[0].url, '/videos/local-viewer/1', HttpStatusCode.OK_200)
123
124 const object: WatchActionObject = res.body
125 expect(object.type).to.equal('WatchAction')
126 expect(object.duration).to.equal('PT2S')
127 expect(object.actionStatus).to.equal('CompletedActionStatus')
128 expect(object.watchSections).to.have.lengthOf(1)
129 expect(object.watchSections[0].startTimestamp).to.equal(0)
130 expect(object.watchSections[0].endTimestamp).to.equal(2)
131 })
132
133 after(async function () {
134 await cleanupTests(servers)
135 })
136})
diff --git a/packages/tests/src/api/activitypub/fetch.ts b/packages/tests/src/api/activitypub/fetch.ts
new file mode 100644
index 000000000..c7f5288cc
--- /dev/null
+++ b/packages/tests/src/api/activitypub/fetch.ts
@@ -0,0 +1,82 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { SQLCommand } from '@tests/shared/sql-command.js'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test ActivityPub fetcher', function () {
15 let servers: PeerTubeServer[]
16 let sqlCommandServer1: SQLCommand
17
18 // ---------------------------------------------------------------
19
20 before(async function () {
21 this.timeout(60000)
22
23 servers = await createMultipleServers(3)
24
25 // Get the access tokens
26 await setAccessTokensToServers(servers)
27
28 const user = { username: 'user1', password: 'password' }
29 for (const server of servers) {
30 await server.users.create({ username: user.username, password: user.password })
31 }
32
33 const userAccessToken = await servers[0].login.getAccessToken(user)
34
35 await servers[0].videos.upload({ attributes: { name: 'video root' } })
36 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'bad video root' } })
37 await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'video user' } })
38
39 sqlCommandServer1 = new SQLCommand(servers[0])
40
41 {
42 const to = servers[0].url + '/accounts/user1'
43 const value = servers[1].url + '/accounts/user1'
44 await sqlCommandServer1.setActorField(to, 'url', value)
45 }
46
47 {
48 const value = servers[2].url + '/videos/watch/' + uuid
49 await sqlCommandServer1.setVideoField(uuid, 'url', value)
50 }
51 })
52
53 it('Should add only the video with a valid actor URL', async function () {
54 this.timeout(60000)
55
56 await doubleFollow(servers[0], servers[1])
57 await waitJobs(servers)
58
59 {
60 const { total, data } = await servers[0].videos.list({ sort: 'createdAt' })
61
62 expect(total).to.equal(3)
63 expect(data[0].name).to.equal('video root')
64 expect(data[1].name).to.equal('bad video root')
65 expect(data[2].name).to.equal('video user')
66 }
67
68 {
69 const { total, data } = await servers[1].videos.list({ sort: 'createdAt' })
70
71 expect(total).to.equal(1)
72 expect(data[0].name).to.equal('video root')
73 }
74 })
75
76 after(async function () {
77 this.timeout(20000)
78
79 await sqlCommandServer1.cleanup()
80 await cleanupTests(servers)
81 })
82})
diff --git a/packages/tests/src/api/activitypub/index.ts b/packages/tests/src/api/activitypub/index.ts
new file mode 100644
index 000000000..ef4f1aafb
--- /dev/null
+++ b/packages/tests/src/api/activitypub/index.ts
@@ -0,0 +1,5 @@
1import './cleaner.js'
2import './client.js'
3import './fetch.js'
4import './refresher.js'
5import './security.js'
diff --git a/packages/tests/src/api/activitypub/refresher.ts b/packages/tests/src/api/activitypub/refresher.ts
new file mode 100644
index 000000000..90aa1a5ad
--- /dev/null
+++ b/packages/tests/src/api/activitypub/refresher.ts
@@ -0,0 +1,157 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { SQLCommand } from '@tests/shared/sql-command.js'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, VideoPlaylistPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 killallServers,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test AP refresher', function () {
18 let servers: PeerTubeServer[] = []
19 let sqlCommandServer2: SQLCommand
20 let videoUUID1: string
21 let videoUUID2: string
22 let videoUUID3: string
23 let playlistUUID1: string
24 let playlistUUID2: string
25
26 before(async function () {
27 this.timeout(240000)
28
29 servers = await createMultipleServers(2)
30
31 // Get the access tokens
32 await setAccessTokensToServers(servers)
33 await setDefaultVideoChannel(servers)
34
35 for (const server of servers) {
36 await server.config.disableTranscoding()
37 }
38
39 {
40 videoUUID1 = (await servers[1].videos.quickUpload({ name: 'video1' })).uuid
41 videoUUID2 = (await servers[1].videos.quickUpload({ name: 'video2' })).uuid
42 videoUUID3 = (await servers[1].videos.quickUpload({ name: 'video3' })).uuid
43 }
44
45 {
46 const token1 = await servers[1].users.generateUserAndToken('user1')
47 await servers[1].videos.upload({ token: token1, attributes: { name: 'video4' } })
48
49 const token2 = await servers[1].users.generateUserAndToken('user2')
50 await servers[1].videos.upload({ token: token2, attributes: { name: 'video5' } })
51 }
52
53 {
54 const attributes = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
55 const created = await servers[1].playlists.create({ attributes })
56 playlistUUID1 = created.uuid
57 }
58
59 {
60 const attributes = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].store.channel.id }
61 const created = await servers[1].playlists.create({ attributes })
62 playlistUUID2 = created.uuid
63 }
64
65 await doubleFollow(servers[0], servers[1])
66
67 sqlCommandServer2 = new SQLCommand(servers[1])
68 })
69
70 describe('Videos refresher', function () {
71
72 it('Should remove a deleted remote video', async function () {
73 this.timeout(60000)
74
75 await wait(10000)
76
77 // Change UUID so the remote server returns a 404
78 await sqlCommandServer2.setVideoField(videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
79
80 await servers[0].videos.get({ id: videoUUID1 })
81 await servers[0].videos.get({ id: videoUUID2 })
82
83 await waitJobs(servers)
84
85 await servers[0].videos.get({ id: videoUUID1, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
86 await servers[0].videos.get({ id: videoUUID2 })
87 })
88
89 it('Should not update a remote video if the remote instance is down', async function () {
90 this.timeout(70000)
91
92 await killallServers([ servers[1] ])
93
94 await sqlCommandServer2.setVideoField(videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
95
96 // Video will need a refresh
97 await wait(10000)
98
99 await servers[0].videos.get({ id: videoUUID3 })
100 // The refresh should fail
101 await waitJobs([ servers[0] ])
102
103 await servers[1].run()
104
105 await servers[0].videos.get({ id: videoUUID3 })
106 })
107 })
108
109 describe('Actors refresher', function () {
110
111 it('Should remove a deleted actor', async function () {
112 this.timeout(60000)
113
114 const command = servers[0].accounts
115
116 await wait(10000)
117
118 // Change actor name so the remote server returns a 404
119 const to = servers[1].url + '/accounts/user2'
120 await sqlCommandServer2.setActorField(to, 'preferredUsername', 'toto')
121
122 await command.get({ accountName: 'user1@' + servers[1].host })
123 await command.get({ accountName: 'user2@' + servers[1].host })
124
125 await waitJobs(servers)
126
127 await command.get({ accountName: 'user1@' + servers[1].host, expectedStatus: HttpStatusCode.OK_200 })
128 await command.get({ accountName: 'user2@' + servers[1].host, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
129 })
130 })
131
132 describe('Playlist refresher', function () {
133
134 it('Should remove a deleted playlist', async function () {
135 this.timeout(60000)
136
137 await wait(10000)
138
139 // Change UUID so the remote server returns a 404
140 await sqlCommandServer2.setPlaylistField(playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
141
142 await servers[0].playlists.get({ playlistId: playlistUUID1 })
143 await servers[0].playlists.get({ playlistId: playlistUUID2 })
144
145 await waitJobs(servers)
146
147 await servers[0].playlists.get({ playlistId: playlistUUID1, expectedStatus: HttpStatusCode.OK_200 })
148 await servers[0].playlists.get({ playlistId: playlistUUID2, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
149 })
150 })
151
152 after(async function () {
153 await sqlCommandServer2.cleanup()
154
155 await cleanupTests(servers)
156 })
157})
diff --git a/packages/tests/src/api/activitypub/security.ts b/packages/tests/src/api/activitypub/security.ts
new file mode 100644
index 000000000..d9649de50
--- /dev/null
+++ b/packages/tests/src/api/activitypub/security.ts
@@ -0,0 +1,331 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { wait } from '@peertube/peertube-core-utils'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
6import { PeerTubeServer, cleanupTests, createMultipleServers, killallServers } from '@peertube/peertube-server-commands'
7import {
8 activityPubContextify,
9 buildGlobalHTTPHeaders,
10 signAndContextify
11} from '@peertube/peertube-server/server/helpers/activity-pub-utils.js'
12import { buildDigest } from '@peertube/peertube-server/server/helpers/peertube-crypto.js'
13import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@peertube/peertube-server/server/initializers/constants.js'
14import { makePOSTAPRequest } from '@tests/shared/requests.js'
15import { SQLCommand } from '@tests/shared/sql-command.js'
16import { expect } from 'chai'
17import { readJsonSync } from 'fs-extra/esm'
18
19function fakeFilter () {
20 return (data: any) => Promise.resolve(data)
21}
22
23function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) {
24 const url = ofServerUrl + '/accounts/peertube'
25
26 return Promise.all([
27 onServer.setActorField(url, 'publicKey', publicKey),
28 onServer.setActorField(url, 'privateKey', privateKey)
29 ])
30}
31
32function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) {
33 const url = ofServerUrl + '/accounts/peertube'
34
35 return Promise.all([
36 onServer.setActorField(url, 'createdAt', updatedAt),
37 onServer.setActorField(url, 'updatedAt', updatedAt)
38 ])
39}
40
41function getAnnounceWithoutContext (server: PeerTubeServer) {
42 const json = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
43 const result: typeof json = {}
44
45 for (const key of Object.keys(json)) {
46 if (Array.isArray(json[key])) {
47 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
48 } else {
49 result[key] = json[key].replace(':9002', `:${server.port}`)
50 }
51 }
52
53 return result
54}
55
56async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
57 const follow = {
58 type: 'Follow',
59 id: by.url + '/' + new Date().getTime(),
60 actor: by.url,
61 object: to.url
62 }
63
64 const body = await activityPubContextify(follow, 'Follow', fakeFilter())
65
66 const httpSignature = {
67 algorithm: HTTP_SIGNATURE.ALGORITHM,
68 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
69 keyId: by.url,
70 key: by.privateKey,
71 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
72 }
73 const headers = {
74 'digest': buildDigest(body),
75 'content-type': 'application/activity+json',
76 'accept': ACTIVITY_PUB.ACCEPT_HEADER
77 }
78
79 return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
80}
81
82describe('Test ActivityPub security', function () {
83 let servers: PeerTubeServer[]
84 let sqlCommands: SQLCommand[] = []
85
86 let url: string
87
88 const keys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
89 const invalidKeys = readJsonSync(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
90 const baseHttpSignature = () => ({
91 algorithm: HTTP_SIGNATURE.ALGORITHM,
92 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
93 keyId: 'acct:peertube@' + servers[1].host,
94 key: keys.privateKey,
95 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
96 })
97
98 // ---------------------------------------------------------------
99
100 before(async function () {
101 this.timeout(60000)
102
103 servers = await createMultipleServers(3)
104
105 sqlCommands = servers.map(s => new SQLCommand(s))
106
107 url = servers[0].url + '/inbox'
108
109 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null)
110 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
111
112 const to = { url: servers[0].url + '/accounts/peertube' }
113 const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
114 await makeFollowRequest(to, by)
115 })
116
117 describe('When checking HTTP signature', function () {
118
119 it('Should fail with an invalid digest', async function () {
120 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
121 const headers = {
122 Digest: buildDigest({ hello: 'coucou' })
123 }
124
125 try {
126 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
127 expect(true, 'Did not throw').to.be.false
128 } catch (err) {
129 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
130 }
131 })
132
133 it('Should fail with an invalid date', async function () {
134 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
135 const headers = buildGlobalHTTPHeaders(body)
136 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
137
138 try {
139 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
140 expect(true, 'Did not throw').to.be.false
141 } catch (err) {
142 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
143 }
144 })
145
146 it('Should fail with bad keys', async function () {
147 await setKeysOfServer(sqlCommands[0], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
148 await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
149
150 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
151 const headers = buildGlobalHTTPHeaders(body)
152
153 try {
154 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
155 expect(true, 'Did not throw').to.be.false
156 } catch (err) {
157 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
158 }
159 })
160
161 it('Should reject requests without appropriate signed headers', async function () {
162 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
163 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
164
165 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
166 const headers = buildGlobalHTTPHeaders(body)
167
168 const signatureOptions = baseHttpSignature()
169 const badHeadersMatrix = [
170 [ '(request-target)', 'date', 'digest' ],
171 [ 'host', 'date', 'digest' ],
172 [ '(request-target)', 'host', 'digest' ]
173 ]
174
175 for (const badHeaders of badHeadersMatrix) {
176 signatureOptions.headers = badHeaders
177
178 try {
179 await makePOSTAPRequest(url, body, signatureOptions, headers)
180 expect(true, 'Did not throw').to.be.false
181 } catch (err) {
182 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
183 }
184 }
185 })
186
187 it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
188 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
189 const headers = buildGlobalHTTPHeaders(body)
190
191 const signatureOptions = baseHttpSignature()
192 signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ]
193
194 const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers)
195 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
196 })
197
198 it('Should succeed with a valid HTTP signature', async function () {
199 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
200 const headers = buildGlobalHTTPHeaders(body)
201
202 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
203 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
204 })
205
206 it('Should refresh the actor keys', async function () {
207 this.timeout(20000)
208
209 // Update keys of server 2 to invalid keys
210 // Server 1 should refresh the actor and fail
211 await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
212 await setUpdatedAtOfServer(sqlCommands[0], servers[1].url, '2015-07-17 22:00:00+00')
213
214 // Invalid peertube actor cache
215 await killallServers([ servers[1] ])
216 await servers[1].run()
217
218 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce', fakeFilter())
219 const headers = buildGlobalHTTPHeaders(body)
220
221 try {
222 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
223 expect(true, 'Did not throw').to.be.false
224 } catch (err) {
225 console.error(err)
226 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
227 }
228 })
229 })
230
231 describe('When checking Linked Data Signature', function () {
232 before(async function () {
233 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
234 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
235 await setKeysOfServer(sqlCommands[2], servers[2].url, keys.publicKey, keys.privateKey)
236
237 const to = { url: servers[0].url + '/accounts/peertube' }
238 const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
239 await makeFollowRequest(to, by)
240 })
241
242 it('Should fail with bad keys', async function () {
243 await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
244 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
245
246 const body = getAnnounceWithoutContext(servers[1])
247 body.actor = servers[2].url + '/accounts/peertube'
248
249 const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' }
250 const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter())
251
252 const headers = buildGlobalHTTPHeaders(signedBody)
253
254 try {
255 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
256 expect(true, 'Did not throw').to.be.false
257 } catch (err) {
258 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
259 }
260 })
261
262 it('Should fail with an altered body', async function () {
263 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
264 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
265
266 const body = getAnnounceWithoutContext(servers[1])
267 body.actor = servers[2].url + '/accounts/peertube'
268
269 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
270 const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter())
271
272 signedBody.actor = servers[2].url + '/account/peertube'
273
274 const headers = buildGlobalHTTPHeaders(signedBody)
275
276 try {
277 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
278 expect(true, 'Did not throw').to.be.false
279 } catch (err) {
280 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
281 }
282 })
283
284 it('Should succeed with a valid signature', async function () {
285 const body = getAnnounceWithoutContext(servers[1])
286 body.actor = servers[2].url + '/accounts/peertube'
287
288 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
289 const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter())
290
291 const headers = buildGlobalHTTPHeaders(signedBody)
292
293 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
294 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
295 })
296
297 it('Should refresh the actor keys', async function () {
298 this.timeout(20000)
299
300 // Wait refresh invalidation
301 await wait(10000)
302
303 // Update keys of server 3 to invalid keys
304 // Server 1 should refresh the actor and fail
305 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
306
307 const body = getAnnounceWithoutContext(servers[1])
308 body.actor = servers[2].url + '/accounts/peertube'
309
310 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
311 const signedBody = await signAndContextify(signer, body, 'Announce', fakeFilter())
312
313 const headers = buildGlobalHTTPHeaders(signedBody)
314
315 try {
316 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
317 expect(true, 'Did not throw').to.be.false
318 } catch (err) {
319 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
320 }
321 })
322 })
323
324 after(async function () {
325 for (const sql of sqlCommands) {
326 await sql.cleanup()
327 }
328
329 await cleanupTests(servers)
330 })
331})
diff --git a/packages/tests/src/api/check-params/abuses.ts b/packages/tests/src/api/check-params/abuses.ts
new file mode 100644
index 000000000..1effc82b1
--- /dev/null
+++ b/packages/tests/src/api/check-params/abuses.ts
@@ -0,0 +1,438 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { AbuseCreate, AbuseState, HttpStatusCode } from '@peertube/peertube-models'
5import {
6 AbusesCommand,
7 cleanupTests,
8 createSingleServer,
9 doubleFollow,
10 makeGetRequest,
11 makePostBodyRequest,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test abuses API validators', function () {
18 const basePath = '/api/v1/abuses/'
19
20 let server: PeerTubeServer
21
22 let userToken = ''
23 let userToken2 = ''
24 let abuseId: number
25 let messageId: number
26
27 let command: AbusesCommand
28
29 // ---------------------------------------------------------------
30
31 before(async function () {
32 this.timeout(30000)
33
34 server = await createSingleServer(1)
35
36 await setAccessTokensToServers([ server ])
37
38 userToken = await server.users.generateUserAndToken('user_1')
39 userToken2 = await server.users.generateUserAndToken('user_2')
40
41 server.store.videoCreated = await server.videos.upload()
42
43 command = server.abuses
44 })
45
46 describe('When listing abuses for admins', function () {
47 const path = basePath
48
49 it('Should fail with a bad start pagination', async function () {
50 await checkBadStartPagination(server.url, path, server.accessToken)
51 })
52
53 it('Should fail with a bad count pagination', async function () {
54 await checkBadCountPagination(server.url, path, server.accessToken)
55 })
56
57 it('Should fail with an incorrect sort', async function () {
58 await checkBadSortPagination(server.url, path, server.accessToken)
59 })
60
61 it('Should fail with a non authenticated user', async function () {
62 await makeGetRequest({
63 url: server.url,
64 path,
65 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
66 })
67 })
68
69 it('Should fail with a non admin user', async function () {
70 await makeGetRequest({
71 url: server.url,
72 path,
73 token: userToken,
74 expectedStatus: HttpStatusCode.FORBIDDEN_403
75 })
76 })
77
78 it('Should fail with a bad id filter', async function () {
79 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } })
80 })
81
82 it('Should fail with a bad filter', async function () {
83 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } })
84 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } })
85 })
86
87 it('Should fail with bad predefined reason', async function () {
88 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } })
89 })
90
91 it('Should fail with a bad state filter', async function () {
92 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } })
93 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } })
94 })
95
96 it('Should fail with a bad videoIs filter', async function () {
97 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } })
98 })
99
100 it('Should succeed with the correct params', async function () {
101 const query = {
102 id: 13,
103 predefinedReason: 'violentOrRepulsive',
104 filter: 'comment',
105 state: 2,
106 videoIs: 'deleted'
107 }
108
109 await makeGetRequest({ url: server.url, path, token: server.accessToken, query, expectedStatus: HttpStatusCode.OK_200 })
110 })
111 })
112
113 describe('When listing abuses for users', function () {
114 const path = '/api/v1/users/me/abuses'
115
116 it('Should fail with a bad start pagination', async function () {
117 await checkBadStartPagination(server.url, path, userToken)
118 })
119
120 it('Should fail with a bad count pagination', async function () {
121 await checkBadCountPagination(server.url, path, userToken)
122 })
123
124 it('Should fail with an incorrect sort', async function () {
125 await checkBadSortPagination(server.url, path, userToken)
126 })
127
128 it('Should fail with a non authenticated user', async function () {
129 await makeGetRequest({
130 url: server.url,
131 path,
132 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
133 })
134 })
135
136 it('Should fail with a bad id filter', async function () {
137 await makeGetRequest({ url: server.url, path, token: userToken, query: { id: 'toto' } })
138 })
139
140 it('Should fail with a bad state filter', async function () {
141 await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 'toto' } })
142 await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 0 } })
143 })
144
145 it('Should succeed with the correct params', async function () {
146 const query = {
147 id: 13,
148 state: 2
149 }
150
151 await makeGetRequest({ url: server.url, path, token: userToken, query, expectedStatus: HttpStatusCode.OK_200 })
152 })
153 })
154
155 describe('When reporting an abuse', function () {
156 const path = basePath
157
158 it('Should fail with nothing', async function () {
159 const fields = {}
160 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
161 })
162
163 it('Should fail with a wrong video', async function () {
164 const fields = { video: { id: 'blabla' }, reason: 'my super reason' }
165 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
166 })
167
168 it('Should fail with an unknown video', async function () {
169 const fields = { video: { id: 42 }, reason: 'my super reason' }
170 await makePostBodyRequest({
171 url: server.url,
172 path,
173 token: userToken,
174 fields,
175 expectedStatus: HttpStatusCode.NOT_FOUND_404
176 })
177 })
178
179 it('Should fail with a wrong comment', async function () {
180 const fields = { comment: { id: 'blabla' }, reason: 'my super reason' }
181 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
182 })
183
184 it('Should fail with an unknown comment', async function () {
185 const fields = { comment: { id: 42 }, reason: 'my super reason' }
186 await makePostBodyRequest({
187 url: server.url,
188 path,
189 token: userToken,
190 fields,
191 expectedStatus: HttpStatusCode.NOT_FOUND_404
192 })
193 })
194
195 it('Should fail with a wrong account', async function () {
196 const fields = { account: { id: 'blabla' }, reason: 'my super reason' }
197 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
198 })
199
200 it('Should fail with an unknown account', async function () {
201 const fields = { account: { id: 42 }, reason: 'my super reason' }
202 await makePostBodyRequest({
203 url: server.url,
204 path,
205 token: userToken,
206 fields,
207 expectedStatus: HttpStatusCode.NOT_FOUND_404
208 })
209 })
210
211 it('Should fail with not account, comment or video', async function () {
212 const fields = { reason: 'my super reason' }
213 await makePostBodyRequest({
214 url: server.url,
215 path,
216 token: userToken,
217 fields,
218 expectedStatus: HttpStatusCode.BAD_REQUEST_400
219 })
220 })
221
222 it('Should fail with a non authenticated user', async function () {
223 const fields = { video: { id: server.store.videoCreated.id }, reason: 'my super reason' }
224
225 await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
226 })
227
228 it('Should fail with a reason too short', async function () {
229 const fields = { video: { id: server.store.videoCreated.id }, reason: 'h' }
230
231 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
232 })
233
234 it('Should fail with a too big reason', async function () {
235 const fields = { video: { id: server.store.videoCreated.id }, reason: 'super'.repeat(605) }
236
237 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
238 })
239
240 it('Should succeed with the correct parameters (basic)', async function () {
241 const fields: AbuseCreate = { video: { id: server.store.videoCreated.shortUUID }, reason: 'my super reason' }
242
243 const res = await makePostBodyRequest({
244 url: server.url,
245 path,
246 token: userToken,
247 fields,
248 expectedStatus: HttpStatusCode.OK_200
249 })
250 abuseId = res.body.abuse.id
251 })
252
253 it('Should fail with a wrong predefined reason', async function () {
254 const fields = { video: server.store.videoCreated, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
255
256 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
257 })
258
259 it('Should fail with negative timestamps', async function () {
260 const fields = { video: { id: server.store.videoCreated.id, startAt: -1 }, reason: 'my super reason' }
261
262 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
263 })
264
265 it('Should fail mith misordered startAt/endAt', async function () {
266 const fields = { video: { id: server.store.videoCreated.id, startAt: 5, endAt: 1 }, reason: 'my super reason' }
267
268 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
269 })
270
271 it('Should succeed with the correct parameters (advanced)', async function () {
272 const fields: AbuseCreate = {
273 video: {
274 id: server.store.videoCreated.id,
275 startAt: 1,
276 endAt: 5
277 },
278 reason: 'my super reason',
279 predefinedReasons: [ 'serverRules' ]
280 }
281
282 await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.OK_200 })
283 })
284 })
285
286 describe('When updating an abuse', function () {
287
288 it('Should fail with a non authenticated user', async function () {
289 await command.update({ token: 'blabla', abuseId, body: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
290 })
291
292 it('Should fail with a non admin user', async function () {
293 await command.update({ token: userToken, abuseId, body: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
294 })
295
296 it('Should fail with a bad abuse id', async function () {
297 await command.update({ abuseId: 45, body: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
298 })
299
300 it('Should fail with a bad state', async function () {
301 const body = { state: 5 as any }
302 await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
303 })
304
305 it('Should fail with a bad moderation comment', async function () {
306 const body = { moderationComment: 'b'.repeat(3001) }
307 await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
308 })
309
310 it('Should succeed with the correct params', async function () {
311 const body = { state: AbuseState.ACCEPTED }
312 await command.update({ abuseId, body })
313 })
314 })
315
316 describe('When creating an abuse message', function () {
317 const message = 'my super message'
318
319 it('Should fail with an invalid abuse id', async function () {
320 await command.addMessage({ token: userToken2, abuseId: 888, message, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
321 })
322
323 it('Should fail with a non authenticated user', async function () {
324 await command.addMessage({ token: 'fake_token', abuseId, message, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
325 })
326
327 it('Should fail with an invalid logged in user', async function () {
328 await command.addMessage({ token: userToken2, abuseId, message, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
329 })
330
331 it('Should fail with an invalid message', async function () {
332 await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
333 })
334
335 it('Should succeed with the correct params', async function () {
336 const res = await command.addMessage({ token: userToken, abuseId, message })
337 messageId = res.body.abuseMessage.id
338 })
339 })
340
341 describe('When listing abuse messages', function () {
342
343 it('Should fail with an invalid abuse id', async function () {
344 await command.listMessages({ token: userToken, abuseId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
345 })
346
347 it('Should fail with a non authenticated user', async function () {
348 await command.listMessages({ token: 'fake_token', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
349 })
350
351 it('Should fail with an invalid logged in user', async function () {
352 await command.listMessages({ token: userToken2, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
353 })
354
355 it('Should succeed with the correct params', async function () {
356 await command.listMessages({ token: userToken, abuseId })
357 })
358 })
359
360 describe('When deleting an abuse message', function () {
361 it('Should fail with an invalid abuse id', async function () {
362 await command.deleteMessage({ token: userToken, abuseId: 888, messageId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
363 })
364
365 it('Should fail with an invalid message id', async function () {
366 await command.deleteMessage({ token: userToken, abuseId, messageId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
367 })
368
369 it('Should fail with a non authenticated user', async function () {
370 await command.deleteMessage({ token: 'fake_token', abuseId, messageId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
371 })
372
373 it('Should fail with an invalid logged in user', async function () {
374 await command.deleteMessage({ token: userToken2, abuseId, messageId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
375 })
376
377 it('Should succeed with the correct params', async function () {
378 await command.deleteMessage({ token: userToken, abuseId, messageId })
379 })
380 })
381
382 describe('When deleting a video abuse', function () {
383
384 it('Should fail with a non authenticated user', async function () {
385 await command.delete({ token: 'blabla', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
386 })
387
388 it('Should fail with a non admin user', async function () {
389 await command.delete({ token: userToken, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
390 })
391
392 it('Should fail with a bad abuse id', async function () {
393 await command.delete({ abuseId: 45, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
394 })
395
396 it('Should succeed with the correct params', async function () {
397 await command.delete({ abuseId })
398 })
399 })
400
401 describe('When trying to manage messages of a remote abuse', function () {
402 let remoteAbuseId: number
403 let anotherServer: PeerTubeServer
404
405 before(async function () {
406 this.timeout(50000)
407
408 anotherServer = await createSingleServer(2)
409 await setAccessTokensToServers([ anotherServer ])
410
411 await doubleFollow(anotherServer, server)
412
413 const server2VideoId = await anotherServer.videos.getId({ uuid: server.store.videoCreated.uuid })
414 await anotherServer.abuses.report({ reason: 'remote server', videoId: server2VideoId })
415
416 await waitJobs([ server, anotherServer ])
417
418 const body = await command.getAdminList({ sort: '-createdAt' })
419 remoteAbuseId = body.data[0].id
420 })
421
422 it('Should fail when listing abuse messages of a remote abuse', async function () {
423 await command.listMessages({ abuseId: remoteAbuseId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
424 })
425
426 it('Should fail when creating abuse message of a remote abuse', async function () {
427 await command.addMessage({ abuseId: remoteAbuseId, message: 'message', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
428 })
429
430 after(async function () {
431 await cleanupTests([ anotherServer ])
432 })
433 })
434
435 after(async function () {
436 await cleanupTests([ server ])
437 })
438})
diff --git a/packages/tests/src/api/check-params/accounts.ts b/packages/tests/src/api/check-params/accounts.ts
new file mode 100644
index 000000000..87810bbd3
--- /dev/null
+++ b/packages/tests/src/api/check-params/accounts.ts
@@ -0,0 +1,43 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands'
6
7describe('Test accounts API validators', function () {
8 const path = '/api/v1/accounts/'
9 let server: PeerTubeServer
10
11 // ---------------------------------------------------------------
12
13 before(async function () {
14 this.timeout(30000)
15
16 server = await createSingleServer(1)
17 })
18
19 describe('When listing accounts', function () {
20 it('Should fail with a bad start pagination', async function () {
21 await checkBadStartPagination(server.url, path, server.accessToken)
22 })
23
24 it('Should fail with a bad count pagination', async function () {
25 await checkBadCountPagination(server.url, path, server.accessToken)
26 })
27
28 it('Should fail with an incorrect sort', async function () {
29 await checkBadSortPagination(server.url, path, server.accessToken)
30 })
31 })
32
33 describe('When getting an account', function () {
34
35 it('Should return 404 with a non existing name', async function () {
36 await server.accounts.get({ accountName: 'arfaze', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
37 })
38 })
39
40 after(async function () {
41 await cleanupTests([ server ])
42 })
43})
diff --git a/packages/tests/src/api/check-params/blocklist.ts b/packages/tests/src/api/check-params/blocklist.ts
new file mode 100644
index 000000000..fcd6d08f8
--- /dev/null
+++ b/packages/tests/src/api/check-params/blocklist.ts
@@ -0,0 +1,556 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 makeDeleteRequest,
10 makeGetRequest,
11 makePostBodyRequest,
12 PeerTubeServer,
13 setAccessTokensToServers
14} from '@peertube/peertube-server-commands'
15
16describe('Test blocklist API validators', function () {
17 let servers: PeerTubeServer[]
18 let server: PeerTubeServer
19 let userAccessToken: string
20
21 before(async function () {
22 this.timeout(60000)
23
24 servers = await createMultipleServers(2)
25 await setAccessTokensToServers(servers)
26
27 server = servers[0]
28
29 const user = { username: 'user1', password: 'password' }
30 await server.users.create({ username: user.username, password: user.password })
31
32 userAccessToken = await server.login.getAccessToken(user)
33
34 await doubleFollow(servers[0], servers[1])
35 })
36
37 // ---------------------------------------------------------------
38
39 describe('When managing user blocklist', function () {
40
41 describe('When managing user accounts blocklist', function () {
42 const path = '/api/v1/users/me/blocklist/accounts'
43
44 describe('When listing blocked accounts', function () {
45 it('Should fail with an unauthenticated user', async function () {
46 await makeGetRequest({
47 url: server.url,
48 path,
49 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
50 })
51 })
52
53 it('Should fail with a bad start pagination', async function () {
54 await checkBadStartPagination(server.url, path, server.accessToken)
55 })
56
57 it('Should fail with a bad count pagination', async function () {
58 await checkBadCountPagination(server.url, path, server.accessToken)
59 })
60
61 it('Should fail with an incorrect sort', async function () {
62 await checkBadSortPagination(server.url, path, server.accessToken)
63 })
64 })
65
66 describe('When blocking an account', function () {
67 it('Should fail with an unauthenticated user', async function () {
68 await makePostBodyRequest({
69 url: server.url,
70 path,
71 fields: { accountName: 'user1' },
72 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
73 })
74 })
75
76 it('Should fail with an unknown account', async function () {
77 await makePostBodyRequest({
78 url: server.url,
79 token: server.accessToken,
80 path,
81 fields: { accountName: 'user2' },
82 expectedStatus: HttpStatusCode.NOT_FOUND_404
83 })
84 })
85
86 it('Should fail to block ourselves', async function () {
87 await makePostBodyRequest({
88 url: server.url,
89 token: server.accessToken,
90 path,
91 fields: { accountName: 'root' },
92 expectedStatus: HttpStatusCode.CONFLICT_409
93 })
94 })
95
96 it('Should succeed with the correct params', async function () {
97 await makePostBodyRequest({
98 url: server.url,
99 token: server.accessToken,
100 path,
101 fields: { accountName: 'user1' },
102 expectedStatus: HttpStatusCode.NO_CONTENT_204
103 })
104 })
105 })
106
107 describe('When unblocking an account', function () {
108 it('Should fail with an unauthenticated user', async function () {
109 await makeDeleteRequest({
110 url: server.url,
111 path: path + '/user1',
112 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
113 })
114 })
115
116 it('Should fail with an unknown account block', async function () {
117 await makeDeleteRequest({
118 url: server.url,
119 path: path + '/user2',
120 token: server.accessToken,
121 expectedStatus: HttpStatusCode.NOT_FOUND_404
122 })
123 })
124
125 it('Should succeed with the correct params', async function () {
126 await makeDeleteRequest({
127 url: server.url,
128 path: path + '/user1',
129 token: server.accessToken,
130 expectedStatus: HttpStatusCode.NO_CONTENT_204
131 })
132 })
133 })
134 })
135
136 describe('When managing user servers blocklist', function () {
137 const path = '/api/v1/users/me/blocklist/servers'
138
139 describe('When listing blocked servers', function () {
140 it('Should fail with an unauthenticated user', async function () {
141 await makeGetRequest({
142 url: server.url,
143 path,
144 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
145 })
146 })
147
148 it('Should fail with a bad start pagination', async function () {
149 await checkBadStartPagination(server.url, path, server.accessToken)
150 })
151
152 it('Should fail with a bad count pagination', async function () {
153 await checkBadCountPagination(server.url, path, server.accessToken)
154 })
155
156 it('Should fail with an incorrect sort', async function () {
157 await checkBadSortPagination(server.url, path, server.accessToken)
158 })
159 })
160
161 describe('When blocking a server', function () {
162 it('Should fail with an unauthenticated user', async function () {
163 await makePostBodyRequest({
164 url: server.url,
165 path,
166 fields: { host: '127.0.0.1:9002' },
167 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
168 })
169 })
170
171 it('Should succeed with an unknown server', async function () {
172 await makePostBodyRequest({
173 url: server.url,
174 token: server.accessToken,
175 path,
176 fields: { host: '127.0.0.1:9003' },
177 expectedStatus: HttpStatusCode.NO_CONTENT_204
178 })
179 })
180
181 it('Should fail with our own server', async function () {
182 await makePostBodyRequest({
183 url: server.url,
184 token: server.accessToken,
185 path,
186 fields: { host: server.host },
187 expectedStatus: HttpStatusCode.CONFLICT_409
188 })
189 })
190
191 it('Should succeed with the correct params', async function () {
192 await makePostBodyRequest({
193 url: server.url,
194 token: server.accessToken,
195 path,
196 fields: { host: servers[1].host },
197 expectedStatus: HttpStatusCode.NO_CONTENT_204
198 })
199 })
200 })
201
202 describe('When unblocking a server', function () {
203 it('Should fail with an unauthenticated user', async function () {
204 await makeDeleteRequest({
205 url: server.url,
206 path: path + '/' + servers[1].host,
207 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
208 })
209 })
210
211 it('Should fail with an unknown server block', async function () {
212 await makeDeleteRequest({
213 url: server.url,
214 path: path + '/127.0.0.1:9004',
215 token: server.accessToken,
216 expectedStatus: HttpStatusCode.NOT_FOUND_404
217 })
218 })
219
220 it('Should succeed with the correct params', async function () {
221 await makeDeleteRequest({
222 url: server.url,
223 path: path + '/' + servers[1].host,
224 token: server.accessToken,
225 expectedStatus: HttpStatusCode.NO_CONTENT_204
226 })
227 })
228 })
229 })
230 })
231
232 describe('When managing server blocklist', function () {
233
234 describe('When managing server accounts blocklist', function () {
235 const path = '/api/v1/server/blocklist/accounts'
236
237 describe('When listing blocked accounts', function () {
238 it('Should fail with an unauthenticated user', async function () {
239 await makeGetRequest({
240 url: server.url,
241 path,
242 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
243 })
244 })
245
246 it('Should fail with a user without the appropriate rights', async function () {
247 await makeGetRequest({
248 url: server.url,
249 token: userAccessToken,
250 path,
251 expectedStatus: HttpStatusCode.FORBIDDEN_403
252 })
253 })
254
255 it('Should fail with a bad start pagination', async function () {
256 await checkBadStartPagination(server.url, path, server.accessToken)
257 })
258
259 it('Should fail with a bad count pagination', async function () {
260 await checkBadCountPagination(server.url, path, server.accessToken)
261 })
262
263 it('Should fail with an incorrect sort', async function () {
264 await checkBadSortPagination(server.url, path, server.accessToken)
265 })
266 })
267
268 describe('When blocking an account', function () {
269 it('Should fail with an unauthenticated user', async function () {
270 await makePostBodyRequest({
271 url: server.url,
272 path,
273 fields: { accountName: 'user1' },
274 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
275 })
276 })
277
278 it('Should fail with a user without the appropriate rights', async function () {
279 await makePostBodyRequest({
280 url: server.url,
281 token: userAccessToken,
282 path,
283 fields: { accountName: 'user1' },
284 expectedStatus: HttpStatusCode.FORBIDDEN_403
285 })
286 })
287
288 it('Should fail with an unknown account', async function () {
289 await makePostBodyRequest({
290 url: server.url,
291 token: server.accessToken,
292 path,
293 fields: { accountName: 'user2' },
294 expectedStatus: HttpStatusCode.NOT_FOUND_404
295 })
296 })
297
298 it('Should fail to block ourselves', async function () {
299 await makePostBodyRequest({
300 url: server.url,
301 token: server.accessToken,
302 path,
303 fields: { accountName: 'root' },
304 expectedStatus: HttpStatusCode.CONFLICT_409
305 })
306 })
307
308 it('Should succeed with the correct params', async function () {
309 await makePostBodyRequest({
310 url: server.url,
311 token: server.accessToken,
312 path,
313 fields: { accountName: 'user1' },
314 expectedStatus: HttpStatusCode.NO_CONTENT_204
315 })
316 })
317 })
318
319 describe('When unblocking an account', function () {
320 it('Should fail with an unauthenticated user', async function () {
321 await makeDeleteRequest({
322 url: server.url,
323 path: path + '/user1',
324 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
325 })
326 })
327
328 it('Should fail with a user without the appropriate rights', async function () {
329 await makeDeleteRequest({
330 url: server.url,
331 path: path + '/user1',
332 token: userAccessToken,
333 expectedStatus: HttpStatusCode.FORBIDDEN_403
334 })
335 })
336
337 it('Should fail with an unknown account block', async function () {
338 await makeDeleteRequest({
339 url: server.url,
340 path: path + '/user2',
341 token: server.accessToken,
342 expectedStatus: HttpStatusCode.NOT_FOUND_404
343 })
344 })
345
346 it('Should succeed with the correct params', async function () {
347 await makeDeleteRequest({
348 url: server.url,
349 path: path + '/user1',
350 token: server.accessToken,
351 expectedStatus: HttpStatusCode.NO_CONTENT_204
352 })
353 })
354 })
355 })
356
357 describe('When managing server servers blocklist', function () {
358 const path = '/api/v1/server/blocklist/servers'
359
360 describe('When listing blocked servers', function () {
361 it('Should fail with an unauthenticated user', async function () {
362 await makeGetRequest({
363 url: server.url,
364 path,
365 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
366 })
367 })
368
369 it('Should fail with a user without the appropriate rights', async function () {
370 await makeGetRequest({
371 url: server.url,
372 token: userAccessToken,
373 path,
374 expectedStatus: HttpStatusCode.FORBIDDEN_403
375 })
376 })
377
378 it('Should fail with a bad start pagination', async function () {
379 await checkBadStartPagination(server.url, path, server.accessToken)
380 })
381
382 it('Should fail with a bad count pagination', async function () {
383 await checkBadCountPagination(server.url, path, server.accessToken)
384 })
385
386 it('Should fail with an incorrect sort', async function () {
387 await checkBadSortPagination(server.url, path, server.accessToken)
388 })
389 })
390
391 describe('When blocking a server', function () {
392 it('Should fail with an unauthenticated user', async function () {
393 await makePostBodyRequest({
394 url: server.url,
395 path,
396 fields: { host: servers[1].host },
397 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
398 })
399 })
400
401 it('Should fail with a user without the appropriate rights', async function () {
402 await makePostBodyRequest({
403 url: server.url,
404 token: userAccessToken,
405 path,
406 fields: { host: servers[1].host },
407 expectedStatus: HttpStatusCode.FORBIDDEN_403
408 })
409 })
410
411 it('Should succeed with an unknown server', async function () {
412 await makePostBodyRequest({
413 url: server.url,
414 token: server.accessToken,
415 path,
416 fields: { host: '127.0.0.1:9003' },
417 expectedStatus: HttpStatusCode.NO_CONTENT_204
418 })
419 })
420
421 it('Should fail with our own server', async function () {
422 await makePostBodyRequest({
423 url: server.url,
424 token: server.accessToken,
425 path,
426 fields: { host: server.host },
427 expectedStatus: HttpStatusCode.CONFLICT_409
428 })
429 })
430
431 it('Should succeed with the correct params', async function () {
432 await makePostBodyRequest({
433 url: server.url,
434 token: server.accessToken,
435 path,
436 fields: { host: servers[1].host },
437 expectedStatus: HttpStatusCode.NO_CONTENT_204
438 })
439 })
440 })
441
442 describe('When unblocking a server', function () {
443 it('Should fail with an unauthenticated user', async function () {
444 await makeDeleteRequest({
445 url: server.url,
446 path: path + '/' + servers[1].host,
447 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
448 })
449 })
450
451 it('Should fail with a user without the appropriate rights', async function () {
452 await makeDeleteRequest({
453 url: server.url,
454 path: path + '/' + servers[1].host,
455 token: userAccessToken,
456 expectedStatus: HttpStatusCode.FORBIDDEN_403
457 })
458 })
459
460 it('Should fail with an unknown server block', async function () {
461 await makeDeleteRequest({
462 url: server.url,
463 path: path + '/127.0.0.1:9004',
464 token: server.accessToken,
465 expectedStatus: HttpStatusCode.NOT_FOUND_404
466 })
467 })
468
469 it('Should succeed with the correct params', async function () {
470 await makeDeleteRequest({
471 url: server.url,
472 path: path + '/' + servers[1].host,
473 token: server.accessToken,
474 expectedStatus: HttpStatusCode.NO_CONTENT_204
475 })
476 })
477 })
478 })
479 })
480
481 describe('When getting blocklist status', function () {
482 const path = '/api/v1/blocklist/status'
483
484 it('Should fail with a bad token', async function () {
485 await makeGetRequest({
486 url: server.url,
487 path,
488 token: 'false',
489 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
490 })
491 })
492
493 it('Should fail with a bad accounts field', async function () {
494 await makeGetRequest({
495 url: server.url,
496 path,
497 query: {
498 accounts: 1
499 },
500 expectedStatus: HttpStatusCode.BAD_REQUEST_400
501 })
502
503 await makeGetRequest({
504 url: server.url,
505 path,
506 query: {
507 accounts: [ 1 ]
508 },
509 expectedStatus: HttpStatusCode.BAD_REQUEST_400
510 })
511 })
512
513 it('Should fail with a bad hosts field', async function () {
514 await makeGetRequest({
515 url: server.url,
516 path,
517 query: {
518 hosts: 1
519 },
520 expectedStatus: HttpStatusCode.BAD_REQUEST_400
521 })
522
523 await makeGetRequest({
524 url: server.url,
525 path,
526 query: {
527 hosts: [ 1 ]
528 },
529 expectedStatus: HttpStatusCode.BAD_REQUEST_400
530 })
531 })
532
533 it('Should succeed with the correct parameters', async function () {
534 await makeGetRequest({
535 url: server.url,
536 path,
537 query: {},
538 expectedStatus: HttpStatusCode.OK_200
539 })
540
541 await makeGetRequest({
542 url: server.url,
543 path,
544 query: {
545 hosts: [ 'example.com' ],
546 accounts: [ 'john@example.com' ]
547 },
548 expectedStatus: HttpStatusCode.OK_200
549 })
550 })
551 })
552
553 after(async function () {
554 await cleanupTests(servers)
555 })
556})
diff --git a/packages/tests/src/api/check-params/bulk.ts b/packages/tests/src/api/check-params/bulk.ts
new file mode 100644
index 000000000..def0c38eb
--- /dev/null
+++ b/packages/tests/src/api/check-params/bulk.ts
@@ -0,0 +1,86 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createSingleServer,
7 makePostBodyRequest,
8 PeerTubeServer,
9 setAccessTokensToServers
10} from '@peertube/peertube-server-commands'
11
12describe('Test bulk API validators', function () {
13 let server: PeerTubeServer
14 let userAccessToken: string
15
16 // ---------------------------------------------------------------
17
18 before(async function () {
19 this.timeout(120000)
20
21 server = await createSingleServer(1)
22 await setAccessTokensToServers([ server ])
23
24 const user = { username: 'user1', password: 'password' }
25 await server.users.create({ username: user.username, password: user.password })
26
27 userAccessToken = await server.login.getAccessToken(user)
28 })
29
30 describe('When removing comments of', function () {
31 const path = '/api/v1/bulk/remove-comments-of'
32
33 it('Should fail with an unauthenticated user', async function () {
34 await makePostBodyRequest({
35 url: server.url,
36 path,
37 fields: { accountName: 'user1', scope: 'my-videos' },
38 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
39 })
40 })
41
42 it('Should fail with an unknown account', async function () {
43 await makePostBodyRequest({
44 url: server.url,
45 token: server.accessToken,
46 path,
47 fields: { accountName: 'user2', scope: 'my-videos' },
48 expectedStatus: HttpStatusCode.NOT_FOUND_404
49 })
50 })
51
52 it('Should fail with an invalid scope', async function () {
53 await makePostBodyRequest({
54 url: server.url,
55 token: server.accessToken,
56 path,
57 fields: { accountName: 'user1', scope: 'my-videoss' },
58 expectedStatus: HttpStatusCode.BAD_REQUEST_400
59 })
60 })
61
62 it('Should fail to delete comments of the instance without the appropriate rights', async function () {
63 await makePostBodyRequest({
64 url: server.url,
65 token: userAccessToken,
66 path,
67 fields: { accountName: 'user1', scope: 'instance' },
68 expectedStatus: HttpStatusCode.FORBIDDEN_403
69 })
70 })
71
72 it('Should succeed with the correct params', async function () {
73 await makePostBodyRequest({
74 url: server.url,
75 token: server.accessToken,
76 path,
77 fields: { accountName: 'user1', scope: 'instance' },
78 expectedStatus: HttpStatusCode.NO_CONTENT_204
79 })
80 })
81 })
82
83 after(async function () {
84 await cleanupTests([ server ])
85 })
86})
diff --git a/packages/tests/src/api/check-params/channel-import-videos.ts b/packages/tests/src/api/check-params/channel-import-videos.ts
new file mode 100644
index 000000000..0e897dad7
--- /dev/null
+++ b/packages/tests/src/api/check-params/channel-import-videos.ts
@@ -0,0 +1,209 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { FIXTURE_URLS } from '@tests/shared/tests.js'
4import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
5import { HttpStatusCode } from '@peertube/peertube-models'
6import {
7 ChannelsCommand,
8 cleanupTests,
9 createSingleServer,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel
13} from '@peertube/peertube-server-commands'
14
15describe('Test videos import in a channel API validator', function () {
16 let server: PeerTubeServer
17 const userInfo = {
18 accessToken: '',
19 channelName: 'fake_channel',
20 channelId: -1,
21 id: -1,
22 videoQuota: -1,
23 videoQuotaDaily: -1,
24 channelSyncId: -1
25 }
26 let command: ChannelsCommand
27
28 // ---------------------------------------------------------------
29
30 before(async function () {
31 this.timeout(120000)
32
33 server = await createSingleServer(1)
34
35 await setAccessTokensToServers([ server ])
36 await setDefaultVideoChannel([ server ])
37
38 await server.config.enableImports()
39 await server.config.enableChannelSync()
40
41 const userCreds = {
42 username: 'fake',
43 password: 'fake_password'
44 }
45
46 {
47 const user = await server.users.create({ username: userCreds.username, password: userCreds.password })
48 userInfo.id = user.id
49 userInfo.accessToken = await server.login.getAccessToken(userCreds)
50
51 const info = await server.users.getMyInfo({ token: userInfo.accessToken })
52 userInfo.channelId = info.videoChannels[0].id
53 }
54
55 {
56 const { videoChannelSync } = await server.channelSyncs.create({
57 token: userInfo.accessToken,
58 attributes: {
59 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
60 videoChannelId: userInfo.channelId
61 }
62 })
63 userInfo.channelSyncId = videoChannelSync.id
64 }
65
66 command = server.channels
67 })
68
69 it('Should fail when HTTP upload is disabled', async function () {
70 await server.config.disableChannelSync()
71 await server.config.disableImports()
72
73 await command.importVideos({
74 channelName: server.store.channel.name,
75 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
76 token: server.accessToken,
77 expectedStatus: HttpStatusCode.FORBIDDEN_403
78 })
79
80 await server.config.enableImports()
81 })
82
83 it('Should fail when externalChannelUrl is not provided', async function () {
84 await command.importVideos({
85 channelName: server.store.channel.name,
86 externalChannelUrl: null,
87 token: server.accessToken,
88 expectedStatus: HttpStatusCode.BAD_REQUEST_400
89 })
90 })
91
92 it('Should fail when externalChannelUrl is malformed', async function () {
93 await command.importVideos({
94 channelName: server.store.channel.name,
95 externalChannelUrl: 'not-a-url',
96 token: server.accessToken,
97 expectedStatus: HttpStatusCode.BAD_REQUEST_400
98 })
99 })
100
101 it('Should fail with a bad sync id', async function () {
102 await command.importVideos({
103 channelName: server.store.channel.name,
104 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
105 videoChannelSyncId: 'toto' as any,
106 token: server.accessToken,
107 expectedStatus: HttpStatusCode.BAD_REQUEST_400
108 })
109 })
110
111 it('Should fail with a unknown sync id', async function () {
112 await command.importVideos({
113 channelName: server.store.channel.name,
114 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
115 videoChannelSyncId: 42,
116 token: server.accessToken,
117 expectedStatus: HttpStatusCode.NOT_FOUND_404
118 })
119 })
120
121 it('Should fail with a sync id of another channel', async function () {
122 await command.importVideos({
123 channelName: server.store.channel.name,
124 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
125 videoChannelSyncId: userInfo.channelSyncId,
126 token: server.accessToken,
127 expectedStatus: HttpStatusCode.FORBIDDEN_403
128 })
129 })
130
131 it('Should fail with no authentication', async function () {
132 await command.importVideos({
133 channelName: server.store.channel.name,
134 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
135 token: null,
136 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
137 })
138 })
139
140 it('Should fail when sync is not owned by the user', async function () {
141 await command.importVideos({
142 channelName: server.store.channel.name,
143 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
144 token: userInfo.accessToken,
145 expectedStatus: HttpStatusCode.FORBIDDEN_403
146 })
147 })
148
149 it('Should fail when the user has no quota', async function () {
150 await server.users.update({
151 userId: userInfo.id,
152 videoQuota: 0
153 })
154
155 await command.importVideos({
156 channelName: 'fake_channel',
157 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
158 token: userInfo.accessToken,
159 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
160 })
161
162 await server.users.update({
163 userId: userInfo.id,
164 videoQuota: userInfo.videoQuota
165 })
166 })
167
168 it('Should fail when the user has no daily quota', async function () {
169 await server.users.update({
170 userId: userInfo.id,
171 videoQuotaDaily: 0
172 })
173
174 await command.importVideos({
175 channelName: 'fake_channel',
176 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
177 token: userInfo.accessToken,
178 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
179 })
180
181 await server.users.update({
182 userId: userInfo.id,
183 videoQuotaDaily: userInfo.videoQuotaDaily
184 })
185 })
186
187 it('Should succeed when sync is run by its owner', async function () {
188 if (!areHttpImportTestsDisabled()) return
189
190 await command.importVideos({
191 channelName: 'fake_channel',
192 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
193 token: userInfo.accessToken
194 })
195 })
196
197 it('Should succeed when sync is run with root and for another user\'s channel', async function () {
198 if (!areHttpImportTestsDisabled()) return
199
200 await command.importVideos({
201 channelName: 'fake_channel',
202 externalChannelUrl: FIXTURE_URLS.youtubeChannel
203 })
204 })
205
206 after(async function () {
207 await cleanupTests([ server ])
208 })
209})
diff --git a/packages/tests/src/api/check-params/config.ts b/packages/tests/src/api/check-params/config.ts
new file mode 100644
index 000000000..8179a8815
--- /dev/null
+++ b/packages/tests/src/api/check-params/config.ts
@@ -0,0 +1,428 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2import merge from 'lodash-es/merge.js'
3import { omit } from '@peertube/peertube-core-utils'
4import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeDeleteRequest,
9 makeGetRequest,
10 makePutBodyRequest,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15describe('Test config API validators', function () {
16 const path = '/api/v1/config/custom'
17 let server: PeerTubeServer
18 let userAccessToken: string
19 const updateParams: CustomConfig = {
20 instance: {
21 name: 'PeerTube updated',
22 shortDescription: 'my short description',
23 description: 'my super description',
24 terms: 'my super terms',
25 codeOfConduct: 'my super coc',
26
27 creationReason: 'my super reason',
28 moderationInformation: 'my super moderation information',
29 administrator: 'Kuja',
30 maintenanceLifetime: 'forever',
31 businessModel: 'my super business model',
32 hardwareInformation: '2vCore 3GB RAM',
33
34 languages: [ 'en', 'es' ],
35 categories: [ 1, 2 ],
36
37 isNSFW: true,
38 defaultNSFWPolicy: 'blur',
39
40 defaultClientRoute: '/videos/recently-added',
41
42 customizations: {
43 javascript: 'alert("coucou")',
44 css: 'body { background-color: red; }'
45 }
46 },
47 theme: {
48 default: 'default'
49 },
50 services: {
51 twitter: {
52 username: '@MySuperUsername',
53 whitelisted: true
54 }
55 },
56 client: {
57 videos: {
58 miniature: {
59 preferAuthorDisplayName: false
60 }
61 },
62 menu: {
63 login: {
64 redirectOnSingleExternalAuth: false
65 }
66 }
67 },
68 cache: {
69 previews: {
70 size: 2
71 },
72 captions: {
73 size: 3
74 },
75 torrents: {
76 size: 4
77 },
78 storyboards: {
79 size: 5
80 }
81 },
82 signup: {
83 enabled: false,
84 limit: 5,
85 requiresApproval: false,
86 requiresEmailVerification: false,
87 minimumAge: 16
88 },
89 admin: {
90 email: 'superadmin1@example.com'
91 },
92 contactForm: {
93 enabled: false
94 },
95 user: {
96 history: {
97 videos: {
98 enabled: true
99 }
100 },
101 videoQuota: 5242881,
102 videoQuotaDaily: 318742
103 },
104 videoChannels: {
105 maxPerUser: 20
106 },
107 transcoding: {
108 enabled: true,
109 remoteRunners: {
110 enabled: true
111 },
112 allowAdditionalExtensions: true,
113 allowAudioFiles: true,
114 concurrency: 1,
115 threads: 1,
116 profile: 'vod_profile',
117 resolutions: {
118 '0p': false,
119 '144p': false,
120 '240p': false,
121 '360p': true,
122 '480p': true,
123 '720p': false,
124 '1080p': false,
125 '1440p': false,
126 '2160p': false
127 },
128 alwaysTranscodeOriginalResolution: false,
129 webVideos: {
130 enabled: true
131 },
132 hls: {
133 enabled: false
134 }
135 },
136 live: {
137 enabled: true,
138
139 allowReplay: false,
140 latencySetting: {
141 enabled: false
142 },
143 maxDuration: 30,
144 maxInstanceLives: -1,
145 maxUserLives: 50,
146
147 transcoding: {
148 enabled: true,
149 remoteRunners: {
150 enabled: true
151 },
152 threads: 4,
153 profile: 'live_profile',
154 resolutions: {
155 '144p': true,
156 '240p': true,
157 '360p': true,
158 '480p': true,
159 '720p': true,
160 '1080p': true,
161 '1440p': true,
162 '2160p': true
163 },
164 alwaysTranscodeOriginalResolution: false
165 }
166 },
167 videoStudio: {
168 enabled: true,
169 remoteRunners: {
170 enabled: true
171 }
172 },
173 videoFile: {
174 update: {
175 enabled: true
176 }
177 },
178 import: {
179 videos: {
180 concurrency: 1,
181 http: {
182 enabled: false
183 },
184 torrent: {
185 enabled: false
186 }
187 },
188 videoChannelSynchronization: {
189 enabled: false,
190 maxPerUser: 10
191 }
192 },
193 trending: {
194 videos: {
195 algorithms: {
196 enabled: [ 'hot', 'most-viewed', 'most-liked' ],
197 default: 'most-viewed'
198 }
199 }
200 },
201 autoBlacklist: {
202 videos: {
203 ofUsers: {
204 enabled: false
205 }
206 }
207 },
208 followers: {
209 instance: {
210 enabled: false,
211 manualApproval: true
212 }
213 },
214 followings: {
215 instance: {
216 autoFollowBack: {
217 enabled: true
218 },
219 autoFollowIndex: {
220 enabled: true,
221 indexUrl: 'https://index.example.com'
222 }
223 }
224 },
225 broadcastMessage: {
226 enabled: true,
227 dismissable: true,
228 message: 'super message',
229 level: 'warning'
230 },
231 search: {
232 remoteUri: {
233 users: true,
234 anonymous: true
235 },
236 searchIndex: {
237 enabled: true,
238 url: 'https://search.joinpeertube.org',
239 disableLocalSearch: true,
240 isDefaultSearch: true
241 }
242 }
243 }
244
245 // ---------------------------------------------------------------
246
247 before(async function () {
248 this.timeout(30000)
249
250 server = await createSingleServer(1)
251
252 await setAccessTokensToServers([ server ])
253
254 const user = {
255 username: 'user1',
256 password: 'password'
257 }
258 await server.users.create({ username: user.username, password: user.password })
259 userAccessToken = await server.login.getAccessToken(user)
260 })
261
262 describe('When getting the configuration', function () {
263 it('Should fail without token', async function () {
264 await makeGetRequest({
265 url: server.url,
266 path,
267 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
268 })
269 })
270
271 it('Should fail if the user is not an administrator', async function () {
272 await makeGetRequest({
273 url: server.url,
274 path,
275 token: userAccessToken,
276 expectedStatus: HttpStatusCode.FORBIDDEN_403
277 })
278 })
279 })
280
281 describe('When updating the configuration', function () {
282 it('Should fail without token', async function () {
283 await makePutBodyRequest({
284 url: server.url,
285 path,
286 fields: updateParams,
287 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
288 })
289 })
290
291 it('Should fail if the user is not an administrator', async function () {
292 await makePutBodyRequest({
293 url: server.url,
294 path,
295 fields: updateParams,
296 token: userAccessToken,
297 expectedStatus: HttpStatusCode.FORBIDDEN_403
298 })
299 })
300
301 it('Should fail if it misses a key', async function () {
302 const newUpdateParams = { ...updateParams, admin: omit(updateParams.admin, [ 'email' ]) }
303
304 await makePutBodyRequest({
305 url: server.url,
306 path,
307 fields: newUpdateParams,
308 token: server.accessToken,
309 expectedStatus: HttpStatusCode.BAD_REQUEST_400
310 })
311 })
312
313 it('Should fail with a bad default NSFW policy', async function () {
314 const newUpdateParams = {
315 ...updateParams,
316
317 instance: {
318 defaultNSFWPolicy: 'hello'
319 }
320 }
321
322 await makePutBodyRequest({
323 url: server.url,
324 path,
325 fields: newUpdateParams,
326 token: server.accessToken,
327 expectedStatus: HttpStatusCode.BAD_REQUEST_400
328 })
329 })
330
331 it('Should fail if email disabled and signup requires email verification', async function () {
332 // opposite scenario - success when enable enabled - covered via tests/api/users/user-verification.ts
333 const newUpdateParams = {
334 ...updateParams,
335
336 signup: {
337 enabled: true,
338 limit: 5,
339 requiresApproval: true,
340 requiresEmailVerification: true
341 }
342 }
343
344 await makePutBodyRequest({
345 url: server.url,
346 path,
347 fields: newUpdateParams,
348 token: server.accessToken,
349 expectedStatus: HttpStatusCode.BAD_REQUEST_400
350 })
351 })
352
353 it('Should fail with a disabled web videos & hls transcoding', async function () {
354 const newUpdateParams = {
355 ...updateParams,
356
357 transcoding: {
358 hls: {
359 enabled: false
360 },
361 web_videos: {
362 enabled: false
363 }
364 }
365 }
366
367 await makePutBodyRequest({
368 url: server.url,
369 path,
370 fields: newUpdateParams,
371 token: server.accessToken,
372 expectedStatus: HttpStatusCode.BAD_REQUEST_400
373 })
374 })
375
376 it('Should fail with a disabled http upload & enabled sync', async function () {
377 const newUpdateParams: CustomConfig = merge({}, updateParams, {
378 import: {
379 videos: {
380 http: { enabled: false }
381 },
382 videoChannelSynchronization: { enabled: true }
383 }
384 })
385
386 await makePutBodyRequest({
387 url: server.url,
388 path,
389 fields: newUpdateParams,
390 token: server.accessToken,
391 expectedStatus: HttpStatusCode.BAD_REQUEST_400
392 })
393 })
394
395 it('Should succeed with the correct parameters', async function () {
396 await makePutBodyRequest({
397 url: server.url,
398 path,
399 fields: updateParams,
400 token: server.accessToken,
401 expectedStatus: HttpStatusCode.OK_200
402 })
403 })
404 })
405
406 describe('When deleting the configuration', function () {
407 it('Should fail without token', async function () {
408 await makeDeleteRequest({
409 url: server.url,
410 path,
411 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
412 })
413 })
414
415 it('Should fail if the user is not an administrator', async function () {
416 await makeDeleteRequest({
417 url: server.url,
418 path,
419 token: userAccessToken,
420 expectedStatus: HttpStatusCode.FORBIDDEN_403
421 })
422 })
423 })
424
425 after(async function () {
426 await cleanupTests([ server ])
427 })
428})
diff --git a/packages/tests/src/api/check-params/contact-form.ts b/packages/tests/src/api/check-params/contact-form.ts
new file mode 100644
index 000000000..009cb2ad9
--- /dev/null
+++ b/packages/tests/src/api/check-params/contact-form.ts
@@ -0,0 +1,86 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 ConfigCommand,
8 ContactFormCommand,
9 createSingleServer,
10 killallServers,
11 PeerTubeServer
12} from '@peertube/peertube-server-commands'
13
14describe('Test contact form API validators', function () {
15 let server: PeerTubeServer
16 const emails: object[] = []
17 const defaultBody = {
18 fromName: 'super name',
19 fromEmail: 'toto@example.com',
20 subject: 'my subject',
21 body: 'Hello, how are you?'
22 }
23 let emailPort: number
24 let command: ContactFormCommand
25
26 // ---------------------------------------------------------------
27
28 before(async function () {
29 this.timeout(60000)
30
31 emailPort = await MockSmtpServer.Instance.collectEmails(emails)
32
33 // Email is disabled
34 server = await createSingleServer(1)
35 command = server.contactForm
36 })
37
38 it('Should not accept a contact form if emails are disabled', async function () {
39 await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 })
40 })
41
42 it('Should not accept a contact form if it is disabled in the configuration', async function () {
43 this.timeout(25000)
44
45 await killallServers([ server ])
46
47 // Contact form is disabled
48 await server.run({ ...ConfigCommand.getEmailOverrideConfig(emailPort), contact_form: { enabled: false } })
49 await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 })
50 })
51
52 it('Should not accept a contact form if from email is invalid', async function () {
53 this.timeout(25000)
54
55 await killallServers([ server ])
56
57 // Email & contact form enabled
58 await server.run(ConfigCommand.getEmailOverrideConfig(emailPort))
59
60 await command.send({ ...defaultBody, fromEmail: 'badEmail', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
61 await command.send({ ...defaultBody, fromEmail: 'badEmail@', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
62 await command.send({ ...defaultBody, fromEmail: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
63 })
64
65 it('Should not accept a contact form if from name is invalid', async function () {
66 await command.send({ ...defaultBody, fromName: 'name'.repeat(100), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
67 await command.send({ ...defaultBody, fromName: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
68 await command.send({ ...defaultBody, fromName: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
69 })
70
71 it('Should not accept a contact form if body is invalid', async function () {
72 await command.send({ ...defaultBody, body: 'body'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
73 await command.send({ ...defaultBody, body: 'a', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
74 await command.send({ ...defaultBody, body: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
75 })
76
77 it('Should accept a contact form with the correct parameters', async function () {
78 await command.send(defaultBody)
79 })
80
81 after(async function () {
82 MockSmtpServer.Instance.kill()
83
84 await cleanupTests([ server ])
85 })
86})
diff --git a/packages/tests/src/api/check-params/custom-pages.ts b/packages/tests/src/api/check-params/custom-pages.ts
new file mode 100644
index 000000000..180a5e406
--- /dev/null
+++ b/packages/tests/src/api/check-params/custom-pages.ts
@@ -0,0 +1,79 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeGetRequest,
8 makePutBodyRequest,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12
13describe('Test custom pages validators', function () {
14 const path = '/api/v1/custom-pages/homepage/instance'
15
16 let server: PeerTubeServer
17 let userAccessToken: string
18
19 // ---------------------------------------------------------------
20
21 before(async function () {
22 this.timeout(120000)
23
24 server = await createSingleServer(1)
25 await setAccessTokensToServers([ server ])
26
27 const user = { username: 'user1', password: 'password' }
28 await server.users.create({ username: user.username, password: user.password })
29
30 userAccessToken = await server.login.getAccessToken(user)
31 })
32
33 describe('When updating instance homepage', function () {
34
35 it('Should fail with an unauthenticated user', async function () {
36 await makePutBodyRequest({
37 url: server.url,
38 path,
39 fields: { content: 'super content' },
40 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
41 })
42 })
43
44 it('Should fail with a non admin user', async function () {
45 await makePutBodyRequest({
46 url: server.url,
47 path,
48 token: userAccessToken,
49 fields: { content: 'super content' },
50 expectedStatus: HttpStatusCode.FORBIDDEN_403
51 })
52 })
53
54 it('Should succeed with the correct params', async function () {
55 await makePutBodyRequest({
56 url: server.url,
57 path,
58 token: server.accessToken,
59 fields: { content: 'super content' },
60 expectedStatus: HttpStatusCode.NO_CONTENT_204
61 })
62 })
63 })
64
65 describe('When getting instance homapage', function () {
66
67 it('Should succeed with the correct params', async function () {
68 await makeGetRequest({
69 url: server.url,
70 path,
71 expectedStatus: HttpStatusCode.OK_200
72 })
73 })
74 })
75
76 after(async function () {
77 await cleanupTests([ server ])
78 })
79})
diff --git a/packages/tests/src/api/check-params/debug.ts b/packages/tests/src/api/check-params/debug.ts
new file mode 100644
index 000000000..4a7c18a62
--- /dev/null
+++ b/packages/tests/src/api/check-params/debug.ts
@@ -0,0 +1,67 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeGetRequest,
8 PeerTubeServer,
9 setAccessTokensToServers
10} from '@peertube/peertube-server-commands'
11
12describe('Test debug API validators', function () {
13 const path = '/api/v1/server/debug'
14 let server: PeerTubeServer
15 let userAccessToken = ''
16
17 // ---------------------------------------------------------------
18
19 before(async function () {
20 this.timeout(120000)
21
22 server = await createSingleServer(1)
23
24 await setAccessTokensToServers([ server ])
25
26 const user = {
27 username: 'user1',
28 password: 'my super password'
29 }
30 await server.users.create({ username: user.username, password: user.password })
31 userAccessToken = await server.login.getAccessToken(user)
32 })
33
34 describe('When getting debug endpoint', function () {
35
36 it('Should fail with a non authenticated user', async function () {
37 await makeGetRequest({
38 url: server.url,
39 path,
40 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
41 })
42 })
43
44 it('Should fail with a non admin user', async function () {
45 await makeGetRequest({
46 url: server.url,
47 path,
48 token: userAccessToken,
49 expectedStatus: HttpStatusCode.FORBIDDEN_403
50 })
51 })
52
53 it('Should succeed with the correct params', async function () {
54 await makeGetRequest({
55 url: server.url,
56 path,
57 token: server.accessToken,
58 query: { startDate: new Date().toISOString() },
59 expectedStatus: HttpStatusCode.OK_200
60 })
61 })
62 })
63
64 after(async function () {
65 await cleanupTests([ server ])
66 })
67})
diff --git a/packages/tests/src/api/check-params/follows.ts b/packages/tests/src/api/check-params/follows.ts
new file mode 100644
index 000000000..e92a3acd6
--- /dev/null
+++ b/packages/tests/src/api/check-params/follows.ts
@@ -0,0 +1,369 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeDeleteRequest,
9 makeGetRequest,
10 makePostBodyRequest,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15describe('Test server follows API validators', function () {
16 let server: PeerTubeServer
17
18 // ---------------------------------------------------------------
19
20 before(async function () {
21 this.timeout(30000)
22
23 server = await createSingleServer(1)
24
25 await setAccessTokensToServers([ server ])
26 })
27
28 describe('When managing following', function () {
29 let userAccessToken = null
30
31 before(async function () {
32 userAccessToken = await server.users.generateUserAndToken('user1')
33 })
34
35 describe('When adding follows', function () {
36 const path = '/api/v1/server/following'
37
38 it('Should fail with nothing', async function () {
39 await makePostBodyRequest({
40 url: server.url,
41 path,
42 token: server.accessToken,
43 expectedStatus: HttpStatusCode.BAD_REQUEST_400
44 })
45 })
46
47 it('Should fail if hosts is not composed by hosts', async function () {
48 await makePostBodyRequest({
49 url: server.url,
50 path,
51 fields: { hosts: [ '127.0.0.1:9002', '127.0.0.1:coucou' ] },
52 token: server.accessToken,
53 expectedStatus: HttpStatusCode.BAD_REQUEST_400
54 })
55 })
56
57 it('Should fail if hosts is composed with http schemes', async function () {
58 await makePostBodyRequest({
59 url: server.url,
60 path,
61 fields: { hosts: [ '127.0.0.1:9002', 'http://127.0.0.1:9003' ] },
62 token: server.accessToken,
63 expectedStatus: HttpStatusCode.BAD_REQUEST_400
64 })
65 })
66
67 it('Should fail if hosts are not unique', async function () {
68 await makePostBodyRequest({
69 url: server.url,
70 path,
71 fields: { urls: [ '127.0.0.1:9002', '127.0.0.1:9002' ] },
72 token: server.accessToken,
73 expectedStatus: HttpStatusCode.BAD_REQUEST_400
74 })
75 })
76
77 it('Should fail if handles is not composed by handles', async function () {
78 await makePostBodyRequest({
79 url: server.url,
80 path,
81 fields: { handles: [ 'hello@example.com', '127.0.0.1:9001' ] },
82 token: server.accessToken,
83 expectedStatus: HttpStatusCode.BAD_REQUEST_400
84 })
85 })
86
87 it('Should fail if handles are not unique', async function () {
88 await makePostBodyRequest({
89 url: server.url,
90 path,
91 fields: { urls: [ 'hello@example.com', 'hello@example.com' ] },
92 token: server.accessToken,
93 expectedStatus: HttpStatusCode.BAD_REQUEST_400
94 })
95 })
96
97 it('Should fail with an invalid token', async function () {
98 await makePostBodyRequest({
99 url: server.url,
100 path,
101 fields: { hosts: [ '127.0.0.1:9002' ] },
102 token: 'fake_token',
103 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
104 })
105 })
106
107 it('Should fail if the user is not an administrator', async function () {
108 await makePostBodyRequest({
109 url: server.url,
110 path,
111 fields: { hosts: [ '127.0.0.1:9002' ] },
112 token: userAccessToken,
113 expectedStatus: HttpStatusCode.FORBIDDEN_403
114 })
115 })
116 })
117
118 describe('When listing followings', function () {
119 const path = '/api/v1/server/following'
120
121 it('Should fail with a bad start pagination', async function () {
122 await checkBadStartPagination(server.url, path)
123 })
124
125 it('Should fail with a bad count pagination', async function () {
126 await checkBadCountPagination(server.url, path)
127 })
128
129 it('Should fail with an incorrect sort', async function () {
130 await checkBadSortPagination(server.url, path)
131 })
132
133 it('Should fail with an incorrect state', async function () {
134 await makeGetRequest({
135 url: server.url,
136 path,
137 query: {
138 state: 'blabla'
139 }
140 })
141 })
142
143 it('Should fail with an incorrect actor type', async function () {
144 await makeGetRequest({
145 url: server.url,
146 path,
147 query: {
148 actorType: 'blabla'
149 }
150 })
151 })
152
153 it('Should fail succeed with the correct params', async function () {
154 await makeGetRequest({
155 url: server.url,
156 path,
157 expectedStatus: HttpStatusCode.OK_200,
158 query: {
159 state: 'accepted',
160 actorType: 'Application'
161 }
162 })
163 })
164 })
165
166 describe('When listing followers', function () {
167 const path = '/api/v1/server/followers'
168
169 it('Should fail with a bad start pagination', async function () {
170 await checkBadStartPagination(server.url, path)
171 })
172
173 it('Should fail with a bad count pagination', async function () {
174 await checkBadCountPagination(server.url, path)
175 })
176
177 it('Should fail with an incorrect sort', async function () {
178 await checkBadSortPagination(server.url, path)
179 })
180
181 it('Should fail with an incorrect actor type', async function () {
182 await makeGetRequest({
183 url: server.url,
184 path,
185 query: {
186 actorType: 'blabla'
187 }
188 })
189 })
190
191 it('Should fail with an incorrect state', async function () {
192 await makeGetRequest({
193 url: server.url,
194 path,
195 query: {
196 state: 'blabla',
197 actorType: 'Application'
198 }
199 })
200 })
201
202 it('Should fail succeed with the correct params', async function () {
203 await makeGetRequest({
204 url: server.url,
205 path,
206 expectedStatus: HttpStatusCode.OK_200,
207 query: {
208 state: 'accepted'
209 }
210 })
211 })
212 })
213
214 describe('When removing a follower', function () {
215 const path = '/api/v1/server/followers'
216
217 it('Should fail with an invalid token', async function () {
218 await makeDeleteRequest({
219 url: server.url,
220 path: path + '/toto@127.0.0.1:9002',
221 token: 'fake_token',
222 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
223 })
224 })
225
226 it('Should fail if the user is not an administrator', async function () {
227 await makeDeleteRequest({
228 url: server.url,
229 path: path + '/toto@127.0.0.1:9002',
230 token: userAccessToken,
231 expectedStatus: HttpStatusCode.FORBIDDEN_403
232 })
233 })
234
235 it('Should fail with an invalid follower', async function () {
236 await makeDeleteRequest({
237 url: server.url,
238 path: path + '/toto',
239 token: server.accessToken,
240 expectedStatus: HttpStatusCode.BAD_REQUEST_400
241 })
242 })
243
244 it('Should fail with an unknown follower', async function () {
245 await makeDeleteRequest({
246 url: server.url,
247 path: path + '/toto@127.0.0.1:9003',
248 token: server.accessToken,
249 expectedStatus: HttpStatusCode.NOT_FOUND_404
250 })
251 })
252 })
253
254 describe('When accepting a follower', function () {
255 const path = '/api/v1/server/followers'
256
257 it('Should fail with an invalid token', async function () {
258 await makePostBodyRequest({
259 url: server.url,
260 path: path + '/toto@127.0.0.1:9002/accept',
261 token: 'fake_token',
262 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
263 })
264 })
265
266 it('Should fail if the user is not an administrator', async function () {
267 await makePostBodyRequest({
268 url: server.url,
269 path: path + '/toto@127.0.0.1:9002/accept',
270 token: userAccessToken,
271 expectedStatus: HttpStatusCode.FORBIDDEN_403
272 })
273 })
274
275 it('Should fail with an invalid follower', async function () {
276 await makePostBodyRequest({
277 url: server.url,
278 path: path + '/toto/accept',
279 token: server.accessToken,
280 expectedStatus: HttpStatusCode.BAD_REQUEST_400
281 })
282 })
283
284 it('Should fail with an unknown follower', async function () {
285 await makePostBodyRequest({
286 url: server.url,
287 path: path + '/toto@127.0.0.1:9003/accept',
288 token: server.accessToken,
289 expectedStatus: HttpStatusCode.NOT_FOUND_404
290 })
291 })
292 })
293
294 describe('When rejecting a follower', function () {
295 const path = '/api/v1/server/followers'
296
297 it('Should fail with an invalid token', async function () {
298 await makePostBodyRequest({
299 url: server.url,
300 path: path + '/toto@127.0.0.1:9002/reject',
301 token: 'fake_token',
302 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
303 })
304 })
305
306 it('Should fail if the user is not an administrator', async function () {
307 await makePostBodyRequest({
308 url: server.url,
309 path: path + '/toto@127.0.0.1:9002/reject',
310 token: userAccessToken,
311 expectedStatus: HttpStatusCode.FORBIDDEN_403
312 })
313 })
314
315 it('Should fail with an invalid follower', async function () {
316 await makePostBodyRequest({
317 url: server.url,
318 path: path + '/toto/reject',
319 token: server.accessToken,
320 expectedStatus: HttpStatusCode.BAD_REQUEST_400
321 })
322 })
323
324 it('Should fail with an unknown follower', async function () {
325 await makePostBodyRequest({
326 url: server.url,
327 path: path + '/toto@127.0.0.1:9003/reject',
328 token: server.accessToken,
329 expectedStatus: HttpStatusCode.NOT_FOUND_404
330 })
331 })
332 })
333
334 describe('When removing following', function () {
335 const path = '/api/v1/server/following'
336
337 it('Should fail with an invalid token', async function () {
338 await makeDeleteRequest({
339 url: server.url,
340 path: path + '/127.0.0.1:9002',
341 token: 'fake_token',
342 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
343 })
344 })
345
346 it('Should fail if the user is not an administrator', async function () {
347 await makeDeleteRequest({
348 url: server.url,
349 path: path + '/127.0.0.1:9002',
350 token: userAccessToken,
351 expectedStatus: HttpStatusCode.FORBIDDEN_403
352 })
353 })
354
355 it('Should fail if we do not follow this server', async function () {
356 await makeDeleteRequest({
357 url: server.url,
358 path: path + '/example.com',
359 token: server.accessToken,
360 expectedStatus: HttpStatusCode.NOT_FOUND_404
361 })
362 })
363 })
364 })
365
366 after(async function () {
367 await cleanupTests([ server ])
368 })
369})
diff --git a/packages/tests/src/api/check-params/index.ts b/packages/tests/src/api/check-params/index.ts
new file mode 100644
index 000000000..ed5fe6b06
--- /dev/null
+++ b/packages/tests/src/api/check-params/index.ts
@@ -0,0 +1,45 @@
1import './abuses.js'
2import './accounts.js'
3import './blocklist.js'
4import './bulk.js'
5import './channel-import-videos.js'
6import './config.js'
7import './contact-form.js'
8import './custom-pages.js'
9import './debug.js'
10import './follows.js'
11import './jobs.js'
12import './live.js'
13import './logs.js'
14import './metrics.js'
15import './my-user.js'
16import './plugins.js'
17import './redundancy.js'
18import './registrations.js'
19import './runners.js'
20import './search.js'
21import './services.js'
22import './transcoding.js'
23import './two-factor.js'
24import './upload-quota.js'
25import './user-notifications.js'
26import './user-subscriptions.js'
27import './users-admin.js'
28import './users-emails.js'
29import './video-blacklist.js'
30import './video-captions.js'
31import './video-channel-syncs.js'
32import './video-channels.js'
33import './video-comments.js'
34import './video-files.js'
35import './video-imports.js'
36import './video-playlists.js'
37import './video-storyboards.js'
38import './video-source.js'
39import './video-studio.js'
40import './video-token.js'
41import './videos-common-filters.js'
42import './videos-history.js'
43import './videos-overviews.js'
44import './videos.js'
45import './views.js'
diff --git a/packages/tests/src/api/check-params/jobs.ts b/packages/tests/src/api/check-params/jobs.ts
new file mode 100644
index 000000000..331d58c6a
--- /dev/null
+++ b/packages/tests/src/api/check-params/jobs.ts
@@ -0,0 +1,125 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeGetRequest,
9 makePostBodyRequest,
10 PeerTubeServer,
11 setAccessTokensToServers
12} from '@peertube/peertube-server-commands'
13
14describe('Test jobs API validators', function () {
15 const path = '/api/v1/jobs/failed'
16 let server: PeerTubeServer
17 let userAccessToken = ''
18
19 // ---------------------------------------------------------------
20
21 before(async function () {
22 this.timeout(120000)
23
24 server = await createSingleServer(1)
25
26 await setAccessTokensToServers([ server ])
27
28 const user = {
29 username: 'user1',
30 password: 'my super password'
31 }
32 await server.users.create({ username: user.username, password: user.password })
33 userAccessToken = await server.login.getAccessToken(user)
34 })
35
36 describe('When listing jobs', function () {
37
38 it('Should fail with a bad state', async function () {
39 await makeGetRequest({
40 url: server.url,
41 token: server.accessToken,
42 path: path + 'ade'
43 })
44 })
45
46 it('Should fail with an incorrect job type', async function () {
47 await makeGetRequest({
48 url: server.url,
49 token: server.accessToken,
50 path,
51 query: {
52 jobType: 'toto'
53 }
54 })
55 })
56
57 it('Should fail with a bad start pagination', async function () {
58 await checkBadStartPagination(server.url, path, server.accessToken)
59 })
60
61 it('Should fail with a bad count pagination', async function () {
62 await checkBadCountPagination(server.url, path, server.accessToken)
63 })
64
65 it('Should fail with an incorrect sort', async function () {
66 await checkBadSortPagination(server.url, path, server.accessToken)
67 })
68
69 it('Should fail with a non authenticated user', async function () {
70 await makeGetRequest({
71 url: server.url,
72 path,
73 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
74 })
75 })
76
77 it('Should fail with a non admin user', async function () {
78 await makeGetRequest({
79 url: server.url,
80 path,
81 token: userAccessToken,
82 expectedStatus: HttpStatusCode.FORBIDDEN_403
83 })
84 })
85 })
86
87 describe('When pausing/resuming the job queue', async function () {
88 const commands = [ 'pause', 'resume' ]
89
90 it('Should fail with a non authenticated user', async function () {
91 for (const command of commands) {
92 await makePostBodyRequest({
93 url: server.url,
94 path: '/api/v1/jobs/' + command,
95 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
96 })
97 }
98 })
99
100 it('Should fail with a non admin user', async function () {
101 for (const command of commands) {
102 await makePostBodyRequest({
103 url: server.url,
104 path: '/api/v1/jobs/' + command,
105 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
106 })
107 }
108 })
109
110 it('Should succeed with the correct params', async function () {
111 for (const command of commands) {
112 await makePostBodyRequest({
113 url: server.url,
114 path: '/api/v1/jobs/' + command,
115 token: server.accessToken,
116 expectedStatus: HttpStatusCode.NO_CONTENT_204
117 })
118 }
119 })
120 })
121
122 after(async function () {
123 await cleanupTests([ server ])
124 })
125})
diff --git a/packages/tests/src/api/check-params/live.ts b/packages/tests/src/api/check-params/live.ts
new file mode 100644
index 000000000..5900823ea
--- /dev/null
+++ b/packages/tests/src/api/check-params/live.ts
@@ -0,0 +1,590 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { omit } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
6import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
7import {
8 cleanupTests,
9 createSingleServer,
10 LiveCommand,
11 makePostBodyRequest,
12 makeUploadRequest,
13 PeerTubeServer,
14 sendRTMPStream,
15 setAccessTokensToServers,
16 stopFfmpeg
17} from '@peertube/peertube-server-commands'
18
19describe('Test video lives API validator', function () {
20 const path = '/api/v1/videos/live'
21 let server: PeerTubeServer
22 let userAccessToken = ''
23 let channelId: number
24 let video: VideoCreateResult
25 let videoIdNotLive: number
26 let command: LiveCommand
27
28 // ---------------------------------------------------------------
29
30 before(async function () {
31 this.timeout(30000)
32
33 server = await createSingleServer(1)
34
35 await setAccessTokensToServers([ server ])
36
37 await server.config.updateCustomSubConfig({
38 newConfig: {
39 live: {
40 enabled: true,
41 latencySetting: {
42 enabled: false
43 },
44 maxInstanceLives: 20,
45 maxUserLives: 20,
46 allowReplay: true
47 }
48 }
49 })
50
51 const username = 'user1'
52 const password = 'my super password'
53 await server.users.create({ username, password })
54 userAccessToken = await server.login.getAccessToken({ username, password })
55
56 {
57 const { videoChannels } = await server.users.getMyInfo()
58 channelId = videoChannels[0].id
59 }
60
61 {
62 videoIdNotLive = (await server.videos.quickUpload({ name: 'not live' })).id
63 }
64
65 command = server.live
66 })
67
68 describe('When creating a live', function () {
69 let baseCorrectParams
70
71 before(function () {
72 baseCorrectParams = {
73 name: 'my super name',
74 category: 5,
75 licence: 1,
76 language: 'pt',
77 nsfw: false,
78 commentsEnabled: true,
79 downloadEnabled: true,
80 waitTranscoding: true,
81 description: 'my super description',
82 support: 'my super support text',
83 tags: [ 'tag1', 'tag2' ],
84 privacy: VideoPrivacy.PUBLIC,
85 channelId,
86 saveReplay: false,
87 replaySettings: undefined,
88 permanentLive: false,
89 latencyMode: LiveVideoLatencyMode.DEFAULT
90 }
91 })
92
93 it('Should fail with nothing', async function () {
94 const fields = {}
95 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
96 })
97
98 it('Should fail with a long name', async function () {
99 const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
100
101 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
102 })
103
104 it('Should fail with a bad category', async function () {
105 const fields = { ...baseCorrectParams, category: 125 }
106
107 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
108 })
109
110 it('Should fail with a bad licence', async function () {
111 const fields = { ...baseCorrectParams, licence: 125 }
112
113 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
114 })
115
116 it('Should fail with a bad language', async function () {
117 const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
118
119 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
120 })
121
122 it('Should fail with a long description', async function () {
123 const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
124
125 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
126 })
127
128 it('Should fail with a long support text', async function () {
129 const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
130
131 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
132 })
133
134 it('Should fail without a channel', async function () {
135 const fields = omit(baseCorrectParams, [ 'channelId' ])
136
137 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
138 })
139
140 it('Should fail with a bad channel', async function () {
141 const fields = { ...baseCorrectParams, channelId: 545454 }
142
143 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
144 })
145
146 it('Should fail with a bad privacy for replay settings', async function () {
147 const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: 999 } }
148
149 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
150 })
151
152 it('Should fail with another user channel', async function () {
153 const user = {
154 username: 'fake',
155 password: 'fake_password'
156 }
157 await server.users.create({ username: user.username, password: user.password })
158
159 const accessTokenUser = await server.login.getAccessToken(user)
160 const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
161 const customChannelId = videoChannels[0].id
162
163 const fields = { ...baseCorrectParams, channelId: customChannelId }
164
165 await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
166 })
167
168 it('Should fail with too many tags', async function () {
169 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
170
171 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
172 })
173
174 it('Should fail with a tag length too low', async function () {
175 const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
176
177 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
178 })
179
180 it('Should fail with a tag length too big', async function () {
181 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
182
183 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
184 })
185
186 it('Should fail with an incorrect thumbnail file', async function () {
187 const fields = baseCorrectParams
188 const attaches = {
189 thumbnailfile: buildAbsoluteFixturePath('video_short.mp4')
190 }
191
192 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
193 })
194
195 it('Should fail with a big thumbnail file', async function () {
196 const fields = baseCorrectParams
197 const attaches = {
198 thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png')
199 }
200
201 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
202 })
203
204 it('Should fail with an incorrect preview file', async function () {
205 const fields = baseCorrectParams
206 const attaches = {
207 previewfile: buildAbsoluteFixturePath('video_short.mp4')
208 }
209
210 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
211 })
212
213 it('Should fail with a big preview file', async function () {
214 const fields = baseCorrectParams
215 const attaches = {
216 previewfile: buildAbsoluteFixturePath('custom-preview-big.png')
217 }
218
219 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
220 })
221
222 it('Should fail with bad latency setting', async function () {
223 const fields = { ...baseCorrectParams, latencyMode: 42 }
224
225 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
226 })
227
228 it('Should fail to set latency if the server does not allow it', async function () {
229 const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
230
231 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
232 })
233
234 it('Should succeed with the correct parameters', async function () {
235 this.timeout(30000)
236
237 const res = await makePostBodyRequest({
238 url: server.url,
239 path,
240 token: server.accessToken,
241 fields: baseCorrectParams,
242 expectedStatus: HttpStatusCode.OK_200
243 })
244
245 video = res.body.video
246 })
247
248 it('Should forbid if live is disabled', async function () {
249 await server.config.updateCustomSubConfig({
250 newConfig: {
251 live: {
252 enabled: false
253 }
254 }
255 })
256
257 await makePostBodyRequest({
258 url: server.url,
259 path,
260 token: server.accessToken,
261 fields: baseCorrectParams,
262 expectedStatus: HttpStatusCode.FORBIDDEN_403
263 })
264 })
265
266 it('Should forbid to save replay if not enabled by the admin', async function () {
267 const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
268
269 await server.config.updateCustomSubConfig({
270 newConfig: {
271 live: {
272 enabled: true,
273 allowReplay: false
274 }
275 }
276 })
277
278 await makePostBodyRequest({
279 url: server.url,
280 path,
281 token: server.accessToken,
282 fields,
283 expectedStatus: HttpStatusCode.FORBIDDEN_403
284 })
285 })
286
287 it('Should allow to save replay if enabled by the admin', async function () {
288 const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } }
289
290 await server.config.updateCustomSubConfig({
291 newConfig: {
292 live: {
293 enabled: true,
294 allowReplay: true
295 }
296 }
297 })
298
299 await makePostBodyRequest({
300 url: server.url,
301 path,
302 token: server.accessToken,
303 fields,
304 expectedStatus: HttpStatusCode.OK_200
305 })
306 })
307
308 it('Should not allow live if max instance lives is reached', async function () {
309 await server.config.updateCustomSubConfig({
310 newConfig: {
311 live: {
312 enabled: true,
313 maxInstanceLives: 1
314 }
315 }
316 })
317
318 await makePostBodyRequest({
319 url: server.url,
320 path,
321 token: server.accessToken,
322 fields: baseCorrectParams,
323 expectedStatus: HttpStatusCode.FORBIDDEN_403
324 })
325 })
326
327 it('Should not allow live if max user lives is reached', async function () {
328 await server.config.updateCustomSubConfig({
329 newConfig: {
330 live: {
331 enabled: true,
332 maxInstanceLives: 20,
333 maxUserLives: 1
334 }
335 }
336 })
337
338 await makePostBodyRequest({
339 url: server.url,
340 path,
341 token: server.accessToken,
342 fields: baseCorrectParams,
343 expectedStatus: HttpStatusCode.FORBIDDEN_403
344 })
345 })
346 })
347
348 describe('When getting live information', function () {
349
350 it('Should fail with a bad access token', async function () {
351 await command.get({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
352 })
353
354 it('Should not display private information without access token', async function () {
355 const live = await command.get({ token: '', videoId: video.id })
356
357 expect(live.rtmpUrl).to.not.exist
358 expect(live.streamKey).to.not.exist
359 expect(live.latencyMode).to.exist
360 })
361
362 it('Should not display private information with token of another user', async function () {
363 const live = await command.get({ token: userAccessToken, videoId: video.id })
364
365 expect(live.rtmpUrl).to.not.exist
366 expect(live.streamKey).to.not.exist
367 expect(live.latencyMode).to.exist
368 })
369
370 it('Should display private information with appropriate token', async function () {
371 const live = await command.get({ videoId: video.id })
372
373 expect(live.rtmpUrl).to.exist
374 expect(live.streamKey).to.exist
375 expect(live.latencyMode).to.exist
376 })
377
378 it('Should fail with a bad video id', async function () {
379 await command.get({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
380 })
381
382 it('Should fail with an unknown video id', async function () {
383 await command.get({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
384 })
385
386 it('Should fail with a non live video', async function () {
387 await command.get({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
388 })
389
390 it('Should succeed with the correct params', async function () {
391 await command.get({ videoId: video.id })
392 await command.get({ videoId: video.uuid })
393 await command.get({ videoId: video.shortUUID })
394 })
395 })
396
397 describe('When getting live sessions', function () {
398
399 it('Should fail with a bad access token', async function () {
400 await command.listSessions({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
401 })
402
403 it('Should fail without token', async function () {
404 await command.listSessions({ token: null, videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
405 })
406
407 it('Should fail with the token of another user', async function () {
408 await command.listSessions({ token: userAccessToken, videoId: video.id, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
409 })
410
411 it('Should fail with a bad video id', async function () {
412 await command.listSessions({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
413 })
414
415 it('Should fail with an unknown video id', async function () {
416 await command.listSessions({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
417 })
418
419 it('Should fail with a non live video', async function () {
420 await command.listSessions({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
421 })
422
423 it('Should succeed with the correct params', async function () {
424 await command.listSessions({ videoId: video.id })
425 })
426 })
427
428 describe('When getting live session of a replay', function () {
429
430 it('Should fail with a bad video id', async function () {
431 await command.getReplaySession({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
432 })
433
434 it('Should fail with an unknown video id', async function () {
435 await command.getReplaySession({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
436 })
437
438 it('Should fail with a non replay video', async function () {
439 await command.getReplaySession({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
440 })
441 })
442
443 describe('When updating live information', async function () {
444
445 it('Should fail without access token', async function () {
446 await command.update({ token: '', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
447 })
448
449 it('Should fail with a bad access token', async function () {
450 await command.update({ token: 'toto', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
451 })
452
453 it('Should fail with access token of another user', async function () {
454 await command.update({ token: userAccessToken, videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
455 })
456
457 it('Should fail with a bad video id', async function () {
458 await command.update({ videoId: 'toto', fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
459 })
460
461 it('Should fail with an unknown video id', async function () {
462 await command.update({ videoId: 454555, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
463 })
464
465 it('Should fail with a non live video', async function () {
466 await command.update({ videoId: videoIdNotLive, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
467 })
468
469 it('Should fail with bad latency setting', async function () {
470 const fields = { latencyMode: 42 as any }
471
472 await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
473 })
474
475 it('Should fail with a bad privacy for replay settings', async function () {
476 const fields = { saveReplay: true, replaySettings: { privacy: 999 as any } }
477
478 await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
479 })
480
481 it('Should fail with save replay enabled but without replay settings', async function () {
482 await server.config.updateCustomSubConfig({
483 newConfig: {
484 live: {
485 enabled: true,
486 allowReplay: true
487 }
488 }
489 })
490
491 const fields = { saveReplay: true }
492
493 await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
494 })
495
496 it('Should fail with save replay disabled and replay settings', async function () {
497 const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } }
498
499 await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
500 })
501
502 it('Should fail with only replay settings when save replay is disabled', async function () {
503 const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } }
504
505 await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
506 })
507
508 it('Should fail to set latency if the server does not allow it', async function () {
509 const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY }
510
511 await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
512 })
513
514 it('Should succeed with the correct params', async function () {
515 await command.update({ videoId: video.id, fields: { saveReplay: false } })
516 await command.update({ videoId: video.uuid, fields: { saveReplay: false } })
517 await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } })
518
519 await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
520
521 })
522
523 it('Should fail to update replay status if replay is not allowed on the instance', async function () {
524 await server.config.updateCustomSubConfig({
525 newConfig: {
526 live: {
527 enabled: true,
528 allowReplay: false
529 }
530 }
531 })
532
533 await command.update({ videoId: video.id, fields: { saveReplay: true }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
534 })
535
536 it('Should fail to update a live if it has already started', async function () {
537 this.timeout(40000)
538
539 const live = await command.get({ videoId: video.id })
540
541 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
542
543 await command.waitUntilPublished({ videoId: video.id })
544 await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
545
546 await stopFfmpeg(ffmpegCommand)
547 })
548
549 it('Should fail to change live privacy if it has already started', async function () {
550 this.timeout(40000)
551
552 const live = await command.get({ videoId: video.id })
553
554 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
555
556 await command.waitUntilPublished({ videoId: video.id })
557
558 await server.videos.update({
559 id: video.id,
560 attributes: { privacy: VideoPrivacy.PUBLIC } // Same privacy, it's fine
561 })
562
563 await server.videos.update({
564 id: video.id,
565 attributes: { privacy: VideoPrivacy.UNLISTED },
566 expectedStatus: HttpStatusCode.BAD_REQUEST_400
567 })
568
569 await stopFfmpeg(ffmpegCommand)
570 })
571
572 it('Should fail to stream twice in the save live', async function () {
573 this.timeout(40000)
574
575 const live = await command.get({ videoId: video.id })
576
577 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
578
579 await command.waitUntilPublished({ videoId: video.id })
580
581 await command.runAndTestStreamError({ videoId: video.id, shouldHaveError: true })
582
583 await stopFfmpeg(ffmpegCommand)
584 })
585 })
586
587 after(async function () {
588 await cleanupTests([ server ])
589 })
590})
diff --git a/packages/tests/src/api/check-params/logs.ts b/packages/tests/src/api/check-params/logs.ts
new file mode 100644
index 000000000..629530e30
--- /dev/null
+++ b/packages/tests/src/api/check-params/logs.ts
@@ -0,0 +1,163 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeGetRequest,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12
13describe('Test logs API validators', function () {
14 const path = '/api/v1/server/logs'
15 let server: PeerTubeServer
16 let userAccessToken = ''
17
18 // ---------------------------------------------------------------
19
20 before(async function () {
21 this.timeout(120000)
22
23 server = await createSingleServer(1)
24
25 await setAccessTokensToServers([ server ])
26
27 const user = {
28 username: 'user1',
29 password: 'my super password'
30 }
31 await server.users.create({ username: user.username, password: user.password })
32 userAccessToken = await server.login.getAccessToken(user)
33 })
34
35 describe('When getting logs', function () {
36
37 it('Should fail with a non authenticated user', async function () {
38 await makeGetRequest({
39 url: server.url,
40 path,
41 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
42 })
43 })
44
45 it('Should fail with a non admin user', async function () {
46 await makeGetRequest({
47 url: server.url,
48 path,
49 token: userAccessToken,
50 expectedStatus: HttpStatusCode.FORBIDDEN_403
51 })
52 })
53
54 it('Should fail with a missing startDate query', async function () {
55 await makeGetRequest({
56 url: server.url,
57 path,
58 token: server.accessToken,
59 expectedStatus: HttpStatusCode.BAD_REQUEST_400
60 })
61 })
62
63 it('Should fail with a bad startDate query', async function () {
64 await makeGetRequest({
65 url: server.url,
66 path,
67 token: server.accessToken,
68 query: { startDate: 'toto' },
69 expectedStatus: HttpStatusCode.BAD_REQUEST_400
70 })
71 })
72
73 it('Should fail with a bad endDate query', async function () {
74 await makeGetRequest({
75 url: server.url,
76 path,
77 token: server.accessToken,
78 query: { startDate: new Date().toISOString(), endDate: 'toto' },
79 expectedStatus: HttpStatusCode.BAD_REQUEST_400
80 })
81 })
82
83 it('Should fail with a bad level parameter', async function () {
84 await makeGetRequest({
85 url: server.url,
86 path,
87 token: server.accessToken,
88 query: { startDate: new Date().toISOString(), level: 'toto' },
89 expectedStatus: HttpStatusCode.BAD_REQUEST_400
90 })
91 })
92
93 it('Should succeed with the correct params', async function () {
94 await makeGetRequest({
95 url: server.url,
96 path,
97 token: server.accessToken,
98 query: { startDate: new Date().toISOString() },
99 expectedStatus: HttpStatusCode.OK_200
100 })
101 })
102 })
103
104 describe('When creating client logs', function () {
105 const base = {
106 level: 'warn' as 'warn',
107 message: 'my super message',
108 url: 'https://example.com/toto'
109 }
110 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
111
112 it('Should fail with an invalid level', async function () {
113 await server.logs.createLogClient({ payload: { ...base, level: '' as any }, expectedStatus })
114 await server.logs.createLogClient({ payload: { ...base, level: undefined }, expectedStatus })
115 await server.logs.createLogClient({ payload: { ...base, level: 'toto' as any }, expectedStatus })
116 })
117
118 it('Should fail with an invalid message', async function () {
119 await server.logs.createLogClient({ payload: { ...base, message: undefined }, expectedStatus })
120 await server.logs.createLogClient({ payload: { ...base, message: '' }, expectedStatus })
121 await server.logs.createLogClient({ payload: { ...base, message: 'm'.repeat(2500) }, expectedStatus })
122 })
123
124 it('Should fail with an invalid url', async function () {
125 await server.logs.createLogClient({ payload: { ...base, url: undefined }, expectedStatus })
126 await server.logs.createLogClient({ payload: { ...base, url: 'toto' }, expectedStatus })
127 })
128
129 it('Should fail with an invalid stackTrace', async function () {
130 await server.logs.createLogClient({ payload: { ...base, stackTrace: 's'.repeat(20000) }, expectedStatus })
131 })
132
133 it('Should fail with an invalid userAgent', async function () {
134 await server.logs.createLogClient({ payload: { ...base, userAgent: 's'.repeat(500) }, expectedStatus })
135 })
136
137 it('Should fail with an invalid meta', async function () {
138 await server.logs.createLogClient({ payload: { ...base, meta: 's'.repeat(10000) }, expectedStatus })
139 })
140
141 it('Should succeed with the correct params', async function () {
142 await server.logs.createLogClient({ payload: { ...base, stackTrace: 'stackTrace', meta: '{toto}', userAgent: 'userAgent' } })
143 })
144
145 it('Should rate limit log creation', async function () {
146 let fail = false
147
148 for (let i = 0; i < 10; i++) {
149 try {
150 await server.logs.createLogClient({ token: null, payload: base })
151 } catch {
152 fail = true
153 }
154 }
155
156 expect(fail).to.be.true
157 })
158 })
159
160 after(async function () {
161 await cleanupTests([ server ])
162 })
163})
diff --git a/packages/tests/src/api/check-params/metrics.ts b/packages/tests/src/api/check-params/metrics.ts
new file mode 100644
index 000000000..cda854554
--- /dev/null
+++ b/packages/tests/src/api/check-params/metrics.ts
@@ -0,0 +1,214 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { omit } from '@peertube/peertube-core-utils'
4import { HttpStatusCode, PlaybackMetricCreate, VideoResolution } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makePostBodyRequest,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12
13describe('Test metrics API validators', function () {
14 let server: PeerTubeServer
15 let videoUUID: string
16
17 // ---------------------------------------------------------------
18
19 before(async function () {
20 this.timeout(120000)
21
22 server = await createSingleServer(1, {
23 open_telemetry: {
24 metrics: {
25 enabled: true
26 }
27 }
28 })
29
30 await setAccessTokensToServers([ server ])
31
32 const { uuid } = await server.videos.quickUpload({ name: 'video' })
33 videoUUID = uuid
34 })
35
36 describe('When adding playback metrics', function () {
37 const path = '/api/v1/metrics/playback'
38 let baseParams: PlaybackMetricCreate
39
40 before(function () {
41 baseParams = {
42 playerMode: 'p2p-media-loader',
43 resolution: VideoResolution.H_1080P,
44 fps: 30,
45 resolutionChanges: 1,
46 errors: 2,
47 p2pEnabled: true,
48 downloadedBytesP2P: 0,
49 downloadedBytesHTTP: 0,
50 uploadedBytesP2P: 0,
51 videoId: videoUUID
52 }
53 })
54
55 it('Should fail with an invalid resolution', async function () {
56 await makePostBodyRequest({
57 url: server.url,
58 path,
59 fields: { ...baseParams, resolution: 'toto' }
60 })
61 })
62
63 it('Should fail with an invalid fps', async function () {
64 await makePostBodyRequest({
65 url: server.url,
66 path,
67 fields: { ...baseParams, fps: 'toto' }
68 })
69 })
70
71 it('Should fail with a missing/invalid player mode', async function () {
72 await makePostBodyRequest({
73 url: server.url,
74 path,
75 fields: omit(baseParams, [ 'playerMode' ])
76 })
77
78 await makePostBodyRequest({
79 url: server.url,
80 path,
81 fields: { ...baseParams, playerMode: 'toto' }
82 })
83 })
84
85 it('Should fail with an missing/invalid resolution changes', async function () {
86 await makePostBodyRequest({
87 url: server.url,
88 path,
89 fields: omit(baseParams, [ 'resolutionChanges' ])
90 })
91
92 await makePostBodyRequest({
93 url: server.url,
94 path,
95 fields: { ...baseParams, resolutionChanges: 'toto' }
96 })
97 })
98
99 it('Should fail with an missing/invalid errors', async function () {
100 await makePostBodyRequest({
101 url: server.url,
102 path,
103 fields: omit(baseParams, [ 'errors' ])
104 })
105
106 await makePostBodyRequest({
107 url: server.url,
108 path,
109 fields: { ...baseParams, errors: 'toto' }
110 })
111 })
112
113 it('Should fail with an missing/invalid downloadedBytesP2P', async function () {
114 await makePostBodyRequest({
115 url: server.url,
116 path,
117 fields: omit(baseParams, [ 'downloadedBytesP2P' ])
118 })
119
120 await makePostBodyRequest({
121 url: server.url,
122 path,
123 fields: { ...baseParams, downloadedBytesP2P: 'toto' }
124 })
125 })
126
127 it('Should fail with an missing/invalid downloadedBytesHTTP', async function () {
128 await makePostBodyRequest({
129 url: server.url,
130 path,
131 fields: omit(baseParams, [ 'downloadedBytesHTTP' ])
132 })
133
134 await makePostBodyRequest({
135 url: server.url,
136 path,
137 fields: { ...baseParams, downloadedBytesHTTP: 'toto' }
138 })
139 })
140
141 it('Should fail with an missing/invalid uploadedBytesP2P', async function () {
142 await makePostBodyRequest({
143 url: server.url,
144 path,
145 fields: omit(baseParams, [ 'uploadedBytesP2P' ])
146 })
147
148 await makePostBodyRequest({
149 url: server.url,
150 path,
151 fields: { ...baseParams, uploadedBytesP2P: 'toto' }
152 })
153 })
154
155 it('Should fail with a missing/invalid p2pEnabled', async function () {
156 await makePostBodyRequest({
157 url: server.url,
158 path,
159 fields: omit(baseParams, [ 'p2pEnabled' ])
160 })
161
162 await makePostBodyRequest({
163 url: server.url,
164 path,
165 fields: { ...baseParams, p2pEnabled: 'toto' }
166 })
167 })
168
169 it('Should fail with an invalid totalPeers', async function () {
170 await makePostBodyRequest({
171 url: server.url,
172 path,
173 fields: { ...baseParams, p2pPeers: 'toto' }
174 })
175 })
176
177 it('Should fail with a bad video id', async function () {
178 await makePostBodyRequest({
179 url: server.url,
180 path,
181 fields: { ...baseParams, videoId: 'toto' }
182 })
183 })
184
185 it('Should fail with an unknown video', async function () {
186 await makePostBodyRequest({
187 url: server.url,
188 path,
189 fields: { ...baseParams, videoId: 42 },
190 expectedStatus: HttpStatusCode.NOT_FOUND_404
191 })
192 })
193
194 it('Should succeed with the correct params', async function () {
195 await makePostBodyRequest({
196 url: server.url,
197 path,
198 fields: baseParams,
199 expectedStatus: HttpStatusCode.NO_CONTENT_204
200 })
201
202 await makePostBodyRequest({
203 url: server.url,
204 path,
205 fields: { ...baseParams, p2pEnabled: false, totalPeers: 32 },
206 expectedStatus: HttpStatusCode.NO_CONTENT_204
207 })
208 })
209 })
210
211 after(async function () {
212 await cleanupTests([ server ])
213 })
214})
diff --git a/packages/tests/src/api/check-params/my-user.ts b/packages/tests/src/api/check-params/my-user.ts
new file mode 100644
index 000000000..2ef2e242a
--- /dev/null
+++ b/packages/tests/src/api/check-params/my-user.ts
@@ -0,0 +1,492 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
6import { HttpStatusCode, UserRole, VideoCreateResult } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 createSingleServer,
10 makeGetRequest,
11 makePutBodyRequest,
12 makeUploadRequest,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 UsersCommand
16} from '@peertube/peertube-server-commands'
17
18describe('Test my user API validators', function () {
19 const path = '/api/v1/users/'
20 let userId: number
21 let rootId: number
22 let moderatorId: number
23 let video: VideoCreateResult
24 let server: PeerTubeServer
25 let userToken = ''
26 let moderatorToken = ''
27
28 // ---------------------------------------------------------------
29
30 before(async function () {
31 this.timeout(30000)
32
33 {
34 server = await createSingleServer(1)
35 await setAccessTokensToServers([ server ])
36 }
37
38 {
39 const result = await server.users.generate('user1')
40 userToken = result.token
41 userId = result.userId
42 }
43
44 {
45 const result = await server.users.generate('moderator1', UserRole.MODERATOR)
46 moderatorToken = result.token
47 }
48
49 {
50 const result = await server.users.generate('moderator2', UserRole.MODERATOR)
51 moderatorId = result.userId
52 }
53
54 {
55 video = await server.videos.upload()
56 }
57 })
58
59 describe('When updating my account', function () {
60
61 it('Should fail with an invalid email attribute', async function () {
62 const fields = {
63 email: 'blabla'
64 }
65
66 await makePutBodyRequest({ url: server.url, path: path + 'me', token: server.accessToken, fields })
67 })
68
69 it('Should fail with a too small password', async function () {
70 const fields = {
71 currentPassword: 'password',
72 password: 'bla'
73 }
74
75 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
76 })
77
78 it('Should fail with a too long password', async function () {
79 const fields = {
80 currentPassword: 'password',
81 password: 'super'.repeat(61)
82 }
83
84 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
85 })
86
87 it('Should fail without the current password', async function () {
88 const fields = {
89 currentPassword: 'password',
90 password: 'super'.repeat(61)
91 }
92
93 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
94 })
95
96 it('Should fail with an invalid current password', async function () {
97 const fields = {
98 currentPassword: 'my super password fail',
99 password: 'super'.repeat(61)
100 }
101
102 await makePutBodyRequest({
103 url: server.url,
104 path: path + 'me',
105 token: userToken,
106 fields,
107 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
108 })
109 })
110
111 it('Should fail with an invalid NSFW policy attribute', async function () {
112 const fields = {
113 nsfwPolicy: 'hello'
114 }
115
116 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
117 })
118
119 it('Should fail with an invalid autoPlayVideo attribute', async function () {
120 const fields = {
121 autoPlayVideo: -1
122 }
123
124 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
125 })
126
127 it('Should fail with an invalid autoPlayNextVideo attribute', async function () {
128 const fields = {
129 autoPlayNextVideo: -1
130 }
131
132 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
133 })
134
135 it('Should fail with an invalid videosHistoryEnabled attribute', async function () {
136 const fields = {
137 videosHistoryEnabled: -1
138 }
139
140 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
141 })
142
143 it('Should fail with an non authenticated user', async function () {
144 const fields = {
145 currentPassword: 'password',
146 password: 'my super password'
147 }
148
149 await makePutBodyRequest({
150 url: server.url,
151 path: path + 'me',
152 token: 'super token',
153 fields,
154 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
155 })
156 })
157
158 it('Should fail with a too long description', async function () {
159 const fields = {
160 description: 'super'.repeat(201)
161 }
162
163 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
164 })
165
166 it('Should fail with an invalid videoLanguages attribute', async function () {
167 {
168 const fields = {
169 videoLanguages: 'toto'
170 }
171
172 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
173 }
174
175 {
176 const languages = []
177 for (let i = 0; i < 1000; i++) {
178 languages.push('fr')
179 }
180
181 const fields = {
182 videoLanguages: languages
183 }
184
185 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
186 }
187 })
188
189 it('Should fail with an invalid theme', async function () {
190 const fields = { theme: 'invalid' }
191 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
192 })
193
194 it('Should fail with an unknown theme', async function () {
195 const fields = { theme: 'peertube-theme-unknown' }
196 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
197 })
198
199 it('Should fail with invalid no modal attributes', async function () {
200 const keys = [
201 'noInstanceConfigWarningModal',
202 'noAccountSetupWarningModal',
203 'noWelcomeModal'
204 ]
205
206 for (const key of keys) {
207 const fields = {
208 [key]: -1
209 }
210
211 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
212 }
213 })
214
215 it('Should succeed to change password with the correct params', async function () {
216 const fields = {
217 currentPassword: 'password',
218 password: 'my super password',
219 nsfwPolicy: 'blur',
220 autoPlayVideo: false,
221 email: 'super_email@example.com',
222 theme: 'default',
223 noInstanceConfigWarningModal: true,
224 noWelcomeModal: true,
225 noAccountSetupWarningModal: true
226 }
227
228 await makePutBodyRequest({
229 url: server.url,
230 path: path + 'me',
231 token: userToken,
232 fields,
233 expectedStatus: HttpStatusCode.NO_CONTENT_204
234 })
235 })
236
237 it('Should succeed without password change with the correct params', async function () {
238 const fields = {
239 nsfwPolicy: 'blur',
240 autoPlayVideo: false
241 }
242
243 await makePutBodyRequest({
244 url: server.url,
245 path: path + 'me',
246 token: userToken,
247 fields,
248 expectedStatus: HttpStatusCode.NO_CONTENT_204
249 })
250 })
251 })
252
253 describe('When updating my avatar', function () {
254 it('Should fail without an incorrect input file', async function () {
255 const fields = {}
256 const attaches = {
257 avatarfile: buildAbsoluteFixturePath('video_short.mp4')
258 }
259 await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
260 })
261
262 it('Should fail with a big file', async function () {
263 const fields = {}
264 const attaches = {
265 avatarfile: buildAbsoluteFixturePath('avatar-big.png')
266 }
267 await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
268 })
269
270 it('Should fail with an unauthenticated user', async function () {
271 const fields = {}
272 const attaches = {
273 avatarfile: buildAbsoluteFixturePath('avatar.png')
274 }
275 await makeUploadRequest({
276 url: server.url,
277 path: path + '/me/avatar/pick',
278 fields,
279 attaches,
280 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
281 })
282 })
283
284 it('Should succeed with the correct params', async function () {
285 const fields = {}
286 const attaches = {
287 avatarfile: buildAbsoluteFixturePath('avatar.png')
288 }
289 await makeUploadRequest({
290 url: server.url,
291 path: path + '/me/avatar/pick',
292 token: server.accessToken,
293 fields,
294 attaches,
295 expectedStatus: HttpStatusCode.OK_200
296 })
297 })
298 })
299
300 describe('When managing my scoped tokens', function () {
301
302 it('Should fail to get my scoped tokens with an non authenticated user', async function () {
303 await server.users.getMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
304 })
305
306 it('Should fail to get my scoped tokens with a bad token', async function () {
307 await server.users.getMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
308
309 })
310
311 it('Should succeed to get my scoped tokens', async function () {
312 await server.users.getMyScopedTokens()
313 })
314
315 it('Should fail to renew my scoped tokens with an non authenticated user', async function () {
316 await server.users.renewMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
317 })
318
319 it('Should fail to renew my scoped tokens with a bad token', async function () {
320 await server.users.renewMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
321 })
322
323 it('Should succeed to renew my scoped tokens', async function () {
324 await server.users.renewMyScopedTokens()
325 })
326 })
327
328 describe('When getting my information', function () {
329 it('Should fail with a non authenticated user', async function () {
330 await server.users.getMyInfo({ token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
331 })
332
333 it('Should success with the correct parameters', async function () {
334 await server.users.getMyInfo({ token: userToken })
335 })
336 })
337
338 describe('When getting my video rating', function () {
339 let command: UsersCommand
340
341 before(function () {
342 command = server.users
343 })
344
345 it('Should fail with a non authenticated user', async function () {
346 await command.getMyRating({ token: 'fake_token', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
347 })
348
349 it('Should fail with an incorrect video uuid', async function () {
350 await command.getMyRating({ videoId: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
351 })
352
353 it('Should fail with an unknown video', async function () {
354 await command.getMyRating({ videoId: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
355 })
356
357 it('Should succeed with the correct parameters', async function () {
358 await command.getMyRating({ videoId: video.id })
359 await command.getMyRating({ videoId: video.uuid })
360 await command.getMyRating({ videoId: video.shortUUID })
361 })
362 })
363
364 describe('When retrieving my global ratings', function () {
365 const path = '/api/v1/accounts/user1/ratings'
366
367 it('Should fail with a bad start pagination', async function () {
368 await checkBadStartPagination(server.url, path, userToken)
369 })
370
371 it('Should fail with a bad count pagination', async function () {
372 await checkBadCountPagination(server.url, path, userToken)
373 })
374
375 it('Should fail with an incorrect sort', async function () {
376 await checkBadSortPagination(server.url, path, userToken)
377 })
378
379 it('Should fail with a unauthenticated user', async function () {
380 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
381 })
382
383 it('Should fail with a another user', async function () {
384 await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
385 })
386
387 it('Should fail with a bad type', async function () {
388 await makeGetRequest({
389 url: server.url,
390 path,
391 token: userToken,
392 query: { rating: 'toto ' },
393 expectedStatus: HttpStatusCode.BAD_REQUEST_400
394 })
395 })
396
397 it('Should succeed with the correct params', async function () {
398 await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 })
399 })
400 })
401
402 describe('When getting my global followers', function () {
403 const path = '/api/v1/accounts/user1/followers'
404
405 it('Should fail with a bad start pagination', async function () {
406 await checkBadStartPagination(server.url, path, userToken)
407 })
408
409 it('Should fail with a bad count pagination', async function () {
410 await checkBadCountPagination(server.url, path, userToken)
411 })
412
413 it('Should fail with an incorrect sort', async function () {
414 await checkBadSortPagination(server.url, path, userToken)
415 })
416
417 it('Should fail with a unauthenticated user', async function () {
418 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
419 })
420
421 it('Should fail with a another user', async function () {
422 await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
423 })
424
425 it('Should succeed with the correct params', async function () {
426 await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 })
427 })
428 })
429
430 describe('When blocking/unblocking/removing user', function () {
431
432 it('Should fail with an incorrect id', async function () {
433 const options = { userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
434
435 await server.users.remove(options)
436 await server.users.banUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
437 await server.users.unbanUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
438 })
439
440 it('Should fail with the root user', async function () {
441 const options = { userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
442
443 await server.users.remove(options)
444 await server.users.banUser(options)
445 await server.users.unbanUser(options)
446 })
447
448 it('Should return 404 with a non existing id', async function () {
449 const options = { userId: 4545454, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
450
451 await server.users.remove(options)
452 await server.users.banUser(options)
453 await server.users.unbanUser(options)
454 })
455
456 it('Should fail with a non admin user', async function () {
457 const options = { userId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
458
459 await server.users.remove(options)
460 await server.users.banUser(options)
461 await server.users.unbanUser(options)
462 })
463
464 it('Should fail on a moderator with a moderator', async function () {
465 const options = { userId: moderatorId, token: moderatorToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
466
467 await server.users.remove(options)
468 await server.users.banUser(options)
469 await server.users.unbanUser(options)
470 })
471
472 it('Should succeed on a user with a moderator', async function () {
473 const options = { userId, token: moderatorToken }
474
475 await server.users.banUser(options)
476 await server.users.unbanUser(options)
477 })
478 })
479
480 describe('When deleting our account', function () {
481
482 it('Should fail with with the root account', async function () {
483 await server.users.deleteMe({ expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
484 })
485 })
486
487 after(async function () {
488 MockSmtpServer.Instance.kill()
489
490 await cleanupTests([ server ])
491 })
492})
diff --git a/packages/tests/src/api/check-params/plugins.ts b/packages/tests/src/api/check-params/plugins.ts
new file mode 100644
index 000000000..ab2a426fe
--- /dev/null
+++ b/packages/tests/src/api/check-params/plugins.ts
@@ -0,0 +1,490 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { HttpStatusCode, PeerTubePlugin, PluginType } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeGetRequest,
9 makePostBodyRequest,
10 makePutBodyRequest,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15describe('Test server plugins API validators', function () {
16 let server: PeerTubeServer
17 let userAccessToken = null
18
19 const npmPlugin = 'peertube-plugin-hello-world'
20 const pluginName = 'hello-world'
21 let npmVersion: string
22
23 const themePlugin = 'peertube-theme-background-red'
24 const themeName = 'background-red'
25 let themeVersion: string
26
27 // ---------------------------------------------------------------
28
29 before(async function () {
30 this.timeout(60000)
31
32 server = await createSingleServer(1)
33
34 await setAccessTokensToServers([ server ])
35
36 const user = {
37 username: 'user1',
38 password: 'password'
39 }
40
41 await server.users.create({ username: user.username, password: user.password })
42 userAccessToken = await server.login.getAccessToken(user)
43
44 {
45 const res = await server.plugins.install({ npmName: npmPlugin })
46 const plugin = res.body as PeerTubePlugin
47 npmVersion = plugin.version
48 }
49
50 {
51 const res = await server.plugins.install({ npmName: themePlugin })
52 const plugin = res.body as PeerTubePlugin
53 themeVersion = plugin.version
54 }
55 })
56
57 describe('With static plugin routes', function () {
58 it('Should fail with an unknown plugin name/plugin version', async function () {
59 const paths = [
60 '/plugins/' + pluginName + '/0.0.1/auth/fake-auth',
61 '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png',
62 '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js',
63 '/themes/' + themeName + '/0.0.1/static/images/chocobo.png',
64 '/themes/' + themeName + '/0.0.1/client-scripts/client/video-watch-client-plugin.js',
65 '/themes/' + themeName + '/0.0.1/css/assets/style1.css'
66 ]
67
68 for (const p of paths) {
69 await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
70 }
71 })
72
73 it('Should fail when requesting a plugin in the theme path', async function () {
74 await makeGetRequest({
75 url: server.url,
76 path: '/themes/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png',
77 expectedStatus: HttpStatusCode.NOT_FOUND_404
78 })
79 })
80
81 it('Should fail with invalid versions', async function () {
82 const paths = [
83 '/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth',
84 '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png',
85 '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js',
86 '/themes/' + themeName + '/1/static/images/chocobo.png',
87 '/themes/' + themeName + '/0.0.1000a/client-scripts/client/video-watch-client-plugin.js',
88 '/themes/' + themeName + '/0.a.1/css/assets/style1.css'
89 ]
90
91 for (const p of paths) {
92 await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
93 }
94 })
95
96 it('Should fail with invalid paths', async function () {
97 const paths = [
98 '/plugins/' + pluginName + '/' + npmVersion + '/static/images/../chocobo.png',
99 '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/../client/common-client-plugin.js',
100 '/themes/' + themeName + '/' + themeVersion + '/static/../images/chocobo.png',
101 '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js/..',
102 '/themes/' + themeName + '/' + themeVersion + '/css/../assets/style1.css'
103 ]
104
105 for (const p of paths) {
106 await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
107 }
108 })
109
110 it('Should fail with an unknown auth name', async function () {
111 const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth'
112
113 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
114 })
115
116 it('Should fail with an unknown static file', async function () {
117 const paths = [
118 '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png',
119 '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/fake.js',
120 '/themes/' + themeName + '/' + themeVersion + '/static/fake/chocobo.png',
121 '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/fake.js'
122 ]
123
124 for (const p of paths) {
125 await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
126 }
127 })
128
129 it('Should fail with an unknown CSS file', async function () {
130 await makeGetRequest({
131 url: server.url,
132 path: '/themes/' + themeName + '/' + themeVersion + '/css/assets/fake.css',
133 expectedStatus: HttpStatusCode.NOT_FOUND_404
134 })
135 })
136
137 it('Should succeed with the correct parameters', async function () {
138 const paths = [
139 '/plugins/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png',
140 '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/common-client-plugin.js',
141 '/themes/' + themeName + '/' + themeVersion + '/static/images/chocobo.png',
142 '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js',
143 '/themes/' + themeName + '/' + themeVersion + '/css/assets/style1.css'
144 ]
145
146 for (const p of paths) {
147 await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.OK_200 })
148 }
149
150 const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth'
151 await makeGetRequest({ url: server.url, path: authPath, expectedStatus: HttpStatusCode.FOUND_302 })
152 })
153 })
154
155 describe('When listing available plugins/themes', function () {
156 const path = '/api/v1/plugins/available'
157 const baseQuery = {
158 search: 'super search',
159 pluginType: PluginType.PLUGIN,
160 currentPeerTubeEngine: '1.2.3'
161 }
162
163 it('Should fail with an invalid token', async function () {
164 await makeGetRequest({
165 url: server.url,
166 path,
167 token: 'fake_token',
168 query: baseQuery,
169 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
170 })
171 })
172
173 it('Should fail if the user is not an administrator', async function () {
174 await makeGetRequest({
175 url: server.url,
176 path,
177 token: userAccessToken,
178 query: baseQuery,
179 expectedStatus: HttpStatusCode.FORBIDDEN_403
180 })
181 })
182
183 it('Should fail with a bad start pagination', async function () {
184 await checkBadStartPagination(server.url, path, server.accessToken)
185 })
186
187 it('Should fail with a bad count pagination', async function () {
188 await checkBadCountPagination(server.url, path, server.accessToken)
189 })
190
191 it('Should fail with an incorrect sort', async function () {
192 await checkBadSortPagination(server.url, path, server.accessToken)
193 })
194
195 it('Should fail with an invalid plugin type', async function () {
196 const query = { ...baseQuery, pluginType: 5 }
197
198 await makeGetRequest({
199 url: server.url,
200 path,
201 token: server.accessToken,
202 query
203 })
204 })
205
206 it('Should fail with an invalid current peertube engine', async function () {
207 const query = { ...baseQuery, currentPeerTubeEngine: '1.0' }
208
209 await makeGetRequest({
210 url: server.url,
211 path,
212 token: server.accessToken,
213 query
214 })
215 })
216
217 it('Should success with the correct parameters', async function () {
218 await makeGetRequest({
219 url: server.url,
220 path,
221 token: server.accessToken,
222 query: baseQuery,
223 expectedStatus: HttpStatusCode.OK_200
224 })
225 })
226 })
227
228 describe('When listing local plugins/themes', function () {
229 const path = '/api/v1/plugins'
230 const baseQuery = {
231 pluginType: PluginType.THEME
232 }
233
234 it('Should fail with an invalid token', async function () {
235 await makeGetRequest({
236 url: server.url,
237 path,
238 token: 'fake_token',
239 query: baseQuery,
240 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
241 })
242 })
243
244 it('Should fail if the user is not an administrator', async function () {
245 await makeGetRequest({
246 url: server.url,
247 path,
248 token: userAccessToken,
249 query: baseQuery,
250 expectedStatus: HttpStatusCode.FORBIDDEN_403
251 })
252 })
253
254 it('Should fail with a bad start pagination', async function () {
255 await checkBadStartPagination(server.url, path, server.accessToken)
256 })
257
258 it('Should fail with a bad count pagination', async function () {
259 await checkBadCountPagination(server.url, path, server.accessToken)
260 })
261
262 it('Should fail with an incorrect sort', async function () {
263 await checkBadSortPagination(server.url, path, server.accessToken)
264 })
265
266 it('Should fail with an invalid plugin type', async function () {
267 const query = { ...baseQuery, pluginType: 5 }
268
269 await makeGetRequest({
270 url: server.url,
271 path,
272 token: server.accessToken,
273 query
274 })
275 })
276
277 it('Should success with the correct parameters', async function () {
278 await makeGetRequest({
279 url: server.url,
280 path,
281 token: server.accessToken,
282 query: baseQuery,
283 expectedStatus: HttpStatusCode.OK_200
284 })
285 })
286 })
287
288 describe('When getting a plugin or the registered settings or public settings', function () {
289 const path = '/api/v1/plugins/'
290
291 it('Should fail with an invalid token', async function () {
292 for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) {
293 await makeGetRequest({
294 url: server.url,
295 path: path + suffix,
296 token: 'fake_token',
297 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
298 })
299 }
300 })
301
302 it('Should fail if the user is not an administrator', async function () {
303 for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) {
304 await makeGetRequest({
305 url: server.url,
306 path: path + suffix,
307 token: userAccessToken,
308 expectedStatus: HttpStatusCode.FORBIDDEN_403
309 })
310 }
311 })
312
313 it('Should fail with an invalid npm name', async function () {
314 for (const suffix of [ 'toto', 'toto/registered-settings', 'toto/public-settings' ]) {
315 await makeGetRequest({
316 url: server.url,
317 path: path + suffix,
318 token: server.accessToken,
319 expectedStatus: HttpStatusCode.BAD_REQUEST_400
320 })
321 }
322
323 for (const suffix of [ 'peertube-plugin-TOTO', 'peertube-plugin-TOTO/registered-settings', 'peertube-plugin-TOTO/public-settings' ]) {
324 await makeGetRequest({
325 url: server.url,
326 path: path + suffix,
327 token: server.accessToken,
328 expectedStatus: HttpStatusCode.BAD_REQUEST_400
329 })
330 }
331 })
332
333 it('Should fail with an unknown plugin', async function () {
334 for (const suffix of [ 'peertube-plugin-toto', 'peertube-plugin-toto/registered-settings', 'peertube-plugin-toto/public-settings' ]) {
335 await makeGetRequest({
336 url: server.url,
337 path: path + suffix,
338 token: server.accessToken,
339 expectedStatus: HttpStatusCode.NOT_FOUND_404
340 })
341 }
342 })
343
344 it('Should succeed with the correct parameters', async function () {
345 for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings`, `${npmPlugin}/public-settings` ]) {
346 await makeGetRequest({
347 url: server.url,
348 path: path + suffix,
349 token: server.accessToken,
350 expectedStatus: HttpStatusCode.OK_200
351 })
352 }
353 })
354 })
355
356 describe('When updating plugin settings', function () {
357 const path = '/api/v1/plugins/'
358 const settings = { setting1: 'value1' }
359
360 it('Should fail with an invalid token', async function () {
361 await makePutBodyRequest({
362 url: server.url,
363 path: path + npmPlugin + '/settings',
364 fields: { settings },
365 token: 'fake_token',
366 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
367 })
368 })
369
370 it('Should fail if the user is not an administrator', async function () {
371 await makePutBodyRequest({
372 url: server.url,
373 path: path + npmPlugin + '/settings',
374 fields: { settings },
375 token: userAccessToken,
376 expectedStatus: HttpStatusCode.FORBIDDEN_403
377 })
378 })
379
380 it('Should fail with an invalid npm name', async function () {
381 await makePutBodyRequest({
382 url: server.url,
383 path: path + 'toto/settings',
384 fields: { settings },
385 token: server.accessToken,
386 expectedStatus: HttpStatusCode.BAD_REQUEST_400
387 })
388
389 await makePutBodyRequest({
390 url: server.url,
391 path: path + 'peertube-plugin-TOTO/settings',
392 fields: { settings },
393 token: server.accessToken,
394 expectedStatus: HttpStatusCode.BAD_REQUEST_400
395 })
396 })
397
398 it('Should fail with an unknown plugin', async function () {
399 await makePutBodyRequest({
400 url: server.url,
401 path: path + 'peertube-plugin-toto/settings',
402 fields: { settings },
403 token: server.accessToken,
404 expectedStatus: HttpStatusCode.NOT_FOUND_404
405 })
406 })
407
408 it('Should succeed with the correct parameters', async function () {
409 await makePutBodyRequest({
410 url: server.url,
411 path: path + npmPlugin + '/settings',
412 fields: { settings },
413 token: server.accessToken,
414 expectedStatus: HttpStatusCode.NO_CONTENT_204
415 })
416 })
417 })
418
419 describe('When installing/updating/uninstalling a plugin', function () {
420 const path = '/api/v1/plugins/'
421
422 it('Should fail with an invalid token', async function () {
423 for (const suffix of [ 'install', 'update', 'uninstall' ]) {
424 await makePostBodyRequest({
425 url: server.url,
426 path: path + suffix,
427 fields: { npmName: npmPlugin },
428 token: 'fake_token',
429 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
430 })
431 }
432 })
433
434 it('Should fail if the user is not an administrator', async function () {
435 for (const suffix of [ 'install', 'update', 'uninstall' ]) {
436 await makePostBodyRequest({
437 url: server.url,
438 path: path + suffix,
439 fields: { npmName: npmPlugin },
440 token: userAccessToken,
441 expectedStatus: HttpStatusCode.FORBIDDEN_403
442 })
443 }
444 })
445
446 it('Should fail with an invalid npm name', async function () {
447 for (const suffix of [ 'install', 'update', 'uninstall' ]) {
448 await makePostBodyRequest({
449 url: server.url,
450 path: path + suffix,
451 fields: { npmName: 'toto' },
452 token: server.accessToken,
453 expectedStatus: HttpStatusCode.BAD_REQUEST_400
454 })
455 }
456
457 for (const suffix of [ 'install', 'update', 'uninstall' ]) {
458 await makePostBodyRequest({
459 url: server.url,
460 path: path + suffix,
461 fields: { npmName: 'peertube-plugin-TOTO' },
462 token: server.accessToken,
463 expectedStatus: HttpStatusCode.BAD_REQUEST_400
464 })
465 }
466 })
467
468 it('Should succeed with the correct parameters', async function () {
469 const it = [
470 { suffix: 'install', status: HttpStatusCode.OK_200 },
471 { suffix: 'update', status: HttpStatusCode.OK_200 },
472 { suffix: 'uninstall', status: HttpStatusCode.NO_CONTENT_204 }
473 ]
474
475 for (const obj of it) {
476 await makePostBodyRequest({
477 url: server.url,
478 path: path + obj.suffix,
479 fields: { npmName: npmPlugin },
480 token: server.accessToken,
481 expectedStatus: obj.status
482 })
483 }
484 })
485 })
486
487 after(async function () {
488 await cleanupTests([ server ])
489 })
490})
diff --git a/packages/tests/src/api/check-params/redundancy.ts b/packages/tests/src/api/check-params/redundancy.ts
new file mode 100644
index 000000000..16a5d0a3d
--- /dev/null
+++ b/packages/tests/src/api/check-params/redundancy.ts
@@ -0,0 +1,240 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { HttpStatusCode, VideoCreateResult } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 makeDeleteRequest,
10 makeGetRequest,
11 makePostBodyRequest,
12 makePutBodyRequest,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 waitJobs
16} from '@peertube/peertube-server-commands'
17
18describe('Test server redundancy API validators', function () {
19 let servers: PeerTubeServer[]
20 let userAccessToken = null
21 let videoIdLocal: number
22 let videoRemote: VideoCreateResult
23
24 // ---------------------------------------------------------------
25
26 before(async function () {
27 this.timeout(160000)
28
29 servers = await createMultipleServers(2)
30
31 await setAccessTokensToServers(servers)
32 await doubleFollow(servers[0], servers[1])
33
34 const user = {
35 username: 'user1',
36 password: 'password'
37 }
38
39 await servers[0].users.create({ username: user.username, password: user.password })
40 userAccessToken = await servers[0].login.getAccessToken(user)
41
42 videoIdLocal = (await servers[0].videos.quickUpload({ name: 'video' })).id
43
44 const remoteUUID = (await servers[1].videos.quickUpload({ name: 'video' })).uuid
45
46 await waitJobs(servers)
47
48 videoRemote = await servers[0].videos.get({ id: remoteUUID })
49 })
50
51 describe('When listing redundancies', function () {
52 const path = '/api/v1/server/redundancy/videos'
53
54 let url: string
55 let token: string
56
57 before(function () {
58 url = servers[0].url
59 token = servers[0].accessToken
60 })
61
62 it('Should fail with an invalid token', async function () {
63 await makeGetRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
64 })
65
66 it('Should fail if the user is not an administrator', async function () {
67 await makeGetRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
68 })
69
70 it('Should fail with a bad start pagination', async function () {
71 await checkBadStartPagination(url, path, servers[0].accessToken)
72 })
73
74 it('Should fail with a bad count pagination', async function () {
75 await checkBadCountPagination(url, path, servers[0].accessToken)
76 })
77
78 it('Should fail with an incorrect sort', async function () {
79 await checkBadSortPagination(url, path, servers[0].accessToken)
80 })
81
82 it('Should fail with a bad target', async function () {
83 await makeGetRequest({ url, path, token, query: { target: 'bad target' } })
84 })
85
86 it('Should fail without target', async function () {
87 await makeGetRequest({ url, path, token })
88 })
89
90 it('Should succeed with the correct params', async function () {
91 await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, expectedStatus: HttpStatusCode.OK_200 })
92 })
93 })
94
95 describe('When manually adding a redundancy', function () {
96 const path = '/api/v1/server/redundancy/videos'
97
98 let url: string
99 let token: string
100
101 before(function () {
102 url = servers[0].url
103 token = servers[0].accessToken
104 })
105
106 it('Should fail with an invalid token', async function () {
107 await makePostBodyRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
108 })
109
110 it('Should fail if the user is not an administrator', async function () {
111 await makePostBodyRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
112 })
113
114 it('Should fail without a video id', async function () {
115 await makePostBodyRequest({ url, path, token })
116 })
117
118 it('Should fail with an incorrect video id', async function () {
119 await makePostBodyRequest({ url, path, token, fields: { videoId: 'peertube' } })
120 })
121
122 it('Should fail with a not found video id', async function () {
123 await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
124 })
125
126 it('Should fail with a local a video id', async function () {
127 await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdLocal } })
128 })
129
130 it('Should succeed with the correct params', async function () {
131 await makePostBodyRequest({
132 url,
133 path,
134 token,
135 fields: { videoId: videoRemote.shortUUID },
136 expectedStatus: HttpStatusCode.NO_CONTENT_204
137 })
138 })
139
140 it('Should fail if the video is already duplicated', async function () {
141 this.timeout(30000)
142
143 await waitJobs(servers)
144
145 await makePostBodyRequest({
146 url,
147 path,
148 token,
149 fields: { videoId: videoRemote.uuid },
150 expectedStatus: HttpStatusCode.CONFLICT_409
151 })
152 })
153 })
154
155 describe('When manually removing a redundancy', function () {
156 const path = '/api/v1/server/redundancy/videos/'
157
158 let url: string
159 let token: string
160
161 before(function () {
162 url = servers[0].url
163 token = servers[0].accessToken
164 })
165
166 it('Should fail with an invalid token', async function () {
167 await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
168 })
169
170 it('Should fail if the user is not an administrator', async function () {
171 await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
172 })
173
174 it('Should fail with an incorrect video id', async function () {
175 await makeDeleteRequest({ url, path: path + 'toto', token })
176 })
177
178 it('Should fail with a not found video redundancy', async function () {
179 await makeDeleteRequest({ url, path: path + '454545', token, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
180 })
181 })
182
183 describe('When updating server redundancy', function () {
184 const path = '/api/v1/server/redundancy'
185
186 it('Should fail with an invalid token', async function () {
187 await makePutBodyRequest({
188 url: servers[0].url,
189 path: path + '/' + servers[1].host,
190 fields: { redundancyAllowed: true },
191 token: 'fake_token',
192 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
193 })
194 })
195
196 it('Should fail if the user is not an administrator', async function () {
197 await makePutBodyRequest({
198 url: servers[0].url,
199 path: path + '/' + servers[1].host,
200 fields: { redundancyAllowed: true },
201 token: userAccessToken,
202 expectedStatus: HttpStatusCode.FORBIDDEN_403
203 })
204 })
205
206 it('Should fail if we do not follow this server', async function () {
207 await makePutBodyRequest({
208 url: servers[0].url,
209 path: path + '/example.com',
210 fields: { redundancyAllowed: true },
211 token: servers[0].accessToken,
212 expectedStatus: HttpStatusCode.NOT_FOUND_404
213 })
214 })
215
216 it('Should fail without de redundancyAllowed param', async function () {
217 await makePutBodyRequest({
218 url: servers[0].url,
219 path: path + '/' + servers[1].host,
220 fields: { blabla: true },
221 token: servers[0].accessToken,
222 expectedStatus: HttpStatusCode.BAD_REQUEST_400
223 })
224 })
225
226 it('Should succeed with the correct parameters', async function () {
227 await makePutBodyRequest({
228 url: servers[0].url,
229 path: path + '/' + servers[1].host,
230 fields: { redundancyAllowed: true },
231 token: servers[0].accessToken,
232 expectedStatus: HttpStatusCode.NO_CONTENT_204
233 })
234 })
235 })
236
237 after(async function () {
238 await cleanupTests(servers)
239 })
240})
diff --git a/packages/tests/src/api/check-params/registrations.ts b/packages/tests/src/api/check-params/registrations.ts
new file mode 100644
index 000000000..e4e46da2a
--- /dev/null
+++ b/packages/tests/src/api/check-params/registrations.ts
@@ -0,0 +1,446 @@
1import { omit } from '@peertube/peertube-core-utils'
2import { HttpStatusCode, HttpStatusCodeType, UserRole } from '@peertube/peertube-models'
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import {
5 cleanupTests,
6 createSingleServer,
7 makePostBodyRequest,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultAccountAvatar,
11 setDefaultChannelAvatar
12} from '@peertube/peertube-server-commands'
13
14describe('Test registrations API validators', function () {
15 let server: PeerTubeServer
16 let userToken: string
17 let moderatorToken: string
18
19 // ---------------------------------------------------------------
20
21 before(async function () {
22 this.timeout(30000)
23
24 server = await createSingleServer(1)
25
26 await setAccessTokensToServers([ server ])
27 await setDefaultAccountAvatar([ server ])
28 await setDefaultChannelAvatar([ server ])
29
30 await server.config.enableSignup(false);
31
32 ({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR));
33 ({ token: userToken } = await server.users.generate('user', UserRole.USER))
34 })
35
36 describe('Register', function () {
37 const registrationPath = '/api/v1/users/register'
38 const registrationRequestPath = '/api/v1/users/registrations/request'
39
40 const baseCorrectParams = {
41 username: 'user3',
42 displayName: 'super user',
43 email: 'test3@example.com',
44 password: 'my super password',
45 registrationReason: 'my super registration reason'
46 }
47
48 describe('When registering a new user or requesting user registration', function () {
49
50 async function check (fields: any, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) {
51 await server.config.enableSignup(false)
52 await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus })
53
54 await server.config.enableSignup(true)
55 await makePostBodyRequest({ url: server.url, path: registrationRequestPath, fields, expectedStatus })
56 }
57
58 it('Should fail with a too small username', async function () {
59 const fields = { ...baseCorrectParams, username: '' }
60
61 await check(fields)
62 })
63
64 it('Should fail with a too long username', async function () {
65 const fields = { ...baseCorrectParams, username: 'super'.repeat(50) }
66
67 await check(fields)
68 })
69
70 it('Should fail with an incorrect username', async function () {
71 const fields = { ...baseCorrectParams, username: 'my username' }
72
73 await check(fields)
74 })
75
76 it('Should fail with a missing email', async function () {
77 const fields = omit(baseCorrectParams, [ 'email' ])
78
79 await check(fields)
80 })
81
82 it('Should fail with an invalid email', async function () {
83 const fields = { ...baseCorrectParams, email: 'test_example.com' }
84
85 await check(fields)
86 })
87
88 it('Should fail with a too small password', async function () {
89 const fields = { ...baseCorrectParams, password: 'bla' }
90
91 await check(fields)
92 })
93
94 it('Should fail with a too long password', async function () {
95 const fields = { ...baseCorrectParams, password: 'super'.repeat(61) }
96
97 await check(fields)
98 })
99
100 it('Should fail if we register a user with the same username', async function () {
101 const fields = { ...baseCorrectParams, username: 'root' }
102
103 await check(fields, HttpStatusCode.CONFLICT_409)
104 })
105
106 it('Should fail with a "peertube" username', async function () {
107 const fields = { ...baseCorrectParams, username: 'peertube' }
108
109 await check(fields, HttpStatusCode.CONFLICT_409)
110 })
111
112 it('Should fail if we register a user with the same email', async function () {
113 const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' }
114
115 await check(fields, HttpStatusCode.CONFLICT_409)
116 })
117
118 it('Should fail with a bad display name', async function () {
119 const fields = { ...baseCorrectParams, displayName: 'a'.repeat(150) }
120
121 await check(fields)
122 })
123
124 it('Should fail with a bad channel name', async function () {
125 const fields = { ...baseCorrectParams, channel: { name: '[]azf', displayName: 'toto' } }
126
127 await check(fields)
128 })
129
130 it('Should fail with a bad channel display name', async function () {
131 const fields = { ...baseCorrectParams, channel: { name: 'toto', displayName: '' } }
132
133 await check(fields)
134 })
135
136 it('Should fail with a channel name that is the same as username', async function () {
137 const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } }
138 const fields = { ...baseCorrectParams, ...source }
139
140 await check(fields)
141 })
142
143 it('Should fail with an existing channel', async function () {
144 const attributes = { name: 'existing_channel', displayName: 'hello', description: 'super description' }
145 await server.channels.create({ attributes })
146
147 const fields = { ...baseCorrectParams, channel: { name: 'existing_channel', displayName: 'toto' } }
148
149 await check(fields, HttpStatusCode.CONFLICT_409)
150 })
151
152 it('Should fail on a server with registration disabled', async function () {
153 this.timeout(60000)
154
155 await server.config.updateExistingSubConfig({
156 newConfig: {
157 signup: {
158 enabled: false
159 }
160 }
161 })
162
163 await server.registrations.register({ username: 'user4', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
164 await server.registrations.requestRegistration({
165 username: 'user4',
166 registrationReason: 'reason',
167 expectedStatus: HttpStatusCode.FORBIDDEN_403
168 })
169 })
170
171 it('Should fail if the user limit is reached', async function () {
172 this.timeout(60000)
173
174 const { total } = await server.users.list()
175
176 await server.config.enableSignup(false, total)
177 await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
178
179 await server.config.enableSignup(true, total)
180 await server.registrations.requestRegistration({
181 username: 'user42',
182 registrationReason: 'reason',
183 expectedStatus: HttpStatusCode.FORBIDDEN_403
184 })
185 })
186
187 it('Should succeed if the user limit is not reached', async function () {
188 this.timeout(60000)
189
190 const { total } = await server.users.list()
191
192 await server.config.enableSignup(false, total + 1)
193 await server.registrations.register({ username: 'user43', expectedStatus: HttpStatusCode.NO_CONTENT_204 })
194
195 await server.config.enableSignup(true, total + 2)
196 await server.registrations.requestRegistration({
197 username: 'user44',
198 registrationReason: 'reason',
199 expectedStatus: HttpStatusCode.OK_200
200 })
201 })
202 })
203
204 describe('On direct registration', function () {
205
206 it('Should succeed with the correct params', async function () {
207 await server.config.enableSignup(false)
208
209 const fields = {
210 username: 'user_direct_1',
211 displayName: 'super user direct 1',
212 email: 'user_direct_1@example.com',
213 password: 'my super password',
214 channel: { name: 'super_user_direct_1_channel', displayName: 'super user direct 1 channel' }
215 }
216
217 await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
218 })
219
220 it('Should fail if the instance requires approval', async function () {
221 this.timeout(60000)
222
223 await server.config.enableSignup(true)
224 await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
225 })
226 })
227
228 describe('On registration request', function () {
229
230 before(async function () {
231 this.timeout(60000)
232
233 await server.config.enableSignup(true)
234 })
235
236 it('Should fail with an invalid registration reason', async function () {
237 for (const registrationReason of [ '', 't', 't'.repeat(5000) ]) {
238 await server.registrations.requestRegistration({
239 username: 'user_request_1',
240 registrationReason,
241 expectedStatus: HttpStatusCode.BAD_REQUEST_400
242 })
243 }
244 })
245
246 it('Should succeed with the correct params', async function () {
247 await server.registrations.requestRegistration({
248 username: 'user_request_2',
249 registrationReason: 'tt',
250 channel: {
251 displayName: 'my user request 2 channel',
252 name: 'user_request_2_channel'
253 }
254 })
255 })
256
257 it('Should fail if the username is already awaiting registration approval', async function () {
258 await server.registrations.requestRegistration({
259 username: 'user_request_2',
260 registrationReason: 'tt',
261 channel: {
262 displayName: 'my user request 42 channel',
263 name: 'user_request_42_channel'
264 },
265 expectedStatus: HttpStatusCode.CONFLICT_409
266 })
267 })
268
269 it('Should fail if the email is already awaiting registration approval', async function () {
270 await server.registrations.requestRegistration({
271 username: 'user42',
272 email: 'user_request_2@example.com',
273 registrationReason: 'tt',
274 channel: {
275 displayName: 'my user request 42 channel',
276 name: 'user_request_42_channel'
277 },
278 expectedStatus: HttpStatusCode.CONFLICT_409
279 })
280 })
281
282 it('Should fail if the channel is already awaiting registration approval', async function () {
283 await server.registrations.requestRegistration({
284 username: 'user42',
285 registrationReason: 'tt',
286 channel: {
287 displayName: 'my user request 2 channel',
288 name: 'user_request_2_channel'
289 },
290 expectedStatus: HttpStatusCode.CONFLICT_409
291 })
292 })
293
294 it('Should fail if the instance does not require approval', async function () {
295 this.timeout(60000)
296
297 await server.config.enableSignup(false)
298
299 await server.registrations.requestRegistration({
300 username: 'user42',
301 registrationReason: 'toto',
302 expectedStatus: HttpStatusCode.BAD_REQUEST_400
303 })
304 })
305 })
306 })
307
308 describe('Registrations accept/reject', function () {
309 let id1: number
310 let id2: number
311
312 before(async function () {
313 this.timeout(60000)
314
315 await server.config.enableSignup(true);
316
317 ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' }));
318 ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' }))
319 })
320
321 it('Should fail to accept/reject registration without token', async function () {
322 const options = { id: id1, moderationResponse: 'tt', token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }
323 await server.registrations.accept(options)
324 await server.registrations.reject(options)
325 })
326
327 it('Should fail to accept/reject registration with a non moderator user', async function () {
328 const options = { id: id1, moderationResponse: 'tt', token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
329 await server.registrations.accept(options)
330 await server.registrations.reject(options)
331 })
332
333 it('Should fail to accept/reject registration with a bad registration id', async function () {
334 {
335 const options = { id: 't' as any, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
336 await server.registrations.accept(options)
337 await server.registrations.reject(options)
338 }
339
340 {
341 const options = { id: 42, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
342 await server.registrations.accept(options)
343 await server.registrations.reject(options)
344 }
345 })
346
347 it('Should fail to accept/reject registration with a bad moderation resposne', async function () {
348 for (const moderationResponse of [ '', 't', 't'.repeat(5000) ]) {
349 const options = { id: id1, moderationResponse, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
350 await server.registrations.accept(options)
351 await server.registrations.reject(options)
352 }
353 })
354
355 it('Should succeed to accept a registration', async function () {
356 await server.registrations.accept({ id: id1, moderationResponse: 'tt', token: moderatorToken })
357 })
358
359 it('Should succeed to reject a registration', async function () {
360 await server.registrations.reject({ id: id2, moderationResponse: 'tt', token: moderatorToken })
361 })
362
363 it('Should fail to accept/reject a registration that was already accepted/rejected', async function () {
364 for (const id of [ id1, id2 ]) {
365 const options = { id, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.CONFLICT_409 }
366 await server.registrations.accept(options)
367 await server.registrations.reject(options)
368 }
369 })
370 })
371
372 describe('Registrations deletion', function () {
373 let id1: number
374 let id2: number
375 let id3: number
376
377 before(async function () {
378 ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' }));
379 ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' }));
380 ({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' }))
381
382 await server.registrations.accept({ id: id2, moderationResponse: 'tt' })
383 await server.registrations.reject({ id: id3, moderationResponse: 'tt' })
384 })
385
386 it('Should fail to delete registration without token', async function () {
387 await server.registrations.delete({ id: id1, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
388 })
389
390 it('Should fail to delete registration with a non moderator user', async function () {
391 await server.registrations.delete({ id: id1, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
392 })
393
394 it('Should fail to delete registration with a bad registration id', async function () {
395 await server.registrations.delete({ id: 't' as any, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
396 await server.registrations.delete({ id: 42, token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
397 })
398
399 it('Should succeed with the correct params', async function () {
400 await server.registrations.delete({ id: id1, token: moderatorToken })
401 await server.registrations.delete({ id: id2, token: moderatorToken })
402 await server.registrations.delete({ id: id3, token: moderatorToken })
403 })
404 })
405
406 describe('Listing registrations', function () {
407 const path = '/api/v1/users/registrations'
408
409 it('Should fail with a bad start pagination', async function () {
410 await checkBadStartPagination(server.url, path, server.accessToken)
411 })
412
413 it('Should fail with a bad count pagination', async function () {
414 await checkBadCountPagination(server.url, path, server.accessToken)
415 })
416
417 it('Should fail with an incorrect sort', async function () {
418 await checkBadSortPagination(server.url, path, server.accessToken)
419 })
420
421 it('Should fail with a non authenticated user', async function () {
422 await server.registrations.list({
423 token: null,
424 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
425 })
426 })
427
428 it('Should fail with a non admin user', async function () {
429 await server.registrations.list({
430 token: userToken,
431 expectedStatus: HttpStatusCode.FORBIDDEN_403
432 })
433 })
434
435 it('Should succeed with the correct params', async function () {
436 await server.registrations.list({
437 token: moderatorToken,
438 search: 'toto'
439 })
440 })
441 })
442
443 after(async function () {
444 await cleanupTests([ server ])
445 })
446})
diff --git a/packages/tests/src/api/check-params/runners.ts b/packages/tests/src/api/check-params/runners.ts
new file mode 100644
index 000000000..dd2d2f0a1
--- /dev/null
+++ b/packages/tests/src/api/check-params/runners.ts
@@ -0,0 +1,911 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2import { basename } from 'path'
3import {
4 HttpStatusCode,
5 HttpStatusCodeType,
6 isVideoStudioTaskIntro,
7 RunnerJob,
8 RunnerJobState,
9 RunnerJobStudioTranscodingPayload,
10 RunnerJobSuccessPayload,
11 RunnerJobUpdatePayload,
12 VideoPrivacy,
13 VideoStudioTaskIntro
14} from '@peertube/peertube-models'
15import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
16import {
17 cleanupTests,
18 createSingleServer,
19 makePostBodyRequest,
20 PeerTubeServer,
21 sendRTMPStream,
22 setAccessTokensToServers,
23 setDefaultVideoChannel,
24 stopFfmpeg,
25 VideoStudioCommand,
26 waitJobs
27} from '@peertube/peertube-server-commands'
28
29const badUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
30
31describe('Test managing runners', function () {
32 let server: PeerTubeServer
33
34 let userToken: string
35
36 let registrationTokenId: number
37 let registrationToken: string
38
39 let runnerToken: string
40 let runnerToken2: string
41
42 let completedJobToken: string
43 let completedJobUUID: string
44
45 let cancelledJobToken: string
46 let cancelledJobUUID: string
47
48 before(async function () {
49 this.timeout(120000)
50
51 const config = {
52 rates_limit: {
53 api: {
54 max: 5000
55 }
56 }
57 }
58
59 server = await createSingleServer(1, config)
60 await setAccessTokensToServers([ server ])
61 await setDefaultVideoChannel([ server ])
62
63 userToken = await server.users.generateUserAndToken('user1')
64
65 const { data } = await server.runnerRegistrationTokens.list()
66 registrationToken = data[0].registrationToken
67 registrationTokenId = data[0].id
68
69 await server.config.enableTranscoding({ hls: true, webVideo: true })
70 await server.config.enableStudio()
71 await server.config.enableRemoteTranscoding()
72 await server.config.enableRemoteStudio()
73
74 runnerToken = await server.runners.autoRegisterRunner()
75 runnerToken2 = await server.runners.autoRegisterRunner()
76
77 {
78 await server.videos.quickUpload({ name: 'video 1' })
79 await server.videos.quickUpload({ name: 'video 2' })
80
81 await waitJobs([ server ])
82
83 {
84 const job = await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
85 completedJobToken = job.jobToken
86 completedJobUUID = job.uuid
87 }
88
89 {
90 const { job } = await server.runnerJobs.autoAccept({ runnerToken })
91 cancelledJobToken = job.jobToken
92 cancelledJobUUID = job.uuid
93 await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID })
94 }
95 }
96 })
97
98 describe('Managing runner registration tokens', function () {
99
100 describe('Common', function () {
101
102 it('Should fail to generate, list or delete runner registration token without oauth token', async function () {
103 const expectedStatus = HttpStatusCode.UNAUTHORIZED_401
104
105 await server.runnerRegistrationTokens.generate({ token: null, expectedStatus })
106 await server.runnerRegistrationTokens.list({ token: null, expectedStatus })
107 await server.runnerRegistrationTokens.delete({ token: null, id: registrationTokenId, expectedStatus })
108 })
109
110 it('Should fail to generate, list or delete runner registration token without admin rights', async function () {
111 const expectedStatus = HttpStatusCode.FORBIDDEN_403
112
113 await server.runnerRegistrationTokens.generate({ token: userToken, expectedStatus })
114 await server.runnerRegistrationTokens.list({ token: userToken, expectedStatus })
115 await server.runnerRegistrationTokens.delete({ token: userToken, id: registrationTokenId, expectedStatus })
116 })
117 })
118
119 describe('Delete', function () {
120
121 it('Should fail to delete with a bad id', async function () {
122 await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
123 })
124 })
125
126 describe('List', function () {
127 const path = '/api/v1/runners/registration-tokens'
128
129 it('Should fail to list with a bad start pagination', async function () {
130 await checkBadStartPagination(server.url, path, server.accessToken)
131 })
132
133 it('Should fail to list with a bad count pagination', async function () {
134 await checkBadCountPagination(server.url, path, server.accessToken)
135 })
136
137 it('Should fail to list with an incorrect sort', async function () {
138 await checkBadSortPagination(server.url, path, server.accessToken)
139 })
140
141 it('Should succeed to list with the correct params', async function () {
142 await server.runnerRegistrationTokens.list({ start: 0, count: 5, sort: '-createdAt' })
143 })
144 })
145 })
146
147 describe('Managing runners', function () {
148 let toDeleteId: number
149
150 describe('Register', function () {
151 const name = 'runner name'
152
153 it('Should fail with a bad registration token', async function () {
154 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
155
156 await server.runners.register({ name, registrationToken: 'a'.repeat(4000), expectedStatus })
157 await server.runners.register({ name, registrationToken: null, expectedStatus })
158 })
159
160 it('Should fail with an unknown registration token', async function () {
161 await server.runners.register({ name, registrationToken: 'aaa', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
162 })
163
164 it('Should fail with a bad name', async function () {
165 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
166
167 await server.runners.register({ name: '', registrationToken, expectedStatus })
168 await server.runners.register({ name: 'a'.repeat(200), registrationToken, expectedStatus })
169 })
170
171 it('Should fail with an invalid description', async function () {
172 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
173
174 await server.runners.register({ name, description: '', registrationToken, expectedStatus })
175 await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus })
176 })
177
178 it('Should succeed with the correct params', async function () {
179 const { id } = await server.runners.register({ name, description: 'super description', registrationToken })
180
181 toDeleteId = id
182 })
183
184 it('Should fail with the same runner name', async function () {
185 await server.runners.register({
186 name,
187 description: 'super description',
188 registrationToken,
189 expectedStatus: HttpStatusCode.BAD_REQUEST_400
190 })
191 })
192 })
193
194 describe('Delete', function () {
195
196 it('Should fail without oauth token', async function () {
197 await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
198 })
199
200 it('Should fail without admin rights', async function () {
201 await server.runners.delete({ token: userToken, id: toDeleteId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
202 })
203
204 it('Should fail with a bad id', async function () {
205 await server.runners.delete({ id: 'hi' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
206 })
207
208 it('Should fail with an unknown id', async function () {
209 await server.runners.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
210 })
211
212 it('Should succeed with the correct params', async function () {
213 await server.runners.delete({ id: toDeleteId })
214 })
215 })
216
217 describe('List', function () {
218 const path = '/api/v1/runners'
219
220 it('Should fail without oauth token', async function () {
221 await server.runners.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
222 })
223
224 it('Should fail without admin rights', async function () {
225 await server.runners.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
226 })
227
228 it('Should fail to list with a bad start pagination', async function () {
229 await checkBadStartPagination(server.url, path, server.accessToken)
230 })
231
232 it('Should fail to list with a bad count pagination', async function () {
233 await checkBadCountPagination(server.url, path, server.accessToken)
234 })
235
236 it('Should fail to list with an incorrect sort', async function () {
237 await checkBadSortPagination(server.url, path, server.accessToken)
238 })
239
240 it('Should fail with an invalid state', async function () {
241 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
242 })
243
244 it('Should succeed to list with the correct params', async function () {
245 await server.runners.list({ start: 0, count: 5, sort: '-createdAt' })
246 })
247 })
248
249 })
250
251 describe('Runner jobs by admin', function () {
252
253 describe('Cancel', function () {
254 let jobUUID: string
255
256 before(async function () {
257 this.timeout(60000)
258
259 await server.videos.quickUpload({ name: 'video' })
260 await waitJobs([ server ])
261
262 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
263 jobUUID = availableJobs[0].uuid
264 })
265
266 it('Should fail without oauth token', async function () {
267 await server.runnerJobs.cancelByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
268 })
269
270 it('Should fail without admin rights', async function () {
271 await server.runnerJobs.cancelByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
272 })
273
274 it('Should fail with a bad job uuid', async function () {
275 await server.runnerJobs.cancelByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
276 })
277
278 it('Should fail with an unknown job uuid', async function () {
279 const jobUUID = badUUID
280 await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
281 })
282
283 it('Should fail with an already cancelled job', async function () {
284 await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
285 })
286
287 it('Should succeed with the correct params', async function () {
288 await server.runnerJobs.cancelByAdmin({ jobUUID })
289 })
290 })
291
292 describe('List', function () {
293 const path = '/api/v1/runners/jobs'
294
295 it('Should fail without oauth token', async function () {
296 await server.runnerJobs.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
297 })
298
299 it('Should fail without admin rights', async function () {
300 await server.runnerJobs.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
301 })
302
303 it('Should fail to list with a bad start pagination', async function () {
304 await checkBadStartPagination(server.url, path, server.accessToken)
305 })
306
307 it('Should fail to list with a bad count pagination', async function () {
308 await checkBadCountPagination(server.url, path, server.accessToken)
309 })
310
311 it('Should fail to list with an incorrect sort', async function () {
312 await checkBadSortPagination(server.url, path, server.accessToken)
313 })
314
315 it('Should fail with an invalid state', async function () {
316 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: 42 as any })
317 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ 42 ] as any })
318 })
319
320 it('Should succeed with the correct params', async function () {
321 await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ RunnerJobState.COMPLETED ] })
322 })
323 })
324
325 describe('Delete', function () {
326 let jobUUID: string
327
328 before(async function () {
329 this.timeout(60000)
330
331 await server.videos.quickUpload({ name: 'video' })
332 await waitJobs([ server ])
333
334 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
335 jobUUID = availableJobs[0].uuid
336 })
337
338 it('Should fail without oauth token', async function () {
339 await server.runnerJobs.deleteByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
340 })
341
342 it('Should fail without admin rights', async function () {
343 await server.runnerJobs.deleteByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
344 })
345
346 it('Should fail with a bad job uuid', async function () {
347 await server.runnerJobs.deleteByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
348 })
349
350 it('Should fail with an unknown job uuid', async function () {
351 const jobUUID = badUUID
352 await server.runnerJobs.deleteByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
353 })
354
355 it('Should succeed with the correct params', async function () {
356 await server.runnerJobs.deleteByAdmin({ jobUUID })
357 })
358 })
359
360 })
361
362 describe('Runner jobs by runners', function () {
363 let jobUUID: string
364 let jobToken: string
365 let videoUUID: string
366
367 let jobUUID2: string
368 let jobToken2: string
369
370 let videoUUID2: string
371
372 let pendingUUID: string
373
374 let videoStudioUUID: string
375 let studioFile: string
376
377 let liveAcceptedJob: RunnerJob & { jobToken: string }
378 let studioAcceptedJob: RunnerJob & { jobToken: string }
379
380 async function fetchVideoInputFiles (options: {
381 jobUUID: string
382 videoUUID: string
383 runnerToken: string
384 jobToken: string
385 expectedStatus: HttpStatusCodeType
386 }) {
387 const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken } = options
388
389 const basePath = '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID
390 const paths = [ `${basePath}/max-quality`, `${basePath}/previews/max-quality` ]
391
392 for (const path of paths) {
393 await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
394 }
395 }
396
397 async function fetchStudioFiles (options: {
398 jobUUID: string
399 videoUUID: string
400 runnerToken: string
401 jobToken: string
402 studioFile?: string
403 expectedStatus: HttpStatusCodeType
404 }) {
405 const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken, studioFile } = options
406
407 const path = `/api/v1/runners/jobs/${jobUUID}/files/videos/${videoUUID}/studio/task-files/${studioFile}`
408
409 await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus })
410 }
411
412 before(async function () {
413 this.timeout(120000)
414
415 {
416 await server.runnerJobs.cancelAllJobs({ state: RunnerJobState.PENDING })
417 }
418
419 {
420 const { uuid } = await server.videos.quickUpload({ name: 'video' })
421 videoUUID = uuid
422
423 await waitJobs([ server ])
424
425 const { job } = await server.runnerJobs.autoAccept({ runnerToken })
426 jobUUID = job.uuid
427 jobToken = job.jobToken
428 }
429
430 {
431 const { uuid } = await server.videos.quickUpload({ name: 'video' })
432 videoUUID2 = uuid
433
434 await waitJobs([ server ])
435
436 const { job } = await server.runnerJobs.autoAccept({ runnerToken: runnerToken2 })
437 jobUUID2 = job.uuid
438 jobToken2 = job.jobToken
439 }
440
441 {
442 await server.videos.quickUpload({ name: 'video' })
443 await waitJobs([ server ])
444
445 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
446 pendingUUID = availableJobs[0].uuid
447 }
448
449 {
450 await server.config.disableTranscoding()
451
452 const { uuid } = await server.videos.quickUpload({ name: 'video studio' })
453 videoStudioUUID = uuid
454
455 await server.config.enableTranscoding({ hls: true, webVideo: true })
456 await server.config.enableStudio()
457
458 await server.videoStudio.createEditionTasks({
459 videoId: videoStudioUUID,
460 tasks: VideoStudioCommand.getComplexTask()
461 })
462
463 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'video-studio-transcoding' })
464 studioAcceptedJob = job
465
466 const tasks = (job.payload as RunnerJobStudioTranscodingPayload).tasks
467 const fileUrl = (tasks.find(t => isVideoStudioTaskIntro(t)) as VideoStudioTaskIntro).options.file as string
468 studioFile = basename(fileUrl)
469 }
470
471 {
472 await server.config.enableLive({
473 allowReplay: false,
474 resolutions: 'max',
475 transcoding: true
476 })
477
478 const { live } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
479
480 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
481 await waitJobs([ server ])
482
483 await server.runnerJobs.requestLiveJob(runnerToken)
484
485 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
486 liveAcceptedJob = job
487
488 await stopFfmpeg(ffmpegCommand)
489 }
490 })
491
492 describe('Common runner tokens validations', function () {
493
494 async function testEndpoints (options: {
495 jobUUID: string
496 runnerToken: string
497 jobToken: string
498 expectedStatus: HttpStatusCodeType
499 }) {
500 await server.runnerJobs.abort({ ...options, reason: 'reason' })
501 await server.runnerJobs.update({ ...options })
502 await server.runnerJobs.error({ ...options, message: 'message' })
503 await server.runnerJobs.success({ ...options, payload: { videoFile: 'video_short.mp4' } })
504 }
505
506 it('Should fail with an invalid job uuid', async function () {
507 const options = { jobUUID: 'a', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
508
509 await testEndpoints({ ...options, jobToken })
510 await fetchVideoInputFiles({ ...options, videoUUID, jobToken })
511 await fetchStudioFiles({ ...options, videoUUID, jobToken: studioAcceptedJob.jobToken, studioFile })
512 })
513
514 it('Should fail with an unknown job uuid', async function () {
515 const options = { jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
516
517 await testEndpoints({ ...options, jobToken })
518 await fetchVideoInputFiles({ ...options, videoUUID, jobToken })
519 await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID, studioFile })
520 })
521
522 it('Should fail with an invalid runner token', async function () {
523 const options = { runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
524
525 await testEndpoints({ ...options, jobUUID, jobToken })
526 await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
527 await fetchStudioFiles({
528 ...options,
529 jobToken: studioAcceptedJob.jobToken,
530 jobUUID: studioAcceptedJob.uuid,
531 videoUUID: videoStudioUUID,
532 studioFile
533 })
534 })
535
536 it('Should fail with an unknown runner token', async function () {
537 const options = { runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
538
539 await testEndpoints({ ...options, jobUUID, jobToken })
540 await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
541 await fetchStudioFiles({
542 ...options,
543 jobToken: studioAcceptedJob.jobToken,
544 jobUUID: studioAcceptedJob.uuid,
545 videoUUID: videoStudioUUID,
546 studioFile
547 })
548 })
549
550 it('Should fail with an invalid job token job uuid', async function () {
551 const options = { runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }
552
553 await testEndpoints({ ...options, jobUUID })
554 await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
555 await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
556 })
557
558 it('Should fail with an unknown job token job uuid', async function () {
559 const options = { runnerToken, jobToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
560
561 await testEndpoints({ ...options, jobUUID })
562 await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
563 await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
564 })
565
566 it('Should fail with a runner token not associated to this job', async function () {
567 const options = { runnerToken: runnerToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
568
569 await testEndpoints({ ...options, jobUUID, jobToken })
570 await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken })
571 await fetchStudioFiles({
572 ...options,
573 jobToken: studioAcceptedJob.jobToken,
574 jobUUID: studioAcceptedJob.uuid,
575 videoUUID: videoStudioUUID,
576 studioFile
577 })
578 })
579
580 it('Should fail with a job uuid not associated to the job token', async function () {
581 {
582 const options = { jobUUID: jobUUID2, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
583
584 await testEndpoints({ ...options, jobToken })
585 await fetchVideoInputFiles({ ...options, jobToken, videoUUID })
586 await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID: videoStudioUUID, studioFile })
587 }
588
589 {
590 const options = { runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 }
591
592 await testEndpoints({ ...options, jobUUID })
593 await fetchVideoInputFiles({ ...options, jobUUID, videoUUID })
594 await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile })
595 }
596 })
597 })
598
599 describe('Unregister', function () {
600
601 it('Should fail without a runner token', async function () {
602 await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
603 })
604
605 it('Should fail with a bad a runner token', async function () {
606 await server.runners.unregister({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
607 })
608
609 it('Should fail with an unknown runner token', async function () {
610 await server.runners.unregister({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
611 })
612 })
613
614 describe('Request', function () {
615
616 it('Should fail without a runner token', async function () {
617 await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
618 })
619
620 it('Should fail with a bad a runner token', async function () {
621 await server.runnerJobs.request({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
622 })
623
624 it('Should fail with an unknown runner token', async function () {
625 await server.runnerJobs.request({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
626 })
627 })
628
629 describe('Accept', function () {
630
631 it('Should fail with a bad a job uuid', async function () {
632 await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
633 })
634
635 it('Should fail with an unknown job uuid', async function () {
636 await server.runnerJobs.accept({ jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
637 })
638
639 it('Should fail with a job not in pending state', async function () {
640 await server.runnerJobs.accept({ jobUUID: completedJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
641 await server.runnerJobs.accept({ jobUUID: cancelledJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
642 })
643
644 it('Should fail without a runner token', async function () {
645 await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
646 })
647
648 it('Should fail with a bad a runner token', async function () {
649 await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
650 })
651
652 it('Should fail with an unknown runner token', async function () {
653 await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
654 })
655 })
656
657 describe('Abort', function () {
658
659 it('Should fail without a reason', async function () {
660 await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
661 })
662
663 it('Should fail with a bad reason', async function () {
664 const reason = 'reason'.repeat(5000)
665 await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
666 })
667
668 it('Should fail with a job not in processing state', async function () {
669 await server.runnerJobs.abort({
670 jobUUID: completedJobUUID,
671 jobToken: completedJobToken,
672 runnerToken,
673 reason: 'reason',
674 expectedStatus: HttpStatusCode.BAD_REQUEST_400
675 })
676 })
677 })
678
679 describe('Update', function () {
680
681 describe('Common', function () {
682
683 it('Should fail with an invalid progress', async function () {
684 await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
685 })
686
687 it('Should fail with a job not in processing state', async function () {
688 await server.runnerJobs.update({
689 jobUUID: cancelledJobUUID,
690 jobToken: cancelledJobToken,
691 runnerToken,
692 expectedStatus: HttpStatusCode.NOT_FOUND_404
693 })
694 })
695 })
696
697 describe('Live RTMP to HLS', function () {
698 const base: RunnerJobUpdatePayload = {
699 masterPlaylistFile: 'live/master.m3u8',
700 resolutionPlaylistFilename: '0.m3u8',
701 resolutionPlaylistFile: 'live/1.m3u8',
702 type: 'add-chunk',
703 videoChunkFile: 'live/1-000069.ts',
704 videoChunkFilename: '1-000068.ts'
705 }
706
707 function testUpdate (payload: RunnerJobUpdatePayload) {
708 return server.runnerJobs.update({
709 jobUUID: liveAcceptedJob.uuid,
710 jobToken: liveAcceptedJob.jobToken,
711 payload,
712 runnerToken,
713 expectedStatus: HttpStatusCode.BAD_REQUEST_400
714 })
715 }
716
717 it('Should fail with an invalid resolutionPlaylistFilename', async function () {
718 await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
719 await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
720 await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
721 })
722
723 it('Should fail with an invalid videoChunkFilename', async function () {
724 await testUpdate({ ...base, resolutionPlaylistFilename: undefined })
725 await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' })
726 await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' })
727 })
728
729 it('Should fail with an invalid type', async function () {
730 await testUpdate({ ...base, type: undefined })
731 await testUpdate({ ...base, type: 'toto' as any })
732 })
733 })
734 })
735
736 describe('Error', function () {
737
738 it('Should fail with a missing error message', async function () {
739 await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
740 })
741
742 it('Should fail with an invalid error messgae', async function () {
743 const message = 'a'.repeat(6000)
744 await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
745 })
746
747 it('Should fail with a job not in processing state', async function () {
748 await server.runnerJobs.error({
749 jobUUID: completedJobUUID,
750 jobToken: completedJobToken,
751 message: 'my message',
752 runnerToken,
753 expectedStatus: HttpStatusCode.BAD_REQUEST_400
754 })
755 })
756 })
757
758 describe('Success', function () {
759 let vodJobUUID: string
760 let vodJobToken: string
761
762 describe('Common', function () {
763
764 it('Should fail with a job not in processing state', async function () {
765 await server.runnerJobs.success({
766 jobUUID: completedJobUUID,
767 jobToken: completedJobToken,
768 payload: { videoFile: 'video_short.mp4' },
769 runnerToken,
770 expectedStatus: HttpStatusCode.BAD_REQUEST_400
771 })
772 })
773 })
774
775 describe('VOD', function () {
776
777 it('Should fail with an invalid vod web video payload', async function () {
778 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' })
779
780 await server.runnerJobs.success({
781 jobUUID: job.uuid,
782 jobToken: job.jobToken,
783 payload: { hello: 'video_short.mp4' } as any,
784 runnerToken,
785 expectedStatus: HttpStatusCode.BAD_REQUEST_400
786 })
787
788 vodJobUUID = job.uuid
789 vodJobToken = job.jobToken
790 })
791
792 it('Should fail with an invalid vod hls payload', async function () {
793 // To create HLS jobs
794 const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' }
795 await server.runnerJobs.success({ runnerToken, jobUUID: vodJobUUID, jobToken: vodJobToken, payload })
796
797 await waitJobs([ server ])
798
799 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-hls-transcoding' })
800
801 await server.runnerJobs.success({
802 jobUUID: job.uuid,
803 jobToken: job.jobToken,
804 payload: { videoFile: 'video_short.mp4' } as any,
805 runnerToken,
806 expectedStatus: HttpStatusCode.BAD_REQUEST_400
807 })
808 })
809
810 it('Should fail with an invalid vod audio merge payload', async function () {
811 const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
812 await server.videos.upload({ attributes, mode: 'legacy' })
813
814 await waitJobs([ server ])
815
816 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-audio-merge-transcoding' })
817
818 await server.runnerJobs.success({
819 jobUUID: job.uuid,
820 jobToken: job.jobToken,
821 payload: { hello: 'video_short.mp4' } as any,
822 runnerToken,
823 expectedStatus: HttpStatusCode.BAD_REQUEST_400
824 })
825 })
826 })
827
828 describe('Video studio', function () {
829
830 it('Should fail with an invalid video studio transcoding payload', async function () {
831 await server.runnerJobs.success({
832 jobUUID: studioAcceptedJob.uuid,
833 jobToken: studioAcceptedJob.jobToken,
834 payload: { hello: 'video_short.mp4' } as any,
835 runnerToken,
836 expectedStatus: HttpStatusCode.BAD_REQUEST_400
837 })
838 })
839 })
840 })
841
842 describe('Job files', function () {
843
844 describe('Check video param for common job file routes', function () {
845
846 async function fetchFiles (options: {
847 videoUUID?: string
848 expectedStatus: HttpStatusCodeType
849 }) {
850 await fetchVideoInputFiles({ videoUUID, ...options, jobToken, jobUUID, runnerToken })
851
852 await fetchStudioFiles({
853 videoUUID: videoStudioUUID,
854
855 ...options,
856
857 jobToken: studioAcceptedJob.jobToken,
858 jobUUID: studioAcceptedJob.uuid,
859 runnerToken,
860 studioFile
861 })
862 }
863
864 it('Should fail with an invalid video id', async function () {
865 await fetchFiles({
866 videoUUID: 'a',
867 expectedStatus: HttpStatusCode.BAD_REQUEST_400
868 })
869 })
870
871 it('Should fail with an unknown video id', async function () {
872 const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464'
873
874 await fetchFiles({
875 videoUUID,
876 expectedStatus: HttpStatusCode.NOT_FOUND_404
877 })
878 })
879
880 it('Should fail with a video id not associated to this job', async function () {
881 await fetchFiles({
882 videoUUID: videoUUID2,
883 expectedStatus: HttpStatusCode.FORBIDDEN_403
884 })
885 })
886
887 it('Should succeed with the correct params', async function () {
888 await fetchFiles({ expectedStatus: HttpStatusCode.OK_200 })
889 })
890 })
891
892 describe('Video studio tasks file routes', function () {
893
894 it('Should fail with an invalid studio filename', async function () {
895 await fetchStudioFiles({
896 videoUUID: videoStudioUUID,
897 jobUUID: studioAcceptedJob.uuid,
898 runnerToken,
899 jobToken: studioAcceptedJob.jobToken,
900 studioFile: 'toto',
901 expectedStatus: HttpStatusCode.BAD_REQUEST_400
902 })
903 })
904 })
905 })
906 })
907
908 after(async function () {
909 await cleanupTests([ server ])
910 })
911})
diff --git a/packages/tests/src/api/check-params/search.ts b/packages/tests/src/api/check-params/search.ts
new file mode 100644
index 000000000..b886cbc82
--- /dev/null
+++ b/packages/tests/src/api/check-params/search.ts
@@ -0,0 +1,278 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode } from '@peertube/peertube-models'
4import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeGetRequest,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12
13function updateSearchIndex (server: PeerTubeServer, enabled: boolean, disableLocalSearch = false) {
14 return server.config.updateCustomSubConfig({
15 newConfig: {
16 search: {
17 searchIndex: {
18 enabled,
19 disableLocalSearch
20 }
21 }
22 }
23 })
24}
25
26describe('Test videos API validator', function () {
27 let server: PeerTubeServer
28
29 // ---------------------------------------------------------------
30
31 before(async function () {
32 this.timeout(30000)
33
34 server = await createSingleServer(1)
35 await setAccessTokensToServers([ server ])
36 })
37
38 describe('When searching videos', function () {
39 const path = '/api/v1/search/videos/'
40
41 const query = {
42 search: 'coucou'
43 }
44
45 it('Should fail with a bad start pagination', async function () {
46 await checkBadStartPagination(server.url, path, null, query)
47 })
48
49 it('Should fail with a bad count pagination', async function () {
50 await checkBadCountPagination(server.url, path, null, query)
51 })
52
53 it('Should fail with an incorrect sort', async function () {
54 await checkBadSortPagination(server.url, path, null, query)
55 })
56
57 it('Should succeed with the correct parameters', async function () {
58 await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
59 })
60
61 it('Should fail with an invalid category', async function () {
62 const customQuery1 = { ...query, categoryOneOf: [ 'aa', 'b' ] }
63 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
64
65 const customQuery2 = { ...query, categoryOneOf: 'a' }
66 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
67 })
68
69 it('Should succeed with a valid category', async function () {
70 const customQuery1 = { ...query, categoryOneOf: [ 1, 7 ] }
71 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
72
73 const customQuery2 = { ...query, categoryOneOf: 1 }
74 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
75 })
76
77 it('Should fail with an invalid licence', async function () {
78 const customQuery1 = { ...query, licenceOneOf: [ 'aa', 'b' ] }
79 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
80
81 const customQuery2 = { ...query, licenceOneOf: 'a' }
82 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
83 })
84
85 it('Should succeed with a valid licence', async function () {
86 const customQuery1 = { ...query, licenceOneOf: [ 1, 2 ] }
87 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
88
89 const customQuery2 = { ...query, licenceOneOf: 1 }
90 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
91 })
92
93 it('Should succeed with a valid language', async function () {
94 const customQuery1 = { ...query, languageOneOf: [ 'fr', 'en' ] }
95 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
96
97 const customQuery2 = { ...query, languageOneOf: 'fr' }
98 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
99 })
100
101 it('Should succeed with valid tags', async function () {
102 const customQuery1 = { ...query, tagsOneOf: [ 'tag1', 'tag2' ] }
103 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 })
104
105 const customQuery2 = { ...query, tagsOneOf: 'tag1' }
106 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 })
107
108 const customQuery3 = { ...query, tagsAllOf: [ 'tag1', 'tag2' ] }
109 await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.OK_200 })
110
111 const customQuery4 = { ...query, tagsAllOf: 'tag1' }
112 await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.OK_200 })
113 })
114
115 it('Should fail with invalid durations', async function () {
116 const customQuery1 = { ...query, durationMin: 'hello' }
117 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
118
119 const customQuery2 = { ...query, durationMax: 'hello' }
120 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
121 })
122
123 it('Should fail with invalid dates', async function () {
124 const customQuery1 = { ...query, startDate: 'hello' }
125 await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
126
127 const customQuery2 = { ...query, endDate: 'hello' }
128 await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
129
130 const customQuery3 = { ...query, originallyPublishedStartDate: 'hello' }
131 await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
132
133 const customQuery4 = { ...query, originallyPublishedEndDate: 'hello' }
134 await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
135 })
136
137 it('Should fail with an invalid host', async function () {
138 const customQuery = { ...query, host: '6565' }
139 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
140 })
141
142 it('Should succeed with a host', async function () {
143 const customQuery = { ...query, host: 'example.com' }
144 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
145 })
146
147 it('Should fail with invalid uuids', async function () {
148 const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
149 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
150 })
151
152 it('Should succeed with valid uuids', async function () {
153 const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
154 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
155 })
156 })
157
158 describe('When searching video playlists', function () {
159 const path = '/api/v1/search/video-playlists/'
160
161 const query = {
162 search: 'coucou',
163 host: 'example.com'
164 }
165
166 it('Should fail with a bad start pagination', async function () {
167 await checkBadStartPagination(server.url, path, null, query)
168 })
169
170 it('Should fail with a bad count pagination', async function () {
171 await checkBadCountPagination(server.url, path, null, query)
172 })
173
174 it('Should fail with an incorrect sort', async function () {
175 await checkBadSortPagination(server.url, path, null, query)
176 })
177
178 it('Should fail with an invalid host', async function () {
179 await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
180 })
181
182 it('Should fail with invalid uuids', async function () {
183 const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
184 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
185 })
186
187 it('Should succeed with the correct parameters', async function () {
188 await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
189 })
190 })
191
192 describe('When searching video channels', function () {
193 const path = '/api/v1/search/video-channels/'
194
195 const query = {
196 search: 'coucou',
197 host: 'example.com'
198 }
199
200 it('Should fail with a bad start pagination', async function () {
201 await checkBadStartPagination(server.url, path, null, query)
202 })
203
204 it('Should fail with a bad count pagination', async function () {
205 await checkBadCountPagination(server.url, path, null, query)
206 })
207
208 it('Should fail with an incorrect sort', async function () {
209 await checkBadSortPagination(server.url, path, null, query)
210 })
211
212 it('Should fail with an invalid host', async function () {
213 await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
214 })
215
216 it('Should fail with invalid handles', async function () {
217 await makeGetRequest({ url: server.url, path, query: { ...query, handles: [ '' ] }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
218 })
219
220 it('Should succeed with the correct parameters', async function () {
221 await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
222 })
223 })
224
225 describe('Search target', function () {
226
227 it('Should fail/succeed depending on the search target', async function () {
228 const query = { search: 'coucou' }
229 const paths = [
230 '/api/v1/search/video-playlists/',
231 '/api/v1/search/video-channels/',
232 '/api/v1/search/videos/'
233 ]
234
235 for (const path of paths) {
236 {
237 const customQuery = { ...query, searchTarget: 'hello' }
238 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
239 }
240
241 {
242 const customQuery = { ...query, searchTarget: undefined }
243 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
244 }
245
246 {
247 const customQuery = { ...query, searchTarget: 'local' }
248 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
249 }
250
251 {
252 const customQuery = { ...query, searchTarget: 'search-index' }
253 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
254 }
255
256 await updateSearchIndex(server, true, true)
257
258 {
259 const customQuery = { ...query, searchTarget: 'search-index' }
260 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
261 }
262
263 await updateSearchIndex(server, true, false)
264
265 {
266 const customQuery = { ...query, searchTarget: 'local' }
267 await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
268 }
269
270 await updateSearchIndex(server, false, false)
271 }
272 })
273 })
274
275 after(async function () {
276 await cleanupTests([ server ])
277 })
278})
diff --git a/packages/tests/src/api/check-params/services.ts b/packages/tests/src/api/check-params/services.ts
new file mode 100644
index 000000000..0b0466d84
--- /dev/null
+++ b/packages/tests/src/api/check-params/services.ts
@@ -0,0 +1,207 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import {
4 HttpStatusCode,
5 HttpStatusCodeType,
6 VideoCreateResult,
7 VideoPlaylistCreateResult,
8 VideoPlaylistPrivacy,
9 VideoPrivacy
10} from '@peertube/peertube-models'
11import {
12 cleanupTests,
13 createSingleServer,
14 makeGetRequest,
15 PeerTubeServer,
16 setAccessTokensToServers,
17 setDefaultVideoChannel
18} from '@peertube/peertube-server-commands'
19
20describe('Test services API validators', function () {
21 let server: PeerTubeServer
22 let playlistUUID: string
23
24 let privateVideo: VideoCreateResult
25 let unlistedVideo: VideoCreateResult
26
27 let privatePlaylist: VideoPlaylistCreateResult
28 let unlistedPlaylist: VideoPlaylistCreateResult
29
30 // ---------------------------------------------------------------
31
32 before(async function () {
33 this.timeout(60000)
34
35 server = await createSingleServer(1)
36 await setAccessTokensToServers([ server ])
37 await setDefaultVideoChannel([ server ])
38
39 server.store.videoCreated = await server.videos.upload({ attributes: { name: 'my super name' } })
40
41 privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })
42 unlistedVideo = await server.videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED })
43
44 {
45 const created = await server.playlists.create({
46 attributes: {
47 displayName: 'super playlist',
48 privacy: VideoPlaylistPrivacy.PUBLIC,
49 videoChannelId: server.store.channel.id
50 }
51 })
52
53 playlistUUID = created.uuid
54
55 privatePlaylist = await server.playlists.create({
56 attributes: {
57 displayName: 'private',
58 privacy: VideoPlaylistPrivacy.PRIVATE,
59 videoChannelId: server.store.channel.id
60 }
61 })
62
63 unlistedPlaylist = await server.playlists.create({
64 attributes: {
65 displayName: 'unlisted',
66 privacy: VideoPlaylistPrivacy.UNLISTED,
67 videoChannelId: server.store.channel.id
68 }
69 })
70 }
71 })
72
73 describe('Test oEmbed API validators', function () {
74
75 it('Should fail with an invalid url', async function () {
76 const embedUrl = 'hello.com'
77 await checkParamEmbed(server, embedUrl)
78 })
79
80 it('Should fail with an invalid host', async function () {
81 const embedUrl = 'http://hello.com/videos/watch/' + server.store.videoCreated.uuid
82 await checkParamEmbed(server, embedUrl)
83 })
84
85 it('Should fail with an invalid element id', async function () {
86 const embedUrl = `${server.url}/videos/watch/blabla`
87 await checkParamEmbed(server, embedUrl)
88 })
89
90 it('Should fail with an unknown element', async function () {
91 const embedUrl = `${server.url}/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c`
92 await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_FOUND_404)
93 })
94
95 it('Should fail with an invalid path', async function () {
96 const embedUrl = `${server.url}/videos/watchs/${server.store.videoCreated.uuid}`
97
98 await checkParamEmbed(server, embedUrl)
99 })
100
101 it('Should fail with an invalid max height', async function () {
102 const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
103
104 await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxheight: 'hello' })
105 })
106
107 it('Should fail with an invalid max width', async function () {
108 const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
109
110 await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxwidth: 'hello' })
111 })
112
113 it('Should fail with an invalid format', async function () {
114 const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
115
116 await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { format: 'blabla' })
117 })
118
119 it('Should fail with a non supported format', async function () {
120 const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
121
122 await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_IMPLEMENTED_501, { format: 'xml' })
123 })
124
125 it('Should fail with a private video', async function () {
126 const embedUrl = `${server.url}/videos/watch/${privateVideo.uuid}`
127
128 await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
129 })
130
131 it('Should fail with an unlisted video with the int id', async function () {
132 const embedUrl = `${server.url}/videos/watch/${unlistedVideo.id}`
133
134 await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
135 })
136
137 it('Should succeed with an unlisted video using the uuid id', async function () {
138 for (const uuid of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) {
139 const embedUrl = `${server.url}/videos/watch/${uuid}`
140
141 await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200)
142 }
143 })
144
145 it('Should fail with a private playlist', async function () {
146 const embedUrl = `${server.url}/videos/watch/playlist/${privatePlaylist.uuid}`
147
148 await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
149 })
150
151 it('Should fail with an unlisted playlist using the int id', async function () {
152 const embedUrl = `${server.url}/videos/watch/playlist/${unlistedPlaylist.id}`
153
154 await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403)
155 })
156
157 it('Should succeed with an unlisted playlist using the uuid id', async function () {
158 for (const uuid of [ unlistedPlaylist.uuid, unlistedPlaylist.shortUUID ]) {
159 const embedUrl = `${server.url}/videos/watch/playlist/${uuid}`
160
161 await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200)
162 }
163 })
164
165 it('Should succeed with the correct params with a video', async function () {
166 const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}`
167 const query = {
168 format: 'json',
169 maxheight: 400,
170 maxwidth: 400
171 }
172
173 await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query)
174 })
175
176 it('Should succeed with the correct params with a playlist', async function () {
177 const embedUrl = `${server.url}/videos/watch/playlist/${playlistUUID}`
178 const query = {
179 format: 'json',
180 maxheight: 400,
181 maxwidth: 400
182 }
183
184 await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query)
185 })
186 })
187
188 after(async function () {
189 await cleanupTests([ server ])
190 })
191})
192
193function checkParamEmbed (
194 server: PeerTubeServer,
195 embedUrl: string,
196 expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400,
197 query = {}
198) {
199 const path = '/services/oembed'
200
201 return makeGetRequest({
202 url: server.url,
203 path,
204 query: Object.assign(query, { url: embedUrl }),
205 expectedStatus
206 })
207}
diff --git a/packages/tests/src/api/check-params/transcoding.ts b/packages/tests/src/api/check-params/transcoding.ts
new file mode 100644
index 000000000..50935c59e
--- /dev/null
+++ b/packages/tests/src/api/check-params/transcoding.ts
@@ -0,0 +1,112 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode, UserRole } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createMultipleServers,
7 doubleFollow,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 waitJobs
11} from '@peertube/peertube-server-commands'
12
13describe('Test transcoding API validators', function () {
14 let servers: PeerTubeServer[]
15
16 let userToken: string
17 let moderatorToken: string
18
19 let remoteId: string
20 let validId: string
21
22 // ---------------------------------------------------------------
23
24 before(async function () {
25 this.timeout(120000)
26
27 servers = await createMultipleServers(2)
28 await setAccessTokensToServers(servers)
29
30 await doubleFollow(servers[0], servers[1])
31
32 userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER)
33 moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR)
34
35 {
36 const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' })
37 remoteId = uuid
38 }
39
40 {
41 const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' })
42 validId = uuid
43 }
44
45 await waitJobs(servers)
46
47 await servers[0].config.enableTranscoding()
48 })
49
50 it('Should not run transcoding of a unknown video', async function () {
51 await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
52 await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'web-video', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
53 })
54
55 it('Should not run transcoding of a remote video', async function () {
56 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
57
58 await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus })
59 await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'web-video', expectedStatus })
60 })
61
62 it('Should not run transcoding by a non admin user', async function () {
63 const expectedStatus = HttpStatusCode.FORBIDDEN_403
64
65 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus })
66 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', token: moderatorToken, expectedStatus })
67 })
68
69 it('Should not run transcoding without transcoding type', async function () {
70 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
71 })
72
73 it('Should not run transcoding with an incorrect transcoding type', async function () {
74 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
75
76 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'toto' as any, expectedStatus })
77 })
78
79 it('Should not run transcoding if the instance disabled it', async function () {
80 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
81
82 await servers[0].config.disableTranscoding()
83
84 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus })
85 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus })
86 })
87
88 it('Should run transcoding', async function () {
89 this.timeout(120_000)
90
91 await servers[0].config.enableTranscoding()
92
93 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' })
94 await waitJobs(servers)
95
96 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
97 await waitJobs(servers)
98 })
99
100 it('Should not run transcoding on a video that is already being transcoded if forceTranscoding is not set', async function () {
101 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' })
102
103 const expectedStatus = HttpStatusCode.CONFLICT_409
104 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus })
105
106 await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true })
107 })
108
109 after(async function () {
110 await cleanupTests(servers)
111 })
112})
diff --git a/packages/tests/src/api/check-params/two-factor.ts b/packages/tests/src/api/check-params/two-factor.ts
new file mode 100644
index 000000000..0b1766eca
--- /dev/null
+++ b/packages/tests/src/api/check-params/two-factor.ts
@@ -0,0 +1,294 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createSingleServer,
7 PeerTubeServer,
8 setAccessTokensToServers,
9 TwoFactorCommand
10} from '@peertube/peertube-server-commands'
11
12describe('Test two factor API validators', function () {
13 let server: PeerTubeServer
14
15 let rootId: number
16 let rootPassword: string
17 let rootRequestToken: string
18 let rootOTPToken: string
19
20 let userId: number
21 let userToken = ''
22 let userPassword: string
23 let userRequestToken: string
24 let userOTPToken: string
25
26 // ---------------------------------------------------------------
27
28 before(async function () {
29 this.timeout(30000)
30
31 {
32 server = await createSingleServer(1)
33 await setAccessTokensToServers([ server ])
34 }
35
36 {
37 const result = await server.users.generate('user1')
38 userToken = result.token
39 userId = result.userId
40 userPassword = result.password
41 }
42
43 {
44 const { id } = await server.users.getMyInfo()
45 rootId = id
46 rootPassword = server.store.user.password
47 }
48 })
49
50 describe('When requesting two factor', function () {
51
52 it('Should fail with an unknown user id', async function () {
53 await server.twoFactor.request({ userId: 42, currentPassword: rootPassword, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
54 })
55
56 it('Should fail with an invalid user id', async function () {
57 await server.twoFactor.request({
58 userId: 'invalid' as any,
59 currentPassword: rootPassword,
60 expectedStatus: HttpStatusCode.BAD_REQUEST_400
61 })
62 })
63
64 it('Should fail to request another user two factor without the appropriate rights', async function () {
65 await server.twoFactor.request({
66 userId: rootId,
67 token: userToken,
68 currentPassword: userPassword,
69 expectedStatus: HttpStatusCode.FORBIDDEN_403
70 })
71 })
72
73 it('Should succeed to request another user two factor with the appropriate rights', async function () {
74 await server.twoFactor.request({ userId, currentPassword: rootPassword })
75 })
76
77 it('Should fail to request two factor without a password', async function () {
78 await server.twoFactor.request({
79 userId,
80 token: userToken,
81 currentPassword: undefined,
82 expectedStatus: HttpStatusCode.BAD_REQUEST_400
83 })
84 })
85
86 it('Should fail to request two factor with an incorrect password', async function () {
87 await server.twoFactor.request({
88 userId,
89 token: userToken,
90 currentPassword: rootPassword,
91 expectedStatus: HttpStatusCode.FORBIDDEN_403
92 })
93 })
94
95 it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () {
96 await server.twoFactor.request({ userId })
97 })
98
99 it('Should fail to request two factor without a password when targeting myself with an admin account', async function () {
100 await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
101 await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
102 })
103
104 it('Should succeed to request my two factor auth', async function () {
105 {
106 const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
107 userRequestToken = otpRequest.requestToken
108 userOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
109 }
110
111 {
112 const { otpRequest } = await server.twoFactor.request({ userId: rootId, currentPassword: rootPassword })
113 rootRequestToken = otpRequest.requestToken
114 rootOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
115 }
116 })
117 })
118
119 describe('When confirming two factor request', function () {
120
121 it('Should fail with an unknown user id', async function () {
122 await server.twoFactor.confirmRequest({
123 userId: 42,
124 requestToken: rootRequestToken,
125 otpToken: rootOTPToken,
126 expectedStatus: HttpStatusCode.NOT_FOUND_404
127 })
128 })
129
130 it('Should fail with an invalid user id', async function () {
131 await server.twoFactor.confirmRequest({
132 userId: 'invalid' as any,
133 requestToken: rootRequestToken,
134 otpToken: rootOTPToken,
135 expectedStatus: HttpStatusCode.BAD_REQUEST_400
136 })
137 })
138
139 it('Should fail to confirm another user two factor request without the appropriate rights', async function () {
140 await server.twoFactor.confirmRequest({
141 userId: rootId,
142 token: userToken,
143 requestToken: rootRequestToken,
144 otpToken: rootOTPToken,
145 expectedStatus: HttpStatusCode.FORBIDDEN_403
146 })
147 })
148
149 it('Should fail without request token', async function () {
150 await server.twoFactor.confirmRequest({
151 userId,
152 requestToken: undefined,
153 otpToken: userOTPToken,
154 expectedStatus: HttpStatusCode.BAD_REQUEST_400
155 })
156 })
157
158 it('Should fail with an invalid request token', async function () {
159 await server.twoFactor.confirmRequest({
160 userId,
161 requestToken: 'toto',
162 otpToken: userOTPToken,
163 expectedStatus: HttpStatusCode.FORBIDDEN_403
164 })
165 })
166
167 it('Should fail with request token of another user', async function () {
168 await server.twoFactor.confirmRequest({
169 userId,
170 requestToken: rootRequestToken,
171 otpToken: userOTPToken,
172 expectedStatus: HttpStatusCode.FORBIDDEN_403
173 })
174 })
175
176 it('Should fail without an otp token', async function () {
177 await server.twoFactor.confirmRequest({
178 userId,
179 requestToken: userRequestToken,
180 otpToken: undefined,
181 expectedStatus: HttpStatusCode.BAD_REQUEST_400
182 })
183 })
184
185 it('Should fail with a bad otp token', async function () {
186 await server.twoFactor.confirmRequest({
187 userId,
188 requestToken: userRequestToken,
189 otpToken: '123456',
190 expectedStatus: HttpStatusCode.FORBIDDEN_403
191 })
192 })
193
194 it('Should succeed to confirm another user two factor request with the appropriate rights', async function () {
195 await server.twoFactor.confirmRequest({
196 userId,
197 requestToken: userRequestToken,
198 otpToken: userOTPToken
199 })
200
201 // Reinit
202 await server.twoFactor.disable({ userId, currentPassword: rootPassword })
203 })
204
205 it('Should succeed to confirm my two factor request', async function () {
206 await server.twoFactor.confirmRequest({
207 userId,
208 token: userToken,
209 requestToken: userRequestToken,
210 otpToken: userOTPToken
211 })
212 })
213
214 it('Should fail to confirm again two factor request', async function () {
215 await server.twoFactor.confirmRequest({
216 userId,
217 token: userToken,
218 requestToken: userRequestToken,
219 otpToken: userOTPToken,
220 expectedStatus: HttpStatusCode.BAD_REQUEST_400
221 })
222 })
223 })
224
225 describe('When disabling two factor', function () {
226
227 it('Should fail with an unknown user id', async function () {
228 await server.twoFactor.disable({
229 userId: 42,
230 currentPassword: rootPassword,
231 expectedStatus: HttpStatusCode.NOT_FOUND_404
232 })
233 })
234
235 it('Should fail with an invalid user id', async function () {
236 await server.twoFactor.disable({
237 userId: 'invalid' as any,
238 currentPassword: rootPassword,
239 expectedStatus: HttpStatusCode.BAD_REQUEST_400
240 })
241 })
242
243 it('Should fail to disable another user two factor without the appropriate rights', async function () {
244 await server.twoFactor.disable({
245 userId: rootId,
246 token: userToken,
247 currentPassword: userPassword,
248 expectedStatus: HttpStatusCode.FORBIDDEN_403
249 })
250 })
251
252 it('Should fail to disable two factor with an incorrect password', async function () {
253 await server.twoFactor.disable({
254 userId,
255 token: userToken,
256 currentPassword: rootPassword,
257 expectedStatus: HttpStatusCode.FORBIDDEN_403
258 })
259 })
260
261 it('Should succeed to disable two factor without a password when targeting a remote user with an admin account', async function () {
262 await server.twoFactor.disable({ userId })
263 await server.twoFactor.requestAndConfirm({ userId })
264 })
265
266 it('Should fail to disable two factor without a password when targeting myself with an admin account', async function () {
267 await server.twoFactor.disable({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
268 await server.twoFactor.disable({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
269 })
270
271 it('Should succeed to disable another user two factor with the appropriate rights', async function () {
272 await server.twoFactor.disable({ userId, currentPassword: rootPassword })
273
274 await server.twoFactor.requestAndConfirm({ userId })
275 })
276
277 it('Should succeed to update my two factor auth', async function () {
278 await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword })
279 })
280
281 it('Should fail to disable again two factor', async function () {
282 await server.twoFactor.disable({
283 userId,
284 token: userToken,
285 currentPassword: userPassword,
286 expectedStatus: HttpStatusCode.BAD_REQUEST_400
287 })
288 })
289 })
290
291 after(async function () {
292 await cleanupTests([ server ])
293 })
294})
diff --git a/packages/tests/src/api/check-params/upload-quota.ts b/packages/tests/src/api/check-params/upload-quota.ts
new file mode 100644
index 000000000..a77792822
--- /dev/null
+++ b/packages/tests/src/api/check-params/upload-quota.ts
@@ -0,0 +1,134 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FIXTURE_URLS } from '@tests/shared/tests.js'
5import { randomInt } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, VideoImportState, VideoPrivacy } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 createSingleServer,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 VideosCommand,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test upload quota', function () {
18 let server: PeerTubeServer
19 let rootId: number
20 let command: VideosCommand
21
22 // ---------------------------------------------------------------
23
24 before(async function () {
25 this.timeout(30000)
26
27 server = await createSingleServer(1)
28 await setAccessTokensToServers([ server ])
29 await setDefaultVideoChannel([ server ])
30
31 const user = await server.users.getMyInfo()
32 rootId = user.id
33
34 await server.users.update({ userId: rootId, videoQuota: 42 })
35
36 command = server.videos
37 })
38
39 describe('When having a video quota', function () {
40
41 it('Should fail with a registered user having too many videos with legacy upload', async function () {
42 this.timeout(120000)
43
44 const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
45 await server.registrations.register(user)
46 const userToken = await server.login.getAccessToken(user)
47
48 const attributes = { fixture: 'video_short2.webm' }
49 for (let i = 0; i < 5; i++) {
50 await command.upload({ token: userToken, attributes })
51 }
52
53 await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
54 })
55
56 it('Should fail with a registered user having too many videos with resumable upload', async function () {
57 this.timeout(120000)
58
59 const user = { username: 'registered' + randomInt(1, 1500), password: 'password' }
60 await server.registrations.register(user)
61 const userToken = await server.login.getAccessToken(user)
62
63 const attributes = { fixture: 'video_short2.webm' }
64 for (let i = 0; i < 5; i++) {
65 await command.upload({ token: userToken, attributes })
66 }
67
68 await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
69 })
70
71 it('Should fail to import with HTTP/Torrent/magnet', async function () {
72 this.timeout(120_000)
73
74 const baseAttributes = {
75 channelId: server.store.channel.id,
76 privacy: VideoPrivacy.PUBLIC
77 }
78 await server.imports.importVideo({ attributes: { ...baseAttributes, targetUrl: FIXTURE_URLS.goodVideo } })
79 await server.imports.importVideo({ attributes: { ...baseAttributes, magnetUri: FIXTURE_URLS.magnet } })
80 await server.imports.importVideo({ attributes: { ...baseAttributes, torrentfile: 'video-720p.torrent' as any } })
81
82 await waitJobs([ server ])
83
84 const { total, data: videoImports } = await server.imports.getMyVideoImports()
85 expect(total).to.equal(3)
86
87 expect(videoImports).to.have.lengthOf(3)
88
89 for (const videoImport of videoImports) {
90 expect(videoImport.state.id).to.equal(VideoImportState.FAILED)
91 expect(videoImport.error).not.to.be.undefined
92 expect(videoImport.error).to.contain('user video quota is exceeded')
93 }
94 })
95 })
96
97 describe('When having a daily video quota', function () {
98
99 it('Should fail with a user having too many videos daily', async function () {
100 await server.users.update({ userId: rootId, videoQuotaDaily: 42 })
101
102 await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
103 await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
104 })
105 })
106
107 describe('When having an absolute and daily video quota', function () {
108 it('Should fail if exceeding total quota', async function () {
109 await server.users.update({
110 userId: rootId,
111 videoQuota: 42,
112 videoQuotaDaily: 1024 * 1024 * 1024
113 })
114
115 await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
116 await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
117 })
118
119 it('Should fail if exceeding daily quota', async function () {
120 await server.users.update({
121 userId: rootId,
122 videoQuota: 1024 * 1024 * 1024,
123 videoQuotaDaily: 42
124 })
125
126 await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' })
127 await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' })
128 })
129 })
130
131 after(async function () {
132 await cleanupTests([ server ])
133 })
134})
diff --git a/packages/tests/src/api/check-params/user-notifications.ts b/packages/tests/src/api/check-params/user-notifications.ts
new file mode 100644
index 000000000..cf20324a1
--- /dev/null
+++ b/packages/tests/src/api/check-params/user-notifications.ts
@@ -0,0 +1,290 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { io } from 'socket.io-client'
4import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
5import { wait } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 createSingleServer,
10 makeGetRequest,
11 makePostBodyRequest,
12 makePutBodyRequest,
13 PeerTubeServer,
14 setAccessTokensToServers
15} from '@peertube/peertube-server-commands'
16
17describe('Test user notifications API validators', function () {
18 let server: PeerTubeServer
19
20 // ---------------------------------------------------------------
21
22 before(async function () {
23 this.timeout(30000)
24
25 server = await createSingleServer(1)
26
27 await setAccessTokensToServers([ server ])
28 })
29
30 describe('When listing my notifications', function () {
31 const path = '/api/v1/users/me/notifications'
32
33 it('Should fail with a bad start pagination', async function () {
34 await checkBadStartPagination(server.url, path, server.accessToken)
35 })
36
37 it('Should fail with a bad count pagination', async function () {
38 await checkBadCountPagination(server.url, path, server.accessToken)
39 })
40
41 it('Should fail with an incorrect sort', async function () {
42 await checkBadSortPagination(server.url, path, server.accessToken)
43 })
44
45 it('Should fail with an incorrect unread parameter', async function () {
46 await makeGetRequest({
47 url: server.url,
48 path,
49 query: {
50 unread: 'toto'
51 },
52 token: server.accessToken,
53 expectedStatus: HttpStatusCode.OK_200
54 })
55 })
56
57 it('Should fail with a non authenticated user', async function () {
58 await makeGetRequest({
59 url: server.url,
60 path,
61 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
62 })
63 })
64
65 it('Should succeed with the correct parameters', async function () {
66 await makeGetRequest({
67 url: server.url,
68 path,
69 token: server.accessToken,
70 expectedStatus: HttpStatusCode.OK_200
71 })
72 })
73 })
74
75 describe('When marking as read my notifications', function () {
76 const path = '/api/v1/users/me/notifications/read'
77
78 it('Should fail with wrong ids parameters', async function () {
79 await makePostBodyRequest({
80 url: server.url,
81 path,
82 fields: {
83 ids: [ 'hello' ]
84 },
85 token: server.accessToken,
86 expectedStatus: HttpStatusCode.BAD_REQUEST_400
87 })
88
89 await makePostBodyRequest({
90 url: server.url,
91 path,
92 fields: {
93 ids: [ ]
94 },
95 token: server.accessToken,
96 expectedStatus: HttpStatusCode.BAD_REQUEST_400
97 })
98
99 await makePostBodyRequest({
100 url: server.url,
101 path,
102 fields: {
103 ids: 5
104 },
105 token: server.accessToken,
106 expectedStatus: HttpStatusCode.BAD_REQUEST_400
107 })
108 })
109
110 it('Should fail with a non authenticated user', async function () {
111 await makePostBodyRequest({
112 url: server.url,
113 path,
114 fields: {
115 ids: [ 5 ]
116 },
117 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
118 })
119 })
120
121 it('Should succeed with the correct parameters', async function () {
122 await makePostBodyRequest({
123 url: server.url,
124 path,
125 fields: {
126 ids: [ 5 ]
127 },
128 token: server.accessToken,
129 expectedStatus: HttpStatusCode.NO_CONTENT_204
130 })
131 })
132 })
133
134 describe('When marking as read my notifications', function () {
135 const path = '/api/v1/users/me/notifications/read-all'
136
137 it('Should fail with a non authenticated user', async function () {
138 await makePostBodyRequest({
139 url: server.url,
140 path,
141 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
142 })
143 })
144
145 it('Should succeed with the correct parameters', async function () {
146 await makePostBodyRequest({
147 url: server.url,
148 path,
149 token: server.accessToken,
150 expectedStatus: HttpStatusCode.NO_CONTENT_204
151 })
152 })
153 })
154
155 describe('When updating my notification settings', function () {
156 const path = '/api/v1/users/me/notification-settings'
157 const correctFields: UserNotificationSetting = {
158 newVideoFromSubscription: UserNotificationSettingValue.WEB,
159 newCommentOnMyVideo: UserNotificationSettingValue.WEB,
160 abuseAsModerator: UserNotificationSettingValue.WEB,
161 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
162 blacklistOnMyVideo: UserNotificationSettingValue.WEB,
163 myVideoImportFinished: UserNotificationSettingValue.WEB,
164 myVideoPublished: UserNotificationSettingValue.WEB,
165 commentMention: UserNotificationSettingValue.WEB,
166 newFollow: UserNotificationSettingValue.WEB,
167 newUserRegistration: UserNotificationSettingValue.WEB,
168 newInstanceFollower: UserNotificationSettingValue.WEB,
169 autoInstanceFollowing: UserNotificationSettingValue.WEB,
170 abuseNewMessage: UserNotificationSettingValue.WEB,
171 abuseStateChange: UserNotificationSettingValue.WEB,
172 newPeerTubeVersion: UserNotificationSettingValue.WEB,
173 myVideoStudioEditionFinished: UserNotificationSettingValue.WEB,
174 newPluginVersion: UserNotificationSettingValue.WEB
175 }
176
177 it('Should fail with missing fields', async function () {
178 await makePutBodyRequest({
179 url: server.url,
180 path,
181 token: server.accessToken,
182 fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB },
183 expectedStatus: HttpStatusCode.BAD_REQUEST_400
184 })
185 })
186
187 it('Should fail with incorrect field values', async function () {
188 {
189 const fields = { ...correctFields, newCommentOnMyVideo: 15 }
190
191 await makePutBodyRequest({
192 url: server.url,
193 path,
194 token: server.accessToken,
195 fields,
196 expectedStatus: HttpStatusCode.BAD_REQUEST_400
197 })
198 }
199
200 {
201 const fields = { ...correctFields, newCommentOnMyVideo: 'toto' }
202
203 await makePutBodyRequest({
204 url: server.url,
205 path,
206 fields,
207 token: server.accessToken,
208 expectedStatus: HttpStatusCode.BAD_REQUEST_400
209 })
210 }
211 })
212
213 it('Should fail with a non authenticated user', async function () {
214 await makePutBodyRequest({
215 url: server.url,
216 path,
217 fields: correctFields,
218 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
219 })
220 })
221
222 it('Should succeed with the correct parameters', async function () {
223 await makePutBodyRequest({
224 url: server.url,
225 path,
226 token: server.accessToken,
227 fields: correctFields,
228 expectedStatus: HttpStatusCode.NO_CONTENT_204
229 })
230 })
231 })
232
233 describe('When connecting to my notification socket', function () {
234
235 it('Should fail with no token', function (next) {
236 const socket = io(`${server.url}/user-notifications`, { reconnection: false })
237
238 socket.once('connect_error', function () {
239 socket.disconnect()
240 next()
241 })
242
243 socket.on('connect', () => {
244 socket.disconnect()
245 next(new Error('Connected with a missing token.'))
246 })
247 })
248
249 it('Should fail with an invalid token', function (next) {
250 const socket = io(`${server.url}/user-notifications`, {
251 query: { accessToken: 'bad_access_token' },
252 reconnection: false
253 })
254
255 socket.once('connect_error', function () {
256 socket.disconnect()
257 next()
258 })
259
260 socket.on('connect', () => {
261 socket.disconnect()
262 next(new Error('Connected with an invalid token.'))
263 })
264 })
265
266 it('Should success with the correct token', function (next) {
267 const socket = io(`${server.url}/user-notifications`, {
268 query: { accessToken: server.accessToken },
269 reconnection: false
270 })
271
272 function errorListener (err) {
273 next(new Error('Error in connection: ' + err))
274 }
275
276 socket.on('connect_error', errorListener)
277
278 socket.once('connect', async () => {
279 socket.disconnect()
280
281 await wait(500)
282 next()
283 })
284 })
285 })
286
287 after(async function () {
288 await cleanupTests([ server ])
289 })
290})
diff --git a/packages/tests/src/api/check-params/user-subscriptions.ts b/packages/tests/src/api/check-params/user-subscriptions.ts
new file mode 100644
index 000000000..e97f513a0
--- /dev/null
+++ b/packages/tests/src/api/check-params/user-subscriptions.ts
@@ -0,0 +1,298 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import {
4 cleanupTests,
5 createSingleServer,
6 makeDeleteRequest,
7 makeGetRequest,
8 makePostBodyRequest,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13import { HttpStatusCode } from '@peertube/peertube-models'
14import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js'
15
16describe('Test user subscriptions API validators', function () {
17 const path = '/api/v1/users/me/subscriptions'
18 let server: PeerTubeServer
19 let userAccessToken = ''
20
21 // ---------------------------------------------------------------
22
23 before(async function () {
24 this.timeout(30000)
25
26 server = await createSingleServer(1)
27
28 await setAccessTokensToServers([ server ])
29
30 const user = {
31 username: 'user1',
32 password: 'my super password'
33 }
34 await server.users.create({ username: user.username, password: user.password })
35 userAccessToken = await server.login.getAccessToken(user)
36 })
37
38 describe('When listing my subscriptions', function () {
39 it('Should fail with a bad start pagination', async function () {
40 await checkBadStartPagination(server.url, path, server.accessToken)
41 })
42
43 it('Should fail with a bad count pagination', async function () {
44 await checkBadCountPagination(server.url, path, server.accessToken)
45 })
46
47 it('Should fail with an incorrect sort', async function () {
48 await checkBadSortPagination(server.url, path, server.accessToken)
49 })
50
51 it('Should fail with a non authenticated user', async function () {
52 await makeGetRequest({
53 url: server.url,
54 path,
55 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
56 })
57 })
58
59 it('Should succeed with the correct parameters', async function () {
60 await makeGetRequest({
61 url: server.url,
62 path,
63 token: userAccessToken,
64 expectedStatus: HttpStatusCode.OK_200
65 })
66 })
67 })
68
69 describe('When listing my subscriptions videos', function () {
70 const path = '/api/v1/users/me/subscriptions/videos'
71
72 it('Should fail with a bad start pagination', async function () {
73 await checkBadStartPagination(server.url, path, server.accessToken)
74 })
75
76 it('Should fail with a bad count pagination', async function () {
77 await checkBadCountPagination(server.url, path, server.accessToken)
78 })
79
80 it('Should fail with an incorrect sort', async function () {
81 await checkBadSortPagination(server.url, path, server.accessToken)
82 })
83
84 it('Should fail with a non authenticated user', async function () {
85 await makeGetRequest({
86 url: server.url,
87 path,
88 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
89 })
90 })
91
92 it('Should succeed with the correct parameters', async function () {
93 await makeGetRequest({
94 url: server.url,
95 path,
96 token: userAccessToken,
97 expectedStatus: HttpStatusCode.OK_200
98 })
99 })
100 })
101
102 describe('When adding a subscription', function () {
103 it('Should fail with a non authenticated user', async function () {
104 await makePostBodyRequest({
105 url: server.url,
106 path,
107 fields: { uri: 'user1_channel@' + server.host },
108 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
109 })
110 })
111
112 it('Should fail with bad URIs', async function () {
113 await makePostBodyRequest({
114 url: server.url,
115 path,
116 token: server.accessToken,
117 fields: { uri: 'root' },
118 expectedStatus: HttpStatusCode.BAD_REQUEST_400
119 })
120
121 await makePostBodyRequest({
122 url: server.url,
123 path,
124 token: server.accessToken,
125 fields: { uri: 'root@' },
126 expectedStatus: HttpStatusCode.BAD_REQUEST_400
127 })
128
129 await makePostBodyRequest({
130 url: server.url,
131 path,
132 token: server.accessToken,
133 fields: { uri: 'root@hello@' },
134 expectedStatus: HttpStatusCode.BAD_REQUEST_400
135 })
136 })
137
138 it('Should succeed with the correct parameters', async function () {
139 this.timeout(20000)
140
141 await makePostBodyRequest({
142 url: server.url,
143 path,
144 token: server.accessToken,
145 fields: { uri: 'user1_channel@' + server.host },
146 expectedStatus: HttpStatusCode.NO_CONTENT_204
147 })
148
149 await waitJobs([ server ])
150 })
151 })
152
153 describe('When getting a subscription', function () {
154 it('Should fail with a non authenticated user', async function () {
155 await makeGetRequest({
156 url: server.url,
157 path: path + '/user1_channel@' + server.host,
158 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
159 })
160 })
161
162 it('Should fail with bad URIs', async function () {
163 await makeGetRequest({
164 url: server.url,
165 path: path + '/root',
166 token: server.accessToken,
167 expectedStatus: HttpStatusCode.BAD_REQUEST_400
168 })
169
170 await makeGetRequest({
171 url: server.url,
172 path: path + '/root@',
173 token: server.accessToken,
174 expectedStatus: HttpStatusCode.BAD_REQUEST_400
175 })
176
177 await makeGetRequest({
178 url: server.url,
179 path: path + '/root@hello@',
180 token: server.accessToken,
181 expectedStatus: HttpStatusCode.BAD_REQUEST_400
182 })
183 })
184
185 it('Should fail with an unknown subscription', async function () {
186 await makeGetRequest({
187 url: server.url,
188 path: path + '/root1@' + server.host,
189 token: server.accessToken,
190 expectedStatus: HttpStatusCode.NOT_FOUND_404
191 })
192 })
193
194 it('Should succeed with the correct parameters', async function () {
195 await makeGetRequest({
196 url: server.url,
197 path: path + '/user1_channel@' + server.host,
198 token: server.accessToken,
199 expectedStatus: HttpStatusCode.OK_200
200 })
201 })
202 })
203
204 describe('When checking if subscriptions exist', function () {
205 const existPath = path + '/exist'
206
207 it('Should fail with a non authenticated user', async function () {
208 await makeGetRequest({
209 url: server.url,
210 path: existPath,
211 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
212 })
213 })
214
215 it('Should fail with bad URIs', async function () {
216 await makeGetRequest({
217 url: server.url,
218 path: existPath,
219 query: { uris: 'toto' },
220 token: server.accessToken,
221 expectedStatus: HttpStatusCode.BAD_REQUEST_400
222 })
223
224 await makeGetRequest({
225 url: server.url,
226 path: existPath,
227 query: { 'uris[]': 1 },
228 token: server.accessToken,
229 expectedStatus: HttpStatusCode.BAD_REQUEST_400
230 })
231 })
232
233 it('Should succeed with the correct parameters', async function () {
234 await makeGetRequest({
235 url: server.url,
236 path: existPath,
237 query: { 'uris[]': 'coucou@' + server.host },
238 token: server.accessToken,
239 expectedStatus: HttpStatusCode.OK_200
240 })
241 })
242 })
243
244 describe('When removing a subscription', function () {
245 it('Should fail with a non authenticated user', async function () {
246 await makeDeleteRequest({
247 url: server.url,
248 path: path + '/user1_channel@' + server.host,
249 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
250 })
251 })
252
253 it('Should fail with bad URIs', async function () {
254 await makeDeleteRequest({
255 url: server.url,
256 path: path + '/root',
257 token: server.accessToken,
258 expectedStatus: HttpStatusCode.BAD_REQUEST_400
259 })
260
261 await makeDeleteRequest({
262 url: server.url,
263 path: path + '/root@',
264 token: server.accessToken,
265 expectedStatus: HttpStatusCode.BAD_REQUEST_400
266 })
267
268 await makeDeleteRequest({
269 url: server.url,
270 path: path + '/root@hello@',
271 token: server.accessToken,
272 expectedStatus: HttpStatusCode.BAD_REQUEST_400
273 })
274 })
275
276 it('Should fail with an unknown subscription', async function () {
277 await makeDeleteRequest({
278 url: server.url,
279 path: path + '/root1@' + server.host,
280 token: server.accessToken,
281 expectedStatus: HttpStatusCode.NOT_FOUND_404
282 })
283 })
284
285 it('Should succeed with the correct parameters', async function () {
286 await makeDeleteRequest({
287 url: server.url,
288 path: path + '/user1_channel@' + server.host,
289 token: server.accessToken,
290 expectedStatus: HttpStatusCode.NO_CONTENT_204
291 })
292 })
293 })
294
295 after(async function () {
296 await cleanupTests([ server ])
297 })
298})
diff --git a/packages/tests/src/api/check-params/users-admin.ts b/packages/tests/src/api/check-params/users-admin.ts
new file mode 100644
index 000000000..1ad222ddc
--- /dev/null
+++ b/packages/tests/src/api/check-params/users-admin.ts
@@ -0,0 +1,457 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { omit } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, UserAdminFlag, UserRole } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 ConfigCommand,
10 createSingleServer,
11 killallServers,
12 makeGetRequest,
13 makePostBodyRequest,
14 makePutBodyRequest,
15 PeerTubeServer,
16 setAccessTokensToServers
17} from '@peertube/peertube-server-commands'
18
19describe('Test users admin API validators', function () {
20 const path = '/api/v1/users/'
21 let userId: number
22 let rootId: number
23 let moderatorId: number
24 let server: PeerTubeServer
25 let userToken = ''
26 let moderatorToken = ''
27 let emailPort: number
28
29 // ---------------------------------------------------------------
30
31 before(async function () {
32 this.timeout(30000)
33
34 const emails: object[] = []
35 emailPort = await MockSmtpServer.Instance.collectEmails(emails)
36
37 {
38 server = await createSingleServer(1)
39
40 await setAccessTokensToServers([ server ])
41 }
42
43 {
44 const result = await server.users.generate('user1')
45 userToken = result.token
46 userId = result.userId
47 }
48
49 {
50 const result = await server.users.generate('moderator1', UserRole.MODERATOR)
51 moderatorToken = result.token
52 }
53
54 {
55 const result = await server.users.generate('moderator2', UserRole.MODERATOR)
56 moderatorId = result.userId
57 }
58 })
59
60 describe('When listing users', function () {
61 it('Should fail with a bad start pagination', async function () {
62 await checkBadStartPagination(server.url, path, server.accessToken)
63 })
64
65 it('Should fail with a bad count pagination', async function () {
66 await checkBadCountPagination(server.url, path, server.accessToken)
67 })
68
69 it('Should fail with an incorrect sort', async function () {
70 await checkBadSortPagination(server.url, path, server.accessToken)
71 })
72
73 it('Should fail with a non authenticated user', async function () {
74 await makeGetRequest({
75 url: server.url,
76 path,
77 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
78 })
79 })
80
81 it('Should fail with a non admin user', async function () {
82 await makeGetRequest({
83 url: server.url,
84 path,
85 token: userToken,
86 expectedStatus: HttpStatusCode.FORBIDDEN_403
87 })
88 })
89 })
90
91 describe('When adding a new user', function () {
92 const baseCorrectParams = {
93 username: 'user2',
94 email: 'test@example.com',
95 password: 'my super password',
96 videoQuota: -1,
97 videoQuotaDaily: -1,
98 role: UserRole.USER,
99 adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST
100 }
101
102 it('Should fail with a too small username', async function () {
103 const fields = { ...baseCorrectParams, username: '' }
104
105 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
106 })
107
108 it('Should fail with a too long username', async function () {
109 const fields = { ...baseCorrectParams, username: 'super'.repeat(50) }
110
111 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
112 })
113
114 it('Should fail with a not lowercase username', async function () {
115 const fields = { ...baseCorrectParams, username: 'Toto' }
116
117 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
118 })
119
120 it('Should fail with an incorrect username', async function () {
121 const fields = { ...baseCorrectParams, username: 'my username' }
122
123 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
124 })
125
126 it('Should fail with a missing email', async function () {
127 const fields = omit(baseCorrectParams, [ 'email' ])
128
129 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
130 })
131
132 it('Should fail with an invalid email', async function () {
133 const fields = { ...baseCorrectParams, email: 'test_example.com' }
134
135 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
136 })
137
138 it('Should fail with a too small password', async function () {
139 const fields = { ...baseCorrectParams, password: 'bla' }
140
141 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
142 })
143
144 it('Should fail with a too long password', async function () {
145 const fields = { ...baseCorrectParams, password: 'super'.repeat(61) }
146
147 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
148 })
149
150 it('Should fail with empty password and no smtp configured', async function () {
151 const fields = { ...baseCorrectParams, password: '' }
152
153 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
154 })
155
156 it('Should succeed with no password on a server with smtp enabled', async function () {
157 this.timeout(20000)
158
159 await killallServers([ server ])
160
161 await server.run(ConfigCommand.getEmailOverrideConfig(emailPort))
162
163 const fields = {
164 ...baseCorrectParams,
165
166 password: '',
167 username: 'create_password',
168 email: 'create_password@example.com'
169 }
170
171 await makePostBodyRequest({
172 url: server.url,
173 path,
174 token: server.accessToken,
175 fields,
176 expectedStatus: HttpStatusCode.OK_200
177 })
178 })
179
180 it('Should fail with invalid admin flags', async function () {
181 const fields = { ...baseCorrectParams, adminFlags: 'toto' }
182
183 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
184 })
185
186 it('Should fail with an non authenticated user', async function () {
187 await makePostBodyRequest({
188 url: server.url,
189 path,
190 token: 'super token',
191 fields: baseCorrectParams,
192 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
193 })
194 })
195
196 it('Should fail if we add a user with the same username', async function () {
197 const fields = { ...baseCorrectParams, username: 'user1' }
198
199 await makePostBodyRequest({
200 url: server.url,
201 path,
202 token: server.accessToken,
203 fields,
204 expectedStatus: HttpStatusCode.CONFLICT_409
205 })
206 })
207
208 it('Should fail if we add a user with the same email', async function () {
209 const fields = { ...baseCorrectParams, email: 'user1@example.com' }
210
211 await makePostBodyRequest({
212 url: server.url,
213 path,
214 token: server.accessToken,
215 fields,
216 expectedStatus: HttpStatusCode.CONFLICT_409
217 })
218 })
219
220 it('Should fail with an invalid videoQuota', async function () {
221 const fields = { ...baseCorrectParams, videoQuota: -5 }
222
223 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
224 })
225
226 it('Should fail with an invalid videoQuotaDaily', async function () {
227 const fields = { ...baseCorrectParams, videoQuotaDaily: -7 }
228
229 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
230 })
231
232 it('Should fail without a user role', async function () {
233 const fields = omit(baseCorrectParams, [ 'role' ])
234
235 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
236 })
237
238 it('Should fail with an invalid user role', async function () {
239 const fields = { ...baseCorrectParams, role: 88989 }
240
241 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
242 })
243
244 it('Should fail with a "peertube" username', async function () {
245 const fields = { ...baseCorrectParams, username: 'peertube' }
246
247 await makePostBodyRequest({
248 url: server.url,
249 path,
250 token: server.accessToken,
251 fields,
252 expectedStatus: HttpStatusCode.CONFLICT_409
253 })
254 })
255
256 it('Should fail to create a moderator or an admin with a moderator', async function () {
257 for (const role of [ UserRole.MODERATOR, UserRole.ADMINISTRATOR ]) {
258 const fields = { ...baseCorrectParams, role }
259
260 await makePostBodyRequest({
261 url: server.url,
262 path,
263 token: moderatorToken,
264 fields,
265 expectedStatus: HttpStatusCode.FORBIDDEN_403
266 })
267 }
268 })
269
270 it('Should succeed to create a user with a moderator', async function () {
271 const fields = { ...baseCorrectParams, username: 'a4656', email: 'a4656@example.com', role: UserRole.USER }
272
273 await makePostBodyRequest({
274 url: server.url,
275 path,
276 token: moderatorToken,
277 fields,
278 expectedStatus: HttpStatusCode.OK_200
279 })
280 })
281
282 it('Should succeed with the correct params', async function () {
283 await makePostBodyRequest({
284 url: server.url,
285 path,
286 token: server.accessToken,
287 fields: baseCorrectParams,
288 expectedStatus: HttpStatusCode.OK_200
289 })
290 })
291
292 it('Should fail with a non admin user', async function () {
293 const user = { username: 'user1' }
294 userToken = await server.login.getAccessToken(user)
295
296 const fields = {
297 username: 'user3',
298 email: 'test@example.com',
299 password: 'my super password',
300 videoQuota: 42000000
301 }
302 await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
303 })
304 })
305
306 describe('When getting a user', function () {
307
308 it('Should fail with an non authenticated user', async function () {
309 await makeGetRequest({
310 url: server.url,
311 path: path + userId,
312 token: 'super token',
313 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
314 })
315 })
316
317 it('Should fail with a non admin user', async function () {
318 await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
319 })
320
321 it('Should succeed with the correct params', async function () {
322 await makeGetRequest({ url: server.url, path: path + userId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
323 })
324 })
325
326 describe('When updating a user', function () {
327
328 it('Should fail with an invalid email attribute', async function () {
329 const fields = {
330 email: 'blabla'
331 }
332
333 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
334 })
335
336 it('Should fail with an invalid emailVerified attribute', async function () {
337 const fields = {
338 emailVerified: 'yes'
339 }
340
341 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
342 })
343
344 it('Should fail with an invalid videoQuota attribute', async function () {
345 const fields = {
346 videoQuota: -90
347 }
348
349 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
350 })
351
352 it('Should fail with an invalid user role attribute', async function () {
353 const fields = {
354 role: 54878
355 }
356
357 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
358 })
359
360 it('Should fail with a too small password', async function () {
361 const fields = {
362 currentPassword: 'password',
363 password: 'bla'
364 }
365
366 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
367 })
368
369 it('Should fail with a too long password', async function () {
370 const fields = {
371 currentPassword: 'password',
372 password: 'super'.repeat(61)
373 }
374
375 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
376 })
377
378 it('Should fail with an non authenticated user', async function () {
379 const fields = {
380 videoQuota: 42
381 }
382
383 await makePutBodyRequest({
384 url: server.url,
385 path: path + userId,
386 token: 'super token',
387 fields,
388 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
389 })
390 })
391
392 it('Should fail when updating root role', async function () {
393 const fields = {
394 role: UserRole.MODERATOR
395 }
396
397 await makePutBodyRequest({ url: server.url, path: path + rootId, token: server.accessToken, fields })
398 })
399
400 it('Should fail with invalid admin flags', async function () {
401 const fields = { adminFlags: 'toto' }
402
403 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
404 })
405
406 it('Should fail to update an admin with a moderator', async function () {
407 const fields = {
408 videoQuota: 42
409 }
410
411 await makePutBodyRequest({
412 url: server.url,
413 path: path + moderatorId,
414 token: moderatorToken,
415 fields,
416 expectedStatus: HttpStatusCode.FORBIDDEN_403
417 })
418 })
419
420 it('Should succeed to update a user with a moderator', async function () {
421 const fields = {
422 videoQuota: 42
423 }
424
425 await makePutBodyRequest({
426 url: server.url,
427 path: path + userId,
428 token: moderatorToken,
429 fields,
430 expectedStatus: HttpStatusCode.NO_CONTENT_204
431 })
432 })
433
434 it('Should succeed with the correct params', async function () {
435 const fields = {
436 email: 'email@example.com',
437 emailVerified: true,
438 videoQuota: 42,
439 role: UserRole.USER
440 }
441
442 await makePutBodyRequest({
443 url: server.url,
444 path: path + userId,
445 token: server.accessToken,
446 fields,
447 expectedStatus: HttpStatusCode.NO_CONTENT_204
448 })
449 })
450 })
451
452 after(async function () {
453 MockSmtpServer.Instance.kill()
454
455 await cleanupTests([ server ])
456 })
457})
diff --git a/packages/tests/src/api/check-params/users-emails.ts b/packages/tests/src/api/check-params/users-emails.ts
new file mode 100644
index 000000000..e382190ec
--- /dev/null
+++ b/packages/tests/src/api/check-params/users-emails.ts
@@ -0,0 +1,122 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2import { HttpStatusCode, UserRole } from '@peertube/peertube-models'
3import {
4 cleanupTests,
5 createSingleServer,
6 makePostBodyRequest,
7 PeerTubeServer,
8 setAccessTokensToServers
9} from '@peertube/peertube-server-commands'
10
11describe('Test users API validators', function () {
12 let server: PeerTubeServer
13
14 // ---------------------------------------------------------------
15
16 before(async function () {
17 this.timeout(30000)
18
19 server = await createSingleServer(1, {
20 rates_limit: {
21 ask_send_email: {
22 max: 10
23 }
24 }
25 })
26
27 await setAccessTokensToServers([ server ])
28 await server.config.enableSignup(true)
29
30 await server.users.generate('moderator2', UserRole.MODERATOR)
31
32 await server.registrations.requestRegistration({
33 username: 'request1',
34 registrationReason: 'tt'
35 })
36 })
37
38 describe('When asking a password reset', function () {
39 const path = '/api/v1/users/ask-reset-password'
40
41 it('Should fail with a missing email', async function () {
42 const fields = {}
43
44 await makePostBodyRequest({ url: server.url, path, fields })
45 })
46
47 it('Should fail with an invalid email', async function () {
48 const fields = { email: 'hello' }
49
50 await makePostBodyRequest({ url: server.url, path, fields })
51 })
52
53 it('Should success with the correct params', async function () {
54 const fields = { email: 'admin@example.com' }
55
56 await makePostBodyRequest({
57 url: server.url,
58 path,
59 fields,
60 expectedStatus: HttpStatusCode.NO_CONTENT_204
61 })
62 })
63 })
64
65 describe('When asking for an account verification email', function () {
66 const path = '/api/v1/users/ask-send-verify-email'
67
68 it('Should fail with a missing email', async function () {
69 const fields = {}
70
71 await makePostBodyRequest({ url: server.url, path, fields })
72 })
73
74 it('Should fail with an invalid email', async function () {
75 const fields = { email: 'hello' }
76
77 await makePostBodyRequest({ url: server.url, path, fields })
78 })
79
80 it('Should succeed with the correct params', async function () {
81 const fields = { email: 'admin@example.com' }
82
83 await makePostBodyRequest({
84 url: server.url,
85 path,
86 fields,
87 expectedStatus: HttpStatusCode.NO_CONTENT_204
88 })
89 })
90 })
91
92 describe('When asking for a registration verification email', function () {
93 const path = '/api/v1/users/registrations/ask-send-verify-email'
94
95 it('Should fail with a missing email', async function () {
96 const fields = {}
97
98 await makePostBodyRequest({ url: server.url, path, fields })
99 })
100
101 it('Should fail with an invalid email', async function () {
102 const fields = { email: 'hello' }
103
104 await makePostBodyRequest({ url: server.url, path, fields })
105 })
106
107 it('Should succeed with the correct params', async function () {
108 const fields = { email: 'request1@example.com' }
109
110 await makePostBodyRequest({
111 url: server.url,
112 path,
113 fields,
114 expectedStatus: HttpStatusCode.NO_CONTENT_204
115 })
116 })
117 })
118
119 after(async function () {
120 await cleanupTests([ server ])
121 })
122})
diff --git a/packages/tests/src/api/check-params/video-blacklist.ts b/packages/tests/src/api/check-params/video-blacklist.ts
new file mode 100644
index 000000000..6ec070b9b
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-blacklist.ts
@@ -0,0 +1,292 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
5import { HttpStatusCode, VideoBlacklistType } from '@peertube/peertube-models'
6import {
7 BlacklistCommand,
8 cleanupTests,
9 createMultipleServers,
10 doubleFollow,
11 makePostBodyRequest,
12 makePutBodyRequest,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 waitJobs
16} from '@peertube/peertube-server-commands'
17
18describe('Test video blacklist API validators', function () {
19 let servers: PeerTubeServer[]
20 let notBlacklistedVideoId: string
21 let remoteVideoUUID: string
22 let userAccessToken1 = ''
23 let userAccessToken2 = ''
24 let command: BlacklistCommand
25
26 // ---------------------------------------------------------------
27
28 before(async function () {
29 this.timeout(120000)
30
31 servers = await createMultipleServers(2)
32
33 await setAccessTokensToServers(servers)
34 await doubleFollow(servers[0], servers[1])
35
36 {
37 const username = 'user1'
38 const password = 'my super password'
39 await servers[0].users.create({ username, password })
40 userAccessToken1 = await servers[0].login.getAccessToken({ username, password })
41 }
42
43 {
44 const username = 'user2'
45 const password = 'my super password'
46 await servers[0].users.create({ username, password })
47 userAccessToken2 = await servers[0].login.getAccessToken({ username, password })
48 }
49
50 {
51 servers[0].store.videoCreated = await servers[0].videos.upload({ token: userAccessToken1 })
52 }
53
54 {
55 const { uuid } = await servers[0].videos.upload()
56 notBlacklistedVideoId = uuid
57 }
58
59 {
60 const { uuid } = await servers[1].videos.upload()
61 remoteVideoUUID = uuid
62 }
63
64 await waitJobs(servers)
65
66 command = servers[0].blacklist
67 })
68
69 describe('When adding a video in blacklist', function () {
70 const basePath = '/api/v1/videos/'
71
72 it('Should fail with nothing', async function () {
73 const path = basePath + servers[0].store.videoCreated + '/blacklist'
74 const fields = {}
75 await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
76 })
77
78 it('Should fail with a wrong video', async function () {
79 const wrongPath = '/api/v1/videos/blabla/blacklist'
80 const fields = {}
81 await makePostBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields })
82 })
83
84 it('Should fail with a non authenticated user', async function () {
85 const path = basePath + servers[0].store.videoCreated + '/blacklist'
86 const fields = {}
87 await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
88 })
89
90 it('Should fail with a non admin user', async function () {
91 const path = basePath + servers[0].store.videoCreated + '/blacklist'
92 const fields = {}
93 await makePostBodyRequest({
94 url: servers[0].url,
95 path,
96 token: userAccessToken2,
97 fields,
98 expectedStatus: HttpStatusCode.FORBIDDEN_403
99 })
100 })
101
102 it('Should fail with an invalid reason', async function () {
103 const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
104 const fields = { reason: 'a'.repeat(305) }
105
106 await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
107 })
108
109 it('Should fail to unfederate a remote video', async function () {
110 const path = basePath + remoteVideoUUID + '/blacklist'
111 const fields = { unfederate: true }
112
113 await makePostBodyRequest({
114 url: servers[0].url,
115 path,
116 token: servers[0].accessToken,
117 fields,
118 expectedStatus: HttpStatusCode.CONFLICT_409
119 })
120 })
121
122 it('Should succeed with the correct params', async function () {
123 const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
124 const fields = {}
125
126 await makePostBodyRequest({
127 url: servers[0].url,
128 path,
129 token: servers[0].accessToken,
130 fields,
131 expectedStatus: HttpStatusCode.NO_CONTENT_204
132 })
133 })
134 })
135
136 describe('When updating a video in blacklist', function () {
137 const basePath = '/api/v1/videos/'
138
139 it('Should fail with a wrong video', async function () {
140 const wrongPath = '/api/v1/videos/blabla/blacklist'
141 const fields = {}
142 await makePutBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields })
143 })
144
145 it('Should fail with a video not blacklisted', async function () {
146 const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist'
147 const fields = {}
148 await makePutBodyRequest({
149 url: servers[0].url,
150 path,
151 token: servers[0].accessToken,
152 fields,
153 expectedStatus: HttpStatusCode.NOT_FOUND_404
154 })
155 })
156
157 it('Should fail with a non authenticated user', async function () {
158 const path = basePath + servers[0].store.videoCreated + '/blacklist'
159 const fields = {}
160 await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
161 })
162
163 it('Should fail with a non admin user', async function () {
164 const path = basePath + servers[0].store.videoCreated + '/blacklist'
165 const fields = {}
166 await makePutBodyRequest({
167 url: servers[0].url,
168 path,
169 token: userAccessToken2,
170 fields,
171 expectedStatus: HttpStatusCode.FORBIDDEN_403
172 })
173 })
174
175 it('Should fail with an invalid reason', async function () {
176 const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist'
177 const fields = { reason: 'a'.repeat(305) }
178
179 await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields })
180 })
181
182 it('Should succeed with the correct params', async function () {
183 const path = basePath + servers[0].store.videoCreated.shortUUID + '/blacklist'
184 const fields = { reason: 'hello' }
185
186 await makePutBodyRequest({
187 url: servers[0].url,
188 path,
189 token: servers[0].accessToken,
190 fields,
191 expectedStatus: HttpStatusCode.NO_CONTENT_204
192 })
193 })
194 })
195
196 describe('When getting blacklisted video', function () {
197
198 it('Should fail with a non authenticated user', async function () {
199 await servers[0].videos.get({ id: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
200 })
201
202 it('Should fail with another user', async function () {
203 await servers[0].videos.getWithToken({
204 token: userAccessToken2,
205 id: servers[0].store.videoCreated.uuid,
206 expectedStatus: HttpStatusCode.FORBIDDEN_403
207 })
208 })
209
210 it('Should succeed with the owner authenticated user', async function () {
211 const video = await servers[0].videos.getWithToken({ token: userAccessToken1, id: servers[0].store.videoCreated.uuid })
212 expect(video.blacklisted).to.be.true
213 })
214
215 it('Should succeed with an admin', async function () {
216 const video = servers[0].store.videoCreated
217
218 for (const id of [ video.id, video.uuid, video.shortUUID ]) {
219 const video = await servers[0].videos.getWithToken({ id, expectedStatus: HttpStatusCode.OK_200 })
220 expect(video.blacklisted).to.be.true
221 }
222 })
223 })
224
225 describe('When removing a video in blacklist', function () {
226
227 it('Should fail with a non authenticated user', async function () {
228 await command.remove({
229 token: 'fake token',
230 videoId: servers[0].store.videoCreated.uuid,
231 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
232 })
233 })
234
235 it('Should fail with a non admin user', async function () {
236 await command.remove({
237 token: userAccessToken2,
238 videoId: servers[0].store.videoCreated.uuid,
239 expectedStatus: HttpStatusCode.FORBIDDEN_403
240 })
241 })
242
243 it('Should fail with an incorrect id', async function () {
244 await command.remove({ videoId: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
245 })
246
247 it('Should fail with a not blacklisted video', async function () {
248 // The video was not added to the blacklist so it should fail
249 await command.remove({ videoId: notBlacklistedVideoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
250 })
251
252 it('Should succeed with the correct params', async function () {
253 await command.remove({ videoId: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
254 })
255 })
256
257 describe('When listing videos in blacklist', function () {
258 const basePath = '/api/v1/videos/blacklist/'
259
260 it('Should fail with a non authenticated user', async function () {
261 await servers[0].blacklist.list({ token: 'fake token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
262 })
263
264 it('Should fail with a non admin user', async function () {
265 await servers[0].blacklist.list({ token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
266 })
267
268 it('Should fail with a bad start pagination', async function () {
269 await checkBadStartPagination(servers[0].url, basePath, servers[0].accessToken)
270 })
271
272 it('Should fail with a bad count pagination', async function () {
273 await checkBadCountPagination(servers[0].url, basePath, servers[0].accessToken)
274 })
275
276 it('Should fail with an incorrect sort', async function () {
277 await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken)
278 })
279
280 it('Should fail with an invalid type', async function () {
281 await servers[0].blacklist.list({ type: 0 as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
282 })
283
284 it('Should succeed with the correct parameters', async function () {
285 await servers[0].blacklist.list({ type: VideoBlacklistType.MANUAL })
286 })
287 })
288
289 after(async function () {
290 await cleanupTests(servers)
291 })
292})
diff --git a/packages/tests/src/api/check-params/video-captions.ts b/packages/tests/src/api/check-params/video-captions.ts
new file mode 100644
index 000000000..4150b095f
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-captions.ts
@@ -0,0 +1,307 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
4import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeDeleteRequest,
9 makeGetRequest,
10 makeUploadRequest,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15describe('Test video captions API validator', function () {
16 const path = '/api/v1/videos/'
17
18 let server: PeerTubeServer
19 let userAccessToken: string
20 let video: VideoCreateResult
21 let privateVideo: VideoCreateResult
22
23 // ---------------------------------------------------------------
24
25 before(async function () {
26 this.timeout(120000)
27
28 server = await createSingleServer(1)
29
30 await setAccessTokensToServers([ server ])
31
32 video = await server.videos.upload()
33 privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
34
35 {
36 const user = {
37 username: 'user1',
38 password: 'my super password'
39 }
40 await server.users.create({ username: user.username, password: user.password })
41 userAccessToken = await server.login.getAccessToken(user)
42 }
43 })
44
45 describe('When adding video caption', function () {
46 const fields = { }
47 const attaches = {
48 captionfile: buildAbsoluteFixturePath('subtitle-good1.vtt')
49 }
50
51 it('Should fail without a valid uuid', async function () {
52 await makeUploadRequest({
53 method: 'PUT',
54 url: server.url,
55 path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr',
56 token: server.accessToken,
57 fields,
58 attaches
59 })
60 })
61
62 it('Should fail with an unknown id', async function () {
63 await makeUploadRequest({
64 method: 'PUT',
65 url: server.url,
66 path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr',
67 token: server.accessToken,
68 fields,
69 attaches,
70 expectedStatus: 404
71 })
72 })
73
74 it('Should fail with a missing language in path', async function () {
75 const captionPath = path + video.uuid + '/captions'
76 await makeUploadRequest({
77 method: 'PUT',
78 url: server.url,
79 path: captionPath,
80 token: server.accessToken,
81 fields,
82 attaches
83 })
84 })
85
86 it('Should fail with an unknown language', async function () {
87 const captionPath = path + video.uuid + '/captions/15'
88 await makeUploadRequest({
89 method: 'PUT',
90 url: server.url,
91 path: captionPath,
92 token: server.accessToken,
93 fields,
94 attaches
95 })
96 })
97
98 it('Should fail without access token', async function () {
99 const captionPath = path + video.uuid + '/captions/fr'
100 await makeUploadRequest({
101 method: 'PUT',
102 url: server.url,
103 path: captionPath,
104 fields,
105 attaches,
106 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
107 })
108 })
109
110 it('Should fail with a bad access token', async function () {
111 const captionPath = path + video.uuid + '/captions/fr'
112 await makeUploadRequest({
113 method: 'PUT',
114 url: server.url,
115 path: captionPath,
116 token: 'blabla',
117 fields,
118 attaches,
119 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
120 })
121 })
122
123 // We accept any file now
124 // it('Should fail with an invalid captionfile extension', async function () {
125 // const attaches = {
126 // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.txt')
127 // }
128 //
129 // const captionPath = path + video.uuid + '/captions/fr'
130 // await makeUploadRequest({
131 // method: 'PUT',
132 // url: server.url,
133 // path: captionPath,
134 // token: server.accessToken,
135 // fields,
136 // attaches,
137 // expectedStatus: HttpStatusCode.BAD_REQUEST_400
138 // })
139 // })
140
141 // We don't check the extension yet
142 // it('Should fail with an invalid captionfile extension and octet-stream mime type', async function () {
143 // await createVideoCaption({
144 // url: server.url,
145 // accessToken: server.accessToken,
146 // language: 'zh',
147 // videoId: video.uuid,
148 // fixture: 'subtitle-bad.txt',
149 // mimeType: 'application/octet-stream',
150 // expectedStatus: HttpStatusCode.BAD_REQUEST_400
151 // })
152 // })
153
154 it('Should succeed with a valid captionfile extension and octet-stream mime type', async function () {
155 await server.captions.add({
156 language: 'zh',
157 videoId: video.uuid,
158 fixture: 'subtitle-good.srt',
159 mimeType: 'application/octet-stream'
160 })
161 })
162
163 // We don't check the file validity yet
164 // it('Should fail with an invalid captionfile srt', async function () {
165 // const attaches = {
166 // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.srt')
167 // }
168 //
169 // const captionPath = path + video.uuid + '/captions/fr'
170 // await makeUploadRequest({
171 // method: 'PUT',
172 // url: server.url,
173 // path: captionPath,
174 // token: server.accessToken,
175 // fields,
176 // attaches,
177 // expectedStatus: HttpStatusCode.INTERNAL_SERVER_ERROR_500
178 // })
179 // })
180
181 it('Should success with the correct parameters', async function () {
182 const captionPath = path + video.uuid + '/captions/fr'
183 await makeUploadRequest({
184 method: 'PUT',
185 url: server.url,
186 path: captionPath,
187 token: server.accessToken,
188 fields,
189 attaches,
190 expectedStatus: HttpStatusCode.NO_CONTENT_204
191 })
192 })
193 })
194
195 describe('When listing video captions', function () {
196 it('Should fail without a valid uuid', async function () {
197 await makeGetRequest({ url: server.url, path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions' })
198 })
199
200 it('Should fail with an unknown id', async function () {
201 await makeGetRequest({
202 url: server.url,
203 path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions',
204 expectedStatus: HttpStatusCode.NOT_FOUND_404
205 })
206 })
207
208 it('Should fail with a private video without token', async function () {
209 await makeGetRequest({
210 url: server.url,
211 path: path + privateVideo.shortUUID + '/captions',
212 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
213 })
214 })
215
216 it('Should fail with another user token', async function () {
217 await makeGetRequest({
218 url: server.url,
219 token: userAccessToken,
220 path: path + privateVideo.shortUUID + '/captions',
221 expectedStatus: HttpStatusCode.FORBIDDEN_403
222 })
223 })
224
225 it('Should success with the correct parameters', async function () {
226 await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', expectedStatus: HttpStatusCode.OK_200 })
227
228 await makeGetRequest({
229 url: server.url,
230 path: path + privateVideo.shortUUID + '/captions',
231 token: server.accessToken,
232 expectedStatus: HttpStatusCode.OK_200
233 })
234 })
235 })
236
237 describe('When deleting video caption', function () {
238 it('Should fail without a valid uuid', async function () {
239 await makeDeleteRequest({
240 url: server.url,
241 path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr',
242 token: server.accessToken
243 })
244 })
245
246 it('Should fail with an unknown id', async function () {
247 await makeDeleteRequest({
248 url: server.url,
249 path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr',
250 token: server.accessToken,
251 expectedStatus: HttpStatusCode.NOT_FOUND_404
252 })
253 })
254
255 it('Should fail with an invalid language', async function () {
256 await makeDeleteRequest({
257 url: server.url,
258 path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/16',
259 token: server.accessToken
260 })
261 })
262
263 it('Should fail with a missing language', async function () {
264 const captionPath = path + video.shortUUID + '/captions'
265 await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken })
266 })
267
268 it('Should fail with an unknown language', async function () {
269 const captionPath = path + video.shortUUID + '/captions/15'
270 await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken })
271 })
272
273 it('Should fail without access token', async function () {
274 const captionPath = path + video.shortUUID + '/captions/fr'
275 await makeDeleteRequest({ url: server.url, path: captionPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
276 })
277
278 it('Should fail with a bad access token', async function () {
279 const captionPath = path + video.shortUUID + '/captions/fr'
280 await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
281 })
282
283 it('Should fail with another user', async function () {
284 const captionPath = path + video.shortUUID + '/captions/fr'
285 await makeDeleteRequest({
286 url: server.url,
287 path: captionPath,
288 token: userAccessToken,
289 expectedStatus: HttpStatusCode.FORBIDDEN_403
290 })
291 })
292
293 it('Should success with the correct parameters', async function () {
294 const captionPath = path + video.shortUUID + '/captions/fr'
295 await makeDeleteRequest({
296 url: server.url,
297 path: captionPath,
298 token: server.accessToken,
299 expectedStatus: HttpStatusCode.NO_CONTENT_204
300 })
301 })
302 })
303
304 after(async function () {
305 await cleanupTests([ server ])
306 })
307})
diff --git a/packages/tests/src/api/check-params/video-channel-syncs.ts b/packages/tests/src/api/check-params/video-channel-syncs.ts
new file mode 100644
index 000000000..d95f3319a
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-channel-syncs.ts
@@ -0,0 +1,319 @@
1import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
2import { FIXTURE_URLS } from '@tests/shared/tests.js'
3import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models'
4import {
5 ChannelSyncsCommand,
6 createSingleServer,
7 makePostBodyRequest,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@peertube/peertube-server-commands'
12
13describe('Test video channel sync API validator', () => {
14 const path = '/api/v1/video-channel-syncs'
15 let server: PeerTubeServer
16 let command: ChannelSyncsCommand
17 let rootChannelId: number
18 let rootChannelSyncId: number
19 const userInfo = {
20 accessToken: '',
21 username: 'user1',
22 id: -1,
23 channelId: -1,
24 syncId: -1
25 }
26
27 async function withChannelSyncDisabled<T> (callback: () => Promise<T>): Promise<void> {
28 try {
29 await server.config.disableChannelSync()
30 await callback()
31 } finally {
32 await server.config.enableChannelSync()
33 }
34 }
35
36 async function withMaxSyncsPerUser<T> (maxSync: number, callback: () => Promise<T>): Promise<void> {
37 const origConfig = await server.config.getCustomConfig()
38
39 await server.config.updateExistingSubConfig({
40 newConfig: {
41 import: {
42 videoChannelSynchronization: {
43 maxPerUser: maxSync
44 }
45 }
46 }
47 })
48
49 try {
50 await callback()
51 } finally {
52 await server.config.updateCustomConfig({ newCustomConfig: origConfig })
53 }
54 }
55
56 before(async function () {
57 this.timeout(30_000)
58
59 server = await createSingleServer(1)
60
61 await setAccessTokensToServers([ server ])
62 await setDefaultVideoChannel([ server ])
63
64 command = server.channelSyncs
65
66 rootChannelId = server.store.channel.id
67
68 {
69 userInfo.accessToken = await server.users.generateUserAndToken(userInfo.username)
70
71 const { videoChannels, id: userId } = await server.users.getMyInfo({ token: userInfo.accessToken })
72 userInfo.id = userId
73 userInfo.channelId = videoChannels[0].id
74 }
75
76 await server.config.enableChannelSync()
77 })
78
79 describe('When creating a sync', function () {
80 let baseCorrectParams: VideoChannelSyncCreate
81
82 before(function () {
83 baseCorrectParams = {
84 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
85 videoChannelId: rootChannelId
86 }
87 })
88
89 it('Should fail when sync is disabled', async function () {
90 await withChannelSyncDisabled(async () => {
91 await command.create({
92 token: server.accessToken,
93 attributes: baseCorrectParams,
94 expectedStatus: HttpStatusCode.FORBIDDEN_403
95 })
96 })
97 })
98
99 it('Should fail with nothing', async function () {
100 const fields = {}
101 await makePostBodyRequest({
102 url: server.url,
103 path,
104 token: server.accessToken,
105 fields,
106 expectedStatus: HttpStatusCode.BAD_REQUEST_400
107 })
108 })
109
110 it('Should fail with no authentication', async function () {
111 await command.create({
112 token: null,
113 attributes: baseCorrectParams,
114 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
115 })
116 })
117
118 it('Should fail without a target url', async function () {
119 const attributes: VideoChannelSyncCreate = {
120 ...baseCorrectParams,
121 externalChannelUrl: null
122 }
123 await command.create({
124 token: server.accessToken,
125 attributes,
126 expectedStatus: HttpStatusCode.BAD_REQUEST_400
127 })
128 })
129
130 it('Should fail without a channelId', async function () {
131 const attributes: VideoChannelSyncCreate = {
132 ...baseCorrectParams,
133 videoChannelId: null
134 }
135 await command.create({
136 token: server.accessToken,
137 attributes,
138 expectedStatus: HttpStatusCode.BAD_REQUEST_400
139 })
140 })
141
142 it('Should fail with a channelId refering nothing', async function () {
143 const attributes: VideoChannelSyncCreate = {
144 ...baseCorrectParams,
145 videoChannelId: 42
146 }
147 await command.create({
148 token: server.accessToken,
149 attributes,
150 expectedStatus: HttpStatusCode.NOT_FOUND_404
151 })
152 })
153
154 it('Should fail to create a sync when the user does not own the channel', async function () {
155 await command.create({
156 token: userInfo.accessToken,
157 attributes: baseCorrectParams,
158 expectedStatus: HttpStatusCode.FORBIDDEN_403
159 })
160 })
161
162 it('Should succeed to create a sync with root and for another user\'s channel', async function () {
163 const { videoChannelSync } = await command.create({
164 token: server.accessToken,
165 attributes: {
166 ...baseCorrectParams,
167 videoChannelId: userInfo.channelId
168 },
169 expectedStatus: HttpStatusCode.OK_200
170 })
171 userInfo.syncId = videoChannelSync.id
172 })
173
174 it('Should succeed with the correct parameters', async function () {
175 const { videoChannelSync } = await command.create({
176 token: server.accessToken,
177 attributes: baseCorrectParams,
178 expectedStatus: HttpStatusCode.OK_200
179 })
180 rootChannelSyncId = videoChannelSync.id
181 })
182
183 it('Should fail when the user exceeds allowed number of synchronizations', async function () {
184 await withMaxSyncsPerUser(1, async () => {
185 await command.create({
186 token: server.accessToken,
187 attributes: {
188 ...baseCorrectParams,
189 videoChannelId: userInfo.channelId
190 },
191 expectedStatus: HttpStatusCode.BAD_REQUEST_400
192 })
193 })
194 })
195 })
196
197 describe('When listing my channel syncs', function () {
198 const myPath = '/api/v1/accounts/root/video-channel-syncs'
199
200 it('Should fail with a bad start pagination', async function () {
201 await checkBadStartPagination(server.url, myPath, server.accessToken)
202 })
203
204 it('Should fail with a bad count pagination', async function () {
205 await checkBadCountPagination(server.url, myPath, server.accessToken)
206 })
207
208 it('Should fail with an incorrect sort', async function () {
209 await checkBadSortPagination(server.url, myPath, server.accessToken)
210 })
211
212 it('Should succeed with the correct parameters', async function () {
213 await command.listByAccount({
214 accountName: 'root',
215 token: server.accessToken,
216 expectedStatus: HttpStatusCode.OK_200
217 })
218 })
219
220 it('Should fail with no authentication', async function () {
221 await command.listByAccount({
222 accountName: 'root',
223 token: null,
224 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
225 })
226 })
227
228 it('Should fail when a simple user lists another user\'s synchronizations', async function () {
229 await command.listByAccount({
230 accountName: 'root',
231 token: userInfo.accessToken,
232 expectedStatus: HttpStatusCode.FORBIDDEN_403
233 })
234 })
235
236 it('Should succeed when root lists another user\'s synchronizations', async function () {
237 await command.listByAccount({
238 accountName: userInfo.username,
239 token: server.accessToken,
240 expectedStatus: HttpStatusCode.OK_200
241 })
242 })
243
244 it('Should succeed even with synchronization disabled', async function () {
245 await withChannelSyncDisabled(async function () {
246 await command.listByAccount({
247 accountName: 'root',
248 token: server.accessToken,
249 expectedStatus: HttpStatusCode.OK_200
250 })
251 })
252 })
253 })
254
255 describe('When triggering deletion', function () {
256 it('should fail with no authentication', async function () {
257 await command.delete({
258 channelSyncId: userInfo.syncId,
259 token: null,
260 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
261 })
262 })
263
264 it('Should fail when channelSyncId does not refer to any sync', async function () {
265 await command.delete({
266 channelSyncId: 42,
267 token: server.accessToken,
268 expectedStatus: HttpStatusCode.NOT_FOUND_404
269 })
270 })
271
272 it('Should fail when sync is not owned by the user', async function () {
273 await command.delete({
274 channelSyncId: rootChannelSyncId,
275 token: userInfo.accessToken,
276 expectedStatus: HttpStatusCode.FORBIDDEN_403
277 })
278 })
279
280 it('Should succeed when root delete a sync they do not own', async function () {
281 await command.delete({
282 channelSyncId: userInfo.syncId,
283 token: server.accessToken,
284 expectedStatus: HttpStatusCode.NO_CONTENT_204
285 })
286 })
287
288 it('should succeed when user delete a sync they own', async function () {
289 const { videoChannelSync } = await command.create({
290 attributes: {
291 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
292 videoChannelId: userInfo.channelId
293 },
294 token: server.accessToken,
295 expectedStatus: HttpStatusCode.OK_200
296 })
297
298 await command.delete({
299 channelSyncId: videoChannelSync.id,
300 token: server.accessToken,
301 expectedStatus: HttpStatusCode.NO_CONTENT_204
302 })
303 })
304
305 it('Should succeed even when synchronization is disabled', async function () {
306 await withChannelSyncDisabled(async function () {
307 await command.delete({
308 channelSyncId: rootChannelSyncId,
309 token: server.accessToken,
310 expectedStatus: HttpStatusCode.NO_CONTENT_204
311 })
312 })
313 })
314 })
315
316 after(async function () {
317 await server?.kill()
318 })
319})
diff --git a/packages/tests/src/api/check-params/video-channels.ts b/packages/tests/src/api/check-params/video-channels.ts
new file mode 100644
index 000000000..84b962b19
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-channels.ts
@@ -0,0 +1,379 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { omit } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models'
6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
7import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
8import {
9 ChannelsCommand,
10 cleanupTests,
11 createSingleServer,
12 makeGetRequest,
13 makePostBodyRequest,
14 makePutBodyRequest,
15 makeUploadRequest,
16 PeerTubeServer,
17 setAccessTokensToServers
18} from '@peertube/peertube-server-commands'
19
20describe('Test video channels API validator', function () {
21 const videoChannelPath = '/api/v1/video-channels'
22 let server: PeerTubeServer
23 const userInfo = {
24 accessToken: '',
25 channelName: 'fake_channel',
26 id: -1,
27 videoQuota: -1,
28 videoQuotaDaily: -1
29 }
30 let command: ChannelsCommand
31
32 // ---------------------------------------------------------------
33
34 before(async function () {
35 this.timeout(30000)
36
37 server = await createSingleServer(1)
38
39 await setAccessTokensToServers([ server ])
40
41 const userCreds = {
42 username: 'fake',
43 password: 'fake_password'
44 }
45
46 {
47 const user = await server.users.create({ username: userCreds.username, password: userCreds.password })
48 userInfo.id = user.id
49 userInfo.accessToken = await server.login.getAccessToken(userCreds)
50 }
51
52 command = server.channels
53 })
54
55 describe('When listing a video channels', function () {
56 it('Should fail with a bad start pagination', async function () {
57 await checkBadStartPagination(server.url, videoChannelPath, server.accessToken)
58 })
59
60 it('Should fail with a bad count pagination', async function () {
61 await checkBadCountPagination(server.url, videoChannelPath, server.accessToken)
62 })
63
64 it('Should fail with an incorrect sort', async function () {
65 await checkBadSortPagination(server.url, videoChannelPath, server.accessToken)
66 })
67 })
68
69 describe('When listing account video channels', function () {
70 const accountChannelPath = '/api/v1/accounts/fake/video-channels'
71
72 it('Should fail with a bad start pagination', async function () {
73 await checkBadStartPagination(server.url, accountChannelPath, server.accessToken)
74 })
75
76 it('Should fail with a bad count pagination', async function () {
77 await checkBadCountPagination(server.url, accountChannelPath, server.accessToken)
78 })
79
80 it('Should fail with an incorrect sort', async function () {
81 await checkBadSortPagination(server.url, accountChannelPath, server.accessToken)
82 })
83
84 it('Should fail with a unknown account', async function () {
85 await server.channels.listByAccount({ accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
86 })
87
88 it('Should succeed with the correct parameters', async function () {
89 await makeGetRequest({
90 url: server.url,
91 path: accountChannelPath,
92 expectedStatus: HttpStatusCode.OK_200
93 })
94 })
95 })
96
97 describe('When adding a video channel', function () {
98 const baseCorrectParams = {
99 name: 'super_channel',
100 displayName: 'hello',
101 description: 'super description',
102 support: 'super support text'
103 }
104
105 it('Should fail with a non authenticated user', async function () {
106 await makePostBodyRequest({
107 url: server.url,
108 path: videoChannelPath,
109 token: 'none',
110 fields: baseCorrectParams,
111 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
112 })
113 })
114
115 it('Should fail with nothing', async function () {
116 const fields = {}
117 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
118 })
119
120 it('Should fail without a name', async function () {
121 const fields = omit(baseCorrectParams, [ 'name' ])
122 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
123 })
124
125 it('Should fail with a bad name', async function () {
126 const fields = { ...baseCorrectParams, name: 'super name' }
127 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
128 })
129
130 it('Should fail without a name', async function () {
131 const fields = omit(baseCorrectParams, [ 'displayName' ])
132 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
133 })
134
135 it('Should fail with a long name', async function () {
136 const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) }
137 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
138 })
139
140 it('Should fail with a long description', async function () {
141 const fields = { ...baseCorrectParams, description: 'super'.repeat(201) }
142 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
143 })
144
145 it('Should fail with a long support text', async function () {
146 const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
147 await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields })
148 })
149
150 it('Should succeed with the correct parameters', async function () {
151 await makePostBodyRequest({
152 url: server.url,
153 path: videoChannelPath,
154 token: server.accessToken,
155 fields: baseCorrectParams,
156 expectedStatus: HttpStatusCode.OK_200
157 })
158 })
159
160 it('Should fail when adding a channel with the same username', async function () {
161 await makePostBodyRequest({
162 url: server.url,
163 path: videoChannelPath,
164 token: server.accessToken,
165 fields: baseCorrectParams,
166 expectedStatus: HttpStatusCode.CONFLICT_409
167 })
168 })
169 })
170
171 describe('When updating a video channel', function () {
172 const baseCorrectParams: VideoChannelUpdate = {
173 displayName: 'hello',
174 description: 'super description',
175 support: 'toto',
176 bulkVideosSupportUpdate: false
177 }
178 let path: string
179
180 before(async function () {
181 path = videoChannelPath + '/super_channel'
182 })
183
184 it('Should fail with a non authenticated user', async function () {
185 await makePutBodyRequest({
186 url: server.url,
187 path,
188 token: 'hi',
189 fields: baseCorrectParams,
190 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
191 })
192 })
193
194 it('Should fail with another authenticated user', async function () {
195 await makePutBodyRequest({
196 url: server.url,
197 path,
198 token: userInfo.accessToken,
199 fields: baseCorrectParams,
200 expectedStatus: HttpStatusCode.FORBIDDEN_403
201 })
202 })
203
204 it('Should fail with a long name', async function () {
205 const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) }
206 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
207 })
208
209 it('Should fail with a long description', async function () {
210 const fields = { ...baseCorrectParams, description: 'super'.repeat(201) }
211 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
212 })
213
214 it('Should fail with a long support text', async function () {
215 const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
216 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
217 })
218
219 it('Should fail with a bad bulkVideosSupportUpdate field', async function () {
220 const fields = { ...baseCorrectParams, bulkVideosSupportUpdate: 'super' }
221 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
222 })
223
224 it('Should succeed with the correct parameters', async function () {
225 await makePutBodyRequest({
226 url: server.url,
227 path,
228 token: server.accessToken,
229 fields: baseCorrectParams,
230 expectedStatus: HttpStatusCode.NO_CONTENT_204
231 })
232 })
233 })
234
235 describe('When updating video channel avatars/banners', function () {
236 const types = [ 'avatar', 'banner' ]
237 let path: string
238
239 before(async function () {
240 path = videoChannelPath + '/super_channel'
241 })
242
243 it('Should fail with an incorrect input file', async function () {
244 for (const type of types) {
245 const fields = {}
246 const attaches = {
247 [type + 'file']: buildAbsoluteFixturePath('video_short.mp4')
248 }
249
250 await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches })
251 }
252 })
253
254 it('Should fail with a big file', async function () {
255 for (const type of types) {
256 const fields = {}
257 const attaches = {
258 [type + 'file']: buildAbsoluteFixturePath('avatar-big.png')
259 }
260 await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches })
261 }
262 })
263
264 it('Should fail with an unauthenticated user', async function () {
265 for (const type of types) {
266 const fields = {}
267 const attaches = {
268 [type + 'file']: buildAbsoluteFixturePath('avatar.png')
269 }
270 await makeUploadRequest({
271 url: server.url,
272 path: `${path}/${type}/pick`,
273 fields,
274 attaches,
275 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
276 })
277 }
278 })
279
280 it('Should succeed with the correct params', async function () {
281 for (const type of types) {
282 const fields = {}
283 const attaches = {
284 [type + 'file']: buildAbsoluteFixturePath('avatar.png')
285 }
286 await makeUploadRequest({
287 url: server.url,
288 path: `${path}/${type}/pick`,
289 token: server.accessToken,
290 fields,
291 attaches,
292 expectedStatus: HttpStatusCode.OK_200
293 })
294 }
295 })
296 })
297
298 describe('When getting a video channel', function () {
299 it('Should return the list of the video channels with nothing', async function () {
300 const res = await makeGetRequest({
301 url: server.url,
302 path: videoChannelPath,
303 expectedStatus: HttpStatusCode.OK_200
304 })
305
306 expect(res.body.data).to.be.an('array')
307 })
308
309 it('Should return 404 with an incorrect video channel', async function () {
310 await makeGetRequest({
311 url: server.url,
312 path: videoChannelPath + '/super_channel2',
313 expectedStatus: HttpStatusCode.NOT_FOUND_404
314 })
315 })
316
317 it('Should succeed with the correct parameters', async function () {
318 await makeGetRequest({
319 url: server.url,
320 path: videoChannelPath + '/super_channel',
321 expectedStatus: HttpStatusCode.OK_200
322 })
323 })
324 })
325
326 describe('When getting channel followers', function () {
327 const path = '/api/v1/video-channels/super_channel/followers'
328
329 it('Should fail with a bad start pagination', async function () {
330 await checkBadStartPagination(server.url, path, server.accessToken)
331 })
332
333 it('Should fail with a bad count pagination', async function () {
334 await checkBadCountPagination(server.url, path, server.accessToken)
335 })
336
337 it('Should fail with an incorrect sort', async function () {
338 await checkBadSortPagination(server.url, path, server.accessToken)
339 })
340
341 it('Should fail with a unauthenticated user', async function () {
342 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
343 })
344
345 it('Should fail with a another user', async function () {
346 await makeGetRequest({ url: server.url, path, token: userInfo.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
347 })
348
349 it('Should succeed with the correct params', async function () {
350 await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
351 })
352 })
353
354 describe('When deleting a video channel', function () {
355 it('Should fail with a non authenticated user', async function () {
356 await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
357 })
358
359 it('Should fail with another authenticated user', async function () {
360 await command.delete({ token: userInfo.accessToken, channelName: 'super_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
361 })
362
363 it('Should fail with an unknown video channel id', async function () {
364 await command.delete({ channelName: 'super_channel2', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
365 })
366
367 it('Should succeed with the correct parameters', async function () {
368 await command.delete({ channelName: 'super_channel' })
369 })
370
371 it('Should fail to delete the last user video channel', async function () {
372 await command.delete({ channelName: 'root_channel', expectedStatus: HttpStatusCode.CONFLICT_409 })
373 })
374 })
375
376 after(async function () {
377 await cleanupTests([ server ])
378 })
379})
diff --git a/packages/tests/src/api/check-params/video-comments.ts b/packages/tests/src/api/check-params/video-comments.ts
new file mode 100644
index 000000000..177361606
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-comments.ts
@@ -0,0 +1,484 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
5import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 makeDeleteRequest,
10 makeGetRequest,
11 makePostBodyRequest,
12 PeerTubeServer,
13 setAccessTokensToServers
14} from '@peertube/peertube-server-commands'
15
16describe('Test video comments API validator', function () {
17 let pathThread: string
18 let pathComment: string
19
20 let server: PeerTubeServer
21
22 let video: VideoCreateResult
23
24 let userAccessToken: string
25 let userAccessToken2: string
26
27 let commentId: number
28 let privateCommentId: number
29 let privateVideo: VideoCreateResult
30
31 // ---------------------------------------------------------------
32
33 before(async function () {
34 this.timeout(120000)
35
36 server = await createSingleServer(1)
37
38 await setAccessTokensToServers([ server ])
39
40 {
41 video = await server.videos.upload({ attributes: {} })
42 pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads'
43 }
44
45 {
46 privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } })
47 }
48
49 {
50 const created = await server.comments.createThread({ videoId: video.uuid, text: 'coucou' })
51 commentId = created.id
52 pathComment = '/api/v1/videos/' + video.uuid + '/comments/' + commentId
53 }
54
55 {
56 const created = await server.comments.createThread({ videoId: privateVideo.uuid, text: 'coucou' })
57 privateCommentId = created.id
58 }
59
60 {
61 const user = { username: 'user1', password: 'my super password' }
62 await server.users.create({ username: user.username, password: user.password })
63 userAccessToken = await server.login.getAccessToken(user)
64 }
65
66 {
67 const user = { username: 'user2', password: 'my super password' }
68 await server.users.create({ username: user.username, password: user.password })
69 userAccessToken2 = await server.login.getAccessToken(user)
70 }
71 })
72
73 describe('When listing video comment threads', function () {
74 it('Should fail with a bad start pagination', async function () {
75 await checkBadStartPagination(server.url, pathThread, server.accessToken)
76 })
77
78 it('Should fail with a bad count pagination', async function () {
79 await checkBadCountPagination(server.url, pathThread, server.accessToken)
80 })
81
82 it('Should fail with an incorrect sort', async function () {
83 await checkBadSortPagination(server.url, pathThread, server.accessToken)
84 })
85
86 it('Should fail with an incorrect video', async function () {
87 await makeGetRequest({
88 url: server.url,
89 path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads',
90 expectedStatus: HttpStatusCode.NOT_FOUND_404
91 })
92 })
93
94 it('Should fail with a private video without token', async function () {
95 await makeGetRequest({
96 url: server.url,
97 path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
98 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
99 })
100 })
101
102 it('Should fail with another user token', async function () {
103 await makeGetRequest({
104 url: server.url,
105 token: userAccessToken,
106 path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
107 expectedStatus: HttpStatusCode.FORBIDDEN_403
108 })
109 })
110
111 it('Should succeed with the correct params', async function () {
112 await makeGetRequest({
113 url: server.url,
114 token: server.accessToken,
115 path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
116 expectedStatus: HttpStatusCode.OK_200
117 })
118 })
119 })
120
121 describe('When listing comments of a thread', function () {
122 it('Should fail with an incorrect video', async function () {
123 await makeGetRequest({
124 url: server.url,
125 path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads/' + commentId,
126 expectedStatus: HttpStatusCode.NOT_FOUND_404
127 })
128 })
129
130 it('Should fail with an incorrect thread id', async function () {
131 await makeGetRequest({
132 url: server.url,
133 path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/156',
134 expectedStatus: HttpStatusCode.NOT_FOUND_404
135 })
136 })
137
138 it('Should fail with a private video without token', async function () {
139 await makeGetRequest({
140 url: server.url,
141 path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
142 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
143 })
144 })
145
146 it('Should fail with another user token', async function () {
147 await makeGetRequest({
148 url: server.url,
149 token: userAccessToken,
150 path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
151 expectedStatus: HttpStatusCode.FORBIDDEN_403
152 })
153 })
154
155 it('Should success with the correct params', async function () {
156 await makeGetRequest({
157 url: server.url,
158 token: server.accessToken,
159 path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId,
160 expectedStatus: HttpStatusCode.OK_200
161 })
162
163 await makeGetRequest({
164 url: server.url,
165 path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/' + commentId,
166 expectedStatus: HttpStatusCode.OK_200
167 })
168 })
169 })
170
171 describe('When adding a video thread', function () {
172
173 it('Should fail with a non authenticated user', async function () {
174 const fields = {
175 text: 'text'
176 }
177 await makePostBodyRequest({
178 url: server.url,
179 path: pathThread,
180 token: 'none',
181 fields,
182 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
183 })
184 })
185
186 it('Should fail with nothing', async function () {
187 const fields = {}
188 await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields })
189 })
190
191 it('Should fail with a short comment', async function () {
192 const fields = {
193 text: ''
194 }
195 await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields })
196 })
197
198 it('Should fail with a long comment', async function () {
199 const fields = {
200 text: 'h'.repeat(10001)
201 }
202 await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields })
203 })
204
205 it('Should fail with an incorrect video', async function () {
206 const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads'
207 const fields = { text: 'super comment' }
208
209 await makePostBodyRequest({
210 url: server.url,
211 path,
212 token: server.accessToken,
213 fields,
214 expectedStatus: HttpStatusCode.NOT_FOUND_404
215 })
216 })
217
218 it('Should fail with a private video of another user', async function () {
219 const fields = { text: 'super comment' }
220
221 await makePostBodyRequest({
222 url: server.url,
223 path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads',
224 token: userAccessToken,
225 fields,
226 expectedStatus: HttpStatusCode.FORBIDDEN_403
227 })
228 })
229
230 it('Should succeed with the correct parameters', async function () {
231 const fields = { text: 'super comment' }
232
233 await makePostBodyRequest({
234 url: server.url,
235 path: pathThread,
236 token: server.accessToken,
237 fields,
238 expectedStatus: HttpStatusCode.OK_200
239 })
240 })
241 })
242
243 describe('When adding a comment to a thread', function () {
244
245 it('Should fail with a non authenticated user', async function () {
246 const fields = {
247 text: 'text'
248 }
249 await makePostBodyRequest({
250 url: server.url,
251 path: pathComment,
252 token: 'none',
253 fields,
254 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
255 })
256 })
257
258 it('Should fail with nothing', async function () {
259 const fields = {}
260 await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields })
261 })
262
263 it('Should fail with a short comment', async function () {
264 const fields = {
265 text: ''
266 }
267 await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields })
268 })
269
270 it('Should fail with a long comment', async function () {
271 const fields = {
272 text: 'h'.repeat(10001)
273 }
274 await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields })
275 })
276
277 it('Should fail with an incorrect video', async function () {
278 const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId
279 const fields = {
280 text: 'super comment'
281 }
282 await makePostBodyRequest({
283 url: server.url,
284 path,
285 token: server.accessToken,
286 fields,
287 expectedStatus: HttpStatusCode.NOT_FOUND_404
288 })
289 })
290
291 it('Should fail with a private video of another user', async function () {
292 const fields = { text: 'super comment' }
293
294 await makePostBodyRequest({
295 url: server.url,
296 path: '/api/v1/videos/' + privateVideo.uuid + '/comments/' + privateCommentId,
297 token: userAccessToken,
298 fields,
299 expectedStatus: HttpStatusCode.FORBIDDEN_403
300 })
301 })
302
303 it('Should fail with an incorrect comment', async function () {
304 const path = '/api/v1/videos/' + video.uuid + '/comments/124'
305 const fields = {
306 text: 'super comment'
307 }
308 await makePostBodyRequest({
309 url: server.url,
310 path,
311 token: server.accessToken,
312 fields,
313 expectedStatus: HttpStatusCode.NOT_FOUND_404
314 })
315 })
316
317 it('Should succeed with the correct parameters', async function () {
318 const fields = {
319 text: 'super comment'
320 }
321 await makePostBodyRequest({
322 url: server.url,
323 path: pathComment,
324 token: server.accessToken,
325 fields,
326 expectedStatus: HttpStatusCode.OK_200
327 })
328 })
329 })
330
331 describe('When removing video comments', function () {
332 it('Should fail with a non authenticated user', async function () {
333 await makeDeleteRequest({ url: server.url, path: pathComment, token: 'none', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
334 })
335
336 it('Should fail with another user', async function () {
337 await makeDeleteRequest({
338 url: server.url,
339 path: pathComment,
340 token: userAccessToken,
341 expectedStatus: HttpStatusCode.FORBIDDEN_403
342 })
343 })
344
345 it('Should fail with an incorrect video', async function () {
346 const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId
347 await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
348 })
349
350 it('Should fail with an incorrect comment', async function () {
351 const path = '/api/v1/videos/' + video.uuid + '/comments/124'
352 await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
353 })
354
355 it('Should succeed with the same user', async function () {
356 let commentToDelete: number
357
358 {
359 const created = await server.comments.createThread({ videoId: video.uuid, token: userAccessToken, text: 'hello' })
360 commentToDelete = created.id
361 }
362
363 const path = '/api/v1/videos/' + video.uuid + '/comments/' + commentToDelete
364
365 await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
366 await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
367 })
368
369 it('Should succeed with the owner of the video', async function () {
370 let commentToDelete: number
371 let anotherVideoUUID: string
372
373 {
374 const { uuid } = await server.videos.upload({ token: userAccessToken, attributes: { name: 'video' } })
375 anotherVideoUUID = uuid
376 }
377
378 {
379 const created = await server.comments.createThread({ videoId: anotherVideoUUID, text: 'hello' })
380 commentToDelete = created.id
381 }
382
383 const path = '/api/v1/videos/' + anotherVideoUUID + '/comments/' + commentToDelete
384
385 await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
386 await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
387 })
388
389 it('Should succeed with the correct parameters', async function () {
390 await makeDeleteRequest({
391 url: server.url,
392 path: pathComment,
393 token: server.accessToken,
394 expectedStatus: HttpStatusCode.NO_CONTENT_204
395 })
396 })
397 })
398
399 describe('When a video has comments disabled', function () {
400 before(async function () {
401 video = await server.videos.upload({ attributes: { commentsEnabled: false } })
402 pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads'
403 })
404
405 it('Should return an empty thread list', async function () {
406 const res = await makeGetRequest({
407 url: server.url,
408 path: pathThread,
409 expectedStatus: HttpStatusCode.OK_200
410 })
411 expect(res.body.total).to.equal(0)
412 expect(res.body.data).to.have.lengthOf(0)
413 })
414
415 it('Should return an thread comments list')
416
417 it('Should return conflict on thread add', async function () {
418 const fields = {
419 text: 'super comment'
420 }
421 await makePostBodyRequest({
422 url: server.url,
423 path: pathThread,
424 token: server.accessToken,
425 fields,
426 expectedStatus: HttpStatusCode.CONFLICT_409
427 })
428 })
429
430 it('Should return conflict on comment thread add')
431 })
432
433 describe('When listing admin comments threads', function () {
434 const path = '/api/v1/videos/comments'
435
436 it('Should fail with a bad start pagination', async function () {
437 await checkBadStartPagination(server.url, path, server.accessToken)
438 })
439
440 it('Should fail with a bad count pagination', async function () {
441 await checkBadCountPagination(server.url, path, server.accessToken)
442 })
443
444 it('Should fail with an incorrect sort', async function () {
445 await checkBadSortPagination(server.url, path, server.accessToken)
446 })
447
448 it('Should fail with a non authenticated user', async function () {
449 await makeGetRequest({
450 url: server.url,
451 path,
452 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
453 })
454 })
455
456 it('Should fail with a non admin user', async function () {
457 await makeGetRequest({
458 url: server.url,
459 path,
460 token: userAccessToken,
461 expectedStatus: HttpStatusCode.FORBIDDEN_403
462 })
463 })
464
465 it('Should succeed with the correct params', async function () {
466 await makeGetRequest({
467 url: server.url,
468 path,
469 token: server.accessToken,
470 query: {
471 isLocal: false,
472 search: 'toto',
473 searchAccount: 'toto',
474 searchVideo: 'toto'
475 },
476 expectedStatus: HttpStatusCode.OK_200
477 })
478 })
479 })
480
481 after(async function () {
482 await cleanupTests([ server ])
483 })
484})
diff --git a/packages/tests/src/api/check-params/video-files.ts b/packages/tests/src/api/check-params/video-files.ts
new file mode 100644
index 000000000..b5819ff19
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-files.ts
@@ -0,0 +1,195 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { getAllFiles } from '@peertube/peertube-core-utils'
4import { HttpStatusCode, UserRole, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 makeRawRequest,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test videos files', function () {
16 let servers: PeerTubeServer[]
17
18 let userToken: string
19 let moderatorToken: string
20
21 // ---------------------------------------------------------------
22
23 before(async function () {
24 this.timeout(300_000)
25
26 servers = await createMultipleServers(2)
27 await setAccessTokensToServers(servers)
28
29 await doubleFollow(servers[0], servers[1])
30
31 userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER)
32 moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR)
33 })
34
35 describe('Getting metadata', function () {
36 let video: VideoDetails
37
38 before(async function () {
39 const { uuid } = await servers[0].videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
40 video = await servers[0].videos.getWithToken({ id: uuid })
41 })
42
43 it('Should not get metadata of private video without token', async function () {
44 for (const file of getAllFiles(video)) {
45 await makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
46 }
47 })
48
49 it('Should not get metadata of private video without the appropriate token', async function () {
50 for (const file of getAllFiles(video)) {
51 await makeRawRequest({ url: file.metadataUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
52 }
53 })
54
55 it('Should get metadata of private video with the appropriate token', async function () {
56 for (const file of getAllFiles(video)) {
57 await makeRawRequest({ url: file.metadataUrl, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 })
58 }
59 })
60 })
61
62 describe('Deleting files', function () {
63 let webVideoId: string
64 let hlsId: string
65 let remoteId: string
66
67 let validId1: string
68 let validId2: string
69
70 let hlsFileId: number
71 let webVideoFileId: number
72
73 let remoteHLSFileId: number
74 let remoteWebVideoFileId: number
75
76 before(async function () {
77 this.timeout(300_000)
78
79 {
80 const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' })
81 await waitJobs(servers)
82
83 const video = await servers[1].videos.get({ id: uuid })
84 remoteId = video.uuid
85 remoteHLSFileId = video.streamingPlaylists[0].files[0].id
86 remoteWebVideoFileId = video.files[0].id
87 }
88
89 {
90 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
91
92 {
93 const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' })
94 await waitJobs(servers)
95
96 const video = await servers[0].videos.get({ id: uuid })
97 validId1 = video.uuid
98 hlsFileId = video.streamingPlaylists[0].files[0].id
99 webVideoFileId = video.files[0].id
100 }
101
102 {
103 const { uuid } = await servers[0].videos.quickUpload({ name: 'both 2' })
104 validId2 = uuid
105 }
106 }
107
108 await waitJobs(servers)
109
110 {
111 await servers[0].config.enableTranscoding({ hls: true, webVideo: false })
112 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' })
113 hlsId = uuid
114 }
115
116 await waitJobs(servers)
117
118 {
119 await servers[0].config.enableTranscoding({ webVideo: true, hls: false })
120 const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' })
121 webVideoId = uuid
122 }
123
124 await waitJobs(servers)
125 })
126
127 it('Should not delete files of a unknown video', async function () {
128 const expectedStatus = HttpStatusCode.NOT_FOUND_404
129
130 await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus })
131 await servers[0].videos.removeAllWebVideoFiles({ videoId: 404, expectedStatus })
132
133 await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus })
134 await servers[0].videos.removeWebVideoFile({ videoId: 404, fileId: webVideoFileId, expectedStatus })
135 })
136
137 it('Should not delete unknown files', async function () {
138 const expectedStatus = HttpStatusCode.NOT_FOUND_404
139
140 await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webVideoFileId, expectedStatus })
141 await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: hlsFileId, expectedStatus })
142 })
143
144 it('Should not delete files of a remote video', async function () {
145 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
146
147 await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus })
148 await servers[0].videos.removeAllWebVideoFiles({ videoId: remoteId, expectedStatus })
149
150 await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus })
151 await servers[0].videos.removeWebVideoFile({ videoId: remoteId, fileId: remoteWebVideoFileId, expectedStatus })
152 })
153
154 it('Should not delete files by a non admin user', async function () {
155 const expectedStatus = HttpStatusCode.FORBIDDEN_403
156
157 await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus })
158 await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus })
159
160 await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: userToken, expectedStatus })
161 await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: moderatorToken, expectedStatus })
162
163 await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus })
164 await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus })
165
166 await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: userToken, expectedStatus })
167 await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: moderatorToken, expectedStatus })
168 })
169
170 it('Should not delete files if the files are not available', async function () {
171 await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
172 await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
173
174 await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
175 await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
176 })
177
178 it('Should not delete files if no both versions are available', async function () {
179 await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
180 await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
181 })
182
183 it('Should delete files if both versions are available', async function () {
184 await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId })
185 await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId })
186
187 await servers[0].videos.removeHLSPlaylist({ videoId: validId1 })
188 await servers[0].videos.removeAllWebVideoFiles({ videoId: validId2 })
189 })
190 })
191
192 after(async function () {
193 await cleanupTests(servers)
194 })
195})
diff --git a/packages/tests/src/api/check-params/video-imports.ts b/packages/tests/src/api/check-params/video-imports.ts
new file mode 100644
index 000000000..e078cedd6
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-imports.ts
@@ -0,0 +1,433 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { omit } from '@peertube/peertube-core-utils'
4import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
5import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
6import { FIXTURE_URLS } from '@tests/shared/tests.js'
7import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
8import {
9 cleanupTests,
10 createSingleServer,
11 makeGetRequest,
12 makePostBodyRequest,
13 makeUploadRequest,
14 PeerTubeServer,
15 setAccessTokensToServers,
16 setDefaultVideoChannel,
17 waitJobs
18} from '@peertube/peertube-server-commands'
19
20describe('Test video imports API validator', function () {
21 const path = '/api/v1/videos/imports'
22 let server: PeerTubeServer
23 let userAccessToken = ''
24 let channelId: number
25
26 // ---------------------------------------------------------------
27
28 before(async function () {
29 this.timeout(30000)
30
31 server = await createSingleServer(1)
32
33 await setAccessTokensToServers([ server ])
34 await setDefaultVideoChannel([ server ])
35
36 const username = 'user1'
37 const password = 'my super password'
38 await server.users.create({ username, password })
39 userAccessToken = await server.login.getAccessToken({ username, password })
40
41 {
42 const { videoChannels } = await server.users.getMyInfo()
43 channelId = videoChannels[0].id
44 }
45 })
46
47 describe('When listing my video imports', function () {
48 const myPath = '/api/v1/users/me/videos/imports'
49
50 it('Should fail with a bad start pagination', async function () {
51 await checkBadStartPagination(server.url, myPath, server.accessToken)
52 })
53
54 it('Should fail with a bad count pagination', async function () {
55 await checkBadCountPagination(server.url, myPath, server.accessToken)
56 })
57
58 it('Should fail with an incorrect sort', async function () {
59 await checkBadSortPagination(server.url, myPath, server.accessToken)
60 })
61
62 it('Should fail with a bad videoChannelSyncId param', async function () {
63 await makeGetRequest({
64 url: server.url,
65 path: myPath,
66 query: { videoChannelSyncId: 'toto' },
67 token: server.accessToken
68 })
69 })
70
71 it('Should success with the correct parameters', async function () {
72 await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
73 })
74 })
75
76 describe('When adding a video import', function () {
77 let baseCorrectParams
78
79 before(function () {
80 baseCorrectParams = {
81 targetUrl: FIXTURE_URLS.goodVideo,
82 name: 'my super name',
83 category: 5,
84 licence: 1,
85 language: 'pt',
86 nsfw: false,
87 commentsEnabled: true,
88 downloadEnabled: true,
89 waitTranscoding: true,
90 description: 'my super description',
91 support: 'my super support text',
92 tags: [ 'tag1', 'tag2' ],
93 privacy: VideoPrivacy.PUBLIC,
94 channelId
95 }
96 })
97
98 it('Should fail with nothing', async function () {
99 const fields = {}
100 await makePostBodyRequest({
101 url: server.url,
102 path,
103 token: server.accessToken,
104 fields,
105 expectedStatus: HttpStatusCode.BAD_REQUEST_400
106 })
107 })
108
109 it('Should fail without a target url', async function () {
110 const fields = omit(baseCorrectParams, [ 'targetUrl' ])
111 await makePostBodyRequest({
112 url: server.url,
113 path,
114 token: server.accessToken,
115 fields,
116 expectedStatus: HttpStatusCode.BAD_REQUEST_400
117 })
118 })
119
120 it('Should fail with a bad target url', async function () {
121 const fields = { ...baseCorrectParams, targetUrl: 'htt://hello' }
122
123 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
124 })
125
126 it('Should fail with localhost', async function () {
127 const fields = { ...baseCorrectParams, targetUrl: 'http://localhost:8000' }
128
129 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
130 })
131
132 it('Should fail with a private IP target urls', async function () {
133 const targetUrls = [
134 'http://127.0.0.1:8000',
135 'http://127.0.0.1',
136 'http://127.0.0.1/hello',
137 'https://192.168.1.42',
138 'http://192.168.1.42',
139 'http://127.0.0.1.cpy.re'
140 ]
141
142 for (const targetUrl of targetUrls) {
143 const fields = { ...baseCorrectParams, targetUrl }
144
145 await makePostBodyRequest({
146 url: server.url,
147 path,
148 token: server.accessToken,
149 fields,
150 expectedStatus: HttpStatusCode.FORBIDDEN_403
151 })
152 }
153 })
154
155 it('Should fail with a long name', async function () {
156 const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
157
158 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
159 })
160
161 it('Should fail with a bad category', async function () {
162 const fields = { ...baseCorrectParams, category: 125 }
163
164 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
165 })
166
167 it('Should fail with a bad licence', async function () {
168 const fields = { ...baseCorrectParams, licence: 125 }
169
170 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
171 })
172
173 it('Should fail with a bad language', async function () {
174 const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
175
176 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
177 })
178
179 it('Should fail with a long description', async function () {
180 const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
181
182 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
183 })
184
185 it('Should fail with a long support text', async function () {
186 const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
187
188 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
189 })
190
191 it('Should fail without a channel', async function () {
192 const fields = omit(baseCorrectParams, [ 'channelId' ])
193
194 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
195 })
196
197 it('Should fail with a bad channel', async function () {
198 const fields = { ...baseCorrectParams, channelId: 545454 }
199
200 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
201 })
202
203 it('Should fail with another user channel', async function () {
204 const user = {
205 username: 'fake',
206 password: 'fake_password'
207 }
208 await server.users.create({ username: user.username, password: user.password })
209
210 const accessTokenUser = await server.login.getAccessToken(user)
211 const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
212 const customChannelId = videoChannels[0].id
213
214 const fields = { ...baseCorrectParams, channelId: customChannelId }
215
216 await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
217 })
218
219 it('Should fail with too many tags', async function () {
220 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
221
222 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
223 })
224
225 it('Should fail with a tag length too low', async function () {
226 const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
227
228 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
229 })
230
231 it('Should fail with a tag length too big', async function () {
232 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
233
234 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
235 })
236
237 it('Should fail with an incorrect thumbnail file', async function () {
238 const fields = baseCorrectParams
239 const attaches = {
240 thumbnailfile: buildAbsoluteFixturePath('video_short.mp4')
241 }
242
243 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
244 })
245
246 it('Should fail with a big thumbnail file', async function () {
247 const fields = baseCorrectParams
248 const attaches = {
249 thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png')
250 }
251
252 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
253 })
254
255 it('Should fail with an incorrect preview file', async function () {
256 const fields = baseCorrectParams
257 const attaches = {
258 previewfile: buildAbsoluteFixturePath('video_short.mp4')
259 }
260
261 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
262 })
263
264 it('Should fail with a big preview file', async function () {
265 const fields = baseCorrectParams
266 const attaches = {
267 previewfile: buildAbsoluteFixturePath('custom-preview-big.png')
268 }
269
270 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
271 })
272
273 it('Should fail with an invalid torrent file', async function () {
274 const fields = omit(baseCorrectParams, [ 'targetUrl' ])
275 const attaches = {
276 torrentfile: buildAbsoluteFixturePath('avatar-big.png')
277 }
278
279 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
280 })
281
282 it('Should fail with an invalid magnet URI', async function () {
283 let fields = omit(baseCorrectParams, [ 'targetUrl' ])
284 fields = { ...fields, magnetUri: 'blabla' }
285
286 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
287 })
288
289 it('Should succeed with the correct parameters', async function () {
290 this.timeout(120000)
291
292 await makePostBodyRequest({
293 url: server.url,
294 path,
295 token: server.accessToken,
296 fields: baseCorrectParams,
297 expectedStatus: HttpStatusCode.OK_200
298 })
299 })
300
301 it('Should forbid to import http videos', async function () {
302 await server.config.updateCustomSubConfig({
303 newConfig: {
304 import: {
305 videos: {
306 http: {
307 enabled: false
308 },
309 torrent: {
310 enabled: true
311 }
312 }
313 }
314 }
315 })
316
317 await makePostBodyRequest({
318 url: server.url,
319 path,
320 token: server.accessToken,
321 fields: baseCorrectParams,
322 expectedStatus: HttpStatusCode.CONFLICT_409
323 })
324 })
325
326 it('Should forbid to import torrent videos', async function () {
327 await server.config.updateCustomSubConfig({
328 newConfig: {
329 import: {
330 videos: {
331 http: {
332 enabled: true
333 },
334 torrent: {
335 enabled: false
336 }
337 }
338 }
339 }
340 })
341
342 let fields = omit(baseCorrectParams, [ 'targetUrl' ])
343 fields = { ...fields, magnetUri: FIXTURE_URLS.magnet }
344
345 await makePostBodyRequest({
346 url: server.url,
347 path,
348 token: server.accessToken,
349 fields,
350 expectedStatus: HttpStatusCode.CONFLICT_409
351 })
352
353 fields = omit(fields, [ 'magnetUri' ])
354 const attaches = {
355 torrentfile: buildAbsoluteFixturePath('video-720p.torrent')
356 }
357
358 await makeUploadRequest({
359 url: server.url,
360 path,
361 token: server.accessToken,
362 fields,
363 attaches,
364 expectedStatus: HttpStatusCode.CONFLICT_409
365 })
366 })
367 })
368
369 describe('Deleting/cancelling a video import', function () {
370 let importId: number
371
372 async function importVideo () {
373 const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
374 const res = await server.imports.importVideo({ attributes })
375
376 return res.id
377 }
378
379 before(async function () {
380 importId = await importVideo()
381 })
382
383 it('Should fail with an invalid import id', async function () {
384 await server.imports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
385 await server.imports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
386 })
387
388 it('Should fail with an unknown import id', async function () {
389 await server.imports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
390 await server.imports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
391 })
392
393 it('Should fail without token', async function () {
394 await server.imports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
395 await server.imports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
396 })
397
398 it('Should fail with another user token', async function () {
399 await server.imports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
400 await server.imports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
401 })
402
403 it('Should fail to cancel non pending import', async function () {
404 this.timeout(60000)
405
406 await waitJobs([ server ])
407
408 await server.imports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
409 })
410
411 it('Should succeed to delete an import', async function () {
412 await server.imports.delete({ importId })
413 })
414
415 it('Should fail to delete a pending import', async function () {
416 await server.jobs.pauseJobQueue()
417
418 importId = await importVideo()
419
420 await server.imports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 })
421 })
422
423 it('Should succeed to cancel an import', async function () {
424 importId = await importVideo()
425
426 await server.imports.cancel({ importId })
427 })
428 })
429
430 after(async function () {
431 await cleanupTests([ server ])
432 })
433})
diff --git a/packages/tests/src/api/check-params/video-passwords.ts b/packages/tests/src/api/check-params/video-passwords.ts
new file mode 100644
index 000000000..3f57ebe74
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-passwords.ts
@@ -0,0 +1,604 @@
1import { expect } from 'chai'
2import {
3 HttpStatusCode,
4 HttpStatusCodeType,
5 PeerTubeProblemDocument,
6 ServerErrorCode,
7 VideoCreateResult,
8 VideoPrivacy
9} from '@peertube/peertube-models'
10import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
11import {
12 cleanupTests,
13 createSingleServer,
14 makePostBodyRequest,
15 PeerTubeServer,
16 setAccessTokensToServers
17} from '@peertube/peertube-server-commands'
18import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
19import { FIXTURE_URLS } from '@tests/shared/tests.js'
20import { checkUploadVideoParam } from '@tests/shared/videos.js'
21
22describe('Test video passwords validator', function () {
23 let path: string
24 let server: PeerTubeServer
25 let userAccessToken = ''
26 let video: VideoCreateResult
27 let channelId: number
28 let publicVideo: VideoCreateResult
29 let commentId: number
30 // ---------------------------------------------------------------
31
32 before(async function () {
33 this.timeout(50000)
34
35 server = await createSingleServer(1)
36
37 await setAccessTokensToServers([ server ])
38
39 await server.config.updateCustomSubConfig({
40 newConfig: {
41 live: {
42 enabled: true,
43 latencySetting: {
44 enabled: false
45 },
46 allowReplay: false
47 },
48 import: {
49 videos: {
50 http:{
51 enabled: true
52 }
53 }
54 }
55 }
56 })
57
58 userAccessToken = await server.users.generateUserAndToken('user1')
59
60 {
61 const body = await server.users.getMyInfo()
62 channelId = body.videoChannels[0].id
63 }
64
65 {
66 video = await server.videos.quickUpload({
67 name: 'password protected video',
68 privacy: VideoPrivacy.PASSWORD_PROTECTED,
69 videoPasswords: [ 'password1', 'password2' ]
70 })
71 }
72 path = '/api/v1/videos/'
73 })
74
75 async function checkVideoPasswordOptions (options: {
76 server: PeerTubeServer
77 token: string
78 videoPasswords: string[]
79 expectedStatus: HttpStatusCodeType
80 mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live'
81 }) {
82 const { server, token, videoPasswords, expectedStatus = HttpStatusCode.OK_200, mode } = options
83 const attaches = {
84 fixture: buildAbsoluteFixturePath('video_short.webm')
85 }
86 const baseCorrectParams = {
87 name: 'my super name',
88 category: 5,
89 licence: 1,
90 language: 'pt',
91 nsfw: false,
92 commentsEnabled: true,
93 downloadEnabled: true,
94 waitTranscoding: true,
95 description: 'my super description',
96 support: 'my super support text',
97 tags: [ 'tag1', 'tag2' ],
98 privacy: VideoPrivacy.PASSWORD_PROTECTED,
99 channelId,
100 originallyPublishedAt: new Date().toISOString()
101 }
102 if (mode === 'uploadLegacy') {
103 const fields = { ...baseCorrectParams, videoPasswords }
104 return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'legacy' })
105 }
106
107 if (mode === 'uploadResumable') {
108 const fields = { ...baseCorrectParams, videoPasswords }
109 return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'resumable' })
110 }
111
112 if (mode === 'import') {
113 const attributes = { ...baseCorrectParams, targetUrl: FIXTURE_URLS.goodVideo, videoPasswords }
114 return server.imports.importVideo({ attributes, expectedStatus })
115 }
116
117 if (mode === 'updateVideo') {
118 const attributes = { ...baseCorrectParams, videoPasswords }
119 return server.videos.update({ token, expectedStatus, id: video.id, attributes })
120 }
121
122 if (mode === 'updatePasswords') {
123 return server.videoPasswords.updateAll({ token, expectedStatus, videoId: video.id, passwords: videoPasswords })
124 }
125
126 if (mode === 'live') {
127 const fields = { ...baseCorrectParams, videoPasswords }
128
129 return server.live.create({ fields, expectedStatus })
130 }
131 }
132
133 function validateVideoPasswordList (mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live') {
134
135 it('Should fail with a password protected privacy without providing a password', async function () {
136 await checkVideoPasswordOptions({
137 server,
138 token: server.accessToken,
139 videoPasswords: undefined,
140 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
141 mode
142 })
143 })
144
145 it('Should fail with a password protected privacy and an empty password list', async function () {
146 const videoPasswords = []
147
148 await checkVideoPasswordOptions({
149 server,
150 token: server.accessToken,
151 videoPasswords,
152 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
153 mode
154 })
155 })
156
157 it('Should fail with a password protected privacy and a too short password', async function () {
158 const videoPasswords = [ 'p' ]
159
160 await checkVideoPasswordOptions({
161 server,
162 token: server.accessToken,
163 videoPasswords,
164 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
165 mode
166 })
167 })
168
169 it('Should fail with a password protected privacy and a too long password', async function () {
170 const videoPasswords = [ 'Very very very very very very very very very very very very very very very very very very long password' ]
171
172 await checkVideoPasswordOptions({
173 server,
174 token: server.accessToken,
175 videoPasswords,
176 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
177 mode
178 })
179 })
180
181 it('Should fail with a password protected privacy and an empty password', async function () {
182 const videoPasswords = [ '' ]
183
184 await checkVideoPasswordOptions({
185 server,
186 token: server.accessToken,
187 videoPasswords,
188 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
189 mode
190 })
191 })
192
193 it('Should fail with a password protected privacy and duplicated passwords', async function () {
194 const videoPasswords = [ 'password', 'password' ]
195
196 await checkVideoPasswordOptions({
197 server,
198 token: server.accessToken,
199 videoPasswords,
200 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
201 mode
202 })
203 })
204
205 if (mode === 'updatePasswords') {
206 it('Should fail for an unauthenticated user', async function () {
207 const videoPasswords = [ 'password' ]
208 await checkVideoPasswordOptions({
209 server,
210 token: null,
211 videoPasswords,
212 expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
213 mode
214 })
215 })
216
217 it('Should fail for an unauthorized user', async function () {
218 const videoPasswords = [ 'password' ]
219 await checkVideoPasswordOptions({
220 server,
221 token: userAccessToken,
222 videoPasswords,
223 expectedStatus: HttpStatusCode.FORBIDDEN_403,
224 mode
225 })
226 })
227 }
228
229 it('Should succeed with a password protected privacy and correct passwords', async function () {
230 const videoPasswords = [ 'password1', 'password2' ]
231 const expectedStatus = mode === 'updatePasswords' || mode === 'updateVideo'
232 ? HttpStatusCode.NO_CONTENT_204
233 : HttpStatusCode.OK_200
234
235 await checkVideoPasswordOptions({ server, token: server.accessToken, videoPasswords, expectedStatus, mode })
236 })
237 }
238
239 describe('When adding or updating a video', function () {
240 describe('Resumable upload', function () {
241 validateVideoPasswordList('uploadResumable')
242 })
243
244 describe('Legacy upload', function () {
245 validateVideoPasswordList('uploadLegacy')
246 })
247
248 describe('When importing a video', function () {
249 validateVideoPasswordList('import')
250 })
251
252 describe('When updating a video', function () {
253 validateVideoPasswordList('updateVideo')
254 })
255
256 describe('When updating the password list of a video', function () {
257 validateVideoPasswordList('updatePasswords')
258 })
259
260 describe('When creating a live', function () {
261 validateVideoPasswordList('live')
262 })
263 })
264
265 async function checkVideoAccessOptions (options: {
266 server: PeerTubeServer
267 token?: string
268 videoPassword?: string
269 expectedStatus: HttpStatusCodeType
270 mode: 'get' | 'getWithPassword' | 'getWithToken' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token'
271 }) {
272 const { server, token = null, videoPassword, expectedStatus, mode } = options
273
274 if (mode === 'get') {
275 return server.videos.get({ id: video.id, expectedStatus })
276 }
277
278 if (mode === 'getWithToken') {
279 return server.videos.getWithToken({
280 id: video.id,
281 token,
282 expectedStatus
283 })
284 }
285
286 if (mode === 'getWithPassword') {
287 return server.videos.getWithPassword({
288 id: video.id,
289 token,
290 expectedStatus,
291 password: videoPassword
292 })
293 }
294
295 if (mode === 'rate') {
296 return server.videos.rate({
297 id: video.id,
298 token,
299 expectedStatus,
300 rating: 'like',
301 videoPassword
302 })
303 }
304
305 if (mode === 'createThread') {
306 const fields = { text: 'super comment' }
307 const headers = videoPassword !== undefined && videoPassword !== null
308 ? { 'x-peertube-video-password': videoPassword }
309 : undefined
310 const body = await makePostBodyRequest({
311 url: server.url,
312 path: path + video.uuid + '/comment-threads',
313 token,
314 fields,
315 headers,
316 expectedStatus
317 })
318 return JSON.parse(body.text)
319 }
320
321 if (mode === 'replyThread') {
322 const fields = { text: 'super reply' }
323 const headers = videoPassword !== undefined && videoPassword !== null
324 ? { 'x-peertube-video-password': videoPassword }
325 : undefined
326 return makePostBodyRequest({
327 url: server.url,
328 path: path + video.uuid + '/comments/' + commentId,
329 token,
330 fields,
331 headers,
332 expectedStatus
333 })
334 }
335 if (mode === 'listThreads') {
336 return server.comments.listThreads({
337 videoId: video.id,
338 token,
339 expectedStatus,
340 videoPassword
341 })
342 }
343
344 if (mode === 'listCaptions') {
345 return server.captions.list({
346 videoId: video.id,
347 token,
348 expectedStatus,
349 videoPassword
350 })
351 }
352
353 if (mode === 'token') {
354 return server.videoToken.create({
355 videoId: video.id,
356 token,
357 expectedStatus,
358 videoPassword
359 })
360 }
361 }
362
363 function checkVideoError (error: any, mode: 'providePassword' | 'incorrectPassword') {
364 const serverCode = mode === 'providePassword'
365 ? ServerErrorCode.VIDEO_REQUIRES_PASSWORD
366 : ServerErrorCode.INCORRECT_VIDEO_PASSWORD
367
368 const message = mode === 'providePassword'
369 ? 'Please provide a password to access this password protected video'
370 : 'Incorrect video password. Access to the video is denied.'
371
372 if (!error.code) {
373 error = JSON.parse(error.text)
374 }
375
376 expect(error.code).to.equal(serverCode)
377 expect(error.detail).to.equal(message)
378 expect(error.error).to.equal(message)
379
380 expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
381 }
382
383 function validateVideoAccess (mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token') {
384 const requiresUserAuth = [ 'createThread', 'replyThread', 'rate' ].includes(mode)
385 let tokens: string[]
386 if (!requiresUserAuth) {
387 it('Should fail without providing a password for an unlogged user', async function () {
388 const body = await checkVideoAccessOptions({ server, expectedStatus: HttpStatusCode.FORBIDDEN_403, mode })
389 const error = body as unknown as PeerTubeProblemDocument
390
391 checkVideoError(error, 'providePassword')
392 })
393 }
394
395 it('Should fail without providing a password for an unauthorised user', async function () {
396 const tmp = mode === 'get' ? 'getWithToken' : mode
397
398 const body = await checkVideoAccessOptions({
399 server,
400 token: userAccessToken,
401 expectedStatus: HttpStatusCode.FORBIDDEN_403,
402 mode: tmp
403 })
404
405 const error = body as unknown as PeerTubeProblemDocument
406
407 checkVideoError(error, 'providePassword')
408 })
409
410 it('Should fail if a wrong password is entered', async function () {
411 const tmp = mode === 'get' ? 'getWithPassword' : mode
412 tokens = [ userAccessToken, server.accessToken ]
413
414 if (!requiresUserAuth) tokens.push(null)
415
416 for (const token of tokens) {
417 const body = await checkVideoAccessOptions({
418 server,
419 token,
420 videoPassword: 'toto',
421 expectedStatus: HttpStatusCode.FORBIDDEN_403,
422 mode: tmp
423 })
424 const error = body as unknown as PeerTubeProblemDocument
425
426 checkVideoError(error, 'incorrectPassword')
427 }
428 })
429
430 it('Should fail if an empty password is entered', async function () {
431 const tmp = mode === 'get' ? 'getWithPassword' : mode
432
433 for (const token of tokens) {
434 const body = await checkVideoAccessOptions({
435 server,
436 token,
437 videoPassword: '',
438 expectedStatus: HttpStatusCode.FORBIDDEN_403,
439 mode: tmp
440 })
441 const error = body as unknown as PeerTubeProblemDocument
442
443 checkVideoError(error, 'incorrectPassword')
444 }
445 })
446
447 it('Should fail if an inccorect password containing the correct password is entered', async function () {
448 const tmp = mode === 'get' ? 'getWithPassword' : mode
449
450 for (const token of tokens) {
451 const body = await checkVideoAccessOptions({
452 server,
453 token,
454 videoPassword: 'password11',
455 expectedStatus: HttpStatusCode.FORBIDDEN_403,
456 mode: tmp
457 })
458 const error = body as unknown as PeerTubeProblemDocument
459
460 checkVideoError(error, 'incorrectPassword')
461 }
462 })
463
464 it('Should succeed without providing a password for an authorised user', async function () {
465 const tmp = mode === 'get' ? 'getWithToken' : mode
466 const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200
467
468 const body = await checkVideoAccessOptions({ server, token: server.accessToken, expectedStatus, mode: tmp })
469
470 if (mode === 'createThread') commentId = body.comment.id
471 })
472
473 it('Should succeed using correct passwords', async function () {
474 const tmp = mode === 'get' ? 'getWithPassword' : mode
475 const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200
476
477 for (const token of tokens) {
478 await checkVideoAccessOptions({ server, videoPassword: 'password1', token, expectedStatus, mode: tmp })
479 await checkVideoAccessOptions({ server, videoPassword: 'password2', token, expectedStatus, mode: tmp })
480 }
481 })
482 }
483
484 describe('When accessing password protected video', function () {
485
486 describe('For getting a password protected video', function () {
487 validateVideoAccess('get')
488 })
489
490 describe('For rating a video', function () {
491 validateVideoAccess('rate')
492 })
493
494 describe('For creating a thread', function () {
495 validateVideoAccess('createThread')
496 })
497
498 describe('For replying to a thread', function () {
499 validateVideoAccess('replyThread')
500 })
501
502 describe('For listing threads', function () {
503 validateVideoAccess('listThreads')
504 })
505
506 describe('For getting captions', function () {
507 validateVideoAccess('listCaptions')
508 })
509
510 describe('For creating video file token', function () {
511 validateVideoAccess('token')
512 })
513 })
514
515 describe('When listing passwords', function () {
516 it('Should fail with a bad start pagination', async function () {
517 await checkBadStartPagination(server.url, path + video.uuid + '/passwords', server.accessToken)
518 })
519
520 it('Should fail with a bad count pagination', async function () {
521 await checkBadCountPagination(server.url, path + video.uuid + '/passwords', server.accessToken)
522 })
523
524 it('Should fail with an incorrect sort', async function () {
525 await checkBadSortPagination(server.url, path + video.uuid + '/passwords', server.accessToken)
526 })
527
528 it('Should fail for unauthenticated user', async function () {
529 await server.videoPasswords.list({
530 token: null,
531 expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
532 videoId: video.id
533 })
534 })
535
536 it('Should fail for unauthorized user', async function () {
537 await server.videoPasswords.list({
538 token: userAccessToken,
539 expectedStatus: HttpStatusCode.FORBIDDEN_403,
540 videoId: video.id
541 })
542 })
543
544 it('Should succeed with the correct parameters', async function () {
545 await server.videoPasswords.list({
546 token: server.accessToken,
547 expectedStatus: HttpStatusCode.OK_200,
548 videoId: video.id
549 })
550 })
551 })
552
553 describe('When deleting a password', async function () {
554 const passwords = (await server.videoPasswords.list({ videoId: video.id })).data
555
556 it('Should fail with wrong password id', async function () {
557 await server.videoPasswords.remove({ id: -1, videoId: video.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
558 })
559
560 it('Should fail for unauthenticated user', async function () {
561 await server.videoPasswords.remove({
562 id: passwords[0].id,
563 token: null,
564 videoId: video.id,
565 expectedStatus: HttpStatusCode.FORBIDDEN_403
566 })
567 })
568
569 it('Should fail for unauthorized user', async function () {
570 await server.videoPasswords.remove({
571 id: passwords[0].id,
572 token: userAccessToken,
573 videoId: video.id,
574 expectedStatus: HttpStatusCode.BAD_REQUEST_400
575 })
576 })
577
578 it('Should fail for non password protected video', async function () {
579 publicVideo = await server.videos.quickUpload({ name: 'public video' })
580 await server.videoPasswords.remove({ id: passwords[0].id, videoId: publicVideo.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
581 })
582
583 it('Should fail for password not linked to correct video', async function () {
584 const video2 = await server.videos.quickUpload({
585 name: 'password protected video',
586 privacy: VideoPrivacy.PASSWORD_PROTECTED,
587 videoPasswords: [ 'password1', 'password2' ]
588 })
589 await server.videoPasswords.remove({ id: passwords[0].id, videoId: video2.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
590 })
591
592 it('Should succeed with correct parameter', async function () {
593 await server.videoPasswords.remove({ id: passwords[0].id, videoId: video.id, expectedStatus: HttpStatusCode.NO_CONTENT_204 })
594 })
595
596 it('Should fail for last password of a video', async function () {
597 await server.videoPasswords.remove({ id: passwords[1].id, videoId: video.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
598 })
599 })
600
601 after(async function () {
602 await cleanupTests([ server ])
603 })
604})
diff --git a/packages/tests/src/api/check-params/video-playlists.ts b/packages/tests/src/api/check-params/video-playlists.ts
new file mode 100644
index 000000000..7f5be18d4
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-playlists.ts
@@ -0,0 +1,695 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import {
5 HttpStatusCode,
6 VideoPlaylistCreate,
7 VideoPlaylistCreateResult,
8 VideoPlaylistElementCreate,
9 VideoPlaylistElementUpdate,
10 VideoPlaylistPrivacy,
11 VideoPlaylistReorder,
12 VideoPlaylistType
13} from '@peertube/peertube-models'
14import {
15 cleanupTests,
16 createSingleServer,
17 makeGetRequest,
18 PeerTubeServer,
19 PlaylistsCommand,
20 setAccessTokensToServers,
21 setDefaultVideoChannel
22} from '@peertube/peertube-server-commands'
23
24describe('Test video playlists API validator', function () {
25 let server: PeerTubeServer
26 let userAccessToken: string
27
28 let playlist: VideoPlaylistCreateResult
29 let privatePlaylistUUID: string
30
31 let watchLaterPlaylistId: number
32 let videoId: number
33 let elementId: number
34
35 let command: PlaylistsCommand
36
37 // ---------------------------------------------------------------
38
39 before(async function () {
40 this.timeout(30000)
41
42 server = await createSingleServer(1)
43
44 await setAccessTokensToServers([ server ])
45 await setDefaultVideoChannel([ server ])
46
47 userAccessToken = await server.users.generateUserAndToken('user1')
48 videoId = (await server.videos.quickUpload({ name: 'video 1' })).id
49
50 command = server.playlists
51
52 {
53 const { data } = await command.listByAccount({
54 token: server.accessToken,
55 handle: 'root',
56 start: 0,
57 count: 5,
58 playlistType: VideoPlaylistType.WATCH_LATER
59 })
60 watchLaterPlaylistId = data[0].id
61 }
62
63 {
64 playlist = await command.create({
65 attributes: {
66 displayName: 'super playlist',
67 privacy: VideoPlaylistPrivacy.PUBLIC,
68 videoChannelId: server.store.channel.id
69 }
70 })
71 }
72
73 {
74 const created = await command.create({
75 attributes: {
76 displayName: 'private',
77 privacy: VideoPlaylistPrivacy.PRIVATE
78 }
79 })
80 privatePlaylistUUID = created.uuid
81 }
82 })
83
84 describe('When listing playlists', function () {
85 const globalPath = '/api/v1/video-playlists'
86 const accountPath = '/api/v1/accounts/root/video-playlists'
87 const videoChannelPath = '/api/v1/video-channels/root_channel/video-playlists'
88
89 it('Should fail with a bad start pagination', async function () {
90 await checkBadStartPagination(server.url, globalPath, server.accessToken)
91 await checkBadStartPagination(server.url, accountPath, server.accessToken)
92 await checkBadStartPagination(server.url, videoChannelPath, server.accessToken)
93 })
94
95 it('Should fail with a bad count pagination', async function () {
96 await checkBadCountPagination(server.url, globalPath, server.accessToken)
97 await checkBadCountPagination(server.url, accountPath, server.accessToken)
98 await checkBadCountPagination(server.url, videoChannelPath, server.accessToken)
99 })
100
101 it('Should fail with an incorrect sort', async function () {
102 await checkBadSortPagination(server.url, globalPath, server.accessToken)
103 await checkBadSortPagination(server.url, accountPath, server.accessToken)
104 await checkBadSortPagination(server.url, videoChannelPath, server.accessToken)
105 })
106
107 it('Should fail with a bad playlist type', async function () {
108 await makeGetRequest({ url: server.url, path: globalPath, query: { playlistType: 3 } })
109 await makeGetRequest({ url: server.url, path: accountPath, query: { playlistType: 3 } })
110 await makeGetRequest({ url: server.url, path: videoChannelPath, query: { playlistType: 3 } })
111 })
112
113 it('Should fail with a bad account parameter', async function () {
114 const accountPath = '/api/v1/accounts/root2/video-playlists'
115
116 await makeGetRequest({
117 url: server.url,
118 path: accountPath,
119 expectedStatus: HttpStatusCode.NOT_FOUND_404,
120 token: server.accessToken
121 })
122 })
123
124 it('Should fail with a bad video channel parameter', async function () {
125 const accountPath = '/api/v1/video-channels/bad_channel/video-playlists'
126
127 await makeGetRequest({
128 url: server.url,
129 path: accountPath,
130 expectedStatus: HttpStatusCode.NOT_FOUND_404,
131 token: server.accessToken
132 })
133 })
134
135 it('Should success with the correct parameters', async function () {
136 await makeGetRequest({ url: server.url, path: globalPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
137 await makeGetRequest({ url: server.url, path: accountPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
138 await makeGetRequest({
139 url: server.url,
140 path: videoChannelPath,
141 expectedStatus: HttpStatusCode.OK_200,
142 token: server.accessToken
143 })
144 })
145 })
146
147 describe('When listing videos of a playlist', function () {
148 const path = '/api/v1/video-playlists/'
149
150 it('Should fail with a bad start pagination', async function () {
151 await checkBadStartPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken)
152 })
153
154 it('Should fail with a bad count pagination', async function () {
155 await checkBadCountPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken)
156 })
157
158 it('Should success with the correct parameters', async function () {
159 await makeGetRequest({ url: server.url, path: path + playlist.shortUUID + '/videos', expectedStatus: HttpStatusCode.OK_200 })
160 })
161 })
162
163 describe('When getting a video playlist', function () {
164 it('Should fail with a bad id or uuid', async function () {
165 await command.get({ playlistId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
166 })
167
168 it('Should fail with an unknown playlist', async function () {
169 await command.get({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
170 })
171
172 it('Should fail to get an unlisted playlist with the number id', async function () {
173 const playlist = await command.create({
174 attributes: {
175 displayName: 'super playlist',
176 videoChannelId: server.store.channel.id,
177 privacy: VideoPlaylistPrivacy.UNLISTED
178 }
179 })
180
181 await command.get({ playlistId: playlist.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
182 await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 })
183 })
184
185 it('Should succeed with the correct params', async function () {
186 await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 })
187 })
188 })
189
190 describe('When creating/updating a video playlist', function () {
191 const getBase = (
192 attributes?: Partial<VideoPlaylistCreate>,
193 wrapper?: Partial<Parameters<PlaylistsCommand['create']>[0]>
194 ) => {
195 return {
196 attributes: {
197 displayName: 'display name',
198 privacy: VideoPlaylistPrivacy.UNLISTED,
199 thumbnailfile: 'custom-thumbnail.jpg',
200 videoChannelId: server.store.channel.id,
201
202 ...attributes
203 },
204
205 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
206
207 ...wrapper
208 }
209 }
210 const getUpdate = (params: any, playlistId: number | string) => {
211 return { ...params, playlistId }
212 }
213
214 it('Should fail with an unauthenticated user', async function () {
215 const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
216
217 await command.create(params)
218 await command.update(getUpdate(params, playlist.shortUUID))
219 })
220
221 it('Should fail without displayName', async function () {
222 const params = getBase({ displayName: undefined })
223
224 await command.create(params)
225 })
226
227 it('Should fail with an incorrect display name', async function () {
228 const params = getBase({ displayName: 's'.repeat(300) })
229
230 await command.create(params)
231 await command.update(getUpdate(params, playlist.shortUUID))
232 })
233
234 it('Should fail with an incorrect description', async function () {
235 const params = getBase({ description: 't' })
236
237 await command.create(params)
238 await command.update(getUpdate(params, playlist.shortUUID))
239 })
240
241 it('Should fail with an incorrect privacy', async function () {
242 const params = getBase({ privacy: 45 as any })
243
244 await command.create(params)
245 await command.update(getUpdate(params, playlist.shortUUID))
246 })
247
248 it('Should fail with an unknown video channel id', async function () {
249 const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
250
251 await command.create(params)
252 await command.update(getUpdate(params, playlist.shortUUID))
253 })
254
255 it('Should fail with an incorrect thumbnail file', async function () {
256 const params = getBase({ thumbnailfile: 'video_short.mp4' })
257
258 await command.create(params)
259 await command.update(getUpdate(params, playlist.shortUUID))
260 })
261
262 it('Should fail with a thumbnail file too big', async function () {
263 const params = getBase({ thumbnailfile: 'custom-preview-big.png' })
264
265 await command.create(params)
266 await command.update(getUpdate(params, playlist.shortUUID))
267 })
268
269 it('Should fail to set "public" a playlist not assigned to a channel', async function () {
270 const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: undefined })
271 const params2 = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: 'null' as any })
272 const params3 = getBase({ privacy: undefined, videoChannelId: 'null' as any })
273
274 await command.create(params)
275 await command.create(params2)
276 await command.update(getUpdate(params, privatePlaylistUUID))
277 await command.update(getUpdate(params2, playlist.shortUUID))
278 await command.update(getUpdate(params3, playlist.shortUUID))
279 })
280
281 it('Should fail with an unknown playlist to update', async function () {
282 await command.update(getUpdate(
283 getBase({}, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }),
284 42
285 ))
286 })
287
288 it('Should fail to update a playlist of another user', async function () {
289 await command.update(getUpdate(
290 getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }),
291 playlist.shortUUID
292 ))
293 })
294
295 it('Should fail to update the watch later playlist', async function () {
296 await command.update(getUpdate(
297 getBase({}, { expectedStatus: HttpStatusCode.BAD_REQUEST_400 }),
298 watchLaterPlaylistId
299 ))
300 })
301
302 it('Should succeed with the correct params', async function () {
303 {
304 const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 })
305 await command.create(params)
306 }
307
308 {
309 const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
310 await command.update(getUpdate(params, playlist.shortUUID))
311 }
312 })
313 })
314
315 describe('When adding an element in a playlist', function () {
316 const getBase = (
317 attributes?: Partial<VideoPlaylistElementCreate>,
318 wrapper?: Partial<Parameters<PlaylistsCommand['addElement']>[0]>
319 ) => {
320 return {
321 attributes: {
322 videoId,
323 startTimestamp: 2,
324 stopTimestamp: 3,
325
326 ...attributes
327 },
328
329 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
330 playlistId: playlist.id,
331
332 ...wrapper
333 }
334 }
335
336 it('Should fail with an unauthenticated user', async function () {
337 const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
338 await command.addElement(params)
339 })
340
341 it('Should fail with the playlist of another user', async function () {
342 const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
343 await command.addElement(params)
344 })
345
346 it('Should fail with an unknown or incorrect playlist id', async function () {
347 {
348 const params = getBase({}, { playlistId: 'toto' })
349 await command.addElement(params)
350 }
351
352 {
353 const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
354 await command.addElement(params)
355 }
356 })
357
358 it('Should fail with an unknown or incorrect video id', async function () {
359 const params = getBase({ videoId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
360 await command.addElement(params)
361 })
362
363 it('Should fail with a bad start/stop timestamp', async function () {
364 {
365 const params = getBase({ startTimestamp: -42 })
366 await command.addElement(params)
367 }
368
369 {
370 const params = getBase({ stopTimestamp: 'toto' as any })
371 await command.addElement(params)
372 }
373 })
374
375 it('Succeed with the correct params', async function () {
376 const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 })
377 const created = await command.addElement(params)
378 elementId = created.id
379 })
380 })
381
382 describe('When updating an element in a playlist', function () {
383 const getBase = (
384 attributes?: Partial<VideoPlaylistElementUpdate>,
385 wrapper?: Partial<Parameters<PlaylistsCommand['updateElement']>[0]>
386 ) => {
387 return {
388 attributes: {
389 startTimestamp: 1,
390 stopTimestamp: 2,
391
392 ...attributes
393 },
394
395 elementId,
396 playlistId: playlist.id,
397 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
398
399 ...wrapper
400 }
401 }
402
403 it('Should fail with an unauthenticated user', async function () {
404 const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
405 await command.updateElement(params)
406 })
407
408 it('Should fail with the playlist of another user', async function () {
409 const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
410 await command.updateElement(params)
411 })
412
413 it('Should fail with an unknown or incorrect playlist id', async function () {
414 {
415 const params = getBase({}, { playlistId: 'toto' })
416 await command.updateElement(params)
417 }
418
419 {
420 const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
421 await command.updateElement(params)
422 }
423 })
424
425 it('Should fail with an unknown or incorrect playlistElement id', async function () {
426 {
427 const params = getBase({}, { elementId: 'toto' })
428 await command.updateElement(params)
429 }
430
431 {
432 const params = getBase({}, { elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
433 await command.updateElement(params)
434 }
435 })
436
437 it('Should fail with a bad start/stop timestamp', async function () {
438 {
439 const params = getBase({ startTimestamp: 'toto' as any })
440 await command.updateElement(params)
441 }
442
443 {
444 const params = getBase({ stopTimestamp: -42 })
445 await command.updateElement(params)
446 }
447 })
448
449 it('Should fail with an unknown element', async function () {
450 const params = getBase({}, { elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
451 await command.updateElement(params)
452 })
453
454 it('Succeed with the correct params', async function () {
455 const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
456 await command.updateElement(params)
457 })
458 })
459
460 describe('When reordering elements of a playlist', function () {
461 let videoId3: number
462 let videoId4: number
463
464 const getBase = (
465 attributes?: Partial<VideoPlaylistReorder>,
466 wrapper?: Partial<Parameters<PlaylistsCommand['reorderElements']>[0]>
467 ) => {
468 return {
469 attributes: {
470 startPosition: 1,
471 insertAfterPosition: 2,
472 reorderLength: 3,
473
474 ...attributes
475 },
476
477 playlistId: playlist.shortUUID,
478 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
479
480 ...wrapper
481 }
482 }
483
484 before(async function () {
485 videoId3 = (await server.videos.quickUpload({ name: 'video 3' })).id
486 videoId4 = (await server.videos.quickUpload({ name: 'video 4' })).id
487
488 for (const id of [ videoId3, videoId4 ]) {
489 await command.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: id } })
490 }
491 })
492
493 it('Should fail with an unauthenticated user', async function () {
494 const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
495 await command.reorderElements(params)
496 })
497
498 it('Should fail with the playlist of another user', async function () {
499 const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
500 await command.reorderElements(params)
501 })
502
503 it('Should fail with an invalid playlist', async function () {
504 {
505 const params = getBase({}, { playlistId: 'toto' })
506 await command.reorderElements(params)
507 }
508
509 {
510 const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
511 await command.reorderElements(params)
512 }
513 })
514
515 it('Should fail with an invalid start position', async function () {
516 {
517 const params = getBase({ startPosition: -1 })
518 await command.reorderElements(params)
519 }
520
521 {
522 const params = getBase({ startPosition: 'toto' as any })
523 await command.reorderElements(params)
524 }
525
526 {
527 const params = getBase({ startPosition: 42 })
528 await command.reorderElements(params)
529 }
530 })
531
532 it('Should fail with an invalid insert after position', async function () {
533 {
534 const params = getBase({ insertAfterPosition: 'toto' as any })
535 await command.reorderElements(params)
536 }
537
538 {
539 const params = getBase({ insertAfterPosition: -2 })
540 await command.reorderElements(params)
541 }
542
543 {
544 const params = getBase({ insertAfterPosition: 42 })
545 await command.reorderElements(params)
546 }
547 })
548
549 it('Should fail with an invalid reorder length', async function () {
550 {
551 const params = getBase({ reorderLength: 'toto' as any })
552 await command.reorderElements(params)
553 }
554
555 {
556 const params = getBase({ reorderLength: -2 })
557 await command.reorderElements(params)
558 }
559
560 {
561 const params = getBase({ reorderLength: 42 })
562 await command.reorderElements(params)
563 }
564 })
565
566 it('Succeed with the correct params', async function () {
567 const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 })
568 await command.reorderElements(params)
569 })
570 })
571
572 describe('When checking exists in playlist endpoint', function () {
573 const path = '/api/v1/users/me/video-playlists/videos-exist'
574
575 it('Should fail with an unauthenticated user', async function () {
576 await makeGetRequest({
577 url: server.url,
578 path,
579 query: { videoIds: [ 1, 2 ] },
580 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
581 })
582 })
583
584 it('Should fail with invalid video ids', async function () {
585 await makeGetRequest({
586 url: server.url,
587 token: server.accessToken,
588 path,
589 query: { videoIds: 'toto' }
590 })
591
592 await makeGetRequest({
593 url: server.url,
594 token: server.accessToken,
595 path,
596 query: { videoIds: [ 'toto' ] }
597 })
598
599 await makeGetRequest({
600 url: server.url,
601 token: server.accessToken,
602 path,
603 query: { videoIds: [ 1, 'toto' ] }
604 })
605 })
606
607 it('Should succeed with the correct params', async function () {
608 await makeGetRequest({
609 url: server.url,
610 token: server.accessToken,
611 path,
612 query: { videoIds: [ 1, 2 ] },
613 expectedStatus: HttpStatusCode.OK_200
614 })
615 })
616 })
617
618 describe('When deleting an element in a playlist', function () {
619 const getBase = (wrapper: Partial<Parameters<PlaylistsCommand['removeElement']>[0]>) => {
620 return {
621 elementId,
622 playlistId: playlist.uuid,
623 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
624
625 ...wrapper
626 }
627 }
628
629 it('Should fail with an unauthenticated user', async function () {
630 const params = getBase({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
631 await command.removeElement(params)
632 })
633
634 it('Should fail with the playlist of another user', async function () {
635 const params = getBase({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
636 await command.removeElement(params)
637 })
638
639 it('Should fail with an unknown or incorrect playlist id', async function () {
640 {
641 const params = getBase({ playlistId: 'toto' })
642 await command.removeElement(params)
643 }
644
645 {
646 const params = getBase({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
647 await command.removeElement(params)
648 }
649 })
650
651 it('Should fail with an unknown or incorrect video id', async function () {
652 {
653 const params = getBase({ elementId: 'toto' as any })
654 await command.removeElement(params)
655 }
656
657 {
658 const params = getBase({ elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
659 await command.removeElement(params)
660 }
661 })
662
663 it('Should fail with an unknown element', async function () {
664 const params = getBase({ elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
665 await command.removeElement(params)
666 })
667
668 it('Succeed with the correct params', async function () {
669 const params = getBase({ expectedStatus: HttpStatusCode.NO_CONTENT_204 })
670 await command.removeElement(params)
671 })
672 })
673
674 describe('When deleting a playlist', function () {
675 it('Should fail with an unknown playlist', async function () {
676 await command.delete({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
677 })
678
679 it('Should fail with a playlist of another user', async function () {
680 await command.delete({ token: userAccessToken, playlistId: playlist.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
681 })
682
683 it('Should fail with the watch later playlist', async function () {
684 await command.delete({ playlistId: watchLaterPlaylistId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
685 })
686
687 it('Should succeed with the correct params', async function () {
688 await command.delete({ playlistId: playlist.uuid })
689 })
690 })
691
692 after(async function () {
693 await cleanupTests([ server ])
694 })
695})
diff --git a/packages/tests/src/api/check-params/video-source.ts b/packages/tests/src/api/check-params/video-source.ts
new file mode 100644
index 000000000..918182b8d
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-source.ts
@@ -0,0 +1,154 @@
1import { HttpStatusCode } from '@peertube/peertube-models'
2import {
3 cleanupTests,
4 createSingleServer,
5 PeerTubeServer,
6 setAccessTokensToServers,
7 setDefaultVideoChannel,
8 waitJobs
9} from '@peertube/peertube-server-commands'
10
11describe('Test video sources API validator', function () {
12 let server: PeerTubeServer = null
13 let uuid: string
14 let userToken: string
15
16 before(async function () {
17 this.timeout(120000)
18
19 server = await createSingleServer(1)
20 await setAccessTokensToServers([ server ])
21 await setDefaultVideoChannel([ server ])
22
23 userToken = await server.users.generateUserAndToken('user1')
24 })
25
26 describe('When getting latest source', function () {
27
28 before(async function () {
29 const created = await server.videos.quickUpload({ name: 'video' })
30 uuid = created.uuid
31 })
32
33 it('Should fail without a valid uuid', async function () {
34 await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
35 })
36
37 it('Should receive 404 when passing a non existing video id', async function () {
38 await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
39 })
40
41 it('Should not get the source as unauthenticated', async function () {
42 await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null })
43 })
44
45 it('Should not get the source with another user', async function () {
46 await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken })
47 })
48
49 it('Should succeed with the correct parameters get the source as another user', async function () {
50 await server.videos.getSource({ id: uuid })
51 })
52 })
53
54 describe('When updating source video file', function () {
55 let userAccessToken: string
56 let userId: number
57
58 let videoId: string
59 let userVideoId: string
60
61 before(async function () {
62 const res = await server.users.generate('user2')
63 userAccessToken = res.token
64 userId = res.userId
65
66 const { uuid } = await server.videos.quickUpload({ name: 'video' })
67 videoId = uuid
68
69 await waitJobs([ server ])
70 })
71
72 it('Should fail if not enabled on the instance', async function () {
73 await server.config.disableFileUpdate()
74
75 await server.videos.replaceSourceFile({ videoId, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
76 })
77
78 it('Should fail on an unknown video', async function () {
79 await server.config.enableFileUpdate()
80
81 await server.videos.replaceSourceFile({ videoId: 404, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
82 })
83
84 it('Should fail with an invalid video', async function () {
85 await server.config.enableLive({ allowReplay: false })
86
87 const { video } = await server.live.quickCreate({ saveReplay: false, permanentLive: true })
88 await server.videos.replaceSourceFile({
89 videoId: video.uuid,
90 fixture: 'video_short.mp4',
91 expectedStatus: HttpStatusCode.BAD_REQUEST_400
92 })
93 })
94
95 it('Should fail without token', async function () {
96 await server.videos.replaceSourceFile({
97 token: null,
98 videoId,
99 fixture: 'video_short.mp4',
100 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
101 })
102 })
103
104 it('Should fail with another user', async function () {
105 await server.videos.replaceSourceFile({
106 token: userAccessToken,
107 videoId,
108 fixture: 'video_short.mp4',
109 expectedStatus: HttpStatusCode.FORBIDDEN_403
110 })
111 })
112
113 it('Should fail with an incorrect input file', async function () {
114 await server.videos.replaceSourceFile({
115 fixture: 'video_short_fake.webm',
116 videoId,
117 completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422
118 })
119
120 await server.videos.replaceSourceFile({
121 fixture: 'video_short.mkv',
122 videoId,
123 expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415
124 })
125 })
126
127 it('Should fail if quota is exceeded', async function () {
128 this.timeout(60000)
129
130 const { uuid } = await server.videos.quickUpload({ name: 'user video' })
131 userVideoId = uuid
132 await waitJobs([ server ])
133
134 await server.users.update({ userId, videoQuota: 1 })
135 await server.videos.replaceSourceFile({
136 token: userAccessToken,
137 videoId: uuid,
138 fixture: 'video_short.mp4',
139 expectedStatus: HttpStatusCode.FORBIDDEN_403
140 })
141 })
142
143 it('Should succeed with the correct params', async function () {
144 this.timeout(60000)
145
146 await server.users.update({ userId, videoQuota: 1000 * 1000 * 1000 })
147 await server.videos.replaceSourceFile({ videoId: userVideoId, fixture: 'video_short.mp4' })
148 })
149 })
150
151 after(async function () {
152 await cleanupTests([ server ])
153 })
154})
diff --git a/packages/tests/src/api/check-params/video-storyboards.ts b/packages/tests/src/api/check-params/video-storyboards.ts
new file mode 100644
index 000000000..f83b541d8
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-storyboards.ts
@@ -0,0 +1,45 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
4import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
5
6describe('Test video storyboards API validator', function () {
7 let server: PeerTubeServer
8
9 let publicVideo: { uuid: string }
10 let privateVideo: { uuid: string }
11
12 // ---------------------------------------------------------------
13
14 before(async function () {
15 this.timeout(120000)
16
17 server = await createSingleServer(1)
18 await setAccessTokensToServers([ server ])
19
20 publicVideo = await server.videos.quickUpload({ name: 'public' })
21 privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE })
22 })
23
24 it('Should fail without a valid uuid', async function () {
25 await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
26 })
27
28 it('Should receive 404 when passing a non existing video id', async function () {
29 await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
30 })
31
32 it('Should not get the private storyboard without the appropriate token', async function () {
33 await server.storyboard.list({ id: privateVideo.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null })
34 await server.storyboard.list({ id: publicVideo.uuid, expectedStatus: HttpStatusCode.OK_200, token: null })
35 })
36
37 it('Should succeed with the correct parameters', async function () {
38 await server.storyboard.list({ id: privateVideo.uuid })
39 await server.storyboard.list({ id: publicVideo.uuid })
40 })
41
42 after(async function () {
43 await cleanupTests([ server ])
44 })
45})
diff --git a/packages/tests/src/api/check-params/video-studio.ts b/packages/tests/src/api/check-params/video-studio.ts
new file mode 100644
index 000000000..ae83f3590
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-studio.ts
@@ -0,0 +1,392 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode, HttpStatusCodeType, VideoStudioTask } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createSingleServer,
7 PeerTubeServer,
8 setAccessTokensToServers,
9 VideoStudioCommand,
10 waitJobs
11} from '@peertube/peertube-server-commands'
12
13describe('Test video studio API validator', function () {
14 let server: PeerTubeServer
15 let command: VideoStudioCommand
16 let userAccessToken: string
17 let videoUUID: string
18
19 // ---------------------------------------------------------------
20
21 before(async function () {
22 this.timeout(120_000)
23
24 server = await createSingleServer(1)
25
26 await setAccessTokensToServers([ server ])
27 userAccessToken = await server.users.generateUserAndToken('user1')
28
29 await server.config.enableMinimumTranscoding()
30
31 const { uuid } = await server.videos.quickUpload({ name: 'video' })
32 videoUUID = uuid
33
34 command = server.videoStudio
35
36 await waitJobs([ server ])
37 })
38
39 describe('Task creation', function () {
40
41 describe('Config settings', function () {
42
43 it('Should fail if studio is disabled', async function () {
44 await server.config.updateExistingSubConfig({
45 newConfig: {
46 videoStudio: {
47 enabled: false
48 }
49 }
50 })
51
52 await command.createEditionTasks({
53 videoId: videoUUID,
54 tasks: VideoStudioCommand.getComplexTask(),
55 expectedStatus: HttpStatusCode.BAD_REQUEST_400
56 })
57 })
58
59 it('Should fail to enable studio if transcoding is disabled', async function () {
60 await server.config.updateExistingSubConfig({
61 newConfig: {
62 videoStudio: {
63 enabled: true
64 },
65 transcoding: {
66 enabled: false
67 }
68 },
69 expectedStatus: HttpStatusCode.BAD_REQUEST_400
70 })
71 })
72
73 it('Should succeed to enable video studio', async function () {
74 await server.config.updateExistingSubConfig({
75 newConfig: {
76 videoStudio: {
77 enabled: true
78 },
79 transcoding: {
80 enabled: true
81 }
82 }
83 })
84 })
85 })
86
87 describe('Common tasks', function () {
88
89 it('Should fail without token', async function () {
90 await command.createEditionTasks({
91 token: null,
92 videoId: videoUUID,
93 tasks: VideoStudioCommand.getComplexTask(),
94 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
95 })
96 })
97
98 it('Should fail with another user token', async function () {
99 await command.createEditionTasks({
100 token: userAccessToken,
101 videoId: videoUUID,
102 tasks: VideoStudioCommand.getComplexTask(),
103 expectedStatus: HttpStatusCode.FORBIDDEN_403
104 })
105 })
106
107 it('Should fail with an invalid video', async function () {
108 await command.createEditionTasks({
109 videoId: 'tintin',
110 tasks: VideoStudioCommand.getComplexTask(),
111 expectedStatus: HttpStatusCode.BAD_REQUEST_400
112 })
113 })
114
115 it('Should fail with an unknown video', async function () {
116 await command.createEditionTasks({
117 videoId: 42,
118 tasks: VideoStudioCommand.getComplexTask(),
119 expectedStatus: HttpStatusCode.NOT_FOUND_404
120 })
121 })
122
123 it('Should fail with an already in transcoding state video', async function () {
124 this.timeout(60000)
125
126 const { uuid } = await server.videos.quickUpload({ name: 'transcoded video' })
127 await waitJobs([ server ])
128
129 await server.jobs.pauseJobQueue()
130 await server.videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
131
132 await command.createEditionTasks({
133 videoId: uuid,
134 tasks: VideoStudioCommand.getComplexTask(),
135 expectedStatus: HttpStatusCode.CONFLICT_409
136 })
137
138 await server.jobs.resumeJobQueue()
139 })
140
141 it('Should fail with a bad complex task', async function () {
142 await command.createEditionTasks({
143 videoId: videoUUID,
144 tasks: [
145 {
146 name: 'cut',
147 options: {
148 start: 1,
149 end: 2
150 }
151 },
152 {
153 name: 'hadock',
154 options: {
155 start: 1,
156 end: 2
157 }
158 }
159 ] as any,
160 expectedStatus: HttpStatusCode.BAD_REQUEST_400
161 })
162 })
163
164 it('Should fail without task', async function () {
165 await command.createEditionTasks({
166 videoId: videoUUID,
167 tasks: [],
168 expectedStatus: HttpStatusCode.BAD_REQUEST_400
169 })
170 })
171
172 it('Should fail with too many tasks', async function () {
173 const tasks: VideoStudioTask[] = []
174
175 for (let i = 0; i < 110; i++) {
176 tasks.push({
177 name: 'cut',
178 options: {
179 start: 1
180 }
181 })
182 }
183
184 await command.createEditionTasks({
185 videoId: videoUUID,
186 tasks,
187 expectedStatus: HttpStatusCode.BAD_REQUEST_400
188 })
189 })
190
191 it('Should succeed with correct parameters', async function () {
192 await server.jobs.pauseJobQueue()
193
194 await command.createEditionTasks({
195 videoId: videoUUID,
196 tasks: VideoStudioCommand.getComplexTask(),
197 expectedStatus: HttpStatusCode.NO_CONTENT_204
198 })
199 })
200
201 it('Should fail with a video that is already waiting for edition', async function () {
202 this.timeout(120000)
203
204 await command.createEditionTasks({
205 videoId: videoUUID,
206 tasks: VideoStudioCommand.getComplexTask(),
207 expectedStatus: HttpStatusCode.CONFLICT_409
208 })
209
210 await server.jobs.resumeJobQueue()
211
212 await waitJobs([ server ])
213 })
214 })
215
216 describe('Cut task', function () {
217
218 async function cut (start: number, end: number, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) {
219 await command.createEditionTasks({
220 videoId: videoUUID,
221 tasks: [
222 {
223 name: 'cut',
224 options: {
225 start,
226 end
227 }
228 }
229 ],
230 expectedStatus
231 })
232 }
233
234 it('Should fail with bad start/end', async function () {
235 const invalid = [
236 'tintin',
237 -1,
238 undefined
239 ]
240
241 for (const value of invalid) {
242 await cut(value as any, undefined)
243 await cut(undefined, value as any)
244 }
245 })
246
247 it('Should fail with the same start/end', async function () {
248 await cut(2, 2)
249 })
250
251 it('Should fail with inconsistents start/end', async function () {
252 await cut(2, 1)
253 })
254
255 it('Should fail without start and end', async function () {
256 await cut(undefined, undefined)
257 })
258
259 it('Should succeed with the correct params', async function () {
260 this.timeout(120000)
261
262 await cut(0, 2, HttpStatusCode.NO_CONTENT_204)
263
264 await waitJobs([ server ])
265 })
266 })
267
268 describe('Watermark task', function () {
269
270 async function addWatermark (file: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) {
271 await command.createEditionTasks({
272 videoId: videoUUID,
273 tasks: [
274 {
275 name: 'add-watermark',
276 options: {
277 file
278 }
279 }
280 ],
281 expectedStatus
282 })
283 }
284
285 it('Should fail without waterkmark', async function () {
286 await addWatermark(undefined)
287 })
288
289 it('Should fail with an invalid watermark', async function () {
290 await addWatermark('video_short.mp4')
291 })
292
293 it('Should succeed with the correct params', async function () {
294 this.timeout(120000)
295
296 await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204)
297
298 await waitJobs([ server ])
299 })
300 })
301
302 describe('Intro/Outro task', function () {
303
304 async function addIntroOutro (
305 type: 'add-intro' | 'add-outro',
306 file: string,
307 expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400
308 ) {
309 await command.createEditionTasks({
310 videoId: videoUUID,
311 tasks: [
312 {
313 name: type,
314 options: {
315 file
316 }
317 }
318 ],
319 expectedStatus
320 })
321 }
322
323 it('Should fail without file', async function () {
324 await addIntroOutro('add-intro', undefined)
325 await addIntroOutro('add-outro', undefined)
326 })
327
328 it('Should fail with an invalid file', async function () {
329 await addIntroOutro('add-intro', 'custom-thumbnail.jpg')
330 await addIntroOutro('add-outro', 'custom-thumbnail.jpg')
331 })
332
333 it('Should fail with a file that does not contain video stream', async function () {
334 await addIntroOutro('add-intro', 'sample.ogg')
335 await addIntroOutro('add-outro', 'sample.ogg')
336
337 })
338
339 it('Should succeed with the correct params', async function () {
340 this.timeout(120000)
341
342 await addIntroOutro('add-intro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204)
343 await waitJobs([ server ])
344
345 await addIntroOutro('add-outro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204)
346 await waitJobs([ server ])
347 })
348
349 it('Should check total quota when creating the task', async function () {
350 this.timeout(120000)
351
352 const user = await server.users.create({ username: 'user_quota_1' })
353 const token = await server.login.getAccessToken('user_quota_1')
354 const { uuid } = await server.videos.quickUpload({ token, name: 'video_quota_1', fixture: 'video_short.mp4' })
355
356 const addIntroOutroByUser = (type: 'add-intro' | 'add-outro', expectedStatus: HttpStatusCodeType) => {
357 return command.createEditionTasks({
358 token,
359 videoId: uuid,
360 tasks: [
361 {
362 name: type,
363 options: {
364 file: 'video_short.mp4'
365 }
366 }
367 ],
368 expectedStatus
369 })
370 }
371
372 await waitJobs([ server ])
373
374 const { videoQuotaUsed } = await server.users.getMyQuotaUsed({ token })
375 await server.users.update({ userId: user.id, videoQuota: Math.round(videoQuotaUsed * 2.5) })
376
377 // Still valid
378 await addIntroOutroByUser('add-intro', HttpStatusCode.NO_CONTENT_204)
379
380 await waitJobs([ server ])
381
382 // Too much quota
383 await addIntroOutroByUser('add-intro', HttpStatusCode.PAYLOAD_TOO_LARGE_413)
384 await addIntroOutroByUser('add-outro', HttpStatusCode.PAYLOAD_TOO_LARGE_413)
385 })
386 })
387 })
388
389 after(async function () {
390 await cleanupTests([ server ])
391 })
392})
diff --git a/packages/tests/src/api/check-params/video-token.ts b/packages/tests/src/api/check-params/video-token.ts
new file mode 100644
index 000000000..5f838102d
--- /dev/null
+++ b/packages/tests/src/api/check-params/video-token.ts
@@ -0,0 +1,70 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
4import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
5
6describe('Test video tokens', function () {
7 let server: PeerTubeServer
8 let privateVideoId: string
9 let passwordProtectedVideoId: string
10 let userToken: string
11
12 const videoPassword = 'password'
13
14 // ---------------------------------------------------------------
15
16 before(async function () {
17 this.timeout(300_000)
18
19 server = await createSingleServer(1)
20 await setAccessTokensToServers([ server ])
21 {
22 const { uuid } = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE })
23 privateVideoId = uuid
24 }
25 {
26 const { uuid } = await server.videos.quickUpload({
27 name: 'password protected video',
28 privacy: VideoPrivacy.PASSWORD_PROTECTED,
29 videoPasswords: [ videoPassword ]
30 })
31 passwordProtectedVideoId = uuid
32 }
33 userToken = await server.users.generateUserAndToken('user1')
34 })
35
36 it('Should not generate tokens on private video for unauthenticated user', async function () {
37 await server.videoToken.create({ videoId: privateVideoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
38 })
39
40 it('Should not generate tokens of unknown video', async function () {
41 await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
42 })
43
44 it('Should not generate tokens with incorrect password', async function () {
45 await server.videoToken.create({
46 videoId: passwordProtectedVideoId,
47 token: null,
48 expectedStatus: HttpStatusCode.FORBIDDEN_403,
49 videoPassword: 'incorrectPassword'
50 })
51 })
52
53 it('Should not generate tokens of a non owned video', async function () {
54 await server.videoToken.create({ videoId: privateVideoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
55 })
56
57 it('Should generate token', async function () {
58 await server.videoToken.create({ videoId: privateVideoId })
59 })
60
61 it('Should generate token on password protected video', async function () {
62 await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: null })
63 await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: userToken })
64 await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword })
65 })
66
67 after(async function () {
68 await cleanupTests([ server ])
69 })
70})
diff --git a/packages/tests/src/api/check-params/videos-common-filters.ts b/packages/tests/src/api/check-params/videos-common-filters.ts
new file mode 100644
index 000000000..dbae3010c
--- /dev/null
+++ b/packages/tests/src/api/check-params/videos-common-filters.ts
@@ -0,0 +1,171 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import {
4 HttpStatusCode,
5 HttpStatusCodeType,
6 UserRole,
7 VideoInclude,
8 VideoIncludeType,
9 VideoPrivacy,
10 VideoPrivacyType
11} from '@peertube/peertube-models'
12import {
13 cleanupTests,
14 createSingleServer,
15 makeGetRequest,
16 PeerTubeServer,
17 setAccessTokensToServers,
18 setDefaultVideoChannel
19} from '@peertube/peertube-server-commands'
20
21describe('Test video filters validators', function () {
22 let server: PeerTubeServer
23 let userAccessToken: string
24 let moderatorAccessToken: string
25
26 // ---------------------------------------------------------------
27
28 before(async function () {
29 this.timeout(30000)
30
31 server = await createSingleServer(1)
32
33 await setAccessTokensToServers([ server ])
34 await setDefaultVideoChannel([ server ])
35
36 const user = { username: 'user1', password: 'my super password' }
37 await server.users.create({ username: user.username, password: user.password })
38 userAccessToken = await server.login.getAccessToken(user)
39
40 const moderator = { username: 'moderator', password: 'my super password' }
41 await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
42
43 moderatorAccessToken = await server.login.getAccessToken(moderator)
44 })
45
46 describe('When setting video filters', function () {
47
48 const validIncludes = [
49 VideoInclude.NONE,
50 VideoInclude.BLOCKED_OWNER,
51 VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
52 ]
53
54 async function testEndpoints (options: {
55 token?: string
56 isLocal?: boolean
57 include?: VideoIncludeType
58 privacyOneOf?: VideoPrivacyType[]
59 expectedStatus: HttpStatusCodeType
60 excludeAlreadyWatched?: boolean
61 unauthenticatedUser?: boolean
62 }) {
63 const paths = [
64 '/api/v1/video-channels/root_channel/videos',
65 '/api/v1/accounts/root/videos',
66 '/api/v1/videos',
67 '/api/v1/search/videos'
68 ]
69
70 for (const path of paths) {
71 const token = options.unauthenticatedUser
72 ? undefined
73 : options.token || server.accessToken
74
75 await makeGetRequest({
76 url: server.url,
77 path,
78 token,
79 query: {
80 isLocal: options.isLocal,
81 privacyOneOf: options.privacyOneOf,
82 include: options.include,
83 excludeAlreadyWatched: options.excludeAlreadyWatched
84 },
85 expectedStatus: options.expectedStatus
86 })
87 }
88 }
89
90 it('Should fail with a bad privacyOneOf', async function () {
91 await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
92 })
93
94 it('Should succeed with a good privacyOneOf', async function () {
95 await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 })
96 })
97
98 it('Should fail to use privacyOneOf with a simple user', async function () {
99 await testEndpoints({
100 privacyOneOf: [ VideoPrivacy.INTERNAL ],
101 token: userAccessToken,
102 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
103 })
104 })
105
106 it('Should fail with a bad include', async function () {
107 await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
108 })
109
110 it('Should succeed with a good include', async function () {
111 for (const include of validIncludes) {
112 await testEndpoints({ include, expectedStatus: HttpStatusCode.OK_200 })
113 }
114 })
115
116 it('Should fail to include more videos with a simple user', async function () {
117 for (const include of validIncludes) {
118 await testEndpoints({ token: userAccessToken, include, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
119 }
120 })
121
122 it('Should succeed to list all local/all with a moderator', async function () {
123 for (const include of validIncludes) {
124 await testEndpoints({ token: moderatorAccessToken, include, expectedStatus: HttpStatusCode.OK_200 })
125 }
126 })
127
128 it('Should succeed to list all local/all with an admin', async function () {
129 for (const include of validIncludes) {
130 await testEndpoints({ token: server.accessToken, include, expectedStatus: HttpStatusCode.OK_200 })
131 }
132 })
133
134 // Because we cannot authenticate the user on the RSS endpoint
135 it('Should fail on the feeds endpoint with the all filter', async function () {
136 for (const include of [ VideoInclude.NOT_PUBLISHED_STATE ]) {
137 await makeGetRequest({
138 url: server.url,
139 path: '/feeds/videos.json',
140 expectedStatus: HttpStatusCode.UNAUTHORIZED_401,
141 query: {
142 include
143 }
144 })
145 }
146 })
147
148 it('Should succeed on the feeds endpoint with the local filter', async function () {
149 await makeGetRequest({
150 url: server.url,
151 path: '/feeds/videos.json',
152 expectedStatus: HttpStatusCode.OK_200,
153 query: {
154 isLocal: true
155 }
156 })
157 })
158
159 it('Should fail when trying to exclude already watched videos for an unlogged user', async function () {
160 await testEndpoints({ excludeAlreadyWatched: true, unauthenticatedUser: true, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
161 })
162
163 it('Should succeed when trying to exclude already watched videos for a logged user', async function () {
164 await testEndpoints({ token: userAccessToken, excludeAlreadyWatched: true, expectedStatus: HttpStatusCode.OK_200 })
165 })
166 })
167
168 after(async function () {
169 await cleanupTests([ server ])
170 })
171})
diff --git a/packages/tests/src/api/check-params/videos-history.ts b/packages/tests/src/api/check-params/videos-history.ts
new file mode 100644
index 000000000..65d1e9fac
--- /dev/null
+++ b/packages/tests/src/api/check-params/videos-history.ts
@@ -0,0 +1,145 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { checkBadCountPagination, checkBadStartPagination } from '@tests/shared/checks.js'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeDeleteRequest,
9 makeGetRequest,
10 makePostBodyRequest,
11 makePutBodyRequest,
12 PeerTubeServer,
13 setAccessTokensToServers
14} from '@peertube/peertube-server-commands'
15
16describe('Test videos history API validator', function () {
17 const myHistoryPath = '/api/v1/users/me/history/videos'
18 const myHistoryRemove = myHistoryPath + '/remove'
19 let viewPath: string
20 let server: PeerTubeServer
21 let videoId: number
22
23 // ---------------------------------------------------------------
24
25 before(async function () {
26 this.timeout(30000)
27
28 server = await createSingleServer(1)
29
30 await setAccessTokensToServers([ server ])
31
32 const { id, uuid } = await server.videos.upload()
33 viewPath = '/api/v1/videos/' + uuid + '/views'
34 videoId = id
35 })
36
37 describe('When notifying a user is watching a video', function () {
38
39 it('Should fail with a bad token', async function () {
40 const fields = { currentTime: 5 }
41 await makePutBodyRequest({ url: server.url, path: viewPath, fields, token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
42 })
43
44 it('Should succeed with the correct parameters', async function () {
45 const fields = { currentTime: 5 }
46
47 await makePutBodyRequest({
48 url: server.url,
49 path: viewPath,
50 fields,
51 token: server.accessToken,
52 expectedStatus: HttpStatusCode.NO_CONTENT_204
53 })
54 })
55 })
56
57 describe('When listing user videos history', function () {
58 it('Should fail with a bad start pagination', async function () {
59 await checkBadStartPagination(server.url, myHistoryPath, server.accessToken)
60 })
61
62 it('Should fail with a bad count pagination', async function () {
63 await checkBadCountPagination(server.url, myHistoryPath, server.accessToken)
64 })
65
66 it('Should fail with an unauthenticated user', async function () {
67 await makeGetRequest({ url: server.url, path: myHistoryPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
68 })
69
70 it('Should succeed with the correct params', async function () {
71 await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, expectedStatus: HttpStatusCode.OK_200 })
72 })
73 })
74
75 describe('When removing a specific user video history element', function () {
76 let path: string
77
78 before(function () {
79 path = myHistoryPath + '/' + videoId
80 })
81
82 it('Should fail with an unauthenticated user', async function () {
83 await makeDeleteRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
84 })
85
86 it('Should fail with a bad videoId parameter', async function () {
87 await makeDeleteRequest({
88 url: server.url,
89 token: server.accessToken,
90 path: myHistoryRemove + '/hi',
91 expectedStatus: HttpStatusCode.BAD_REQUEST_400
92 })
93 })
94
95 it('Should succeed with the correct parameters', async function () {
96 await makeDeleteRequest({
97 url: server.url,
98 token: server.accessToken,
99 path,
100 expectedStatus: HttpStatusCode.NO_CONTENT_204
101 })
102 })
103 })
104
105 describe('When removing all user videos history', function () {
106 it('Should fail with an unauthenticated user', async function () {
107 await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
108 })
109
110 it('Should fail with a bad beforeDate parameter', async function () {
111 const body = { beforeDate: '15' }
112 await makePostBodyRequest({
113 url: server.url,
114 token: server.accessToken,
115 path: myHistoryRemove,
116 fields: body,
117 expectedStatus: HttpStatusCode.BAD_REQUEST_400
118 })
119 })
120
121 it('Should succeed with a valid beforeDate param', async function () {
122 const body = { beforeDate: new Date().toISOString() }
123 await makePostBodyRequest({
124 url: server.url,
125 token: server.accessToken,
126 path: myHistoryRemove,
127 fields: body,
128 expectedStatus: HttpStatusCode.NO_CONTENT_204
129 })
130 })
131
132 it('Should succeed without body', async function () {
133 await makePostBodyRequest({
134 url: server.url,
135 token: server.accessToken,
136 path: myHistoryRemove,
137 expectedStatus: HttpStatusCode.NO_CONTENT_204
138 })
139 })
140 })
141
142 after(async function () {
143 await cleanupTests([ server ])
144 })
145})
diff --git a/packages/tests/src/api/check-params/videos-overviews.ts b/packages/tests/src/api/check-params/videos-overviews.ts
new file mode 100644
index 000000000..ba6f6ac69
--- /dev/null
+++ b/packages/tests/src/api/check-params/videos-overviews.ts
@@ -0,0 +1,31 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands'
4
5describe('Test videos overview API validator', function () {
6 let server: PeerTubeServer
7
8 // ---------------------------------------------------------------
9
10 before(async function () {
11 this.timeout(30000)
12
13 server = await createSingleServer(1)
14 })
15
16 describe('When getting videos overview', function () {
17
18 it('Should fail with a bad pagination', async function () {
19 await server.overviews.getVideos({ page: 0, expectedStatus: 400 })
20 await server.overviews.getVideos({ page: 100, expectedStatus: 400 })
21 })
22
23 it('Should succeed with a good pagination', async function () {
24 await server.overviews.getVideos({ page: 1 })
25 })
26 })
27
28 after(async function () {
29 await cleanupTests([ server ])
30 })
31})
diff --git a/packages/tests/src/api/check-params/videos.ts b/packages/tests/src/api/check-params/videos.ts
new file mode 100644
index 000000000..c349ed9fe
--- /dev/null
+++ b/packages/tests/src/api/check-params/videos.ts
@@ -0,0 +1,883 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { join } from 'path'
5import { omit, randomInt } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, PeerTubeProblemDocument, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
7import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
8import {
9 cleanupTests,
10 createSingleServer,
11 makeDeleteRequest,
12 makeGetRequest,
13 makePutBodyRequest,
14 makeUploadRequest,
15 PeerTubeServer,
16 setAccessTokensToServers
17} from '@peertube/peertube-server-commands'
18import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js'
19import { checkUploadVideoParam } from '@tests/shared/videos.js'
20
21describe('Test videos API validator', function () {
22 const path = '/api/v1/videos/'
23 let server: PeerTubeServer
24 let userAccessToken = ''
25 let accountName: string
26 let channelId: number
27 let channelName: string
28 let video: VideoCreateResult
29 let privateVideo: VideoCreateResult
30
31 // ---------------------------------------------------------------
32
33 before(async function () {
34 this.timeout(30000)
35
36 server = await createSingleServer(1)
37
38 await setAccessTokensToServers([ server ])
39
40 userAccessToken = await server.users.generateUserAndToken('user1')
41
42 {
43 const body = await server.users.getMyInfo()
44 channelId = body.videoChannels[0].id
45 channelName = body.videoChannels[0].name
46 accountName = body.account.name + '@' + body.account.host
47 }
48
49 {
50 privateVideo = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE })
51 }
52 })
53
54 describe('When listing videos', function () {
55 it('Should fail with a bad start pagination', async function () {
56 await checkBadStartPagination(server.url, path)
57 })
58
59 it('Should fail with a bad count pagination', async function () {
60 await checkBadCountPagination(server.url, path)
61 })
62
63 it('Should fail with an incorrect sort', async function () {
64 await checkBadSortPagination(server.url, path)
65 })
66
67 it('Should fail with a bad skipVideos query', async function () {
68 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: 'toto' } })
69 })
70
71 it('Should success with the correct parameters', async function () {
72 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: false } })
73 })
74 })
75
76 describe('When searching a video', function () {
77
78 it('Should fail with nothing', async function () {
79 await makeGetRequest({
80 url: server.url,
81 path: join(path, 'search'),
82 expectedStatus: HttpStatusCode.BAD_REQUEST_400
83 })
84 })
85
86 it('Should fail with a bad start pagination', async function () {
87 await checkBadStartPagination(server.url, join(path, 'search', 'test'))
88 })
89
90 it('Should fail with a bad count pagination', async function () {
91 await checkBadCountPagination(server.url, join(path, 'search', 'test'))
92 })
93
94 it('Should fail with an incorrect sort', async function () {
95 await checkBadSortPagination(server.url, join(path, 'search', 'test'))
96 })
97
98 it('Should success with the correct parameters', async function () {
99 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
100 })
101 })
102
103 describe('When listing my videos', function () {
104 const path = '/api/v1/users/me/videos'
105
106 it('Should fail with a bad start pagination', async function () {
107 await checkBadStartPagination(server.url, path, server.accessToken)
108 })
109
110 it('Should fail with a bad count pagination', async function () {
111 await checkBadCountPagination(server.url, path, server.accessToken)
112 })
113
114 it('Should fail with an incorrect sort', async function () {
115 await checkBadSortPagination(server.url, path, server.accessToken)
116 })
117
118 it('Should fail with an invalid channel', async function () {
119 await makeGetRequest({ url: server.url, token: server.accessToken, path, query: { channelId: 'toto' } })
120 })
121
122 it('Should fail with an unknown channel', async function () {
123 await makeGetRequest({
124 url: server.url,
125 token: server.accessToken,
126 path,
127 query: { channelId: 89898 },
128 expectedStatus: HttpStatusCode.NOT_FOUND_404
129 })
130 })
131
132 it('Should success with the correct parameters', async function () {
133 await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 })
134 })
135 })
136
137 describe('When listing account videos', function () {
138 let path: string
139
140 before(async function () {
141 path = '/api/v1/accounts/' + accountName + '/videos'
142 })
143
144 it('Should fail with a bad start pagination', async function () {
145 await checkBadStartPagination(server.url, path, server.accessToken)
146 })
147
148 it('Should fail with a bad count pagination', async function () {
149 await checkBadCountPagination(server.url, path, server.accessToken)
150 })
151
152 it('Should fail with an incorrect sort', async function () {
153 await checkBadSortPagination(server.url, path, server.accessToken)
154 })
155
156 it('Should success with the correct parameters', async function () {
157 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
158 })
159 })
160
161 describe('When listing video channel videos', function () {
162 let path: string
163
164 before(async function () {
165 path = '/api/v1/video-channels/' + channelName + '/videos'
166 })
167
168 it('Should fail with a bad start pagination', async function () {
169 await checkBadStartPagination(server.url, path, server.accessToken)
170 })
171
172 it('Should fail with a bad count pagination', async function () {
173 await checkBadCountPagination(server.url, path, server.accessToken)
174 })
175
176 it('Should fail with an incorrect sort', async function () {
177 await checkBadSortPagination(server.url, path, server.accessToken)
178 })
179
180 it('Should success with the correct parameters', async function () {
181 await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 })
182 })
183 })
184
185 describe('When adding a video', function () {
186 let baseCorrectParams
187 const baseCorrectAttaches = {
188 fixture: buildAbsoluteFixturePath('video_short.webm')
189 }
190
191 before(function () {
192 // Put in before to have channelId
193 baseCorrectParams = {
194 name: 'my super name',
195 category: 5,
196 licence: 1,
197 language: 'pt',
198 nsfw: false,
199 commentsEnabled: true,
200 downloadEnabled: true,
201 waitTranscoding: true,
202 description: 'my super description',
203 support: 'my super support text',
204 tags: [ 'tag1', 'tag2' ],
205 privacy: VideoPrivacy.PUBLIC,
206 channelId,
207 originallyPublishedAt: new Date().toISOString()
208 }
209 })
210
211 function runSuite (mode: 'legacy' | 'resumable') {
212
213 const baseOptions = () => {
214 return {
215 server,
216 token: server.accessToken,
217 expectedStatus: HttpStatusCode.BAD_REQUEST_400,
218 mode
219 }
220 }
221
222 it('Should fail with nothing', async function () {
223 const fields = {}
224 const attaches = {}
225 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
226 })
227
228 it('Should fail without name', async function () {
229 const fields = omit(baseCorrectParams, [ 'name' ])
230 const attaches = baseCorrectAttaches
231
232 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
233 })
234
235 it('Should fail with a long name', async function () {
236 const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
237 const attaches = baseCorrectAttaches
238
239 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
240 })
241
242 it('Should fail with a bad category', async function () {
243 const fields = { ...baseCorrectParams, category: 125 }
244 const attaches = baseCorrectAttaches
245
246 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
247 })
248
249 it('Should fail with a bad licence', async function () {
250 const fields = { ...baseCorrectParams, licence: 125 }
251 const attaches = baseCorrectAttaches
252
253 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
254 })
255
256 it('Should fail with a bad language', async function () {
257 const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
258 const attaches = baseCorrectAttaches
259
260 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
261 })
262
263 it('Should fail with a long description', async function () {
264 const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
265 const attaches = baseCorrectAttaches
266
267 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
268 })
269
270 it('Should fail with a long support text', async function () {
271 const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
272 const attaches = baseCorrectAttaches
273
274 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
275 })
276
277 it('Should fail without a channel', async function () {
278 const fields = omit(baseCorrectParams, [ 'channelId' ])
279 const attaches = baseCorrectAttaches
280
281 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
282 })
283
284 it('Should fail with a bad channel', async function () {
285 const fields = { ...baseCorrectParams, channelId: 545454 }
286 const attaches = baseCorrectAttaches
287
288 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
289 })
290
291 it('Should fail with another user channel', async function () {
292 const user = {
293 username: 'fake' + randomInt(0, 1500),
294 password: 'fake_password'
295 }
296 await server.users.create({ username: user.username, password: user.password })
297
298 const accessTokenUser = await server.login.getAccessToken(user)
299 const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser })
300 const customChannelId = videoChannels[0].id
301
302 const fields = { ...baseCorrectParams, channelId: customChannelId }
303 const attaches = baseCorrectAttaches
304
305 await checkUploadVideoParam({
306 ...baseOptions(),
307 token: userAccessToken,
308 attributes: { ...fields, ...attaches }
309 })
310 })
311
312 it('Should fail with too many tags', async function () {
313 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
314 const attaches = baseCorrectAttaches
315
316 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
317 })
318
319 it('Should fail with a tag length too low', async function () {
320 const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
321 const attaches = baseCorrectAttaches
322
323 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
324 })
325
326 it('Should fail with a tag length too big', async function () {
327 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
328 const attaches = baseCorrectAttaches
329
330 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
331 })
332
333 it('Should fail with a bad schedule update (miss updateAt)', async function () {
334 const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }
335 const attaches = baseCorrectAttaches
336
337 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
338 })
339
340 it('Should fail with a bad schedule update (wrong updateAt)', async function () {
341 const fields = {
342 ...baseCorrectParams,
343
344 scheduleUpdate: {
345 privacy: VideoPrivacy.PUBLIC,
346 updateAt: 'toto'
347 }
348 }
349 const attaches = baseCorrectAttaches
350
351 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
352 })
353
354 it('Should fail with a bad originally published at attribute', async function () {
355 const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' }
356 const attaches = baseCorrectAttaches
357
358 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
359 })
360
361 it('Should fail without an input file', async function () {
362 const fields = baseCorrectParams
363 const attaches = {}
364 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
365 })
366
367 it('Should fail with an incorrect input file', async function () {
368 const fields = baseCorrectParams
369 let attaches = { fixture: buildAbsoluteFixturePath('video_short_fake.webm') }
370
371 await checkUploadVideoParam({
372 ...baseOptions(),
373 attributes: { ...fields, ...attaches },
374 // 200 for the init request, 422 when the file has finished being uploaded
375 expectedStatus: undefined,
376 completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422
377 })
378
379 attaches = { fixture: buildAbsoluteFixturePath('video_short.mkv') }
380 await checkUploadVideoParam({
381 ...baseOptions(),
382 attributes: { ...fields, ...attaches },
383 expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415
384 })
385 })
386
387 it('Should fail with an incorrect thumbnail file', async function () {
388 const fields = baseCorrectParams
389 const attaches = {
390 thumbnailfile: buildAbsoluteFixturePath('video_short.mp4'),
391 fixture: buildAbsoluteFixturePath('video_short.mp4')
392 }
393
394 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
395 })
396
397 it('Should fail with a big thumbnail file', async function () {
398 const fields = baseCorrectParams
399 const attaches = {
400 thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png'),
401 fixture: buildAbsoluteFixturePath('video_short.mp4')
402 }
403
404 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
405 })
406
407 it('Should fail with an incorrect preview file', async function () {
408 const fields = baseCorrectParams
409 const attaches = {
410 previewfile: buildAbsoluteFixturePath('video_short.mp4'),
411 fixture: buildAbsoluteFixturePath('video_short.mp4')
412 }
413
414 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
415 })
416
417 it('Should fail with a big preview file', async function () {
418 const fields = baseCorrectParams
419 const attaches = {
420 previewfile: buildAbsoluteFixturePath('custom-preview-big.png'),
421 fixture: buildAbsoluteFixturePath('video_short.mp4')
422 }
423
424 await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
425 })
426
427 it('Should report the appropriate error', async function () {
428 const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
429 const attaches = baseCorrectAttaches
430
431 const attributes = { ...fields, ...attaches }
432 const body = await checkUploadVideoParam({ ...baseOptions(), attributes })
433
434 const error = body as unknown as PeerTubeProblemDocument
435
436 if (mode === 'legacy') {
437 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy')
438 } else {
439 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit')
440 }
441
442 expect(error.type).to.equal('about:blank')
443 expect(error.title).to.equal('Bad Request')
444
445 expect(error.detail).to.equal('Incorrect request parameters: language')
446 expect(error.error).to.equal('Incorrect request parameters: language')
447
448 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
449 expect(error['invalid-params'].language).to.exist
450 })
451
452 it('Should succeed with the correct parameters', async function () {
453 this.timeout(30000)
454
455 const fields = baseCorrectParams
456
457 {
458 const attaches = baseCorrectAttaches
459 await checkUploadVideoParam({
460 ...baseOptions(),
461 attributes: { ...fields, ...attaches },
462 expectedStatus: HttpStatusCode.OK_200
463 })
464 }
465
466 {
467 const attaches = {
468 ...baseCorrectAttaches,
469
470 videofile: buildAbsoluteFixturePath('video_short.mp4')
471 }
472
473 await checkUploadVideoParam({
474 ...baseOptions(),
475 attributes: { ...fields, ...attaches },
476 expectedStatus: HttpStatusCode.OK_200
477 })
478 }
479
480 {
481 const attaches = {
482 ...baseCorrectAttaches,
483
484 videofile: buildAbsoluteFixturePath('video_short.ogv')
485 }
486
487 await checkUploadVideoParam({
488 ...baseOptions(),
489 attributes: { ...fields, ...attaches },
490 expectedStatus: HttpStatusCode.OK_200
491 })
492 }
493 })
494 }
495
496 describe('Resumable upload', function () {
497 runSuite('resumable')
498 })
499
500 describe('Legacy upload', function () {
501 runSuite('legacy')
502 })
503 })
504
505 describe('When updating a video', function () {
506 const baseCorrectParams = {
507 name: 'my super name',
508 category: 5,
509 licence: 2,
510 language: 'pt',
511 nsfw: false,
512 commentsEnabled: false,
513 downloadEnabled: false,
514 description: 'my super description',
515 privacy: VideoPrivacy.PUBLIC,
516 tags: [ 'tag1', 'tag2' ]
517 }
518
519 before(async function () {
520 const { data } = await server.videos.list()
521 video = data[0]
522 })
523
524 it('Should fail with nothing', async function () {
525 const fields = {}
526 await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields })
527 })
528
529 it('Should fail without a valid uuid', async function () {
530 const fields = baseCorrectParams
531 await makePutBodyRequest({ url: server.url, path: path + 'blabla', token: server.accessToken, fields })
532 })
533
534 it('Should fail with an unknown id', async function () {
535 const fields = baseCorrectParams
536
537 await makePutBodyRequest({
538 url: server.url,
539 path: path + '4da6fde3-88f7-4d16-b119-108df5630b06',
540 token: server.accessToken,
541 fields,
542 expectedStatus: HttpStatusCode.NOT_FOUND_404
543 })
544 })
545
546 it('Should fail with a long name', async function () {
547 const fields = { ...baseCorrectParams, name: 'super'.repeat(65) }
548
549 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
550 })
551
552 it('Should fail with a bad category', async function () {
553 const fields = { ...baseCorrectParams, category: 125 }
554
555 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
556 })
557
558 it('Should fail with a bad licence', async function () {
559 const fields = { ...baseCorrectParams, licence: 125 }
560
561 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
562 })
563
564 it('Should fail with a bad language', async function () {
565 const fields = { ...baseCorrectParams, language: 'a'.repeat(15) }
566
567 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
568 })
569
570 it('Should fail with a long description', async function () {
571 const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) }
572
573 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
574 })
575
576 it('Should fail with a long support text', async function () {
577 const fields = { ...baseCorrectParams, support: 'super'.repeat(201) }
578
579 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
580 })
581
582 it('Should fail with a bad channel', async function () {
583 const fields = { ...baseCorrectParams, channelId: 545454 }
584
585 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
586 })
587
588 it('Should fail with too many tags', async function () {
589 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] }
590
591 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
592 })
593
594 it('Should fail with a tag length too low', async function () {
595 const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] }
596
597 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
598 })
599
600 it('Should fail with a tag length too big', async function () {
601 const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] }
602
603 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
604 })
605
606 it('Should fail with a bad schedule update (miss updateAt)', async function () {
607 const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } }
608
609 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
610 })
611
612 it('Should fail with a bad schedule update (wrong updateAt)', async function () {
613 const fields = { ...baseCorrectParams, scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } }
614
615 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
616 })
617
618 it('Should fail with a bad originally published at param', async function () {
619 const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' }
620
621 await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
622 })
623
624 it('Should fail with an incorrect thumbnail file', async function () {
625 const fields = baseCorrectParams
626 const attaches = {
627 thumbnailfile: buildAbsoluteFixturePath('video_short.mp4')
628 }
629
630 await makeUploadRequest({
631 url: server.url,
632 method: 'PUT',
633 path: path + video.shortUUID,
634 token: server.accessToken,
635 fields,
636 attaches
637 })
638 })
639
640 it('Should fail with a big thumbnail file', async function () {
641 const fields = baseCorrectParams
642 const attaches = {
643 thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png')
644 }
645
646 await makeUploadRequest({
647 url: server.url,
648 method: 'PUT',
649 path: path + video.shortUUID,
650 token: server.accessToken,
651 fields,
652 attaches
653 })
654 })
655
656 it('Should fail with an incorrect preview file', async function () {
657 const fields = baseCorrectParams
658 const attaches = {
659 previewfile: buildAbsoluteFixturePath('video_short.mp4')
660 }
661
662 await makeUploadRequest({
663 url: server.url,
664 method: 'PUT',
665 path: path + video.shortUUID,
666 token: server.accessToken,
667 fields,
668 attaches
669 })
670 })
671
672 it('Should fail with a big preview file', async function () {
673 const fields = baseCorrectParams
674 const attaches = {
675 previewfile: buildAbsoluteFixturePath('custom-preview-big.png')
676 }
677
678 await makeUploadRequest({
679 url: server.url,
680 method: 'PUT',
681 path: path + video.shortUUID,
682 token: server.accessToken,
683 fields,
684 attaches
685 })
686 })
687
688 it('Should fail with a video of another user without the appropriate right', async function () {
689 const fields = baseCorrectParams
690
691 await makePutBodyRequest({
692 url: server.url,
693 path: path + video.shortUUID,
694 token: userAccessToken,
695 fields,
696 expectedStatus: HttpStatusCode.FORBIDDEN_403
697 })
698 })
699
700 it('Should fail with a video of another server')
701
702 it('Shoud report the appropriate error', async function () {
703 const fields = { ...baseCorrectParams, licence: 125 }
704
705 const res = await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
706 const error = res.body as PeerTubeProblemDocument
707
708 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo')
709
710 expect(error.type).to.equal('about:blank')
711 expect(error.title).to.equal('Bad Request')
712
713 expect(error.detail).to.equal('Incorrect request parameters: licence')
714 expect(error.error).to.equal('Incorrect request parameters: licence')
715
716 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
717 expect(error['invalid-params'].licence).to.exist
718 })
719
720 it('Should succeed with the correct parameters', async function () {
721 const fields = baseCorrectParams
722
723 await makePutBodyRequest({
724 url: server.url,
725 path: path + video.shortUUID,
726 token: server.accessToken,
727 fields,
728 expectedStatus: HttpStatusCode.NO_CONTENT_204
729 })
730 })
731 })
732
733 describe('When getting a video', function () {
734 it('Should return the list of the videos with nothing', async function () {
735 const res = await makeGetRequest({
736 url: server.url,
737 path,
738 expectedStatus: HttpStatusCode.OK_200
739 })
740
741 expect(res.body.data).to.be.an('array')
742 expect(res.body.data.length).to.equal(6)
743 })
744
745 it('Should fail without a correct uuid', async function () {
746 await server.videos.get({ id: 'coucou', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
747 })
748
749 it('Should return 404 with an incorrect video', async function () {
750 await server.videos.get({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
751 })
752
753 it('Shoud report the appropriate error', async function () {
754 const body = await server.videos.get({ id: 'hi', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
755 const error = body as unknown as PeerTubeProblemDocument
756
757 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo')
758
759 expect(error.type).to.equal('about:blank')
760 expect(error.title).to.equal('Bad Request')
761
762 expect(error.detail).to.equal('Incorrect request parameters: id')
763 expect(error.error).to.equal('Incorrect request parameters: id')
764
765 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
766 expect(error['invalid-params'].id).to.exist
767 })
768
769 it('Should succeed with the correct parameters', async function () {
770 await server.videos.get({ id: video.shortUUID })
771 })
772 })
773
774 describe('When rating a video', function () {
775 let videoId: number
776
777 before(async function () {
778 const { data } = await server.videos.list()
779 videoId = data[0].id
780 })
781
782 it('Should fail without a valid uuid', async function () {
783 const fields = {
784 rating: 'like'
785 }
786 await makePutBodyRequest({ url: server.url, path: path + 'blabla/rate', token: server.accessToken, fields })
787 })
788
789 it('Should fail with an unknown id', async function () {
790 const fields = {
791 rating: 'like'
792 }
793 await makePutBodyRequest({
794 url: server.url,
795 path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate',
796 token: server.accessToken,
797 fields,
798 expectedStatus: HttpStatusCode.NOT_FOUND_404
799 })
800 })
801
802 it('Should fail with a wrong rating', async function () {
803 const fields = {
804 rating: 'likes'
805 }
806 await makePutBodyRequest({ url: server.url, path: path + videoId + '/rate', token: server.accessToken, fields })
807 })
808
809 it('Should fail with a private video of another user', async function () {
810 const fields = {
811 rating: 'like'
812 }
813 await makePutBodyRequest({
814 url: server.url,
815 path: path + privateVideo.uuid + '/rate',
816 token: userAccessToken,
817 fields,
818 expectedStatus: HttpStatusCode.FORBIDDEN_403
819 })
820 })
821
822 it('Should succeed with the correct parameters', async function () {
823 const fields = {
824 rating: 'like'
825 }
826 await makePutBodyRequest({
827 url: server.url,
828 path: path + videoId + '/rate',
829 token: server.accessToken,
830 fields,
831 expectedStatus: HttpStatusCode.NO_CONTENT_204
832 })
833 })
834 })
835
836 describe('When removing a video', function () {
837 it('Should have 404 with nothing', async function () {
838 await makeDeleteRequest({
839 url: server.url,
840 path,
841 expectedStatus: HttpStatusCode.BAD_REQUEST_400
842 })
843 })
844
845 it('Should fail without a correct uuid', async function () {
846 await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
847 })
848
849 it('Should fail with a video which does not exist', async function () {
850 await server.videos.remove({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
851 })
852
853 it('Should fail with a video of another user without the appropriate right', async function () {
854 await server.videos.remove({ token: userAccessToken, id: video.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
855 })
856
857 it('Should fail with a video of another server')
858
859 it('Shoud report the appropriate error', async function () {
860 const body = await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
861 const error = body as PeerTubeProblemDocument
862
863 expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo')
864
865 expect(error.type).to.equal('about:blank')
866 expect(error.title).to.equal('Bad Request')
867
868 expect(error.detail).to.equal('Incorrect request parameters: id')
869 expect(error.error).to.equal('Incorrect request parameters: id')
870
871 expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
872 expect(error['invalid-params'].id).to.exist
873 })
874
875 it('Should succeed with the correct parameters', async function () {
876 await server.videos.remove({ id: video.uuid })
877 })
878 })
879
880 after(async function () {
881 await cleanupTests([ server ])
882 })
883})
diff --git a/packages/tests/src/api/check-params/views.ts b/packages/tests/src/api/check-params/views.ts
new file mode 100644
index 000000000..c454d4b80
--- /dev/null
+++ b/packages/tests/src/api/check-params/views.ts
@@ -0,0 +1,227 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createMultipleServers,
7 doubleFollow,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@peertube/peertube-server-commands'
12
13describe('Test videos views', function () {
14 let servers: PeerTubeServer[]
15 let liveVideoId: string
16 let videoId: string
17 let remoteVideoId: string
18 let userAccessToken: string
19
20 before(async function () {
21 this.timeout(120000)
22
23 servers = await createMultipleServers(2)
24 await setAccessTokensToServers(servers)
25 await setDefaultVideoChannel(servers)
26
27 await servers[0].config.enableLive({ allowReplay: false, transcoding: false });
28
29 ({ uuid: videoId } = await servers[0].videos.quickUpload({ name: 'video' }));
30 ({ uuid: remoteVideoId } = await servers[1].videos.quickUpload({ name: 'video' }));
31 ({ uuid: liveVideoId } = await servers[0].live.create({
32 fields: {
33 name: 'live',
34 privacy: VideoPrivacy.PUBLIC,
35 channelId: servers[0].store.channel.id
36 }
37 }))
38
39 userAccessToken = await servers[0].users.generateUserAndToken('user')
40
41 await doubleFollow(servers[0], servers[1])
42 })
43
44 describe('When viewing a video', async function () {
45
46 it('Should fail without current time', async function () {
47 await servers[0].views.view({ id: videoId, currentTime: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
48 })
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, 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,
73 token: userAccessToken,
74 expectedStatus: HttpStatusCode.FORBIDDEN_403
75 })
76 })
77
78 it('Should fail with an invalid start date', async function () {
79 await servers[0].videoStats.getOverallStats({
80 videoId,
81 startDate: 'fake' as any,
82 endDate: new Date().toISOString(),
83 expectedStatus: HttpStatusCode.BAD_REQUEST_400
84 })
85 })
86
87 it('Should fail with an invalid end date', async function () {
88 await servers[0].videoStats.getOverallStats({
89 videoId,
90 startDate: new Date().toISOString(),
91 endDate: 'fake' as any,
92 expectedStatus: HttpStatusCode.BAD_REQUEST_400
93 })
94 })
95
96 it('Should succeed with the correct parameters', async function () {
97 await servers[0].videoStats.getOverallStats({
98 videoId,
99 startDate: new Date().toISOString(),
100 endDate: new Date().toISOString()
101 })
102 })
103 })
104
105 describe('When getting timeserie stats', function () {
106
107 it('Should fail with a remote video', async function () {
108 await servers[0].videoStats.getTimeserieStats({
109 videoId: remoteVideoId,
110 metric: 'viewers',
111 expectedStatus: HttpStatusCode.FORBIDDEN_403
112 })
113 })
114
115 it('Should fail without token', async function () {
116 await servers[0].videoStats.getTimeserieStats({
117 videoId,
118 token: null,
119 metric: 'viewers',
120 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
121 })
122 })
123
124 it('Should fail with another token', async function () {
125 await servers[0].videoStats.getTimeserieStats({
126 videoId,
127 token: userAccessToken,
128 metric: 'viewers',
129 expectedStatus: HttpStatusCode.FORBIDDEN_403
130 })
131 })
132
133 it('Should fail with an invalid metric', async function () {
134 await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
135 })
136
137 it('Should fail with an invalid start date', async function () {
138 await servers[0].videoStats.getTimeserieStats({
139 videoId,
140 metric: 'viewers',
141 startDate: 'fake' as any,
142 endDate: new Date(),
143 expectedStatus: HttpStatusCode.BAD_REQUEST_400
144 })
145 })
146
147 it('Should fail with an invalid end date', async function () {
148 await servers[0].videoStats.getTimeserieStats({
149 videoId,
150 metric: 'viewers',
151 startDate: new Date(),
152 endDate: 'fake' as any,
153 expectedStatus: HttpStatusCode.BAD_REQUEST_400
154 })
155 })
156
157 it('Should fail if start date is specified but not end date', async function () {
158 await servers[0].videoStats.getTimeserieStats({
159 videoId,
160 metric: 'viewers',
161 startDate: new Date(),
162 expectedStatus: HttpStatusCode.BAD_REQUEST_400
163 })
164 })
165
166 it('Should fail if end date is specified but not start date', async function () {
167 await servers[0].videoStats.getTimeserieStats({
168 videoId,
169 metric: 'viewers',
170 endDate: new Date(),
171 expectedStatus: HttpStatusCode.BAD_REQUEST_400
172 })
173 })
174
175 it('Should fail with a too big interval', async function () {
176 await servers[0].videoStats.getTimeserieStats({
177 videoId,
178 metric: 'viewers',
179 startDate: new Date('2000-04-07T08:31:57.126Z'),
180 endDate: new Date(),
181 expectedStatus: HttpStatusCode.BAD_REQUEST_400
182 })
183 })
184
185 it('Should succeed with the correct parameters', async function () {
186 await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' })
187 })
188 })
189
190 describe('When getting retention stats', function () {
191
192 it('Should fail with a remote video', async function () {
193 await servers[0].videoStats.getRetentionStats({
194 videoId: remoteVideoId,
195 expectedStatus: HttpStatusCode.FORBIDDEN_403
196 })
197 })
198
199 it('Should fail without token', async function () {
200 await servers[0].videoStats.getRetentionStats({
201 videoId,
202 token: null,
203 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
204 })
205 })
206
207 it('Should fail with another token', async function () {
208 await servers[0].videoStats.getRetentionStats({
209 videoId,
210 token: userAccessToken,
211 expectedStatus: HttpStatusCode.FORBIDDEN_403
212 })
213 })
214
215 it('Should fail on live video', async function () {
216 await servers[0].videoStats.getRetentionStats({ videoId: liveVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
217 })
218
219 it('Should succeed with the correct parameters', async function () {
220 await servers[0].videoStats.getRetentionStats({ videoId })
221 })
222 })
223
224 after(async function () {
225 await cleanupTests(servers)
226 })
227})
diff --git a/packages/tests/src/api/live/index.ts b/packages/tests/src/api/live/index.ts
new file mode 100644
index 000000000..e61e6c611
--- /dev/null
+++ b/packages/tests/src/api/live/index.ts
@@ -0,0 +1,7 @@
1import './live-constraints.js'
2import './live-fast-restream.js'
3import './live-socket-messages.js'
4import './live-permanent.js'
5import './live-rtmps.js'
6import './live-save-replay.js'
7import './live.js'
diff --git a/packages/tests/src/api/live/live-constraints.ts b/packages/tests/src/api/live/live-constraints.ts
new file mode 100644
index 000000000..f62994cbd
--- /dev/null
+++ b/packages/tests/src/api/live/live-constraints.ts
@@ -0,0 +1,237 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoError, UserVideoQuota, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 ConfigCommand,
9 createMultipleServers,
10 doubleFollow,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel,
14 stopFfmpeg,
15 waitJobs,
16 waitUntilLiveReplacedByReplayOnAllServers,
17 waitUntilLiveWaitingOnAllServers
18} from '@peertube/peertube-server-commands'
19import { checkLiveCleanup } from '../../shared/live.js'
20
21describe('Test live constraints', function () {
22 let servers: PeerTubeServer[] = []
23 let userId: number
24 let userAccessToken: string
25 let userChannelId: number
26
27 async function createLiveWrapper (options: { replay: boolean, permanent: boolean }) {
28 const { replay, permanent } = options
29
30 const liveAttributes = {
31 name: 'user live',
32 channelId: userChannelId,
33 privacy: VideoPrivacy.PUBLIC,
34 saveReplay: replay,
35 replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
36 permanentLive: permanent
37 }
38
39 const { uuid } = await servers[0].live.create({ token: userAccessToken, fields: liveAttributes })
40 return uuid
41 }
42
43 async function checkSaveReplay (videoId: string, resolutions = [ 720 ]) {
44 for (const server of servers) {
45 const video = await server.videos.get({ id: videoId })
46 expect(video.isLive).to.be.false
47 expect(video.duration).to.be.greaterThan(0)
48 }
49
50 await checkLiveCleanup({ server: servers[0], permanent: false, videoUUID: videoId, savedResolutions: resolutions })
51 }
52
53 function updateQuota (options: { total: number, daily: number }) {
54 return servers[0].users.update({
55 userId,
56 videoQuota: options.total,
57 videoQuotaDaily: options.daily
58 })
59 }
60
61 before(async function () {
62 this.timeout(120000)
63
64 servers = await createMultipleServers(2)
65
66 // Get the access tokens
67 await setAccessTokensToServers(servers)
68 await setDefaultVideoChannel(servers)
69
70 await servers[0].config.updateCustomSubConfig({
71 newConfig: {
72 live: {
73 enabled: true,
74 allowReplay: true,
75 transcoding: {
76 enabled: false
77 }
78 }
79 }
80 })
81
82 {
83 const res = await servers[0].users.generate('user1')
84 userId = res.userId
85 userChannelId = res.userChannelId
86 userAccessToken = res.token
87
88 await updateQuota({ total: 1, daily: -1 })
89 }
90
91 // Server 1 and server 2 follow each other
92 await doubleFollow(servers[0], servers[1])
93 })
94
95 it('Should not have size limit if save replay is disabled', async function () {
96 this.timeout(60000)
97
98 const userVideoLiveoId = await createLiveWrapper({ replay: false, permanent: false })
99 await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false })
100 })
101
102 it('Should have size limit depending on user global quota if save replay is enabled on non permanent live', async function () {
103 this.timeout(60000)
104
105 // Wait for user quota memoize cache invalidation
106 await wait(5000)
107
108 const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
109 await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
110
111 await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId)
112 await waitJobs(servers)
113
114 await checkSaveReplay(userVideoLiveoId)
115
116 const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId })
117 expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED)
118 })
119
120 it('Should have size limit depending on user global quota if save replay is enabled on a permanent live', async function () {
121 this.timeout(60000)
122
123 // Wait for user quota memoize cache invalidation
124 await wait(5000)
125
126 const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: true })
127 await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
128
129 await waitJobs(servers)
130 await waitUntilLiveWaitingOnAllServers(servers, userVideoLiveoId)
131
132 const session = await servers[0].live.findLatestSession({ videoId: userVideoLiveoId })
133 expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED)
134 })
135
136 it('Should have size limit depending on user daily quota if save replay is enabled', async function () {
137 this.timeout(60000)
138
139 // Wait for user quota memoize cache invalidation
140 await wait(5000)
141
142 await updateQuota({ total: -1, daily: 1 })
143
144 const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
145 await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
146
147 await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId)
148 await waitJobs(servers)
149
150 await checkSaveReplay(userVideoLiveoId)
151
152 const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId })
153 expect(session.error).to.equal(LiveVideoError.QUOTA_EXCEEDED)
154 })
155
156 it('Should succeed without quota limit', async function () {
157 this.timeout(60000)
158
159 // Wait for user quota memoize cache invalidation
160 await wait(5000)
161
162 await updateQuota({ total: 10 * 1000 * 1000, daily: -1 })
163
164 const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
165 await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: false })
166 })
167
168 it('Should have the same quota in admin and as a user', async function () {
169 this.timeout(120000)
170
171 const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
172 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ token: userAccessToken, videoId: userVideoLiveoId })
173
174 await servers[0].live.waitUntilPublished({ videoId: userVideoLiveoId })
175 // Wait previous live cleanups
176 await wait(3000)
177
178 const baseQuota = await servers[0].users.getMyQuotaUsed({ token: userAccessToken })
179
180 let quotaUser: UserVideoQuota
181
182 do {
183 await wait(500)
184
185 quotaUser = await servers[0].users.getMyQuotaUsed({ token: userAccessToken })
186 } while (quotaUser.videoQuotaUsed <= baseQuota.videoQuotaUsed)
187
188 const { data } = await servers[0].users.list()
189 const quotaAdmin = data.find(u => u.username === 'user1')
190
191 expect(quotaUser.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed)
192 expect(quotaUser.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily)
193
194 expect(quotaAdmin.videoQuotaUsed).to.be.above(baseQuota.videoQuotaUsed)
195 expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(baseQuota.videoQuotaUsedDaily)
196
197 expect(quotaUser.videoQuotaUsed).to.be.above(10)
198 expect(quotaUser.videoQuotaUsedDaily).to.be.above(10)
199 expect(quotaAdmin.videoQuotaUsed).to.be.above(10)
200 expect(quotaAdmin.videoQuotaUsedDaily).to.be.above(10)
201
202 await stopFfmpeg(ffmpegCommand)
203 })
204
205 it('Should have max duration limit', async function () {
206 this.timeout(240000)
207
208 await servers[0].config.updateCustomSubConfig({
209 newConfig: {
210 live: {
211 enabled: true,
212 allowReplay: true,
213 maxDuration: 15,
214 transcoding: {
215 enabled: true,
216 resolutions: ConfigCommand.getCustomConfigResolutions(true)
217 }
218 }
219 }
220 })
221
222 const userVideoLiveoId = await createLiveWrapper({ replay: true, permanent: false })
223 await servers[0].live.runAndTestStreamError({ token: userAccessToken, videoId: userVideoLiveoId, shouldHaveError: true })
224
225 await waitUntilLiveReplacedByReplayOnAllServers(servers, userVideoLiveoId)
226 await waitJobs(servers)
227
228 await checkSaveReplay(userVideoLiveoId, [ 720, 480, 360, 240, 144 ])
229
230 const session = await servers[0].live.getReplaySession({ videoId: userVideoLiveoId })
231 expect(session.error).to.equal(LiveVideoError.DURATION_EXCEEDED)
232 })
233
234 after(async function () {
235 await cleanupTests(servers)
236 })
237})
diff --git a/packages/tests/src/api/live/live-fast-restream.ts b/packages/tests/src/api/live/live-fast-restream.ts
new file mode 100644
index 000000000..d34b00cbe
--- /dev/null
+++ b/packages/tests/src/api/live/live-fast-restream.ts
@@ -0,0 +1,153 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultVideoChannel,
12 stopFfmpeg,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15
16describe('Fast restream in live', function () {
17 let server: PeerTubeServer
18
19 async function createLiveWrapper (options: { permanent: boolean, replay: boolean }) {
20 const attributes: LiveVideoCreate = {
21 channelId: server.store.channel.id,
22 privacy: VideoPrivacy.PUBLIC,
23 name: 'my super live',
24 saveReplay: options.replay,
25 replaySettings: options.replay ? { privacy: VideoPrivacy.PUBLIC } : undefined,
26 permanentLive: options.permanent
27 }
28
29 const { uuid } = await server.live.create({ fields: attributes })
30 return uuid
31 }
32
33 async function fastRestreamWrapper ({ replay }: { replay: boolean }) {
34 const liveVideoUUID = await createLiveWrapper({ permanent: true, replay })
35 await waitJobs([ server ])
36
37 const rtmpOptions = {
38 videoId: liveVideoUUID,
39 copyCodecs: true,
40 fixtureName: 'video_short.mp4'
41 }
42
43 // Streaming session #1
44 let ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions)
45 await server.live.waitUntilPublished({ videoId: liveVideoUUID })
46
47 const video = await server.videos.get({ id: liveVideoUUID })
48 const session1PlaylistId = video.streamingPlaylists[0].id
49
50 await stopFfmpeg(ffmpegCommand)
51 await server.live.waitUntilWaiting({ videoId: liveVideoUUID })
52
53 // Streaming session #2
54 ffmpegCommand = await server.live.sendRTMPStreamInVideo(rtmpOptions)
55
56 let hasNewPlaylist = false
57 do {
58 const video = await server.videos.get({ id: liveVideoUUID })
59 hasNewPlaylist = video.streamingPlaylists.length === 1 && video.streamingPlaylists[0].id !== session1PlaylistId
60
61 await wait(100)
62 } while (!hasNewPlaylist)
63
64 await server.live.waitUntilSegmentGeneration({
65 server,
66 videoUUID: liveVideoUUID,
67 segment: 1,
68 playlistNumber: 0
69 })
70
71 return { ffmpegCommand, liveVideoUUID }
72 }
73
74 async function ensureLastLiveWorks (liveId: string) {
75 // Equivalent to PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY
76 for (let i = 0; i < 100; i++) {
77 const video = await server.videos.get({ id: liveId })
78 expect(video.streamingPlaylists).to.have.lengthOf(1)
79
80 try {
81 await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 })
82 await server.streamingPlaylists.get({ url: video.streamingPlaylists[0].playlistUrl })
83 await server.streamingPlaylists.getSegmentSha256({ url: video.streamingPlaylists[0].segmentsSha256Url })
84 } catch (err) {
85 // FIXME: try to debug error in CI "Unexpected end of JSON input"
86 console.error(err)
87 throw err
88 }
89
90 await wait(100)
91 }
92 }
93
94 async function runTest (replay: boolean) {
95 const { ffmpegCommand, liveVideoUUID } = await fastRestreamWrapper({ replay })
96
97 // TODO: remove, we try to debug a test timeout failure here
98 console.log('Ensuring last live works')
99
100 await ensureLastLiveWorks(liveVideoUUID)
101
102 await stopFfmpeg(ffmpegCommand)
103 await server.live.waitUntilWaiting({ videoId: liveVideoUUID })
104
105 // Wait for replays
106 await waitJobs([ server ])
107
108 const { total, data: sessions } = await server.live.listSessions({ videoId: liveVideoUUID })
109
110 expect(total).to.equal(2)
111 expect(sessions).to.have.lengthOf(2)
112
113 for (const session of sessions) {
114 expect(session.error).to.be.null
115
116 if (replay) {
117 expect(session.replayVideo).to.exist
118
119 await server.videos.get({ id: session.replayVideo.uuid })
120 } else {
121 expect(session.replayVideo).to.not.exist
122 }
123 }
124 }
125
126 before(async function () {
127 this.timeout(120000)
128
129 const env = { PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY: '10000' }
130 server = await createSingleServer(1, {}, { env })
131
132 // Get the access tokens
133 await setAccessTokensToServers([ server ])
134 await setDefaultVideoChannel([ server ])
135
136 await server.config.enableMinimumTranscoding({ webVideo: false, hls: true })
137 await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' })
138 })
139
140 it('Should correctly fast restream in a permanent live with and without save replay', async function () {
141 this.timeout(480000)
142
143 // A test can take a long time, so prefer to run them in parallel
144 await Promise.all([
145 runTest(true),
146 runTest(false)
147 ])
148 })
149
150 after(async function () {
151 await cleanupTests([ server ])
152 })
153})
diff --git a/packages/tests/src/api/live/live-permanent.ts b/packages/tests/src/api/live/live-permanent.ts
new file mode 100644
index 000000000..4ffcc7ed4
--- /dev/null
+++ b/packages/tests/src/api/live/live-permanent.ts
@@ -0,0 +1,204 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoCreate, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models'
6import { checkLiveCleanup } from '@tests/shared/live.js'
7import {
8 cleanupTests,
9 ConfigCommand,
10 createMultipleServers,
11 doubleFollow,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 setDefaultVideoChannel,
15 stopFfmpeg,
16 waitJobs
17} from '@peertube/peertube-server-commands'
18
19describe('Permanent live', function () {
20 let servers: PeerTubeServer[] = []
21 let videoUUID: string
22
23 async function createLiveWrapper (permanentLive: boolean) {
24 const attributes: LiveVideoCreate = {
25 channelId: servers[0].store.channel.id,
26 privacy: VideoPrivacy.PUBLIC,
27 name: 'my super live',
28 saveReplay: false,
29 permanentLive
30 }
31
32 const { uuid } = await servers[0].live.create({ fields: attributes })
33 return uuid
34 }
35
36 async function checkVideoState (videoId: string, state: VideoStateType) {
37 for (const server of servers) {
38 const video = await server.videos.get({ id: videoId })
39 expect(video.state.id).to.equal(state)
40 }
41 }
42
43 before(async function () {
44 this.timeout(120000)
45
46 servers = await createMultipleServers(2)
47
48 // Get the access tokens
49 await setAccessTokensToServers(servers)
50 await setDefaultVideoChannel(servers)
51
52 // Server 1 and server 2 follow each other
53 await doubleFollow(servers[0], servers[1])
54
55 await servers[0].config.updateCustomSubConfig({
56 newConfig: {
57 live: {
58 enabled: true,
59 allowReplay: true,
60 maxDuration: -1,
61 transcoding: {
62 enabled: true,
63 resolutions: ConfigCommand.getCustomConfigResolutions(true)
64 }
65 }
66 }
67 })
68 })
69
70 it('Should create a non permanent live and update it to be a permanent live', async function () {
71 this.timeout(20000)
72
73 const videoUUID = await createLiveWrapper(false)
74
75 {
76 const live = await servers[0].live.get({ videoId: videoUUID })
77 expect(live.permanentLive).to.be.false
78 }
79
80 await servers[0].live.update({ videoId: videoUUID, fields: { permanentLive: true } })
81
82 {
83 const live = await servers[0].live.get({ videoId: videoUUID })
84 expect(live.permanentLive).to.be.true
85 }
86 })
87
88 it('Should create a permanent live', async function () {
89 this.timeout(20000)
90
91 videoUUID = await createLiveWrapper(true)
92
93 const live = await servers[0].live.get({ videoId: videoUUID })
94 expect(live.permanentLive).to.be.true
95
96 await waitJobs(servers)
97 })
98
99 it('Should stream into this permanent live', async function () {
100 this.timeout(240_000)
101
102 const beforePublication = new Date()
103 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
104
105 for (const server of servers) {
106 await server.live.waitUntilPublished({ videoId: videoUUID })
107 }
108
109 await checkVideoState(videoUUID, VideoState.PUBLISHED)
110
111 for (const server of servers) {
112 const video = await server.videos.get({ id: videoUUID })
113 expect(new Date(video.publishedAt)).greaterThan(beforePublication)
114 }
115
116 await stopFfmpeg(ffmpegCommand)
117 await servers[0].live.waitUntilWaiting({ videoId: videoUUID })
118
119 await waitJobs(servers)
120 })
121
122 it('Should have cleaned up this live', async function () {
123 this.timeout(40000)
124
125 await wait(5000)
126 await waitJobs(servers)
127
128 for (const server of servers) {
129 const videoDetails = await server.videos.get({ id: videoUUID })
130
131 expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
132 }
133
134 await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID })
135 })
136
137 it('Should have set this live to waiting for live state', async function () {
138 this.timeout(20000)
139
140 await checkVideoState(videoUUID, VideoState.WAITING_FOR_LIVE)
141 })
142
143 it('Should be able to stream again in the permanent live', async function () {
144 this.timeout(60000)
145
146 await servers[0].config.updateCustomSubConfig({
147 newConfig: {
148 live: {
149 enabled: true,
150 allowReplay: true,
151 maxDuration: -1,
152 transcoding: {
153 enabled: true,
154 resolutions: ConfigCommand.getCustomConfigResolutions(false)
155 }
156 }
157 }
158 })
159
160 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
161
162 for (const server of servers) {
163 await server.live.waitUntilPublished({ videoId: videoUUID })
164 }
165
166 await checkVideoState(videoUUID, VideoState.PUBLISHED)
167
168 const count = await servers[0].live.countPlaylists({ videoUUID })
169 // master playlist and 720p playlist
170 expect(count).to.equal(2)
171
172 await stopFfmpeg(ffmpegCommand)
173 })
174
175 it('Should have appropriate sessions', async function () {
176 this.timeout(60000)
177
178 await servers[0].live.waitUntilWaiting({ videoId: videoUUID })
179
180 const { data, total } = await servers[0].live.listSessions({ videoId: videoUUID })
181 expect(total).to.equal(2)
182 expect(data).to.have.lengthOf(2)
183
184 for (const session of data) {
185 expect(session.startDate).to.exist
186 expect(session.endDate).to.exist
187
188 expect(session.error).to.not.exist
189 }
190 })
191
192 it('Should remove the live and have cleaned up the directory', async function () {
193 this.timeout(60000)
194
195 await servers[0].videos.remove({ id: videoUUID })
196 await waitJobs(servers)
197
198 await checkLiveCleanup({ server: servers[0], permanent: true, videoUUID })
199 })
200
201 after(async function () {
202 await cleanupTests(servers)
203 })
204})
diff --git a/packages/tests/src/api/live/live-rtmps.ts b/packages/tests/src/api/live/live-rtmps.ts
new file mode 100644
index 000000000..4ab59ed4c
--- /dev/null
+++ b/packages/tests/src/api/live/live-rtmps.ts
@@ -0,0 +1,143 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 PeerTubeServer,
10 sendRTMPStream,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 stopFfmpeg,
14 testFfmpegStreamError,
15 waitUntilLivePublishedOnAllServers
16} from '@peertube/peertube-server-commands'
17
18describe('Test live RTMPS', function () {
19 let server: PeerTubeServer
20 let rtmpUrl: string
21 let rtmpsUrl: string
22
23 async function createLiveWrapper () {
24 const liveAttributes = {
25 name: 'live',
26 channelId: server.store.channel.id,
27 privacy: VideoPrivacy.PUBLIC,
28 saveReplay: false
29 }
30
31 const { uuid } = await server.live.create({ fields: liveAttributes })
32
33 const live = await server.live.get({ videoId: uuid })
34 const video = await server.videos.get({ id: uuid })
35
36 return Object.assign(video, live)
37 }
38
39 before(async function () {
40 this.timeout(120000)
41
42 server = await createSingleServer(1)
43
44 // Get the access tokens
45 await setAccessTokensToServers([ server ])
46 await setDefaultVideoChannel([ server ])
47
48 await server.config.updateCustomSubConfig({
49 newConfig: {
50 live: {
51 enabled: true,
52 allowReplay: true,
53 transcoding: {
54 enabled: false
55 }
56 }
57 }
58 })
59
60 rtmpUrl = 'rtmp://' + server.hostname + ':' + server.rtmpPort + '/live'
61 rtmpsUrl = 'rtmps://' + server.hostname + ':' + server.rtmpsPort + '/live'
62 })
63
64 it('Should enable RTMPS endpoint only', async function () {
65 this.timeout(240000)
66
67 await server.kill()
68 await server.run({
69 live: {
70 rtmp: {
71 enabled: false
72 },
73 rtmps: {
74 enabled: true,
75 port: server.rtmpsPort,
76 key_file: buildAbsoluteFixturePath('rtmps.key'),
77 cert_file: buildAbsoluteFixturePath('rtmps.cert')
78 }
79 }
80 })
81
82 {
83 const liveVideo = await createLiveWrapper()
84
85 expect(liveVideo.rtmpUrl).to.not.exist
86 expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl)
87
88 const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey })
89 await testFfmpegStreamError(command, true)
90 }
91
92 {
93 const liveVideo = await createLiveWrapper()
94
95 const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey })
96 await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
97 await stopFfmpeg(command)
98 }
99 })
100
101 it('Should enable both RTMP and RTMPS', async function () {
102 this.timeout(240000)
103
104 await server.kill()
105 await server.run({
106 live: {
107 rtmp: {
108 enabled: true,
109 port: server.rtmpPort
110 },
111 rtmps: {
112 enabled: true,
113 port: server.rtmpsPort,
114 key_file: buildAbsoluteFixturePath('rtmps.key'),
115 cert_file: buildAbsoluteFixturePath('rtmps.cert')
116 }
117 }
118 })
119
120 {
121 const liveVideo = await createLiveWrapper()
122
123 expect(liveVideo.rtmpUrl).to.equal(rtmpUrl)
124 expect(liveVideo.rtmpsUrl).to.equal(rtmpsUrl)
125
126 const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl, streamKey: liveVideo.streamKey })
127 await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
128 await stopFfmpeg(command)
129 }
130
131 {
132 const liveVideo = await createLiveWrapper()
133
134 const command = sendRTMPStream({ rtmpBaseUrl: rtmpsUrl, streamKey: liveVideo.streamKey })
135 await waitUntilLivePublishedOnAllServers([ server ], liveVideo.uuid)
136 await stopFfmpeg(command)
137 }
138 })
139
140 after(async function () {
141 await cleanupTests([ server ])
142 })
143})
diff --git a/packages/tests/src/api/live/live-save-replay.ts b/packages/tests/src/api/live/live-save-replay.ts
new file mode 100644
index 000000000..84135365b
--- /dev/null
+++ b/packages/tests/src/api/live/live-save-replay.ts
@@ -0,0 +1,583 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { wait } from '@peertube/peertube-core-utils'
6import {
7 HttpStatusCode,
8 HttpStatusCodeType,
9 LiveVideoCreate,
10 LiveVideoError,
11 VideoPrivacy,
12 VideoPrivacyType,
13 VideoState,
14 VideoStateType
15} from '@peertube/peertube-models'
16import { checkLiveCleanup } from '@tests/shared/live.js'
17import {
18 cleanupTests,
19 ConfigCommand,
20 createMultipleServers,
21 doubleFollow,
22 findExternalSavedVideo,
23 PeerTubeServer,
24 setAccessTokensToServers,
25 setDefaultVideoChannel,
26 stopFfmpeg,
27 testFfmpegStreamError,
28 waitJobs,
29 waitUntilLivePublishedOnAllServers,
30 waitUntilLiveReplacedByReplayOnAllServers,
31 waitUntilLiveWaitingOnAllServers
32} from '@peertube/peertube-server-commands'
33
34describe('Save replay setting', function () {
35 let servers: PeerTubeServer[] = []
36 let liveVideoUUID: string
37 let ffmpegCommand: FfmpegCommand
38
39 async function createLiveWrapper (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) {
40 if (liveVideoUUID) {
41 try {
42 await servers[0].videos.remove({ id: liveVideoUUID })
43 await waitJobs(servers)
44 } catch {}
45 }
46
47 const attributes: LiveVideoCreate = {
48 channelId: servers[0].store.channel.id,
49 privacy: VideoPrivacy.PUBLIC,
50 name: 'live'.repeat(30),
51 saveReplay: options.replay,
52 replaySettings: options.replaySettings,
53 permanentLive: options.permanent
54 }
55
56 const { uuid } = await servers[0].live.create({ fields: attributes })
57 return uuid
58 }
59
60 async function publishLive (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) {
61 liveVideoUUID = await createLiveWrapper(options)
62
63 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
64 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
65
66 const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
67
68 await waitJobs(servers)
69 await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
70
71 return { ffmpegCommand, liveDetails }
72 }
73
74 async function publishLiveAndDelete (options: { permanent: boolean, replay: boolean, replaySettings?: { privacy: VideoPrivacyType } }) {
75 const { ffmpegCommand, liveDetails } = await publishLive(options)
76
77 await Promise.all([
78 servers[0].videos.remove({ id: liveVideoUUID }),
79 testFfmpegStreamError(ffmpegCommand, true)
80 ])
81
82 await waitJobs(servers)
83 await wait(5000)
84 await waitJobs(servers)
85
86 return { liveDetails }
87 }
88
89 async function publishLiveAndBlacklist (options: {
90 permanent: boolean
91 replay: boolean
92 replaySettings?: { privacy: VideoPrivacyType }
93 }) {
94 const { ffmpegCommand, liveDetails } = await publishLive(options)
95
96 await Promise.all([
97 servers[0].blacklist.add({ videoId: liveVideoUUID, reason: 'bad live', unfederate: true }),
98 testFfmpegStreamError(ffmpegCommand, true)
99 ])
100
101 await waitJobs(servers)
102 await wait(5000)
103 await waitJobs(servers)
104
105 return { liveDetails }
106 }
107
108 async function checkVideosExist (videoId: string, existsInList: boolean, expectedStatus?: HttpStatusCodeType) {
109 for (const server of servers) {
110 const length = existsInList ? 1 : 0
111
112 const { data, total } = await server.videos.list()
113 expect(data).to.have.lengthOf(length)
114 expect(total).to.equal(length)
115
116 if (expectedStatus) {
117 await server.videos.get({ id: videoId, expectedStatus })
118 }
119 }
120 }
121
122 async function checkVideoState (videoId: string, state: VideoStateType) {
123 for (const server of servers) {
124 const video = await server.videos.get({ id: videoId })
125 expect(video.state.id).to.equal(state)
126 }
127 }
128
129 async function checkVideoPrivacy (videoId: string, privacy: VideoPrivacyType) {
130 for (const server of servers) {
131 const video = await server.videos.get({ id: videoId })
132 expect(video.privacy.id).to.equal(privacy)
133 }
134 }
135
136 before(async function () {
137 this.timeout(120000)
138
139 servers = await createMultipleServers(2)
140
141 // Get the access tokens
142 await setAccessTokensToServers(servers)
143 await setDefaultVideoChannel(servers)
144
145 // Server 1 and server 2 follow each other
146 await doubleFollow(servers[0], servers[1])
147
148 await servers[0].config.updateCustomSubConfig({
149 newConfig: {
150 live: {
151 enabled: true,
152 allowReplay: true,
153 maxDuration: -1,
154 transcoding: {
155 enabled: false,
156 resolutions: ConfigCommand.getCustomConfigResolutions(true)
157 }
158 }
159 }
160 })
161 })
162
163 describe('With save replay disabled', function () {
164 let sessionStartDateMin: Date
165 let sessionStartDateMax: Date
166 let sessionEndDateMin: Date
167
168 it('Should correctly create and federate the "waiting for stream" live', async function () {
169 this.timeout(40000)
170
171 liveVideoUUID = await createLiveWrapper({ permanent: false, replay: false })
172
173 await waitJobs(servers)
174
175 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
176 await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
177 })
178
179 it('Should correctly have updated the live and federated it when streaming in the live', async function () {
180 this.timeout(120000)
181
182 ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
183
184 sessionStartDateMin = new Date()
185 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
186 sessionStartDateMax = new Date()
187
188 await waitJobs(servers)
189
190 await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
191 await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
192 })
193
194 it('Should correctly delete the video files after the stream ended', async function () {
195 this.timeout(120000)
196
197 sessionEndDateMin = new Date()
198 await stopFfmpeg(ffmpegCommand)
199
200 for (const server of servers) {
201 await server.live.waitUntilEnded({ videoId: liveVideoUUID })
202 }
203 await waitJobs(servers)
204
205 // Live still exist, but cannot be played anymore
206 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
207 await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED)
208
209 // No resolutions saved since we did not save replay
210 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
211 })
212
213 it('Should have appropriate ended session', async function () {
214 const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
215 expect(total).to.equal(1)
216 expect(data).to.have.lengthOf(1)
217
218 const session = data[0]
219
220 const startDate = new Date(session.startDate)
221 expect(startDate).to.be.above(sessionStartDateMin)
222 expect(startDate).to.be.below(sessionStartDateMax)
223
224 expect(session.endDate).to.exist
225 expect(new Date(session.endDate)).to.be.above(sessionEndDateMin)
226
227 expect(session.saveReplay).to.be.false
228 expect(session.error).to.not.exist
229 expect(session.replayVideo).to.not.exist
230 })
231
232 it('Should correctly terminate the stream on blacklist and delete the live', async function () {
233 this.timeout(120000)
234
235 await publishLiveAndBlacklist({ permanent: false, replay: false })
236
237 await checkVideosExist(liveVideoUUID, false)
238
239 await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
240 await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
241
242 await wait(5000)
243 await waitJobs(servers)
244 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
245 })
246
247 it('Should have blacklisted session error', async function () {
248 const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID })
249 expect(session.startDate).to.exist
250 expect(session.endDate).to.exist
251
252 expect(session.error).to.equal(LiveVideoError.BLACKLISTED)
253 expect(session.replayVideo).to.not.exist
254 })
255
256 it('Should correctly terminate the stream on delete and delete the video', async function () {
257 this.timeout(120000)
258
259 await publishLiveAndDelete({ permanent: false, replay: false })
260
261 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
262 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
263 })
264 })
265
266 describe('With save replay enabled on non permanent live', function () {
267
268 it('Should correctly create and federate the "waiting for stream" live', async function () {
269 this.timeout(120000)
270
271 liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
272
273 await waitJobs(servers)
274
275 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
276 await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
277 await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
278 })
279
280 it('Should correctly have updated the live and federated it when streaming in the live', async function () {
281 this.timeout(120000)
282
283 ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
284 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
285
286 await waitJobs(servers)
287
288 await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
289 await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
290 await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
291 })
292
293 it('Should correctly have saved the live and federated it after the streaming', async function () {
294 this.timeout(120000)
295
296 const session = await servers[0].live.findLatestSession({ videoId: liveVideoUUID })
297 expect(session.endDate).to.not.exist
298 expect(session.endingProcessed).to.be.false
299 expect(session.saveReplay).to.be.true
300 expect(session.replaySettings).to.exist
301 expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
302
303 await stopFfmpeg(ffmpegCommand)
304
305 await waitUntilLiveReplacedByReplayOnAllServers(servers, liveVideoUUID)
306 await waitJobs(servers)
307
308 // Live has been transcoded
309 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
310 await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
311 await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED)
312 })
313
314 it('Should find the replay live session', async function () {
315 const session = await servers[0].live.getReplaySession({ videoId: liveVideoUUID })
316
317 expect(session).to.exist
318
319 expect(session.startDate).to.exist
320 expect(session.endDate).to.exist
321
322 expect(session.error).to.not.exist
323 expect(session.saveReplay).to.be.true
324 expect(session.endingProcessed).to.be.true
325 expect(session.replaySettings).to.exist
326 expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
327
328 expect(session.replayVideo).to.exist
329 expect(session.replayVideo.id).to.exist
330 expect(session.replayVideo.shortUUID).to.exist
331 expect(session.replayVideo.uuid).to.equal(liveVideoUUID)
332 })
333
334 it('Should update the saved live and correctly federate the updated attributes', async function () {
335 this.timeout(120000)
336
337 await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'video updated', privacy: VideoPrivacy.PUBLIC } })
338 await waitJobs(servers)
339
340 for (const server of servers) {
341 const video = await server.videos.get({ id: liveVideoUUID })
342 expect(video.name).to.equal('video updated')
343 expect(video.isLive).to.be.false
344 expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
345 }
346 })
347
348 it('Should have cleaned up the live files', async function () {
349 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] })
350 })
351
352 it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
353 this.timeout(120000)
354
355 await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
356
357 await checkVideosExist(liveVideoUUID, false)
358
359 await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
360 await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
361
362 await wait(5000)
363 await waitJobs(servers)
364 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false, savedResolutions: [ 720 ] })
365 })
366
367 it('Should correctly terminate the stream on delete and delete the video', async function () {
368 this.timeout(120000)
369
370 await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
371
372 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
373 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
374 })
375 })
376
377 describe('With save replay enabled on permanent live', function () {
378 let lastReplayUUID: string
379
380 describe('With a first live and its replay', function () {
381
382 it('Should correctly create and federate the "waiting for stream" live', async function () {
383 this.timeout(120000)
384
385 liveVideoUUID = await createLiveWrapper({ permanent: true, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
386
387 await waitJobs(servers)
388
389 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
390 await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
391 await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
392 })
393
394 it('Should correctly have updated the live and federated it when streaming in the live', async function () {
395 this.timeout(120000)
396
397 ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
398 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
399
400 await waitJobs(servers)
401
402 await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
403 await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
404 await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
405 })
406
407 it('Should correctly have saved the live and federated it after the streaming', async function () {
408 this.timeout(120000)
409
410 const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
411
412 await stopFfmpeg(ffmpegCommand)
413
414 await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
415 await waitJobs(servers)
416
417 const video = await findExternalSavedVideo(servers[0], liveDetails)
418 expect(video).to.exist
419
420 for (const server of servers) {
421 await server.videos.get({ id: video.uuid })
422 }
423
424 lastReplayUUID = video.uuid
425 })
426
427 it('Should have appropriate ended session and replay live session', async function () {
428 const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
429 expect(total).to.equal(1)
430 expect(data).to.have.lengthOf(1)
431
432 const sessionFromLive = data[0]
433 const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID })
434
435 for (const session of [ sessionFromLive, sessionFromReplay ]) {
436 expect(session.startDate).to.exist
437 expect(session.endDate).to.exist
438
439 expect(session.replaySettings).to.exist
440 expect(session.replaySettings.privacy).to.equal(VideoPrivacy.UNLISTED)
441
442 expect(session.error).to.not.exist
443
444 expect(session.replayVideo).to.exist
445 expect(session.replayVideo.id).to.exist
446 expect(session.replayVideo.shortUUID).to.exist
447 expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
448 }
449 })
450
451 it('Should have the first live replay with correct settings', async function () {
452 await checkVideosExist(lastReplayUUID, false, HttpStatusCode.OK_200)
453 await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
454 await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.UNLISTED)
455 })
456 })
457
458 describe('With a second live and its replay', function () {
459
460 it('Should update the replay settings', async function () {
461 await servers[0].live.update({ videoId: liveVideoUUID, fields: { replaySettings: { privacy: VideoPrivacy.PUBLIC } } })
462 await waitJobs(servers)
463
464 const live = await servers[0].live.get({ videoId: liveVideoUUID })
465
466 expect(live.saveReplay).to.be.true
467 expect(live.replaySettings).to.exist
468 expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
469
470 })
471
472 it('Should correctly have updated the live and federated it when streaming in the live', async function () {
473 this.timeout(120000)
474
475 ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
476 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
477
478 await waitJobs(servers)
479
480 await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
481 await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
482 await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
483 })
484
485 it('Should correctly have saved the live and federated it after the streaming', async function () {
486 this.timeout(120000)
487
488 const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
489
490 await stopFfmpeg(ffmpegCommand)
491
492 await waitUntilLiveWaitingOnAllServers(servers, liveVideoUUID)
493 await waitJobs(servers)
494
495 const video = await findExternalSavedVideo(servers[0], liveDetails)
496 expect(video).to.exist
497
498 for (const server of servers) {
499 await server.videos.get({ id: video.uuid })
500 }
501
502 lastReplayUUID = video.uuid
503 })
504
505 it('Should have appropriate ended session and replay live session', async function () {
506 const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
507 expect(total).to.equal(2)
508 expect(data).to.have.lengthOf(2)
509
510 const sessionFromLive = data[1]
511 const sessionFromReplay = await servers[0].live.getReplaySession({ videoId: lastReplayUUID })
512
513 for (const session of [ sessionFromLive, sessionFromReplay ]) {
514 expect(session.startDate).to.exist
515 expect(session.endDate).to.exist
516
517 expect(session.replaySettings).to.exist
518 expect(session.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
519
520 expect(session.error).to.not.exist
521
522 expect(session.replayVideo).to.exist
523 expect(session.replayVideo.id).to.exist
524 expect(session.replayVideo.shortUUID).to.exist
525 expect(session.replayVideo.uuid).to.equal(lastReplayUUID)
526 }
527 })
528
529 it('Should have the first live replay with correct settings', async function () {
530 await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200)
531 await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
532 await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
533 })
534
535 it('Should have cleaned up the live files', async function () {
536 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
537 })
538
539 it('Should correctly terminate the stream on blacklist and blacklist the saved replay video', async function () {
540 this.timeout(120000)
541
542 await servers[0].videos.remove({ id: lastReplayUUID })
543 const { liveDetails } = await publishLiveAndBlacklist({
544 permanent: true,
545 replay: true,
546 replaySettings: { privacy: VideoPrivacy.PUBLIC }
547 })
548
549 const replay = await findExternalSavedVideo(servers[0], liveDetails)
550 expect(replay).to.exist
551
552 for (const videoId of [ liveVideoUUID, replay.uuid ]) {
553 await checkVideosExist(videoId, false)
554
555 await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
556 await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
557 }
558
559 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
560 })
561
562 it('Should correctly terminate the stream on delete and not save the video', async function () {
563 this.timeout(120000)
564
565 const { liveDetails } = await publishLiveAndDelete({
566 permanent: true,
567 replay: true,
568 replaySettings: { privacy: VideoPrivacy.PUBLIC }
569 })
570
571 const replay = await findExternalSavedVideo(servers[0], liveDetails)
572 expect(replay).to.not.exist
573
574 await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
575 await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
576 })
577 })
578 })
579
580 after(async function () {
581 await cleanupTests(servers)
582 })
583})
diff --git a/packages/tests/src/api/live/live-socket-messages.ts b/packages/tests/src/api/live/live-socket-messages.ts
new file mode 100644
index 000000000..80bae154c
--- /dev/null
+++ b/packages/tests/src/api/live/live-socket-messages.ts
@@ -0,0 +1,186 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { LiveVideoEventPayload, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 stopFfmpeg,
14 waitJobs,
15 waitUntilLivePublishedOnAllServers
16} from '@peertube/peertube-server-commands'
17
18describe('Test live socket messages', function () {
19 let servers: PeerTubeServer[] = []
20
21 before(async function () {
22 this.timeout(120000)
23
24 servers = await createMultipleServers(2)
25
26 // Get the access tokens
27 await setAccessTokensToServers(servers)
28 await setDefaultVideoChannel(servers)
29
30 await servers[0].config.updateCustomSubConfig({
31 newConfig: {
32 live: {
33 enabled: true,
34 allowReplay: true,
35 transcoding: {
36 enabled: false
37 }
38 }
39 }
40 })
41
42 // Server 1 and server 2 follow each other
43 await doubleFollow(servers[0], servers[1])
44 })
45
46 describe('Live socket messages', function () {
47
48 async function createLiveWrapper () {
49 const liveAttributes = {
50 name: 'live video',
51 channelId: servers[0].store.channel.id,
52 privacy: VideoPrivacy.PUBLIC
53 }
54
55 const { uuid } = await servers[0].live.create({ fields: liveAttributes })
56 return uuid
57 }
58
59 it('Should correctly send a message when the live starts and ends', async function () {
60 this.timeout(60000)
61
62 const localStateChanges: VideoStateType[] = []
63 const remoteStateChanges: VideoStateType[] = []
64
65 const liveVideoUUID = await createLiveWrapper()
66 await waitJobs(servers)
67
68 {
69 const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
70
71 const localSocket = servers[0].socketIO.getLiveNotificationSocket()
72 localSocket.on('state-change', data => localStateChanges.push(data.state))
73 localSocket.emit('subscribe', { videoId })
74 }
75
76 {
77 const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID })
78
79 const remoteSocket = servers[1].socketIO.getLiveNotificationSocket()
80 remoteSocket.on('state-change', data => remoteStateChanges.push(data.state))
81 remoteSocket.emit('subscribe', { videoId })
82 }
83
84 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
85
86 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
87 await waitJobs(servers)
88
89 for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
90 expect(stateChanges).to.have.length.at.least(1)
91 expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.PUBLISHED)
92 }
93
94 await stopFfmpeg(ffmpegCommand)
95
96 for (const server of servers) {
97 await server.live.waitUntilEnded({ videoId: liveVideoUUID })
98 }
99 await waitJobs(servers)
100
101 for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
102 expect(stateChanges).to.have.length.at.least(2)
103 expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.LIVE_ENDED)
104 }
105 })
106
107 it('Should correctly send views change notification', async function () {
108 this.timeout(60000)
109
110 let localLastVideoViews = 0
111 let remoteLastVideoViews = 0
112
113 const liveVideoUUID = await createLiveWrapper()
114 await waitJobs(servers)
115
116 {
117 const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
118
119 const localSocket = servers[0].socketIO.getLiveNotificationSocket()
120 localSocket.on('views-change', (data: LiveVideoEventPayload) => { localLastVideoViews = data.viewers })
121 localSocket.emit('subscribe', { videoId })
122 }
123
124 {
125 const videoId = await servers[1].videos.getId({ uuid: liveVideoUUID })
126
127 const remoteSocket = servers[1].socketIO.getLiveNotificationSocket()
128 remoteSocket.on('views-change', (data: LiveVideoEventPayload) => { remoteLastVideoViews = data.viewers })
129 remoteSocket.emit('subscribe', { videoId })
130 }
131
132 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
133
134 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
135 await waitJobs(servers)
136
137 expect(localLastVideoViews).to.equal(0)
138 expect(remoteLastVideoViews).to.equal(0)
139
140 await servers[0].views.simulateView({ id: liveVideoUUID })
141 await servers[1].views.simulateView({ id: liveVideoUUID })
142
143 await waitJobs(servers)
144
145 expect(localLastVideoViews).to.equal(2)
146 expect(remoteLastVideoViews).to.equal(2)
147
148 await stopFfmpeg(ffmpegCommand)
149 })
150
151 it('Should not receive a notification after unsubscribe', async function () {
152 this.timeout(120000)
153
154 const stateChanges: VideoStateType[] = []
155
156 const liveVideoUUID = await createLiveWrapper()
157 await waitJobs(servers)
158
159 const videoId = await servers[0].videos.getId({ uuid: liveVideoUUID })
160
161 const socket = servers[0].socketIO.getLiveNotificationSocket()
162 socket.on('state-change', data => stateChanges.push(data.state))
163 socket.emit('subscribe', { videoId })
164
165 const command = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoUUID })
166
167 await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
168 await waitJobs(servers)
169
170 // Notifier waits before sending a notification
171 await wait(10000)
172
173 expect(stateChanges).to.have.lengthOf(1)
174 socket.emit('unsubscribe', { videoId })
175
176 await stopFfmpeg(command)
177 await waitJobs(servers)
178
179 expect(stateChanges).to.have.lengthOf(1)
180 })
181 })
182
183 after(async function () {
184 await cleanupTests(servers)
185 })
186})
diff --git a/packages/tests/src/api/live/live.ts b/packages/tests/src/api/live/live.ts
new file mode 100644
index 000000000..20804f889
--- /dev/null
+++ b/packages/tests/src/api/live/live.ts
@@ -0,0 +1,766 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { basename, join } from 'path'
5import { getAllFiles, wait } from '@peertube/peertube-core-utils'
6import { ffprobePromise, getVideoStream } from '@peertube/peertube-ffmpeg'
7import {
8 HttpStatusCode,
9 LiveVideo,
10 LiveVideoCreate,
11 LiveVideoLatencyMode,
12 VideoDetails,
13 VideoPrivacy,
14 VideoState,
15 VideoStreamingPlaylistType
16} from '@peertube/peertube-models'
17import {
18 cleanupTests,
19 createMultipleServers,
20 doubleFollow,
21 killallServers,
22 LiveCommand,
23 makeGetRequest,
24 makeRawRequest,
25 PeerTubeServer,
26 sendRTMPStream,
27 setAccessTokensToServers,
28 setDefaultVideoChannel,
29 stopFfmpeg,
30 testFfmpegStreamError,
31 waitJobs,
32 waitUntilLivePublishedOnAllServers
33} from '@peertube/peertube-server-commands'
34import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
35import { testLiveVideoResolutions } from '@tests/shared/live.js'
36import { SQLCommand } from '@tests/shared/sql-command.js'
37
38describe('Test live', function () {
39 let servers: PeerTubeServer[] = []
40 let commands: LiveCommand[]
41
42 before(async function () {
43 this.timeout(120000)
44
45 servers = await createMultipleServers(2)
46
47 // Get the access tokens
48 await setAccessTokensToServers(servers)
49 await setDefaultVideoChannel(servers)
50
51 await servers[0].config.updateCustomSubConfig({
52 newConfig: {
53 live: {
54 enabled: true,
55 allowReplay: true,
56 latencySetting: {
57 enabled: true
58 },
59 transcoding: {
60 enabled: false
61 }
62 }
63 }
64 })
65
66 // Server 1 and server 2 follow each other
67 await doubleFollow(servers[0], servers[1])
68
69 commands = servers.map(s => s.live)
70 })
71
72 describe('Live creation, update and delete', function () {
73 let liveVideoUUID: string
74
75 it('Should create a live with the appropriate parameters', async function () {
76 this.timeout(20000)
77
78 const attributes: LiveVideoCreate = {
79 category: 1,
80 licence: 2,
81 language: 'fr',
82 description: 'super live description',
83 support: 'support field',
84 channelId: servers[0].store.channel.id,
85 nsfw: false,
86 waitTranscoding: false,
87 name: 'my super live',
88 tags: [ 'tag1', 'tag2' ],
89 commentsEnabled: false,
90 downloadEnabled: false,
91 saveReplay: true,
92 replaySettings: { privacy: VideoPrivacy.PUBLIC },
93 latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
94 privacy: VideoPrivacy.PUBLIC,
95 previewfile: 'video_short1-preview.webm.jpg',
96 thumbnailfile: 'video_short1.webm.jpg'
97 }
98
99 const live = await commands[0].create({ fields: attributes })
100 liveVideoUUID = live.uuid
101
102 await waitJobs(servers)
103
104 for (const server of servers) {
105 const video = await server.videos.get({ id: liveVideoUUID })
106
107 expect(video.category.id).to.equal(1)
108 expect(video.licence.id).to.equal(2)
109 expect(video.language.id).to.equal('fr')
110 expect(video.description).to.equal('super live description')
111 expect(video.support).to.equal('support field')
112
113 expect(video.channel.name).to.equal(servers[0].store.channel.name)
114 expect(video.channel.host).to.equal(servers[0].store.channel.host)
115
116 expect(video.isLive).to.be.true
117
118 expect(video.nsfw).to.be.false
119 expect(video.waitTranscoding).to.be.false
120 expect(video.name).to.equal('my super live')
121 expect(video.tags).to.deep.equal([ 'tag1', 'tag2' ])
122 expect(video.commentsEnabled).to.be.false
123 expect(video.downloadEnabled).to.be.false
124 expect(video.privacy.id).to.equal(VideoPrivacy.PUBLIC)
125
126 await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath)
127 await testImageGeneratedByFFmpeg(server.url, 'video_short1.webm', video.thumbnailPath)
128
129 const live = await server.live.get({ videoId: liveVideoUUID })
130
131 if (server.url === servers[0].url) {
132 expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
133 expect(live.streamKey).to.not.be.empty
134
135 expect(live.replaySettings).to.exist
136 expect(live.replaySettings.privacy).to.equal(VideoPrivacy.PUBLIC)
137 } else {
138 expect(live.rtmpUrl).to.not.exist
139 expect(live.streamKey).to.not.exist
140 }
141
142 expect(live.saveReplay).to.be.true
143 expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY)
144 }
145 })
146
147 it('Should have a default preview and thumbnail', async function () {
148 this.timeout(20000)
149
150 const attributes: LiveVideoCreate = {
151 name: 'default live thumbnail',
152 channelId: servers[0].store.channel.id,
153 privacy: VideoPrivacy.UNLISTED,
154 nsfw: true
155 }
156
157 const live = await commands[0].create({ fields: attributes })
158 const videoId = live.uuid
159
160 await waitJobs(servers)
161
162 for (const server of servers) {
163 const video = await server.videos.get({ id: videoId })
164 expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
165 expect(video.nsfw).to.be.true
166
167 await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
168 await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
169 }
170 })
171
172 it('Should not have the live listed since nobody streams into', async function () {
173 for (const server of servers) {
174 const { total, data } = await server.videos.list()
175
176 expect(total).to.equal(0)
177 expect(data).to.have.lengthOf(0)
178 }
179 })
180
181 it('Should not be able to update a live of another server', async function () {
182 await commands[1].update({ videoId: liveVideoUUID, fields: { saveReplay: false }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
183 })
184
185 it('Should update the live', async function () {
186 await commands[0].update({ videoId: liveVideoUUID, fields: { saveReplay: false, latencyMode: LiveVideoLatencyMode.DEFAULT } })
187 await waitJobs(servers)
188 })
189
190 it('Have the live updated', async function () {
191 for (const server of servers) {
192 const live = await server.live.get({ videoId: liveVideoUUID })
193
194 if (server.url === servers[0].url) {
195 expect(live.rtmpUrl).to.equal('rtmp://' + server.hostname + ':' + servers[0].rtmpPort + '/live')
196 expect(live.streamKey).to.not.be.empty
197 } else {
198 expect(live.rtmpUrl).to.not.exist
199 expect(live.streamKey).to.not.exist
200 }
201
202 expect(live.saveReplay).to.be.false
203 expect(live.replaySettings).to.not.exist
204 expect(live.latencyMode).to.equal(LiveVideoLatencyMode.DEFAULT)
205 }
206 })
207
208 it('Delete the live', async function () {
209 await servers[0].videos.remove({ id: liveVideoUUID })
210 await waitJobs(servers)
211 })
212
213 it('Should have the live deleted', async function () {
214 for (const server of servers) {
215 await server.videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
216 await server.live.get({ videoId: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
217 }
218 })
219 })
220
221 describe('Live filters', function () {
222 let ffmpegCommand: any
223 let liveVideoId: string
224 let vodVideoId: string
225
226 before(async function () {
227 this.timeout(240000)
228
229 vodVideoId = (await servers[0].videos.quickUpload({ name: 'vod video' })).uuid
230
231 const liveOptions = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: servers[0].store.channel.id }
232 const live = await commands[0].create({ fields: liveOptions })
233 liveVideoId = live.uuid
234
235 ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId })
236 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
237 await waitJobs(servers)
238 })
239
240 it('Should only display lives', async function () {
241 const { data, total } = await servers[0].videos.list({ isLive: true })
242
243 expect(total).to.equal(1)
244 expect(data).to.have.lengthOf(1)
245 expect(data[0].name).to.equal('live')
246 })
247
248 it('Should not display lives', async function () {
249 const { data, total } = await servers[0].videos.list({ isLive: false })
250
251 expect(total).to.equal(1)
252 expect(data).to.have.lengthOf(1)
253 expect(data[0].name).to.equal('vod video')
254 })
255
256 it('Should display my lives', async function () {
257 this.timeout(60000)
258
259 await stopFfmpeg(ffmpegCommand)
260 await waitJobs(servers)
261
262 const { data } = await servers[0].videos.listMyVideos({ isLive: true })
263
264 const result = data.every(v => v.isLive)
265 expect(result).to.be.true
266 })
267
268 it('Should not display my lives', async function () {
269 const { data } = await servers[0].videos.listMyVideos({ isLive: false })
270
271 const result = data.every(v => !v.isLive)
272 expect(result).to.be.true
273 })
274
275 after(async function () {
276 await servers[0].videos.remove({ id: vodVideoId })
277 await servers[0].videos.remove({ id: liveVideoId })
278 })
279 })
280
281 describe('Stream checks', function () {
282 let liveVideo: LiveVideo & VideoDetails
283 let rtmpUrl: string
284
285 before(function () {
286 rtmpUrl = 'rtmp://' + servers[0].hostname + ':' + servers[0].rtmpPort + ''
287 })
288
289 async function createLiveWrapper () {
290 const liveAttributes = {
291 name: 'user live',
292 channelId: servers[0].store.channel.id,
293 privacy: VideoPrivacy.PUBLIC,
294 saveReplay: false
295 }
296
297 const { uuid } = await commands[0].create({ fields: liveAttributes })
298
299 const live = await commands[0].get({ videoId: uuid })
300 const video = await servers[0].videos.get({ id: uuid })
301
302 return Object.assign(video, live)
303 }
304
305 it('Should not allow a stream without the appropriate path', async function () {
306 this.timeout(60000)
307
308 liveVideo = await createLiveWrapper()
309
310 const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/bad-live', streamKey: liveVideo.streamKey })
311 await testFfmpegStreamError(command, true)
312 })
313
314 it('Should not allow a stream without the appropriate stream key', async function () {
315 this.timeout(60000)
316
317 const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: 'bad-stream-key' })
318 await testFfmpegStreamError(command, true)
319 })
320
321 it('Should succeed with the correct params', async function () {
322 this.timeout(60000)
323
324 const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
325 await testFfmpegStreamError(command, false)
326 })
327
328 it('Should list this live now someone stream into it', async function () {
329 for (const server of servers) {
330 const { total, data } = await server.videos.list()
331
332 expect(total).to.equal(1)
333 expect(data).to.have.lengthOf(1)
334
335 const video = data[0]
336 expect(video.name).to.equal('user live')
337 expect(video.isLive).to.be.true
338 }
339 })
340
341 it('Should not allow a stream on a live that was blacklisted', async function () {
342 this.timeout(60000)
343
344 liveVideo = await createLiveWrapper()
345
346 await servers[0].blacklist.add({ videoId: liveVideo.uuid })
347
348 const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
349 await testFfmpegStreamError(command, true)
350 })
351
352 it('Should not allow a stream on a live that was deleted', async function () {
353 this.timeout(60000)
354
355 liveVideo = await createLiveWrapper()
356
357 await servers[0].videos.remove({ id: liveVideo.uuid })
358
359 const command = sendRTMPStream({ rtmpBaseUrl: rtmpUrl + '/live', streamKey: liveVideo.streamKey })
360 await testFfmpegStreamError(command, true)
361 })
362 })
363
364 describe('Live transcoding', function () {
365 let liveVideoId: string
366 let sqlCommandServer1: SQLCommand
367
368 async function createLiveWrapper (saveReplay: boolean) {
369 const liveAttributes = {
370 name: 'live video',
371 channelId: servers[0].store.channel.id,
372 privacy: VideoPrivacy.PUBLIC,
373 saveReplay,
374 replaySettings: saveReplay
375 ? { privacy: VideoPrivacy.PUBLIC }
376 : undefined
377 }
378
379 const { uuid } = await commands[0].create({ fields: liveAttributes })
380 return uuid
381 }
382
383 function updateConf (resolutions: number[]) {
384 return servers[0].config.updateCustomSubConfig({
385 newConfig: {
386 live: {
387 enabled: true,
388 allowReplay: true,
389 maxDuration: -1,
390 transcoding: {
391 enabled: true,
392 resolutions: {
393 '144p': resolutions.includes(144),
394 '240p': resolutions.includes(240),
395 '360p': resolutions.includes(360),
396 '480p': resolutions.includes(480),
397 '720p': resolutions.includes(720),
398 '1080p': resolutions.includes(1080),
399 '2160p': resolutions.includes(2160)
400 }
401 }
402 }
403 }
404 })
405 }
406
407 before(async function () {
408 await updateConf([])
409
410 sqlCommandServer1 = new SQLCommand(servers[0])
411 })
412
413 it('Should enable transcoding without additional resolutions', async function () {
414 this.timeout(120000)
415
416 liveVideoId = await createLiveWrapper(false)
417
418 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId })
419 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
420 await waitJobs(servers)
421
422 await testLiveVideoResolutions({
423 originServer: servers[0],
424 sqlCommand: sqlCommandServer1,
425 servers,
426 liveVideoId,
427 resolutions: [ 720 ],
428 transcoded: true
429 })
430
431 await stopFfmpeg(ffmpegCommand)
432 })
433
434 it('Should transcode audio only RTMP stream', async function () {
435 this.timeout(120000)
436
437 liveVideoId = await createLiveWrapper(false)
438
439 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short_no_audio.mp4' })
440 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
441 await waitJobs(servers)
442
443 await stopFfmpeg(ffmpegCommand)
444 })
445
446 it('Should enable transcoding with some resolutions', async function () {
447 this.timeout(240000)
448
449 const resolutions = [ 240, 480 ]
450 await updateConf(resolutions)
451 liveVideoId = await createLiveWrapper(false)
452
453 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId })
454 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
455 await waitJobs(servers)
456
457 await testLiveVideoResolutions({
458 originServer: servers[0],
459 sqlCommand: sqlCommandServer1,
460 servers,
461 liveVideoId,
462 resolutions: resolutions.concat([ 720 ]),
463 transcoded: true
464 })
465
466 await stopFfmpeg(ffmpegCommand)
467 })
468
469 it('Should correctly set the appropriate bitrate depending on the input', async function () {
470 this.timeout(120000)
471
472 liveVideoId = await createLiveWrapper(false)
473
474 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({
475 videoId: liveVideoId,
476 fixtureName: 'video_short.mp4',
477 copyCodecs: true
478 })
479 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
480 await waitJobs(servers)
481
482 const video = await servers[0].videos.get({ id: liveVideoId })
483
484 const masterPlaylist = video.streamingPlaylists[0].playlistUrl
485 const probe = await ffprobePromise(masterPlaylist)
486
487 const bitrates = probe.streams.map(s => parseInt(s.tags.variant_bitrate))
488 for (const bitrate of bitrates) {
489 expect(bitrate).to.exist
490 expect(isNaN(bitrate)).to.be.false
491 expect(bitrate).to.be.below(61_000_000) // video_short.mp4 bitrate
492 }
493
494 await stopFfmpeg(ffmpegCommand)
495 })
496
497 it('Should enable transcoding with some resolutions and correctly save them', async function () {
498 this.timeout(500_000)
499
500 const resolutions = [ 240, 360, 720 ]
501
502 await updateConf(resolutions)
503 liveVideoId = await createLiveWrapper(true)
504
505 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
506 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
507 await waitJobs(servers)
508
509 await testLiveVideoResolutions({
510 originServer: servers[0],
511 sqlCommand: sqlCommandServer1,
512 servers,
513 liveVideoId,
514 resolutions,
515 transcoded: true
516 })
517
518 await stopFfmpeg(ffmpegCommand)
519 await commands[0].waitUntilEnded({ videoId: liveVideoId })
520
521 await waitJobs(servers)
522
523 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
524
525 const maxBitrateLimits = {
526 720: 6500 * 1000, // 60FPS
527 360: 1250 * 1000,
528 240: 700 * 1000
529 }
530
531 const minBitrateLimits = {
532 720: 4800 * 1000,
533 360: 1000 * 1000,
534 240: 550 * 1000
535 }
536
537 for (const server of servers) {
538 const video = await server.videos.get({ id: liveVideoId })
539
540 expect(video.state.id).to.equal(VideoState.PUBLISHED)
541 expect(video.duration).to.be.greaterThan(1)
542 expect(video.files).to.have.lengthOf(0)
543
544 const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
545 await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
546 await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
547
548 // We should have generated random filenames
549 expect(basename(hlsPlaylist.playlistUrl)).to.not.equal('master.m3u8')
550 expect(basename(hlsPlaylist.segmentsSha256Url)).to.not.equal('segments-sha256.json')
551
552 expect(hlsPlaylist.files).to.have.lengthOf(resolutions.length)
553
554 for (const resolution of resolutions) {
555 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
556
557 expect(file).to.exist
558 expect(file.size).to.be.greaterThan(1)
559
560 if (resolution >= 720) {
561 expect(file.fps).to.be.approximately(60, 10)
562 } else {
563 expect(file.fps).to.be.approximately(30, 3)
564 }
565
566 const filename = basename(file.fileUrl)
567 expect(filename).to.not.contain(video.uuid)
568
569 const segmentPath = servers[0].servers.buildDirectory(join('streaming-playlists', 'hls', video.uuid, filename))
570
571 const probe = await ffprobePromise(segmentPath)
572 const videoStream = await getVideoStream(segmentPath, probe)
573
574 expect(probe.format.bit_rate).to.be.below(maxBitrateLimits[videoStream.height])
575 expect(probe.format.bit_rate).to.be.at.least(minBitrateLimits[videoStream.height])
576
577 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
578 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
579 }
580 }
581 })
582
583 it('Should not generate an upper resolution than original file', async function () {
584 this.timeout(500_000)
585
586 const resolutions = [ 240, 480 ]
587 await updateConf(resolutions)
588
589 await servers[0].config.updateExistingSubConfig({
590 newConfig: {
591 live: {
592 transcoding: {
593 alwaysTranscodeOriginalResolution: false
594 }
595 }
596 }
597 })
598
599 liveVideoId = await createLiveWrapper(true)
600
601 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
602 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
603 await waitJobs(servers)
604
605 await testLiveVideoResolutions({
606 originServer: servers[0],
607 sqlCommand: sqlCommandServer1,
608 servers,
609 liveVideoId,
610 resolutions,
611 transcoded: true
612 })
613
614 await stopFfmpeg(ffmpegCommand)
615 await commands[0].waitUntilEnded({ videoId: liveVideoId })
616
617 await waitJobs(servers)
618
619 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
620
621 const video = await servers[0].videos.get({ id: liveVideoId })
622 const hlsFiles = video.streamingPlaylists[0].files
623
624 expect(video.files).to.have.lengthOf(0)
625 expect(hlsFiles).to.have.lengthOf(resolutions.length)
626
627 // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
628 expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions)
629 })
630
631 it('Should only keep the original resolution if all resolutions are disabled', async function () {
632 this.timeout(600_000)
633
634 await updateConf([])
635 liveVideoId = await createLiveWrapper(true)
636
637 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
638 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
639 await waitJobs(servers)
640
641 await testLiveVideoResolutions({
642 originServer: servers[0],
643 sqlCommand: sqlCommandServer1,
644 servers,
645 liveVideoId,
646 resolutions: [ 720 ],
647 transcoded: true
648 })
649
650 await stopFfmpeg(ffmpegCommand)
651 await commands[0].waitUntilEnded({ videoId: liveVideoId })
652
653 await waitJobs(servers)
654
655 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
656
657 const video = await servers[0].videos.get({ id: liveVideoId })
658 const hlsFiles = video.streamingPlaylists[0].files
659
660 expect(video.files).to.have.lengthOf(0)
661 expect(hlsFiles).to.have.lengthOf(1)
662
663 expect(hlsFiles[0].resolution.id).to.equal(720)
664 })
665
666 after(async function () {
667 await sqlCommandServer1.cleanup()
668 })
669 })
670
671 describe('After a server restart', function () {
672 let liveVideoId: string
673 let liveVideoReplayId: string
674 let permanentLiveVideoReplayId: string
675
676 let permanentLiveReplayName: string
677
678 let beforeServerRestart: Date
679
680 async function createLiveWrapper (options: { saveReplay: boolean, permanent: boolean }) {
681 const liveAttributes: LiveVideoCreate = {
682 name: 'live video',
683 channelId: servers[0].store.channel.id,
684 privacy: VideoPrivacy.PUBLIC,
685 saveReplay: options.saveReplay,
686 replaySettings: options.saveReplay
687 ? { privacy: VideoPrivacy.PUBLIC }
688 : undefined,
689 permanentLive: options.permanent
690 }
691
692 const { uuid } = await commands[0].create({ fields: liveAttributes })
693 return uuid
694 }
695
696 before(async function () {
697 this.timeout(600_000)
698
699 liveVideoId = await createLiveWrapper({ saveReplay: false, permanent: false })
700 liveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: false })
701 permanentLiveVideoReplayId = await createLiveWrapper({ saveReplay: true, permanent: true })
702
703 await Promise.all([
704 commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId }),
705 commands[0].sendRTMPStreamInVideo({ videoId: permanentLiveVideoReplayId }),
706 commands[0].sendRTMPStreamInVideo({ videoId: liveVideoReplayId })
707 ])
708
709 await Promise.all([
710 commands[0].waitUntilPublished({ videoId: liveVideoId }),
711 commands[0].waitUntilPublished({ videoId: permanentLiveVideoReplayId }),
712 commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
713 ])
714
715 for (const videoUUID of [ liveVideoId, liveVideoReplayId, permanentLiveVideoReplayId ]) {
716 await commands[0].waitUntilSegmentGeneration({
717 server: servers[0],
718 videoUUID,
719 playlistNumber: 0,
720 segment: 2
721 })
722 }
723
724 {
725 const video = await servers[0].videos.get({ id: permanentLiveVideoReplayId })
726 permanentLiveReplayName = video.name + ' - ' + new Date(video.publishedAt).toLocaleString()
727 }
728
729 await killallServers([ servers[0] ])
730
731 beforeServerRestart = new Date()
732 await servers[0].run()
733
734 await wait(5000)
735 await waitJobs(servers)
736 })
737
738 it('Should cleanup lives', async function () {
739 this.timeout(60000)
740
741 await commands[0].waitUntilEnded({ videoId: liveVideoId })
742 await commands[0].waitUntilWaiting({ videoId: permanentLiveVideoReplayId })
743 })
744
745 it('Should save a non permanent live replay', async function () {
746 this.timeout(240000)
747
748 await commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
749
750 const session = await commands[0].getReplaySession({ videoId: liveVideoReplayId })
751 expect(session.endDate).to.exist
752 expect(new Date(session.endDate)).to.be.above(beforeServerRestart)
753 })
754
755 it('Should have saved a permanent live replay', async function () {
756 this.timeout(120000)
757
758 const { data } = await servers[0].videos.listMyVideos({ sort: '-publishedAt' })
759 expect(data.find(v => v.name === permanentLiveReplayName)).to.exist
760 })
761 })
762
763 after(async function () {
764 await cleanupTests(servers)
765 })
766})
diff --git a/packages/tests/src/api/moderation/abuses.ts b/packages/tests/src/api/moderation/abuses.ts
new file mode 100644
index 000000000..649de224e
--- /dev/null
+++ b/packages/tests/src/api/moderation/abuses.ts
@@ -0,0 +1,887 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@peertube/peertube-models'
5import {
6 AbusesCommand,
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test abuses', function () {
18 let servers: PeerTubeServer[] = []
19 let abuseServer1: AdminAbuse
20 let abuseServer2: AdminAbuse
21 let commands: AbusesCommand[]
22
23 before(async function () {
24 this.timeout(50000)
25
26 // Run servers
27 servers = await createMultipleServers(2)
28
29 await setAccessTokensToServers(servers)
30 await setDefaultChannelAvatar(servers)
31 await setDefaultAccountAvatar(servers)
32
33 // Server 1 and server 2 follow each other
34 await doubleFollow(servers[0], servers[1])
35
36 commands = servers.map(s => s.abuses)
37 })
38
39 describe('Video abuses', function () {
40
41 before(async function () {
42 this.timeout(50000)
43
44 // Upload some videos on each servers
45 {
46 const attributes = {
47 name: 'my super name for server 1',
48 description: 'my super description for server 1'
49 }
50 await servers[0].videos.upload({ attributes })
51 }
52
53 {
54 const attributes = {
55 name: 'my super name for server 2',
56 description: 'my super description for server 2'
57 }
58 await servers[1].videos.upload({ attributes })
59 }
60
61 // Wait videos propagation, server 2 has transcoding enabled
62 await waitJobs(servers)
63
64 const { data } = await servers[0].videos.list()
65 expect(data.length).to.equal(2)
66
67 servers[0].store.videoCreated = data.find(video => video.name === 'my super name for server 1')
68 servers[1].store.videoCreated = data.find(video => video.name === 'my super name for server 2')
69 })
70
71 it('Should not have abuses', async function () {
72 const body = await commands[0].getAdminList()
73
74 expect(body.total).to.equal(0)
75 expect(body.data).to.be.an('array')
76 expect(body.data.length).to.equal(0)
77 })
78
79 it('Should report abuse on a local video', async function () {
80 this.timeout(15000)
81
82 const reason = 'my super bad reason'
83 await commands[0].report({ videoId: servers[0].store.videoCreated.id, reason })
84
85 // We wait requests propagation, even if the server 1 is not supposed to make a request to server 2
86 await waitJobs(servers)
87 })
88
89 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
90 {
91 const body = await commands[0].getAdminList()
92
93 expect(body.total).to.equal(1)
94 expect(body.data).to.be.an('array')
95 expect(body.data.length).to.equal(1)
96
97 const abuse = body.data[0]
98 expect(abuse.reason).to.equal('my super bad reason')
99
100 expect(abuse.reporterAccount.name).to.equal('root')
101 expect(abuse.reporterAccount.host).to.equal(servers[0].host)
102
103 expect(abuse.video.id).to.equal(servers[0].store.videoCreated.id)
104 expect(abuse.video.channel).to.exist
105
106 expect(abuse.comment).to.be.null
107
108 expect(abuse.flaggedAccount.name).to.equal('root')
109 expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
110
111 expect(abuse.video.countReports).to.equal(1)
112 expect(abuse.video.nthReport).to.equal(1)
113
114 expect(abuse.countReportsForReporter).to.equal(1)
115 expect(abuse.countReportsForReportee).to.equal(1)
116 }
117
118 {
119 const body = await commands[1].getAdminList()
120 expect(body.total).to.equal(0)
121 expect(body.data).to.be.an('array')
122 expect(body.data.length).to.equal(0)
123 }
124 })
125
126 it('Should report abuse on a remote video', async function () {
127 const reason = 'my super bad reason 2'
128 const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid })
129 await commands[0].report({ videoId, reason })
130
131 // We wait requests propagation
132 await waitJobs(servers)
133 })
134
135 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
136 {
137 const body = await commands[0].getAdminList()
138
139 expect(body.total).to.equal(2)
140 expect(body.data.length).to.equal(2)
141
142 const abuse1 = body.data[0]
143 expect(abuse1.reason).to.equal('my super bad reason')
144 expect(abuse1.reporterAccount.name).to.equal('root')
145 expect(abuse1.reporterAccount.host).to.equal(servers[0].host)
146
147 expect(abuse1.video.id).to.equal(servers[0].store.videoCreated.id)
148 expect(abuse1.video.countReports).to.equal(1)
149 expect(abuse1.video.nthReport).to.equal(1)
150
151 expect(abuse1.comment).to.be.null
152
153 expect(abuse1.flaggedAccount.name).to.equal('root')
154 expect(abuse1.flaggedAccount.host).to.equal(servers[0].host)
155
156 expect(abuse1.state.id).to.equal(AbuseState.PENDING)
157 expect(abuse1.state.label).to.equal('Pending')
158 expect(abuse1.moderationComment).to.be.null
159
160 const abuse2 = body.data[1]
161 expect(abuse2.reason).to.equal('my super bad reason 2')
162
163 expect(abuse2.reporterAccount.name).to.equal('root')
164 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
165
166 expect(abuse2.video.uuid).to.equal(servers[1].store.videoCreated.uuid)
167
168 expect(abuse2.comment).to.be.null
169
170 expect(abuse2.flaggedAccount.name).to.equal('root')
171 expect(abuse2.flaggedAccount.host).to.equal(servers[1].host)
172
173 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
174 expect(abuse2.state.label).to.equal('Pending')
175 expect(abuse2.moderationComment).to.be.null
176 }
177
178 {
179 const body = await commands[1].getAdminList()
180 expect(body.total).to.equal(1)
181 expect(body.data.length).to.equal(1)
182
183 abuseServer2 = body.data[0]
184 expect(abuseServer2.reason).to.equal('my super bad reason 2')
185 expect(abuseServer2.reporterAccount.name).to.equal('root')
186 expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
187
188 expect(abuseServer2.flaggedAccount.name).to.equal('root')
189 expect(abuseServer2.flaggedAccount.host).to.equal(servers[1].host)
190
191 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
192 expect(abuseServer2.state.label).to.equal('Pending')
193 expect(abuseServer2.moderationComment).to.be.null
194 }
195 })
196
197 it('Should hide video abuses from blocked accounts', async function () {
198 {
199 const videoId = await servers[1].videos.getId({ uuid: servers[0].store.videoCreated.uuid })
200 await commands[1].report({ videoId, reason: 'will mute this' })
201 await waitJobs(servers)
202
203 const body = await commands[0].getAdminList()
204 expect(body.total).to.equal(3)
205 }
206
207 const accountToBlock = 'root@' + servers[1].host
208
209 {
210 await servers[0].blocklist.addToServerBlocklist({ account: accountToBlock })
211
212 const body = await commands[0].getAdminList()
213 expect(body.total).to.equal(2)
214
215 const abuse = body.data.find(a => a.reason === 'will mute this')
216 expect(abuse).to.be.undefined
217 }
218
219 {
220 await servers[0].blocklist.removeFromServerBlocklist({ account: accountToBlock })
221
222 const body = await commands[0].getAdminList()
223 expect(body.total).to.equal(3)
224 }
225 })
226
227 it('Should hide video abuses from blocked servers', async function () {
228 const serverToBlock = servers[1].host
229
230 {
231 await servers[0].blocklist.addToServerBlocklist({ server: serverToBlock })
232
233 const body = await commands[0].getAdminList()
234 expect(body.total).to.equal(2)
235
236 const abuse = body.data.find(a => a.reason === 'will mute this')
237 expect(abuse).to.be.undefined
238 }
239
240 {
241 await servers[0].blocklist.removeFromServerBlocklist({ server: serverToBlock })
242
243 const body = await commands[0].getAdminList()
244 expect(body.total).to.equal(3)
245 }
246 })
247
248 it('Should keep the video abuse when deleting the video', async function () {
249 await servers[1].videos.remove({ id: abuseServer2.video.uuid })
250
251 await waitJobs(servers)
252
253 const body = await commands[1].getAdminList()
254 expect(body.total).to.equal(2, 'wrong number of videos returned')
255 expect(body.data).to.have.lengthOf(2, 'wrong number of videos returned')
256
257 const abuse = body.data[0]
258 expect(abuse.id).to.equal(abuseServer2.id, 'wrong origin server id for first video')
259 expect(abuse.video.id).to.equal(abuseServer2.video.id, 'wrong video id')
260 expect(abuse.video.channel).to.exist
261 expect(abuse.video.deleted).to.be.true
262 })
263
264 it('Should include counts of reports from reporter and reportee', async function () {
265 // register a second user to have two reporters/reportees
266 const user = { username: 'user2', password: 'password' }
267 await servers[0].users.create({ ...user })
268 const userAccessToken = await servers[0].login.getAccessToken(user)
269
270 // upload a third video via this user
271 const attributes = {
272 name: 'my second super name for server 1',
273 description: 'my second super description for server 1'
274 }
275 const { id } = await servers[0].videos.upload({ token: userAccessToken, attributes })
276 const video3Id = id
277
278 // resume with the test
279 const reason3 = 'my super bad reason 3'
280 await commands[0].report({ videoId: video3Id, reason: reason3 })
281
282 const reason4 = 'my super bad reason 4'
283 await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: reason4 })
284
285 {
286 const body = await commands[0].getAdminList()
287 const abuses = body.data
288
289 const abuseVideo3 = body.data.find(a => a.video.id === video3Id)
290 expect(abuseVideo3).to.not.be.undefined
291 expect(abuseVideo3.video.countReports).to.equal(1, 'wrong reports count for video 3')
292 expect(abuseVideo3.video.nthReport).to.equal(1, 'wrong report position in report list for video 3')
293 expect(abuseVideo3.countReportsForReportee).to.equal(1, 'wrong reports count for reporter on video 3 abuse')
294 expect(abuseVideo3.countReportsForReporter).to.equal(3, 'wrong reports count for reportee on video 3 abuse')
295
296 const abuseServer1 = abuses.find(a => a.video.id === servers[0].store.videoCreated.id)
297 expect(abuseServer1.countReportsForReportee).to.equal(3, 'wrong reports count for reporter on video 1 abuse')
298 }
299 })
300
301 it('Should list predefined reasons as well as timestamps for the reported video', async function () {
302 const reason5 = 'my super bad reason 5'
303 const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
304 const createRes = await commands[0].report({
305 videoId: servers[0].store.videoCreated.id,
306 reason: reason5,
307 predefinedReasons: predefinedReasons5,
308 startAt: 1,
309 endAt: 5
310 })
311
312 const body = await commands[0].getAdminList()
313
314 {
315 const abuse = body.data.find(a => a.id === createRes.abuse.id)
316 expect(abuse.reason).to.equals(reason5)
317 expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, 'predefined reasons do not match the one reported')
318 expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
319 expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
320 }
321 })
322
323 it('Should delete the video abuse', async function () {
324 await commands[1].delete({ abuseId: abuseServer2.id })
325
326 await waitJobs(servers)
327
328 {
329 const body = await commands[1].getAdminList()
330 expect(body.total).to.equal(1)
331 expect(body.data.length).to.equal(1)
332 expect(body.data[0].id).to.not.equal(abuseServer2.id)
333 }
334
335 {
336 const body = await commands[0].getAdminList()
337 expect(body.total).to.equal(6)
338 }
339 })
340
341 it('Should list and filter video abuses', async function () {
342 async function list (query: Parameters<AbusesCommand['getAdminList']>[0]) {
343 const body = await commands[0].getAdminList(query)
344
345 return body.data
346 }
347
348 expect(await list({ id: 56 })).to.have.lengthOf(0)
349 expect(await list({ id: 1 })).to.have.lengthOf(1)
350
351 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
352 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
353
354 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
355
356 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
357 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
358
359 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
360 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
361
362 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
363 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
364
365 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
366 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
367
368 expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
369 expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
370
371 expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
372 expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
373 })
374 })
375
376 describe('Comment abuses', function () {
377
378 async function getComment (server: PeerTubeServer, videoIdArg: number | string) {
379 const videoId = typeof videoIdArg === 'string'
380 ? await server.videos.getId({ uuid: videoIdArg })
381 : videoIdArg
382
383 const { data } = await server.comments.listThreads({ videoId })
384
385 return data[0]
386 }
387
388 before(async function () {
389 this.timeout(50000)
390
391 servers[0].store.videoCreated = await servers[0].videos.quickUpload({ name: 'server 1' })
392 servers[1].store.videoCreated = await servers[1].videos.quickUpload({ name: 'server 2' })
393
394 await servers[0].comments.createThread({ videoId: servers[0].store.videoCreated.id, text: 'comment server 1' })
395 await servers[1].comments.createThread({ videoId: servers[1].store.videoCreated.id, text: 'comment server 2' })
396
397 await waitJobs(servers)
398 })
399
400 it('Should report abuse on a comment', async function () {
401 this.timeout(15000)
402
403 const comment = await getComment(servers[0], servers[0].store.videoCreated.id)
404
405 const reason = 'it is a bad comment'
406 await commands[0].report({ commentId: comment.id, reason })
407
408 await waitJobs(servers)
409 })
410
411 it('Should have 1 comment abuse on server 1 and 0 on server 2', async function () {
412 {
413 const comment = await getComment(servers[0], servers[0].store.videoCreated.id)
414 const body = await commands[0].getAdminList({ filter: 'comment' })
415
416 expect(body.total).to.equal(1)
417 expect(body.data).to.have.lengthOf(1)
418
419 const abuse = body.data[0]
420 expect(abuse.reason).to.equal('it is a bad comment')
421
422 expect(abuse.reporterAccount.name).to.equal('root')
423 expect(abuse.reporterAccount.host).to.equal(servers[0].host)
424
425 expect(abuse.video).to.be.null
426
427 expect(abuse.comment.deleted).to.be.false
428 expect(abuse.comment.id).to.equal(comment.id)
429 expect(abuse.comment.text).to.equal(comment.text)
430 expect(abuse.comment.video.name).to.equal('server 1')
431 expect(abuse.comment.video.id).to.equal(servers[0].store.videoCreated.id)
432 expect(abuse.comment.video.uuid).to.equal(servers[0].store.videoCreated.uuid)
433
434 expect(abuse.countReportsForReporter).to.equal(5)
435 expect(abuse.countReportsForReportee).to.equal(5)
436 }
437
438 {
439 const body = await commands[1].getAdminList({ filter: 'comment' })
440 expect(body.total).to.equal(0)
441 expect(body.data.length).to.equal(0)
442 }
443 })
444
445 it('Should report abuse on a remote comment', async function () {
446 const comment = await getComment(servers[0], servers[1].store.videoCreated.uuid)
447
448 const reason = 'it is a really bad comment'
449 await commands[0].report({ commentId: comment.id, reason })
450
451 await waitJobs(servers)
452 })
453
454 it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
455 const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.shortUUID)
456
457 {
458 const body = await commands[0].getAdminList({ filter: 'comment' })
459 expect(body.total).to.equal(2)
460 expect(body.data.length).to.equal(2)
461
462 const abuse = body.data[0]
463 expect(abuse.reason).to.equal('it is a bad comment')
464 expect(abuse.countReportsForReporter).to.equal(6)
465 expect(abuse.countReportsForReportee).to.equal(5)
466
467 const abuse2 = body.data[1]
468
469 expect(abuse2.reason).to.equal('it is a really bad comment')
470
471 expect(abuse2.reporterAccount.name).to.equal('root')
472 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
473
474 expect(abuse2.video).to.be.null
475
476 expect(abuse2.comment.deleted).to.be.false
477 expect(abuse2.comment.id).to.equal(commentServer2.id)
478 expect(abuse2.comment.text).to.equal(commentServer2.text)
479 expect(abuse2.comment.video.name).to.equal('server 2')
480 expect(abuse2.comment.video.uuid).to.equal(servers[1].store.videoCreated.uuid)
481
482 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
483 expect(abuse2.state.label).to.equal('Pending')
484
485 expect(abuse2.moderationComment).to.be.null
486
487 expect(abuse2.countReportsForReporter).to.equal(6)
488 expect(abuse2.countReportsForReportee).to.equal(2)
489 }
490
491 {
492 const body = await commands[1].getAdminList({ filter: 'comment' })
493 expect(body.total).to.equal(1)
494 expect(body.data.length).to.equal(1)
495
496 abuseServer2 = body.data[0]
497 expect(abuseServer2.reason).to.equal('it is a really bad comment')
498 expect(abuseServer2.reporterAccount.name).to.equal('root')
499 expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
500
501 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
502 expect(abuseServer2.state.label).to.equal('Pending')
503
504 expect(abuseServer2.moderationComment).to.be.null
505
506 expect(abuseServer2.countReportsForReporter).to.equal(1)
507 expect(abuseServer2.countReportsForReportee).to.equal(1)
508 }
509 })
510
511 it('Should keep the comment abuse when deleting the comment', async function () {
512 const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.uuid)
513
514 await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id })
515
516 await waitJobs(servers)
517
518 const body = await commands[0].getAdminList({ filter: 'comment' })
519 expect(body.total).to.equal(2)
520 expect(body.data).to.have.lengthOf(2)
521
522 const abuse = body.data.find(a => a.comment?.id === commentServer2.id)
523 expect(abuse).to.not.be.undefined
524
525 expect(abuse.comment.text).to.be.empty
526 expect(abuse.comment.video.name).to.equal('server 2')
527 expect(abuse.comment.deleted).to.be.true
528 })
529
530 it('Should delete the comment abuse', async function () {
531 await commands[1].delete({ abuseId: abuseServer2.id })
532
533 await waitJobs(servers)
534
535 {
536 const body = await commands[1].getAdminList({ filter: 'comment' })
537 expect(body.total).to.equal(0)
538 expect(body.data.length).to.equal(0)
539 }
540
541 {
542 const body = await commands[0].getAdminList({ filter: 'comment' })
543 expect(body.total).to.equal(2)
544 }
545 })
546
547 it('Should list and filter video abuses', async function () {
548 {
549 const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'foo' })
550 expect(body.total).to.equal(0)
551 }
552
553 {
554 const body = await commands[0].getAdminList({ filter: 'comment', searchReportee: 'ot' })
555 expect(body.total).to.equal(2)
556 }
557
558 {
559 const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: 'createdAt' })
560 expect(body.data).to.have.lengthOf(1)
561 expect(body.data[0].comment.text).to.be.empty
562 }
563
564 {
565 const body = await commands[0].getAdminList({ filter: 'comment', start: 1, count: 1, sort: '-createdAt' })
566 expect(body.data).to.have.lengthOf(1)
567 expect(body.data[0].comment.text).to.equal('comment server 1')
568 }
569 })
570 })
571
572 describe('Account abuses', function () {
573
574 function getAccountFromServer (server: PeerTubeServer, targetName: string, targetServer: PeerTubeServer) {
575 return server.accounts.get({ accountName: targetName + '@' + targetServer.host })
576 }
577
578 before(async function () {
579 this.timeout(50000)
580
581 await servers[0].users.create({ username: 'user_1', password: 'donald' })
582
583 const token = await servers[1].users.generateUserAndToken('user_2')
584 await servers[1].videos.upload({ token, attributes: { name: 'super video' } })
585
586 await waitJobs(servers)
587 })
588
589 it('Should report abuse on an account', async function () {
590 this.timeout(15000)
591
592 const account = await getAccountFromServer(servers[0], 'user_1', servers[0])
593
594 const reason = 'it is a bad account'
595 await commands[0].report({ accountId: account.id, reason })
596
597 await waitJobs(servers)
598 })
599
600 it('Should have 1 account abuse on server 1 and 0 on server 2', async function () {
601 {
602 const body = await commands[0].getAdminList({ filter: 'account' })
603
604 expect(body.total).to.equal(1)
605 expect(body.data).to.have.lengthOf(1)
606
607 const abuse = body.data[0]
608 expect(abuse.reason).to.equal('it is a bad account')
609
610 expect(abuse.reporterAccount.name).to.equal('root')
611 expect(abuse.reporterAccount.host).to.equal(servers[0].host)
612
613 expect(abuse.video).to.be.null
614 expect(abuse.comment).to.be.null
615
616 expect(abuse.flaggedAccount.name).to.equal('user_1')
617 expect(abuse.flaggedAccount.host).to.equal(servers[0].host)
618 }
619
620 {
621 const body = await commands[1].getAdminList({ filter: 'comment' })
622 expect(body.total).to.equal(0)
623 expect(body.data.length).to.equal(0)
624 }
625 })
626
627 it('Should report abuse on a remote account', async function () {
628 const account = await getAccountFromServer(servers[0], 'user_2', servers[1])
629
630 const reason = 'it is a really bad account'
631 await commands[0].report({ accountId: account.id, reason })
632
633 await waitJobs(servers)
634 })
635
636 it('Should have 2 comment abuses on server 1 and 1 on server 2', async function () {
637 {
638 const body = await commands[0].getAdminList({ filter: 'account' })
639 expect(body.total).to.equal(2)
640 expect(body.data.length).to.equal(2)
641
642 const abuse: AdminAbuse = body.data[0]
643 expect(abuse.reason).to.equal('it is a bad account')
644
645 const abuse2: AdminAbuse = body.data[1]
646 expect(abuse2.reason).to.equal('it is a really bad account')
647
648 expect(abuse2.reporterAccount.name).to.equal('root')
649 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
650
651 expect(abuse2.video).to.be.null
652 expect(abuse2.comment).to.be.null
653
654 expect(abuse2.state.id).to.equal(AbuseState.PENDING)
655 expect(abuse2.state.label).to.equal('Pending')
656
657 expect(abuse2.moderationComment).to.be.null
658 }
659
660 {
661 const body = await commands[1].getAdminList({ filter: 'account' })
662 expect(body.total).to.equal(1)
663 expect(body.data.length).to.equal(1)
664
665 abuseServer2 = body.data[0]
666
667 expect(abuseServer2.reason).to.equal('it is a really bad account')
668
669 expect(abuseServer2.reporterAccount.name).to.equal('root')
670 expect(abuseServer2.reporterAccount.host).to.equal(servers[0].host)
671
672 expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
673 expect(abuseServer2.state.label).to.equal('Pending')
674
675 expect(abuseServer2.moderationComment).to.be.null
676 }
677 })
678
679 it('Should keep the account abuse when deleting the account', async function () {
680 const account = await getAccountFromServer(servers[1], 'user_2', servers[1])
681 await servers[1].users.remove({ userId: account.userId })
682
683 await waitJobs(servers)
684
685 const body = await commands[0].getAdminList({ filter: 'account' })
686 expect(body.total).to.equal(2)
687 expect(body.data).to.have.lengthOf(2)
688
689 const abuse = body.data.find(a => a.reason === 'it is a really bad account')
690 expect(abuse).to.not.be.undefined
691 })
692
693 it('Should delete the account abuse', async function () {
694 await commands[1].delete({ abuseId: abuseServer2.id })
695
696 await waitJobs(servers)
697
698 {
699 const body = await commands[1].getAdminList({ filter: 'account' })
700 expect(body.total).to.equal(0)
701 expect(body.data.length).to.equal(0)
702 }
703
704 {
705 const body = await commands[0].getAdminList({ filter: 'account' })
706 expect(body.total).to.equal(2)
707
708 abuseServer1 = body.data[0]
709 }
710 })
711 })
712
713 describe('Common actions on abuses', function () {
714
715 it('Should update the state of an abuse', async function () {
716 await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.REJECTED } })
717
718 const body = await commands[0].getAdminList({ id: abuseServer1.id })
719 expect(body.data[0].state.id).to.equal(AbuseState.REJECTED)
720 })
721
722 it('Should add a moderation comment', async function () {
723 await commands[0].update({ abuseId: abuseServer1.id, body: { state: AbuseState.ACCEPTED, moderationComment: 'Valid' } })
724
725 const body = await commands[0].getAdminList({ id: abuseServer1.id })
726 expect(body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
727 expect(body.data[0].moderationComment).to.equal('Valid')
728 })
729 })
730
731 describe('My abuses', async function () {
732 let abuseId1: number
733 let userAccessToken: string
734
735 before(async function () {
736 userAccessToken = await servers[0].users.generateUserAndToken('user_42')
737
738 await commands[0].report({ token: userAccessToken, videoId: servers[0].store.videoCreated.id, reason: 'user reason 1' })
739
740 const videoId = await servers[0].videos.getId({ uuid: servers[1].store.videoCreated.uuid })
741 await commands[0].report({ token: userAccessToken, videoId, reason: 'user reason 2' })
742 })
743
744 it('Should correctly list my abuses', async function () {
745 {
746 const body = await commands[0].getUserList({ token: userAccessToken, start: 0, count: 5, sort: 'createdAt' })
747 expect(body.total).to.equal(2)
748
749 const abuses = body.data
750 expect(abuses[0].reason).to.equal('user reason 1')
751 expect(abuses[1].reason).to.equal('user reason 2')
752
753 abuseId1 = abuses[0].id
754 }
755
756 {
757 const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: 'createdAt' })
758 expect(body.total).to.equal(2)
759
760 const abuses: UserAbuse[] = body.data
761 expect(abuses[0].reason).to.equal('user reason 2')
762 }
763
764 {
765 const body = await commands[0].getUserList({ token: userAccessToken, start: 1, count: 1, sort: '-createdAt' })
766 expect(body.total).to.equal(2)
767
768 const abuses: UserAbuse[] = body.data
769 expect(abuses[0].reason).to.equal('user reason 1')
770 }
771 })
772
773 it('Should correctly filter my abuses by id', async function () {
774 const body = await commands[0].getUserList({ token: userAccessToken, id: abuseId1 })
775 expect(body.total).to.equal(1)
776
777 const abuses: UserAbuse[] = body.data
778 expect(abuses[0].reason).to.equal('user reason 1')
779 })
780
781 it('Should correctly filter my abuses by search', async function () {
782 const body = await commands[0].getUserList({ token: userAccessToken, search: 'server 2' })
783 expect(body.total).to.equal(1)
784
785 const abuses: UserAbuse[] = body.data
786 expect(abuses[0].reason).to.equal('user reason 2')
787 })
788
789 it('Should correctly filter my abuses by state', async function () {
790 await commands[0].update({ abuseId: abuseId1, body: { state: AbuseState.REJECTED } })
791
792 const body = await commands[0].getUserList({ token: userAccessToken, state: AbuseState.REJECTED })
793 expect(body.total).to.equal(1)
794
795 const abuses: UserAbuse[] = body.data
796 expect(abuses[0].reason).to.equal('user reason 1')
797 })
798 })
799
800 describe('Abuse messages', async function () {
801 let abuseId: number
802 let userToken: string
803 let abuseMessageUserId: number
804 let abuseMessageModerationId: number
805
806 before(async function () {
807 userToken = await servers[0].users.generateUserAndToken('user_43')
808
809 const body = await commands[0].report({ token: userToken, videoId: servers[0].store.videoCreated.id, reason: 'user 43 reason 1' })
810 abuseId = body.abuse.id
811 })
812
813 it('Should create some messages on the abuse', async function () {
814 await commands[0].addMessage({ token: userToken, abuseId, message: 'message 1' })
815 await commands[0].addMessage({ abuseId, message: 'message 2' })
816 await commands[0].addMessage({ abuseId, message: 'message 3' })
817 await commands[0].addMessage({ token: userToken, abuseId, message: 'message 4' })
818 })
819
820 it('Should have the correct messages count when listing abuses', async function () {
821 const results = await Promise.all([
822 commands[0].getAdminList({ start: 0, count: 50 }),
823 commands[0].getUserList({ token: userToken, start: 0, count: 50 })
824 ])
825
826 for (const body of results) {
827 const abuses = body.data
828 const abuse = abuses.find(a => a.id === abuseId)
829 expect(abuse.countMessages).to.equal(4)
830 }
831 })
832
833 it('Should correctly list messages of this abuse', async function () {
834 const results = await Promise.all([
835 commands[0].listMessages({ abuseId }),
836 commands[0].listMessages({ token: userToken, abuseId })
837 ])
838
839 for (const body of results) {
840 expect(body.total).to.equal(4)
841
842 const abuseMessages: AbuseMessage[] = body.data
843
844 expect(abuseMessages[0].message).to.equal('message 1')
845 expect(abuseMessages[0].byModerator).to.be.false
846 expect(abuseMessages[0].account.name).to.equal('user_43')
847
848 abuseMessageUserId = abuseMessages[0].id
849
850 expect(abuseMessages[1].message).to.equal('message 2')
851 expect(abuseMessages[1].byModerator).to.be.true
852 expect(abuseMessages[1].account.name).to.equal('root')
853
854 expect(abuseMessages[2].message).to.equal('message 3')
855 expect(abuseMessages[2].byModerator).to.be.true
856 expect(abuseMessages[2].account.name).to.equal('root')
857 abuseMessageModerationId = abuseMessages[2].id
858
859 expect(abuseMessages[3].message).to.equal('message 4')
860 expect(abuseMessages[3].byModerator).to.be.false
861 expect(abuseMessages[3].account.name).to.equal('user_43')
862 }
863 })
864
865 it('Should delete messages', async function () {
866 await commands[0].deleteMessage({ abuseId, messageId: abuseMessageModerationId })
867 await commands[0].deleteMessage({ token: userToken, abuseId, messageId: abuseMessageUserId })
868
869 const results = await Promise.all([
870 commands[0].listMessages({ abuseId }),
871 commands[0].listMessages({ token: userToken, abuseId })
872 ])
873
874 for (const body of results) {
875 expect(body.total).to.equal(2)
876
877 const abuseMessages: AbuseMessage[] = body.data
878 expect(abuseMessages[0].message).to.equal('message 2')
879 expect(abuseMessages[1].message).to.equal('message 4')
880 }
881 })
882 })
883
884 after(async function () {
885 await cleanupTests(servers)
886 })
887})
diff --git a/packages/tests/src/api/moderation/blocklist-notification.ts b/packages/tests/src/api/moderation/blocklist-notification.ts
new file mode 100644
index 000000000..abf36313b
--- /dev/null
+++ b/packages/tests/src/api/moderation/blocklist-notification.ts
@@ -0,0 +1,231 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { UserNotificationType, UserNotificationType_Type } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14async function checkNotifications (server: PeerTubeServer, token: string, expected: UserNotificationType_Type[]) {
15 const { data } = await server.notifications.list({ token, start: 0, count: 10, unread: true })
16 expect(data).to.have.lengthOf(expected.length)
17
18 for (const type of expected) {
19 expect(data.find(n => n.type === type)).to.exist
20 }
21}
22
23describe('Test blocklist notifications', function () {
24 let servers: PeerTubeServer[]
25 let videoUUID: string
26
27 let userToken1: string
28 let userToken2: string
29 let remoteUserToken: string
30
31 async function resetState () {
32 try {
33 await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user1_channel@' + servers[0].host })
34 await servers[1].subscriptions.remove({ token: remoteUserToken, uri: 'user2_channel@' + servers[0].host })
35 } catch {}
36
37 await waitJobs(servers)
38
39 await servers[0].notifications.markAsReadAll({ token: userToken1 })
40 await servers[0].notifications.markAsReadAll({ token: userToken2 })
41
42 {
43 const { uuid } = await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video' } })
44 videoUUID = uuid
45
46 await waitJobs(servers)
47 }
48
49 {
50 await servers[1].comments.createThread({
51 token: remoteUserToken,
52 videoId: videoUUID,
53 text: '@user2@' + servers[0].host + ' hello'
54 })
55 }
56
57 {
58
59 await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user1_channel@' + servers[0].host })
60 await servers[1].subscriptions.add({ token: remoteUserToken, targetUri: 'user2_channel@' + servers[0].host })
61 }
62
63 await waitJobs(servers)
64 }
65
66 before(async function () {
67 this.timeout(60000)
68
69 servers = await createMultipleServers(2)
70 await setAccessTokensToServers(servers)
71
72 {
73 const user = { username: 'user1', password: 'password' }
74 await servers[0].users.create({
75 username: user.username,
76 password: user.password,
77 videoQuota: -1,
78 videoQuotaDaily: -1
79 })
80
81 userToken1 = await servers[0].login.getAccessToken(user)
82 await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } })
83 }
84
85 {
86 const user = { username: 'user2', password: 'password' }
87 await servers[0].users.create({ username: user.username, password: user.password })
88
89 userToken2 = await servers[0].login.getAccessToken(user)
90 }
91
92 {
93 const user = { username: 'user3', password: 'password' }
94 await servers[1].users.create({ username: user.username, password: user.password })
95
96 remoteUserToken = await servers[1].login.getAccessToken(user)
97 }
98
99 await doubleFollow(servers[0], servers[1])
100 })
101
102 describe('User blocks another user', function () {
103
104 before(async function () {
105 this.timeout(30000)
106
107 await resetState()
108 })
109
110 it('Should have appropriate notifications', async function () {
111 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
112 await checkNotifications(servers[0], userToken1, notifs)
113 })
114
115 it('Should block an account', async function () {
116 await servers[0].blocklist.addToMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host })
117 await waitJobs(servers)
118 })
119
120 it('Should not have notifications from this account', async function () {
121 await checkNotifications(servers[0], userToken1, [])
122 })
123
124 it('Should have notifications of this account on user 2', async function () {
125 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
126
127 await checkNotifications(servers[0], userToken2, notifs)
128
129 await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, account: 'user3@' + servers[1].host })
130 })
131 })
132
133 describe('User blocks another server', function () {
134
135 before(async function () {
136 this.timeout(30000)
137
138 await resetState()
139 })
140
141 it('Should have appropriate notifications', async function () {
142 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
143 await checkNotifications(servers[0], userToken1, notifs)
144 })
145
146 it('Should block an account', async function () {
147 await servers[0].blocklist.addToMyBlocklist({ token: userToken1, server: servers[1].host })
148 await waitJobs(servers)
149 })
150
151 it('Should not have notifications from this account', async function () {
152 await checkNotifications(servers[0], userToken1, [])
153 })
154
155 it('Should have notifications of this account on user 2', async function () {
156 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
157
158 await checkNotifications(servers[0], userToken2, notifs)
159
160 await servers[0].blocklist.removeFromMyBlocklist({ token: userToken1, server: servers[1].host })
161 })
162 })
163
164 describe('Server blocks a user', function () {
165
166 before(async function () {
167 this.timeout(30000)
168
169 await resetState()
170 })
171
172 it('Should have appropriate notifications', async function () {
173 {
174 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
175 await checkNotifications(servers[0], userToken1, notifs)
176 }
177
178 {
179 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
180 await checkNotifications(servers[0], userToken2, notifs)
181 }
182 })
183
184 it('Should block an account', async function () {
185 await servers[0].blocklist.addToServerBlocklist({ account: 'user3@' + servers[1].host })
186 await waitJobs(servers)
187 })
188
189 it('Should not have notifications from this account', async function () {
190 await checkNotifications(servers[0], userToken1, [])
191 await checkNotifications(servers[0], userToken2, [])
192
193 await servers[0].blocklist.removeFromServerBlocklist({ account: 'user3@' + servers[1].host })
194 })
195 })
196
197 describe('Server blocks a server', function () {
198
199 before(async function () {
200 this.timeout(30000)
201
202 await resetState()
203 })
204
205 it('Should have appropriate notifications', async function () {
206 {
207 const notifs = [ UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, UserNotificationType.NEW_FOLLOW ]
208 await checkNotifications(servers[0], userToken1, notifs)
209 }
210
211 {
212 const notifs = [ UserNotificationType.COMMENT_MENTION, UserNotificationType.NEW_FOLLOW ]
213 await checkNotifications(servers[0], userToken2, notifs)
214 }
215 })
216
217 it('Should block an account', async function () {
218 await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host })
219 await waitJobs(servers)
220 })
221
222 it('Should not have notifications from this account', async function () {
223 await checkNotifications(servers[0], userToken1, [])
224 await checkNotifications(servers[0], userToken2, [])
225 })
226 })
227
228 after(async function () {
229 await cleanupTests(servers)
230 })
231})
diff --git a/packages/tests/src/api/moderation/blocklist.ts b/packages/tests/src/api/moderation/blocklist.ts
new file mode 100644
index 000000000..a84515241
--- /dev/null
+++ b/packages/tests/src/api/moderation/blocklist.ts
@@ -0,0 +1,902 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { UserNotificationType } from '@peertube/peertube-models'
5import {
6 BlocklistCommand,
7 cleanupTests,
8 CommentsCommand,
9 createMultipleServers,
10 doubleFollow,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultAccountAvatar,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17async function checkAllVideos (server: PeerTubeServer, token: string) {
18 {
19 const { data } = await server.videos.listWithToken({ token })
20 expect(data).to.have.lengthOf(5)
21 }
22
23 {
24 const { data } = await server.videos.list()
25 expect(data).to.have.lengthOf(5)
26 }
27}
28
29async function checkAllComments (server: PeerTubeServer, token: string, videoUUID: string) {
30 const { data } = await server.comments.listThreads({ videoId: videoUUID, start: 0, count: 25, sort: '-createdAt', token })
31
32 const threads = data.filter(t => t.isDeleted === false)
33 expect(threads).to.have.lengthOf(2)
34
35 for (const thread of threads) {
36 const tree = await server.comments.getThread({ videoId: videoUUID, threadId: thread.id, token })
37 expect(tree.children).to.have.lengthOf(1)
38 }
39}
40
41async function checkCommentNotification (
42 mainServer: PeerTubeServer,
43 comment: { server: PeerTubeServer, token: string, videoUUID: string, text: string },
44 check: 'presence' | 'absence'
45) {
46 const command = comment.server.comments
47
48 const { threadId, createdAt } = await command.createThread({ token: comment.token, videoId: comment.videoUUID, text: comment.text })
49
50 await waitJobs([ mainServer, comment.server ])
51
52 const { data } = await mainServer.notifications.list({ start: 0, count: 30 })
53 const commentNotifications = data.filter(n => n.comment && n.comment.video.uuid === comment.videoUUID && n.createdAt >= createdAt)
54
55 if (check === 'presence') expect(commentNotifications).to.have.lengthOf(1)
56 else expect(commentNotifications).to.have.lengthOf(0)
57
58 await command.delete({ token: comment.token, videoId: comment.videoUUID, commentId: threadId })
59
60 await waitJobs([ mainServer, comment.server ])
61}
62
63describe('Test blocklist', function () {
64 let servers: PeerTubeServer[]
65 let videoUUID1: string
66 let videoUUID2: string
67 let videoUUID3: string
68 let userToken1: string
69 let userModeratorToken: string
70 let userToken2: string
71
72 let command: BlocklistCommand
73 let commentsCommand: CommentsCommand[]
74
75 before(async function () {
76 this.timeout(120000)
77
78 servers = await createMultipleServers(3)
79 await setAccessTokensToServers(servers)
80 await setDefaultAccountAvatar(servers)
81
82 command = servers[0].blocklist
83 commentsCommand = servers.map(s => s.comments)
84
85 {
86 const user = { username: 'user1', password: 'password' }
87 await servers[0].users.create({ username: user.username, password: user.password })
88
89 userToken1 = await servers[0].login.getAccessToken(user)
90 await servers[0].videos.upload({ token: userToken1, attributes: { name: 'video user 1' } })
91 }
92
93 {
94 const user = { username: 'moderator', password: 'password' }
95 await servers[0].users.create({ username: user.username, password: user.password })
96
97 userModeratorToken = await servers[0].login.getAccessToken(user)
98 }
99
100 {
101 const user = { username: 'user2', password: 'password' }
102 await servers[1].users.create({ username: user.username, password: user.password })
103
104 userToken2 = await servers[1].login.getAccessToken(user)
105 await servers[1].videos.upload({ token: userToken2, attributes: { name: 'video user 2' } })
106 }
107
108 {
109 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } })
110 videoUUID1 = uuid
111 }
112
113 {
114 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } })
115 videoUUID2 = uuid
116 }
117
118 {
119 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } })
120 videoUUID3 = uuid
121 }
122
123 await doubleFollow(servers[0], servers[1])
124 await doubleFollow(servers[0], servers[2])
125
126 {
127 const created = await commentsCommand[0].createThread({ videoId: videoUUID1, text: 'comment root 1' })
128 const reply = await commentsCommand[0].addReply({
129 token: userToken1,
130 videoId: videoUUID1,
131 toCommentId: created.id,
132 text: 'comment user 1'
133 })
134 await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: reply.id, text: 'comment root 1' })
135 }
136
137 {
138 const created = await commentsCommand[0].createThread({ token: userToken1, videoId: videoUUID1, text: 'comment user 1' })
139 await commentsCommand[0].addReply({ videoId: videoUUID1, toCommentId: created.id, text: 'comment root 1' })
140 }
141
142 await waitJobs(servers)
143 })
144
145 describe('User blocklist', function () {
146
147 describe('When managing account blocklist', function () {
148 it('Should list all videos', function () {
149 return checkAllVideos(servers[0], servers[0].accessToken)
150 })
151
152 it('Should list the comments', function () {
153 return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
154 })
155
156 it('Should block a remote account', async function () {
157 await command.addToMyBlocklist({ account: 'user2@' + servers[1].host })
158 })
159
160 it('Should hide its videos', async function () {
161 const { data } = await servers[0].videos.listWithToken()
162
163 expect(data).to.have.lengthOf(4)
164
165 const v = data.find(v => v.name === 'video user 2')
166 expect(v).to.be.undefined
167 })
168
169 it('Should block a local account', async function () {
170 await command.addToMyBlocklist({ account: 'user1' })
171 })
172
173 it('Should hide its videos', async function () {
174 const { data } = await servers[0].videos.listWithToken()
175
176 expect(data).to.have.lengthOf(3)
177
178 const v = data.find(v => v.name === 'video user 1')
179 expect(v).to.be.undefined
180 })
181
182 it('Should hide its comments', async function () {
183 const { data } = await commentsCommand[0].listThreads({
184 token: servers[0].accessToken,
185 videoId: videoUUID1,
186 start: 0,
187 count: 25,
188 sort: '-createdAt'
189 })
190
191 expect(data).to.have.lengthOf(1)
192 expect(data[0].totalReplies).to.equal(1)
193
194 const t = data.find(t => t.text === 'comment user 1')
195 expect(t).to.be.undefined
196
197 for (const thread of data) {
198 const tree = await commentsCommand[0].getThread({
199 videoId: videoUUID1,
200 threadId: thread.id,
201 token: servers[0].accessToken
202 })
203 expect(tree.children).to.have.lengthOf(0)
204 }
205 })
206
207 it('Should not have notifications from blocked accounts', async function () {
208 this.timeout(20000)
209
210 {
211 const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' }
212 await checkCommentNotification(servers[0], comment, 'absence')
213 }
214
215 {
216 const comment = {
217 server: servers[0],
218 token: userToken1,
219 videoUUID: videoUUID2,
220 text: 'hello @root@' + servers[0].host
221 }
222 await checkCommentNotification(servers[0], comment, 'absence')
223 }
224 })
225
226 it('Should list all the videos with another user', async function () {
227 return checkAllVideos(servers[0], userToken1)
228 })
229
230 it('Should list blocked accounts', async function () {
231 {
232 const body = await command.listMyAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' })
233 expect(body.total).to.equal(2)
234
235 const block = body.data[0]
236 expect(block.byAccount.displayName).to.equal('root')
237 expect(block.byAccount.name).to.equal('root')
238 expect(block.blockedAccount.displayName).to.equal('user2')
239 expect(block.blockedAccount.name).to.equal('user2')
240 expect(block.blockedAccount.host).to.equal('' + servers[1].host)
241 }
242
243 {
244 const body = await command.listMyAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' })
245 expect(body.total).to.equal(2)
246
247 const block = body.data[0]
248 expect(block.byAccount.displayName).to.equal('root')
249 expect(block.byAccount.name).to.equal('root')
250 expect(block.blockedAccount.displayName).to.equal('user1')
251 expect(block.blockedAccount.name).to.equal('user1')
252 expect(block.blockedAccount.host).to.equal('' + servers[0].host)
253 }
254 })
255
256 it('Should search blocked accounts', async function () {
257 const body = await command.listMyAccountBlocklist({ start: 0, count: 10, search: 'user2' })
258 expect(body.total).to.equal(1)
259
260 expect(body.data[0].blockedAccount.name).to.equal('user2')
261 })
262
263 it('Should get blocked status', async function () {
264 const remoteHandle = 'user2@' + servers[1].host
265 const localHandle = 'user1@' + servers[0].host
266 const unknownHandle = 'user5@' + servers[0].host
267
268 {
269 const status = await command.getStatus({ accounts: [ remoteHandle ] })
270 expect(Object.keys(status.accounts)).to.have.lengthOf(1)
271 expect(status.accounts[remoteHandle].blockedByUser).to.be.false
272 expect(status.accounts[remoteHandle].blockedByServer).to.be.false
273
274 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
275 }
276
277 {
278 const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ remoteHandle ] })
279 expect(Object.keys(status.accounts)).to.have.lengthOf(1)
280 expect(status.accounts[remoteHandle].blockedByUser).to.be.true
281 expect(status.accounts[remoteHandle].blockedByServer).to.be.false
282
283 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
284 }
285
286 {
287 const status = await command.getStatus({ token: servers[0].accessToken, accounts: [ localHandle, remoteHandle, unknownHandle ] })
288 expect(Object.keys(status.accounts)).to.have.lengthOf(3)
289
290 for (const handle of [ localHandle, remoteHandle ]) {
291 expect(status.accounts[handle].blockedByUser).to.be.true
292 expect(status.accounts[handle].blockedByServer).to.be.false
293 }
294
295 expect(status.accounts[unknownHandle].blockedByUser).to.be.false
296 expect(status.accounts[unknownHandle].blockedByServer).to.be.false
297
298 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
299 }
300 })
301
302 it('Should not allow a remote blocked user to comment my videos', async function () {
303 this.timeout(60000)
304
305 {
306 await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID3, text: 'comment user 2' })
307 await waitJobs(servers)
308
309 await commentsCommand[0].createThread({ token: servers[0].accessToken, videoId: videoUUID3, text: 'uploader' })
310 await waitJobs(servers)
311
312 const commentId = await commentsCommand[1].findCommentId({ videoId: videoUUID3, text: 'uploader' })
313 const message = 'reply by user 2'
314 const reply = await commentsCommand[1].addReply({ token: userToken2, videoId: videoUUID3, toCommentId: commentId, text: message })
315 await commentsCommand[1].addReply({ videoId: videoUUID3, toCommentId: reply.id, text: 'another reply' })
316
317 await waitJobs(servers)
318 }
319
320 // Server 2 has all the comments
321 {
322 const { data } = await commentsCommand[1].listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
323
324 expect(data).to.have.lengthOf(2)
325 expect(data[0].text).to.equal('uploader')
326 expect(data[1].text).to.equal('comment user 2')
327
328 const tree = await commentsCommand[1].getThread({ videoId: videoUUID3, threadId: data[0].id })
329 expect(tree.children).to.have.lengthOf(1)
330 expect(tree.children[0].comment.text).to.equal('reply by user 2')
331 expect(tree.children[0].children).to.have.lengthOf(1)
332 expect(tree.children[0].children[0].comment.text).to.equal('another reply')
333 }
334
335 // Server 1 and 3 should only have uploader comments
336 for (const server of [ servers[0], servers[2] ]) {
337 const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
338
339 expect(data).to.have.lengthOf(1)
340 expect(data[0].text).to.equal('uploader')
341
342 const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id })
343
344 if (server.serverNumber === 1) expect(tree.children).to.have.lengthOf(0)
345 else expect(tree.children).to.have.lengthOf(1)
346 }
347 })
348
349 it('Should unblock the remote account', async function () {
350 await command.removeFromMyBlocklist({ account: 'user2@' + servers[1].host })
351 })
352
353 it('Should display its videos', async function () {
354 const { data } = await servers[0].videos.listWithToken()
355 expect(data).to.have.lengthOf(4)
356
357 const v = data.find(v => v.name === 'video user 2')
358 expect(v).not.to.be.undefined
359 })
360
361 it('Should display its comments on my video', async function () {
362 for (const server of servers) {
363 const { data } = await server.comments.listThreads({ videoId: videoUUID3, count: 25, sort: '-createdAt' })
364
365 // Server 3 should not have 2 comment threads, because server 1 did not forward the server 2 comment
366 if (server.serverNumber === 3) {
367 expect(data).to.have.lengthOf(1)
368 continue
369 }
370
371 expect(data).to.have.lengthOf(2)
372 expect(data[0].text).to.equal('uploader')
373 expect(data[1].text).to.equal('comment user 2')
374
375 const tree = await server.comments.getThread({ videoId: videoUUID3, threadId: data[0].id })
376 expect(tree.children).to.have.lengthOf(1)
377 expect(tree.children[0].comment.text).to.equal('reply by user 2')
378 expect(tree.children[0].children).to.have.lengthOf(1)
379 expect(tree.children[0].children[0].comment.text).to.equal('another reply')
380 }
381 })
382
383 it('Should unblock the local account', async function () {
384 await command.removeFromMyBlocklist({ account: 'user1' })
385 })
386
387 it('Should display its comments', function () {
388 return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
389 })
390
391 it('Should have a notification from a non blocked account', async function () {
392 this.timeout(20000)
393
394 {
395 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
396 await checkCommentNotification(servers[0], comment, 'presence')
397 }
398
399 {
400 const comment = {
401 server: servers[0],
402 token: userToken1,
403 videoUUID: videoUUID2,
404 text: 'hello @root@' + servers[0].host
405 }
406 await checkCommentNotification(servers[0], comment, 'presence')
407 }
408 })
409 })
410
411 describe('When managing server blocklist', function () {
412
413 it('Should list all videos', function () {
414 return checkAllVideos(servers[0], servers[0].accessToken)
415 })
416
417 it('Should list the comments', function () {
418 return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
419 })
420
421 it('Should block a remote server', async function () {
422 await command.addToMyBlocklist({ server: '' + servers[1].host })
423 })
424
425 it('Should hide its videos', async function () {
426 const { data } = await servers[0].videos.listWithToken()
427
428 expect(data).to.have.lengthOf(3)
429
430 const v1 = data.find(v => v.name === 'video user 2')
431 const v2 = data.find(v => v.name === 'video server 2')
432
433 expect(v1).to.be.undefined
434 expect(v2).to.be.undefined
435 })
436
437 it('Should list all the videos with another user', async function () {
438 return checkAllVideos(servers[0], userToken1)
439 })
440
441 it('Should hide its comments', async function () {
442 const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' })
443
444 await waitJobs(servers)
445
446 await checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
447
448 await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id })
449 })
450
451 it('Should not have notifications from blocked server', async function () {
452 this.timeout(20000)
453
454 {
455 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' }
456 await checkCommentNotification(servers[0], comment, 'absence')
457 }
458
459 {
460 const comment = {
461 server: servers[1],
462 token: userToken2,
463 videoUUID: videoUUID1,
464 text: 'hello @root@' + servers[0].host
465 }
466 await checkCommentNotification(servers[0], comment, 'absence')
467 }
468 })
469
470 it('Should list blocked servers', async function () {
471 const body = await command.listMyServerBlocklist({ start: 0, count: 1, sort: 'createdAt' })
472 expect(body.total).to.equal(1)
473
474 const block = body.data[0]
475 expect(block.byAccount.displayName).to.equal('root')
476 expect(block.byAccount.name).to.equal('root')
477 expect(block.blockedServer.host).to.equal('' + servers[1].host)
478 })
479
480 it('Should search blocked servers', async function () {
481 const body = await command.listMyServerBlocklist({ start: 0, count: 10, search: servers[1].host })
482 expect(body.total).to.equal(1)
483
484 expect(body.data[0].blockedServer.host).to.equal(servers[1].host)
485 })
486
487 it('Should get blocklist status', async function () {
488 const blockedServer = servers[1].host
489 const notBlockedServer = 'example.com'
490
491 {
492 const status = await command.getStatus({ hosts: [ blockedServer, notBlockedServer ] })
493 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
494
495 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
496 expect(status.hosts[blockedServer].blockedByUser).to.be.false
497 expect(status.hosts[blockedServer].blockedByServer).to.be.false
498
499 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
500 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
501 }
502
503 {
504 const status = await command.getStatus({ token: servers[0].accessToken, hosts: [ blockedServer, notBlockedServer ] })
505 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
506
507 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
508 expect(status.hosts[blockedServer].blockedByUser).to.be.true
509 expect(status.hosts[blockedServer].blockedByServer).to.be.false
510
511 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
512 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
513 }
514 })
515
516 it('Should unblock the remote server', async function () {
517 await command.removeFromMyBlocklist({ server: '' + servers[1].host })
518 })
519
520 it('Should display its videos', function () {
521 return checkAllVideos(servers[0], servers[0].accessToken)
522 })
523
524 it('Should display its comments', function () {
525 return checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
526 })
527
528 it('Should have notification from unblocked server', async function () {
529 this.timeout(20000)
530
531 {
532 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
533 await checkCommentNotification(servers[0], comment, 'presence')
534 }
535
536 {
537 const comment = {
538 server: servers[1],
539 token: userToken2,
540 videoUUID: videoUUID1,
541 text: 'hello @root@' + servers[0].host
542 }
543 await checkCommentNotification(servers[0], comment, 'presence')
544 }
545 })
546 })
547 })
548
549 describe('Server blocklist', function () {
550
551 describe('When managing account blocklist', function () {
552 it('Should list all videos', async function () {
553 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
554 await checkAllVideos(servers[0], token)
555 }
556 })
557
558 it('Should list the comments', async function () {
559 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
560 await checkAllComments(servers[0], token, videoUUID1)
561 }
562 })
563
564 it('Should block a remote account', async function () {
565 await command.addToServerBlocklist({ account: 'user2@' + servers[1].host })
566 })
567
568 it('Should hide its videos', async function () {
569 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
570 const { data } = await servers[0].videos.listWithToken({ token })
571
572 expect(data).to.have.lengthOf(4)
573
574 const v = data.find(v => v.name === 'video user 2')
575 expect(v).to.be.undefined
576 }
577 })
578
579 it('Should block a local account', async function () {
580 await command.addToServerBlocklist({ account: 'user1' })
581 })
582
583 it('Should hide its videos', async function () {
584 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
585 const { data } = await servers[0].videos.listWithToken({ token })
586
587 expect(data).to.have.lengthOf(3)
588
589 const v = data.find(v => v.name === 'video user 1')
590 expect(v).to.be.undefined
591 }
592 })
593
594 it('Should hide its comments', async function () {
595 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
596 const { data } = await commentsCommand[0].listThreads({ videoId: videoUUID1, count: 20, sort: '-createdAt', token })
597 const threads = data.filter(t => t.isDeleted === false)
598
599 expect(threads).to.have.lengthOf(1)
600 expect(threads[0].totalReplies).to.equal(1)
601
602 const t = threads.find(t => t.text === 'comment user 1')
603 expect(t).to.be.undefined
604
605 for (const thread of threads) {
606 const tree = await commentsCommand[0].getThread({ videoId: videoUUID1, threadId: thread.id, token })
607 expect(tree.children).to.have.lengthOf(0)
608 }
609 }
610 })
611
612 it('Should not have notification from blocked accounts by instance', async function () {
613 this.timeout(20000)
614
615 {
616 const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' }
617 await checkCommentNotification(servers[0], comment, 'absence')
618 }
619
620 {
621 const comment = {
622 server: servers[1],
623 token: userToken2,
624 videoUUID: videoUUID1,
625 text: 'hello @root@' + servers[0].host
626 }
627 await checkCommentNotification(servers[0], comment, 'absence')
628 }
629 })
630
631 it('Should list blocked accounts', async function () {
632 {
633 const body = await command.listServerAccountBlocklist({ start: 0, count: 1, sort: 'createdAt' })
634 expect(body.total).to.equal(2)
635
636 const block = body.data[0]
637 expect(block.byAccount.displayName).to.equal('peertube')
638 expect(block.byAccount.name).to.equal('peertube')
639 expect(block.blockedAccount.displayName).to.equal('user2')
640 expect(block.blockedAccount.name).to.equal('user2')
641 expect(block.blockedAccount.host).to.equal('' + servers[1].host)
642 }
643
644 {
645 const body = await command.listServerAccountBlocklist({ start: 1, count: 2, sort: 'createdAt' })
646 expect(body.total).to.equal(2)
647
648 const block = body.data[0]
649 expect(block.byAccount.displayName).to.equal('peertube')
650 expect(block.byAccount.name).to.equal('peertube')
651 expect(block.blockedAccount.displayName).to.equal('user1')
652 expect(block.blockedAccount.name).to.equal('user1')
653 expect(block.blockedAccount.host).to.equal('' + servers[0].host)
654 }
655 })
656
657 it('Should search blocked accounts', async function () {
658 const body = await command.listServerAccountBlocklist({ start: 0, count: 10, search: 'user2' })
659 expect(body.total).to.equal(1)
660
661 expect(body.data[0].blockedAccount.name).to.equal('user2')
662 })
663
664 it('Should get blocked status', async function () {
665 const remoteHandle = 'user2@' + servers[1].host
666 const localHandle = 'user1@' + servers[0].host
667 const unknownHandle = 'user5@' + servers[0].host
668
669 for (const token of [ undefined, servers[0].accessToken ]) {
670 const status = await command.getStatus({ token, accounts: [ localHandle, remoteHandle, unknownHandle ] })
671 expect(Object.keys(status.accounts)).to.have.lengthOf(3)
672
673 for (const handle of [ localHandle, remoteHandle ]) {
674 expect(status.accounts[handle].blockedByUser).to.be.false
675 expect(status.accounts[handle].blockedByServer).to.be.true
676 }
677
678 expect(status.accounts[unknownHandle].blockedByUser).to.be.false
679 expect(status.accounts[unknownHandle].blockedByServer).to.be.false
680
681 expect(Object.keys(status.hosts)).to.have.lengthOf(0)
682 }
683 })
684
685 it('Should unblock the remote account', async function () {
686 await command.removeFromServerBlocklist({ account: 'user2@' + servers[1].host })
687 })
688
689 it('Should display its videos', async function () {
690 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
691 const { data } = await servers[0].videos.listWithToken({ token })
692 expect(data).to.have.lengthOf(4)
693
694 const v = data.find(v => v.name === 'video user 2')
695 expect(v).not.to.be.undefined
696 }
697 })
698
699 it('Should unblock the local account', async function () {
700 await command.removeFromServerBlocklist({ account: 'user1' })
701 })
702
703 it('Should display its comments', async function () {
704 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
705 await checkAllComments(servers[0], token, videoUUID1)
706 }
707 })
708
709 it('Should have notifications from unblocked accounts', async function () {
710 this.timeout(20000)
711
712 {
713 const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'displayed comment' }
714 await checkCommentNotification(servers[0], comment, 'presence')
715 }
716
717 {
718 const comment = {
719 server: servers[1],
720 token: userToken2,
721 videoUUID: videoUUID1,
722 text: 'hello @root@' + servers[0].host
723 }
724 await checkCommentNotification(servers[0], comment, 'presence')
725 }
726 })
727 })
728
729 describe('When managing server blocklist', function () {
730
731 it('Should list all videos', async function () {
732 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
733 await checkAllVideos(servers[0], token)
734 }
735 })
736
737 it('Should list the comments', async function () {
738 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
739 await checkAllComments(servers[0], token, videoUUID1)
740 }
741 })
742
743 it('Should block a remote server', async function () {
744 await command.addToServerBlocklist({ server: '' + servers[1].host })
745 })
746
747 it('Should hide its videos', async function () {
748 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
749 const requests = [
750 servers[0].videos.list(),
751 servers[0].videos.listWithToken({ token })
752 ]
753
754 for (const req of requests) {
755 const { data } = await req
756 expect(data).to.have.lengthOf(3)
757
758 const v1 = data.find(v => v.name === 'video user 2')
759 const v2 = data.find(v => v.name === 'video server 2')
760
761 expect(v1).to.be.undefined
762 expect(v2).to.be.undefined
763 }
764 }
765 })
766
767 it('Should hide its comments', async function () {
768 const { id } = await commentsCommand[1].createThread({ token: userToken2, videoId: videoUUID1, text: 'hidden comment 2' })
769
770 await waitJobs(servers)
771
772 await checkAllComments(servers[0], servers[0].accessToken, videoUUID1)
773
774 await commentsCommand[1].delete({ token: userToken2, videoId: videoUUID1, commentId: id })
775 })
776
777 it('Should not have notification from blocked instances by instance', async function () {
778 this.timeout(50000)
779
780 {
781 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' }
782 await checkCommentNotification(servers[0], comment, 'absence')
783 }
784
785 {
786 const comment = {
787 server: servers[1],
788 token: userToken2,
789 videoUUID: videoUUID1,
790 text: 'hello @root@' + servers[0].host
791 }
792 await checkCommentNotification(servers[0], comment, 'absence')
793 }
794
795 {
796 const now = new Date()
797 await servers[1].follows.unfollow({ target: servers[0] })
798 await waitJobs(servers)
799 await servers[1].follows.follow({ hosts: [ servers[0].host ] })
800
801 await waitJobs(servers)
802
803 const { data } = await servers[0].notifications.list({ start: 0, count: 30 })
804 const commentNotifications = data.filter(n => {
805 return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString()
806 })
807
808 expect(commentNotifications).to.have.lengthOf(0)
809 }
810 })
811
812 it('Should list blocked servers', async function () {
813 const body = await command.listServerServerBlocklist({ start: 0, count: 1, sort: 'createdAt' })
814 expect(body.total).to.equal(1)
815
816 const block = body.data[0]
817 expect(block.byAccount.displayName).to.equal('peertube')
818 expect(block.byAccount.name).to.equal('peertube')
819 expect(block.blockedServer.host).to.equal('' + servers[1].host)
820 })
821
822 it('Should search blocked servers', async function () {
823 const body = await command.listServerServerBlocklist({ start: 0, count: 10, search: servers[1].host })
824 expect(body.total).to.equal(1)
825
826 expect(body.data[0].blockedServer.host).to.equal(servers[1].host)
827 })
828
829 it('Should get blocklist status', async function () {
830 const blockedServer = servers[1].host
831 const notBlockedServer = 'example.com'
832
833 for (const token of [ undefined, servers[0].accessToken ]) {
834 const status = await command.getStatus({ token, hosts: [ blockedServer, notBlockedServer ] })
835 expect(Object.keys(status.accounts)).to.have.lengthOf(0)
836
837 expect(Object.keys(status.hosts)).to.have.lengthOf(2)
838 expect(status.hosts[blockedServer].blockedByUser).to.be.false
839 expect(status.hosts[blockedServer].blockedByServer).to.be.true
840
841 expect(status.hosts[notBlockedServer].blockedByUser).to.be.false
842 expect(status.hosts[notBlockedServer].blockedByServer).to.be.false
843 }
844 })
845
846 it('Should unblock the remote server', async function () {
847 await command.removeFromServerBlocklist({ server: '' + servers[1].host })
848 })
849
850 it('Should list all videos', async function () {
851 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
852 await checkAllVideos(servers[0], token)
853 }
854 })
855
856 it('Should list the comments', async function () {
857 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
858 await checkAllComments(servers[0], token, videoUUID1)
859 }
860 })
861
862 it('Should have notification from unblocked instances', async function () {
863 this.timeout(50000)
864
865 {
866 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
867 await checkCommentNotification(servers[0], comment, 'presence')
868 }
869
870 {
871 const comment = {
872 server: servers[1],
873 token: userToken2,
874 videoUUID: videoUUID1,
875 text: 'hello @root@' + servers[0].host
876 }
877 await checkCommentNotification(servers[0], comment, 'presence')
878 }
879
880 {
881 const now = new Date()
882 await servers[1].follows.unfollow({ target: servers[0] })
883 await waitJobs(servers)
884 await servers[1].follows.follow({ hosts: [ servers[0].host ] })
885
886 await waitJobs(servers)
887
888 const { data } = await servers[0].notifications.list({ start: 0, count: 30 })
889 const commentNotifications = data.filter(n => {
890 return n.type === UserNotificationType.NEW_INSTANCE_FOLLOWER && n.createdAt >= now.toISOString()
891 })
892
893 expect(commentNotifications).to.have.lengthOf(1)
894 }
895 })
896 })
897 })
898
899 after(async function () {
900 await cleanupTests(servers)
901 })
902})
diff --git a/packages/tests/src/api/moderation/index.ts b/packages/tests/src/api/moderation/index.ts
new file mode 100644
index 000000000..e3794d01e
--- /dev/null
+++ b/packages/tests/src/api/moderation/index.ts
@@ -0,0 +1,4 @@
1export * from './abuses.js'
2export * from './blocklist-notification.js'
3export * from './blocklist.js'
4export * from './video-blacklist.js'
diff --git a/packages/tests/src/api/moderation/video-blacklist.ts b/packages/tests/src/api/moderation/video-blacklist.ts
new file mode 100644
index 000000000..341dadad0
--- /dev/null
+++ b/packages/tests/src/api/moderation/video-blacklist.ts
@@ -0,0 +1,414 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FIXTURE_URLS } from '@tests/shared/tests.js'
5import { sortObjectComparator } from '@peertube/peertube-core-utils'
6import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models'
7import {
8 BlacklistCommand,
9 cleanupTests,
10 createMultipleServers,
11 doubleFollow,
12 killallServers,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 setDefaultChannelAvatar,
16 waitJobs
17} from '@peertube/peertube-server-commands'
18
19describe('Test video blacklist', function () {
20 let servers: PeerTubeServer[] = []
21 let videoId: number
22 let command: BlacklistCommand
23
24 async function blacklistVideosOnServer (server: PeerTubeServer) {
25 const { data } = await server.videos.list()
26
27 for (const video of data) {
28 await server.blacklist.add({ videoId: video.id, reason: 'super reason' })
29 }
30 }
31
32 before(async function () {
33 this.timeout(120000)
34
35 // Run servers
36 servers = await createMultipleServers(2)
37
38 // Get the access tokens
39 await setAccessTokensToServers(servers)
40
41 // Server 1 and server 2 follow each other
42 await doubleFollow(servers[0], servers[1])
43 await setDefaultChannelAvatar(servers[0])
44
45 // Upload 2 videos on server 2
46 await servers[1].videos.upload({ attributes: { name: 'My 1st video', description: 'A video on server 2' } })
47 await servers[1].videos.upload({ attributes: { name: 'My 2nd video', description: 'A video on server 2' } })
48
49 // Wait videos propagation, server 2 has transcoding enabled
50 await waitJobs(servers)
51
52 command = servers[0].blacklist
53
54 // Blacklist the two videos on server 1
55 await blacklistVideosOnServer(servers[0])
56 })
57
58 describe('When listing/searching videos', function () {
59
60 it('Should not have the video blacklisted in videos list/search on server 1', async function () {
61 {
62 const { total, data } = await servers[0].videos.list()
63
64 expect(total).to.equal(0)
65 expect(data).to.be.an('array')
66 expect(data.length).to.equal(0)
67 }
68
69 {
70 const body = await servers[0].search.searchVideos({ search: 'video' })
71
72 expect(body.total).to.equal(0)
73 expect(body.data).to.be.an('array')
74 expect(body.data.length).to.equal(0)
75 }
76 })
77
78 it('Should have the blacklisted video in videos list/search on server 2', async function () {
79 {
80 const { total, data } = await servers[1].videos.list()
81
82 expect(total).to.equal(2)
83 expect(data).to.be.an('array')
84 expect(data.length).to.equal(2)
85 }
86
87 {
88 const body = await servers[1].search.searchVideos({ search: 'video' })
89
90 expect(body.total).to.equal(2)
91 expect(body.data).to.be.an('array')
92 expect(body.data.length).to.equal(2)
93 }
94 })
95 })
96
97 describe('When listing manually blacklisted videos', function () {
98 it('Should display all the blacklisted videos', async function () {
99 const body = await command.list()
100 expect(body.total).to.equal(2)
101
102 const blacklistedVideos = body.data
103 expect(blacklistedVideos).to.be.an('array')
104 expect(blacklistedVideos.length).to.equal(2)
105
106 for (const blacklistedVideo of blacklistedVideos) {
107 expect(blacklistedVideo.reason).to.equal('super reason')
108 videoId = blacklistedVideo.video.id
109 }
110 })
111
112 it('Should display all the blacklisted videos when applying manual type filter', async function () {
113 const body = await command.list({ type: VideoBlacklistType.MANUAL })
114 expect(body.total).to.equal(2)
115
116 const blacklistedVideos = body.data
117 expect(blacklistedVideos).to.be.an('array')
118 expect(blacklistedVideos.length).to.equal(2)
119 })
120
121 it('Should display nothing when applying automatic type filter', async function () {
122 const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
123 expect(body.total).to.equal(0)
124
125 const blacklistedVideos = body.data
126 expect(blacklistedVideos).to.be.an('array')
127 expect(blacklistedVideos.length).to.equal(0)
128 })
129
130 it('Should get the correct sort when sorting by descending id', async function () {
131 const body = await command.list({ sort: '-id' })
132 expect(body.total).to.equal(2)
133
134 const blacklistedVideos = body.data
135 expect(blacklistedVideos).to.be.an('array')
136 expect(blacklistedVideos.length).to.equal(2)
137
138 const result = [ ...body.data ].sort(sortObjectComparator('id', 'desc'))
139 expect(blacklistedVideos).to.deep.equal(result)
140 })
141
142 it('Should get the correct sort when sorting by descending video name', async function () {
143 const body = await command.list({ sort: '-name' })
144 expect(body.total).to.equal(2)
145
146 const blacklistedVideos = body.data
147 expect(blacklistedVideos).to.be.an('array')
148 expect(blacklistedVideos.length).to.equal(2)
149
150 const result = [ ...body.data ].sort(sortObjectComparator('name', 'desc'))
151 expect(blacklistedVideos).to.deep.equal(result)
152 })
153
154 it('Should get the correct sort when sorting by ascending creation date', async function () {
155 const body = await command.list({ sort: 'createdAt' })
156 expect(body.total).to.equal(2)
157
158 const blacklistedVideos = body.data
159 expect(blacklistedVideos).to.be.an('array')
160 expect(blacklistedVideos.length).to.equal(2)
161
162 const result = [ ...body.data ].sort(sortObjectComparator('createdAt', 'asc'))
163 expect(blacklistedVideos).to.deep.equal(result)
164 })
165 })
166
167 describe('When updating blacklisted videos', function () {
168 it('Should change the reason', async function () {
169 await command.update({ videoId, reason: 'my super reason updated' })
170
171 const body = await command.list({ sort: '-name' })
172 const video = body.data.find(b => b.video.id === videoId)
173
174 expect(video.reason).to.equal('my super reason updated')
175 })
176 })
177
178 describe('When listing my videos', function () {
179 it('Should display blacklisted videos', async function () {
180 await blacklistVideosOnServer(servers[1])
181
182 const { total, data } = await servers[1].videos.listMyVideos()
183
184 expect(total).to.equal(2)
185 expect(data).to.have.lengthOf(2)
186
187 for (const video of data) {
188 expect(video.blacklisted).to.be.true
189 expect(video.blacklistedReason).to.equal('super reason')
190 }
191 })
192 })
193
194 describe('When removing a blacklisted video', function () {
195 let videoToRemove: VideoBlacklist
196 let blacklist = []
197
198 it('Should not have any video in videos list on server 1', async function () {
199 const { total, data } = await servers[0].videos.list()
200 expect(total).to.equal(0)
201 expect(data).to.be.an('array')
202 expect(data.length).to.equal(0)
203 })
204
205 it('Should remove a video from the blacklist on server 1', async function () {
206 // Get one video in the blacklist
207 const body = await command.list({ sort: '-name' })
208 videoToRemove = body.data[0]
209 blacklist = body.data.slice(1)
210
211 // Remove it
212 await command.remove({ videoId: videoToRemove.video.id })
213 })
214
215 it('Should have the ex-blacklisted video in videos list on server 1', async function () {
216 const { total, data } = await servers[0].videos.list()
217 expect(total).to.equal(1)
218
219 expect(data).to.be.an('array')
220 expect(data.length).to.equal(1)
221
222 expect(data[0].name).to.equal(videoToRemove.video.name)
223 expect(data[0].id).to.equal(videoToRemove.video.id)
224 })
225
226 it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () {
227 const body = await command.list({ sort: '-name' })
228 expect(body.total).to.equal(1)
229
230 const videos = body.data
231 expect(videos).to.be.an('array')
232 expect(videos.length).to.equal(1)
233 expect(videos).to.deep.equal(blacklist)
234 })
235 })
236
237 describe('When blacklisting local videos', function () {
238 let video3UUID: string
239 let video4UUID: string
240
241 before(async function () {
242 {
243 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 3' } })
244 video3UUID = uuid
245 }
246 {
247 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'Video 4' } })
248 video4UUID = uuid
249 }
250
251 await waitJobs(servers)
252 })
253
254 it('Should blacklist video 3 and keep it federated', async function () {
255 await command.add({ videoId: video3UUID, reason: 'super reason', unfederate: false })
256
257 await waitJobs(servers)
258
259 {
260 const { data } = await servers[0].videos.list()
261 expect(data.find(v => v.uuid === video3UUID)).to.be.undefined
262 }
263
264 {
265 const { data } = await servers[1].videos.list()
266 expect(data.find(v => v.uuid === video3UUID)).to.not.be.undefined
267 }
268 })
269
270 it('Should unfederate the video', async function () {
271 await command.add({ videoId: video4UUID, reason: 'super reason', unfederate: true })
272
273 await waitJobs(servers)
274
275 for (const server of servers) {
276 const { data } = await server.videos.list()
277 expect(data.find(v => v.uuid === video4UUID)).to.be.undefined
278 }
279 })
280
281 it('Should have the video unfederated even after an Update AP message', async function () {
282 await servers[0].videos.update({ id: video4UUID, attributes: { description: 'super description' } })
283
284 await waitJobs(servers)
285
286 for (const server of servers) {
287 const { data } = await server.videos.list()
288 expect(data.find(v => v.uuid === video4UUID)).to.be.undefined
289 }
290 })
291
292 it('Should have the correct video blacklist unfederate attribute', async function () {
293 const body = await command.list({ sort: 'createdAt' })
294
295 const blacklistedVideos = body.data
296 const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID)
297 const video4Blacklisted = blacklistedVideos.find(b => b.video.uuid === video4UUID)
298
299 expect(video3Blacklisted.unfederated).to.be.false
300 expect(video4Blacklisted.unfederated).to.be.true
301 })
302
303 it('Should remove the video from blacklist and refederate the video', async function () {
304 await command.remove({ videoId: video4UUID })
305
306 await waitJobs(servers)
307
308 for (const server of servers) {
309 const { data } = await server.videos.list()
310 expect(data.find(v => v.uuid === video4UUID)).to.not.be.undefined
311 }
312 })
313
314 })
315
316 describe('When auto blacklist videos', function () {
317 let userWithoutFlag: string
318 let userWithFlag: string
319 let channelOfUserWithoutFlag: number
320
321 before(async function () {
322 this.timeout(20000)
323
324 await killallServers([ servers[0] ])
325
326 const config = {
327 auto_blacklist: {
328 videos: {
329 of_users: {
330 enabled: true
331 }
332 }
333 }
334 }
335 await servers[0].run(config)
336
337 {
338 const user = { username: 'user_without_flag', password: 'password' }
339 await servers[0].users.create({
340 username: user.username,
341 adminFlags: UserAdminFlag.NONE,
342 password: user.password,
343 role: UserRole.USER
344 })
345
346 userWithoutFlag = await servers[0].login.getAccessToken(user)
347
348 const { videoChannels } = await servers[0].users.getMyInfo({ token: userWithoutFlag })
349 channelOfUserWithoutFlag = videoChannels[0].id
350 }
351
352 {
353 const user = { username: 'user_with_flag', password: 'password' }
354 await servers[0].users.create({
355 username: user.username,
356 adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST,
357 password: user.password,
358 role: UserRole.USER
359 })
360
361 userWithFlag = await servers[0].login.getAccessToken(user)
362 }
363
364 await waitJobs(servers)
365 })
366
367 it('Should auto blacklist a video on upload', async function () {
368 await servers[0].videos.upload({ token: userWithoutFlag, attributes: { name: 'blacklisted' } })
369
370 const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
371 expect(body.total).to.equal(1)
372 expect(body.data[0].video.name).to.equal('blacklisted')
373 })
374
375 it('Should auto blacklist a video on URL import', async function () {
376 this.timeout(15000)
377
378 const attributes = {
379 targetUrl: FIXTURE_URLS.goodVideo,
380 name: 'URL import',
381 channelId: channelOfUserWithoutFlag
382 }
383 await servers[0].imports.importVideo({ token: userWithoutFlag, attributes })
384
385 const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
386 expect(body.total).to.equal(2)
387 expect(body.data[1].video.name).to.equal('URL import')
388 })
389
390 it('Should auto blacklist a video on torrent import', async function () {
391 const attributes = {
392 magnetUri: FIXTURE_URLS.magnet,
393 name: 'Torrent import',
394 channelId: channelOfUserWithoutFlag
395 }
396 await servers[0].imports.importVideo({ token: userWithoutFlag, attributes })
397
398 const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
399 expect(body.total).to.equal(3)
400 expect(body.data[2].video.name).to.equal('Torrent import')
401 })
402
403 it('Should not auto blacklist a video on upload if the user has the bypass blacklist flag', async function () {
404 await servers[0].videos.upload({ token: userWithFlag, attributes: { name: 'not blacklisted' } })
405
406 const body = await command.list({ type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED })
407 expect(body.total).to.equal(3)
408 })
409 })
410
411 after(async function () {
412 await cleanupTests(servers)
413 })
414})
diff --git a/packages/tests/src/api/notifications/admin-notifications.ts b/packages/tests/src/api/notifications/admin-notifications.ts
new file mode 100644
index 000000000..2186dc55a
--- /dev/null
+++ b/packages/tests/src/api/notifications/admin-notifications.ts
@@ -0,0 +1,154 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
6import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
7import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
8import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js'
9import { CheckerBaseParams, prepareNotificationsTest, checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications.js'
10import { SQLCommand } from '@tests/shared/sql-command.js'
11
12describe('Test admin notifications', function () {
13 let server: PeerTubeServer
14 let sqlCommand: SQLCommand
15 let userNotifications: UserNotification[] = []
16 let adminNotifications: UserNotification[] = []
17 let emails: object[] = []
18 let baseParams: CheckerBaseParams
19 let joinPeerTubeServer: MockJoinPeerTubeVersions
20
21 before(async function () {
22 this.timeout(120000)
23
24 joinPeerTubeServer = new MockJoinPeerTubeVersions()
25 const port = await joinPeerTubeServer.initialize()
26
27 const config = {
28 peertube: {
29 check_latest_version: {
30 enabled: true,
31 url: `http://127.0.0.1:${port}/versions.json`
32 }
33 },
34 plugins: {
35 index: {
36 enabled: true,
37 check_latest_versions_interval: '3 seconds'
38 }
39 }
40 }
41
42 const res = await prepareNotificationsTest(1, config)
43 emails = res.emails
44 server = res.servers[0]
45
46 userNotifications = res.userNotifications
47 adminNotifications = res.adminNotifications
48
49 baseParams = {
50 server,
51 emails,
52 socketNotifications: adminNotifications,
53 token: server.accessToken
54 }
55
56 await server.plugins.install({ npmName: 'peertube-plugin-hello-world' })
57 await server.plugins.install({ npmName: 'peertube-theme-background-red' })
58
59 sqlCommand = new SQLCommand(server)
60 })
61
62 describe('Latest PeerTube version notification', function () {
63
64 it('Should not send a notification to admins if there is no new version', async function () {
65 this.timeout(30000)
66
67 joinPeerTubeServer.setLatestVersion('1.4.2')
68
69 await wait(3000)
70 await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '1.4.2', checkType: 'absence' })
71 })
72
73 it('Should send a notification to admins on new version', async function () {
74 this.timeout(30000)
75
76 joinPeerTubeServer.setLatestVersion('15.4.2')
77
78 await wait(3000)
79 await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.2', checkType: 'presence' })
80 })
81
82 it('Should not send the same notification to admins', async function () {
83 this.timeout(30000)
84
85 await wait(3000)
86 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
87 })
88
89 it('Should not have sent a notification to users', async function () {
90 this.timeout(30000)
91
92 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
93 })
94
95 it('Should send a new notification after a new release', async function () {
96 this.timeout(30000)
97
98 joinPeerTubeServer.setLatestVersion('15.4.3')
99
100 await wait(3000)
101 await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.3', checkType: 'presence' })
102 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
103 })
104 })
105
106 describe('Latest plugin version notification', function () {
107
108 it('Should not send a notification to admins if there is no new plugin version', async function () {
109 this.timeout(30000)
110
111 await wait(6000)
112 await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'absence' })
113 })
114
115 it('Should send a notification to admins on new plugin version', async function () {
116 this.timeout(30000)
117
118 await sqlCommand.setPluginVersion('hello-world', '0.0.1')
119 await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1')
120 await wait(6000)
121
122 await checkNewPluginVersion({ ...baseParams, pluginType: PluginType.PLUGIN, pluginName: 'hello-world', checkType: 'presence' })
123 })
124
125 it('Should not send the same notification to admins', async function () {
126 this.timeout(30000)
127
128 await wait(6000)
129
130 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
131 })
132
133 it('Should not have sent a notification to users', async function () {
134 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
135 })
136
137 it('Should send a new notification after a new plugin release', async function () {
138 this.timeout(30000)
139
140 await sqlCommand.setPluginVersion('hello-world', '0.0.1')
141 await sqlCommand.setPluginLatestVersion('hello-world', '0.0.1')
142 await wait(6000)
143
144 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
145 })
146 })
147
148 after(async function () {
149 MockSmtpServer.Instance.kill()
150
151 await sqlCommand.cleanup()
152 await cleanupTests([ server ])
153 })
154})
diff --git a/packages/tests/src/api/notifications/comments-notifications.ts b/packages/tests/src/api/notifications/comments-notifications.ts
new file mode 100644
index 000000000..5647d1286
--- /dev/null
+++ b/packages/tests/src/api/notifications/comments-notifications.ts
@@ -0,0 +1,300 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { UserNotification } from '@peertube/peertube-models'
5import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
6import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
7import { prepareNotificationsTest, CheckerBaseParams, checkNewCommentOnMyVideo, checkCommentMention } from '@tests/shared/notifications.js'
8
9describe('Test comments notifications', function () {
10 let servers: PeerTubeServer[] = []
11 let userToken: string
12 let userNotifications: UserNotification[] = []
13 let emails: object[] = []
14
15 const commentText = '**hello** <a href="https://joinpeertube.org">world</a>, <h1>what do you think about peertube?</h1>'
16 const expectedHtml = '<strong>hello</strong> <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">world</a>' +
17 ', </p>what do you think about peertube?'
18
19 before(async function () {
20 this.timeout(120000)
21
22 const res = await prepareNotificationsTest(2)
23 emails = res.emails
24 userToken = res.userAccessToken
25 servers = res.servers
26 userNotifications = res.userNotifications
27 })
28
29 describe('Comment on my video notifications', function () {
30 let baseParams: CheckerBaseParams
31
32 before(() => {
33 baseParams = {
34 server: servers[0],
35 emails,
36 socketNotifications: userNotifications,
37 token: userToken
38 }
39 })
40
41 it('Should not send a new comment notification after a comment on another video', async function () {
42 this.timeout(30000)
43
44 const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
45
46 const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
47 const commentId = created.id
48
49 await waitJobs(servers)
50 await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
51 })
52
53 it('Should not send a new comment notification if I comment my own video', async function () {
54 this.timeout(30000)
55
56 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
57
58 const created = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: 'comment' })
59 const commentId = created.id
60
61 await waitJobs(servers)
62 await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
63 })
64
65 it('Should not send a new comment notification if the account is muted', async function () {
66 this.timeout(30000)
67
68 await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
69
70 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
71
72 const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
73 const commentId = created.id
74
75 await waitJobs(servers)
76 await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'absence' })
77
78 await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' })
79 })
80
81 it('Should send a new comment notification after a local comment on my video', async function () {
82 this.timeout(30000)
83
84 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
85
86 const created = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
87 const commentId = created.id
88
89 await waitJobs(servers)
90 await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' })
91 })
92
93 it('Should send a new comment notification after a remote comment on my video', async function () {
94 this.timeout(30000)
95
96 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
97
98 await waitJobs(servers)
99
100 await servers[1].comments.createThread({ videoId: uuid, text: 'comment' })
101
102 await waitJobs(servers)
103
104 const { data } = await servers[0].comments.listThreads({ videoId: uuid })
105 expect(data).to.have.lengthOf(1)
106
107 const commentId = data[0].id
108 await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId: commentId, commentId, checkType: 'presence' })
109 })
110
111 it('Should send a new comment notification after a local reply on my video', async function () {
112 this.timeout(30000)
113
114 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
115
116 const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
117
118 const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' })
119
120 await waitJobs(servers)
121 await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' })
122 })
123
124 it('Should send a new comment notification after a remote reply on my video', async function () {
125 this.timeout(30000)
126
127 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
128 await waitJobs(servers)
129
130 {
131 const created = await servers[1].comments.createThread({ videoId: uuid, text: 'comment' })
132 const threadId = created.id
133 await servers[1].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'reply' })
134 }
135
136 await waitJobs(servers)
137
138 const { data } = await servers[0].comments.listThreads({ videoId: uuid })
139 expect(data).to.have.lengthOf(1)
140
141 const threadId = data[0].id
142 const tree = await servers[0].comments.getThread({ videoId: uuid, threadId })
143
144 expect(tree.children).to.have.lengthOf(1)
145 const commentId = tree.children[0].comment.id
146
147 await checkNewCommentOnMyVideo({ ...baseParams, shortUUID, threadId, commentId, checkType: 'presence' })
148 })
149
150 it('Should convert markdown in comment to html', async function () {
151 this.timeout(30000)
152
153 const { uuid } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'cool video' } })
154
155 await servers[0].comments.createThread({ videoId: uuid, text: commentText })
156
157 await waitJobs(servers)
158
159 const latestEmail = emails[emails.length - 1]
160 expect(latestEmail['html']).to.contain(expectedHtml)
161 })
162 })
163
164 describe('Mention notifications', function () {
165 let baseParams: CheckerBaseParams
166 const byAccountDisplayName = 'super root name'
167
168 before(async function () {
169 baseParams = {
170 server: servers[0],
171 emails,
172 socketNotifications: userNotifications,
173 token: userToken
174 }
175
176 await servers[0].users.updateMe({ displayName: 'super root name' })
177 await servers[1].users.updateMe({ displayName: 'super root 2 name' })
178 })
179
180 it('Should not send a new mention comment notification if I mention the video owner', async function () {
181 this.timeout(30000)
182
183 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
184
185 const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
186
187 await waitJobs(servers)
188 await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
189 })
190
191 it('Should not send a new mention comment notification if I mention myself', async function () {
192 this.timeout(30000)
193
194 const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
195
196 const { id: commentId } = await servers[0].comments.createThread({ token: userToken, videoId: uuid, text: '@user_1 hello' })
197
198 await waitJobs(servers)
199 await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
200 })
201
202 it('Should not send a new mention notification if the account is muted', async function () {
203 this.timeout(30000)
204
205 await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
206
207 const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
208
209 const { id: commentId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
210
211 await waitJobs(servers)
212 await checkCommentMention({ ...baseParams, shortUUID, threadId: commentId, commentId, byAccountDisplayName, checkType: 'absence' })
213
214 await servers[0].blocklist.removeFromMyBlocklist({ token: userToken, account: 'root' })
215 })
216
217 it('Should not send a new mention notification if the remote account mention a local account', async function () {
218 this.timeout(30000)
219
220 const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
221
222 await waitJobs(servers)
223 const { id: threadId } = await servers[1].comments.createThread({ videoId: uuid, text: '@user_1 hello' })
224
225 await waitJobs(servers)
226
227 const byAccountDisplayName = 'super root 2 name'
228 await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'absence' })
229 })
230
231 it('Should send a new mention notification after local comments', async function () {
232 this.timeout(30000)
233
234 const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
235
236 const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hellotext: 1' })
237
238 await waitJobs(servers)
239 await checkCommentMention({ ...baseParams, shortUUID, threadId, commentId: threadId, byAccountDisplayName, checkType: 'presence' })
240
241 const { id: commentId } = await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: 'hello 2 @user_1' })
242
243 await waitJobs(servers)
244 await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' })
245 })
246
247 it('Should send a new mention notification after remote comments', async function () {
248 this.timeout(30000)
249
250 const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
251
252 await waitJobs(servers)
253
254 const text1 = `hello @user_1@${servers[0].host} 1`
255 const { id: server2ThreadId } = await servers[1].comments.createThread({ videoId: uuid, text: text1 })
256
257 await waitJobs(servers)
258
259 const { data } = await servers[0].comments.listThreads({ videoId: uuid })
260 expect(data).to.have.lengthOf(1)
261
262 const byAccountDisplayName = 'super root 2 name'
263 const threadId = data[0].id
264 await checkCommentMention({ ...baseParams, shortUUID, commentId: threadId, threadId, byAccountDisplayName, checkType: 'presence' })
265
266 const text2 = `@user_1@${servers[0].host} hello 2 @root@${servers[0].host}`
267 await servers[1].comments.addReply({ videoId: uuid, toCommentId: server2ThreadId, text: text2 })
268
269 await waitJobs(servers)
270
271 const tree = await servers[0].comments.getThread({ videoId: uuid, threadId })
272
273 expect(tree.children).to.have.lengthOf(1)
274 const commentId = tree.children[0].comment.id
275
276 await checkCommentMention({ ...baseParams, shortUUID, commentId, threadId, byAccountDisplayName, checkType: 'presence' })
277 })
278
279 it('Should convert markdown in comment to html', async function () {
280 this.timeout(30000)
281
282 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
283
284 const { id: threadId } = await servers[0].comments.createThread({ videoId: uuid, text: '@user_1 hello 1' })
285
286 await servers[0].comments.addReply({ videoId: uuid, toCommentId: threadId, text: '@user_1 ' + commentText })
287
288 await waitJobs(servers)
289
290 const latestEmail = emails[emails.length - 1]
291 expect(latestEmail['html']).to.contain(expectedHtml)
292 })
293 })
294
295 after(async function () {
296 MockSmtpServer.Instance.kill()
297
298 await cleanupTests(servers)
299 })
300})
diff --git a/packages/tests/src/api/notifications/index.ts b/packages/tests/src/api/notifications/index.ts
new file mode 100644
index 000000000..d63d94182
--- /dev/null
+++ b/packages/tests/src/api/notifications/index.ts
@@ -0,0 +1,6 @@
1import './admin-notifications.js'
2import './comments-notifications.js'
3import './moderation-notifications.js'
4import './notifications-api.js'
5import './registrations-notifications.js'
6import './user-notifications.js'
diff --git a/packages/tests/src/api/notifications/moderation-notifications.ts b/packages/tests/src/api/notifications/moderation-notifications.ts
new file mode 100644
index 000000000..493764882
--- /dev/null
+++ b/packages/tests/src/api/notifications/moderation-notifications.ts
@@ -0,0 +1,609 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { wait } from '@peertube/peertube-core-utils'
4import { AbuseState, CustomConfig, UserNotification, UserRole, VideoPrivacy } from '@peertube/peertube-models'
5import { buildUUID } from '@peertube/peertube-node-utils'
6import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
7import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
8import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js'
9import {
10 prepareNotificationsTest,
11 CheckerBaseParams,
12 checkNewVideoAbuseForModerators,
13 checkNewCommentAbuseForModerators,
14 checkNewAccountAbuseForModerators,
15 checkAbuseStateChange,
16 checkNewAbuseMessage,
17 checkNewBlacklistOnMyVideo,
18 checkNewInstanceFollower,
19 checkAutoInstanceFollowing,
20 checkVideoAutoBlacklistForModerators,
21 checkVideoIsPublished,
22 checkNewVideoFromSubscription
23} from '@tests/shared/notifications.js'
24
25describe('Test moderation notifications', function () {
26 let servers: PeerTubeServer[] = []
27 let userToken1: string
28 let userToken2: string
29
30 let userNotifications: UserNotification[] = []
31 let adminNotifications: UserNotification[] = []
32 let adminNotificationsServer2: UserNotification[] = []
33 let emails: object[] = []
34
35 before(async function () {
36 this.timeout(120000)
37
38 const res = await prepareNotificationsTest(3)
39 emails = res.emails
40 userToken1 = res.userAccessToken
41 servers = res.servers
42 userNotifications = res.userNotifications
43 adminNotifications = res.adminNotifications
44 adminNotificationsServer2 = res.adminNotificationsServer2
45
46 userToken2 = await servers[1].users.generateUserAndToken('user2', UserRole.USER)
47 })
48
49 describe('Abuse for moderators notification', function () {
50 let baseParams: CheckerBaseParams
51
52 before(() => {
53 baseParams = {
54 server: servers[0],
55 emails,
56 socketNotifications: adminNotifications,
57 token: servers[0].accessToken
58 }
59 })
60
61 it('Should not send a notification to moderators on local abuse reported by an admin', async function () {
62 this.timeout(50000)
63
64 const name = 'video for abuse ' + buildUUID()
65 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
66
67 await servers[0].abuses.report({ videoId: video.id, reason: 'super reason' })
68
69 await waitJobs(servers)
70 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'absence' })
71 })
72
73 it('Should send a notification to moderators on local video abuse', async function () {
74 this.timeout(50000)
75
76 const name = 'video for abuse ' + buildUUID()
77 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
78
79 await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
80
81 await waitJobs(servers)
82 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
83 })
84
85 it('Should send a notification to moderators on remote video abuse', async function () {
86 this.timeout(50000)
87
88 const name = 'video for abuse ' + buildUUID()
89 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
90
91 await waitJobs(servers)
92
93 const videoId = await servers[1].videos.getId({ uuid: video.uuid })
94 await servers[1].abuses.report({ token: userToken2, videoId, reason: 'super reason' })
95
96 await waitJobs(servers)
97 await checkNewVideoAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
98 })
99
100 it('Should send a notification to moderators on local comment abuse', async function () {
101 this.timeout(50000)
102
103 const name = 'video for abuse ' + buildUUID()
104 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
105 const comment = await servers[0].comments.createThread({
106 token: userToken1,
107 videoId: video.id,
108 text: 'comment abuse ' + buildUUID()
109 })
110
111 await waitJobs(servers)
112
113 await servers[0].abuses.report({ token: userToken1, commentId: comment.id, reason: 'super reason' })
114
115 await waitJobs(servers)
116 await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
117 })
118
119 it('Should send a notification to moderators on remote comment abuse', async function () {
120 this.timeout(50000)
121
122 const name = 'video for abuse ' + buildUUID()
123 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
124
125 await servers[0].comments.createThread({
126 token: userToken1,
127 videoId: video.id,
128 text: 'comment abuse ' + buildUUID()
129 })
130
131 await waitJobs(servers)
132
133 const { data } = await servers[1].comments.listThreads({ videoId: video.uuid })
134 const commentId = data[0].id
135 await servers[1].abuses.report({ token: userToken2, commentId, reason: 'super reason' })
136
137 await waitJobs(servers)
138 await checkNewCommentAbuseForModerators({ ...baseParams, shortUUID: video.shortUUID, videoName: name, checkType: 'presence' })
139 })
140
141 it('Should send a notification to moderators on local account abuse', async function () {
142 this.timeout(50000)
143
144 const username = 'user' + new Date().getTime()
145 const { account } = await servers[0].users.create({ username, password: 'donald' })
146 const accountId = account.id
147
148 await servers[0].abuses.report({ token: userToken1, accountId, reason: 'super reason' })
149
150 await waitJobs(servers)
151 await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
152 })
153
154 it('Should send a notification to moderators on remote account abuse', async function () {
155 this.timeout(50000)
156
157 const username = 'user' + new Date().getTime()
158 const tmpToken = await servers[0].users.generateUserAndToken(username)
159 await servers[0].videos.upload({ token: tmpToken, attributes: { name: 'super video' } })
160
161 await waitJobs(servers)
162
163 const account = await servers[1].accounts.get({ accountName: username + '@' + servers[0].host })
164 await servers[1].abuses.report({ token: userToken2, accountId: account.id, reason: 'super reason' })
165
166 await waitJobs(servers)
167 await checkNewAccountAbuseForModerators({ ...baseParams, displayName: username, checkType: 'presence' })
168 })
169 })
170
171 describe('Abuse state change notification', function () {
172 let baseParams: CheckerBaseParams
173 let abuseId: number
174
175 before(async function () {
176 baseParams = {
177 server: servers[0],
178 emails,
179 socketNotifications: userNotifications,
180 token: userToken1
181 }
182
183 const name = 'abuse ' + buildUUID()
184 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
185
186 const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
187 abuseId = body.abuse.id
188 })
189
190 it('Should send a notification to reporter if the abuse has been accepted', async function () {
191 this.timeout(30000)
192
193 await servers[0].abuses.update({ abuseId, body: { state: AbuseState.ACCEPTED } })
194 await waitJobs(servers)
195
196 await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.ACCEPTED, checkType: 'presence' })
197 })
198
199 it('Should send a notification to reporter if the abuse has been rejected', async function () {
200 this.timeout(30000)
201
202 await servers[0].abuses.update({ abuseId, body: { state: AbuseState.REJECTED } })
203 await waitJobs(servers)
204
205 await checkAbuseStateChange({ ...baseParams, abuseId, state: AbuseState.REJECTED, checkType: 'presence' })
206 })
207 })
208
209 describe('New abuse message notification', function () {
210 let baseParamsUser: CheckerBaseParams
211 let baseParamsAdmin: CheckerBaseParams
212 let abuseId: number
213 let abuseId2: number
214
215 before(async function () {
216 baseParamsUser = {
217 server: servers[0],
218 emails,
219 socketNotifications: userNotifications,
220 token: userToken1
221 }
222
223 baseParamsAdmin = {
224 server: servers[0],
225 emails,
226 socketNotifications: adminNotifications,
227 token: servers[0].accessToken
228 }
229
230 const name = 'abuse ' + buildUUID()
231 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
232
233 {
234 const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason' })
235 abuseId = body.abuse.id
236 }
237
238 {
239 const body = await servers[0].abuses.report({ token: userToken1, videoId: video.id, reason: 'super reason 2' })
240 abuseId2 = body.abuse.id
241 }
242 })
243
244 it('Should send a notification to reporter on new message', async function () {
245 this.timeout(30000)
246
247 const message = 'my super message to users'
248 await servers[0].abuses.addMessage({ abuseId, message })
249 await waitJobs(servers)
250
251 await checkNewAbuseMessage({ ...baseParamsUser, abuseId, message, toEmail: 'user_1@example.com', checkType: 'presence' })
252 })
253
254 it('Should not send a notification to the admin if sent by the admin', async function () {
255 this.timeout(30000)
256
257 const message = 'my super message that should not be sent to the admin'
258 await servers[0].abuses.addMessage({ abuseId, message })
259 await waitJobs(servers)
260
261 const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com'
262 await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId, message, toEmail, checkType: 'absence' })
263 })
264
265 it('Should send a notification to moderators', async function () {
266 this.timeout(30000)
267
268 const message = 'my super message to moderators'
269 await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message })
270 await waitJobs(servers)
271
272 const toEmail = 'admin' + servers[0].internalServerNumber + '@example.com'
273 await checkNewAbuseMessage({ ...baseParamsAdmin, abuseId: abuseId2, message, toEmail, checkType: 'presence' })
274 })
275
276 it('Should not send a notification to reporter if sent by the reporter', async function () {
277 this.timeout(30000)
278
279 const message = 'my super message that should not be sent to reporter'
280 await servers[0].abuses.addMessage({ token: userToken1, abuseId: abuseId2, message })
281 await waitJobs(servers)
282
283 const toEmail = 'user_1@example.com'
284 await checkNewAbuseMessage({ ...baseParamsUser, abuseId: abuseId2, message, toEmail, checkType: 'absence' })
285 })
286 })
287
288 describe('Video blacklist on my video', function () {
289 let baseParams: CheckerBaseParams
290
291 before(() => {
292 baseParams = {
293 server: servers[0],
294 emails,
295 socketNotifications: userNotifications,
296 token: userToken1
297 }
298 })
299
300 it('Should send a notification to video owner on blacklist', async function () {
301 this.timeout(30000)
302
303 const name = 'video for abuse ' + buildUUID()
304 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
305
306 await servers[0].blacklist.add({ videoId: uuid })
307
308 await waitJobs(servers)
309 await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'blacklist' })
310 })
311
312 it('Should send a notification to video owner on unblacklist', async function () {
313 this.timeout(30000)
314
315 const name = 'video for abuse ' + buildUUID()
316 const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes: { name } })
317
318 await servers[0].blacklist.add({ videoId: uuid })
319
320 await waitJobs(servers)
321 await servers[0].blacklist.remove({ videoId: uuid })
322 await waitJobs(servers)
323
324 await wait(500)
325 await checkNewBlacklistOnMyVideo({ ...baseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' })
326 })
327 })
328
329 describe('New instance follows', function () {
330 const instanceIndexServer = new MockInstancesIndex()
331 let config: any
332 let baseParams: CheckerBaseParams
333
334 before(async function () {
335 baseParams = {
336 server: servers[0],
337 emails,
338 socketNotifications: adminNotifications,
339 token: servers[0].accessToken
340 }
341
342 const port = await instanceIndexServer.initialize()
343 instanceIndexServer.addInstance(servers[1].host)
344
345 config = {
346 followings: {
347 instance: {
348 autoFollowIndex: {
349 indexUrl: `http://127.0.0.1:${port}/api/v1/instances/hosts`,
350 enabled: true
351 }
352 }
353 }
354 }
355 })
356
357 it('Should send a notification only to admin when there is a new instance follower', async function () {
358 this.timeout(60000)
359
360 await servers[2].follows.follow({ hosts: [ servers[0].url ] })
361
362 await waitJobs(servers)
363
364 await checkNewInstanceFollower({ ...baseParams, followerHost: servers[2].host, checkType: 'presence' })
365
366 const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
367 await checkNewInstanceFollower({ ...baseParams, ...userOverride, followerHost: servers[2].host, checkType: 'absence' })
368 })
369
370 it('Should send a notification on auto follow back', async function () {
371 this.timeout(40000)
372
373 await servers[2].follows.unfollow({ target: servers[0] })
374 await waitJobs(servers)
375
376 const config = {
377 followings: {
378 instance: {
379 autoFollowBack: { enabled: true }
380 }
381 }
382 }
383 await servers[0].config.updateCustomSubConfig({ newConfig: config })
384
385 await servers[2].follows.follow({ hosts: [ servers[0].url ] })
386
387 await waitJobs(servers)
388
389 const followerHost = servers[0].host
390 const followingHost = servers[2].host
391 await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' })
392
393 const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
394 await checkAutoInstanceFollowing({ ...baseParams, ...userOverride, followerHost, followingHost, checkType: 'absence' })
395
396 config.followings.instance.autoFollowBack.enabled = false
397 await servers[0].config.updateCustomSubConfig({ newConfig: config })
398 await servers[0].follows.unfollow({ target: servers[2] })
399 await servers[2].follows.unfollow({ target: servers[0] })
400 })
401
402 it('Should send a notification on auto instances index follow', async function () {
403 this.timeout(30000)
404 await servers[0].follows.unfollow({ target: servers[1] })
405
406 await servers[0].config.updateCustomSubConfig({ newConfig: config })
407
408 await wait(5000)
409 await waitJobs(servers)
410
411 const followerHost = servers[0].host
412 const followingHost = servers[1].host
413 await checkAutoInstanceFollowing({ ...baseParams, followerHost, followingHost, checkType: 'presence' })
414
415 config.followings.instance.autoFollowIndex.enabled = false
416 await servers[0].config.updateCustomSubConfig({ newConfig: config })
417 await servers[0].follows.unfollow({ target: servers[1] })
418 })
419 })
420
421 describe('Video-related notifications when video auto-blacklist is enabled', function () {
422 let userBaseParams: CheckerBaseParams
423 let adminBaseParamsServer1: CheckerBaseParams
424 let adminBaseParamsServer2: CheckerBaseParams
425 let uuid: string
426 let shortUUID: string
427 let videoName: string
428 let currentCustomConfig: CustomConfig
429
430 before(async function () {
431
432 adminBaseParamsServer1 = {
433 server: servers[0],
434 emails,
435 socketNotifications: adminNotifications,
436 token: servers[0].accessToken
437 }
438
439 adminBaseParamsServer2 = {
440 server: servers[1],
441 emails,
442 socketNotifications: adminNotificationsServer2,
443 token: servers[1].accessToken
444 }
445
446 userBaseParams = {
447 server: servers[0],
448 emails,
449 socketNotifications: userNotifications,
450 token: userToken1
451 }
452
453 currentCustomConfig = await servers[0].config.getCustomConfig()
454
455 const autoBlacklistTestsCustomConfig = {
456 ...currentCustomConfig,
457
458 autoBlacklist: {
459 videos: {
460 ofUsers: {
461 enabled: true
462 }
463 }
464 }
465 }
466
467 // enable transcoding otherwise own publish notification after transcoding not expected
468 autoBlacklistTestsCustomConfig.transcoding.enabled = true
469 await servers[0].config.updateCustomConfig({ newCustomConfig: autoBlacklistTestsCustomConfig })
470
471 await servers[0].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
472 await servers[1].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
473 })
474
475 it('Should send notification to moderators on new video with auto-blacklist', async function () {
476 this.timeout(120000)
477
478 videoName = 'video with auto-blacklist ' + buildUUID()
479 const video = await servers[0].videos.upload({ token: userToken1, attributes: { name: videoName } })
480 shortUUID = video.shortUUID
481 uuid = video.uuid
482
483 await waitJobs(servers)
484 await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName, checkType: 'presence' })
485 })
486
487 it('Should not send video publish notification if auto-blacklisted', async function () {
488 this.timeout(120000)
489
490 await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' })
491 })
492
493 it('Should not send a local user subscription notification if auto-blacklisted', async function () {
494 this.timeout(120000)
495
496 await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' })
497 })
498
499 it('Should not send a remote user subscription notification if auto-blacklisted', async function () {
500 await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'absence' })
501 })
502
503 it('Should send video published and unblacklist after video unblacklisted', async function () {
504 this.timeout(120000)
505
506 await servers[0].blacklist.remove({ videoId: uuid })
507
508 await waitJobs(servers)
509
510 // FIXME: Can't test as two notifications sent to same user and util only checks last one
511 // One notification might be better anyways
512 // await checkNewBlacklistOnMyVideo(userBaseParams, videoUUID, videoName, 'unblacklist')
513 // await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'presence')
514 })
515
516 it('Should send a local user subscription notification after removed from blacklist', async function () {
517 this.timeout(120000)
518
519 await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' })
520 })
521
522 it('Should send a remote user subscription notification after removed from blacklist', async function () {
523 this.timeout(120000)
524
525 await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' })
526 })
527
528 it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
529 this.timeout(120000)
530
531 const updateAt = new Date(new Date().getTime() + 1000000)
532
533 const name = 'video with auto-blacklist and future schedule ' + buildUUID()
534
535 const attributes = {
536 name,
537 privacy: VideoPrivacy.PRIVATE,
538 scheduleUpdate: {
539 updateAt: updateAt.toISOString(),
540 privacy: VideoPrivacy.PUBLIC
541 }
542 }
543
544 const { shortUUID, uuid } = await servers[0].videos.upload({ token: userToken1, attributes })
545
546 await servers[0].blacklist.remove({ videoId: uuid })
547
548 await waitJobs(servers)
549 await checkNewBlacklistOnMyVideo({ ...userBaseParams, shortUUID, videoName: name, blacklistType: 'unblacklist' })
550
551 // FIXME: Can't test absence as two notifications sent to same user and util only checks last one
552 // One notification might be better anyways
553 // await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
554
555 await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' })
556 await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' })
557 })
558
559 it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () {
560 this.timeout(120000)
561
562 // In 2 seconds
563 const updateAt = new Date(new Date().getTime() + 2000)
564
565 const name = 'video with schedule done and still auto-blacklisted ' + buildUUID()
566
567 const attributes = {
568 name,
569 privacy: VideoPrivacy.PRIVATE,
570 scheduleUpdate: {
571 updateAt: updateAt.toISOString(),
572 privacy: VideoPrivacy.PUBLIC
573 }
574 }
575
576 const { shortUUID } = await servers[0].videos.upload({ token: userToken1, attributes })
577
578 await wait(6000)
579 await checkVideoIsPublished({ ...userBaseParams, videoName: name, shortUUID, checkType: 'absence' })
580 await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName: name, shortUUID, checkType: 'absence' })
581 await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName: name, shortUUID, checkType: 'absence' })
582 })
583
584 it('Should not send a notification to moderators on new video without auto-blacklist', async function () {
585 this.timeout(120000)
586
587 const name = 'video without auto-blacklist ' + buildUUID()
588
589 // admin with blacklist right will not be auto-blacklisted
590 const { shortUUID } = await servers[0].videos.upload({ attributes: { name } })
591
592 await waitJobs(servers)
593 await checkVideoAutoBlacklistForModerators({ ...adminBaseParamsServer1, shortUUID, videoName: name, checkType: 'absence' })
594 })
595
596 after(async () => {
597 await servers[0].config.updateCustomConfig({ newCustomConfig: currentCustomConfig })
598
599 await servers[0].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
600 await servers[1].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
601 })
602 })
603
604 after(async function () {
605 MockSmtpServer.Instance.kill()
606
607 await cleanupTests(servers)
608 })
609})
diff --git a/packages/tests/src/api/notifications/notifications-api.ts b/packages/tests/src/api/notifications/notifications-api.ts
new file mode 100644
index 000000000..1c7461553
--- /dev/null
+++ b/packages/tests/src/api/notifications/notifications-api.ts
@@ -0,0 +1,206 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { UserNotification, UserNotificationSettingValue } from '@peertube/peertube-models'
5import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
6import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
7import {
8 prepareNotificationsTest,
9 CheckerBaseParams,
10 getAllNotificationsSettings,
11 checkNewVideoFromSubscription
12} from '@tests/shared/notifications.js'
13
14describe('Test notifications API', function () {
15 let server: PeerTubeServer
16 let userNotifications: UserNotification[] = []
17 let userToken: string
18 let emails: object[] = []
19
20 before(async function () {
21 this.timeout(120000)
22
23 const res = await prepareNotificationsTest(1)
24 emails = res.emails
25 userToken = res.userAccessToken
26 userNotifications = res.userNotifications
27 server = res.servers[0]
28
29 await server.subscriptions.add({ token: userToken, targetUri: 'root_channel@' + server.host })
30
31 for (let i = 0; i < 10; i++) {
32 await server.videos.randomUpload({ wait: false })
33 }
34
35 await waitJobs([ server ])
36 })
37
38 describe('Notification list & count', function () {
39
40 it('Should correctly list notifications', async function () {
41 const { data, total } = await server.notifications.list({ token: userToken, start: 0, count: 2 })
42
43 expect(data).to.have.lengthOf(2)
44 expect(total).to.equal(10)
45 })
46 })
47
48 describe('Mark as read', function () {
49
50 it('Should mark as read some notifications', async function () {
51 const { data } = await server.notifications.list({ token: userToken, start: 2, count: 3 })
52 const ids = data.map(n => n.id)
53
54 await server.notifications.markAsRead({ token: userToken, ids })
55 })
56
57 it('Should have the notifications marked as read', async function () {
58 const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10 })
59
60 expect(data[0].read).to.be.false
61 expect(data[1].read).to.be.false
62 expect(data[2].read).to.be.true
63 expect(data[3].read).to.be.true
64 expect(data[4].read).to.be.true
65 expect(data[5].read).to.be.false
66 })
67
68 it('Should only list read notifications', async function () {
69 const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: false })
70
71 for (const notification of data) {
72 expect(notification.read).to.be.true
73 }
74 })
75
76 it('Should only list unread notifications', async function () {
77 const { data } = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true })
78
79 for (const notification of data) {
80 expect(notification.read).to.be.false
81 }
82 })
83
84 it('Should mark as read all notifications', async function () {
85 await server.notifications.markAsReadAll({ token: userToken })
86
87 const body = await server.notifications.list({ token: userToken, start: 0, count: 10, unread: true })
88
89 expect(body.total).to.equal(0)
90 expect(body.data).to.have.lengthOf(0)
91 })
92 })
93
94 describe('Notification settings', function () {
95 let baseParams: CheckerBaseParams
96
97 before(() => {
98 baseParams = {
99 server,
100 emails,
101 socketNotifications: userNotifications,
102 token: userToken
103 }
104 })
105
106 it('Should not have notifications', async function () {
107 this.timeout(40000)
108
109 await server.notifications.updateMySettings({
110 token: userToken,
111 settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.NONE }
112 })
113
114 {
115 const info = await server.users.getMyInfo({ token: userToken })
116 expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE)
117 }
118
119 const { name, shortUUID } = await server.videos.randomUpload()
120
121 const check = { web: true, mail: true }
122 await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
123 })
124
125 it('Should only have web notifications', async function () {
126 this.timeout(20000)
127
128 await server.notifications.updateMySettings({
129 token: userToken,
130 settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.WEB }
131 })
132
133 {
134 const info = await server.users.getMyInfo({ token: userToken })
135 expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB)
136 }
137
138 const { name, shortUUID } = await server.videos.randomUpload()
139
140 {
141 const check = { mail: true, web: false }
142 await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
143 }
144
145 {
146 const check = { mail: false, web: true }
147 await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' })
148 }
149 })
150
151 it('Should only have mail notifications', async function () {
152 this.timeout(20000)
153
154 await server.notifications.updateMySettings({
155 token: userToken,
156 settings: { ...getAllNotificationsSettings(), newVideoFromSubscription: UserNotificationSettingValue.EMAIL }
157 })
158
159 {
160 const info = await server.users.getMyInfo({ token: userToken })
161 expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL)
162 }
163
164 const { name, shortUUID } = await server.videos.randomUpload()
165
166 {
167 const check = { mail: false, web: true }
168 await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'absence' })
169 }
170
171 {
172 const check = { mail: true, web: false }
173 await checkNewVideoFromSubscription({ ...baseParams, check, videoName: name, shortUUID, checkType: 'presence' })
174 }
175 })
176
177 it('Should have email and web notifications', async function () {
178 this.timeout(20000)
179
180 await server.notifications.updateMySettings({
181 token: userToken,
182 settings: {
183 ...getAllNotificationsSettings(),
184 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
185 }
186 })
187
188 {
189 const info = await server.users.getMyInfo({ token: userToken })
190 expect(info.notificationSettings.newVideoFromSubscription).to.equal(
191 UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
192 )
193 }
194
195 const { name, shortUUID } = await server.videos.randomUpload()
196
197 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
198 })
199 })
200
201 after(async function () {
202 MockSmtpServer.Instance.kill()
203
204 await cleanupTests([ server ])
205 })
206})
diff --git a/packages/tests/src/api/notifications/registrations-notifications.ts b/packages/tests/src/api/notifications/registrations-notifications.ts
new file mode 100644
index 000000000..1f166cb36
--- /dev/null
+++ b/packages/tests/src/api/notifications/registrations-notifications.ts
@@ -0,0 +1,83 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { UserNotification } from '@peertube/peertube-models'
4import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
5import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
6import { CheckerBaseParams, prepareNotificationsTest, checkUserRegistered, checkRegistrationRequest } from '@tests/shared/notifications.js'
7
8describe('Test registrations notifications', function () {
9 let server: PeerTubeServer
10 let userToken1: string
11
12 let userNotifications: UserNotification[] = []
13 let adminNotifications: UserNotification[] = []
14 let emails: object[] = []
15
16 let baseParams: CheckerBaseParams
17
18 before(async function () {
19 this.timeout(120000)
20
21 const res = await prepareNotificationsTest(1)
22
23 server = res.servers[0]
24 emails = res.emails
25 userToken1 = res.userAccessToken
26 adminNotifications = res.adminNotifications
27 userNotifications = res.userNotifications
28
29 baseParams = {
30 server,
31 emails,
32 socketNotifications: adminNotifications,
33 token: server.accessToken
34 }
35 })
36
37 describe('New direct registration for moderators', function () {
38
39 before(async function () {
40 await server.config.enableSignup(false)
41 })
42
43 it('Should send a notification only to moderators when a user registers on the instance', async function () {
44 this.timeout(50000)
45
46 await server.registrations.register({ username: 'user_10' })
47
48 await waitJobs([ server ])
49
50 await checkUserRegistered({ ...baseParams, username: 'user_10', checkType: 'presence' })
51
52 const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
53 await checkUserRegistered({ ...baseParams, ...userOverride, username: 'user_10', checkType: 'absence' })
54 })
55 })
56
57 describe('New registration request for moderators', function () {
58
59 before(async function () {
60 await server.config.enableSignup(true)
61 })
62
63 it('Should send a notification on new registration request', async function () {
64 this.timeout(50000)
65
66 const registrationReason = 'my reason'
67 await server.registrations.requestRegistration({ username: 'user_11', registrationReason })
68
69 await waitJobs([ server ])
70
71 await checkRegistrationRequest({ ...baseParams, username: 'user_11', registrationReason, checkType: 'presence' })
72
73 const userOverride = { socketNotifications: userNotifications, token: userToken1, check: { web: true, mail: false } }
74 await checkRegistrationRequest({ ...baseParams, ...userOverride, username: 'user_11', registrationReason, checkType: 'absence' })
75 })
76 })
77
78 after(async function () {
79 MockSmtpServer.Instance.kill()
80
81 await cleanupTests([ server ])
82 })
83})
diff --git a/packages/tests/src/api/notifications/user-notifications.ts b/packages/tests/src/api/notifications/user-notifications.ts
new file mode 100644
index 000000000..4c03cdb47
--- /dev/null
+++ b/packages/tests/src/api/notifications/user-notifications.ts
@@ -0,0 +1,574 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { UserNotification, UserNotificationType, VideoPrivacy, VideoStudioTask } from '@peertube/peertube-models'
6import { buildUUID } from '@peertube/peertube-node-utils'
7import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
8import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
9import {
10 prepareNotificationsTest,
11 CheckerBaseParams,
12 checkNewVideoFromSubscription,
13 checkVideoIsPublished,
14 checkVideoStudioEditionIsFinished,
15 checkMyVideoImportIsFinished,
16 checkNewActorFollow
17} from '@tests/shared/notifications.js'
18import { FIXTURE_URLS } from '@tests/shared/tests.js'
19import { uploadRandomVideoOnServers } from '@tests/shared/videos.js'
20
21describe('Test user notifications', function () {
22 let servers: PeerTubeServer[] = []
23 let userAccessToken: string
24
25 let userNotifications: UserNotification[] = []
26 let adminNotifications: UserNotification[] = []
27 let adminNotificationsServer2: UserNotification[] = []
28 let emails: object[] = []
29
30 let channelId: number
31
32 before(async function () {
33 this.timeout(120000)
34
35 const res = await prepareNotificationsTest(3)
36 emails = res.emails
37 userAccessToken = res.userAccessToken
38 servers = res.servers
39 userNotifications = res.userNotifications
40 adminNotifications = res.adminNotifications
41 adminNotificationsServer2 = res.adminNotificationsServer2
42 channelId = res.channelId
43 })
44
45 describe('New video from my subscription notification', function () {
46 let baseParams: CheckerBaseParams
47
48 before(() => {
49 baseParams = {
50 server: servers[0],
51 emails,
52 socketNotifications: userNotifications,
53 token: userAccessToken
54 }
55 })
56
57 it('Should not send notifications if the user does not follow the video publisher', async function () {
58 this.timeout(50000)
59
60 await uploadRandomVideoOnServers(servers, 1)
61
62 const notification = await servers[0].notifications.getLatest({ token: userAccessToken })
63 expect(notification).to.be.undefined
64
65 expect(emails).to.have.lengthOf(0)
66 expect(userNotifications).to.have.lengthOf(0)
67 })
68
69 it('Should send a new video notification if the user follows the local video publisher', async function () {
70 this.timeout(15000)
71
72 await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[0].host })
73 await waitJobs(servers)
74
75 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1)
76 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
77 })
78
79 it('Should send a new video notification from a remote account', async function () {
80 this.timeout(150000) // Server 2 has transcoding enabled
81
82 await servers[0].subscriptions.add({ token: userAccessToken, targetUri: 'root_channel@' + servers[1].host })
83 await waitJobs(servers)
84
85 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2)
86 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
87 })
88
89 it('Should send a new video notification on a scheduled publication', async function () {
90 this.timeout(50000)
91
92 // In 2 seconds
93 const updateAt = new Date(new Date().getTime() + 2000)
94
95 const data = {
96 privacy: VideoPrivacy.PRIVATE,
97 scheduleUpdate: {
98 updateAt: updateAt.toISOString(),
99 privacy: VideoPrivacy.PUBLIC
100 }
101 }
102 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
103
104 await wait(6000)
105 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
106 })
107
108 it('Should send a new video notification on a remote scheduled publication', async function () {
109 this.timeout(100000)
110
111 // In 2 seconds
112 const updateAt = new Date(new Date().getTime() + 2000)
113
114 const data = {
115 privacy: VideoPrivacy.PRIVATE,
116 scheduleUpdate: {
117 updateAt: updateAt.toISOString(),
118 privacy: VideoPrivacy.PUBLIC
119 }
120 }
121 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
122 await waitJobs(servers)
123
124 await wait(6000)
125 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
126 })
127
128 it('Should not send a notification before the video is published', async function () {
129 this.timeout(150000)
130
131 const updateAt = new Date(new Date().getTime() + 1000000)
132
133 const data = {
134 privacy: VideoPrivacy.PRIVATE,
135 scheduleUpdate: {
136 updateAt: updateAt.toISOString(),
137 privacy: VideoPrivacy.PUBLIC
138 }
139 }
140 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
141
142 await wait(6000)
143 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
144 })
145
146 it('Should send a new video notification when a video becomes public', async function () {
147 this.timeout(50000)
148
149 const data = { privacy: VideoPrivacy.PRIVATE }
150 const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
151
152 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
153
154 await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
155
156 await waitJobs(servers)
157 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
158 })
159
160 it('Should send a new video notification when a remote video becomes public', async function () {
161 this.timeout(120000)
162
163 const data = { privacy: VideoPrivacy.PRIVATE }
164 const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
165
166 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
167
168 await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
169
170 await waitJobs(servers)
171 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
172 })
173
174 it('Should not send a new video notification when a video becomes unlisted', async function () {
175 this.timeout(50000)
176
177 const data = { privacy: VideoPrivacy.PRIVATE }
178 const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 1, data)
179
180 await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
181
182 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
183 })
184
185 it('Should not send a new video notification when a remote video becomes unlisted', async function () {
186 this.timeout(100000)
187
188 const data = { privacy: VideoPrivacy.PRIVATE }
189 const { name, uuid, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
190
191 await servers[1].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
192
193 await waitJobs(servers)
194 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
195 })
196
197 it('Should send a new video notification after a video import', async function () {
198 this.timeout(100000)
199
200 const name = 'video import ' + buildUUID()
201
202 const attributes = {
203 name,
204 channelId,
205 privacy: VideoPrivacy.PUBLIC,
206 targetUrl: FIXTURE_URLS.goodVideo
207 }
208 const { video } = await servers[0].imports.importVideo({ attributes })
209
210 await waitJobs(servers)
211
212 await checkNewVideoFromSubscription({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
213 })
214 })
215
216 describe('My video is published', function () {
217 let baseParams: CheckerBaseParams
218
219 before(() => {
220 baseParams = {
221 server: servers[1],
222 emails,
223 socketNotifications: adminNotificationsServer2,
224 token: servers[1].accessToken
225 }
226 })
227
228 it('Should not send a notification if transcoding is not enabled', async function () {
229 this.timeout(50000)
230
231 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 1)
232 await waitJobs(servers)
233
234 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
235 })
236
237 it('Should not send a notification if the wait transcoding is false', async function () {
238 this.timeout(100_000)
239
240 await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: false })
241 await waitJobs(servers)
242
243 const notification = await servers[0].notifications.getLatest({ token: userAccessToken })
244 if (notification) {
245 expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED)
246 }
247 })
248
249 it('Should send a notification even if the video is not transcoded in other resolutions', async function () {
250 this.timeout(100_000)
251
252 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' })
253 await waitJobs(servers)
254
255 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
256 })
257
258 it('Should send a notification with a transcoded video', async function () {
259 this.timeout(100_000)
260
261 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
262 await waitJobs(servers)
263
264 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
265 })
266
267 it('Should send a notification when an imported video is transcoded', async function () {
268 this.timeout(120000)
269
270 const name = 'video import ' + buildUUID()
271
272 const attributes = {
273 name,
274 channelId,
275 privacy: VideoPrivacy.PUBLIC,
276 targetUrl: FIXTURE_URLS.goodVideo,
277 waitTranscoding: true
278 }
279 const { video } = await servers[1].imports.importVideo({ attributes })
280
281 await waitJobs(servers)
282 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' })
283 })
284
285 it('Should send a notification when the scheduled update has been proceeded', async function () {
286 this.timeout(70000)
287
288 // In 2 seconds
289 const updateAt = new Date(new Date().getTime() + 2000)
290
291 const data = {
292 privacy: VideoPrivacy.PRIVATE,
293 scheduleUpdate: {
294 updateAt: updateAt.toISOString(),
295 privacy: VideoPrivacy.PUBLIC
296 }
297 }
298 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
299
300 await wait(6000)
301 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
302 })
303
304 it('Should not send a notification before the video is published', async function () {
305 this.timeout(150000)
306
307 const updateAt = new Date(new Date().getTime() + 1000000)
308
309 const data = {
310 privacy: VideoPrivacy.PRIVATE,
311 scheduleUpdate: {
312 updateAt: updateAt.toISOString(),
313 privacy: VideoPrivacy.PUBLIC
314 }
315 }
316 const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, data)
317
318 await wait(6000)
319 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'absence' })
320 })
321 })
322
323 describe('My live replay is published', function () {
324
325 let baseParams: CheckerBaseParams
326
327 before(() => {
328 baseParams = {
329 server: servers[1],
330 emails,
331 socketNotifications: adminNotificationsServer2,
332 token: servers[1].accessToken
333 }
334 })
335
336 it('Should send a notification is a live replay of a non permanent live is published', async function () {
337 this.timeout(120000)
338
339 const { shortUUID } = await servers[1].live.create({
340 fields: {
341 name: 'non permanent live',
342 privacy: VideoPrivacy.PUBLIC,
343 channelId: servers[1].store.channel.id,
344 saveReplay: true,
345 replaySettings: { privacy: VideoPrivacy.PUBLIC },
346 permanentLive: false
347 }
348 })
349
350 const ffmpegCommand = await servers[1].live.sendRTMPStreamInVideo({ videoId: shortUUID })
351
352 await waitJobs(servers)
353 await servers[1].live.waitUntilPublished({ videoId: shortUUID })
354
355 await stopFfmpeg(ffmpegCommand)
356 await servers[1].live.waitUntilReplacedByReplay({ videoId: shortUUID })
357
358 await waitJobs(servers)
359 await checkVideoIsPublished({ ...baseParams, videoName: 'non permanent live', shortUUID, checkType: 'presence' })
360 })
361
362 it('Should send a notification is a live replay of a permanent live is published', async function () {
363 this.timeout(120000)
364
365 const { shortUUID } = await servers[1].live.create({
366 fields: {
367 name: 'permanent live',
368 privacy: VideoPrivacy.PUBLIC,
369 channelId: servers[1].store.channel.id,
370 saveReplay: true,
371 replaySettings: { privacy: VideoPrivacy.PUBLIC },
372 permanentLive: true
373 }
374 })
375
376 const ffmpegCommand = await servers[1].live.sendRTMPStreamInVideo({ videoId: shortUUID })
377
378 await waitJobs(servers)
379 await servers[1].live.waitUntilPublished({ videoId: shortUUID })
380
381 const liveDetails = await servers[1].videos.get({ id: shortUUID })
382
383 await stopFfmpeg(ffmpegCommand)
384
385 await servers[1].live.waitUntilWaiting({ videoId: shortUUID })
386 await waitJobs(servers)
387
388 const video = await findExternalSavedVideo(servers[1], liveDetails)
389 expect(video).to.exist
390
391 await checkVideoIsPublished({ ...baseParams, videoName: video.name, shortUUID: video.shortUUID, checkType: 'presence' })
392 })
393 })
394
395 describe('Video studio', function () {
396 let baseParams: CheckerBaseParams
397
398 before(() => {
399 baseParams = {
400 server: servers[1],
401 emails,
402 socketNotifications: adminNotificationsServer2,
403 token: servers[1].accessToken
404 }
405 })
406
407 it('Should send a notification after studio edition', async function () {
408 this.timeout(240000)
409
410 const { name, shortUUID, id } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
411
412 await waitJobs(servers)
413 await checkVideoIsPublished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
414
415 const tasks: VideoStudioTask[] = [
416 {
417 name: 'cut',
418 options: {
419 start: 0,
420 end: 1
421 }
422 }
423 ]
424 await servers[1].videoStudio.createEditionTasks({ videoId: id, tasks })
425 await waitJobs(servers)
426
427 await checkVideoStudioEditionIsFinished({ ...baseParams, videoName: name, shortUUID, checkType: 'presence' })
428 })
429 })
430
431 describe('My video is imported', function () {
432 let baseParams: CheckerBaseParams
433
434 before(() => {
435 baseParams = {
436 server: servers[0],
437 emails,
438 socketNotifications: adminNotifications,
439 token: servers[0].accessToken
440 }
441 })
442
443 it('Should send a notification when the video import failed', async function () {
444 this.timeout(70000)
445
446 const name = 'video import ' + buildUUID()
447
448 const attributes = {
449 name,
450 channelId,
451 privacy: VideoPrivacy.PRIVATE,
452 targetUrl: FIXTURE_URLS.badVideo
453 }
454 const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes })
455
456 await waitJobs(servers)
457
458 const url = FIXTURE_URLS.badVideo
459 await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: false, checkType: 'presence' })
460 })
461
462 it('Should send a notification when the video import succeeded', async function () {
463 this.timeout(70000)
464
465 const name = 'video import ' + buildUUID()
466
467 const attributes = {
468 name,
469 channelId,
470 privacy: VideoPrivacy.PRIVATE,
471 targetUrl: FIXTURE_URLS.goodVideo
472 }
473 const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes })
474
475 await waitJobs(servers)
476
477 const url = FIXTURE_URLS.goodVideo
478 await checkMyVideoImportIsFinished({ ...baseParams, videoName: name, shortUUID, url, success: true, checkType: 'presence' })
479 })
480 })
481
482 describe('New actor follow', function () {
483 let baseParams: CheckerBaseParams
484 const myChannelName = 'super channel name'
485 const myUserName = 'super user name'
486
487 before(async function () {
488 baseParams = {
489 server: servers[0],
490 emails,
491 socketNotifications: userNotifications,
492 token: userAccessToken
493 }
494
495 await servers[0].users.updateMe({ displayName: 'super root name' })
496
497 await servers[0].users.updateMe({
498 token: userAccessToken,
499 displayName: myUserName
500 })
501
502 await servers[1].users.updateMe({ displayName: 'super root 2 name' })
503
504 await servers[0].channels.update({
505 token: userAccessToken,
506 channelName: 'user_1_channel',
507 attributes: { displayName: myChannelName }
508 })
509 })
510
511 it('Should notify when a local channel is following one of our channel', async function () {
512 this.timeout(50000)
513
514 await servers[0].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
515 await waitJobs(servers)
516
517 await checkNewActorFollow({
518 ...baseParams,
519 followType: 'channel',
520 followerName: 'root',
521 followerDisplayName: 'super root name',
522 followingDisplayName: myChannelName,
523 checkType: 'presence'
524 })
525
526 await servers[0].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
527 })
528
529 it('Should notify when a remote channel is following one of our channel', async function () {
530 this.timeout(50000)
531
532 await servers[1].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host })
533 await waitJobs(servers)
534
535 await checkNewActorFollow({
536 ...baseParams,
537 followType: 'channel',
538 followerName: 'root',
539 followerDisplayName: 'super root 2 name',
540 followingDisplayName: myChannelName,
541 checkType: 'presence'
542 })
543
544 await servers[1].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host })
545 })
546
547 // PeerTube does not support account -> account follows
548 // it('Should notify when a local account is following one of our channel', async function () {
549 // this.timeout(50000)
550 //
551 // await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@' + servers[0].host)
552 //
553 // await waitJobs(servers)
554 //
555 // await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence')
556 // })
557
558 // it('Should notify when a remote account is following one of our channel', async function () {
559 // this.timeout(50000)
560 //
561 // await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@' + servers[0].host)
562 //
563 // await waitJobs(servers)
564 //
565 // await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence')
566 // })
567 })
568
569 after(async function () {
570 MockSmtpServer.Instance.kill()
571
572 await cleanupTests(servers)
573 })
574})
diff --git a/packages/tests/src/api/object-storage/index.ts b/packages/tests/src/api/object-storage/index.ts
new file mode 100644
index 000000000..51d2a29a0
--- /dev/null
+++ b/packages/tests/src/api/object-storage/index.ts
@@ -0,0 +1,4 @@
1export * from './live.js'
2export * from './video-imports.js'
3export * from './video-static-file-privacy.js'
4export * from './videos.js'
diff --git a/packages/tests/src/api/object-storage/live.ts b/packages/tests/src/api/object-storage/live.ts
new file mode 100644
index 000000000..c8c214af5
--- /dev/null
+++ b/packages/tests/src/api/object-storage/live.ts
@@ -0,0 +1,314 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
5import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 findExternalSavedVideo,
11 makeRawRequest,
12 ObjectStorageCommand,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 setDefaultVideoChannel,
16 stopFfmpeg,
17 waitJobs,
18 waitUntilLivePublishedOnAllServers,
19 waitUntilLiveReplacedByReplayOnAllServers,
20 waitUntilLiveWaitingOnAllServers
21} from '@peertube/peertube-server-commands'
22import { expectStartWith } from '@tests/shared/checks.js'
23import { testLiveVideoResolutions } from '@tests/shared/live.js'
24import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js'
25import { SQLCommand } from '@tests/shared/sql-command.js'
26
27async function createLive (server: PeerTubeServer, permanent: boolean) {
28 const attributes: LiveVideoCreate = {
29 channelId: server.store.channel.id,
30 privacy: VideoPrivacy.PUBLIC,
31 name: 'my super live',
32 saveReplay: true,
33 replaySettings: { privacy: VideoPrivacy.PUBLIC },
34 permanentLive: permanent
35 }
36
37 const { uuid } = await server.live.create({ fields: attributes })
38
39 return uuid
40}
41
42async function checkFilesExist (options: {
43 servers: PeerTubeServer[]
44 videoUUID: string
45 numberOfFiles: number
46 objectStorage: ObjectStorageCommand
47}) {
48 const { servers, videoUUID, numberOfFiles, objectStorage } = options
49
50 for (const server of servers) {
51 const video = await server.videos.get({ id: videoUUID })
52
53 expect(video.files).to.have.lengthOf(0)
54 expect(video.streamingPlaylists).to.have.lengthOf(1)
55
56 const files = video.streamingPlaylists[0].files
57 expect(files).to.have.lengthOf(numberOfFiles)
58
59 for (const file of files) {
60 expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
61
62 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
63 }
64 }
65}
66
67async function checkFilesCleanup (options: {
68 server: PeerTubeServer
69 videoUUID: string
70 resolutions: number[]
71 objectStorage: ObjectStorageCommand
72}) {
73 const { server, videoUUID, resolutions, objectStorage } = options
74
75 const resolutionFiles = resolutions.map((_value, i) => `${i}.m3u8`)
76
77 for (const playlistName of [ 'master.m3u8' ].concat(resolutionFiles)) {
78 await server.live.getPlaylistFile({
79 videoUUID,
80 playlistName,
81 expectedStatus: HttpStatusCode.NOT_FOUND_404,
82 objectStorage
83 })
84 }
85
86 await server.live.getSegmentFile({
87 videoUUID,
88 playlistNumber: 0,
89 segment: 0,
90 objectStorage,
91 expectedStatus: HttpStatusCode.NOT_FOUND_404
92 })
93}
94
95describe('Object storage for lives', function () {
96 if (areMockObjectStorageTestsDisabled()) return
97
98 let servers: PeerTubeServer[]
99 let sqlCommandServer1: SQLCommand
100 const objectStorage = new ObjectStorageCommand()
101
102 before(async function () {
103 this.timeout(120000)
104
105 await objectStorage.prepareDefaultMockBuckets()
106 servers = await createMultipleServers(2, objectStorage.getDefaultMockConfig())
107
108 await setAccessTokensToServers(servers)
109 await setDefaultVideoChannel(servers)
110 await doubleFollow(servers[0], servers[1])
111
112 await servers[0].config.enableTranscoding()
113
114 sqlCommandServer1 = new SQLCommand(servers[0])
115 })
116
117 describe('Without live transcoding', function () {
118 let videoUUID: string
119
120 before(async function () {
121 await servers[0].config.enableLive({ transcoding: false })
122
123 videoUUID = await createLive(servers[0], false)
124 })
125
126 it('Should create a live and publish it on object storage', async function () {
127 this.timeout(220000)
128
129 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUID })
130 await waitUntilLivePublishedOnAllServers(servers, videoUUID)
131
132 await testLiveVideoResolutions({
133 originServer: servers[0],
134 sqlCommand: sqlCommandServer1,
135 servers,
136 liveVideoId: videoUUID,
137 resolutions: [ 720 ],
138 transcoded: false,
139 objectStorage
140 })
141
142 await stopFfmpeg(ffmpegCommand)
143 })
144
145 it('Should have saved the replay on object storage', async function () {
146 this.timeout(220000)
147
148 await waitUntilLiveReplacedByReplayOnAllServers(servers, videoUUID)
149 await waitJobs(servers)
150
151 await checkFilesExist({ servers, videoUUID, numberOfFiles: 1, objectStorage })
152 })
153
154 it('Should have cleaned up live files from object storage', async function () {
155 await checkFilesCleanup({ server: servers[0], videoUUID, resolutions: [ 720 ], objectStorage })
156 })
157 })
158
159 describe('With live transcoding', function () {
160 const resolutions = [ 720, 480, 360, 240, 144 ]
161
162 before(async function () {
163 await servers[0].config.enableLive({ transcoding: true })
164 })
165
166 describe('Normal replay', function () {
167 let videoUUIDNonPermanent: string
168
169 before(async function () {
170 videoUUIDNonPermanent = await createLive(servers[0], false)
171 })
172
173 it('Should create a live and publish it on object storage', async function () {
174 this.timeout(240000)
175
176 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDNonPermanent })
177 await waitUntilLivePublishedOnAllServers(servers, videoUUIDNonPermanent)
178
179 await testLiveVideoResolutions({
180 originServer: servers[0],
181 sqlCommand: sqlCommandServer1,
182 servers,
183 liveVideoId: videoUUIDNonPermanent,
184 resolutions,
185 transcoded: true,
186 objectStorage
187 })
188
189 await stopFfmpeg(ffmpegCommand)
190 })
191
192 it('Should have saved the replay on object storage', async function () {
193 this.timeout(220000)
194
195 await waitUntilLiveReplacedByReplayOnAllServers(servers, videoUUIDNonPermanent)
196 await waitJobs(servers)
197
198 await checkFilesExist({ servers, videoUUID: videoUUIDNonPermanent, numberOfFiles: 5, objectStorage })
199 })
200
201 it('Should have cleaned up live files from object storage', async function () {
202 await checkFilesCleanup({ server: servers[0], videoUUID: videoUUIDNonPermanent, resolutions, objectStorage })
203 })
204 })
205
206 describe('Permanent replay', function () {
207 let videoUUIDPermanent: string
208
209 before(async function () {
210 videoUUIDPermanent = await createLive(servers[0], true)
211 })
212
213 it('Should create a live and publish it on object storage', async function () {
214 this.timeout(240000)
215
216 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent })
217 await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent)
218
219 await testLiveVideoResolutions({
220 originServer: servers[0],
221 sqlCommand: sqlCommandServer1,
222 servers,
223 liveVideoId: videoUUIDPermanent,
224 resolutions,
225 transcoded: true,
226 objectStorage
227 })
228
229 await stopFfmpeg(ffmpegCommand)
230 })
231
232 it('Should have saved the replay on object storage', async function () {
233 this.timeout(220000)
234
235 await waitUntilLiveWaitingOnAllServers(servers, videoUUIDPermanent)
236 await waitJobs(servers)
237
238 const videoLiveDetails = await servers[0].videos.get({ id: videoUUIDPermanent })
239 const replay = await findExternalSavedVideo(servers[0], videoLiveDetails)
240
241 await checkFilesExist({ servers, videoUUID: replay.uuid, numberOfFiles: 5, objectStorage })
242 })
243
244 it('Should have cleaned up live files from object storage', async function () {
245 await checkFilesCleanup({ server: servers[0], videoUUID: videoUUIDPermanent, resolutions, objectStorage })
246 })
247 })
248 })
249
250 describe('With object storage base url', function () {
251 const mockObjectStorageProxy = new MockObjectStorageProxy()
252 let baseMockUrl: string
253
254 before(async function () {
255 this.timeout(120000)
256
257 const port = await mockObjectStorageProxy.initialize()
258 const bucketName = objectStorage.getMockStreamingPlaylistsBucketName()
259 baseMockUrl = `http://127.0.0.1:${port}/${bucketName}`
260
261 await objectStorage.prepareDefaultMockBuckets()
262
263 const config = {
264 object_storage: {
265 enabled: true,
266 endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(),
267 region: ObjectStorageCommand.getMockRegion(),
268
269 credentials: ObjectStorageCommand.getMockCredentialsConfig(),
270
271 streaming_playlists: {
272 bucket_name: bucketName,
273 prefix: '',
274 base_url: baseMockUrl
275 }
276 }
277 }
278
279 await servers[0].kill()
280 await servers[0].run(config)
281
282 await servers[0].config.enableLive({ transcoding: true, resolutions: 'min' })
283 })
284
285 it('Should publish a live and replace the base url', async function () {
286 this.timeout(240000)
287
288 const videoUUIDPermanent = await createLive(servers[0], true)
289
290 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: videoUUIDPermanent })
291 await waitUntilLivePublishedOnAllServers(servers, videoUUIDPermanent)
292
293 await testLiveVideoResolutions({
294 originServer: servers[0],
295 sqlCommand: sqlCommandServer1,
296 servers,
297 liveVideoId: videoUUIDPermanent,
298 resolutions: [ 720 ],
299 transcoded: true,
300 objectStorage,
301 objectStorageBaseUrl: baseMockUrl
302 })
303
304 await stopFfmpeg(ffmpegCommand)
305 })
306 })
307
308 after(async function () {
309 await sqlCommandServer1.cleanup()
310 await objectStorage.cleanupMock()
311
312 await cleanupTests(servers)
313 })
314})
diff --git a/packages/tests/src/api/object-storage/video-imports.ts b/packages/tests/src/api/object-storage/video-imports.ts
new file mode 100644
index 000000000..43f769842
--- /dev/null
+++ b/packages/tests/src/api/object-storage/video-imports.ts
@@ -0,0 +1,112 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { expectStartWith } from '@tests/shared/checks.js'
5import { FIXTURE_URLS } from '@tests/shared/tests.js'
6import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
7import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
8import {
9 cleanupTests,
10 createSingleServer,
11 makeRawRequest,
12 ObjectStorageCommand,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 setDefaultVideoChannel,
16 waitJobs
17} from '@peertube/peertube-server-commands'
18
19async function importVideo (server: PeerTubeServer) {
20 const attributes = {
21 name: 'import 2',
22 privacy: VideoPrivacy.PUBLIC,
23 channelId: server.store.channel.id,
24 targetUrl: FIXTURE_URLS.goodVideo720
25 }
26
27 const { video: { uuid } } = await server.imports.importVideo({ attributes })
28
29 return uuid
30}
31
32describe('Object storage for video import', function () {
33 if (areMockObjectStorageTestsDisabled()) return
34
35 let server: PeerTubeServer
36 const objectStorage = new ObjectStorageCommand()
37
38 before(async function () {
39 this.timeout(120000)
40
41 await objectStorage.prepareDefaultMockBuckets()
42
43 server = await createSingleServer(1, objectStorage.getDefaultMockConfig())
44
45 await setAccessTokensToServers([ server ])
46 await setDefaultVideoChannel([ server ])
47
48 await server.config.enableImports()
49 })
50
51 describe('Without transcoding', async function () {
52
53 before(async function () {
54 await server.config.disableTranscoding()
55 })
56
57 it('Should import a video and have sent it to object storage', async function () {
58 this.timeout(120000)
59
60 const uuid = await importVideo(server)
61 await waitJobs(server)
62
63 const video = await server.videos.get({ id: uuid })
64
65 expect(video.files).to.have.lengthOf(1)
66 expect(video.streamingPlaylists).to.have.lengthOf(0)
67
68 const fileUrl = video.files[0].fileUrl
69 expectStartWith(fileUrl, objectStorage.getMockWebVideosBaseUrl())
70
71 await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.OK_200 })
72 })
73 })
74
75 describe('With transcoding', async function () {
76
77 before(async function () {
78 await server.config.enableTranscoding()
79 })
80
81 it('Should import a video and have sent it to object storage', async function () {
82 this.timeout(120000)
83
84 const uuid = await importVideo(server)
85 await waitJobs(server)
86
87 const video = await server.videos.get({ id: uuid })
88
89 expect(video.files).to.have.lengthOf(5)
90 expect(video.streamingPlaylists).to.have.lengthOf(1)
91 expect(video.streamingPlaylists[0].files).to.have.lengthOf(5)
92
93 for (const file of video.files) {
94 expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl())
95
96 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
97 }
98
99 for (const file of video.streamingPlaylists[0].files) {
100 expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
101
102 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
103 }
104 })
105 })
106
107 after(async function () {
108 await objectStorage.cleanupMock()
109
110 await cleanupTests([ server ])
111 })
112})
diff --git a/packages/tests/src/api/object-storage/video-static-file-privacy.ts b/packages/tests/src/api/object-storage/video-static-file-privacy.ts
new file mode 100644
index 000000000..cf6e9b4b9
--- /dev/null
+++ b/packages/tests/src/api/object-storage/video-static-file-privacy.ts
@@ -0,0 +1,573 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { basename } from 'path'
5import { getAllFiles, getHLS } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
7import { areScalewayObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
8import {
9 cleanupTests,
10 createSingleServer,
11 findExternalSavedVideo,
12 makeRawRequest,
13 ObjectStorageCommand,
14 PeerTubeServer,
15 sendRTMPStream,
16 setAccessTokensToServers,
17 setDefaultVideoChannel,
18 stopFfmpeg,
19 waitJobs
20} from '@peertube/peertube-server-commands'
21import { expectStartWith } from '@tests/shared/checks.js'
22import { SQLCommand } from '@tests/shared/sql-command.js'
23import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js'
24
25function extractFilenameFromUrl (url: string) {
26 const parts = basename(url).split(':')
27
28 return parts[parts.length - 1]
29}
30
31describe('Object storage for video static file privacy', function () {
32 // We need real world object storage to check ACL
33 if (areScalewayObjectStorageTestsDisabled()) return
34
35 let server: PeerTubeServer
36 let sqlCommand: SQLCommand
37 let userToken: string
38
39 // ---------------------------------------------------------------------------
40
41 async function checkPrivateVODFiles (uuid: string) {
42 const video = await server.videos.getWithToken({ id: uuid })
43
44 for (const file of video.files) {
45 expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/web-videos/private/')
46
47 await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
48 }
49
50 for (const file of getAllFiles(video)) {
51 const internalFileUrl = await sqlCommand.getInternalFileUrl(file.id)
52 expectStartWith(internalFileUrl, ObjectStorageCommand.getScalewayBaseUrl())
53 await makeRawRequest({ url: internalFileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
54 }
55
56 const hls = getHLS(video)
57
58 if (hls) {
59 for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
60 expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
61 }
62
63 await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
64 await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
65
66 for (const file of hls.files) {
67 expectStartWith(file.fileUrl, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
68
69 await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
70 }
71 }
72 }
73
74 async function checkPublicVODFiles (uuid: string) {
75 const video = await server.videos.getWithToken({ id: uuid })
76
77 for (const file of getAllFiles(video)) {
78 expectStartWith(file.fileUrl, ObjectStorageCommand.getScalewayBaseUrl())
79
80 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
81 }
82
83 const hls = getHLS(video)
84
85 if (hls) {
86 expectStartWith(hls.playlistUrl, ObjectStorageCommand.getScalewayBaseUrl())
87 expectStartWith(hls.segmentsSha256Url, ObjectStorageCommand.getScalewayBaseUrl())
88
89 await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
90 await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
91 }
92 }
93
94 // ---------------------------------------------------------------------------
95
96 before(async function () {
97 this.timeout(120000)
98
99 server = await createSingleServer(1, ObjectStorageCommand.getDefaultScalewayConfig({ serverNumber: 1 }))
100 await setAccessTokensToServers([ server ])
101 await setDefaultVideoChannel([ server ])
102
103 await server.config.enableMinimumTranscoding()
104
105 userToken = await server.users.generateUserAndToken('user1')
106
107 sqlCommand = new SQLCommand(server)
108 })
109
110 describe('VOD', function () {
111 let privateVideoUUID: string
112 let publicVideoUUID: string
113 let passwordProtectedVideoUUID: string
114 let userPrivateVideoUUID: string
115
116 const correctPassword = 'my super password'
117 const correctPasswordHeader = { 'x-peertube-video-password': correctPassword }
118 const incorrectPasswordHeader = { 'x-peertube-video-password': correctPassword + 'toto' }
119
120 // ---------------------------------------------------------------------------
121
122 async function getSampleFileUrls (videoId: string) {
123 const video = await server.videos.getWithToken({ id: videoId })
124
125 return {
126 webVideoFile: video.files[0].fileUrl,
127 hlsFile: getHLS(video).files[0].fileUrl
128 }
129 }
130
131 // ---------------------------------------------------------------------------
132
133 it('Should upload a private video and have appropriate object storage ACL', async function () {
134 this.timeout(120000)
135
136 {
137 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
138 privateVideoUUID = uuid
139 }
140
141 {
142 const { uuid } = await server.videos.quickUpload({ name: 'user video', token: userToken, privacy: VideoPrivacy.PRIVATE })
143 userPrivateVideoUUID = uuid
144 }
145
146 await waitJobs([ server ])
147
148 await checkPrivateVODFiles(privateVideoUUID)
149 })
150
151 it('Should upload a password protected video and have appropriate object storage ACL', async function () {
152 this.timeout(120000)
153
154 {
155 const { uuid } = await server.videos.quickUpload({
156 name: 'video',
157 privacy: VideoPrivacy.PASSWORD_PROTECTED,
158 videoPasswords: [ correctPassword ]
159 })
160 passwordProtectedVideoUUID = uuid
161 }
162 await waitJobs([ server ])
163
164 await checkPrivateVODFiles(passwordProtectedVideoUUID)
165 })
166
167 it('Should upload a public video and have appropriate object storage ACL', async function () {
168 this.timeout(120000)
169
170 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.UNLISTED })
171 await waitJobs([ server ])
172
173 publicVideoUUID = uuid
174
175 await checkPublicVODFiles(publicVideoUUID)
176 })
177
178 it('Should not get files without appropriate OAuth token', async function () {
179 this.timeout(60000)
180
181 const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID)
182
183 await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
184 await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
185
186 await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
187 await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
188 })
189
190 it('Should not get files without appropriate password or appropriate OAuth token', async function () {
191 this.timeout(60000)
192
193 const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID)
194
195 await makeRawRequest({ url: webVideoFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
196 await makeRawRequest({
197 url: webVideoFile,
198 token: null,
199 headers: incorrectPasswordHeader,
200 expectedStatus: HttpStatusCode.FORBIDDEN_403
201 })
202 await makeRawRequest({ url: webVideoFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
203 await makeRawRequest({
204 url: webVideoFile,
205 token: null,
206 headers: correctPasswordHeader,
207 expectedStatus: HttpStatusCode.OK_200
208 })
209
210 await makeRawRequest({ url: hlsFile, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
211 await makeRawRequest({
212 url: hlsFile,
213 token: null,
214 headers: incorrectPasswordHeader,
215 expectedStatus: HttpStatusCode.FORBIDDEN_403
216 })
217 await makeRawRequest({ url: hlsFile, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
218 await makeRawRequest({
219 url: hlsFile,
220 token: null,
221 headers: correctPasswordHeader,
222 expectedStatus: HttpStatusCode.OK_200
223 })
224 })
225
226 it('Should not get HLS file of another video', async function () {
227 this.timeout(60000)
228
229 const privateVideo = await server.videos.getWithToken({ id: privateVideoUUID })
230 const hlsFilename = basename(getHLS(privateVideo).files[0].fileUrl)
231
232 const badUrl = server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + userPrivateVideoUUID + '/' + hlsFilename
233 const goodUrl = server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + privateVideoUUID + '/' + hlsFilename
234
235 await makeRawRequest({ url: badUrl, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
236 await makeRawRequest({ url: goodUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
237 })
238
239 it('Should correctly check OAuth, video file token of private video', async function () {
240 this.timeout(60000)
241
242 const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID })
243 const goodVideoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID })
244
245 const { webVideoFile, hlsFile } = await getSampleFileUrls(privateVideoUUID)
246
247 for (const url of [ webVideoFile, hlsFile ]) {
248 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
249 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
250 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
251
252 await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
253 await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 })
254
255 }
256 })
257
258 it('Should correctly check OAuth, video file token or video password of password protected video', async function () {
259 this.timeout(60000)
260
261 const badVideoFileToken = await server.videoToken.getVideoFileToken({ token: userToken, videoId: userPrivateVideoUUID })
262 const goodVideoFileToken = await server.videoToken.getVideoFileToken({
263 videoId: passwordProtectedVideoUUID,
264 videoPassword: correctPassword
265 })
266
267 const { webVideoFile, hlsFile } = await getSampleFileUrls(passwordProtectedVideoUUID)
268
269 for (const url of [ hlsFile, webVideoFile ]) {
270 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
271 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
272 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
273
274 await makeRawRequest({ url, query: { videoFileToken: badVideoFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
275 await makeRawRequest({ url, query: { videoFileToken: goodVideoFileToken }, expectedStatus: HttpStatusCode.OK_200 })
276
277 await makeRawRequest({
278 url,
279 headers: incorrectPasswordHeader,
280 expectedStatus: HttpStatusCode.FORBIDDEN_403
281 })
282 await makeRawRequest({ url, headers: correctPasswordHeader, expectedStatus: HttpStatusCode.OK_200 })
283 }
284 })
285
286 it('Should reinject video file token', async function () {
287 this.timeout(120000)
288
289 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID })
290
291 await checkVideoFileTokenReinjection({
292 server,
293 videoUUID: privateVideoUUID,
294 videoFileToken,
295 resolutions: [ 240, 720 ],
296 isLive: false
297 })
298 })
299
300 it('Should update public video to private', async function () {
301 this.timeout(60000)
302
303 await server.videos.update({ id: publicVideoUUID, attributes: { privacy: VideoPrivacy.INTERNAL } })
304
305 await checkPrivateVODFiles(publicVideoUUID)
306 })
307
308 it('Should update private video to public', async function () {
309 this.timeout(60000)
310
311 await server.videos.update({ id: publicVideoUUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
312
313 await checkPublicVODFiles(publicVideoUUID)
314 })
315 })
316
317 describe('Live', function () {
318 let normalLiveId: string
319 let normalLive: LiveVideo
320
321 let permanentLiveId: string
322 let permanentLive: LiveVideo
323
324 let passwordProtectedLiveId: string
325 let passwordProtectedLive: LiveVideo
326
327 const correctPassword = 'my super password'
328
329 let unrelatedFileToken: string
330
331 // ---------------------------------------------------------------------------
332
333 async function checkLiveFiles (live: LiveVideo, liveId: string, videoPassword?: string) {
334 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
335 await server.live.waitUntilPublished({ videoId: liveId })
336
337 const video = videoPassword
338 ? await server.videos.getWithPassword({ id: liveId, password: videoPassword })
339 : await server.videos.getWithToken({ id: liveId })
340
341 const fileToken = videoPassword
342 ? await server.videoToken.getVideoFileToken({ token: null, videoId: video.uuid, videoPassword })
343 : await server.videoToken.getVideoFileToken({ videoId: video.uuid })
344
345 const hls = video.streamingPlaylists[0]
346
347 for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
348 expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
349
350 await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
351 await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
352
353 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
354 await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
355 if (videoPassword) {
356 await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 })
357 }
358 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
359 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
360 await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
361 if (videoPassword) {
362 await makeRawRequest({
363 url,
364 headers: { 'x-peertube-video-password': 'incorrectPassword' },
365 expectedStatus: HttpStatusCode.FORBIDDEN_403
366 })
367 }
368 }
369
370 await stopFfmpeg(ffmpegCommand)
371 }
372
373 async function checkReplay (replay: VideoDetails) {
374 const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid })
375
376 const hls = replay.streamingPlaylists[0]
377 expect(hls.files).to.not.have.lengthOf(0)
378
379 for (const file of hls.files) {
380 await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
381 await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
382
383 await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
384 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
385 await makeRawRequest({
386 url: file.fileUrl,
387 query: { videoFileToken: unrelatedFileToken },
388 expectedStatus: HttpStatusCode.FORBIDDEN_403
389 })
390 }
391
392 for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
393 expectStartWith(url, server.url + '/object-storage-proxy/streaming-playlists/hls/private/')
394
395 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
396 await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
397
398 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
399 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
400 await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
401 }
402 }
403
404 // ---------------------------------------------------------------------------
405
406 before(async function () {
407 await server.config.enableMinimumTranscoding()
408
409 const { uuid } = await server.videos.quickUpload({ name: 'another video' })
410 unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
411
412 await server.config.enableLive({
413 allowReplay: true,
414 transcoding: true,
415 resolutions: 'min'
416 })
417
418 {
419 const { video, live } = await server.live.quickCreate({
420 saveReplay: true,
421 permanentLive: false,
422 privacy: VideoPrivacy.PRIVATE
423 })
424 normalLiveId = video.uuid
425 normalLive = live
426 }
427
428 {
429 const { video, live } = await server.live.quickCreate({
430 saveReplay: true,
431 permanentLive: true,
432 privacy: VideoPrivacy.PRIVATE
433 })
434 permanentLiveId = video.uuid
435 permanentLive = live
436 }
437
438 {
439 const { video, live } = await server.live.quickCreate({
440 saveReplay: false,
441 permanentLive: false,
442 privacy: VideoPrivacy.PASSWORD_PROTECTED,
443 videoPasswords: [ correctPassword ]
444 })
445 passwordProtectedLiveId = video.uuid
446 passwordProtectedLive = live
447 }
448 })
449
450 it('Should create a private normal live and have a private static path', async function () {
451 this.timeout(240000)
452
453 await checkLiveFiles(normalLive, normalLiveId)
454 })
455
456 it('Should create a private permanent live and have a private static path', async function () {
457 this.timeout(240000)
458
459 await checkLiveFiles(permanentLive, permanentLiveId)
460 })
461
462 it('Should create a password protected live and have a private static path', async function () {
463 this.timeout(240000)
464
465 await checkLiveFiles(passwordProtectedLive, passwordProtectedLiveId, correctPassword)
466 })
467
468 it('Should reinject video file token in permanent live', async function () {
469 this.timeout(240000)
470
471 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey })
472 await server.live.waitUntilPublished({ videoId: permanentLiveId })
473
474 const video = await server.videos.getWithToken({ id: permanentLiveId })
475 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid })
476
477 await checkVideoFileTokenReinjection({
478 server,
479 videoUUID: permanentLiveId,
480 videoFileToken,
481 resolutions: [ 720 ],
482 isLive: true
483 })
484
485 await stopFfmpeg(ffmpegCommand)
486 })
487
488 it('Should have created a replay of the normal live with a private static path', async function () {
489 this.timeout(240000)
490
491 await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId })
492
493 const replay = await server.videos.getWithToken({ id: normalLiveId })
494 await checkReplay(replay)
495 })
496
497 it('Should have created a replay of the permanent live with a private static path', async function () {
498 this.timeout(240000)
499
500 await server.live.waitUntilWaiting({ videoId: permanentLiveId })
501 await waitJobs([ server ])
502
503 const live = await server.videos.getWithToken({ id: permanentLiveId })
504 const replayFromList = await findExternalSavedVideo(server, live)
505 const replay = await server.videos.getWithToken({ id: replayFromList.id })
506
507 await checkReplay(replay)
508 })
509 })
510
511 describe('With private files proxy disabled and public ACL for private files', function () {
512 let videoUUID: string
513
514 before(async function () {
515 this.timeout(240000)
516
517 await server.kill()
518
519 const config = ObjectStorageCommand.getDefaultScalewayConfig({
520 serverNumber: 1,
521 enablePrivateProxy: false,
522 privateACL: 'public-read'
523 })
524 await server.run(config)
525
526 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
527 videoUUID = uuid
528
529 await waitJobs([ server ])
530 })
531
532 it('Should display object storage path for a private video and be able to access them', async function () {
533 this.timeout(60000)
534
535 await checkPublicVODFiles(videoUUID)
536 })
537
538 it('Should not be able to access object storage proxy', async function () {
539 const privateVideo = await server.videos.getWithToken({ id: videoUUID })
540 const webVideoFilename = extractFilenameFromUrl(privateVideo.files[0].fileUrl)
541 const hlsFilename = extractFilenameFromUrl(getHLS(privateVideo).files[0].fileUrl)
542
543 await makeRawRequest({
544 url: server.url + '/object-storage-proxy/web-videos/private/' + webVideoFilename,
545 token: server.accessToken,
546 expectedStatus: HttpStatusCode.BAD_REQUEST_400
547 })
548
549 await makeRawRequest({
550 url: server.url + '/object-storage-proxy/streaming-playlists/hls/private/' + videoUUID + '/' + hlsFilename,
551 token: server.accessToken,
552 expectedStatus: HttpStatusCode.BAD_REQUEST_400
553 })
554 })
555 })
556
557 after(async function () {
558 this.timeout(240000)
559
560 const { data } = await server.videos.listAllForAdmin()
561
562 for (const v of data) {
563 await server.videos.remove({ id: v.uuid })
564 }
565
566 for (const v of data) {
567 await server.servers.waitUntilLog('Removed files of video ' + v.url)
568 }
569
570 await sqlCommand.cleanup()
571 await cleanupTests([ server ])
572 })
573})
diff --git a/packages/tests/src/api/object-storage/videos.ts b/packages/tests/src/api/object-storage/videos.ts
new file mode 100644
index 000000000..66bca5cc8
--- /dev/null
+++ b/packages/tests/src/api/object-storage/videos.ts
@@ -0,0 +1,434 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import bytes from 'bytes'
4import { expect } from 'chai'
5import { stat } from 'fs/promises'
6import merge from 'lodash-es/merge.js'
7import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models'
8import { areMockObjectStorageTestsDisabled, sha1 } from '@peertube/peertube-node-utils'
9import {
10 cleanupTests,
11 createMultipleServers,
12 createSingleServer,
13 doubleFollow,
14 killallServers,
15 makeRawRequest,
16 ObjectStorageCommand,
17 PeerTubeServer,
18 setAccessTokensToServers,
19 waitJobs
20} from '@peertube/peertube-server-commands'
21import { expectStartWith, expectLogDoesNotContain } from '@tests/shared/checks.js'
22import { checkTmpIsEmpty } from '@tests/shared/directories.js'
23import { generateHighBitrateVideo } from '@tests/shared/generate.js'
24import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js'
25import { SQLCommand } from '@tests/shared/sql-command.js'
26import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js'
27
28async function checkFiles (options: {
29 server: PeerTubeServer
30 originServer: PeerTubeServer
31 originSQLCommand: SQLCommand
32
33 video: VideoDetails
34
35 baseMockUrl?: string
36
37 playlistBucket: string
38 playlistPrefix?: string
39
40 webVideoBucket: string
41 webVideoPrefix?: string
42}) {
43 const {
44 server,
45 originServer,
46 originSQLCommand,
47 video,
48 playlistBucket,
49 webVideoBucket,
50 baseMockUrl,
51 playlistPrefix,
52 webVideoPrefix
53 } = options
54
55 let allFiles = video.files
56
57 for (const file of video.files) {
58 const baseUrl = baseMockUrl
59 ? `${baseMockUrl}/${webVideoBucket}/`
60 : `http://${webVideoBucket}.${ObjectStorageCommand.getMockEndpointHost()}/`
61
62 const prefix = webVideoPrefix || ''
63 const start = baseUrl + prefix
64
65 expectStartWith(file.fileUrl, start)
66
67 const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 })
68 const location = res.headers['location']
69 expectStartWith(location, start)
70
71 await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 })
72 }
73
74 const hls = video.streamingPlaylists[0]
75
76 if (hls) {
77 allFiles = allFiles.concat(hls.files)
78
79 const baseUrl = baseMockUrl
80 ? `${baseMockUrl}/${playlistBucket}/`
81 : `http://${playlistBucket}.${ObjectStorageCommand.getMockEndpointHost()}/`
82
83 const prefix = playlistPrefix || ''
84 const start = baseUrl + prefix
85
86 expectStartWith(hls.playlistUrl, start)
87 expectStartWith(hls.segmentsSha256Url, start)
88
89 await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
90
91 const resSha = await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
92 expect(JSON.stringify(resSha.body)).to.not.throw
93
94 let i = 0
95 for (const file of hls.files) {
96 expectStartWith(file.fileUrl, start)
97
98 const res = await makeRawRequest({ url: file.fileDownloadUrl, expectedStatus: HttpStatusCode.FOUND_302 })
99 const location = res.headers['location']
100 expectStartWith(location, start)
101
102 await makeRawRequest({ url: location, expectedStatus: HttpStatusCode.OK_200 })
103
104 if (originServer.internalServerNumber === server.internalServerNumber) {
105 const infohash = sha1(`${2 + hls.playlistUrl}+V${i}`)
106 const dbInfohashes = await originSQLCommand.getPlaylistInfohash(hls.id)
107
108 expect(dbInfohashes).to.include(infohash)
109 }
110
111 i++
112 }
113 }
114
115 for (const file of allFiles) {
116 await checkWebTorrentWorks(file.magnetUri)
117
118 const res = await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
119 expect(res.body).to.have.length.above(100)
120 }
121
122 return allFiles.map(f => f.fileUrl)
123}
124
125function runTestSuite (options: {
126 fixture?: string
127
128 maxUploadPart?: string
129
130 playlistBucket: string
131 playlistPrefix?: string
132
133 webVideoBucket: string
134 webVideoPrefix?: string
135
136 useMockBaseUrl?: boolean
137}) {
138 const mockObjectStorageProxy = new MockObjectStorageProxy()
139 const { fixture } = options
140 let baseMockUrl: string
141
142 let servers: PeerTubeServer[]
143 let sqlCommands: SQLCommand[] = []
144 const objectStorage = new ObjectStorageCommand()
145
146 let keptUrls: string[] = []
147
148 const uuidsToDelete: string[] = []
149 let deletedUrls: string[] = []
150
151 before(async function () {
152 this.timeout(240000)
153
154 const port = await mockObjectStorageProxy.initialize()
155 baseMockUrl = options.useMockBaseUrl
156 ? `http://127.0.0.1:${port}`
157 : undefined
158
159 await objectStorage.createMockBucket(options.playlistBucket)
160 await objectStorage.createMockBucket(options.webVideoBucket)
161
162 const config = {
163 object_storage: {
164 enabled: true,
165 endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(),
166 region: ObjectStorageCommand.getMockRegion(),
167
168 credentials: ObjectStorageCommand.getMockCredentialsConfig(),
169
170 max_upload_part: options.maxUploadPart || '5MB',
171
172 streaming_playlists: {
173 bucket_name: options.playlistBucket,
174 prefix: options.playlistPrefix,
175 base_url: baseMockUrl
176 ? `${baseMockUrl}/${options.playlistBucket}`
177 : undefined
178 },
179
180 web_videos: {
181 bucket_name: options.webVideoBucket,
182 prefix: options.webVideoPrefix,
183 base_url: baseMockUrl
184 ? `${baseMockUrl}/${options.webVideoBucket}`
185 : undefined
186 }
187 }
188 }
189
190 servers = await createMultipleServers(2, config)
191
192 await setAccessTokensToServers(servers)
193 await doubleFollow(servers[0], servers[1])
194
195 for (const server of servers) {
196 const { uuid } = await server.videos.quickUpload({ name: 'video to keep' })
197 await waitJobs(servers)
198
199 const files = await server.videos.listFiles({ id: uuid })
200 keptUrls = keptUrls.concat(files.map(f => f.fileUrl))
201 }
202
203 sqlCommands = servers.map(s => new SQLCommand(s))
204 })
205
206 it('Should upload a video and move it to the object storage without transcoding', async function () {
207 this.timeout(40000)
208
209 const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1', fixture })
210 uuidsToDelete.push(uuid)
211
212 await waitJobs(servers)
213
214 for (const server of servers) {
215 const video = await server.videos.get({ id: uuid })
216 const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
217
218 deletedUrls = deletedUrls.concat(files)
219 }
220 })
221
222 it('Should upload a video and move it to the object storage with transcoding', async function () {
223 this.timeout(120000)
224
225 const { uuid } = await servers[1].videos.quickUpload({ name: 'video 2', fixture })
226 uuidsToDelete.push(uuid)
227
228 await waitJobs(servers)
229
230 for (const server of servers) {
231 const video = await server.videos.get({ id: uuid })
232 const files = await checkFiles({ ...options, server, originServer: servers[0], originSQLCommand: sqlCommands[0], video, baseMockUrl })
233
234 deletedUrls = deletedUrls.concat(files)
235 }
236 })
237
238 it('Should fetch correctly all the files', async function () {
239 for (const url of deletedUrls.concat(keptUrls)) {
240 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
241 }
242 })
243
244 it('Should correctly delete the files', async function () {
245 await servers[0].videos.remove({ id: uuidsToDelete[0] })
246 await servers[1].videos.remove({ id: uuidsToDelete[1] })
247
248 await waitJobs(servers)
249
250 for (const url of deletedUrls) {
251 await makeRawRequest({ url, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
252 }
253 })
254
255 it('Should have kept other files', async function () {
256 for (const url of keptUrls) {
257 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
258 }
259 })
260
261 it('Should have an empty tmp directory', async function () {
262 for (const server of servers) {
263 await checkTmpIsEmpty(server)
264 }
265 })
266
267 it('Should not have downloaded files from object storage', async function () {
268 for (const server of servers) {
269 await expectLogDoesNotContain(server, 'from object storage')
270 }
271 })
272
273 after(async function () {
274 await mockObjectStorageProxy.terminate()
275 await objectStorage.cleanupMock()
276
277 for (const sqlCommand of sqlCommands) {
278 await sqlCommand.cleanup()
279 }
280
281 await cleanupTests(servers)
282 })
283}
284
285describe('Object storage for videos', function () {
286 if (areMockObjectStorageTestsDisabled()) return
287
288 const objectStorage = new ObjectStorageCommand()
289
290 describe('Test config', function () {
291 let server: PeerTubeServer
292
293 const baseConfig = objectStorage.getDefaultMockConfig()
294
295 const badCredentials = {
296 access_key_id: 'AKIAIOSFODNN7EXAMPLE',
297 secret_access_key: 'aJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
298 }
299
300 it('Should fail with same bucket names without prefix', function (done) {
301 const config = merge({}, baseConfig, {
302 object_storage: {
303 streaming_playlists: {
304 bucket_name: 'aaa'
305 },
306
307 web_videos: {
308 bucket_name: 'aaa'
309 }
310 }
311 })
312
313 createSingleServer(1, config)
314 .then(() => done(new Error('Did not throw')))
315 .catch(() => done())
316 })
317
318 it('Should fail with bad credentials', async function () {
319 this.timeout(60000)
320
321 await objectStorage.prepareDefaultMockBuckets()
322
323 const config = merge({}, baseConfig, {
324 object_storage: {
325 credentials: badCredentials
326 }
327 })
328
329 server = await createSingleServer(1, config)
330 await setAccessTokensToServers([ server ])
331
332 const { uuid } = await server.videos.quickUpload({ name: 'video' })
333
334 await waitJobs([ server ], { skipDelayed: true })
335 const video = await server.videos.get({ id: uuid })
336
337 expectStartWith(video.files[0].fileUrl, server.url)
338
339 await killallServers([ server ])
340 })
341
342 it('Should succeed with credentials from env', async function () {
343 this.timeout(60000)
344
345 await objectStorage.prepareDefaultMockBuckets()
346
347 const config = merge({}, baseConfig, {
348 object_storage: {
349 credentials: {
350 access_key_id: '',
351 secret_access_key: ''
352 }
353 }
354 })
355
356 const goodCredentials = ObjectStorageCommand.getMockCredentialsConfig()
357
358 server = await createSingleServer(1, config, {
359 env: {
360 AWS_ACCESS_KEY_ID: goodCredentials.access_key_id,
361 AWS_SECRET_ACCESS_KEY: goodCredentials.secret_access_key
362 }
363 })
364
365 await setAccessTokensToServers([ server ])
366
367 const { uuid } = await server.videos.quickUpload({ name: 'video' })
368
369 await waitJobs([ server ], { skipDelayed: true })
370 const video = await server.videos.get({ id: uuid })
371
372 expectStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
373 })
374
375 after(async function () {
376 await objectStorage.cleanupMock()
377
378 await cleanupTests([ server ])
379 })
380 })
381
382 describe('Test simple object storage', function () {
383 runTestSuite({
384 playlistBucket: objectStorage.getMockBucketName('streaming-playlists'),
385 webVideoBucket: objectStorage.getMockBucketName('web-videos')
386 })
387 })
388
389 describe('Test object storage with prefix', function () {
390 runTestSuite({
391 playlistBucket: objectStorage.getMockBucketName('mybucket'),
392 webVideoBucket: objectStorage.getMockBucketName('mybucket'),
393
394 playlistPrefix: 'streaming-playlists_',
395 webVideoPrefix: 'webvideo_'
396 })
397 })
398
399 describe('Test object storage with prefix and base URL', function () {
400 runTestSuite({
401 playlistBucket: objectStorage.getMockBucketName('mybucket'),
402 webVideoBucket: objectStorage.getMockBucketName('mybucket'),
403
404 playlistPrefix: 'streaming-playlists/',
405 webVideoPrefix: 'webvideo/',
406
407 useMockBaseUrl: true
408 })
409 })
410
411 describe('Test object storage with file bigger than upload part', function () {
412 let fixture: string
413 const maxUploadPart = '5MB'
414
415 before(async function () {
416 this.timeout(120000)
417
418 fixture = await generateHighBitrateVideo()
419
420 const { size } = await stat(fixture)
421
422 if (bytes.parse(maxUploadPart) > size) {
423 throw Error(`Fixture file is too small (${size}) to make sense for this test.`)
424 }
425 })
426
427 runTestSuite({
428 maxUploadPart,
429 playlistBucket: objectStorage.getMockBucketName('streaming-playlists'),
430 webVideoBucket: objectStorage.getMockBucketName('web-videos'),
431 fixture
432 })
433 })
434})
diff --git a/packages/tests/src/api/redundancy/index.ts b/packages/tests/src/api/redundancy/index.ts
new file mode 100644
index 000000000..f6b70c8af
--- /dev/null
+++ b/packages/tests/src/api/redundancy/index.ts
@@ -0,0 +1,3 @@
1import './redundancy-constraints.js'
2import './redundancy.js'
3import './manage-redundancy.js'
diff --git a/packages/tests/src/api/redundancy/manage-redundancy.ts b/packages/tests/src/api/redundancy/manage-redundancy.ts
new file mode 100644
index 000000000..14556e26c
--- /dev/null
+++ b/packages/tests/src/api/redundancy/manage-redundancy.ts
@@ -0,0 +1,324 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createMultipleServers,
7 doubleFollow,
8 PeerTubeServer,
9 RedundancyCommand,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13import { VideoPrivacy, VideoRedundanciesTarget } from '@peertube/peertube-models'
14
15describe('Test manage videos redundancy', function () {
16 const targets: VideoRedundanciesTarget[] = [ 'my-videos', 'remote-videos' ]
17
18 let servers: PeerTubeServer[]
19 let video1Server2UUID: string
20 let video2Server2UUID: string
21 let redundanciesToRemove: number[] = []
22
23 let commands: RedundancyCommand[]
24
25 before(async function () {
26 this.timeout(120000)
27
28 const config = {
29 transcoding: {
30 hls: {
31 enabled: true
32 }
33 },
34 redundancy: {
35 videos: {
36 check_interval: '1 second',
37 strategies: [
38 {
39 strategy: 'recently-added',
40 min_lifetime: '1 hour',
41 size: '10MB',
42 min_views: 0
43 }
44 ]
45 }
46 }
47 }
48 servers = await createMultipleServers(3, config)
49
50 // Get the access tokens
51 await setAccessTokensToServers(servers)
52
53 commands = servers.map(s => s.redundancy)
54
55 {
56 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
57 video1Server2UUID = uuid
58 }
59
60 {
61 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2' } })
62 video2Server2UUID = uuid
63 }
64
65 await waitJobs(servers)
66
67 // Server 1 and server 2 follow each other
68 await doubleFollow(servers[0], servers[1])
69 await doubleFollow(servers[0], servers[2])
70 await commands[0].updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
71
72 await waitJobs(servers)
73 })
74
75 it('Should not have redundancies on server 3', async function () {
76 for (const target of targets) {
77 const body = await commands[2].listVideos({ target })
78
79 expect(body.total).to.equal(0)
80 expect(body.data).to.have.lengthOf(0)
81 }
82 })
83
84 it('Should correctly list followings by redundancy', async function () {
85 const body = await servers[0].follows.getFollowings({ sort: '-redundancyAllowed' })
86
87 expect(body.total).to.equal(2)
88 expect(body.data).to.have.lengthOf(2)
89
90 expect(body.data[0].following.host).to.equal(servers[1].host)
91 expect(body.data[1].following.host).to.equal(servers[2].host)
92 })
93
94 it('Should not have "remote-videos" redundancies on server 2', async function () {
95 this.timeout(120000)
96
97 await waitJobs(servers)
98 await servers[0].servers.waitUntilLog('Duplicated ', 10)
99 await waitJobs(servers)
100
101 const body = await commands[1].listVideos({ target: 'remote-videos' })
102
103 expect(body.total).to.equal(0)
104 expect(body.data).to.have.lengthOf(0)
105 })
106
107 it('Should have "my-videos" redundancies on server 2', async function () {
108 this.timeout(120000)
109
110 const body = await commands[1].listVideos({ target: 'my-videos' })
111 expect(body.total).to.equal(2)
112
113 const videos = body.data
114 expect(videos).to.have.lengthOf(2)
115
116 const videos1 = videos.find(v => v.uuid === video1Server2UUID)
117 const videos2 = videos.find(v => v.uuid === video2Server2UUID)
118
119 expect(videos1.name).to.equal('video 1 server 2')
120 expect(videos2.name).to.equal('video 2 server 2')
121
122 expect(videos1.redundancies.files).to.have.lengthOf(4)
123 expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1)
124
125 const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists)
126
127 for (const r of redundancies) {
128 expect(r.strategy).to.be.null
129 expect(r.fileUrl).to.exist
130 expect(r.createdAt).to.exist
131 expect(r.updatedAt).to.exist
132 expect(r.expiresOn).to.exist
133 }
134 })
135
136 it('Should not have "my-videos" redundancies on server 1', async function () {
137 const body = await commands[0].listVideos({ target: 'my-videos' })
138
139 expect(body.total).to.equal(0)
140 expect(body.data).to.have.lengthOf(0)
141 })
142
143 it('Should have "remote-videos" redundancies on server 1', async function () {
144 this.timeout(120000)
145
146 const body = await commands[0].listVideos({ target: 'remote-videos' })
147 expect(body.total).to.equal(2)
148
149 const videos = body.data
150 expect(videos).to.have.lengthOf(2)
151
152 const videos1 = videos.find(v => v.uuid === video1Server2UUID)
153 const videos2 = videos.find(v => v.uuid === video2Server2UUID)
154
155 expect(videos1.name).to.equal('video 1 server 2')
156 expect(videos2.name).to.equal('video 2 server 2')
157
158 expect(videos1.redundancies.files).to.have.lengthOf(4)
159 expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1)
160
161 const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists)
162
163 for (const r of redundancies) {
164 expect(r.strategy).to.equal('recently-added')
165 expect(r.fileUrl).to.exist
166 expect(r.createdAt).to.exist
167 expect(r.updatedAt).to.exist
168 expect(r.expiresOn).to.exist
169 }
170 })
171
172 it('Should correctly paginate and sort results', async function () {
173 {
174 const body = await commands[0].listVideos({
175 target: 'remote-videos',
176 sort: 'name',
177 start: 0,
178 count: 2
179 })
180
181 const videos = body.data
182 expect(videos[0].name).to.equal('video 1 server 2')
183 expect(videos[1].name).to.equal('video 2 server 2')
184 }
185
186 {
187 const body = await commands[0].listVideos({
188 target: 'remote-videos',
189 sort: '-name',
190 start: 0,
191 count: 2
192 })
193
194 const videos = body.data
195 expect(videos[0].name).to.equal('video 2 server 2')
196 expect(videos[1].name).to.equal('video 1 server 2')
197 }
198
199 {
200 const body = await commands[0].listVideos({
201 target: 'remote-videos',
202 sort: '-name',
203 start: 1,
204 count: 1
205 })
206
207 expect(body.data[0].name).to.equal('video 1 server 2')
208 }
209 })
210
211 it('Should manually add a redundancy and list it', async function () {
212 this.timeout(120000)
213
214 const uuid = (await servers[1].videos.quickUpload({ name: 'video 3 server 2', privacy: VideoPrivacy.UNLISTED })).uuid
215 await waitJobs(servers)
216 const videoId = await servers[0].videos.getId({ uuid })
217
218 await commands[0].addVideo({ videoId })
219
220 await waitJobs(servers)
221 await servers[0].servers.waitUntilLog('Duplicated ', 15)
222 await waitJobs(servers)
223
224 {
225 const body = await commands[0].listVideos({
226 target: 'remote-videos',
227 sort: '-name',
228 start: 0,
229 count: 5
230 })
231
232 const video = body.data[0]
233
234 expect(video.name).to.equal('video 3 server 2')
235 expect(video.redundancies.files).to.have.lengthOf(4)
236 expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
237
238 const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
239
240 for (const r of redundancies) {
241 redundanciesToRemove.push(r.id)
242
243 expect(r.strategy).to.equal('manual')
244 expect(r.fileUrl).to.exist
245 expect(r.createdAt).to.exist
246 expect(r.updatedAt).to.exist
247 expect(r.expiresOn).to.be.null
248 }
249 }
250
251 const body = await commands[1].listVideos({
252 target: 'my-videos',
253 sort: '-name',
254 start: 0,
255 count: 5
256 })
257
258 const video = body.data[0]
259 expect(video.name).to.equal('video 3 server 2')
260 expect(video.redundancies.files).to.have.lengthOf(4)
261 expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
262
263 const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
264
265 for (const r of redundancies) {
266 expect(r.strategy).to.be.null
267 expect(r.fileUrl).to.exist
268 expect(r.createdAt).to.exist
269 expect(r.updatedAt).to.exist
270 expect(r.expiresOn).to.be.null
271 }
272 })
273
274 it('Should manually remove a redundancy and remove it from the list', async function () {
275 this.timeout(120000)
276
277 for (const redundancyId of redundanciesToRemove) {
278 await commands[0].removeVideo({ redundancyId })
279 }
280
281 {
282 const body = await commands[0].listVideos({
283 target: 'remote-videos',
284 sort: '-name',
285 start: 0,
286 count: 5
287 })
288
289 const videos = body.data
290
291 expect(videos).to.have.lengthOf(2)
292
293 const video = videos[0]
294 expect(video.name).to.equal('video 2 server 2')
295 expect(video.redundancies.files).to.have.lengthOf(4)
296 expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
297
298 const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
299
300 redundanciesToRemove = redundancies.map(r => r.id)
301 }
302 })
303
304 it('Should remove another (auto) redundancy', async function () {
305 for (const redundancyId of redundanciesToRemove) {
306 await commands[0].removeVideo({ redundancyId })
307 }
308
309 const body = await commands[0].listVideos({
310 target: 'remote-videos',
311 sort: '-name',
312 start: 0,
313 count: 5
314 })
315
316 const videos = body.data
317 expect(videos).to.have.lengthOf(1)
318 expect(videos[0].name).to.equal('video 1 server 2')
319 })
320
321 after(async function () {
322 await cleanupTests(servers)
323 })
324})
diff --git a/packages/tests/src/api/redundancy/redundancy-constraints.ts b/packages/tests/src/api/redundancy/redundancy-constraints.ts
new file mode 100644
index 000000000..24966b270
--- /dev/null
+++ b/packages/tests/src/api/redundancy/redundancy-constraints.ts
@@ -0,0 +1,191 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { VideoPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 killallServers,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test redundancy constraints', function () {
15 let remoteServer: PeerTubeServer
16 let localServer: PeerTubeServer
17 let servers: PeerTubeServer[]
18
19 const remoteServerConfig = {
20 redundancy: {
21 videos: {
22 check_interval: '1 second',
23 strategies: [
24 {
25 strategy: 'recently-added',
26 min_lifetime: '1 hour',
27 size: '100MB',
28 min_views: 0
29 }
30 ]
31 }
32 }
33 }
34
35 async function uploadWrapper (videoName: string) {
36 // Wait for transcoding
37 const { id } = await localServer.videos.upload({ attributes: { name: 'to transcode', privacy: VideoPrivacy.PRIVATE } })
38 await waitJobs([ localServer ])
39
40 // Update video to schedule a federation
41 await localServer.videos.update({ id, attributes: { name: videoName, privacy: VideoPrivacy.PUBLIC } })
42 }
43
44 async function getTotalRedundanciesLocalServer () {
45 const body = await localServer.redundancy.listVideos({ target: 'my-videos' })
46
47 return body.total
48 }
49
50 async function getTotalRedundanciesRemoteServer () {
51 const body = await remoteServer.redundancy.listVideos({ target: 'remote-videos' })
52
53 return body.total
54 }
55
56 before(async function () {
57 this.timeout(120000)
58
59 {
60 remoteServer = await createSingleServer(1, remoteServerConfig)
61 }
62
63 {
64 const config = {
65 remote_redundancy: {
66 videos: {
67 accept_from: 'nobody'
68 }
69 }
70 }
71 localServer = await createSingleServer(2, config)
72 }
73
74 servers = [ remoteServer, localServer ]
75
76 // Get the access tokens
77 await setAccessTokensToServers(servers)
78
79 await localServer.videos.upload({ attributes: { name: 'video 1 server 2' } })
80
81 await waitJobs(servers)
82
83 // Server 1 and server 2 follow each other
84 await remoteServer.follows.follow({ hosts: [ localServer.url ] })
85 await waitJobs(servers)
86 await remoteServer.redundancy.updateRedundancy({ host: localServer.host, redundancyAllowed: true })
87
88 await waitJobs(servers)
89 })
90
91 it('Should have redundancy on server 1 but not on server 2 with a nobody filter', async function () {
92 this.timeout(120000)
93
94 await waitJobs(servers)
95 await remoteServer.servers.waitUntilLog('Duplicated ', 5)
96 await waitJobs(servers)
97
98 {
99 const total = await getTotalRedundanciesRemoteServer()
100 expect(total).to.equal(1)
101 }
102
103 {
104 const total = await getTotalRedundanciesLocalServer()
105 expect(total).to.equal(0)
106 }
107 })
108
109 it('Should have redundancy on server 1 and on server 2 with an anybody filter', async function () {
110 this.timeout(120000)
111
112 const config = {
113 remote_redundancy: {
114 videos: {
115 accept_from: 'anybody'
116 }
117 }
118 }
119 await killallServers([ localServer ])
120 await localServer.run(config)
121
122 await uploadWrapper('video 2 server 2')
123
124 await remoteServer.servers.waitUntilLog('Duplicated ', 10)
125 await waitJobs(servers)
126
127 {
128 const total = await getTotalRedundanciesRemoteServer()
129 expect(total).to.equal(2)
130 }
131
132 {
133 const total = await getTotalRedundanciesLocalServer()
134 expect(total).to.equal(1)
135 }
136 })
137
138 it('Should have redundancy on server 1 but not on server 2 with a followings filter', async function () {
139 this.timeout(120000)
140
141 const config = {
142 remote_redundancy: {
143 videos: {
144 accept_from: 'followings'
145 }
146 }
147 }
148 await killallServers([ localServer ])
149 await localServer.run(config)
150
151 await uploadWrapper('video 3 server 2')
152
153 await remoteServer.servers.waitUntilLog('Duplicated ', 15)
154 await waitJobs(servers)
155
156 {
157 const total = await getTotalRedundanciesRemoteServer()
158 expect(total).to.equal(3)
159 }
160
161 {
162 const total = await getTotalRedundanciesLocalServer()
163 expect(total).to.equal(1)
164 }
165 })
166
167 it('Should have redundancy on server 1 and on server 2 with followings filter now server 2 follows server 1', async function () {
168 this.timeout(120000)
169
170 await localServer.follows.follow({ hosts: [ remoteServer.url ] })
171 await waitJobs(servers)
172
173 await uploadWrapper('video 4 server 2')
174 await remoteServer.servers.waitUntilLog('Duplicated ', 20)
175 await waitJobs(servers)
176
177 {
178 const total = await getTotalRedundanciesRemoteServer()
179 expect(total).to.equal(4)
180 }
181
182 {
183 const total = await getTotalRedundanciesLocalServer()
184 expect(total).to.equal(2)
185 }
186 })
187
188 after(async function () {
189 await cleanupTests(servers)
190 })
191})
diff --git a/packages/tests/src/api/redundancy/redundancy.ts b/packages/tests/src/api/redundancy/redundancy.ts
new file mode 100644
index 000000000..69afae037
--- /dev/null
+++ b/packages/tests/src/api/redundancy/redundancy.ts
@@ -0,0 +1,743 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { readdir } from 'fs/promises'
5import { decode as magnetUriDecode } from 'magnet-uri'
6import { basename, join } from 'path'
7import { wait } from '@peertube/peertube-core-utils'
8import {
9 HttpStatusCode,
10 VideoDetails,
11 VideoFile,
12 VideoPrivacy,
13 VideoRedundancyStrategy,
14 VideoRedundancyStrategyWithManual
15} from '@peertube/peertube-models'
16import {
17 cleanupTests,
18 createMultipleServers,
19 doubleFollow,
20 killallServers,
21 makeRawRequest,
22 PeerTubeServer,
23 setAccessTokensToServers,
24 waitJobs
25} from '@peertube/peertube-server-commands'
26import { checkSegmentHash } from '@tests/shared/streaming-playlists.js'
27import { checkVideoFilesWereRemoved, saveVideoInServers } from '@tests/shared/videos.js'
28
29let servers: PeerTubeServer[] = []
30let video1Server2: VideoDetails
31
32async function checkMagnetWebseeds (file: VideoFile, baseWebseeds: string[], server: PeerTubeServer) {
33 const parsed = magnetUriDecode(file.magnetUri)
34
35 for (const ws of baseWebseeds) {
36 const found = parsed.urlList.find(url => url === `${ws}${basename(file.fileUrl)}`)
37 expect(found, `Webseed ${ws} not found in ${file.magnetUri} on server ${server.url}`).to.not.be.undefined
38 }
39
40 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
41
42 for (const url of parsed.urlList) {
43 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
44 }
45}
46
47async function createServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}, withWebVideo = true) {
48 const strategies: any[] = []
49
50 if (strategy !== null) {
51 strategies.push(
52 {
53 min_lifetime: '1 hour',
54 strategy,
55 size: '400KB',
56
57 ...additionalParams
58 }
59 )
60 }
61
62 const config = {
63 transcoding: {
64 web_videos: {
65 enabled: withWebVideo
66 },
67 hls: {
68 enabled: true
69 }
70 },
71 redundancy: {
72 videos: {
73 check_interval: '5 seconds',
74 strategies
75 }
76 }
77 }
78
79 servers = await createMultipleServers(3, config)
80
81 // Get the access tokens
82 await setAccessTokensToServers(servers)
83
84 {
85 const { id } = await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
86 video1Server2 = await servers[1].videos.get({ id })
87
88 await servers[1].views.simulateView({ id })
89 }
90
91 await waitJobs(servers)
92
93 // Server 1 and server 2 follow each other
94 await doubleFollow(servers[0], servers[1])
95 // Server 1 and server 3 follow each other
96 await doubleFollow(servers[0], servers[2])
97 // Server 2 and server 3 follow each other
98 await doubleFollow(servers[1], servers[2])
99
100 await waitJobs(servers)
101}
102
103async function ensureSameFilenames (videoUUID: string) {
104 let webVideoFilenames: string[]
105 let hlsFilenames: string[]
106
107 for (const server of servers) {
108 const video = await server.videos.getWithToken({ id: videoUUID })
109
110 // Ensure we use the same filenames that the origin
111
112 const localWebVideoFilenames = video.files.map(f => basename(f.fileUrl)).sort()
113 const localHLSFilenames = video.streamingPlaylists[0].files.map(f => basename(f.fileUrl)).sort()
114
115 if (webVideoFilenames) expect(webVideoFilenames).to.deep.equal(localWebVideoFilenames)
116 else webVideoFilenames = localWebVideoFilenames
117
118 if (hlsFilenames) expect(hlsFilenames).to.deep.equal(localHLSFilenames)
119 else hlsFilenames = localHLSFilenames
120 }
121
122 return { webVideoFilenames, hlsFilenames }
123}
124
125async function check1WebSeed (videoUUID?: string) {
126 if (!videoUUID) videoUUID = video1Server2.uuid
127
128 const webseeds = [
129 `${servers[1].url}/static/web-videos/`
130 ]
131
132 for (const server of servers) {
133 // With token to avoid issues with video follow constraints
134 const video = await server.videos.getWithToken({ id: videoUUID })
135
136 for (const f of video.files) {
137 await checkMagnetWebseeds(f, webseeds, server)
138 }
139 }
140
141 await ensureSameFilenames(videoUUID)
142}
143
144async function check2Webseeds (videoUUID?: string) {
145 if (!videoUUID) videoUUID = video1Server2.uuid
146
147 const webseeds = [
148 `${servers[0].url}/static/redundancy/`,
149 `${servers[1].url}/static/web-videos/`
150 ]
151
152 for (const server of servers) {
153 const video = await server.videos.get({ id: videoUUID })
154
155 for (const file of video.files) {
156 await checkMagnetWebseeds(file, webseeds, server)
157 }
158 }
159
160 const { webVideoFilenames } = await ensureSameFilenames(videoUUID)
161
162 const directories = [
163 servers[0].getDirectoryPath('redundancy'),
164 servers[1].getDirectoryPath('web-videos')
165 ]
166
167 for (const directory of directories) {
168 const files = await readdir(directory)
169 expect(files).to.have.length.at.least(4)
170
171 // Ensure we files exist on disk
172 expect(files.find(f => webVideoFilenames.includes(f))).to.exist
173 }
174}
175
176async function check0PlaylistRedundancies (videoUUID?: string) {
177 if (!videoUUID) videoUUID = video1Server2.uuid
178
179 for (const server of servers) {
180 // With token to avoid issues with video follow constraints
181 const video = await server.videos.getWithToken({ id: videoUUID })
182
183 expect(video.streamingPlaylists).to.be.an('array')
184 expect(video.streamingPlaylists).to.have.lengthOf(1)
185 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
186 }
187
188 await ensureSameFilenames(videoUUID)
189}
190
191async function check1PlaylistRedundancies (videoUUID?: string) {
192 if (!videoUUID) videoUUID = video1Server2.uuid
193
194 for (const server of servers) {
195 const video = await server.videos.get({ id: videoUUID })
196
197 expect(video.streamingPlaylists).to.have.lengthOf(1)
198 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
199
200 const redundancy = video.streamingPlaylists[0].redundancies[0]
201
202 expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
203 }
204
205 const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls/' + videoUUID
206 const baseUrlSegment = servers[0].url + '/static/redundancy/hls/' + videoUUID
207
208 const video = await servers[0].videos.get({ id: videoUUID })
209 const hlsPlaylist = video.streamingPlaylists[0]
210
211 for (const resolution of [ 240, 360, 480, 720 ]) {
212 await checkSegmentHash({ server: servers[1], baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist })
213 }
214
215 const { hlsFilenames } = await ensureSameFilenames(videoUUID)
216
217 const directories = [
218 servers[0].getDirectoryPath('redundancy/hls'),
219 servers[1].getDirectoryPath('streaming-playlists/hls')
220 ]
221
222 for (const directory of directories) {
223 const files = await readdir(join(directory, videoUUID))
224 expect(files).to.have.length.at.least(4)
225
226 // Ensure we files exist on disk
227 expect(files.find(f => hlsFilenames.includes(f))).to.exist
228 }
229}
230
231async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) {
232 let totalSize: number = null
233 let statsLength = 1
234
235 if (strategy !== 'manual') {
236 totalSize = 409600
237 statsLength = 2
238 }
239
240 const data = await servers[0].stats.get()
241 expect(data.videosRedundancy).to.have.lengthOf(statsLength)
242
243 const stat = data.videosRedundancy[0]
244 expect(stat.strategy).to.equal(strategy)
245 expect(stat.totalSize).to.equal(totalSize)
246
247 return stat
248}
249
250async function checkStatsWith1Redundancy (strategy: VideoRedundancyStrategyWithManual, onlyHls = false) {
251 const stat = await checkStatsGlobal(strategy)
252
253 expect(stat.totalUsed).to.be.at.least(1).and.below(409601)
254 expect(stat.totalVideoFiles).to.equal(onlyHls ? 4 : 8)
255 expect(stat.totalVideos).to.equal(1)
256}
257
258async function checkStatsWithoutRedundancy (strategy: VideoRedundancyStrategyWithManual) {
259 const stat = await checkStatsGlobal(strategy)
260
261 expect(stat.totalUsed).to.equal(0)
262 expect(stat.totalVideoFiles).to.equal(0)
263 expect(stat.totalVideos).to.equal(0)
264}
265
266async function findServerFollows () {
267 const body = await servers[0].follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' })
268 const follows = body.data
269 const server2 = follows.find(f => f.following.host === `${servers[1].host}`)
270 const server3 = follows.find(f => f.following.host === `${servers[2].host}`)
271
272 return { server2, server3 }
273}
274
275async function enableRedundancyOnServer1 () {
276 await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: true })
277
278 const { server2, server3 } = await findServerFollows()
279
280 expect(server3).to.not.be.undefined
281 expect(server3.following.hostRedundancyAllowed).to.be.false
282
283 expect(server2).to.not.be.undefined
284 expect(server2.following.hostRedundancyAllowed).to.be.true
285}
286
287async function disableRedundancyOnServer1 () {
288 await servers[0].redundancy.updateRedundancy({ host: servers[1].host, redundancyAllowed: false })
289
290 const { server2, server3 } = await findServerFollows()
291
292 expect(server3).to.not.be.undefined
293 expect(server3.following.hostRedundancyAllowed).to.be.false
294
295 expect(server2).to.not.be.undefined
296 expect(server2.following.hostRedundancyAllowed).to.be.false
297}
298
299describe('Test videos redundancy', function () {
300
301 describe('With most-views strategy', function () {
302 const strategy = 'most-views'
303
304 before(function () {
305 this.timeout(240000)
306
307 return createServers(strategy)
308 })
309
310 it('Should have 1 webseed on the first video', async function () {
311 await check1WebSeed()
312 await check0PlaylistRedundancies()
313 await checkStatsWithoutRedundancy(strategy)
314 })
315
316 it('Should enable redundancy on server 1', function () {
317 return enableRedundancyOnServer1()
318 })
319
320 it('Should have 2 webseeds on the first video', async function () {
321 this.timeout(80000)
322
323 await waitJobs(servers)
324 await servers[0].servers.waitUntilLog('Duplicated ', 5)
325 await waitJobs(servers)
326
327 await check2Webseeds()
328 await check1PlaylistRedundancies()
329 await checkStatsWith1Redundancy(strategy)
330 })
331
332 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
333 this.timeout(80000)
334
335 await disableRedundancyOnServer1()
336
337 await waitJobs(servers)
338 await wait(5000)
339
340 await check1WebSeed()
341 await check0PlaylistRedundancies()
342
343 await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
344 })
345
346 after(async function () {
347 return cleanupTests(servers)
348 })
349 })
350
351 describe('With trending strategy', function () {
352 const strategy = 'trending'
353
354 before(function () {
355 this.timeout(240000)
356
357 return createServers(strategy)
358 })
359
360 it('Should have 1 webseed on the first video', async function () {
361 await check1WebSeed()
362 await check0PlaylistRedundancies()
363 await checkStatsWithoutRedundancy(strategy)
364 })
365
366 it('Should enable redundancy on server 1', function () {
367 return enableRedundancyOnServer1()
368 })
369
370 it('Should have 2 webseeds on the first video', async function () {
371 this.timeout(80000)
372
373 await waitJobs(servers)
374 await servers[0].servers.waitUntilLog('Duplicated ', 5)
375 await waitJobs(servers)
376
377 await check2Webseeds()
378 await check1PlaylistRedundancies()
379 await checkStatsWith1Redundancy(strategy)
380 })
381
382 it('Should unfollow server 3 and keep duplicated videos', async function () {
383 this.timeout(80000)
384
385 await servers[0].follows.unfollow({ target: servers[2] })
386
387 await waitJobs(servers)
388 await wait(5000)
389
390 await check2Webseeds()
391 await check1PlaylistRedundancies()
392 await checkStatsWith1Redundancy(strategy)
393 })
394
395 it('Should unfollow server 2 and remove duplicated videos', async function () {
396 this.timeout(80000)
397
398 await servers[0].follows.unfollow({ target: servers[1] })
399
400 await waitJobs(servers)
401 await wait(5000)
402
403 await check1WebSeed()
404 await check0PlaylistRedundancies()
405
406 await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
407 })
408
409 after(async function () {
410 await cleanupTests(servers)
411 })
412 })
413
414 describe('With recently added strategy', function () {
415 const strategy = 'recently-added'
416
417 before(function () {
418 this.timeout(240000)
419
420 return createServers(strategy, { min_views: 3 })
421 })
422
423 it('Should have 1 webseed on the first video', async function () {
424 await check1WebSeed()
425 await check0PlaylistRedundancies()
426 await checkStatsWithoutRedundancy(strategy)
427 })
428
429 it('Should enable redundancy on server 1', function () {
430 return enableRedundancyOnServer1()
431 })
432
433 it('Should still have 1 webseed on the first video', async function () {
434 this.timeout(80000)
435
436 await waitJobs(servers)
437 await wait(15000)
438 await waitJobs(servers)
439
440 await check1WebSeed()
441 await check0PlaylistRedundancies()
442 await checkStatsWithoutRedundancy(strategy)
443 })
444
445 it('Should view 2 times the first video to have > min_views config', async function () {
446 this.timeout(80000)
447
448 await servers[0].views.simulateView({ id: video1Server2.uuid })
449 await servers[2].views.simulateView({ id: video1Server2.uuid })
450
451 await wait(10000)
452 await waitJobs(servers)
453 })
454
455 it('Should have 2 webseeds on the first video', async function () {
456 this.timeout(80000)
457
458 await waitJobs(servers)
459 await servers[0].servers.waitUntilLog('Duplicated ', 5)
460 await waitJobs(servers)
461
462 await check2Webseeds()
463 await check1PlaylistRedundancies()
464 await checkStatsWith1Redundancy(strategy)
465 })
466
467 it('Should remove the video and the redundancy files', async function () {
468 this.timeout(20000)
469
470 await saveVideoInServers(servers, video1Server2.uuid)
471 await servers[1].videos.remove({ id: video1Server2.uuid })
472
473 await waitJobs(servers)
474
475 for (const server of servers) {
476 await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
477 }
478 })
479
480 after(async function () {
481 await cleanupTests(servers)
482 })
483 })
484
485 describe('With only HLS files', function () {
486 const strategy = 'recently-added'
487
488 before(async function () {
489 this.timeout(240000)
490
491 await createServers(strategy, { min_views: 3 }, false)
492 })
493
494 it('Should have 0 playlist redundancy on the first video', async function () {
495 await check1WebSeed()
496 await check0PlaylistRedundancies()
497 })
498
499 it('Should enable redundancy on server 1', function () {
500 return enableRedundancyOnServer1()
501 })
502
503 it('Should still have 0 redundancy on the first video', async function () {
504 this.timeout(80000)
505
506 await waitJobs(servers)
507 await wait(15000)
508 await waitJobs(servers)
509
510 await check0PlaylistRedundancies()
511 await checkStatsWithoutRedundancy(strategy)
512 })
513
514 it('Should have 1 redundancy on the first video', async function () {
515 this.timeout(160000)
516
517 await servers[0].views.simulateView({ id: video1Server2.uuid })
518 await servers[2].views.simulateView({ id: video1Server2.uuid })
519
520 await wait(10000)
521 await waitJobs(servers)
522
523 await waitJobs(servers)
524 await servers[0].servers.waitUntilLog('Duplicated ', 1)
525 await waitJobs(servers)
526
527 await check1PlaylistRedundancies()
528 await checkStatsWith1Redundancy(strategy, true)
529 })
530
531 it('Should remove the video and the redundancy files', async function () {
532 this.timeout(20000)
533
534 await saveVideoInServers(servers, video1Server2.uuid)
535 await servers[1].videos.remove({ id: video1Server2.uuid })
536
537 await waitJobs(servers)
538
539 for (const server of servers) {
540 await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
541 }
542 })
543
544 after(async function () {
545 await cleanupTests(servers)
546 })
547 })
548
549 describe('With manual strategy', function () {
550 before(function () {
551 this.timeout(240000)
552
553 return createServers(null)
554 })
555
556 it('Should have 1 webseed on the first video', async function () {
557 await check1WebSeed()
558 await check0PlaylistRedundancies()
559 await checkStatsWithoutRedundancy('manual')
560 })
561
562 it('Should create a redundancy on first video', async function () {
563 await servers[0].redundancy.addVideo({ videoId: video1Server2.id })
564 })
565
566 it('Should have 2 webseeds on the first video', async function () {
567 this.timeout(80000)
568
569 await waitJobs(servers)
570 await servers[0].servers.waitUntilLog('Duplicated ', 5)
571 await waitJobs(servers)
572
573 await check2Webseeds()
574 await check1PlaylistRedundancies()
575 await checkStatsWith1Redundancy('manual')
576 })
577
578 it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () {
579 this.timeout(80000)
580
581 const body = await servers[0].redundancy.listVideos({ target: 'remote-videos' })
582
583 const videos = body.data
584 expect(videos).to.have.lengthOf(1)
585
586 const video = videos[0]
587
588 for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) {
589 await servers[0].redundancy.removeVideo({ redundancyId: r.id })
590 }
591
592 await waitJobs(servers)
593 await wait(5000)
594
595 await check1WebSeed()
596 await check0PlaylistRedundancies()
597
598 await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
599 })
600
601 after(async function () {
602 await cleanupTests(servers)
603 })
604 })
605
606 describe('Test expiration', function () {
607 const strategy = 'recently-added'
608
609 async function checkContains (servers: PeerTubeServer[], str: string) {
610 for (const server of servers) {
611 const video = await server.videos.get({ id: video1Server2.uuid })
612
613 for (const f of video.files) {
614 expect(f.magnetUri).to.contain(str)
615 }
616 }
617 }
618
619 async function checkNotContains (servers: PeerTubeServer[], str: string) {
620 for (const server of servers) {
621 const video = await server.videos.get({ id: video1Server2.uuid })
622
623 for (const f of video.files) {
624 expect(f.magnetUri).to.not.contain(str)
625 }
626 }
627 }
628
629 before(async function () {
630 this.timeout(240000)
631
632 await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
633
634 await enableRedundancyOnServer1()
635 })
636
637 it('Should still have 2 webseeds after 10 seconds', async function () {
638 this.timeout(80000)
639
640 await wait(10000)
641
642 try {
643 await checkContains(servers, 'http%3A%2F%2F' + servers[0].hostname + '%3A' + servers[0].port)
644 } catch {
645 // Maybe a server deleted a redundancy in the scheduler
646 await wait(2000)
647
648 await checkContains(servers, 'http%3A%2F%2F' + servers[0].hostname + '%3A' + servers[0].port)
649 }
650 })
651
652 it('Should stop server 1 and expire video redundancy', async function () {
653 this.timeout(80000)
654
655 await killallServers([ servers[0] ])
656
657 await wait(15000)
658
659 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2F' + servers[0].port + '%3A' + servers[0].port)
660 })
661
662 after(async function () {
663 await cleanupTests(servers)
664 })
665 })
666
667 describe('Test file replacement', function () {
668 let video2Server2UUID: string
669 const strategy = 'recently-added'
670
671 before(async function () {
672 this.timeout(240000)
673
674 await createServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
675
676 await enableRedundancyOnServer1()
677
678 await waitJobs(servers)
679 await servers[0].servers.waitUntilLog('Duplicated ', 5)
680 await waitJobs(servers)
681
682 await check2Webseeds()
683 await check1PlaylistRedundancies()
684 await checkStatsWith1Redundancy(strategy)
685
686 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 2 server 2', privacy: VideoPrivacy.PRIVATE } })
687 video2Server2UUID = uuid
688
689 // Wait transcoding before federation
690 await waitJobs(servers)
691
692 await servers[1].videos.update({ id: video2Server2UUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
693 })
694
695 it('Should cache video 2 webseeds on the first video', async function () {
696 this.timeout(240000)
697
698 await waitJobs(servers)
699
700 let checked = false
701
702 while (checked === false) {
703 await wait(1000)
704
705 try {
706 await check1WebSeed()
707 await check0PlaylistRedundancies()
708
709 await check2Webseeds(video2Server2UUID)
710 await check1PlaylistRedundancies(video2Server2UUID)
711
712 checked = true
713 } catch {
714 checked = false
715 }
716 }
717 })
718
719 it('Should disable strategy and remove redundancies', async function () {
720 this.timeout(80000)
721
722 await waitJobs(servers)
723
724 await killallServers([ servers[0] ])
725 await servers[0].run({
726 redundancy: {
727 videos: {
728 check_interval: '1 second',
729 strategies: []
730 }
731 }
732 })
733
734 await waitJobs(servers)
735
736 await checkVideoFilesWereRemoved({ server: servers[0], video: video1Server2, onlyVideoFiles: true })
737 })
738
739 after(async function () {
740 await cleanupTests(servers)
741 })
742 })
743})
diff --git a/packages/tests/src/api/runners/index.ts b/packages/tests/src/api/runners/index.ts
new file mode 100644
index 000000000..441ddc874
--- /dev/null
+++ b/packages/tests/src/api/runners/index.ts
@@ -0,0 +1,5 @@
1export * from './runner-common.js'
2export * from './runner-live-transcoding.js'
3export * from './runner-socket.js'
4export * from './runner-studio-transcoding.js'
5export * from './runner-vod-transcoding.js'
diff --git a/packages/tests/src/api/runners/runner-common.ts b/packages/tests/src/api/runners/runner-common.ts
new file mode 100644
index 000000000..53ea321d0
--- /dev/null
+++ b/packages/tests/src/api/runners/runner-common.ts
@@ -0,0 +1,744 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { wait } from '@peertube/peertube-core-utils'
4import {
5 HttpStatusCode,
6 Runner,
7 RunnerJob,
8 RunnerJobAdmin,
9 RunnerJobState,
10 RunnerJobStateType,
11 RunnerJobVODWebVideoTranscodingPayload,
12 RunnerRegistrationToken
13} from '@peertube/peertube-models'
14import {
15 PeerTubeServer,
16 cleanupTests,
17 createSingleServer,
18 setAccessTokensToServers,
19 setDefaultVideoChannel,
20 waitJobs
21} from '@peertube/peertube-server-commands'
22import { expect } from 'chai'
23
24describe('Test runner common actions', function () {
25 let server: PeerTubeServer
26 let registrationToken: string
27 let runnerToken: string
28 let jobMaxPriority: string
29
30 before(async function () {
31 this.timeout(120_000)
32
33 server = await createSingleServer(1, {
34 remote_runners: {
35 stalled_jobs: {
36 vod: '5 seconds'
37 }
38 }
39 })
40
41 await setAccessTokensToServers([ server ])
42 await setDefaultVideoChannel([ server ])
43
44 await server.config.enableTranscoding({ hls: true, webVideo: true })
45 await server.config.enableRemoteTranscoding()
46 })
47
48 describe('Managing runner registration tokens', function () {
49 let base: RunnerRegistrationToken[]
50 let registrationTokenToDelete: RunnerRegistrationToken
51
52 it('Should have a default registration token', async function () {
53 const { total, data } = await server.runnerRegistrationTokens.list()
54
55 expect(total).to.equal(1)
56 expect(data).to.have.lengthOf(1)
57
58 const token = data[0]
59 expect(token.id).to.exist
60 expect(token.createdAt).to.exist
61 expect(token.updatedAt).to.exist
62 expect(token.registeredRunnersCount).to.equal(0)
63 expect(token.registrationToken).to.exist
64 })
65
66 it('Should create other registration tokens', async function () {
67 await server.runnerRegistrationTokens.generate()
68 await server.runnerRegistrationTokens.generate()
69
70 const { total, data } = await server.runnerRegistrationTokens.list()
71 expect(total).to.equal(3)
72 expect(data).to.have.lengthOf(3)
73 })
74
75 it('Should list registration tokens', async function () {
76 {
77 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
78 expect(total).to.equal(3)
79 expect(data).to.have.lengthOf(3)
80 expect(new Date(data[0].createdAt)).to.be.below(new Date(data[1].createdAt))
81 expect(new Date(data[1].createdAt)).to.be.below(new Date(data[2].createdAt))
82
83 base = data
84
85 registrationTokenToDelete = data[0]
86 registrationToken = data[1].registrationToken
87 }
88
89 {
90 const { total, data } = await server.runnerRegistrationTokens.list({ sort: '-createdAt', start: 2, count: 1 })
91 expect(total).to.equal(3)
92 expect(data).to.have.lengthOf(1)
93 expect(data[0].registrationToken).to.equal(base[0].registrationToken)
94 }
95 })
96
97 it('Should have appropriate registeredRunnersCount for registration tokens', async function () {
98 await server.runners.register({ name: 'to delete 1', registrationToken: registrationTokenToDelete.registrationToken })
99 await server.runners.register({ name: 'to delete 2', registrationToken: registrationTokenToDelete.registrationToken })
100
101 const { data } = await server.runnerRegistrationTokens.list()
102
103 for (const d of data) {
104 if (d.registrationToken === registrationTokenToDelete.registrationToken) {
105 expect(d.registeredRunnersCount).to.equal(2)
106 } else {
107 expect(d.registeredRunnersCount).to.equal(0)
108 }
109 }
110
111 const { data: runners } = await server.runners.list()
112 expect(runners).to.have.lengthOf(2)
113 })
114
115 it('Should delete a registration token', async function () {
116 await server.runnerRegistrationTokens.delete({ id: registrationTokenToDelete.id })
117
118 const { total, data } = await server.runnerRegistrationTokens.list({ sort: 'createdAt' })
119 expect(total).to.equal(2)
120 expect(data).to.have.lengthOf(2)
121
122 for (const d of data) {
123 expect(d.registeredRunnersCount).to.equal(0)
124 expect(d.registrationToken).to.not.equal(registrationTokenToDelete.registrationToken)
125 }
126 })
127
128 it('Should have removed runners of this registration token', async function () {
129 const { data: runners } = await server.runners.list()
130 expect(runners).to.have.lengthOf(0)
131 })
132 })
133
134 describe('Managing runners', function () {
135 let toDelete: Runner
136
137 it('Should not have runners available', async function () {
138 const { total, data } = await server.runners.list()
139
140 expect(data).to.have.lengthOf(0)
141 expect(total).to.equal(0)
142 })
143
144 it('Should register runners', async function () {
145 const now = new Date()
146
147 const result = await server.runners.register({
148 name: 'runner 1',
149 description: 'my super runner 1',
150 registrationToken
151 })
152 expect(result.runnerToken).to.exist
153 runnerToken = result.runnerToken
154
155 await server.runners.register({
156 name: 'runner 2',
157 registrationToken
158 })
159
160 const { total, data } = await server.runners.list({ sort: 'createdAt' })
161 expect(total).to.equal(2)
162 expect(data).to.have.lengthOf(2)
163
164 for (const d of data) {
165 expect(d.id).to.exist
166 expect(d.createdAt).to.exist
167 expect(d.updatedAt).to.exist
168 expect(new Date(d.createdAt)).to.be.above(now)
169 expect(new Date(d.updatedAt)).to.be.above(now)
170 expect(new Date(d.lastContact)).to.be.above(now)
171 expect(d.ip).to.exist
172 }
173
174 expect(data[0].name).to.equal('runner 1')
175 expect(data[0].description).to.equal('my super runner 1')
176
177 expect(data[1].name).to.equal('runner 2')
178 expect(data[1].description).to.be.null
179
180 toDelete = data[1]
181 })
182
183 it('Should list runners', async function () {
184 const { total, data } = await server.runners.list({ sort: '-createdAt', start: 1, count: 1 })
185
186 expect(total).to.equal(2)
187 expect(data).to.have.lengthOf(1)
188 expect(data[0].name).to.equal('runner 1')
189 })
190
191 it('Should delete a runner', async function () {
192 await server.runners.delete({ id: toDelete.id })
193
194 const { total, data } = await server.runners.list()
195
196 expect(total).to.equal(1)
197 expect(data).to.have.lengthOf(1)
198 expect(data[0].name).to.equal('runner 1')
199 })
200
201 it('Should unregister a runner', async function () {
202 const registered = await server.runners.autoRegisterRunner()
203
204 {
205 const { total, data } = await server.runners.list()
206 expect(total).to.equal(2)
207 expect(data).to.have.lengthOf(2)
208 }
209
210 await server.runners.unregister({ runnerToken: registered })
211
212 {
213 const { total, data } = await server.runners.list()
214 expect(total).to.equal(1)
215 expect(data).to.have.lengthOf(1)
216 expect(data[0].name).to.equal('runner 1')
217 }
218 })
219 })
220
221 describe('Managing runner jobs', function () {
222 let jobUUID: string
223 let jobToken: string
224 let lastRunnerContact: Date
225 let failedJob: RunnerJob
226
227 async function checkMainJobState (
228 mainJobState: RunnerJobStateType,
229 otherJobStates: RunnerJobStateType[] = [ RunnerJobState.PENDING, RunnerJobState.WAITING_FOR_PARENT_JOB ]
230 ) {
231 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
232
233 for (const job of data) {
234 if (job.uuid === jobUUID) {
235 expect(job.state.id).to.equal(mainJobState)
236 } else {
237 expect(otherJobStates).to.include(job.state.id)
238 }
239 }
240 }
241
242 function getMainJob () {
243 return server.runnerJobs.getJob({ uuid: jobUUID })
244 }
245
246 describe('List jobs', function () {
247
248 it('Should not have jobs', async function () {
249 const { total, data } = await server.runnerJobs.list()
250
251 expect(data).to.have.lengthOf(0)
252 expect(total).to.equal(0)
253 })
254
255 it('Should upload a video and have available jobs', async function () {
256 await server.videos.quickUpload({ name: 'to transcode' })
257 await waitJobs([ server ])
258
259 const { total, data } = await server.runnerJobs.list()
260
261 expect(data).to.have.lengthOf(10)
262 expect(total).to.equal(10)
263
264 for (const job of data) {
265 expect(job.startedAt).to.not.exist
266 expect(job.finishedAt).to.not.exist
267 expect(job.payload).to.exist
268 expect(job.privatePayload).to.exist
269 }
270
271 const hlsJobs = data.filter(d => d.type === 'vod-hls-transcoding')
272 const webVideoJobs = data.filter(d => d.type === 'vod-web-video-transcoding')
273
274 expect(hlsJobs).to.have.lengthOf(5)
275 expect(webVideoJobs).to.have.lengthOf(5)
276
277 const pendingJobs = data.filter(d => d.state.id === RunnerJobState.PENDING)
278 const waitingJobs = data.filter(d => d.state.id === RunnerJobState.WAITING_FOR_PARENT_JOB)
279
280 expect(pendingJobs).to.have.lengthOf(1)
281 expect(waitingJobs).to.have.lengthOf(9)
282 })
283
284 it('Should upload another video and list/sort jobs', async function () {
285 await server.videos.quickUpload({ name: 'to transcode 2' })
286 await waitJobs([ server ])
287
288 {
289 const { total, data } = await server.runnerJobs.list({ start: 0, count: 30 })
290
291 expect(data).to.have.lengthOf(20)
292 expect(total).to.equal(20)
293
294 jobUUID = data[16].uuid
295 }
296
297 {
298 const { total, data } = await server.runnerJobs.list({ start: 3, count: 1, sort: 'createdAt' })
299 expect(total).to.equal(20)
300
301 expect(data).to.have.lengthOf(1)
302 expect(data[0].uuid).to.equal(jobUUID)
303 }
304
305 {
306 let previousPriority = Infinity
307 const { total, data } = await server.runnerJobs.list({ start: 0, count: 100, sort: '-priority' })
308 expect(total).to.equal(20)
309
310 for (const job of data) {
311 expect(job.priority).to.be.at.most(previousPriority)
312 previousPriority = job.priority
313
314 if (job.state.id === RunnerJobState.PENDING) {
315 jobMaxPriority = job.uuid
316 }
317 }
318 }
319 })
320
321 it('Should search jobs', async function () {
322 {
323 const { total, data } = await server.runnerJobs.list({ search: jobUUID })
324
325 expect(data).to.have.lengthOf(1)
326 expect(total).to.equal(1)
327
328 expect(data[0].uuid).to.equal(jobUUID)
329 }
330
331 {
332 const { total, data } = await server.runnerJobs.list({ search: 'toto' })
333
334 expect(data).to.have.lengthOf(0)
335 expect(total).to.equal(0)
336 }
337
338 {
339 const { total, data } = await server.runnerJobs.list({ search: 'hls' })
340
341 expect(data).to.not.have.lengthOf(0)
342 expect(total).to.not.equal(0)
343
344 for (const job of data) {
345 expect(job.type).to.include('hls')
346 }
347 }
348 })
349
350 it('Should filter jobs', async function () {
351 {
352 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.WAITING_FOR_PARENT_JOB ] })
353
354 expect(data).to.not.have.lengthOf(0)
355 expect(total).to.not.equal(0)
356
357 for (const job of data) {
358 expect(job.state.label).to.equal('Waiting for parent job to finish')
359 }
360 }
361
362 {
363 const { total, data } = await server.runnerJobs.list({ stateOneOf: [ RunnerJobState.COMPLETED ] })
364
365 expect(data).to.have.lengthOf(0)
366 expect(total).to.equal(0)
367 }
368 })
369 })
370
371 describe('Accept/update/abort/process a job', function () {
372
373 it('Should request available jobs', async function () {
374 lastRunnerContact = new Date()
375
376 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
377
378 // Only optimize jobs are available
379 expect(availableJobs).to.have.lengthOf(2)
380
381 for (const job of availableJobs) {
382 expect(job.uuid).to.exist
383 expect(job.payload.input).to.exist
384 expect((job.payload as RunnerJobVODWebVideoTranscodingPayload).output).to.exist
385
386 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
387 }
388
389 const hlsJobs = availableJobs.filter(d => d.type === 'vod-hls-transcoding')
390 const webVideoJobs = availableJobs.filter(d => d.type === 'vod-web-video-transcoding')
391
392 expect(hlsJobs).to.have.lengthOf(0)
393 expect(webVideoJobs).to.have.lengthOf(2)
394
395 jobUUID = webVideoJobs[0].uuid
396 })
397
398 it('Should have sorted available jobs by priority', async function () {
399 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
400
401 expect(availableJobs[0].uuid).to.equal(jobMaxPriority)
402 })
403
404 it('Should have last runner contact updated', async function () {
405 await wait(1000)
406
407 const { data } = await server.runners.list({ sort: 'createdAt' })
408 expect(new Date(data[0].lastContact)).to.be.above(lastRunnerContact)
409 })
410
411 it('Should accept a job', async function () {
412 const startedAt = new Date()
413
414 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
415 jobToken = job.jobToken
416
417 const checkProcessingJob = (job: RunnerJob & { jobToken?: string }, fromAccept: boolean) => {
418 expect(job.uuid).to.equal(jobUUID)
419
420 expect(job.type).to.equal('vod-web-video-transcoding')
421 expect(job.state.label).to.equal('Processing')
422 expect(job.state.id).to.equal(RunnerJobState.PROCESSING)
423
424 expect(job.runner).to.exist
425 expect(job.runner.name).to.equal('runner 1')
426 expect(job.runner.description).to.equal('my super runner 1')
427
428 expect(job.progress).to.be.null
429
430 expect(job.startedAt).to.exist
431 expect(new Date(job.startedAt)).to.be.above(startedAt)
432
433 expect(job.finishedAt).to.not.exist
434
435 expect(job.failures).to.equal(0)
436
437 expect(job.payload).to.exist
438
439 if (fromAccept) {
440 expect(job.jobToken).to.exist
441 expect((job as RunnerJobAdmin).privatePayload).to.not.exist
442 } else {
443 expect(job.jobToken).to.not.exist
444 expect((job as RunnerJobAdmin).privatePayload).to.exist
445 }
446 }
447
448 checkProcessingJob(job, true)
449
450 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
451
452 const processingJob = data.find(j => j.uuid === jobUUID)
453 checkProcessingJob(processingJob, false)
454
455 await checkMainJobState(RunnerJobState.PROCESSING)
456 })
457
458 it('Should update a job', async function () {
459 await server.runnerJobs.update({ runnerToken, jobUUID, jobToken, progress: 53 })
460
461 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
462
463 for (const job of data) {
464 if (job.state.id === RunnerJobState.PROCESSING) {
465 expect(job.progress).to.equal(53)
466 } else {
467 expect(job.progress).to.be.null
468 }
469 }
470 })
471
472 it('Should abort a job', async function () {
473 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'for tests' })
474
475 await checkMainJobState(RunnerJobState.PENDING)
476
477 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
478 for (const job of data) {
479 expect(job.progress).to.be.null
480 }
481 })
482
483 it('Should accept the same job again and post a success', async function () {
484 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
485 expect(availableJobs.find(j => j.uuid === jobUUID)).to.exist
486
487 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
488 jobToken = job.jobToken
489
490 await checkMainJobState(RunnerJobState.PROCESSING)
491
492 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
493
494 for (const job of data) {
495 expect(job.progress).to.be.null
496 }
497
498 const payload = {
499 videoFile: 'video_short.mp4'
500 }
501
502 await server.runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
503 })
504
505 it('Should not have available jobs anymore', async function () {
506 await checkMainJobState(RunnerJobState.COMPLETED)
507
508 const job = await getMainJob()
509 expect(job.finishedAt).to.exist
510
511 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
512 expect(availableJobs.find(j => j.uuid === jobUUID)).to.not.exist
513 })
514 })
515
516 describe('Error job', function () {
517
518 it('Should accept another job and post an error', async function () {
519 await server.runnerJobs.cancelAllJobs()
520 await server.videos.quickUpload({ name: 'video' })
521 await waitJobs([ server ])
522
523 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
524 jobUUID = availableJobs[0].uuid
525
526 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
527 jobToken = job.jobToken
528
529 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
530 })
531
532 it('Should have job failures increased', async function () {
533 const job = await getMainJob()
534 expect(job.state.id).to.equal(RunnerJobState.PENDING)
535 expect(job.failures).to.equal(1)
536 expect(job.error).to.be.null
537 expect(job.progress).to.be.null
538 expect(job.finishedAt).to.not.exist
539 })
540
541 it('Should error a job when job attempts is too big', async function () {
542 for (let i = 0; i < 4; i++) {
543 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
544 jobToken = job.jobToken
545
546 await server.runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error ' + i })
547 }
548
549 const job = await getMainJob()
550 expect(job.failures).to.equal(5)
551 expect(job.state.id).to.equal(RunnerJobState.ERRORED)
552 expect(job.state.label).to.equal('Errored')
553 expect(job.error).to.equal('Error 3')
554 expect(job.progress).to.be.null
555 expect(job.finishedAt).to.exist
556
557 failedJob = job
558 })
559
560 it('Should have failed children jobs too', async function () {
561 const { data } = await server.runnerJobs.list({ count: 50, sort: '-updatedAt' })
562
563 const children = data.filter(j => j.parent?.uuid === failedJob.uuid)
564 expect(children).to.have.lengthOf(9)
565
566 for (const child of children) {
567 expect(child.parent.uuid).to.equal(failedJob.uuid)
568 expect(child.parent.type).to.equal(failedJob.type)
569 expect(child.parent.state.id).to.equal(failedJob.state.id)
570 expect(child.parent.state.label).to.equal(failedJob.state.label)
571
572 expect(child.state.id).to.equal(RunnerJobState.PARENT_ERRORED)
573 expect(child.state.label).to.equal('Parent job failed')
574 }
575 })
576 })
577
578 describe('Cancel', function () {
579
580 it('Should cancel a pending job', async function () {
581 await server.videos.quickUpload({ name: 'video' })
582 await waitJobs([ server ])
583
584 {
585 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
586
587 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
588 jobUUID = pendingJob.uuid
589
590 await server.runnerJobs.cancelByAdmin({ jobUUID })
591 }
592
593 {
594 const job = await getMainJob()
595 expect(job.state.id).to.equal(RunnerJobState.CANCELLED)
596 expect(job.state.label).to.equal('Cancelled')
597 }
598
599 {
600 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
601 const children = data.filter(j => j.parent?.uuid === jobUUID)
602 expect(children).to.have.lengthOf(9)
603
604 for (const child of children) {
605 expect(child.state.id).to.equal(RunnerJobState.PARENT_CANCELLED)
606 }
607 }
608 })
609
610 it('Should cancel an already accepted job and skip success/error', async function () {
611 await server.videos.quickUpload({ name: 'video' })
612 await waitJobs([ server ])
613
614 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
615 jobUUID = availableJobs[0].uuid
616
617 const { job } = await server.runnerJobs.accept({ runnerToken, jobUUID })
618 jobToken = job.jobToken
619
620 await server.runnerJobs.cancelByAdmin({ jobUUID })
621
622 await server.runnerJobs.abort({ runnerToken, jobUUID, jobToken, reason: 'aborted', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
623 })
624 })
625
626 describe('Remove', function () {
627
628 it('Should remove a pending job', async function () {
629 await server.videos.quickUpload({ name: 'video' })
630 await waitJobs([ server ])
631
632 {
633 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
634
635 const pendingJob = data.find(j => j.state.id === RunnerJobState.PENDING)
636 jobUUID = pendingJob.uuid
637
638 await server.runnerJobs.deleteByAdmin({ jobUUID })
639 }
640
641 {
642 const { data } = await server.runnerJobs.list({ count: 10, sort: '-updatedAt' })
643
644 const parent = data.find(j => j.uuid === jobUUID)
645 expect(parent).to.not.exist
646
647 const children = data.filter(j => j.parent?.uuid === jobUUID)
648 expect(children).to.have.lengthOf(0)
649 }
650 })
651 })
652
653 describe('Stalled jobs', function () {
654
655 it('Should abort stalled jobs', async function () {
656 this.timeout(60000)
657
658 await server.videos.quickUpload({ name: 'video' })
659 await server.videos.quickUpload({ name: 'video' })
660 await waitJobs([ server ])
661
662 const { job: job1 } = await server.runnerJobs.autoAccept({ runnerToken })
663 const { job: stalledJob } = await server.runnerJobs.autoAccept({ runnerToken })
664
665 for (let i = 0; i < 6; i++) {
666 await wait(2000)
667
668 await server.runnerJobs.update({ runnerToken, jobToken: job1.jobToken, jobUUID: job1.uuid })
669 }
670
671 const refreshedJob1 = await server.runnerJobs.getJob({ uuid: job1.uuid })
672 const refreshedStalledJob = await server.runnerJobs.getJob({ uuid: stalledJob.uuid })
673
674 expect(refreshedJob1.state.id).to.equal(RunnerJobState.PROCESSING)
675 expect(refreshedStalledJob.state.id).to.equal(RunnerJobState.PENDING)
676 })
677 })
678
679 describe('Rate limit', function () {
680
681 before(async function () {
682 this.timeout(60000)
683
684 await server.kill()
685
686 await server.run({
687 rates_limit: {
688 api: {
689 max: 10
690 }
691 }
692 })
693 })
694
695 it('Should rate limit an unknown runner, but not a registered one', async function () {
696 this.timeout(60000)
697
698 await server.videos.quickUpload({ name: 'video' })
699 await waitJobs([ server ])
700
701 const { job } = await server.runnerJobs.autoAccept({ runnerToken })
702
703 for (let i = 0; i < 20; i++) {
704 try {
705 await server.runnerJobs.request({ runnerToken })
706 await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
707 } catch {}
708 }
709
710 // Invalid
711 {
712 await server.runnerJobs.request({ runnerToken: 'toto', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
713 await server.runnerJobs.update({
714 runnerToken: 'toto',
715 jobToken: job.jobToken,
716 jobUUID: job.uuid,
717 expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
718 })
719 }
720
721 // Not provided
722 {
723 await server.runnerJobs.request({ runnerToken: undefined, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
724 await server.runnerJobs.update({
725 runnerToken: undefined,
726 jobToken: job.jobToken,
727 jobUUID: job.uuid,
728 expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429
729 })
730 }
731
732 // Registered
733 {
734 await server.runnerJobs.request({ runnerToken })
735 await server.runnerJobs.update({ runnerToken, jobToken: job.jobToken, jobUUID: job.uuid })
736 }
737 })
738 })
739 })
740
741 after(async function () {
742 await cleanupTests([ server ])
743 })
744})
diff --git a/packages/tests/src/api/runners/runner-live-transcoding.ts b/packages/tests/src/api/runners/runner-live-transcoding.ts
new file mode 100644
index 000000000..20c1e5c2a
--- /dev/null
+++ b/packages/tests/src/api/runners/runner-live-transcoding.ts
@@ -0,0 +1,332 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { readFile } from 'fs/promises'
6import { wait } from '@peertube/peertube-core-utils'
7import {
8 HttpStatusCode,
9 LiveRTMPHLSTranscodingUpdatePayload,
10 LiveVideo,
11 LiveVideoError,
12 LiveVideoErrorType,
13 RunnerJob,
14 RunnerJobLiveRTMPHLSTranscodingPayload,
15 Video,
16 VideoPrivacy,
17 VideoState
18} from '@peertube/peertube-models'
19import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
20import {
21 cleanupTests,
22 createSingleServer,
23 makeRawRequest,
24 PeerTubeServer,
25 sendRTMPStream,
26 setAccessTokensToServers,
27 setDefaultVideoChannel,
28 stopFfmpeg,
29 testFfmpegStreamError,
30 waitJobs
31} from '@peertube/peertube-server-commands'
32
33describe('Test runner live transcoding', function () {
34 let server: PeerTubeServer
35 let runnerToken: string
36 let baseUrl: string
37
38 before(async function () {
39 this.timeout(120_000)
40
41 server = await createSingleServer(1)
42
43 await setAccessTokensToServers([ server ])
44 await setDefaultVideoChannel([ server ])
45
46 await server.config.enableRemoteTranscoding()
47 await server.config.enableTranscoding()
48 runnerToken = await server.runners.autoRegisterRunner()
49
50 baseUrl = server.url + '/static/streaming-playlists/hls'
51 })
52
53 describe('Without transcoding enabled', function () {
54
55 before(async function () {
56 await server.config.enableLive({
57 allowReplay: false,
58 resolutions: 'min',
59 transcoding: false
60 })
61 })
62
63 it('Should not have available jobs', async function () {
64 this.timeout(120000)
65
66 const { live, video } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
67
68 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
69 await server.live.waitUntilPublished({ videoId: video.id })
70
71 await waitJobs([ server ])
72
73 const { availableJobs } = await server.runnerJobs.requestLive({ runnerToken })
74 expect(availableJobs).to.have.lengthOf(0)
75
76 await stopFfmpeg(ffmpegCommand)
77 })
78 })
79
80 describe('With transcoding enabled on classic live', function () {
81 let live: LiveVideo
82 let video: Video
83 let ffmpegCommand: FfmpegCommand
84 let jobUUID: string
85 let acceptedJob: RunnerJob & { jobToken: string }
86
87 async function testPlaylistFile (fixture: string, expected: string) {
88 const text = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${fixture}` })
89 expect(await readFile(buildAbsoluteFixturePath(expected), 'utf-8')).to.equal(text)
90
91 }
92
93 async function testTSFile (fixture: string, expected: string) {
94 const { body } = await makeRawRequest({ url: `${baseUrl}/${video.uuid}/${fixture}`, expectedStatus: HttpStatusCode.OK_200 })
95 expect(await readFile(buildAbsoluteFixturePath(expected))).to.deep.equal(body)
96 }
97
98 before(async function () {
99 await server.config.enableLive({
100 allowReplay: true,
101 resolutions: 'max',
102 transcoding: true
103 })
104 })
105
106 it('Should publish a a live and have available jobs', async function () {
107 this.timeout(120000)
108
109 const data = await server.live.quickCreate({ permanentLive: false, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
110 live = data.live
111 video = data.video
112
113 ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
114 await waitJobs([ server ])
115
116 const job = await server.runnerJobs.requestLiveJob(runnerToken)
117 jobUUID = job.uuid
118
119 expect(job.type).to.equal('live-rtmp-hls-transcoding')
120 expect(job.payload.input.rtmpUrl).to.exist
121
122 expect(job.payload.output.toTranscode).to.have.lengthOf(5)
123
124 for (const { resolution, fps } of job.payload.output.toTranscode) {
125 expect([ 720, 480, 360, 240, 144 ]).to.contain(resolution)
126
127 expect(fps).to.be.above(25)
128 expect(fps).to.be.below(70)
129 }
130 })
131
132 it('Should update the live with a new chunk', async function () {
133 this.timeout(120000)
134
135 const { job } = await server.runnerJobs.accept<RunnerJobLiveRTMPHLSTranscodingPayload>({ jobUUID, runnerToken })
136 acceptedJob = job
137
138 {
139 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
140 masterPlaylistFile: 'live/master.m3u8',
141 resolutionPlaylistFile: 'live/0.m3u8',
142 resolutionPlaylistFilename: '0.m3u8',
143 type: 'add-chunk',
144 videoChunkFile: 'live/0-000067.ts',
145 videoChunkFilename: '0-000067.ts'
146 }
147 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload, progress: 50 })
148
149 const updatedJob = await server.runnerJobs.getJob({ uuid: job.uuid })
150 expect(updatedJob.progress).to.equal(50)
151 }
152
153 {
154 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
155 resolutionPlaylistFile: 'live/1.m3u8',
156 resolutionPlaylistFilename: '1.m3u8',
157 type: 'add-chunk',
158 videoChunkFile: 'live/1-000068.ts',
159 videoChunkFilename: '1-000068.ts'
160 }
161 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: job.jobToken, payload })
162 }
163
164 await wait(1000)
165
166 await testPlaylistFile('master.m3u8', 'live/master.m3u8')
167 await testPlaylistFile('0.m3u8', 'live/0.m3u8')
168 await testPlaylistFile('1.m3u8', 'live/1.m3u8')
169
170 await testTSFile('0-000067.ts', 'live/0-000067.ts')
171 await testTSFile('1-000068.ts', 'live/1-000068.ts')
172 })
173
174 it('Should replace existing m3u8 on update', async function () {
175 this.timeout(120000)
176
177 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
178 masterPlaylistFile: 'live/1.m3u8',
179 resolutionPlaylistFilename: '0.m3u8',
180 resolutionPlaylistFile: 'live/1.m3u8',
181 type: 'add-chunk',
182 videoChunkFile: 'live/1-000069.ts',
183 videoChunkFilename: '1-000068.ts'
184 }
185 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
186 await wait(1000)
187
188 await testPlaylistFile('master.m3u8', 'live/1.m3u8')
189 await testPlaylistFile('0.m3u8', 'live/1.m3u8')
190 await testTSFile('1-000068.ts', 'live/1-000069.ts')
191 })
192
193 it('Should update the live with removed chunks', async function () {
194 this.timeout(120000)
195
196 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
197 resolutionPlaylistFile: 'live/0.m3u8',
198 resolutionPlaylistFilename: '0.m3u8',
199 type: 'remove-chunk',
200 videoChunkFilename: '1-000068.ts'
201 }
202 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
203
204 await wait(1000)
205
206 await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/master.m3u8` })
207 await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/0.m3u8` })
208 await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/1.m3u8` })
209 await makeRawRequest({ url: `${baseUrl}/${video.uuid}/0-000067.ts`, expectedStatus: HttpStatusCode.OK_200 })
210 await makeRawRequest({ url: `${baseUrl}/${video.uuid}/1-000068.ts`, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
211 })
212
213 it('Should complete the live and save the replay', async function () {
214 this.timeout(120000)
215
216 for (const segment of [ '0-000069.ts', '0-000070.ts' ]) {
217 const payload: LiveRTMPHLSTranscodingUpdatePayload = {
218 masterPlaylistFile: 'live/master.m3u8',
219 resolutionPlaylistFilename: '0.m3u8',
220 resolutionPlaylistFile: 'live/0.m3u8',
221 type: 'add-chunk',
222 videoChunkFile: 'live/' + segment,
223 videoChunkFilename: segment
224 }
225 await server.runnerJobs.update({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload })
226
227 await wait(1000)
228 }
229
230 await waitJobs([ server ])
231
232 {
233 const { state } = await server.videos.get({ id: video.uuid })
234 expect(state.id).to.equal(VideoState.PUBLISHED)
235 }
236
237 await stopFfmpeg(ffmpegCommand)
238
239 await server.runnerJobs.success({ jobUUID, runnerToken, jobToken: acceptedJob.jobToken, payload: {} })
240
241 await wait(1500)
242 await waitJobs([ server ])
243
244 {
245 const { state } = await server.videos.get({ id: video.uuid })
246 expect(state.id).to.equal(VideoState.LIVE_ENDED)
247
248 const session = await server.live.findLatestSession({ videoId: video.uuid })
249 expect(session.error).to.be.null
250 }
251 })
252 })
253
254 describe('With transcoding enabled on cancelled/aborted/errored live', function () {
255 let live: LiveVideo
256 let video: Video
257 let ffmpegCommand: FfmpegCommand
258
259 async function prepare () {
260 ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
261 await server.runnerJobs.requestLiveJob(runnerToken)
262
263 const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' })
264
265 return job
266 }
267
268 async function checkSessionError (error: LiveVideoErrorType) {
269 await wait(1500)
270 await waitJobs([ server ])
271
272 const session = await server.live.findLatestSession({ videoId: video.uuid })
273 expect(session.error).to.equal(error)
274 }
275
276 before(async function () {
277 await server.config.enableLive({
278 allowReplay: true,
279 resolutions: 'max',
280 transcoding: true
281 })
282
283 const data = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC })
284 live = data.live
285 video = data.video
286 })
287
288 it('Should abort a running live', async function () {
289 this.timeout(120000)
290
291 const job = await prepare()
292
293 await Promise.all([
294 server.runnerJobs.abort({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, reason: 'abort' }),
295 testFfmpegStreamError(ffmpegCommand, true)
296 ])
297
298 // Abort is not supported
299 await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
300 })
301
302 it('Should cancel a running live', async function () {
303 this.timeout(120000)
304
305 const job = await prepare()
306
307 await Promise.all([
308 server.runnerJobs.cancelByAdmin({ jobUUID: job.uuid }),
309 testFfmpegStreamError(ffmpegCommand, true)
310 ])
311
312 await checkSessionError(LiveVideoError.RUNNER_JOB_CANCEL)
313 })
314
315 it('Should error a running live', async function () {
316 this.timeout(120000)
317
318 const job = await prepare()
319
320 await Promise.all([
321 server.runnerJobs.error({ jobUUID: job.uuid, runnerToken, jobToken: job.jobToken, message: 'error' }),
322 testFfmpegStreamError(ffmpegCommand, true)
323 ])
324
325 await checkSessionError(LiveVideoError.RUNNER_JOB_ERROR)
326 })
327 })
328
329 after(async function () {
330 await cleanupTests([ server ])
331 })
332})
diff --git a/packages/tests/src/api/runners/runner-socket.ts b/packages/tests/src/api/runners/runner-socket.ts
new file mode 100644
index 000000000..726ef084f
--- /dev/null
+++ b/packages/tests/src/api/runners/runner-socket.ts
@@ -0,0 +1,120 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import {
6 cleanupTests,
7 createSingleServer,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test runner socket', function () {
15 let server: PeerTubeServer
16 let runnerToken: string
17
18 before(async function () {
19 this.timeout(120_000)
20
21 server = await createSingleServer(1)
22
23 await setAccessTokensToServers([ server ])
24 await setDefaultVideoChannel([ server ])
25
26 await server.config.enableTranscoding({ hls: true, webVideo: true })
27 await server.config.enableRemoteTranscoding()
28 runnerToken = await server.runners.autoRegisterRunner()
29 })
30
31 it('Should throw an error without runner token', function (done) {
32 const localSocket = server.socketIO.getRunnersSocket({ runnerToken: null })
33 localSocket.on('connect_error', err => {
34 expect(err.message).to.contain('No runner token provided')
35 done()
36 })
37 })
38
39 it('Should throw an error with a bad runner token', function (done) {
40 const localSocket = server.socketIO.getRunnersSocket({ runnerToken: 'ergag' })
41 localSocket.on('connect_error', err => {
42 expect(err.message).to.contain('Invalid runner token')
43 done()
44 })
45 })
46
47 it('Should not send ping if there is no available jobs', async function () {
48 let pings = 0
49 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
50 localSocket.on('available-jobs', () => pings++)
51
52 expect(pings).to.equal(0)
53 })
54
55 it('Should send a ping on available job', async function () {
56 let pings = 0
57 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
58 localSocket.on('available-jobs', () => pings++)
59
60 await server.videos.quickUpload({ name: 'video1' })
61 await waitJobs([ server ])
62
63 // eslint-disable-next-line no-unmodified-loop-condition
64 while (pings !== 1) {
65 await wait(500)
66 }
67
68 await server.videos.quickUpload({ name: 'video2' })
69 await waitJobs([ server ])
70
71 // eslint-disable-next-line no-unmodified-loop-condition
72 while ((pings as number) !== 2) {
73 await wait(500)
74 }
75
76 await server.runnerJobs.cancelAllJobs()
77 })
78
79 it('Should send a ping when a child is ready', async function () {
80 let pings = 0
81 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
82 localSocket.on('available-jobs', () => pings++)
83
84 await server.videos.quickUpload({ name: 'video3' })
85 await waitJobs([ server ])
86
87 // eslint-disable-next-line no-unmodified-loop-condition
88 while (pings !== 1) {
89 await wait(500)
90 }
91
92 await server.runnerJobs.autoProcessWebVideoJob(runnerToken)
93 await waitJobs([ server ])
94
95 // eslint-disable-next-line no-unmodified-loop-condition
96 while ((pings as number) !== 2) {
97 await wait(500)
98 }
99 })
100
101 it('Should not send a ping if the ended job does not have a child', async function () {
102 let pings = 0
103 const localSocket = server.socketIO.getRunnersSocket({ runnerToken })
104 localSocket.on('available-jobs', () => pings++)
105
106 const { availableJobs } = await server.runnerJobs.request({ runnerToken })
107 const job = availableJobs.find(j => j.type === 'vod-web-video-transcoding')
108 await server.runnerJobs.autoProcessWebVideoJob(runnerToken, job.uuid)
109
110 // Wait for debounce
111 await wait(1000)
112 await waitJobs([ server ])
113
114 expect(pings).to.equal(0)
115 })
116
117 after(async function () {
118 await cleanupTests([ server ])
119 })
120})
diff --git a/packages/tests/src/api/runners/runner-studio-transcoding.ts b/packages/tests/src/api/runners/runner-studio-transcoding.ts
new file mode 100644
index 000000000..adf6941c3
--- /dev/null
+++ b/packages/tests/src/api/runners/runner-studio-transcoding.ts
@@ -0,0 +1,169 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { readFile } from 'fs/promises'
5import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
6import {
7 RunnerJobStudioTranscodingPayload,
8 VideoStudioTranscodingSuccess,
9 VideoState,
10 VideoStudioTask,
11 VideoStudioTaskIntro
12} from '@peertube/peertube-models'
13import {
14 cleanupTests,
15 createMultipleServers,
16 doubleFollow,
17 PeerTubeServer,
18 setAccessTokensToServers,
19 setDefaultVideoChannel,
20 VideoStudioCommand,
21 waitJobs
22} from '@peertube/peertube-server-commands'
23import { checkVideoDuration } from '@tests/shared/checks.js'
24import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js'
25
26describe('Test runner video studio transcoding', function () {
27 let servers: PeerTubeServer[] = []
28 let runnerToken: string
29 let videoUUID: string
30 let jobUUID: string
31
32 async function renewStudio (tasks: VideoStudioTask[] = VideoStudioCommand.getComplexTask()) {
33 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
34 videoUUID = uuid
35
36 await waitJobs(servers)
37
38 await servers[0].videoStudio.createEditionTasks({ videoId: uuid, tasks })
39 await waitJobs(servers)
40
41 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
42 expect(availableJobs).to.have.lengthOf(1)
43
44 jobUUID = availableJobs[0].uuid
45 }
46
47 before(async function () {
48 this.timeout(120_000)
49
50 servers = await createMultipleServers(2)
51
52 await setAccessTokensToServers(servers)
53 await setDefaultVideoChannel(servers)
54
55 await doubleFollow(servers[0], servers[1])
56
57 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
58 await servers[0].config.enableStudio()
59 await servers[0].config.enableRemoteStudio()
60
61 runnerToken = await servers[0].runners.autoRegisterRunner()
62 })
63
64 it('Should error a studio transcoding job', async function () {
65 this.timeout(60000)
66
67 await renewStudio()
68
69 for (let i = 0; i < 5; i++) {
70 const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
71 const jobToken = job.jobToken
72
73 await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
74 }
75
76 const video = await servers[0].videos.get({ id: videoUUID })
77 expect(video.state.id).to.equal(VideoState.PUBLISHED)
78
79 await checkPersistentTmpIsEmpty(servers[0])
80 })
81
82 it('Should cancel a transcoding job', async function () {
83 this.timeout(60000)
84
85 await renewStudio()
86
87 await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
88
89 const video = await servers[0].videos.get({ id: videoUUID })
90 expect(video.state.id).to.equal(VideoState.PUBLISHED)
91
92 await checkPersistentTmpIsEmpty(servers[0])
93 })
94
95 it('Should execute a remote studio job', async function () {
96 this.timeout(240_000)
97
98 const tasks = [
99 {
100 name: 'add-outro' as 'add-outro',
101 options: {
102 file: 'video_short.webm'
103 }
104 },
105 {
106 name: 'add-watermark' as 'add-watermark',
107 options: {
108 file: 'custom-thumbnail.png'
109 }
110 },
111 {
112 name: 'add-intro' as 'add-intro',
113 options: {
114 file: 'video_very_short_240p.mp4'
115 }
116 }
117 ]
118
119 await renewStudio(tasks)
120
121 for (const server of servers) {
122 await checkVideoDuration(server, videoUUID, 5)
123 }
124
125 const { job } = await servers[0].runnerJobs.accept<RunnerJobStudioTranscodingPayload>({ runnerToken, jobUUID })
126 const jobToken = job.jobToken
127
128 expect(job.type === 'video-studio-transcoding')
129 expect(job.payload.input.videoFileUrl).to.exist
130
131 // Check video input file
132 {
133 await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
134 }
135
136 // Check task files
137 for (let i = 0; i < tasks.length; i++) {
138 const task = tasks[i]
139 const payloadTask = job.payload.tasks[i]
140
141 expect(payloadTask.name).to.equal(task.name)
142
143 const inputFile = await readFile(buildAbsoluteFixturePath(task.options.file))
144
145 const { body } = await servers[0].runnerJobs.getJobFile({
146 url: (payloadTask as VideoStudioTaskIntro).options.file as string,
147 jobToken,
148 runnerToken
149 })
150
151 expect(body).to.deep.equal(inputFile)
152 }
153
154 const payload: VideoStudioTranscodingSuccess = { videoFile: 'video_very_short_240p.mp4' }
155 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
156
157 await waitJobs(servers)
158
159 for (const server of servers) {
160 await checkVideoDuration(server, videoUUID, 2)
161 }
162
163 await checkPersistentTmpIsEmpty(servers[0])
164 })
165
166 after(async function () {
167 await cleanupTests(servers)
168 })
169})
diff --git a/packages/tests/src/api/runners/runner-vod-transcoding.ts b/packages/tests/src/api/runners/runner-vod-transcoding.ts
new file mode 100644
index 000000000..fe1c8f0b2
--- /dev/null
+++ b/packages/tests/src/api/runners/runner-vod-transcoding.ts
@@ -0,0 +1,545 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { readFile } from 'fs/promises'
5import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
6import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
7import {
8 HttpStatusCode,
9 RunnerJobSuccessPayload,
10 RunnerJobVODAudioMergeTranscodingPayload,
11 RunnerJobVODHLSTranscodingPayload,
12 RunnerJobVODPayload,
13 RunnerJobVODWebVideoTranscodingPayload,
14 VideoState,
15 VODAudioMergeTranscodingSuccess,
16 VODHLSTranscodingSuccess,
17 VODWebVideoTranscodingSuccess
18} from '@peertube/peertube-models'
19import {
20 cleanupTests,
21 createMultipleServers,
22 doubleFollow,
23 makeGetRequest,
24 makeRawRequest,
25 PeerTubeServer,
26 setAccessTokensToServers,
27 setDefaultVideoChannel,
28 waitJobs
29} from '@peertube/peertube-server-commands'
30
31async function processAllJobs (server: PeerTubeServer, runnerToken: string) {
32 do {
33 const { availableJobs } = await server.runnerJobs.requestVOD({ runnerToken })
34 if (availableJobs.length === 0) break
35
36 const { job } = await server.runnerJobs.accept<RunnerJobVODPayload>({ runnerToken, jobUUID: availableJobs[0].uuid })
37
38 const payload: RunnerJobSuccessPayload = {
39 videoFile: `video_short_${job.payload.output.resolution}p.mp4`,
40 resolutionPlaylistFile: `video_short_${job.payload.output.resolution}p.m3u8`
41 }
42 await server.runnerJobs.success({ runnerToken, jobUUID: job.uuid, jobToken: job.jobToken, payload })
43 } while (true)
44
45 await waitJobs([ server ])
46}
47
48describe('Test runner VOD transcoding', function () {
49 let servers: PeerTubeServer[] = []
50 let runnerToken: string
51
52 before(async function () {
53 this.timeout(120_000)
54
55 servers = await createMultipleServers(2)
56
57 await setAccessTokensToServers(servers)
58 await setDefaultVideoChannel(servers)
59
60 await doubleFollow(servers[0], servers[1])
61
62 await servers[0].config.enableRemoteTranscoding()
63 runnerToken = await servers[0].runners.autoRegisterRunner()
64 })
65
66 describe('Without transcoding', function () {
67
68 before(async function () {
69 this.timeout(60000)
70
71 await servers[0].config.disableTranscoding()
72 await servers[0].videos.quickUpload({ name: 'video' })
73
74 await waitJobs(servers)
75 })
76
77 it('Should not have available jobs', async function () {
78 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
79 expect(availableJobs).to.have.lengthOf(0)
80 })
81 })
82
83 describe('With classic transcoding enabled', function () {
84
85 before(async function () {
86 this.timeout(60000)
87
88 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
89 })
90
91 it('Should error a transcoding job', async function () {
92 this.timeout(60000)
93
94 await servers[0].runnerJobs.cancelAllJobs()
95 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
96 await waitJobs(servers)
97
98 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
99 const jobUUID = availableJobs[0].uuid
100
101 for (let i = 0; i < 5; i++) {
102 const { job } = await servers[0].runnerJobs.accept({ runnerToken, jobUUID })
103 const jobToken = job.jobToken
104
105 await servers[0].runnerJobs.error({ runnerToken, jobUUID, jobToken, message: 'Error' })
106 }
107
108 const video = await servers[0].videos.get({ id: uuid })
109 expect(video.state.id).to.equal(VideoState.TRANSCODING_FAILED)
110 })
111
112 it('Should cancel a transcoding job', async function () {
113 await servers[0].runnerJobs.cancelAllJobs()
114 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
115 await waitJobs(servers)
116
117 const { availableJobs } = await servers[0].runnerJobs.request({ runnerToken })
118 const jobUUID = availableJobs[0].uuid
119
120 await servers[0].runnerJobs.cancelByAdmin({ jobUUID })
121
122 const video = await servers[0].videos.get({ id: uuid })
123 expect(video.state.id).to.equal(VideoState.PUBLISHED)
124 })
125 })
126
127 describe('Web video transcoding only', function () {
128 let videoUUID: string
129 let jobToken: string
130 let jobUUID: string
131
132 before(async function () {
133 this.timeout(60000)
134
135 await servers[0].runnerJobs.cancelAllJobs()
136 await servers[0].config.enableTranscoding({ hls: false, webVideo: true })
137
138 const { uuid } = await servers[0].videos.quickUpload({ name: 'web video', fixture: 'video_short.webm' })
139 videoUUID = uuid
140
141 await waitJobs(servers)
142 })
143
144 it('Should have jobs available for remote runners', async function () {
145 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
146 expect(availableJobs).to.have.lengthOf(1)
147
148 jobUUID = availableJobs[0].uuid
149 })
150
151 it('Should have a valid first transcoding job', async function () {
152 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
153 jobToken = job.jobToken
154
155 expect(job.type === 'vod-web-video-transcoding')
156 expect(job.payload.input.videoFileUrl).to.exist
157 expect(job.payload.output.resolution).to.equal(720)
158 expect(job.payload.output.fps).to.equal(25)
159
160 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
161 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.webm'))
162
163 expect(body).to.deep.equal(inputFile)
164 })
165
166 it('Should transcode the max video resolution and send it back to the server', async function () {
167 this.timeout(60000)
168
169 const payload: VODWebVideoTranscodingSuccess = {
170 videoFile: 'video_short.mp4'
171 }
172 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
173
174 await waitJobs(servers)
175 })
176
177 it('Should have the video updated', async function () {
178 for (const server of servers) {
179 const video = await server.videos.get({ id: videoUUID })
180 expect(video.files).to.have.lengthOf(1)
181 expect(video.streamingPlaylists).to.have.lengthOf(0)
182
183 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
184 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
185 }
186 })
187
188 it('Should have 4 lower resolution to transcode', async function () {
189 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
190 expect(availableJobs).to.have.lengthOf(4)
191
192 for (const resolution of [ 480, 360, 240, 144 ]) {
193 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
194 expect(job).to.exist
195 expect(job.type).to.equal('vod-web-video-transcoding')
196
197 if (resolution === 240) jobUUID = job.uuid
198 }
199 })
200
201 it('Should process one of these transcoding jobs', async function () {
202 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
203 jobToken = job.jobToken
204
205 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
206 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
207
208 expect(body).to.deep.equal(inputFile)
209
210 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${job.payload.output.resolution}p.mp4` }
211 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
212 })
213
214 it('Should process all other jobs', async function () {
215 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
216 expect(availableJobs).to.have.lengthOf(3)
217
218 for (const resolution of [ 480, 360, 144 ]) {
219 const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
220 expect(availableJob).to.exist
221 jobUUID = availableJob.uuid
222
223 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODWebVideoTranscodingPayload>({ runnerToken, jobUUID })
224 jobToken = job.jobToken
225
226 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
227 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
228 expect(body).to.deep.equal(inputFile)
229
230 const payload: VODWebVideoTranscodingSuccess = { videoFile: `video_short_${resolution}p.mp4` }
231 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
232 }
233
234 await waitJobs(servers)
235 })
236
237 it('Should have the video updated', async function () {
238 for (const server of servers) {
239 const video = await server.videos.get({ id: videoUUID })
240 expect(video.files).to.have.lengthOf(5)
241 expect(video.streamingPlaylists).to.have.lengthOf(0)
242
243 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
244 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short.mp4')))
245
246 for (const file of video.files) {
247 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
248 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
249 }
250 }
251 })
252
253 it('Should not have available jobs anymore', async function () {
254 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
255 expect(availableJobs).to.have.lengthOf(0)
256 })
257 })
258
259 describe('HLS transcoding only', function () {
260 let videoUUID: string
261 let jobToken: string
262 let jobUUID: string
263
264 before(async function () {
265 this.timeout(60000)
266
267 await servers[0].config.enableTranscoding({ hls: true, webVideo: false })
268
269 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls video', fixture: 'video_short.webm' })
270 videoUUID = uuid
271
272 await waitJobs(servers)
273 })
274
275 it('Should run the optimize job', async function () {
276 this.timeout(60000)
277
278 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
279 })
280
281 it('Should have 5 HLS resolution to transcode', async function () {
282 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
283 expect(availableJobs).to.have.lengthOf(5)
284
285 for (const resolution of [ 720, 480, 360, 240, 144 ]) {
286 const job = availableJobs.find(j => j.payload.output.resolution === resolution)
287 expect(job).to.exist
288 expect(job.type).to.equal('vod-hls-transcoding')
289
290 if (resolution === 480) jobUUID = job.uuid
291 }
292 })
293
294 it('Should process one of these transcoding jobs', async function () {
295 this.timeout(60000)
296
297 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
298 jobToken = job.jobToken
299
300 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
301 const inputFile = await readFile(buildAbsoluteFixturePath('video_short.mp4'))
302
303 expect(body).to.deep.equal(inputFile)
304
305 const payload: VODHLSTranscodingSuccess = {
306 videoFile: 'video_short_480p.mp4',
307 resolutionPlaylistFile: 'video_short_480p.m3u8'
308 }
309 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
310
311 await waitJobs(servers)
312 })
313
314 it('Should have the video updated', async function () {
315 for (const server of servers) {
316 const video = await server.videos.get({ id: videoUUID })
317
318 expect(video.files).to.have.lengthOf(1)
319 expect(video.streamingPlaylists).to.have.lengthOf(1)
320
321 const hls = video.streamingPlaylists[0]
322 expect(hls.files).to.have.lengthOf(1)
323
324 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
325 }
326 })
327
328 it('Should process all other jobs', async function () {
329 this.timeout(60000)
330
331 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
332 expect(availableJobs).to.have.lengthOf(4)
333
334 let maxQualityFile = 'video_short.mp4'
335
336 for (const resolution of [ 720, 360, 240, 144 ]) {
337 const availableJob = availableJobs.find(j => j.payload.output.resolution === resolution)
338 expect(availableJob).to.exist
339 jobUUID = availableJob.uuid
340
341 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
342 jobToken = job.jobToken
343
344 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
345 const inputFile = await readFile(buildAbsoluteFixturePath(maxQualityFile))
346 expect(body).to.deep.equal(inputFile)
347
348 const payload: VODHLSTranscodingSuccess = {
349 videoFile: `video_short_${resolution}p.mp4`,
350 resolutionPlaylistFile: `video_short_${resolution}p.m3u8`
351 }
352 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
353
354 if (resolution === 720) {
355 maxQualityFile = 'video_short_720p.mp4'
356 }
357 }
358
359 await waitJobs(servers)
360 })
361
362 it('Should have the video updated', async function () {
363 for (const server of servers) {
364 const video = await server.videos.get({ id: videoUUID })
365
366 expect(video.files).to.have.lengthOf(0)
367 expect(video.streamingPlaylists).to.have.lengthOf(1)
368
369 const hls = video.streamingPlaylists[0]
370 expect(hls.files).to.have.lengthOf(5)
371
372 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: true, servers, resolutions: [ 720, 480, 360, 240, 144 ] })
373 }
374 })
375
376 it('Should not have available jobs anymore', async function () {
377 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
378 expect(availableJobs).to.have.lengthOf(0)
379 })
380 })
381
382 describe('Web video and HLS transcoding', function () {
383
384 before(async function () {
385 this.timeout(60000)
386
387 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
388
389 await servers[0].videos.quickUpload({ name: 'web video and hls video', fixture: 'video_short.webm' })
390
391 await waitJobs(servers)
392 })
393
394 it('Should process the first optimize job', async function () {
395 this.timeout(60000)
396
397 await servers[0].runnerJobs.autoProcessWebVideoJob(runnerToken)
398 })
399
400 it('Should have 9 jobs to process', async function () {
401 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
402
403 expect(availableJobs).to.have.lengthOf(9)
404
405 const webVideoJobs = availableJobs.filter(j => j.type === 'vod-web-video-transcoding')
406 const hlsJobs = availableJobs.filter(j => j.type === 'vod-hls-transcoding')
407
408 expect(webVideoJobs).to.have.lengthOf(4)
409 expect(hlsJobs).to.have.lengthOf(5)
410 })
411
412 it('Should process all available jobs', async function () {
413 await processAllJobs(servers[0], runnerToken)
414 })
415 })
416
417 describe('Audio merge transcoding', function () {
418 let videoUUID: string
419 let jobToken: string
420 let jobUUID: string
421
422 before(async function () {
423 this.timeout(60000)
424
425 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
426
427 const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
428 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
429 videoUUID = uuid
430
431 await waitJobs(servers)
432 })
433
434 it('Should have an audio merge transcoding job', async function () {
435 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
436 expect(availableJobs).to.have.lengthOf(1)
437
438 expect(availableJobs[0].type).to.equal('vod-audio-merge-transcoding')
439
440 jobUUID = availableJobs[0].uuid
441 })
442
443 it('Should have a valid remote audio merge transcoding job', async function () {
444 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODAudioMergeTranscodingPayload>({ runnerToken, jobUUID })
445 jobToken = job.jobToken
446
447 expect(job.type === 'vod-audio-merge-transcoding')
448 expect(job.payload.input.audioFileUrl).to.exist
449 expect(job.payload.input.previewFileUrl).to.exist
450 expect(job.payload.output.resolution).to.equal(480)
451
452 {
453 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.audioFileUrl, jobToken, runnerToken })
454 const inputFile = await readFile(buildAbsoluteFixturePath('sample.ogg'))
455 expect(body).to.deep.equal(inputFile)
456 }
457
458 {
459 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.previewFileUrl, jobToken, runnerToken })
460
461 const video = await servers[0].videos.get({ id: videoUUID })
462 const { body: inputFile } = await makeGetRequest({
463 url: servers[0].url,
464 path: video.previewPath,
465 expectedStatus: HttpStatusCode.OK_200
466 })
467
468 expect(body).to.deep.equal(inputFile)
469 }
470 })
471
472 it('Should merge the audio', async function () {
473 this.timeout(60000)
474
475 const payload: VODAudioMergeTranscodingSuccess = { videoFile: 'video_short_480p.mp4' }
476 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
477
478 await waitJobs(servers)
479 })
480
481 it('Should have the video updated', async function () {
482 for (const server of servers) {
483 const video = await server.videos.get({ id: videoUUID })
484 expect(video.files).to.have.lengthOf(1)
485 expect(video.streamingPlaylists).to.have.lengthOf(0)
486
487 const { body } = await makeRawRequest({ url: video.files[0].fileUrl, expectedStatus: HttpStatusCode.OK_200 })
488 expect(body).to.deep.equal(await readFile(buildAbsoluteFixturePath('video_short_480p.mp4')))
489 }
490 })
491
492 it('Should have 7 lower resolutions to transcode', async function () {
493 const { availableJobs } = await servers[0].runnerJobs.requestVOD({ runnerToken })
494 expect(availableJobs).to.have.lengthOf(7)
495
496 for (const resolution of [ 360, 240, 144 ]) {
497 const jobs = availableJobs.filter(j => j.payload.output.resolution === resolution)
498 expect(jobs).to.have.lengthOf(2)
499 }
500
501 jobUUID = availableJobs.find(j => j.payload.output.resolution === 480).uuid
502 })
503
504 it('Should process one other job', async function () {
505 this.timeout(60000)
506
507 const { job } = await servers[0].runnerJobs.accept<RunnerJobVODHLSTranscodingPayload>({ runnerToken, jobUUID })
508 jobToken = job.jobToken
509
510 const { body } = await servers[0].runnerJobs.getJobFile({ url: job.payload.input.videoFileUrl, jobToken, runnerToken })
511 const inputFile = await readFile(buildAbsoluteFixturePath('video_short_480p.mp4'))
512 expect(body).to.deep.equal(inputFile)
513
514 const payload: VODHLSTranscodingSuccess = {
515 videoFile: `video_short_480p.mp4`,
516 resolutionPlaylistFile: `video_short_480p.m3u8`
517 }
518 await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
519
520 await waitJobs(servers)
521 })
522
523 it('Should have the video updated', async function () {
524 for (const server of servers) {
525 const video = await server.videos.get({ id: videoUUID })
526
527 expect(video.files).to.have.lengthOf(1)
528 expect(video.streamingPlaylists).to.have.lengthOf(1)
529
530 const hls = video.streamingPlaylists[0]
531 expect(hls.files).to.have.lengthOf(1)
532
533 await completeCheckHlsPlaylist({ videoUUID, hlsOnly: false, servers, resolutions: [ 480 ] })
534 }
535 })
536
537 it('Should process all available jobs', async function () {
538 await processAllJobs(servers[0], runnerToken)
539 })
540 })
541
542 after(async function () {
543 await cleanupTests(servers)
544 })
545})
diff --git a/packages/tests/src/api/search/index.ts b/packages/tests/src/api/search/index.ts
new file mode 100644
index 000000000..f4420261d
--- /dev/null
+++ b/packages/tests/src/api/search/index.ts
@@ -0,0 +1,7 @@
1import './search-activitypub-video-playlists.js'
2import './search-activitypub-video-channels.js'
3import './search-activitypub-videos.js'
4import './search-channels.js'
5import './search-index.js'
6import './search-playlists.js'
7import './search-videos.js'
diff --git a/packages/tests/src/api/search/search-activitypub-video-channels.ts b/packages/tests/src/api/search/search-activitypub-video-channels.ts
new file mode 100644
index 000000000..b63f45b18
--- /dev/null
+++ b/packages/tests/src/api/search/search-activitypub-video-channels.ts
@@ -0,0 +1,255 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideoChannel } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 PeerTubeServer,
10 SearchCommand,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test ActivityPub video channels search', function () {
18 let servers: PeerTubeServer[]
19 let userServer2Token: string
20 let videoServer2UUID: string
21 let channelIdServer2: number
22 let command: SearchCommand
23
24 before(async function () {
25 this.timeout(120000)
26
27 servers = await createMultipleServers(2)
28
29 await setAccessTokensToServers(servers)
30 await setDefaultVideoChannel(servers)
31 await setDefaultAccountAvatar(servers)
32
33 {
34 await servers[0].users.create({ username: 'user1_server1', password: 'password' })
35 const channel = {
36 name: 'channel1_server1',
37 displayName: 'Channel 1 server 1'
38 }
39 await servers[0].channels.create({ attributes: channel })
40 }
41
42 {
43 const user = { username: 'user1_server2', password: 'password' }
44 await servers[1].users.create({ username: user.username, password: user.password })
45 userServer2Token = await servers[1].login.getAccessToken(user)
46
47 const channel = {
48 name: 'channel1_server2',
49 displayName: 'Channel 1 server 2'
50 }
51 const created = await servers[1].channels.create({ token: userServer2Token, attributes: channel })
52 channelIdServer2 = created.id
53
54 const attributes = { name: 'video 1 server 2', channelId: channelIdServer2 }
55 const { uuid } = await servers[1].videos.upload({ token: userServer2Token, attributes })
56 videoServer2UUID = uuid
57 }
58
59 await waitJobs(servers)
60
61 command = servers[0].search
62 })
63
64 it('Should not find a remote video channel', async function () {
65 this.timeout(15000)
66
67 {
68 const search = servers[1].url + '/video-channels/channel1_server3'
69 const body = await command.searchChannels({ search, token: servers[0].accessToken })
70
71 expect(body.total).to.equal(0)
72 expect(body.data).to.be.an('array')
73 expect(body.data).to.have.lengthOf(0)
74 }
75
76 {
77 // Without token
78 const search = servers[1].url + '/video-channels/channel1_server2'
79 const body = await command.searchChannels({ search })
80
81 expect(body.total).to.equal(0)
82 expect(body.data).to.be.an('array')
83 expect(body.data).to.have.lengthOf(0)
84 }
85 })
86
87 it('Should search a local video channel', async function () {
88 const searches = [
89 servers[0].url + '/video-channels/channel1_server1',
90 'channel1_server1@' + servers[0].host
91 ]
92
93 for (const search of searches) {
94 const body = await command.searchChannels({ search })
95
96 expect(body.total).to.equal(1)
97 expect(body.data).to.be.an('array')
98 expect(body.data).to.have.lengthOf(1)
99 expect(body.data[0].name).to.equal('channel1_server1')
100 expect(body.data[0].displayName).to.equal('Channel 1 server 1')
101 }
102 })
103
104 it('Should search a local video channel with an alternative URL', async function () {
105 const search = servers[0].url + '/c/channel1_server1'
106
107 for (const token of [ undefined, servers[0].accessToken ]) {
108 const body = await command.searchChannels({ search, token })
109
110 expect(body.total).to.equal(1)
111 expect(body.data).to.be.an('array')
112 expect(body.data).to.have.lengthOf(1)
113 expect(body.data[0].name).to.equal('channel1_server1')
114 expect(body.data[0].displayName).to.equal('Channel 1 server 1')
115 }
116 })
117
118 it('Should search a local video channel with a query in URL', async function () {
119 const searches = [
120 servers[0].url + '/video-channels/channel1_server1',
121 servers[0].url + '/c/channel1_server1'
122 ]
123
124 for (const search of searches) {
125 for (const token of [ undefined, servers[0].accessToken ]) {
126 const body = await command.searchChannels({ search: search + '?param=2', token })
127
128 expect(body.total).to.equal(1)
129 expect(body.data).to.be.an('array')
130 expect(body.data).to.have.lengthOf(1)
131 expect(body.data[0].name).to.equal('channel1_server1')
132 expect(body.data[0].displayName).to.equal('Channel 1 server 1')
133 }
134 }
135 })
136
137 it('Should search a remote video channel with URL or handle', async function () {
138 const searches = [
139 servers[1].url + '/video-channels/channel1_server2',
140 servers[1].url + '/c/channel1_server2',
141 servers[1].url + '/c/channel1_server2/videos',
142 'channel1_server2@' + servers[1].host
143 ]
144
145 for (const search of searches) {
146 const body = await command.searchChannels({ search, token: servers[0].accessToken })
147
148 expect(body.total).to.equal(1)
149 expect(body.data).to.be.an('array')
150 expect(body.data).to.have.lengthOf(1)
151 expect(body.data[0].name).to.equal('channel1_server2')
152 expect(body.data[0].displayName).to.equal('Channel 1 server 2')
153 }
154 })
155
156 it('Should not list this remote video channel', async function () {
157 const body = await servers[0].channels.list()
158 expect(body.total).to.equal(3)
159 expect(body.data).to.have.lengthOf(3)
160 expect(body.data[0].name).to.equal('channel1_server1')
161 expect(body.data[1].name).to.equal('user1_server1_channel')
162 expect(body.data[2].name).to.equal('root_channel')
163 })
164
165 it('Should list video channel videos of server 2 without token', async function () {
166 this.timeout(30000)
167
168 await waitJobs(servers)
169
170 const { total, data } = await servers[0].videos.listByChannel({
171 token: null,
172 handle: 'channel1_server2@' + servers[1].host
173 })
174 expect(total).to.equal(0)
175 expect(data).to.have.lengthOf(0)
176 })
177
178 it('Should list video channel videos of server 2 with token', async function () {
179 const { total, data } = await servers[0].videos.listByChannel({
180 handle: 'channel1_server2@' + servers[1].host
181 })
182
183 expect(total).to.equal(1)
184 expect(data[0].name).to.equal('video 1 server 2')
185 })
186
187 it('Should update video channel of server 2, and refresh it on server 1', async function () {
188 this.timeout(120000)
189
190 await servers[1].channels.update({
191 token: userServer2Token,
192 channelName: 'channel1_server2',
193 attributes: { displayName: 'channel updated' }
194 })
195 await servers[1].users.updateMe({ token: userServer2Token, displayName: 'user updated' })
196
197 await waitJobs(servers)
198 // Expire video channel
199 await wait(10000)
200
201 const search = servers[1].url + '/video-channels/channel1_server2'
202 const body = await command.searchChannels({ search, token: servers[0].accessToken })
203 expect(body.total).to.equal(1)
204 expect(body.data).to.have.lengthOf(1)
205
206 const videoChannel: VideoChannel = body.data[0]
207 expect(videoChannel.displayName).to.equal('channel updated')
208
209 // We don't return the owner account for now
210 // expect(videoChannel.ownerAccount.displayName).to.equal('user updated')
211 })
212
213 it('Should update and add a video on server 2, and update it on server 1 after a search', async function () {
214 this.timeout(120000)
215
216 await servers[1].videos.update({ token: userServer2Token, id: videoServer2UUID, attributes: { name: 'video 1 updated' } })
217 await servers[1].videos.upload({ token: userServer2Token, attributes: { name: 'video 2 server 2', channelId: channelIdServer2 } })
218
219 await waitJobs(servers)
220
221 // Expire video channel
222 await wait(10000)
223
224 const search = servers[1].url + '/video-channels/channel1_server2'
225 await command.searchChannels({ search, token: servers[0].accessToken })
226
227 await waitJobs(servers)
228
229 const handle = 'channel1_server2@' + servers[1].host
230 const { total, data } = await servers[0].videos.listByChannel({ handle, sort: '-createdAt' })
231
232 expect(total).to.equal(2)
233 expect(data[0].name).to.equal('video 2 server 2')
234 expect(data[1].name).to.equal('video 1 updated')
235 })
236
237 it('Should delete video channel of server 2, and delete it on server 1', async function () {
238 this.timeout(120000)
239
240 await servers[1].channels.delete({ token: userServer2Token, channelName: 'channel1_server2' })
241
242 await waitJobs(servers)
243 // Expire video
244 await wait(10000)
245
246 const search = servers[1].url + '/video-channels/channel1_server2'
247 const body = await command.searchChannels({ search, token: servers[0].accessToken })
248 expect(body.total).to.equal(0)
249 expect(body.data).to.have.lengthOf(0)
250 })
251
252 after(async function () {
253 await cleanupTests(servers)
254 })
255})
diff --git a/packages/tests/src/api/search/search-activitypub-video-playlists.ts b/packages/tests/src/api/search/search-activitypub-video-playlists.ts
new file mode 100644
index 000000000..33ecfd8e7
--- /dev/null
+++ b/packages/tests/src/api/search/search-activitypub-video-playlists.ts
@@ -0,0 +1,214 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 PeerTubeServer,
10 SearchCommand,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test ActivityPub playlists search', function () {
18 let servers: PeerTubeServer[]
19 let playlistServer1UUID: string
20 let playlistServer2UUID: string
21 let video2Server2: string
22
23 let command: SearchCommand
24
25 before(async function () {
26 this.timeout(240000)
27
28 servers = await createMultipleServers(2)
29
30 await setAccessTokensToServers(servers)
31 await setDefaultVideoChannel(servers)
32 await setDefaultAccountAvatar(servers)
33
34 {
35 const video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).uuid
36 const video2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).uuid
37
38 const attributes = {
39 displayName: 'playlist 1 on server 1',
40 privacy: VideoPlaylistPrivacy.PUBLIC,
41 videoChannelId: servers[0].store.channel.id
42 }
43 const created = await servers[0].playlists.create({ attributes })
44 playlistServer1UUID = created.uuid
45
46 for (const videoId of [ video1, video2 ]) {
47 await servers[0].playlists.addElement({ playlistId: playlistServer1UUID, attributes: { videoId } })
48 }
49 }
50
51 {
52 const videoId = (await servers[1].videos.quickUpload({ name: 'video 1' })).uuid
53 video2Server2 = (await servers[1].videos.quickUpload({ name: 'video 2' })).uuid
54
55 const attributes = {
56 displayName: 'playlist 1 on server 2',
57 privacy: VideoPlaylistPrivacy.PUBLIC,
58 videoChannelId: servers[1].store.channel.id
59 }
60 const created = await servers[1].playlists.create({ attributes })
61 playlistServer2UUID = created.uuid
62
63 await servers[1].playlists.addElement({ playlistId: playlistServer2UUID, attributes: { videoId } })
64 }
65
66 await waitJobs(servers)
67
68 command = servers[0].search
69 })
70
71 it('Should not find a remote playlist', async function () {
72 {
73 const search = servers[1].url + '/video-playlists/43'
74 const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
75
76 expect(body.total).to.equal(0)
77 expect(body.data).to.be.an('array')
78 expect(body.data).to.have.lengthOf(0)
79 }
80
81 {
82 // Without token
83 const search = servers[1].url + '/video-playlists/' + playlistServer2UUID
84 const body = await command.searchPlaylists({ search })
85
86 expect(body.total).to.equal(0)
87 expect(body.data).to.be.an('array')
88 expect(body.data).to.have.lengthOf(0)
89 }
90 })
91
92 it('Should search a local playlist', async function () {
93 const search = servers[0].url + '/video-playlists/' + playlistServer1UUID
94 const body = await command.searchPlaylists({ search })
95
96 expect(body.total).to.equal(1)
97 expect(body.data).to.be.an('array')
98 expect(body.data).to.have.lengthOf(1)
99 expect(body.data[0].displayName).to.equal('playlist 1 on server 1')
100 expect(body.data[0].videosLength).to.equal(2)
101 })
102
103 it('Should search a local playlist with an alternative URL', async function () {
104 const searches = [
105 servers[0].url + '/videos/watch/playlist/' + playlistServer1UUID,
106 servers[0].url + '/w/p/' + playlistServer1UUID
107 ]
108
109 for (const search of searches) {
110 for (const token of [ undefined, servers[0].accessToken ]) {
111 const body = await command.searchPlaylists({ search, token })
112
113 expect(body.total).to.equal(1)
114 expect(body.data).to.be.an('array')
115 expect(body.data).to.have.lengthOf(1)
116 expect(body.data[0].displayName).to.equal('playlist 1 on server 1')
117 expect(body.data[0].videosLength).to.equal(2)
118 }
119 }
120 })
121
122 it('Should search a local playlist with a query in URL', async function () {
123 const searches = [
124 servers[0].url + '/videos/watch/playlist/' + playlistServer1UUID,
125 servers[0].url + '/w/p/' + playlistServer1UUID
126 ]
127
128 for (const search of searches) {
129 for (const token of [ undefined, servers[0].accessToken ]) {
130 const body = await command.searchPlaylists({ search: search + '?param=1', token })
131
132 expect(body.total).to.equal(1)
133 expect(body.data).to.be.an('array')
134 expect(body.data).to.have.lengthOf(1)
135 expect(body.data[0].displayName).to.equal('playlist 1 on server 1')
136 expect(body.data[0].videosLength).to.equal(2)
137 }
138 }
139 })
140
141 it('Should search a remote playlist', async function () {
142 const searches = [
143 servers[1].url + '/video-playlists/' + playlistServer2UUID,
144 servers[1].url + '/videos/watch/playlist/' + playlistServer2UUID,
145 servers[1].url + '/w/p/' + playlistServer2UUID
146 ]
147
148 for (const search of searches) {
149 const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
150
151 expect(body.total).to.equal(1)
152 expect(body.data).to.be.an('array')
153 expect(body.data).to.have.lengthOf(1)
154 expect(body.data[0].displayName).to.equal('playlist 1 on server 2')
155 expect(body.data[0].videosLength).to.equal(1)
156 }
157 })
158
159 it('Should not list this remote playlist', async function () {
160 const body = await servers[0].playlists.list({ start: 0, count: 10 })
161 expect(body.total).to.equal(1)
162 expect(body.data).to.have.lengthOf(1)
163 expect(body.data[0].displayName).to.equal('playlist 1 on server 1')
164 })
165
166 it('Should update the playlist of server 2, and refresh it on server 1', async function () {
167 this.timeout(60000)
168
169 await servers[1].playlists.addElement({ playlistId: playlistServer2UUID, attributes: { videoId: video2Server2 } })
170
171 await waitJobs(servers)
172 // Expire playlist
173 await wait(10000)
174
175 // Will run refresh async
176 const search = servers[1].url + '/video-playlists/' + playlistServer2UUID
177 await command.searchPlaylists({ search, token: servers[0].accessToken })
178
179 // Wait refresh
180 await wait(5000)
181
182 const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
183 expect(body.total).to.equal(1)
184 expect(body.data).to.have.lengthOf(1)
185
186 const playlist = body.data[0]
187 expect(playlist.videosLength).to.equal(2)
188 })
189
190 it('Should delete playlist of server 2, and delete it on server 1', async function () {
191 this.timeout(60000)
192
193 await servers[1].playlists.delete({ playlistId: playlistServer2UUID })
194
195 await waitJobs(servers)
196 // Expiration
197 await wait(10000)
198
199 // Will run refresh async
200 const search = servers[1].url + '/video-playlists/' + playlistServer2UUID
201 await command.searchPlaylists({ search, token: servers[0].accessToken })
202
203 // Wait refresh
204 await wait(5000)
205
206 const body = await command.searchPlaylists({ search, token: servers[0].accessToken })
207 expect(body.total).to.equal(0)
208 expect(body.data).to.have.lengthOf(0)
209 })
210
211 after(async function () {
212 await cleanupTests(servers)
213 })
214})
diff --git a/packages/tests/src/api/search/search-activitypub-videos.ts b/packages/tests/src/api/search/search-activitypub-videos.ts
new file mode 100644
index 000000000..72759f21e
--- /dev/null
+++ b/packages/tests/src/api/search/search-activitypub-videos.ts
@@ -0,0 +1,196 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 PeerTubeServer,
10 SearchCommand,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test ActivityPub videos search', function () {
18 let servers: PeerTubeServer[]
19 let videoServer1UUID: string
20 let videoServer2UUID: string
21
22 let command: SearchCommand
23
24 before(async function () {
25 this.timeout(120000)
26
27 servers = await createMultipleServers(2)
28
29 await setAccessTokensToServers(servers)
30 await setDefaultVideoChannel(servers)
31 await setDefaultAccountAvatar(servers)
32
33 {
34 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1 on server 1' } })
35 videoServer1UUID = uuid
36 }
37
38 {
39 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video 1 on server 2' } })
40 videoServer2UUID = uuid
41 }
42
43 await waitJobs(servers)
44
45 command = servers[0].search
46 })
47
48 it('Should not find a remote video', async function () {
49 {
50 const search = servers[1].url + '/videos/watch/43'
51 const body = await command.searchVideos({ search, token: servers[0].accessToken })
52
53 expect(body.total).to.equal(0)
54 expect(body.data).to.be.an('array')
55 expect(body.data).to.have.lengthOf(0)
56 }
57
58 {
59 // Without token
60 const search = servers[1].url + '/videos/watch/' + videoServer2UUID
61 const body = await command.searchVideos({ search })
62
63 expect(body.total).to.equal(0)
64 expect(body.data).to.be.an('array')
65 expect(body.data).to.have.lengthOf(0)
66 }
67 })
68
69 it('Should search a local video', async function () {
70 const search = servers[0].url + '/videos/watch/' + videoServer1UUID
71 const body = await command.searchVideos({ search })
72
73 expect(body.total).to.equal(1)
74 expect(body.data).to.be.an('array')
75 expect(body.data).to.have.lengthOf(1)
76 expect(body.data[0].name).to.equal('video 1 on server 1')
77 })
78
79 it('Should search a local video with an alternative URL', async function () {
80 const search = servers[0].url + '/w/' + videoServer1UUID
81 const body1 = await command.searchVideos({ search })
82 const body2 = await command.searchVideos({ search, token: servers[0].accessToken })
83
84 for (const body of [ body1, body2 ]) {
85 expect(body.total).to.equal(1)
86 expect(body.data).to.be.an('array')
87 expect(body.data).to.have.lengthOf(1)
88 expect(body.data[0].name).to.equal('video 1 on server 1')
89 }
90 })
91
92 it('Should search a local video with a query in URL', async function () {
93 const searches = [
94 servers[0].url + '/w/' + videoServer1UUID,
95 servers[0].url + '/videos/watch/' + videoServer1UUID
96 ]
97
98 for (const search of searches) {
99 for (const token of [ undefined, servers[0].accessToken ]) {
100 const body = await command.searchVideos({ search: search + '?startTime=4', token })
101
102 expect(body.total).to.equal(1)
103 expect(body.data).to.be.an('array')
104 expect(body.data).to.have.lengthOf(1)
105 expect(body.data[0].name).to.equal('video 1 on server 1')
106 }
107 }
108 })
109
110 it('Should search a remote video', async function () {
111 const searches = [
112 servers[1].url + '/w/' + videoServer2UUID,
113 servers[1].url + '/videos/watch/' + videoServer2UUID
114 ]
115
116 for (const search of searches) {
117 const body = await command.searchVideos({ search, token: servers[0].accessToken })
118
119 expect(body.total).to.equal(1)
120 expect(body.data).to.be.an('array')
121 expect(body.data).to.have.lengthOf(1)
122 expect(body.data[0].name).to.equal('video 1 on server 2')
123 }
124 })
125
126 it('Should not list this remote video', async function () {
127 const { total, data } = await servers[0].videos.list()
128 expect(total).to.equal(1)
129 expect(data).to.have.lengthOf(1)
130 expect(data[0].name).to.equal('video 1 on server 1')
131 })
132
133 it('Should update video of server 2, and refresh it on server 1', async function () {
134 this.timeout(120000)
135
136 const channelAttributes = {
137 name: 'super_channel',
138 displayName: 'super channel'
139 }
140 const created = await servers[1].channels.create({ attributes: channelAttributes })
141 const videoChannelId = created.id
142
143 const attributes = {
144 name: 'updated',
145 tag: [ 'tag1', 'tag2' ],
146 privacy: VideoPrivacy.UNLISTED,
147 channelId: videoChannelId
148 }
149 await servers[1].videos.update({ id: videoServer2UUID, attributes })
150
151 await waitJobs(servers)
152 // Expire video
153 await wait(10000)
154
155 // Will run refresh async
156 const search = servers[1].url + '/videos/watch/' + videoServer2UUID
157 await command.searchVideos({ search, token: servers[0].accessToken })
158
159 // Wait refresh
160 await wait(5000)
161
162 const body = await command.searchVideos({ search, token: servers[0].accessToken })
163 expect(body.total).to.equal(1)
164 expect(body.data).to.have.lengthOf(1)
165
166 const video = body.data[0]
167 expect(video.name).to.equal('updated')
168 expect(video.channel.name).to.equal('super_channel')
169 expect(video.privacy.id).to.equal(VideoPrivacy.UNLISTED)
170 })
171
172 it('Should delete video of server 2, and delete it on server 1', async function () {
173 this.timeout(120000)
174
175 await servers[1].videos.remove({ id: videoServer2UUID })
176
177 await waitJobs(servers)
178 // Expire video
179 await wait(10000)
180
181 // Will run refresh async
182 const search = servers[1].url + '/videos/watch/' + videoServer2UUID
183 await command.searchVideos({ search, token: servers[0].accessToken })
184
185 // Wait refresh
186 await wait(5000)
187
188 const body = await command.searchVideos({ search, token: servers[0].accessToken })
189 expect(body.total).to.equal(0)
190 expect(body.data).to.have.lengthOf(0)
191 })
192
193 after(async function () {
194 await cleanupTests(servers)
195 })
196})
diff --git a/packages/tests/src/api/search/search-channels.ts b/packages/tests/src/api/search/search-channels.ts
new file mode 100644
index 000000000..36596e036
--- /dev/null
+++ b/packages/tests/src/api/search/search-channels.ts
@@ -0,0 +1,159 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { VideoChannel } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 doubleFollow,
9 PeerTubeServer,
10 SearchCommand,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar
14} from '@peertube/peertube-server-commands'
15
16describe('Test channels search', function () {
17 let server: PeerTubeServer
18 let remoteServer: PeerTubeServer
19 let command: SearchCommand
20
21 before(async function () {
22 this.timeout(120000)
23
24 const servers = await Promise.all([
25 createSingleServer(1),
26 createSingleServer(2)
27 ])
28 server = servers[0]
29 remoteServer = servers[1]
30
31 await setAccessTokensToServers([ server, remoteServer ])
32 await setDefaultChannelAvatar(server)
33 await setDefaultAccountAvatar(server)
34
35 await servers[1].config.disableTranscoding()
36
37 {
38 await server.users.create({ username: 'user1' })
39 const channel = {
40 name: 'squall_channel',
41 displayName: 'Squall channel'
42 }
43 await server.channels.create({ attributes: channel })
44 }
45
46 {
47 await remoteServer.users.create({ username: 'user1' })
48 const channel = {
49 name: 'zell_channel',
50 displayName: 'Zell channel'
51 }
52 const { id } = await remoteServer.channels.create({ attributes: channel })
53
54 await remoteServer.videos.upload({ attributes: { channelId: id } })
55 }
56
57 await doubleFollow(server, remoteServer)
58
59 command = server.search
60 })
61
62 it('Should make a simple search and not have results', async function () {
63 const body = await command.searchChannels({ search: 'abc' })
64
65 expect(body.total).to.equal(0)
66 expect(body.data).to.have.lengthOf(0)
67 })
68
69 it('Should make a search and have results', async function () {
70 {
71 const search = {
72 search: 'Squall',
73 start: 0,
74 count: 1
75 }
76 const body = await command.advancedChannelSearch({ search })
77 expect(body.total).to.equal(1)
78 expect(body.data).to.have.lengthOf(1)
79
80 const channel: VideoChannel = body.data[0]
81 expect(channel.name).to.equal('squall_channel')
82 expect(channel.displayName).to.equal('Squall channel')
83 }
84
85 {
86 const search = {
87 search: 'Squall',
88 start: 1,
89 count: 1
90 }
91
92 const body = await command.advancedChannelSearch({ search })
93 expect(body.total).to.equal(1)
94 expect(body.data).to.have.lengthOf(0)
95 }
96 })
97
98 it('Should filter by host', async function () {
99 {
100 const search = { search: 'channel', host: remoteServer.host }
101
102 const body = await command.advancedChannelSearch({ search })
103 expect(body.total).to.equal(1)
104 expect(body.data).to.have.lengthOf(1)
105 expect(body.data[0].displayName).to.equal('Zell channel')
106 }
107
108 {
109 const search = { search: 'Sq', host: server.host }
110
111 const body = await command.advancedChannelSearch({ search })
112 expect(body.total).to.equal(1)
113 expect(body.data).to.have.lengthOf(1)
114 expect(body.data[0].displayName).to.equal('Squall channel')
115 }
116
117 {
118 const search = { search: 'Squall', host: 'example.com' }
119
120 const body = await command.advancedChannelSearch({ search })
121 expect(body.total).to.equal(0)
122 expect(body.data).to.have.lengthOf(0)
123 }
124 })
125
126 it('Should filter by names', async function () {
127 {
128 const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel' ] } })
129 expect(body.total).to.equal(1)
130 expect(body.data).to.have.lengthOf(1)
131 expect(body.data[0].displayName).to.equal('Squall channel')
132 }
133
134 {
135 const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel@' + server.host ] } })
136 expect(body.total).to.equal(1)
137 expect(body.data).to.have.lengthOf(1)
138 expect(body.data[0].displayName).to.equal('Squall channel')
139 }
140
141 {
142 const body = await command.advancedChannelSearch({ search: { handles: [ 'chocobozzz_channel' ] } })
143 expect(body.total).to.equal(0)
144 expect(body.data).to.have.lengthOf(0)
145 }
146
147 {
148 const body = await command.advancedChannelSearch({ search: { handles: [ 'squall_channel', 'zell_channel@' + remoteServer.host ] } })
149 expect(body.total).to.equal(2)
150 expect(body.data).to.have.lengthOf(2)
151 expect(body.data[0].displayName).to.equal('Squall channel')
152 expect(body.data[1].displayName).to.equal('Zell channel')
153 }
154 })
155
156 after(async function () {
157 await cleanupTests([ server, remoteServer ])
158 })
159})
diff --git a/packages/tests/src/api/search/search-index.ts b/packages/tests/src/api/search/search-index.ts
new file mode 100644
index 000000000..4bac7ea94
--- /dev/null
+++ b/packages/tests/src/api/search/search-index.ts
@@ -0,0 +1,438 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 BooleanBothQuery,
6 VideoChannelsSearchQuery,
7 VideoPlaylistPrivacy,
8 VideoPlaylistsSearchQuery,
9 VideoPlaylistType,
10 VideosSearchQuery
11} from '@peertube/peertube-models'
12import {
13 cleanupTests,
14 createSingleServer,
15 PeerTubeServer,
16 SearchCommand,
17 setAccessTokensToServers
18} from '@peertube/peertube-server-commands'
19
20describe('Test index search', function () {
21 const localVideoName = 'local video' + new Date().toISOString()
22
23 let server: PeerTubeServer = null
24 let command: SearchCommand
25
26 before(async function () {
27 this.timeout(30000)
28
29 server = await createSingleServer(1)
30
31 await setAccessTokensToServers([ server ])
32
33 await server.videos.upload({ attributes: { name: localVideoName } })
34
35 command = server.search
36 })
37
38 describe('Default search', async function () {
39
40 it('Should make a local videos search by default', async function () {
41 await server.config.updateCustomSubConfig({
42 newConfig: {
43 search: {
44 searchIndex: {
45 enabled: true,
46 isDefaultSearch: false,
47 disableLocalSearch: false
48 }
49 }
50 }
51 })
52
53 const body = await command.searchVideos({ search: 'local video' })
54
55 expect(body.total).to.equal(1)
56 expect(body.data[0].name).to.equal(localVideoName)
57 })
58
59 it('Should make a local channels search by default', async function () {
60 const body = await command.searchChannels({ search: 'root' })
61
62 expect(body.total).to.equal(1)
63 expect(body.data[0].name).to.equal('root_channel')
64 expect(body.data[0].host).to.equal(server.host)
65 })
66
67 it('Should make an index videos search by default', async function () {
68 await server.config.updateCustomSubConfig({
69 newConfig: {
70 search: {
71 searchIndex: {
72 enabled: true,
73 isDefaultSearch: true,
74 disableLocalSearch: false
75 }
76 }
77 }
78 })
79
80 const body = await command.searchVideos({ search: 'local video' })
81 expect(body.total).to.be.greaterThan(2)
82 })
83
84 it('Should make an index channels search by default', async function () {
85 const body = await command.searchChannels({ search: 'root' })
86 expect(body.total).to.be.greaterThan(2)
87 })
88 })
89
90 describe('Videos search', async function () {
91
92 async function check (search: VideosSearchQuery, exists = true) {
93 const body = await command.advancedVideoSearch({ search })
94
95 if (exists === false) {
96 expect(body.total).to.equal(0)
97 expect(body.data).to.have.lengthOf(0)
98 return
99 }
100
101 expect(body.total).to.equal(1)
102 expect(body.data).to.have.lengthOf(1)
103
104 const video = body.data[0]
105
106 expect(video.name).to.equal('What is PeerTube?')
107 expect(video.category.label).to.equal('Science & Technology')
108 expect(video.licence.label).to.equal('Attribution - Share Alike')
109 expect(video.privacy.label).to.equal('Public')
110 expect(video.duration).to.equal(113)
111 expect(video.thumbnailUrl.startsWith('https://framatube.org/static/thumbnails')).to.be.true
112
113 expect(video.account.host).to.equal('framatube.org')
114 expect(video.account.name).to.equal('framasoft')
115 expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft')
116 expect(video.account.avatars.length).to.equal(2, 'Account should have one avatar image')
117
118 expect(video.channel.host).to.equal('framatube.org')
119 expect(video.channel.name).to.equal('joinpeertube')
120 expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube')
121 expect(video.channel.avatars.length).to.equal(2, 'Channel should have one avatar image')
122 }
123
124 const baseSearch: VideosSearchQuery = {
125 search: 'what is peertube',
126 start: 0,
127 count: 2,
128 categoryOneOf: [ 15 ],
129 licenceOneOf: [ 2 ],
130 tagsAllOf: [ 'framasoft', 'peertube' ],
131 startDate: '2018-10-01T10:50:46.396Z',
132 endDate: '2018-10-01T10:55:46.396Z'
133 }
134
135 it('Should make a simple search and not have results', async function () {
136 const body = await command.searchVideos({ search: 'djidane'.repeat(50) })
137
138 expect(body.total).to.equal(0)
139 expect(body.data).to.have.lengthOf(0)
140 })
141
142 it('Should make a simple search and have results', async function () {
143 const body = await command.searchVideos({ search: 'What is PeerTube' })
144
145 expect(body.total).to.be.greaterThan(1)
146 })
147
148 it('Should make a simple search', async function () {
149 await check(baseSearch)
150 })
151
152 it('Should search by start date', async function () {
153 const search = { ...baseSearch, startDate: '2018-10-01T10:54:46.396Z' }
154 await check(search, false)
155 })
156
157 it('Should search by tags', async function () {
158 const search = { ...baseSearch, tagsAllOf: [ 'toto', 'framasoft' ] }
159 await check(search, false)
160 })
161
162 it('Should search by duration', async function () {
163 const search = { ...baseSearch, durationMin: 2000 }
164 await check(search, false)
165 })
166
167 it('Should search by nsfw attribute', async function () {
168 {
169 const search = { ...baseSearch, nsfw: 'true' as BooleanBothQuery }
170 await check(search, false)
171 }
172
173 {
174 const search = { ...baseSearch, nsfw: 'false' as BooleanBothQuery }
175 await check(search, true)
176 }
177
178 {
179 const search = { ...baseSearch, nsfw: 'both' as BooleanBothQuery }
180 await check(search, true)
181 }
182 })
183
184 it('Should search by host', async function () {
185 {
186 const search = { ...baseSearch, host: 'example.com' }
187 await check(search, false)
188 }
189
190 {
191 const search = { ...baseSearch, host: 'framatube.org' }
192 await check(search, true)
193 }
194 })
195
196 it('Should search by uuids', async function () {
197 const goodUUID = '9c9de5e8-0a1e-484a-b099-e80766180a6d'
198 const goodShortUUID = 'kkGMgK9ZtnKfYAgnEtQxbv'
199 const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0'
200 const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej'
201
202 {
203 const uuidsMatrix = [
204 [ goodUUID ],
205 [ goodUUID, badShortUUID ],
206 [ badShortUUID, goodShortUUID ],
207 [ goodUUID, goodShortUUID ]
208 ]
209
210 for (const uuids of uuidsMatrix) {
211 const search = { ...baseSearch, uuids }
212 await check(search, true)
213 }
214 }
215
216 {
217 const uuidsMatrix = [
218 [ badUUID ],
219 [ badShortUUID ]
220 ]
221
222 for (const uuids of uuidsMatrix) {
223 const search = { ...baseSearch, uuids }
224 await check(search, false)
225 }
226 }
227 })
228
229 it('Should have a correct pagination', async function () {
230 const search = {
231 search: 'video',
232 start: 0,
233 count: 5
234 }
235
236 const body = await command.advancedVideoSearch({ search })
237
238 expect(body.total).to.be.greaterThan(5)
239 expect(body.data).to.have.lengthOf(5)
240 })
241
242 it('Should use the nsfw instance policy as default', async function () {
243 let nsfwUUID: string
244
245 {
246 await server.config.updateCustomSubConfig({
247 newConfig: {
248 instance: { defaultNSFWPolicy: 'display' }
249 }
250 })
251
252 const body = await command.searchVideos({ search: 'NSFW search index', sort: '-match' })
253 expect(body.data).to.have.length.greaterThan(0)
254
255 const video = body.data[0]
256 expect(video.nsfw).to.be.true
257
258 nsfwUUID = video.uuid
259 }
260
261 {
262 await server.config.updateCustomSubConfig({
263 newConfig: {
264 instance: { defaultNSFWPolicy: 'do_not_list' }
265 }
266 })
267
268 const body = await command.searchVideos({ search: 'NSFW search index', sort: '-match' })
269
270 try {
271 expect(body.data).to.have.lengthOf(0)
272 } catch {
273 const video = body.data[0]
274
275 expect(video.uuid).not.equal(nsfwUUID)
276 }
277 }
278 })
279 })
280
281 describe('Channels search', async function () {
282
283 async function check (search: VideoChannelsSearchQuery, exists = true) {
284 const body = await command.advancedChannelSearch({ search })
285
286 if (exists === false) {
287 expect(body.total).to.equal(0)
288 expect(body.data).to.have.lengthOf(0)
289 return
290 }
291
292 expect(body.total).to.be.greaterThan(0)
293 expect(body.data).to.have.length.greaterThan(0)
294
295 const videoChannel = body.data[0]
296 expect(videoChannel.url).to.equal('https://framatube.org/video-channels/bf54d359-cfad-4935-9d45-9d6be93f63e8')
297 expect(videoChannel.host).to.equal('framatube.org')
298 expect(videoChannel.avatars.length).to.equal(2, 'Channel should have two avatar images')
299 expect(videoChannel.displayName).to.exist
300
301 expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft')
302 expect(videoChannel.ownerAccount.name).to.equal('framasoft')
303 expect(videoChannel.ownerAccount.host).to.equal('framatube.org')
304 expect(videoChannel.ownerAccount.avatars.length).to.equal(2, 'Account should have two avatar images')
305 }
306
307 it('Should make a simple search and not have results', async function () {
308 const body = await command.searchChannels({ search: 'a'.repeat(500) })
309
310 expect(body.total).to.equal(0)
311 expect(body.data).to.have.lengthOf(0)
312 })
313
314 it('Should make a search and have results', async function () {
315 await check({ search: 'Framasoft', sort: 'createdAt' }, true)
316 })
317
318 it('Should make host search and have appropriate results', async function () {
319 await check({ search: 'Framasoft videos', host: 'example.com' }, false)
320 await check({ search: 'Framasoft videos', host: 'framatube.org' }, true)
321 })
322
323 it('Should make handles search and have appropriate results', async function () {
324 await check({ handles: [ 'bf54d359-cfad-4935-9d45-9d6be93f63e8@framatube.org' ] }, true)
325 await check({ handles: [ 'jeanine', 'bf54d359-cfad-4935-9d45-9d6be93f63e8@framatube.org' ] }, true)
326 await check({ handles: [ 'jeanine', 'chocobozzz_channel2@peertube2.cpy.re' ] }, false)
327 })
328
329 it('Should have a correct pagination', async function () {
330 const body = await command.advancedChannelSearch({ search: { search: 'root', start: 0, count: 2 } })
331
332 expect(body.total).to.be.greaterThan(2)
333 expect(body.data).to.have.lengthOf(2)
334 })
335 })
336
337 describe('Playlists search', async function () {
338
339 async function check (search: VideoPlaylistsSearchQuery, exists = true) {
340 const body = await command.advancedPlaylistSearch({ search })
341
342 if (exists === false) {
343 expect(body.total).to.equal(0)
344 expect(body.data).to.have.lengthOf(0)
345 return
346 }
347
348 expect(body.total).to.be.greaterThan(0)
349 expect(body.data).to.have.length.greaterThan(0)
350
351 const videoPlaylist = body.data[0]
352
353 expect(videoPlaylist.url).to.equal('https://peertube2.cpy.re/videos/watch/playlist/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a')
354 expect(videoPlaylist.thumbnailUrl).to.exist
355 expect(videoPlaylist.embedUrl).to.equal('https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a')
356
357 expect(videoPlaylist.type.id).to.equal(VideoPlaylistType.REGULAR)
358 expect(videoPlaylist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
359 expect(videoPlaylist.videosLength).to.exist
360
361 expect(videoPlaylist.createdAt).to.exist
362 expect(videoPlaylist.updatedAt).to.exist
363
364 expect(videoPlaylist.uuid).to.equal('73804a40-da9a-40c2-b1eb-2c6d9eec8f0a')
365 expect(videoPlaylist.displayName).to.exist
366
367 expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz')
368 expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz')
369 expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re')
370 expect(videoPlaylist.ownerAccount.avatars.length).to.equal(2, 'Account should have two avatar images')
371
372 expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel')
373 expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel')
374 expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re')
375 expect(videoPlaylist.videoChannel.avatars.length).to.equal(2, 'Channel should have two avatar images')
376 }
377
378 it('Should make a simple search and not have results', async function () {
379 const body = await command.searchPlaylists({ search: 'a'.repeat(500) })
380
381 expect(body.total).to.equal(0)
382 expect(body.data).to.have.lengthOf(0)
383 })
384
385 it('Should make a search and have results', async function () {
386 await check({ search: 'E2E playlist', sort: '-match' }, true)
387 })
388
389 it('Should make host search and have appropriate results', async function () {
390 await check({ search: 'E2E playlist', host: 'example.com' }, false)
391 await check({ search: 'E2E playlist', host: 'peertube2.cpy.re', sort: '-match' }, true)
392 })
393
394 it('Should make a search by uuids and have appropriate results', async function () {
395 const goodUUID = '73804a40-da9a-40c2-b1eb-2c6d9eec8f0a'
396 const goodShortUUID = 'fgei1ws1oa6FCaJ2qZPG29'
397 const badUUID = 'c29c5b77-4a04-493d-96a9-2e9267e308f0'
398 const badShortUUID = 'rP5RgUeX9XwTSrspCdkDej'
399
400 {
401 const uuidsMatrix = [
402 [ goodUUID ],
403 [ goodUUID, badShortUUID ],
404 [ badShortUUID, goodShortUUID ],
405 [ goodUUID, goodShortUUID ]
406 ]
407
408 for (const uuids of uuidsMatrix) {
409 const search = { search: 'E2E playlist', sort: '-match', uuids }
410 await check(search, true)
411 }
412 }
413
414 {
415 const uuidsMatrix = [
416 [ badUUID ],
417 [ badShortUUID ]
418 ]
419
420 for (const uuids of uuidsMatrix) {
421 const search = { search: 'E2E playlist', sort: '-match', uuids }
422 await check(search, false)
423 }
424 }
425 })
426
427 it('Should have a correct pagination', async function () {
428 const body = await command.advancedChannelSearch({ search: { search: 'root', start: 0, count: 2 } })
429
430 expect(body.total).to.be.greaterThan(2)
431 expect(body.data).to.have.lengthOf(2)
432 })
433 })
434
435 after(async function () {
436 await cleanupTests([ server ])
437 })
438})
diff --git a/packages/tests/src/api/search/search-playlists.ts b/packages/tests/src/api/search/search-playlists.ts
new file mode 100644
index 000000000..cd16e202e
--- /dev/null
+++ b/packages/tests/src/api/search/search-playlists.ts
@@ -0,0 +1,180 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 doubleFollow,
9 PeerTubeServer,
10 SearchCommand,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar,
14 setDefaultVideoChannel
15} from '@peertube/peertube-server-commands'
16
17describe('Test playlists search', function () {
18 let server: PeerTubeServer
19 let remoteServer: PeerTubeServer
20 let command: SearchCommand
21 let playlistUUID: string
22 let playlistShortUUID: string
23
24 before(async function () {
25 this.timeout(120000)
26
27 const servers = await Promise.all([
28 createSingleServer(1),
29 createSingleServer(2)
30 ])
31 server = servers[0]
32 remoteServer = servers[1]
33
34 await setAccessTokensToServers([ remoteServer, server ])
35 await setDefaultVideoChannel([ remoteServer, server ])
36 await setDefaultChannelAvatar([ remoteServer, server ])
37 await setDefaultAccountAvatar([ remoteServer, server ])
38
39 await servers[1].config.disableTranscoding()
40
41 {
42 const videoId = (await server.videos.upload()).uuid
43
44 const attributes = {
45 displayName: 'Dr. Kenzo Tenma hospital videos',
46 privacy: VideoPlaylistPrivacy.PUBLIC,
47 videoChannelId: server.store.channel.id
48 }
49 const created = await server.playlists.create({ attributes })
50 playlistUUID = created.uuid
51 playlistShortUUID = created.shortUUID
52
53 await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
54 }
55
56 {
57 const videoId = (await remoteServer.videos.upload()).uuid
58
59 const attributes = {
60 displayName: 'Johan & Anna Libert music videos',
61 privacy: VideoPlaylistPrivacy.PUBLIC,
62 videoChannelId: remoteServer.store.channel.id
63 }
64 const created = await remoteServer.playlists.create({ attributes })
65
66 await remoteServer.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
67 }
68
69 {
70 const attributes = {
71 displayName: 'Inspector Lunge playlist',
72 privacy: VideoPlaylistPrivacy.PUBLIC,
73 videoChannelId: server.store.channel.id
74 }
75 await server.playlists.create({ attributes })
76 }
77
78 await doubleFollow(server, remoteServer)
79
80 command = server.search
81 })
82
83 it('Should make a simple search and not have results', async function () {
84 const body = await command.searchPlaylists({ search: 'abc' })
85
86 expect(body.total).to.equal(0)
87 expect(body.data).to.have.lengthOf(0)
88 })
89
90 it('Should make a search and have results', async function () {
91 {
92 const search = {
93 search: 'tenma',
94 start: 0,
95 count: 1
96 }
97 const body = await command.advancedPlaylistSearch({ search })
98 expect(body.total).to.equal(1)
99 expect(body.data).to.have.lengthOf(1)
100
101 const playlist = body.data[0]
102 expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos')
103 expect(playlist.url).to.equal(server.url + '/video-playlists/' + playlist.uuid)
104 }
105
106 {
107 const search = {
108 search: 'Anna Livert music',
109 start: 0,
110 count: 1
111 }
112 const body = await command.advancedPlaylistSearch({ search })
113 expect(body.total).to.equal(1)
114 expect(body.data).to.have.lengthOf(1)
115
116 const playlist = body.data[0]
117 expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
118 }
119 })
120
121 it('Should filter by host', async function () {
122 {
123 const search = { search: 'tenma', host: server.host }
124 const body = await command.advancedPlaylistSearch({ search })
125 expect(body.total).to.equal(1)
126 expect(body.data).to.have.lengthOf(1)
127
128 const playlist = body.data[0]
129 expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos')
130 }
131
132 {
133 const search = { search: 'Anna', host: 'example.com' }
134 const body = await command.advancedPlaylistSearch({ search })
135 expect(body.total).to.equal(0)
136 expect(body.data).to.have.lengthOf(0)
137 }
138
139 {
140 const search = { search: 'video', host: remoteServer.host }
141 const body = await command.advancedPlaylistSearch({ search })
142 expect(body.total).to.equal(1)
143 expect(body.data).to.have.lengthOf(1)
144
145 const playlist = body.data[0]
146 expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
147 }
148 })
149
150 it('Should filter by UUIDs', async function () {
151 for (const uuid of [ playlistUUID, playlistShortUUID ]) {
152 const body = await command.advancedPlaylistSearch({ search: { uuids: [ uuid ] } })
153
154 expect(body.total).to.equal(1)
155 expect(body.data[0].displayName).to.equal('Dr. Kenzo Tenma hospital videos')
156 }
157
158 {
159 const body = await command.advancedPlaylistSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } })
160
161 expect(body.total).to.equal(0)
162 expect(body.data).to.have.lengthOf(0)
163 }
164 })
165
166 it('Should not display playlists without videos', async function () {
167 const search = {
168 search: 'Lunge',
169 start: 0,
170 count: 1
171 }
172 const body = await command.advancedPlaylistSearch({ search })
173 expect(body.total).to.equal(0)
174 expect(body.data).to.have.lengthOf(0)
175 })
176
177 after(async function () {
178 await cleanupTests([ server, remoteServer ])
179 })
180})
diff --git a/packages/tests/src/api/search/search-videos.ts b/packages/tests/src/api/search/search-videos.ts
new file mode 100644
index 000000000..0739f0886
--- /dev/null
+++ b/packages/tests/src/api/search/search-videos.ts
@@ -0,0 +1,568 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 doubleFollow,
10 PeerTubeServer,
11 SearchCommand,
12 setAccessTokensToServers,
13 setDefaultAccountAvatar,
14 setDefaultChannelAvatar,
15 setDefaultVideoChannel,
16 stopFfmpeg
17} from '@peertube/peertube-server-commands'
18
19describe('Test videos search', function () {
20 let server: PeerTubeServer
21 let remoteServer: PeerTubeServer
22 let startDate: string
23 let videoUUID: string
24 let videoShortUUID: string
25
26 let command: SearchCommand
27
28 before(async function () {
29 this.timeout(360000)
30
31 const servers = await Promise.all([
32 createSingleServer(1),
33 createSingleServer(2)
34 ])
35 server = servers[0]
36 remoteServer = servers[1]
37
38 await setAccessTokensToServers([ server, remoteServer ])
39 await setDefaultVideoChannel([ server, remoteServer ])
40 await setDefaultChannelAvatar(server)
41 await setDefaultAccountAvatar(servers)
42
43 {
44 const attributes1 = {
45 name: '1111 2222 3333',
46 fixture: '60fps_720p_small.mp4', // 2 seconds
47 category: 1,
48 licence: 1,
49 nsfw: false,
50 language: 'fr'
51 }
52 await server.videos.upload({ attributes: attributes1 })
53
54 const attributes2 = { ...attributes1, name: attributes1.name + ' - 2', fixture: 'video_short.mp4' }
55 await server.videos.upload({ attributes: attributes2 })
56
57 {
58 const attributes3 = { ...attributes1, name: attributes1.name + ' - 3', language: undefined }
59 const { id, uuid, shortUUID } = await server.videos.upload({ attributes: attributes3 })
60 videoUUID = uuid
61 videoShortUUID = shortUUID
62
63 await server.captions.add({
64 language: 'en',
65 videoId: id,
66 fixture: 'subtitle-good2.vtt',
67 mimeType: 'application/octet-stream'
68 })
69
70 await server.captions.add({
71 language: 'aa',
72 videoId: id,
73 fixture: 'subtitle-good2.vtt',
74 mimeType: 'application/octet-stream'
75 })
76 }
77
78 const attributes4 = { ...attributes1, name: attributes1.name + ' - 4', language: 'pl', nsfw: true }
79 await server.videos.upload({ attributes: attributes4 })
80
81 await wait(1000)
82
83 startDate = new Date().toISOString()
84
85 const attributes5 = { ...attributes1, name: attributes1.name + ' - 5', licence: 2, language: undefined }
86 await server.videos.upload({ attributes: attributes5 })
87
88 const attributes6 = { ...attributes1, name: attributes1.name + ' - 6', tags: [ 't1', 't2' ] }
89 await server.videos.upload({ attributes: attributes6 })
90
91 const attributes7 = { ...attributes1, name: attributes1.name + ' - 7', originallyPublishedAt: '2019-02-12T09:58:08.286Z' }
92 await server.videos.upload({ attributes: attributes7 })
93
94 const attributes8 = { ...attributes1, name: attributes1.name + ' - 8', licence: 4 }
95 await server.videos.upload({ attributes: attributes8 })
96 }
97
98 {
99 const attributes = {
100 name: '3333 4444 5555',
101 fixture: 'video_short.mp4',
102 category: 2,
103 licence: 2,
104 language: 'en'
105 }
106 await server.videos.upload({ attributes })
107
108 await server.videos.upload({ attributes: { ...attributes, name: attributes.name + ' duplicate' } })
109 }
110
111 {
112 const attributes = {
113 name: '6666 7777 8888',
114 fixture: 'video_short.mp4',
115 category: 3,
116 licence: 3,
117 language: 'pl'
118 }
119 await server.videos.upload({ attributes })
120 }
121
122 {
123 const attributes1 = {
124 name: '9999',
125 tags: [ 'aaaa', 'bbbb', 'cccc' ],
126 category: 1
127 }
128 await server.videos.upload({ attributes: attributes1 })
129 await server.videos.upload({ attributes: { ...attributes1, category: 2 } })
130
131 await server.videos.upload({ attributes: { ...attributes1, tags: [ 'cccc', 'dddd' ] } })
132 await server.videos.upload({ attributes: { ...attributes1, tags: [ 'eeee', 'ffff' ] } })
133 }
134
135 {
136 const attributes1 = {
137 name: 'aaaa 2',
138 category: 1
139 }
140 await server.videos.upload({ attributes: attributes1 })
141 await server.videos.upload({ attributes: { ...attributes1, category: 2 } })
142 }
143
144 {
145 await remoteServer.videos.upload({ attributes: { name: 'remote video 1' } })
146 await remoteServer.videos.upload({ attributes: { name: 'remote video 2' } })
147 }
148
149 await doubleFollow(server, remoteServer)
150
151 command = server.search
152 })
153
154 it('Should make a simple search and not have results', async function () {
155 const body = await command.searchVideos({ search: 'abc' })
156
157 expect(body.total).to.equal(0)
158 expect(body.data).to.have.lengthOf(0)
159 })
160
161 it('Should make a simple search and have results', async function () {
162 const body = await command.searchVideos({ search: '4444 5555 duplicate' })
163
164 expect(body.total).to.equal(2)
165
166 const videos = body.data
167 expect(videos).to.have.lengthOf(2)
168
169 // bestmatch
170 expect(videos[0].name).to.equal('3333 4444 5555 duplicate')
171 expect(videos[1].name).to.equal('3333 4444 5555')
172 })
173
174 it('Should make a search on tags too, and have results', async function () {
175 const search = {
176 search: 'aaaa',
177 categoryOneOf: [ 1 ]
178 }
179 const body = await command.advancedVideoSearch({ search })
180
181 expect(body.total).to.equal(2)
182
183 const videos = body.data
184 expect(videos).to.have.lengthOf(2)
185
186 // bestmatch
187 expect(videos[0].name).to.equal('aaaa 2')
188 expect(videos[1].name).to.equal('9999')
189 })
190
191 it('Should filter on tags without a search', async function () {
192 const search = {
193 tagsAllOf: [ 'bbbb' ]
194 }
195 const body = await command.advancedVideoSearch({ search })
196
197 expect(body.total).to.equal(2)
198
199 const videos = body.data
200 expect(videos).to.have.lengthOf(2)
201
202 expect(videos[0].name).to.equal('9999')
203 expect(videos[1].name).to.equal('9999')
204 })
205
206 it('Should filter on category without a search', async function () {
207 const search = {
208 categoryOneOf: [ 3 ]
209 }
210 const body = await command.advancedVideoSearch({ search })
211
212 expect(body.total).to.equal(1)
213
214 const videos = body.data
215 expect(videos).to.have.lengthOf(1)
216
217 expect(videos[0].name).to.equal('6666 7777 8888')
218 })
219
220 it('Should search by tags (one of)', async function () {
221 const query = {
222 search: '9999',
223 categoryOneOf: [ 1 ],
224 tagsOneOf: [ 'aAaa', 'ffff' ]
225 }
226
227 {
228 const body = await command.advancedVideoSearch({ search: query })
229 expect(body.total).to.equal(2)
230 }
231
232 {
233 const body = await command.advancedVideoSearch({ search: { ...query, tagsOneOf: [ 'blabla' ] } })
234 expect(body.total).to.equal(0)
235 }
236 })
237
238 it('Should search by tags (all of)', async function () {
239 const query = {
240 search: '9999',
241 categoryOneOf: [ 1 ],
242 tagsAllOf: [ 'CCcc' ]
243 }
244
245 {
246 const body = await command.advancedVideoSearch({ search: query })
247 expect(body.total).to.equal(2)
248 }
249
250 {
251 const body = await command.advancedVideoSearch({ search: { ...query, tagsAllOf: [ 'blAbla' ] } })
252 expect(body.total).to.equal(0)
253 }
254
255 {
256 const body = await command.advancedVideoSearch({ search: { ...query, tagsAllOf: [ 'bbbb', 'CCCC' ] } })
257 expect(body.total).to.equal(1)
258 }
259 })
260
261 it('Should search by category', async function () {
262 const query = {
263 search: '6666',
264 categoryOneOf: [ 3 ]
265 }
266
267 {
268 const body = await command.advancedVideoSearch({ search: query })
269 expect(body.total).to.equal(1)
270 expect(body.data[0].name).to.equal('6666 7777 8888')
271 }
272
273 {
274 const body = await command.advancedVideoSearch({ search: { ...query, categoryOneOf: [ 2 ] } })
275 expect(body.total).to.equal(0)
276 }
277 })
278
279 it('Should search by licence', async function () {
280 const query = {
281 search: '4444 5555',
282 licenceOneOf: [ 2 ]
283 }
284
285 {
286 const body = await command.advancedVideoSearch({ search: query })
287 expect(body.total).to.equal(2)
288 expect(body.data[0].name).to.equal('3333 4444 5555')
289 expect(body.data[1].name).to.equal('3333 4444 5555 duplicate')
290 }
291
292 {
293 const body = await command.advancedVideoSearch({ search: { ...query, licenceOneOf: [ 3 ] } })
294 expect(body.total).to.equal(0)
295 }
296 })
297
298 it('Should search by languages', async function () {
299 const query = {
300 search: '1111 2222 3333',
301 languageOneOf: [ 'pl', 'en' ]
302 }
303
304 {
305 const body = await command.advancedVideoSearch({ search: query })
306 expect(body.total).to.equal(2)
307 expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
308 expect(body.data[1].name).to.equal('1111 2222 3333 - 4')
309 }
310
311 {
312 const body = await command.advancedVideoSearch({ search: { ...query, languageOneOf: [ 'pl', 'en', '_unknown' ] } })
313 expect(body.total).to.equal(3)
314 expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
315 expect(body.data[1].name).to.equal('1111 2222 3333 - 4')
316 expect(body.data[2].name).to.equal('1111 2222 3333 - 5')
317 }
318
319 {
320 const body = await command.advancedVideoSearch({ search: { ...query, languageOneOf: [ 'eo' ] } })
321 expect(body.total).to.equal(0)
322 }
323 })
324
325 it('Should search by start date', async function () {
326 const query = {
327 search: '1111 2222 3333',
328 startDate
329 }
330
331 const body = await command.advancedVideoSearch({ search: query })
332 expect(body.total).to.equal(4)
333
334 const videos = body.data
335 expect(videos[0].name).to.equal('1111 2222 3333 - 5')
336 expect(videos[1].name).to.equal('1111 2222 3333 - 6')
337 expect(videos[2].name).to.equal('1111 2222 3333 - 7')
338 expect(videos[3].name).to.equal('1111 2222 3333 - 8')
339 })
340
341 it('Should make an advanced search', async function () {
342 const query = {
343 search: '1111 2222 3333',
344 languageOneOf: [ 'pl', 'fr' ],
345 durationMax: 4,
346 nsfw: 'false' as 'false',
347 licenceOneOf: [ 1, 4 ]
348 }
349
350 const body = await command.advancedVideoSearch({ search: query })
351 expect(body.total).to.equal(4)
352
353 const videos = body.data
354 expect(videos[0].name).to.equal('1111 2222 3333')
355 expect(videos[1].name).to.equal('1111 2222 3333 - 6')
356 expect(videos[2].name).to.equal('1111 2222 3333 - 7')
357 expect(videos[3].name).to.equal('1111 2222 3333 - 8')
358 })
359
360 it('Should make an advanced search and sort results', async function () {
361 const query = {
362 search: '1111 2222 3333',
363 languageOneOf: [ 'pl', 'fr' ],
364 durationMax: 4,
365 nsfw: 'false' as 'false',
366 licenceOneOf: [ 1, 4 ],
367 sort: '-name'
368 }
369
370 const body = await command.advancedVideoSearch({ search: query })
371 expect(body.total).to.equal(4)
372
373 const videos = body.data
374 expect(videos[0].name).to.equal('1111 2222 3333 - 8')
375 expect(videos[1].name).to.equal('1111 2222 3333 - 7')
376 expect(videos[2].name).to.equal('1111 2222 3333 - 6')
377 expect(videos[3].name).to.equal('1111 2222 3333')
378 })
379
380 it('Should make an advanced search and only show the first result', async function () {
381 const query = {
382 search: '1111 2222 3333',
383 languageOneOf: [ 'pl', 'fr' ],
384 durationMax: 4,
385 nsfw: 'false' as 'false',
386 licenceOneOf: [ 1, 4 ],
387 sort: '-name',
388 start: 0,
389 count: 1
390 }
391
392 const body = await command.advancedVideoSearch({ search: query })
393 expect(body.total).to.equal(4)
394
395 const videos = body.data
396 expect(videos[0].name).to.equal('1111 2222 3333 - 8')
397 })
398
399 it('Should make an advanced search and only show the last result', async function () {
400 const query = {
401 search: '1111 2222 3333',
402 languageOneOf: [ 'pl', 'fr' ],
403 durationMax: 4,
404 nsfw: 'false' as 'false',
405 licenceOneOf: [ 1, 4 ],
406 sort: '-name',
407 start: 3,
408 count: 1
409 }
410
411 const body = await command.advancedVideoSearch({ search: query })
412 expect(body.total).to.equal(4)
413
414 const videos = body.data
415 expect(videos[0].name).to.equal('1111 2222 3333')
416 })
417
418 it('Should search on originally published date', async function () {
419 const baseQuery = {
420 search: '1111 2222 3333',
421 languageOneOf: [ 'pl', 'fr' ],
422 durationMax: 4,
423 nsfw: 'false' as 'false',
424 licenceOneOf: [ 1, 4 ]
425 }
426
427 {
428 const query = { ...baseQuery, originallyPublishedStartDate: '2019-02-11T09:58:08.286Z' }
429 const body = await command.advancedVideoSearch({ search: query })
430
431 expect(body.total).to.equal(1)
432 expect(body.data[0].name).to.equal('1111 2222 3333 - 7')
433 }
434
435 {
436 const query = { ...baseQuery, originallyPublishedEndDate: '2019-03-11T09:58:08.286Z' }
437 const body = await command.advancedVideoSearch({ search: query })
438
439 expect(body.total).to.equal(1)
440 expect(body.data[0].name).to.equal('1111 2222 3333 - 7')
441 }
442
443 {
444 const query = { ...baseQuery, originallyPublishedEndDate: '2019-01-11T09:58:08.286Z' }
445 const body = await command.advancedVideoSearch({ search: query })
446
447 expect(body.total).to.equal(0)
448 }
449
450 {
451 const query = { ...baseQuery, originallyPublishedStartDate: '2019-03-11T09:58:08.286Z' }
452 const body = await command.advancedVideoSearch({ search: query })
453
454 expect(body.total).to.equal(0)
455 }
456
457 {
458 const query = {
459 ...baseQuery,
460 originallyPublishedStartDate: '2019-01-11T09:58:08.286Z',
461 originallyPublishedEndDate: '2019-01-10T09:58:08.286Z'
462 }
463 const body = await command.advancedVideoSearch({ search: query })
464
465 expect(body.total).to.equal(0)
466 }
467
468 {
469 const query = {
470 ...baseQuery,
471 originallyPublishedStartDate: '2019-01-11T09:58:08.286Z',
472 originallyPublishedEndDate: '2019-04-11T09:58:08.286Z'
473 }
474 const body = await command.advancedVideoSearch({ search: query })
475
476 expect(body.total).to.equal(1)
477 expect(body.data[0].name).to.equal('1111 2222 3333 - 7')
478 }
479 })
480
481 it('Should search by UUID', async function () {
482 const search = videoUUID
483 const body = await command.advancedVideoSearch({ search: { search } })
484
485 expect(body.total).to.equal(1)
486 expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
487 })
488
489 it('Should filter by UUIDs', async function () {
490 for (const uuid of [ videoUUID, videoShortUUID ]) {
491 const body = await command.advancedVideoSearch({ search: { uuids: [ uuid ] } })
492
493 expect(body.total).to.equal(1)
494 expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
495 }
496
497 {
498 const body = await command.advancedVideoSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } })
499
500 expect(body.total).to.equal(0)
501 expect(body.data).to.have.lengthOf(0)
502 }
503 })
504
505 it('Should search by host', async function () {
506 {
507 const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } })
508 expect(body.total).to.equal(1)
509 expect(body.data[0].name).to.equal('6666 7777 8888')
510 }
511
512 {
513 const body = await command.advancedVideoSearch({ search: { search: '1111', host: 'example.com' } })
514 expect(body.total).to.equal(0)
515 expect(body.data).to.have.lengthOf(0)
516 }
517
518 {
519 const body = await command.advancedVideoSearch({ search: { search: 'remote', host: remoteServer.host } })
520 expect(body.total).to.equal(2)
521 expect(body.data).to.have.lengthOf(2)
522 expect(body.data[0].name).to.equal('remote video 1')
523 expect(body.data[1].name).to.equal('remote video 2')
524 }
525 })
526
527 it('Should search by live', async function () {
528 this.timeout(120000)
529
530 {
531 const newConfig = {
532 search: {
533 searchIndex: { enabled: false }
534 },
535 live: { enabled: true }
536 }
537 await server.config.updateCustomSubConfig({ newConfig })
538 }
539
540 {
541 const body = await command.advancedVideoSearch({ search: { isLive: true } })
542
543 expect(body.total).to.equal(0)
544 expect(body.data).to.have.lengthOf(0)
545 }
546
547 {
548 const liveCommand = server.live
549
550 const liveAttributes = { name: 'live', privacy: VideoPrivacy.PUBLIC, channelId: server.store.channel.id }
551 const live = await liveCommand.create({ fields: liveAttributes })
552
553 const ffmpegCommand = await liveCommand.sendRTMPStreamInVideo({ videoId: live.id })
554 await liveCommand.waitUntilPublished({ videoId: live.id })
555
556 const body = await command.advancedVideoSearch({ search: { isLive: true } })
557
558 expect(body.total).to.equal(1)
559 expect(body.data[0].name).to.equal('live')
560
561 await stopFfmpeg(ffmpegCommand)
562 }
563 })
564
565 after(async function () {
566 await cleanupTests([ server ])
567 })
568})
diff --git a/packages/tests/src/api/server/auto-follows.ts b/packages/tests/src/api/server/auto-follows.ts
new file mode 100644
index 000000000..aa272ebcc
--- /dev/null
+++ b/packages/tests/src/api/server/auto-follows.ts
@@ -0,0 +1,189 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockInstancesIndex } from '@tests/shared/mock-servers/index.js'
5import { wait } from '@peertube/peertube-core-utils'
6import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands'
7
8async function checkFollow (follower: PeerTubeServer, following: PeerTubeServer, exists: boolean) {
9 {
10 const body = await following.follows.getFollowers({ start: 0, count: 5, sort: '-createdAt' })
11 const follow = body.data.find(f => f.follower.host === follower.host && f.state === 'accepted')
12
13 if (exists === true) expect(follow, `Follower ${follower.url} should exist on ${following.url}`).to.exist
14 else expect(follow, `Follower ${follower.url} should not exist on ${following.url}`).to.be.undefined
15 }
16
17 {
18 const body = await follower.follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' })
19 const follow = body.data.find(f => f.following.host === following.host && f.state === 'accepted')
20
21 if (exists === true) expect(follow, `Following ${following.url} should exist on ${follower.url}`).to.exist
22 else expect(follow, `Following ${following.url} should not exist on ${follower.url}`).to.be.undefined
23 }
24}
25
26async function server1Follows2 (servers: PeerTubeServer[]) {
27 await servers[0].follows.follow({ hosts: [ servers[1].host ] })
28
29 await waitJobs(servers)
30}
31
32async function resetFollows (servers: PeerTubeServer[]) {
33 try {
34 await servers[0].follows.unfollow({ target: servers[1] })
35 await servers[1].follows.unfollow({ target: servers[0] })
36 } catch { /* empty */
37 }
38
39 await waitJobs(servers)
40
41 await checkFollow(servers[0], servers[1], false)
42 await checkFollow(servers[1], servers[0], false)
43}
44
45describe('Test auto follows', function () {
46 let servers: PeerTubeServer[] = []
47
48 before(async function () {
49 this.timeout(120000)
50
51 servers = await createMultipleServers(3)
52
53 // Get the access tokens
54 await setAccessTokensToServers(servers)
55 })
56
57 describe('Auto follow back', function () {
58
59 it('Should not auto follow back if the option is not enabled', async function () {
60 this.timeout(15000)
61
62 await server1Follows2(servers)
63
64 await checkFollow(servers[0], servers[1], true)
65 await checkFollow(servers[1], servers[0], false)
66
67 await resetFollows(servers)
68 })
69
70 it('Should auto follow back on auto accept if the option is enabled', async function () {
71 this.timeout(15000)
72
73 const config = {
74 followings: {
75 instance: {
76 autoFollowBack: { enabled: true }
77 }
78 }
79 }
80 await servers[1].config.updateCustomSubConfig({ newConfig: config })
81
82 await server1Follows2(servers)
83
84 await checkFollow(servers[0], servers[1], true)
85 await checkFollow(servers[1], servers[0], true)
86
87 await resetFollows(servers)
88 })
89
90 it('Should wait the acceptation before auto follow back', async function () {
91 this.timeout(30000)
92
93 const config = {
94 followings: {
95 instance: {
96 autoFollowBack: { enabled: true }
97 }
98 },
99 followers: {
100 instance: {
101 manualApproval: true
102 }
103 }
104 }
105 await servers[1].config.updateCustomSubConfig({ newConfig: config })
106
107 await server1Follows2(servers)
108
109 await checkFollow(servers[0], servers[1], false)
110 await checkFollow(servers[1], servers[0], false)
111
112 await servers[1].follows.acceptFollower({ follower: 'peertube@' + servers[0].host })
113 await waitJobs(servers)
114
115 await checkFollow(servers[0], servers[1], true)
116 await checkFollow(servers[1], servers[0], true)
117
118 await resetFollows(servers)
119
120 config.followings.instance.autoFollowBack.enabled = false
121 config.followers.instance.manualApproval = false
122 await servers[1].config.updateCustomSubConfig({ newConfig: config })
123 })
124 })
125
126 describe('Auto follow index', function () {
127 const instanceIndexServer = new MockInstancesIndex()
128 let port: number
129
130 before(async function () {
131 port = await instanceIndexServer.initialize()
132 })
133
134 it('Should not auto follow index if the option is not enabled', async function () {
135 this.timeout(30000)
136
137 await wait(5000)
138 await waitJobs(servers)
139
140 await checkFollow(servers[0], servers[1], false)
141 await checkFollow(servers[1], servers[0], false)
142 })
143
144 it('Should auto follow the index', async function () {
145 this.timeout(30000)
146
147 instanceIndexServer.addInstance(servers[1].host)
148
149 const config = {
150 followings: {
151 instance: {
152 autoFollowIndex: {
153 indexUrl: `http://127.0.0.1:${port}/api/v1/instances/hosts`,
154 enabled: true
155 }
156 }
157 }
158 }
159 await servers[0].config.updateCustomSubConfig({ newConfig: config })
160
161 await wait(5000)
162 await waitJobs(servers)
163
164 await checkFollow(servers[0], servers[1], true)
165
166 await resetFollows(servers)
167 })
168
169 it('Should follow new added instances in the index but not old ones', async function () {
170 this.timeout(30000)
171
172 instanceIndexServer.addInstance(servers[2].host)
173
174 await wait(5000)
175 await waitJobs(servers)
176
177 await checkFollow(servers[0], servers[1], false)
178 await checkFollow(servers[0], servers[2], true)
179 })
180
181 after(async function () {
182 await instanceIndexServer.terminate()
183 })
184 })
185
186 after(async function () {
187 await cleanupTests(servers)
188 })
189})
diff --git a/packages/tests/src/api/server/bulk.ts b/packages/tests/src/api/server/bulk.ts
new file mode 100644
index 000000000..725bcfef2
--- /dev/null
+++ b/packages/tests/src/api/server/bulk.ts
@@ -0,0 +1,185 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 BulkCommand,
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test bulk actions', function () {
15 const commentsUser3: { videoId: number, commentId: number }[] = []
16
17 let servers: PeerTubeServer[] = []
18 let user1Token: string
19 let user2Token: string
20 let user3Token: string
21
22 let bulkCommand: BulkCommand
23
24 before(async function () {
25 this.timeout(120000)
26
27 servers = await createMultipleServers(2)
28
29 // Get the access tokens
30 await setAccessTokensToServers(servers)
31
32 {
33 const user = { username: 'user1', password: 'password' }
34 await servers[0].users.create({ username: user.username, password: user.password })
35
36 user1Token = await servers[0].login.getAccessToken(user)
37 }
38
39 {
40 const user = { username: 'user2', password: 'password' }
41 await servers[0].users.create({ username: user.username, password: user.password })
42
43 user2Token = await servers[0].login.getAccessToken(user)
44 }
45
46 {
47 const user = { username: 'user3', password: 'password' }
48 await servers[1].users.create({ username: user.username, password: user.password })
49
50 user3Token = await servers[1].login.getAccessToken(user)
51 }
52
53 await doubleFollow(servers[0], servers[1])
54
55 bulkCommand = new BulkCommand(servers[0])
56 })
57
58 describe('Bulk remove comments', function () {
59 async function checkInstanceCommentsRemoved () {
60 {
61 const { data } = await servers[0].videos.list()
62
63 // Server 1 should not have these comments anymore
64 for (const video of data) {
65 const { data } = await servers[0].comments.listThreads({ videoId: video.id })
66 const comment = data.find(c => c.text === 'comment by user 3')
67
68 expect(comment).to.not.exist
69 }
70 }
71
72 {
73 const { data } = await servers[1].videos.list()
74
75 // Server 1 should not have these comments on videos of server 1
76 for (const video of data) {
77 const { data } = await servers[1].comments.listThreads({ videoId: video.id })
78 const comment = data.find(c => c.text === 'comment by user 3')
79
80 if (video.account.host === servers[0].host) {
81 expect(comment).to.not.exist
82 } else {
83 expect(comment).to.exist
84 }
85 }
86 }
87 }
88
89 before(async function () {
90 this.timeout(240000)
91
92 await servers[0].videos.upload({ attributes: { name: 'video 1 server 1' } })
93 await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } })
94 await servers[0].videos.upload({ token: user1Token, attributes: { name: 'video 3 server 1' } })
95
96 await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
97
98 await waitJobs(servers)
99
100 {
101 const { data } = await servers[0].videos.list()
102 for (const video of data) {
103 await servers[0].comments.createThread({ videoId: video.id, text: 'comment by root server 1' })
104 await servers[0].comments.createThread({ token: user1Token, videoId: video.id, text: 'comment by user 1' })
105 await servers[0].comments.createThread({ token: user2Token, videoId: video.id, text: 'comment by user 2' })
106 }
107 }
108
109 {
110 const { data } = await servers[1].videos.list()
111
112 for (const video of data) {
113 await servers[1].comments.createThread({ videoId: video.id, text: 'comment by root server 2' })
114
115 const comment = await servers[1].comments.createThread({ token: user3Token, videoId: video.id, text: 'comment by user 3' })
116 commentsUser3.push({ videoId: video.id, commentId: comment.id })
117 }
118 }
119
120 await waitJobs(servers)
121 })
122
123 it('Should delete comments of an account on my videos', async function () {
124 this.timeout(60000)
125
126 await bulkCommand.removeCommentsOf({
127 token: user1Token,
128 attributes: {
129 accountName: 'user2',
130 scope: 'my-videos'
131 }
132 })
133
134 await waitJobs(servers)
135
136 for (const server of servers) {
137 const { data } = await server.videos.list()
138
139 for (const video of data) {
140 const { data } = await server.comments.listThreads({ videoId: video.id })
141 const comment = data.find(c => c.text === 'comment by user 2')
142
143 if (video.name === 'video 3 server 1') expect(comment).to.not.exist
144 else expect(comment).to.exist
145 }
146 }
147 })
148
149 it('Should delete comments of an account on the instance', async function () {
150 this.timeout(60000)
151
152 await bulkCommand.removeCommentsOf({
153 attributes: {
154 accountName: 'user3@' + servers[1].host,
155 scope: 'instance'
156 }
157 })
158
159 await waitJobs(servers)
160
161 await checkInstanceCommentsRemoved()
162 })
163
164 it('Should not re create the comment on video update', async function () {
165 this.timeout(60000)
166
167 for (const obj of commentsUser3) {
168 await servers[1].comments.addReply({
169 token: user3Token,
170 videoId: obj.videoId,
171 toCommentId: obj.commentId,
172 text: 'comment by user 3 bis'
173 })
174 }
175
176 await waitJobs(servers)
177
178 await checkInstanceCommentsRemoved()
179 })
180 })
181
182 after(async function () {
183 await cleanupTests(servers)
184 })
185})
diff --git a/packages/tests/src/api/server/config-defaults.ts b/packages/tests/src/api/server/config-defaults.ts
new file mode 100644
index 000000000..e874af012
--- /dev/null
+++ b/packages/tests/src/api/server/config-defaults.ts
@@ -0,0 +1,294 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@peertube/peertube-server-commands'
12import { FIXTURE_URLS } from '@tests/shared/tests.js'
13
14describe('Test config defaults', function () {
15 let server: PeerTubeServer
16 let channelId: number
17
18 before(async function () {
19 this.timeout(30000)
20
21 server = await createSingleServer(1)
22 await setAccessTokensToServers([ server ])
23 await setDefaultVideoChannel([ server ])
24
25 channelId = server.store.channel.id
26 })
27
28 describe('Default publish values', function () {
29
30 before(async function () {
31 const overrideConfig = {
32 defaults: {
33 publish: {
34 comments_enabled: false,
35 download_enabled: false,
36 privacy: VideoPrivacy.INTERNAL,
37 licence: 4
38 }
39 }
40 }
41
42 await server.kill()
43 await server.run(overrideConfig)
44 })
45
46 const attributes = {
47 name: 'video',
48 downloadEnabled: undefined,
49 commentsEnabled: undefined,
50 licence: undefined,
51 privacy: VideoPrivacy.PUBLIC // Privacy is mandatory for server
52 }
53
54 function checkVideo (video: VideoDetails) {
55 expect(video.downloadEnabled).to.be.false
56 expect(video.commentsEnabled).to.be.false
57 expect(video.licence.id).to.equal(4)
58 }
59
60 before(async function () {
61 await server.config.disableTranscoding()
62 await server.config.enableImports()
63 await server.config.enableLive({ allowReplay: false, transcoding: false })
64 })
65
66 it('Should have the correct server configuration', async function () {
67 const config = await server.config.getConfig()
68
69 expect(config.defaults.publish.commentsEnabled).to.be.false
70 expect(config.defaults.publish.downloadEnabled).to.be.false
71 expect(config.defaults.publish.licence).to.equal(4)
72 expect(config.defaults.publish.privacy).to.equal(VideoPrivacy.INTERNAL)
73 })
74
75 it('Should respect default values when uploading a video', async function () {
76 for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) {
77 const { id } = await server.videos.upload({ attributes, mode })
78
79 const video = await server.videos.get({ id })
80 checkVideo(video)
81 }
82 })
83
84 it('Should respect default values when importing a video using URL', async function () {
85 const { video: { id } } = await server.imports.importVideo({
86 attributes: {
87 ...attributes,
88 channelId,
89 targetUrl: FIXTURE_URLS.goodVideo
90 }
91 })
92
93 const video = await server.videos.get({ id })
94 checkVideo(video)
95 })
96
97 it('Should respect default values when importing a video using magnet URI', async function () {
98 const { video: { id } } = await server.imports.importVideo({
99 attributes: {
100 ...attributes,
101 channelId,
102 magnetUri: FIXTURE_URLS.magnet
103 }
104 })
105
106 const video = await server.videos.get({ id })
107 checkVideo(video)
108 })
109
110 it('Should respect default values when creating a live', async function () {
111 const { id } = await server.live.create({
112 fields: {
113 ...attributes,
114 channelId
115 }
116 })
117
118 const video = await server.videos.get({ id })
119 checkVideo(video)
120 })
121 })
122
123 describe('Default P2P values', function () {
124
125 describe('Webapp default value', function () {
126
127 before(async function () {
128 const overrideConfig = {
129 defaults: {
130 p2p: {
131 webapp: {
132 enabled: false
133 }
134 }
135 }
136 }
137
138 await server.kill()
139 await server.run(overrideConfig)
140 })
141
142 it('Should have appropriate P2P config', async function () {
143 const config = await server.config.getConfig()
144
145 expect(config.defaults.p2p.webapp.enabled).to.be.false
146 expect(config.defaults.p2p.embed.enabled).to.be.true
147 })
148
149 it('Should create a user with this default setting', async function () {
150 await server.users.create({ username: 'user_p2p_1' })
151 const userToken = await server.login.getAccessToken('user_p2p_1')
152
153 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
154 expect(p2pEnabled).to.be.false
155 })
156
157 it('Should register a user with this default setting', async function () {
158 await server.registrations.register({ username: 'user_p2p_2' })
159
160 const userToken = await server.login.getAccessToken('user_p2p_2')
161
162 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
163 expect(p2pEnabled).to.be.false
164 })
165 })
166
167 describe('Embed default value', function () {
168
169 before(async function () {
170 const overrideConfig = {
171 defaults: {
172 p2p: {
173 embed: {
174 enabled: false
175 }
176 }
177 },
178 signup: {
179 limit: 15
180 }
181 }
182
183 await server.kill()
184 await server.run(overrideConfig)
185 })
186
187 it('Should have appropriate P2P config', async function () {
188 const config = await server.config.getConfig()
189
190 expect(config.defaults.p2p.webapp.enabled).to.be.true
191 expect(config.defaults.p2p.embed.enabled).to.be.false
192 })
193
194 it('Should create a user with this default setting', async function () {
195 await server.users.create({ username: 'user_p2p_3' })
196 const userToken = await server.login.getAccessToken('user_p2p_3')
197
198 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
199 expect(p2pEnabled).to.be.true
200 })
201
202 it('Should register a user with this default setting', async function () {
203 await server.registrations.register({ username: 'user_p2p_4' })
204
205 const userToken = await server.login.getAccessToken('user_p2p_4')
206
207 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
208 expect(p2pEnabled).to.be.true
209 })
210 })
211 })
212
213 describe('Default user attributes', function () {
214 it('Should create a user and register a user with the default config', async function () {
215 await server.config.updateCustomSubConfig({
216 newConfig: {
217 user: {
218 history: {
219 videos: {
220 enabled: true
221 }
222 },
223 videoQuota : -1,
224 videoQuotaDaily: -1
225 },
226 signup: {
227 enabled: true,
228 requiresApproval: false
229 }
230 }
231 })
232
233 const config = await server.config.getConfig()
234
235 expect(config.user.videoQuota).to.equal(-1)
236 expect(config.user.videoQuotaDaily).to.equal(-1)
237
238 const user1Token = await server.users.generateUserAndToken('user1')
239 const user1 = await server.users.getMyInfo({ token: user1Token })
240
241 const user = { displayName: 'super user 2', username: 'user2', password: 'super password' }
242 const channel = { name: 'my_user_2_channel', displayName: 'my channel' }
243 await server.registrations.register({ ...user, channel })
244 const user2Token = await server.login.getAccessToken(user)
245 const user2 = await server.users.getMyInfo({ token: user2Token })
246
247 for (const user of [ user1, user2 ]) {
248 expect(user.videosHistoryEnabled).to.be.true
249 expect(user.videoQuota).to.equal(-1)
250 expect(user.videoQuotaDaily).to.equal(-1)
251 }
252 })
253
254 it('Should update config and create a user and register a user with the new default config', async function () {
255 await server.config.updateCustomSubConfig({
256 newConfig: {
257 user: {
258 history: {
259 videos: {
260 enabled: false
261 }
262 },
263 videoQuota : 5242881,
264 videoQuotaDaily: 318742
265 },
266 signup: {
267 enabled: true,
268 requiresApproval: false
269 }
270 }
271 })
272
273 const user3Token = await server.users.generateUserAndToken('user3')
274 const user3 = await server.users.getMyInfo({ token: user3Token })
275
276 const user = { displayName: 'super user 4', username: 'user4', password: 'super password' }
277 const channel = { name: 'my_user_4_channel', displayName: 'my channel' }
278 await server.registrations.register({ ...user, channel })
279 const user4Token = await server.login.getAccessToken(user)
280 const user4 = await server.users.getMyInfo({ token: user4Token })
281
282 for (const user of [ user3, user4 ]) {
283 expect(user.videosHistoryEnabled).to.be.false
284 expect(user.videoQuota).to.equal(5242881)
285 expect(user.videoQuotaDaily).to.equal(318742)
286 }
287 })
288
289 })
290
291 after(async function () {
292 await cleanupTests([ server ])
293 })
294})
diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts
new file mode 100644
index 000000000..ce64668f8
--- /dev/null
+++ b/packages/tests/src/api/server/config.ts
@@ -0,0 +1,645 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { parallelTests } from '@peertube/peertube-node-utils'
5import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 killallServers,
10 makeGetRequest,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
16 expect(data.instance.name).to.equal('PeerTube')
17 expect(data.instance.shortDescription).to.equal(
18 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
19 )
20 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
21
22 expect(data.instance.terms).to.equal('No terms for now.')
23 expect(data.instance.creationReason).to.be.empty
24 expect(data.instance.codeOfConduct).to.be.empty
25 expect(data.instance.moderationInformation).to.be.empty
26 expect(data.instance.administrator).to.be.empty
27 expect(data.instance.maintenanceLifetime).to.be.empty
28 expect(data.instance.businessModel).to.be.empty
29 expect(data.instance.hardwareInformation).to.be.empty
30
31 expect(data.instance.languages).to.have.lengthOf(0)
32 expect(data.instance.categories).to.have.lengthOf(0)
33
34 expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
35 expect(data.instance.isNSFW).to.be.false
36 expect(data.instance.defaultNSFWPolicy).to.equal('display')
37 expect(data.instance.customizations.css).to.be.empty
38 expect(data.instance.customizations.javascript).to.be.empty
39
40 expect(data.services.twitter.username).to.equal('@Chocobozzz')
41 expect(data.services.twitter.whitelisted).to.be.false
42
43 expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false
44 expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false
45
46 expect(data.cache.previews.size).to.equal(1)
47 expect(data.cache.captions.size).to.equal(1)
48 expect(data.cache.torrents.size).to.equal(1)
49 expect(data.cache.storyboards.size).to.equal(1)
50
51 expect(data.signup.enabled).to.be.true
52 expect(data.signup.limit).to.equal(4)
53 expect(data.signup.minimumAge).to.equal(16)
54 expect(data.signup.requiresApproval).to.be.false
55 expect(data.signup.requiresEmailVerification).to.be.false
56
57 expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com')
58 expect(data.contactForm.enabled).to.be.true
59
60 expect(data.user.history.videos.enabled).to.be.true
61 expect(data.user.videoQuota).to.equal(5242880)
62 expect(data.user.videoQuotaDaily).to.equal(-1)
63
64 expect(data.videoChannels.maxPerUser).to.equal(20)
65
66 expect(data.transcoding.enabled).to.be.false
67 expect(data.transcoding.remoteRunners.enabled).to.be.false
68 expect(data.transcoding.allowAdditionalExtensions).to.be.false
69 expect(data.transcoding.allowAudioFiles).to.be.false
70 expect(data.transcoding.threads).to.equal(2)
71 expect(data.transcoding.concurrency).to.equal(2)
72 expect(data.transcoding.profile).to.equal('default')
73 expect(data.transcoding.resolutions['144p']).to.be.false
74 expect(data.transcoding.resolutions['240p']).to.be.true
75 expect(data.transcoding.resolutions['360p']).to.be.true
76 expect(data.transcoding.resolutions['480p']).to.be.true
77 expect(data.transcoding.resolutions['720p']).to.be.true
78 expect(data.transcoding.resolutions['1080p']).to.be.true
79 expect(data.transcoding.resolutions['1440p']).to.be.true
80 expect(data.transcoding.resolutions['2160p']).to.be.true
81 expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
82 expect(data.transcoding.webVideos.enabled).to.be.true
83 expect(data.transcoding.hls.enabled).to.be.true
84
85 expect(data.live.enabled).to.be.false
86 expect(data.live.allowReplay).to.be.false
87 expect(data.live.latencySetting.enabled).to.be.true
88 expect(data.live.maxDuration).to.equal(-1)
89 expect(data.live.maxInstanceLives).to.equal(20)
90 expect(data.live.maxUserLives).to.equal(3)
91 expect(data.live.transcoding.enabled).to.be.false
92 expect(data.live.transcoding.remoteRunners.enabled).to.be.false
93 expect(data.live.transcoding.threads).to.equal(2)
94 expect(data.live.transcoding.profile).to.equal('default')
95 expect(data.live.transcoding.resolutions['144p']).to.be.false
96 expect(data.live.transcoding.resolutions['240p']).to.be.false
97 expect(data.live.transcoding.resolutions['360p']).to.be.false
98 expect(data.live.transcoding.resolutions['480p']).to.be.false
99 expect(data.live.transcoding.resolutions['720p']).to.be.false
100 expect(data.live.transcoding.resolutions['1080p']).to.be.false
101 expect(data.live.transcoding.resolutions['1440p']).to.be.false
102 expect(data.live.transcoding.resolutions['2160p']).to.be.false
103 expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
104
105 expect(data.videoStudio.enabled).to.be.false
106 expect(data.videoStudio.remoteRunners.enabled).to.be.false
107
108 expect(data.videoFile.update.enabled).to.be.false
109
110 expect(data.import.videos.concurrency).to.equal(2)
111 expect(data.import.videos.http.enabled).to.be.true
112 expect(data.import.videos.torrent.enabled).to.be.true
113 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
114
115 expect(data.followers.instance.enabled).to.be.true
116 expect(data.followers.instance.manualApproval).to.be.false
117
118 expect(data.followings.instance.autoFollowBack.enabled).to.be.false
119 expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
120 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('')
121
122 expect(data.broadcastMessage.enabled).to.be.false
123 expect(data.broadcastMessage.level).to.equal('info')
124 expect(data.broadcastMessage.message).to.equal('')
125 expect(data.broadcastMessage.dismissable).to.be.false
126}
127
128function checkUpdatedConfig (data: CustomConfig) {
129 expect(data.instance.name).to.equal('PeerTube updated')
130 expect(data.instance.shortDescription).to.equal('my short description')
131 expect(data.instance.description).to.equal('my super description')
132
133 expect(data.instance.terms).to.equal('my super terms')
134 expect(data.instance.creationReason).to.equal('my super creation reason')
135 expect(data.instance.codeOfConduct).to.equal('my super coc')
136 expect(data.instance.moderationInformation).to.equal('my super moderation information')
137 expect(data.instance.administrator).to.equal('Kuja')
138 expect(data.instance.maintenanceLifetime).to.equal('forever')
139 expect(data.instance.businessModel).to.equal('my super business model')
140 expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
141
142 expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
143 expect(data.instance.categories).to.deep.equal([ 1, 2 ])
144
145 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
146 expect(data.instance.isNSFW).to.be.true
147 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
148 expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
149 expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
150
151 expect(data.services.twitter.username).to.equal('@Kuja')
152 expect(data.services.twitter.whitelisted).to.be.true
153
154 expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.true
155 expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.true
156
157 expect(data.cache.previews.size).to.equal(2)
158 expect(data.cache.captions.size).to.equal(3)
159 expect(data.cache.torrents.size).to.equal(4)
160 expect(data.cache.storyboards.size).to.equal(5)
161
162 expect(data.signup.enabled).to.be.false
163 expect(data.signup.limit).to.equal(5)
164 expect(data.signup.requiresApproval).to.be.false
165 expect(data.signup.requiresEmailVerification).to.be.false
166 expect(data.signup.minimumAge).to.equal(10)
167
168 // We override admin email in parallel tests, so skip this exception
169 if (parallelTests() === false) {
170 expect(data.admin.email).to.equal('superadmin1@example.com')
171 }
172
173 expect(data.contactForm.enabled).to.be.false
174
175 expect(data.user.history.videos.enabled).to.be.false
176 expect(data.user.videoQuota).to.equal(5242881)
177 expect(data.user.videoQuotaDaily).to.equal(318742)
178
179 expect(data.videoChannels.maxPerUser).to.equal(24)
180
181 expect(data.transcoding.enabled).to.be.true
182 expect(data.transcoding.remoteRunners.enabled).to.be.true
183 expect(data.transcoding.threads).to.equal(1)
184 expect(data.transcoding.concurrency).to.equal(3)
185 expect(data.transcoding.allowAdditionalExtensions).to.be.true
186 expect(data.transcoding.allowAudioFiles).to.be.true
187 expect(data.transcoding.profile).to.equal('vod_profile')
188 expect(data.transcoding.resolutions['144p']).to.be.false
189 expect(data.transcoding.resolutions['240p']).to.be.false
190 expect(data.transcoding.resolutions['360p']).to.be.true
191 expect(data.transcoding.resolutions['480p']).to.be.true
192 expect(data.transcoding.resolutions['720p']).to.be.false
193 expect(data.transcoding.resolutions['1080p']).to.be.false
194 expect(data.transcoding.resolutions['2160p']).to.be.false
195 expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false
196 expect(data.transcoding.hls.enabled).to.be.false
197 expect(data.transcoding.webVideos.enabled).to.be.true
198
199 expect(data.live.enabled).to.be.true
200 expect(data.live.allowReplay).to.be.true
201 expect(data.live.latencySetting.enabled).to.be.false
202 expect(data.live.maxDuration).to.equal(5000)
203 expect(data.live.maxInstanceLives).to.equal(-1)
204 expect(data.live.maxUserLives).to.equal(10)
205 expect(data.live.transcoding.enabled).to.be.true
206 expect(data.live.transcoding.remoteRunners.enabled).to.be.true
207 expect(data.live.transcoding.threads).to.equal(4)
208 expect(data.live.transcoding.profile).to.equal('live_profile')
209 expect(data.live.transcoding.resolutions['144p']).to.be.true
210 expect(data.live.transcoding.resolutions['240p']).to.be.true
211 expect(data.live.transcoding.resolutions['360p']).to.be.true
212 expect(data.live.transcoding.resolutions['480p']).to.be.true
213 expect(data.live.transcoding.resolutions['720p']).to.be.true
214 expect(data.live.transcoding.resolutions['1080p']).to.be.true
215 expect(data.live.transcoding.resolutions['2160p']).to.be.true
216 expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false
217
218 expect(data.videoStudio.enabled).to.be.true
219 expect(data.videoStudio.remoteRunners.enabled).to.be.true
220
221 expect(data.videoFile.update.enabled).to.be.true
222
223 expect(data.import.videos.concurrency).to.equal(4)
224 expect(data.import.videos.http.enabled).to.be.false
225 expect(data.import.videos.torrent.enabled).to.be.false
226 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
227
228 expect(data.followers.instance.enabled).to.be.false
229 expect(data.followers.instance.manualApproval).to.be.true
230
231 expect(data.followings.instance.autoFollowBack.enabled).to.be.true
232 expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
233 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
234
235 expect(data.broadcastMessage.enabled).to.be.true
236 expect(data.broadcastMessage.level).to.equal('error')
237 expect(data.broadcastMessage.message).to.equal('super bad message')
238 expect(data.broadcastMessage.dismissable).to.be.true
239}
240
241const newCustomConfig: CustomConfig = {
242 instance: {
243 name: 'PeerTube updated',
244 shortDescription: 'my short description',
245 description: 'my super description',
246 terms: 'my super terms',
247 codeOfConduct: 'my super coc',
248
249 creationReason: 'my super creation reason',
250 moderationInformation: 'my super moderation information',
251 administrator: 'Kuja',
252 maintenanceLifetime: 'forever',
253 businessModel: 'my super business model',
254 hardwareInformation: '2vCore 3GB RAM',
255
256 languages: [ 'en', 'es' ],
257 categories: [ 1, 2 ],
258
259 isNSFW: true,
260 defaultNSFWPolicy: 'blur' as 'blur',
261
262 defaultClientRoute: '/videos/recently-added',
263
264 customizations: {
265 javascript: 'alert("coucou")',
266 css: 'body { background-color: red; }'
267 }
268 },
269 theme: {
270 default: 'default'
271 },
272 services: {
273 twitter: {
274 username: '@Kuja',
275 whitelisted: true
276 }
277 },
278 client: {
279 videos: {
280 miniature: {
281 preferAuthorDisplayName: true
282 }
283 },
284 menu: {
285 login: {
286 redirectOnSingleExternalAuth: true
287 }
288 }
289 },
290 cache: {
291 previews: {
292 size: 2
293 },
294 captions: {
295 size: 3
296 },
297 torrents: {
298 size: 4
299 },
300 storyboards: {
301 size: 5
302 }
303 },
304 signup: {
305 enabled: false,
306 limit: 5,
307 requiresApproval: false,
308 requiresEmailVerification: false,
309 minimumAge: 10
310 },
311 admin: {
312 email: 'superadmin1@example.com'
313 },
314 contactForm: {
315 enabled: false
316 },
317 user: {
318 history: {
319 videos: {
320 enabled: false
321 }
322 },
323 videoQuota: 5242881,
324 videoQuotaDaily: 318742
325 },
326 videoChannels: {
327 maxPerUser: 24
328 },
329 transcoding: {
330 enabled: true,
331 remoteRunners: {
332 enabled: true
333 },
334 allowAdditionalExtensions: true,
335 allowAudioFiles: true,
336 threads: 1,
337 concurrency: 3,
338 profile: 'vod_profile',
339 resolutions: {
340 '0p': false,
341 '144p': false,
342 '240p': false,
343 '360p': true,
344 '480p': true,
345 '720p': false,
346 '1080p': false,
347 '1440p': false,
348 '2160p': false
349 },
350 alwaysTranscodeOriginalResolution: false,
351 webVideos: {
352 enabled: true
353 },
354 hls: {
355 enabled: false
356 }
357 },
358 live: {
359 enabled: true,
360 allowReplay: true,
361 latencySetting: {
362 enabled: false
363 },
364 maxDuration: 5000,
365 maxInstanceLives: -1,
366 maxUserLives: 10,
367 transcoding: {
368 enabled: true,
369 remoteRunners: {
370 enabled: true
371 },
372 threads: 4,
373 profile: 'live_profile',
374 resolutions: {
375 '144p': true,
376 '240p': true,
377 '360p': true,
378 '480p': true,
379 '720p': true,
380 '1080p': true,
381 '1440p': true,
382 '2160p': true
383 },
384 alwaysTranscodeOriginalResolution: false
385 }
386 },
387 videoStudio: {
388 enabled: true,
389 remoteRunners: {
390 enabled: true
391 }
392 },
393 videoFile: {
394 update: {
395 enabled: true
396 }
397 },
398 import: {
399 videos: {
400 concurrency: 4,
401 http: {
402 enabled: false
403 },
404 torrent: {
405 enabled: false
406 }
407 },
408 videoChannelSynchronization: {
409 enabled: false,
410 maxPerUser: 10
411 }
412 },
413 trending: {
414 videos: {
415 algorithms: {
416 enabled: [ 'hot', 'most-viewed', 'most-liked' ],
417 default: 'hot'
418 }
419 }
420 },
421 autoBlacklist: {
422 videos: {
423 ofUsers: {
424 enabled: true
425 }
426 }
427 },
428 followers: {
429 instance: {
430 enabled: false,
431 manualApproval: true
432 }
433 },
434 followings: {
435 instance: {
436 autoFollowBack: {
437 enabled: true
438 },
439 autoFollowIndex: {
440 enabled: true,
441 indexUrl: 'https://updated.example.com'
442 }
443 }
444 },
445 broadcastMessage: {
446 enabled: true,
447 level: 'error',
448 message: 'super bad message',
449 dismissable: true
450 },
451 search: {
452 remoteUri: {
453 anonymous: true,
454 users: true
455 },
456 searchIndex: {
457 enabled: true,
458 url: 'https://search.joinpeertube.org',
459 disableLocalSearch: true,
460 isDefaultSearch: true
461 }
462 }
463}
464
465describe('Test static config', function () {
466 let server: PeerTubeServer = null
467
468 before(async function () {
469 this.timeout(30000)
470
471 server = await createSingleServer(1, { webadmin: { configuration: { edition: { allowed: false } } } })
472 await setAccessTokensToServers([ server ])
473 })
474
475 it('Should tell the client that edits are not allowed', async function () {
476 const data = await server.config.getConfig()
477
478 expect(data.webadmin.configuration.edition.allowed).to.be.false
479 })
480
481 it('Should error when client tries to update', async function () {
482 await server.config.updateCustomConfig({ newCustomConfig, expectedStatus: 405 })
483 })
484
485 after(async function () {
486 await cleanupTests([ server ])
487 })
488})
489
490describe('Test config', function () {
491 let server: PeerTubeServer = null
492
493 before(async function () {
494 this.timeout(30000)
495
496 server = await createSingleServer(1)
497 await setAccessTokensToServers([ server ])
498 })
499
500 it('Should have a correct config on a server with registration enabled', async function () {
501 const data = await server.config.getConfig()
502
503 expect(data.signup.allowed).to.be.true
504 })
505
506 it('Should have a correct config on a server with registration enabled and a users limit', async function () {
507 this.timeout(5000)
508
509 await Promise.all([
510 server.registrations.register({ username: 'user1' }),
511 server.registrations.register({ username: 'user2' }),
512 server.registrations.register({ username: 'user3' })
513 ])
514
515 const data = await server.config.getConfig()
516
517 expect(data.signup.allowed).to.be.false
518 })
519
520 it('Should have the correct video allowed extensions', async function () {
521 const data = await server.config.getConfig()
522
523 expect(data.video.file.extensions).to.have.lengthOf(3)
524 expect(data.video.file.extensions).to.contain('.mp4')
525 expect(data.video.file.extensions).to.contain('.webm')
526 expect(data.video.file.extensions).to.contain('.ogv')
527
528 await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
529 await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
530
531 expect(data.contactForm.enabled).to.be.true
532 })
533
534 it('Should get the customized configuration', async function () {
535 const data = await server.config.getCustomConfig()
536
537 checkInitialConfig(server, data)
538 })
539
540 it('Should update the customized configuration', async function () {
541 await server.config.updateCustomConfig({ newCustomConfig })
542
543 const data = await server.config.getCustomConfig()
544 checkUpdatedConfig(data)
545 })
546
547 it('Should have the correct updated video allowed extensions', async function () {
548 this.timeout(30000)
549
550 const data = await server.config.getConfig()
551
552 expect(data.video.file.extensions).to.have.length.above(4)
553 expect(data.video.file.extensions).to.contain('.mp4')
554 expect(data.video.file.extensions).to.contain('.webm')
555 expect(data.video.file.extensions).to.contain('.ogv')
556 expect(data.video.file.extensions).to.contain('.flv')
557 expect(data.video.file.extensions).to.contain('.wmv')
558 expect(data.video.file.extensions).to.contain('.mkv')
559 expect(data.video.file.extensions).to.contain('.mp3')
560 expect(data.video.file.extensions).to.contain('.ogg')
561 expect(data.video.file.extensions).to.contain('.flac')
562
563 await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.OK_200 })
564 await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.OK_200 })
565 })
566
567 it('Should have the configuration updated after a restart', async function () {
568 this.timeout(30000)
569
570 await killallServers([ server ])
571
572 await server.run()
573
574 const data = await server.config.getCustomConfig()
575
576 checkUpdatedConfig(data)
577 })
578
579 it('Should fetch the about information', async function () {
580 const data = await server.config.getAbout()
581
582 expect(data.instance.name).to.equal('PeerTube updated')
583 expect(data.instance.shortDescription).to.equal('my short description')
584 expect(data.instance.description).to.equal('my super description')
585 expect(data.instance.terms).to.equal('my super terms')
586 expect(data.instance.codeOfConduct).to.equal('my super coc')
587
588 expect(data.instance.creationReason).to.equal('my super creation reason')
589 expect(data.instance.moderationInformation).to.equal('my super moderation information')
590 expect(data.instance.administrator).to.equal('Kuja')
591 expect(data.instance.maintenanceLifetime).to.equal('forever')
592 expect(data.instance.businessModel).to.equal('my super business model')
593 expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
594
595 expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
596 expect(data.instance.categories).to.deep.equal([ 1, 2 ])
597 })
598
599 it('Should remove the custom configuration', async function () {
600 await server.config.deleteCustomConfig()
601
602 const data = await server.config.getCustomConfig()
603 checkInitialConfig(server, data)
604 })
605
606 it('Should enable/disable security headers', async function () {
607 this.timeout(25000)
608
609 {
610 const res = await makeGetRequest({
611 url: server.url,
612 path: '/api/v1/config',
613 expectedStatus: 200
614 })
615
616 expect(res.headers['x-frame-options']).to.exist
617 expect(res.headers['x-powered-by']).to.equal('PeerTube')
618 }
619
620 await killallServers([ server ])
621
622 const config = {
623 security: {
624 frameguard: { enabled: false },
625 powered_by_header: { enabled: false }
626 }
627 }
628 await server.run(config)
629
630 {
631 const res = await makeGetRequest({
632 url: server.url,
633 path: '/api/v1/config',
634 expectedStatus: 200
635 })
636
637 expect(res.headers['x-frame-options']).to.not.exist
638 expect(res.headers['x-powered-by']).to.not.exist
639 }
640 })
641
642 after(async function () {
643 await cleanupTests([ server ])
644 })
645})
diff --git a/packages/tests/src/api/server/contact-form.ts b/packages/tests/src/api/server/contact-form.ts
new file mode 100644
index 000000000..03389aa64
--- /dev/null
+++ b/packages/tests/src/api/server/contact-form.ts
@@ -0,0 +1,101 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { wait } from '@peertube/peertube-core-utils'
6import { HttpStatusCode } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 ConfigCommand,
10 ContactFormCommand,
11 createSingleServer,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test contact form', function () {
18 let server: PeerTubeServer
19 const emails: object[] = []
20 let command: ContactFormCommand
21
22 before(async function () {
23 this.timeout(30000)
24
25 const port = await MockSmtpServer.Instance.collectEmails(emails)
26
27 server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(port))
28 await setAccessTokensToServers([ server ])
29
30 command = server.contactForm
31 })
32
33 it('Should send a contact form', async function () {
34 await command.send({
35 fromEmail: 'toto@example.com',
36 body: 'my super message',
37 subject: 'my subject',
38 fromName: 'Super toto'
39 })
40
41 await waitJobs(server)
42
43 expect(emails).to.have.lengthOf(1)
44
45 const email = emails[0]
46
47 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
48 expect(email['replyTo'][0]['address']).equal('toto@example.com')
49 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
50 expect(email['subject']).contains('my subject')
51 expect(email['text']).contains('my super message')
52 })
53
54 it('Should not have duplicated email address in text message', async function () {
55 const text = emails[0]['text'] as string
56
57 const matches = text.match(/toto@example.com/g)
58 expect(matches).to.have.lengthOf(1)
59 })
60
61 it('Should not be able to send another contact form because of the anti spam checker', async function () {
62 await wait(1000)
63
64 await command.send({
65 fromEmail: 'toto@example.com',
66 body: 'my super message',
67 subject: 'my subject',
68 fromName: 'Super toto'
69 })
70
71 await command.send({
72 fromEmail: 'toto@example.com',
73 body: 'my super message',
74 fromName: 'Super toto',
75 subject: 'my subject',
76 expectedStatus: HttpStatusCode.FORBIDDEN_403
77 })
78 })
79
80 it('Should be able to send another contact form after a while', async function () {
81 await wait(1000)
82
83 await command.send({
84 fromEmail: 'toto@example.com',
85 fromName: 'Super toto',
86 subject: 'my subject',
87 body: 'my super message'
88 })
89 })
90
91 it('Should not have the manage preferences link in the email', async function () {
92 const email = emails[0]
93 expect(email['text']).to.not.contain('Manage your notification preferences')
94 })
95
96 after(async function () {
97 MockSmtpServer.Instance.kill()
98
99 await cleanupTests([ server ])
100 })
101})
diff --git a/packages/tests/src/api/server/email.ts b/packages/tests/src/api/server/email.ts
new file mode 100644
index 000000000..6d3f3f3bb
--- /dev/null
+++ b/packages/tests/src/api/server/email.ts
@@ -0,0 +1,371 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { HttpStatusCode } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 ConfigCommand,
9 createSingleServer,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test emails', function () {
16 let server: PeerTubeServer
17 let userId: number
18 let userId2: number
19 let userAccessToken: string
20
21 let videoShortUUID: string
22 let videoId: number
23
24 let videoUserUUID: string
25
26 let verificationString: string
27 let verificationString2: string
28
29 const emails: object[] = []
30 const user = {
31 username: 'user_1',
32 password: 'super_password'
33 }
34
35 before(async function () {
36 this.timeout(120000)
37
38 const emailPort = await MockSmtpServer.Instance.collectEmails(emails)
39 server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(emailPort))
40
41 await setAccessTokensToServers([ server ])
42 await server.config.enableSignup(true)
43
44 {
45 const created = await server.users.create({ username: user.username, password: user.password })
46 userId = created.id
47
48 userAccessToken = await server.login.getAccessToken(user)
49 }
50
51 {
52 const attributes = { name: 'my super user video' }
53 const { uuid } = await server.videos.upload({ token: userAccessToken, attributes })
54 videoUserUUID = uuid
55 }
56
57 {
58 const attributes = {
59 name: 'my super name'
60 }
61 const { shortUUID, id } = await server.videos.upload({ attributes })
62 videoShortUUID = shortUUID
63 videoId = id
64 }
65 })
66
67 describe('When resetting user password', function () {
68
69 it('Should ask to reset the password', async function () {
70 await server.users.askResetPassword({ email: 'user_1@example.com' })
71
72 await waitJobs(server)
73 expect(emails).to.have.lengthOf(1)
74
75 const email = emails[0]
76
77 expect(email['from'][0]['name']).equal('PeerTube')
78 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
79 expect(email['to'][0]['address']).equal('user_1@example.com')
80 expect(email['subject']).contains('password')
81
82 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
83 expect(verificationStringMatches).not.to.be.null
84
85 verificationString = verificationStringMatches[1]
86 expect(verificationString).to.have.length.above(2)
87
88 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
89 expect(userIdMatches).not.to.be.null
90
91 userId = parseInt(userIdMatches[1], 10)
92 expect(verificationString).to.not.be.undefined
93 })
94
95 it('Should not reset the password with an invalid verification string', async function () {
96 await server.users.resetPassword({
97 userId,
98 verificationString: verificationString + 'b',
99 password: 'super_password2',
100 expectedStatus: HttpStatusCode.FORBIDDEN_403
101 })
102 })
103
104 it('Should reset the password', async function () {
105 await server.users.resetPassword({ userId, verificationString, password: 'super_password2' })
106 })
107
108 it('Should not reset the password with the same verification string', async function () {
109 await server.users.resetPassword({
110 userId,
111 verificationString,
112 password: 'super_password3',
113 expectedStatus: HttpStatusCode.FORBIDDEN_403
114 })
115 })
116
117 it('Should login with this new password', async function () {
118 user.password = 'super_password2'
119
120 await server.login.getAccessToken(user)
121 })
122 })
123
124 describe('When creating a user without password', function () {
125
126 it('Should send a create password email', async function () {
127 await server.users.create({ username: 'create_password', password: '' })
128
129 await waitJobs(server)
130 expect(emails).to.have.lengthOf(2)
131
132 const email = emails[1]
133
134 expect(email['from'][0]['name']).equal('PeerTube')
135 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
136 expect(email['to'][0]['address']).equal('create_password@example.com')
137 expect(email['subject']).contains('account')
138 expect(email['subject']).contains('password')
139
140 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
141 expect(verificationStringMatches).not.to.be.null
142
143 verificationString2 = verificationStringMatches[1]
144 expect(verificationString2).to.have.length.above(2)
145
146 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
147 expect(userIdMatches).not.to.be.null
148
149 userId2 = parseInt(userIdMatches[1], 10)
150 })
151
152 it('Should not reset the password with an invalid verification string', async function () {
153 await server.users.resetPassword({
154 userId: userId2,
155 verificationString: verificationString2 + 'c',
156 password: 'newly_created_password',
157 expectedStatus: HttpStatusCode.FORBIDDEN_403
158 })
159 })
160
161 it('Should reset the password', async function () {
162 await server.users.resetPassword({
163 userId: userId2,
164 verificationString: verificationString2,
165 password: 'newly_created_password'
166 })
167 })
168
169 it('Should login with this new password', async function () {
170 await server.login.getAccessToken({
171 username: 'create_password',
172 password: 'newly_created_password'
173 })
174 })
175 })
176
177 describe('When creating an abuse', function () {
178
179 it('Should send the notification email', async function () {
180 const reason = 'my super bad reason'
181 await server.abuses.report({ token: userAccessToken, videoId, reason })
182
183 await waitJobs(server)
184 expect(emails).to.have.lengthOf(3)
185
186 const email = emails[2]
187
188 expect(email['from'][0]['name']).equal('PeerTube')
189 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
190 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
191 expect(email['subject']).contains('abuse')
192 expect(email['text']).contains(videoShortUUID)
193 })
194 })
195
196 describe('When blocking/unblocking user', function () {
197
198 it('Should send the notification email when blocking a user', async function () {
199 const reason = 'my super bad reason'
200 await server.users.banUser({ userId, reason })
201
202 await waitJobs(server)
203 expect(emails).to.have.lengthOf(4)
204
205 const email = emails[3]
206
207 expect(email['from'][0]['name']).equal('PeerTube')
208 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
209 expect(email['to'][0]['address']).equal('user_1@example.com')
210 expect(email['subject']).contains(' blocked')
211 expect(email['text']).contains(' blocked')
212 expect(email['text']).contains('bad reason')
213 })
214
215 it('Should send the notification email when unblocking a user', async function () {
216 await server.users.unbanUser({ userId })
217
218 await waitJobs(server)
219 expect(emails).to.have.lengthOf(5)
220
221 const email = emails[4]
222
223 expect(email['from'][0]['name']).equal('PeerTube')
224 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
225 expect(email['to'][0]['address']).equal('user_1@example.com')
226 expect(email['subject']).contains(' unblocked')
227 expect(email['text']).contains(' unblocked')
228 })
229 })
230
231 describe('When blacklisting a video', function () {
232 it('Should send the notification email', async function () {
233 const reason = 'my super reason'
234 await server.blacklist.add({ videoId: videoUserUUID, reason })
235
236 await waitJobs(server)
237 expect(emails).to.have.lengthOf(6)
238
239 const email = emails[5]
240
241 expect(email['from'][0]['name']).equal('PeerTube')
242 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
243 expect(email['to'][0]['address']).equal('user_1@example.com')
244 expect(email['subject']).contains(' blacklisted')
245 expect(email['text']).contains('my super user video')
246 expect(email['text']).contains('my super reason')
247 })
248
249 it('Should send the notification email', async function () {
250 await server.blacklist.remove({ videoId: videoUserUUID })
251
252 await waitJobs(server)
253 expect(emails).to.have.lengthOf(7)
254
255 const email = emails[6]
256
257 expect(email['from'][0]['name']).equal('PeerTube')
258 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
259 expect(email['to'][0]['address']).equal('user_1@example.com')
260 expect(email['subject']).contains(' unblacklisted')
261 expect(email['text']).contains('my super user video')
262 })
263
264 it('Should have the manage preferences link in the email', async function () {
265 const email = emails[6]
266 expect(email['text']).to.contain('Manage your notification preferences')
267 })
268 })
269
270 describe('When verifying a user email', function () {
271
272 it('Should ask to send the verification email', async function () {
273 await server.users.askSendVerifyEmail({ email: 'user_1@example.com' })
274
275 await waitJobs(server)
276 expect(emails).to.have.lengthOf(8)
277
278 const email = emails[7]
279
280 expect(email['from'][0]['name']).equal('PeerTube')
281 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
282 expect(email['to'][0]['address']).equal('user_1@example.com')
283 expect(email['subject']).contains('Verify')
284
285 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
286 expect(verificationStringMatches).not.to.be.null
287
288 verificationString = verificationStringMatches[1]
289 expect(verificationString).to.not.be.undefined
290 expect(verificationString).to.have.length.above(2)
291
292 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
293 expect(userIdMatches).not.to.be.null
294
295 userId = parseInt(userIdMatches[1], 10)
296 })
297
298 it('Should not verify the email with an invalid verification string', async function () {
299 await server.users.verifyEmail({
300 userId,
301 verificationString: verificationString + 'b',
302 isPendingEmail: false,
303 expectedStatus: HttpStatusCode.FORBIDDEN_403
304 })
305 })
306
307 it('Should verify the email', async function () {
308 await server.users.verifyEmail({ userId, verificationString })
309 })
310 })
311
312 describe('When verifying a registration email', function () {
313 let registrationId: number
314 let registrationIdEmail: number
315
316 before(async function () {
317 const { id } = await server.registrations.requestRegistration({
318 username: 'request_1',
319 email: 'request_1@example.com',
320 registrationReason: 'tt'
321 })
322 registrationId = id
323 })
324
325 it('Should ask to send the verification email', async function () {
326 await server.registrations.askSendVerifyEmail({ email: 'request_1@example.com' })
327
328 await waitJobs(server)
329 expect(emails).to.have.lengthOf(9)
330
331 const email = emails[8]
332
333 expect(email['from'][0]['name']).equal('PeerTube')
334 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
335 expect(email['to'][0]['address']).equal('request_1@example.com')
336 expect(email['subject']).contains('Verify')
337
338 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
339 expect(verificationStringMatches).not.to.be.null
340
341 verificationString = verificationStringMatches[1]
342 expect(verificationString).to.not.be.undefined
343 expect(verificationString).to.have.length.above(2)
344
345 const registrationIdMatches = /registrationId=([0-9]+)/.exec(email['text'])
346 expect(registrationIdMatches).not.to.be.null
347
348 registrationIdEmail = parseInt(registrationIdMatches[1], 10)
349
350 expect(registrationId).to.equal(registrationIdEmail)
351 })
352
353 it('Should not verify the email with an invalid verification string', async function () {
354 await server.registrations.verifyEmail({
355 registrationId: registrationIdEmail,
356 verificationString: verificationString + 'b',
357 expectedStatus: HttpStatusCode.FORBIDDEN_403
358 })
359 })
360
361 it('Should verify the email', async function () {
362 await server.registrations.verifyEmail({ registrationId: registrationIdEmail, verificationString })
363 })
364 })
365
366 after(async function () {
367 MockSmtpServer.Instance.kill()
368
369 await cleanupTests([ server ])
370 })
371})
diff --git a/packages/tests/src/api/server/follow-constraints.ts b/packages/tests/src/api/server/follow-constraints.ts
new file mode 100644
index 000000000..8d277c906
--- /dev/null
+++ b/packages/tests/src/api/server/follow-constraints.ts
@@ -0,0 +1,321 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode, PeerTubeProblemDocument, ServerErrorCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test follow constraints', function () {
15 let servers: PeerTubeServer[] = []
16 let video1UUID: string
17 let video2UUID: string
18 let userToken: string
19
20 before(async function () {
21 this.timeout(240000)
22
23 servers = await createMultipleServers(2)
24
25 // Get the access tokens
26 await setAccessTokensToServers(servers)
27
28 {
29 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } })
30 video1UUID = uuid
31 }
32 {
33 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } })
34 video2UUID = uuid
35 }
36
37 const user = {
38 username: 'user1',
39 password: 'super_password'
40 }
41 await servers[0].users.create({ username: user.username, password: user.password })
42 userToken = await servers[0].login.getAccessToken(user)
43
44 await doubleFollow(servers[0], servers[1])
45 })
46
47 describe('With a followed instance', function () {
48
49 describe('With an unlogged user', function () {
50
51 it('Should get the local video', async function () {
52 await servers[0].videos.get({ id: video1UUID })
53 })
54
55 it('Should get the remote video', async function () {
56 await servers[0].videos.get({ id: video2UUID })
57 })
58
59 it('Should list local account videos', async function () {
60 const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@' + servers[0].host })
61
62 expect(total).to.equal(1)
63 expect(data).to.have.lengthOf(1)
64 })
65
66 it('Should list remote account videos', async function () {
67 const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@' + servers[1].host })
68
69 expect(total).to.equal(1)
70 expect(data).to.have.lengthOf(1)
71 })
72
73 it('Should list local channel videos', async function () {
74 const handle = 'root_channel@' + servers[0].host
75 const { total, data } = await servers[0].videos.listByChannel({ handle })
76
77 expect(total).to.equal(1)
78 expect(data).to.have.lengthOf(1)
79 })
80
81 it('Should list remote channel videos', async function () {
82 const handle = 'root_channel@' + servers[1].host
83 const { total, data } = await servers[0].videos.listByChannel({ handle })
84
85 expect(total).to.equal(1)
86 expect(data).to.have.lengthOf(1)
87 })
88 })
89
90 describe('With a logged user', function () {
91 it('Should get the local video', async function () {
92 await servers[0].videos.getWithToken({ token: userToken, id: video1UUID })
93 })
94
95 it('Should get the remote video', async function () {
96 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
97 })
98
99 it('Should list local account videos', async function () {
100 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[0].host })
101
102 expect(total).to.equal(1)
103 expect(data).to.have.lengthOf(1)
104 })
105
106 it('Should list remote account videos', async function () {
107 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[1].host })
108
109 expect(total).to.equal(1)
110 expect(data).to.have.lengthOf(1)
111 })
112
113 it('Should list local channel videos', async function () {
114 const handle = 'root_channel@' + servers[0].host
115 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
116
117 expect(total).to.equal(1)
118 expect(data).to.have.lengthOf(1)
119 })
120
121 it('Should list remote channel videos', async function () {
122 const handle = 'root_channel@' + servers[1].host
123 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
124
125 expect(total).to.equal(1)
126 expect(data).to.have.lengthOf(1)
127 })
128 })
129 })
130
131 describe('With a non followed instance', function () {
132
133 before(async function () {
134 this.timeout(30000)
135
136 await servers[0].follows.unfollow({ target: servers[1] })
137 })
138
139 describe('With an unlogged user', function () {
140
141 it('Should get the local video', async function () {
142 await servers[0].videos.get({ id: video1UUID })
143 })
144
145 it('Should not get the remote video', async function () {
146 const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
147 const error = body as unknown as PeerTubeProblemDocument
148
149 const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints'
150 expect(error.type).to.equal(doc)
151 expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
152
153 expect(error.detail).to.equal('Cannot get this video regarding follow constraints')
154 expect(error.error).to.equal(error.detail)
155
156 expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
157
158 expect(error.originUrl).to.contains(servers[1].url)
159 })
160
161 it('Should list local account videos', async function () {
162 const { total, data } = await servers[0].videos.listByAccount({
163 token: null,
164 handle: 'root@' + servers[0].host
165 })
166
167 expect(total).to.equal(1)
168 expect(data).to.have.lengthOf(1)
169 })
170
171 it('Should not list remote account videos', async function () {
172 const { total, data } = await servers[0].videos.listByAccount({
173 token: null,
174 handle: 'root@' + servers[1].host
175 })
176
177 expect(total).to.equal(0)
178 expect(data).to.have.lengthOf(0)
179 })
180
181 it('Should list local channel videos', async function () {
182 const handle = 'root_channel@' + servers[0].host
183 const { total, data } = await servers[0].videos.listByChannel({ token: null, handle })
184
185 expect(total).to.equal(1)
186 expect(data).to.have.lengthOf(1)
187 })
188
189 it('Should not list remote channel videos', async function () {
190 const handle = 'root_channel@' + servers[1].host
191 const { total, data } = await servers[0].videos.listByChannel({ token: null, handle })
192
193 expect(total).to.equal(0)
194 expect(data).to.have.lengthOf(0)
195 })
196 })
197
198 describe('With a logged user', function () {
199
200 it('Should get the local video', async function () {
201 await servers[0].videos.getWithToken({ token: userToken, id: video1UUID })
202 })
203
204 it('Should get the remote video', async function () {
205 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
206 })
207
208 it('Should list local account videos', async function () {
209 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[0].host })
210
211 expect(total).to.equal(1)
212 expect(data).to.have.lengthOf(1)
213 })
214
215 it('Should list remote account videos', async function () {
216 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[1].host })
217
218 expect(total).to.equal(1)
219 expect(data).to.have.lengthOf(1)
220 })
221
222 it('Should list local channel videos', async function () {
223 const handle = 'root_channel@' + servers[0].host
224 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
225
226 expect(total).to.equal(1)
227 expect(data).to.have.lengthOf(1)
228 })
229
230 it('Should list remote channel videos', async function () {
231 const handle = 'root_channel@' + servers[1].host
232 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
233
234 expect(total).to.equal(1)
235 expect(data).to.have.lengthOf(1)
236 })
237 })
238 })
239
240 describe('When following a remote account', function () {
241
242 before(async function () {
243 this.timeout(60000)
244
245 await servers[0].follows.follow({ handles: [ 'root@' + servers[1].host ] })
246 await waitJobs(servers)
247 })
248
249 it('Should get the remote video with an unlogged user', async function () {
250 await servers[0].videos.get({ id: video2UUID })
251 })
252
253 it('Should get the remote video with a logged in user', async function () {
254 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
255 })
256 })
257
258 describe('When unfollowing a remote account', function () {
259
260 before(async function () {
261 this.timeout(60000)
262
263 await servers[0].follows.unfollow({ target: 'root@' + servers[1].host })
264 await waitJobs(servers)
265 })
266
267 it('Should not get the remote video with an unlogged user', async function () {
268 const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
269
270 const error = body as unknown as PeerTubeProblemDocument
271 expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
272 })
273
274 it('Should get the remote video with a logged in user', async function () {
275 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
276 })
277 })
278
279 describe('When following a remote channel', function () {
280
281 before(async function () {
282 this.timeout(60000)
283
284 await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[1].host ] })
285 await waitJobs(servers)
286 })
287
288 it('Should get the remote video with an unlogged user', async function () {
289 await servers[0].videos.get({ id: video2UUID })
290 })
291
292 it('Should get the remote video with a logged in user', async function () {
293 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
294 })
295 })
296
297 describe('When unfollowing a remote channel', function () {
298
299 before(async function () {
300 this.timeout(60000)
301
302 await servers[0].follows.unfollow({ target: 'root_channel@' + servers[1].host })
303 await waitJobs(servers)
304 })
305
306 it('Should not get the remote video with an unlogged user', async function () {
307 const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
308
309 const error = body as unknown as PeerTubeProblemDocument
310 expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
311 })
312
313 it('Should get the remote video with a logged in user', async function () {
314 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
315 })
316 })
317
318 after(async function () {
319 await cleanupTests(servers)
320 })
321})
diff --git a/packages/tests/src/api/server/follows-moderation.ts b/packages/tests/src/api/server/follows-moderation.ts
new file mode 100644
index 000000000..811dd5c22
--- /dev/null
+++ b/packages/tests/src/api/server/follows-moderation.ts
@@ -0,0 +1,364 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { expectStartWith } from '@tests/shared/checks.js'
5import { ActorFollow, FollowState } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 FollowsCommand,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15async function checkServer1And2HasFollowers (servers: PeerTubeServer[], state = 'accepted') {
16 const fns = [
17 servers[0].follows.getFollowings.bind(servers[0].follows),
18 servers[1].follows.getFollowers.bind(servers[1].follows)
19 ]
20
21 for (const fn of fns) {
22 const body = await fn({ start: 0, count: 5, sort: 'createdAt' })
23 expect(body.total).to.equal(1)
24
25 const follow = body.data[0]
26 expect(follow.state).to.equal(state)
27 expect(follow.follower.url).to.equal(servers[0].url + '/accounts/peertube')
28 expect(follow.following.url).to.equal(servers[1].url + '/accounts/peertube')
29 }
30}
31
32async function checkFollows (options: {
33 follower: PeerTubeServer
34 followerState: FollowState | 'deleted'
35
36 following: PeerTubeServer
37 followingState: FollowState | 'deleted'
38}) {
39 const { follower, followerState, followingState, following } = options
40
41 const followerUrl = follower.url + '/accounts/peertube'
42 const followingUrl = following.url + '/accounts/peertube'
43 const finder = (d: ActorFollow) => d.follower.url === followerUrl && d.following.url === followingUrl
44
45 {
46 const { data } = await follower.follows.getFollowings()
47 const follow = data.find(finder)
48
49 if (followerState === 'deleted') {
50 expect(follow).to.not.exist
51 } else {
52 expect(follow.state).to.equal(followerState)
53 expect(follow.follower.url).to.equal(followerUrl)
54 expect(follow.following.url).to.equal(followingUrl)
55 }
56 }
57
58 {
59 const { data } = await following.follows.getFollowers()
60 const follow = data.find(finder)
61
62 if (followingState === 'deleted') {
63 expect(follow).to.not.exist
64 } else {
65 expect(follow.state).to.equal(followingState)
66 expect(follow.follower.url).to.equal(followerUrl)
67 expect(follow.following.url).to.equal(followingUrl)
68 }
69 }
70}
71
72async function checkNoFollowers (servers: PeerTubeServer[]) {
73 const fns = [
74 servers[0].follows.getFollowings.bind(servers[0].follows),
75 servers[1].follows.getFollowers.bind(servers[1].follows)
76 ]
77
78 for (const fn of fns) {
79 const body = await fn({ start: 0, count: 5, sort: 'createdAt', state: 'accepted' })
80 expect(body.total).to.equal(0)
81 }
82}
83
84describe('Test follows moderation', function () {
85 let servers: PeerTubeServer[] = []
86 let commands: FollowsCommand[]
87
88 before(async function () {
89 this.timeout(240000)
90
91 servers = await createMultipleServers(3)
92
93 // Get the access tokens
94 await setAccessTokensToServers(servers)
95
96 commands = servers.map(s => s.follows)
97 })
98
99 describe('Default behaviour', function () {
100
101 it('Should have server 1 following server 2', async function () {
102 this.timeout(30000)
103
104 await commands[0].follow({ hosts: [ servers[1].url ] })
105
106 await waitJobs(servers)
107 })
108
109 it('Should have correct follows', async function () {
110 await checkServer1And2HasFollowers(servers)
111 })
112
113 it('Should remove follower on server 2', async function () {
114 await commands[1].removeFollower({ follower: servers[0] })
115
116 await waitJobs(servers)
117 })
118
119 it('Should not not have follows anymore', async function () {
120 await checkNoFollowers(servers)
121 })
122 })
123
124 describe('Disabled/Enabled followers', function () {
125
126 it('Should disable followers on server 2', async function () {
127 const subConfig = {
128 followers: {
129 instance: {
130 enabled: false,
131 manualApproval: false
132 }
133 }
134 }
135
136 await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
137
138 await commands[0].follow({ hosts: [ servers[1].url ] })
139 await waitJobs(servers)
140
141 await checkNoFollowers(servers)
142 })
143
144 it('Should re enable followers on server 2', async function () {
145 const subConfig = {
146 followers: {
147 instance: {
148 enabled: true,
149 manualApproval: false
150 }
151 }
152 }
153
154 await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
155
156 await commands[0].follow({ hosts: [ servers[1].url ] })
157 await waitJobs(servers)
158
159 await checkServer1And2HasFollowers(servers)
160 })
161 })
162
163 describe('Manual approbation', function () {
164
165 it('Should manually approve followers', async function () {
166 this.timeout(20000)
167
168 await commands[0].unfollow({ target: servers[1] })
169 await waitJobs(servers)
170
171 const subConfig = {
172 followers: {
173 instance: {
174 enabled: true,
175 manualApproval: true
176 }
177 }
178 }
179
180 await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
181 await servers[2].config.updateCustomSubConfig({ newConfig: subConfig })
182
183 await commands[0].follow({ hosts: [ servers[1].url ] })
184 await waitJobs(servers)
185
186 await checkServer1And2HasFollowers(servers, 'pending')
187 })
188
189 it('Should accept a follower', async function () {
190 await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host })
191 await waitJobs(servers)
192
193 await checkServer1And2HasFollowers(servers)
194 })
195
196 it('Should reject another follower', async function () {
197 this.timeout(20000)
198
199 await commands[0].follow({ hosts: [ servers[2].url ] })
200 await waitJobs(servers)
201
202 {
203 const body = await commands[0].getFollowings()
204 expect(body.total).to.equal(2)
205 }
206
207 {
208 const body = await commands[1].getFollowers()
209 expect(body.total).to.equal(1)
210 }
211
212 {
213 const body = await commands[2].getFollowers()
214 expect(body.total).to.equal(1)
215 }
216
217 await commands[2].rejectFollower({ follower: 'peertube@' + servers[0].host })
218 await waitJobs(servers)
219
220 { // server 1
221 {
222 const { data } = await commands[0].getFollowings({ state: 'accepted' })
223 expect(data).to.have.lengthOf(1)
224 }
225
226 {
227 const { data } = await commands[0].getFollowings({ state: 'rejected' })
228 expect(data).to.have.lengthOf(1)
229 expectStartWith(data[0].following.url, servers[2].url)
230 }
231 }
232
233 { // server 3
234 {
235 const { data } = await commands[2].getFollowers({ state: 'accepted' })
236 expect(data).to.have.lengthOf(0)
237 }
238
239 {
240 const { data } = await commands[2].getFollowers({ state: 'rejected' })
241 expect(data).to.have.lengthOf(1)
242 expectStartWith(data[0].follower.url, servers[0].url)
243 }
244 }
245 })
246
247 it('Should still auto accept channel followers', async function () {
248 await commands[0].follow({ handles: [ 'root_channel@' + servers[1].host ] })
249
250 await waitJobs(servers)
251
252 const body = await commands[0].getFollowings()
253 const follow = body.data[0]
254 expect(follow.following.name).to.equal('root_channel')
255 expect(follow.state).to.equal('accepted')
256 })
257 })
258
259 describe('Accept/reject state', function () {
260
261 it('Should not change the follow on refollow with and without auto accept', async function () {
262 const run = async () => {
263 await commands[0].follow({ hosts: [ servers[2].url ] })
264 await waitJobs(servers)
265
266 await checkFollows({
267 follower: servers[0],
268 followerState: 'rejected',
269 following: servers[2],
270 followingState: 'rejected'
271 })
272 }
273
274 await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: false } } } })
275 await run()
276
277 await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: true } } } })
278 await run()
279 })
280
281 it('Should not change the rejected status on unfollow', async function () {
282 await commands[0].unfollow({ target: servers[2] })
283 await waitJobs(servers)
284
285 await checkFollows({
286 follower: servers[0],
287 followerState: 'deleted',
288 following: servers[2],
289 followingState: 'rejected'
290 })
291 })
292
293 it('Should delete the follower and add again the follower', async function () {
294 await commands[2].removeFollower({ follower: servers[0] })
295 await waitJobs(servers)
296
297 await commands[0].follow({ hosts: [ servers[2].url ] })
298 await waitJobs(servers)
299
300 await checkFollows({
301 follower: servers[0],
302 followerState: 'pending',
303 following: servers[2],
304 followingState: 'pending'
305 })
306 })
307
308 it('Should be able to reject a previously accepted follower', async function () {
309 await commands[1].rejectFollower({ follower: 'peertube@' + servers[0].host })
310 await waitJobs(servers)
311
312 await checkFollows({
313 follower: servers[0],
314 followerState: 'rejected',
315 following: servers[1],
316 followingState: 'rejected'
317 })
318 })
319
320 it('Should be able to re accept a previously rejected follower', async function () {
321 await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host })
322 await waitJobs(servers)
323
324 await checkFollows({
325 follower: servers[0],
326 followerState: 'accepted',
327 following: servers[1],
328 followingState: 'accepted'
329 })
330 })
331 })
332
333 describe('Muted servers', function () {
334
335 it('Should ignore follow requests of muted servers', async function () {
336 await servers[1].blocklist.addToServerBlocklist({ server: servers[0].host })
337
338 await commands[0].unfollow({ target: servers[1] })
339
340 await waitJobs(servers)
341
342 await checkFollows({
343 follower: servers[0],
344 followerState: 'deleted',
345 following: servers[1],
346 followingState: 'deleted'
347 })
348
349 await commands[0].follow({ hosts: [ servers[1].host ] })
350 await waitJobs(servers)
351
352 await checkFollows({
353 follower: servers[0],
354 followerState: 'rejected',
355 following: servers[1],
356 followingState: 'deleted'
357 })
358 })
359 })
360
361 after(async function () {
362 await cleanupTests(servers)
363 })
364})
diff --git a/packages/tests/src/api/server/follows.ts b/packages/tests/src/api/server/follows.ts
new file mode 100644
index 000000000..fbe2e87da
--- /dev/null
+++ b/packages/tests/src/api/server/follows.ts
@@ -0,0 +1,644 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { Video, VideoPrivacy } from '@peertube/peertube-models'
5import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands'
6import { expectAccountFollows, expectChannelsFollows } from '@tests/shared/actors.js'
7import { testCaptionFile } from '@tests/shared/captions.js'
8import { dateIsValid } from '@tests/shared/checks.js'
9import { completeVideoCheck } from '@tests/shared/videos.js'
10
11describe('Test follows', function () {
12
13 describe('Complex follow', function () {
14 let servers: PeerTubeServer[] = []
15
16 before(async function () {
17 this.timeout(120000)
18
19 servers = await createMultipleServers(3)
20
21 // Get the access tokens
22 await setAccessTokensToServers(servers)
23 })
24
25 describe('Data propagation after follow', function () {
26
27 it('Should not have followers/followings', async function () {
28 for (const server of servers) {
29 const bodies = await Promise.all([
30 server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }),
31 server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
32 ])
33
34 for (const body of bodies) {
35 expect(body.total).to.equal(0)
36
37 const follows = body.data
38 expect(follows).to.be.an('array')
39 expect(follows).to.have.lengthOf(0)
40 }
41 }
42 })
43
44 it('Should have server 1 following root account of server 2 and server 3', async function () {
45 this.timeout(30000)
46
47 await servers[0].follows.follow({
48 hosts: [ servers[2].url ],
49 handles: [ 'root@' + servers[1].host ]
50 })
51
52 await waitJobs(servers)
53 })
54
55 it('Should have 2 followings on server 1', async function () {
56 const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' })
57 expect(body.total).to.equal(2)
58
59 let follows = body.data
60 expect(follows).to.be.an('array')
61 expect(follows).to.have.lengthOf(1)
62
63 const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' })
64 follows = follows.concat(body2.data)
65
66 const server2Follow = follows.find(f => f.following.host === servers[1].host)
67 const server3Follow = follows.find(f => f.following.host === servers[2].host)
68
69 expect(server2Follow).to.not.be.undefined
70 expect(server2Follow.following.name).to.equal('root')
71 expect(server2Follow.state).to.equal('accepted')
72
73 expect(server3Follow).to.not.be.undefined
74 expect(server3Follow.following.name).to.equal('peertube')
75 expect(server3Follow.state).to.equal('accepted')
76 })
77
78 it('Should have 0 followings on server 2 and 3', async function () {
79 for (const server of [ servers[1], servers[2] ]) {
80 const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
81 expect(body.total).to.equal(0)
82
83 const follows = body.data
84 expect(follows).to.be.an('array')
85 expect(follows).to.have.lengthOf(0)
86 }
87 })
88
89 it('Should have 1 followers on server 3', async function () {
90 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
91 expect(body.total).to.equal(1)
92
93 const follows = body.data
94 expect(follows).to.be.an('array')
95 expect(follows).to.have.lengthOf(1)
96 expect(follows[0].follower.host).to.equal(servers[0].host)
97 })
98
99 it('Should have 0 followers on server 1 and 2', async function () {
100 for (const server of [ servers[0], servers[1] ]) {
101 const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' })
102 expect(body.total).to.equal(0)
103
104 const follows = body.data
105 expect(follows).to.be.an('array')
106 expect(follows).to.have.lengthOf(0)
107 }
108 })
109
110 it('Should search/filter followings on server 1', async function () {
111 const sort = 'createdAt'
112 const start = 0
113 const count = 1
114
115 {
116 const search = ':' + servers[1].port
117
118 {
119 const body = await servers[0].follows.getFollowings({ start, count, sort, search })
120 expect(body.total).to.equal(1)
121
122 const follows = body.data
123 expect(follows).to.have.lengthOf(1)
124 expect(follows[0].following.host).to.equal(servers[1].host)
125 }
126
127 {
128 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' })
129 expect(body.total).to.equal(1)
130 expect(body.data).to.have.lengthOf(1)
131 }
132
133 {
134 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
135 expect(body.total).to.equal(1)
136 expect(body.data).to.have.lengthOf(1)
137 }
138
139 {
140 const body = await servers[0].follows.getFollowings({
141 start,
142 count,
143 sort,
144 search,
145 state: 'accepted',
146 actorType: 'Application'
147 })
148 expect(body.total).to.equal(0)
149 expect(body.data).to.have.lengthOf(0)
150 }
151
152 {
153 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' })
154 expect(body.total).to.equal(0)
155 expect(body.data).to.have.lengthOf(0)
156 }
157 }
158
159 {
160 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' })
161 expect(body.total).to.equal(1)
162 expect(body.data).to.have.lengthOf(1)
163 }
164
165 {
166 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' })
167 expect(body.total).to.equal(0)
168
169 expect(body.data).to.have.lengthOf(0)
170 }
171 })
172
173 it('Should search/filter followers on server 2', async function () {
174 const start = 0
175 const count = 5
176 const sort = 'createdAt'
177
178 {
179 const search = servers[0].port + ''
180
181 {
182 const body = await servers[2].follows.getFollowers({ start, count, sort, search })
183 expect(body.total).to.equal(1)
184
185 const follows = body.data
186 expect(follows).to.have.lengthOf(1)
187 expect(follows[0].following.host).to.equal(servers[2].host)
188 }
189
190 {
191 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' })
192 expect(body.total).to.equal(1)
193 expect(body.data).to.have.lengthOf(1)
194 }
195
196 {
197 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
198 expect(body.total).to.equal(0)
199 expect(body.data).to.have.lengthOf(0)
200 }
201
202 {
203 const body = await servers[2].follows.getFollowers({
204 start,
205 count,
206 sort,
207 search,
208 state: 'accepted',
209 actorType: 'Application'
210 })
211 expect(body.total).to.equal(1)
212 expect(body.data).to.have.lengthOf(1)
213 }
214
215 {
216 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' })
217 expect(body.total).to.equal(0)
218 expect(body.data).to.have.lengthOf(0)
219 }
220 }
221
222 {
223 const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' })
224 expect(body.total).to.equal(0)
225
226 const follows = body.data
227 expect(follows).to.have.lengthOf(0)
228 }
229 })
230
231 it('Should have the correct follows counts', async function () {
232 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
233 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
234 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
235
236 // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
237 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
238 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
239 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
240
241 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
242 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
243 })
244
245 it('Should unfollow server 3 on server 1', async function () {
246 this.timeout(15000)
247
248 await servers[0].follows.unfollow({ target: servers[2] })
249
250 await waitJobs(servers)
251 })
252
253 it('Should not follow server 3 on server 1 anymore', async function () {
254 const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' })
255 expect(body.total).to.equal(1)
256
257 const follows = body.data
258 expect(follows).to.be.an('array')
259 expect(follows).to.have.lengthOf(1)
260
261 expect(follows[0].following.host).to.equal(servers[1].host)
262 })
263
264 it('Should not have server 1 as follower on server 3 anymore', async function () {
265 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
266 expect(body.total).to.equal(0)
267
268 const follows = body.data
269 expect(follows).to.be.an('array')
270 expect(follows).to.have.lengthOf(0)
271 })
272
273 it('Should have the correct follows counts after the unfollow', async function () {
274 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
275 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
276 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
277
278 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
279 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
280 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
281
282 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 })
283 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
284 })
285
286 it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
287 this.timeout(160000)
288
289 await servers[1].videos.upload({ attributes: { name: 'server2' } })
290 await servers[2].videos.upload({ attributes: { name: 'server3' } })
291
292 await waitJobs(servers)
293
294 {
295 const { total, data } = await servers[0].videos.list()
296 expect(total).to.equal(1)
297 expect(data[0].name).to.equal('server2')
298 }
299
300 {
301 const { total, data } = await servers[1].videos.list()
302 expect(total).to.equal(1)
303 expect(data[0].name).to.equal('server2')
304 }
305
306 {
307 const { total, data } = await servers[2].videos.list()
308 expect(total).to.equal(1)
309 expect(data[0].name).to.equal('server3')
310 }
311 })
312
313 it('Should remove account follow', async function () {
314 this.timeout(15000)
315
316 await servers[0].follows.unfollow({ target: 'root@' + servers[1].host })
317
318 await waitJobs(servers)
319 })
320
321 it('Should have removed the account follow', async function () {
322 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
323 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
324
325 {
326 const { total, data } = await servers[0].follows.getFollowings()
327 expect(total).to.equal(0)
328 expect(data).to.have.lengthOf(0)
329 }
330
331 {
332 const { total, data } = await servers[0].videos.list()
333 expect(total).to.equal(0)
334 expect(data).to.have.lengthOf(0)
335 }
336 })
337
338 it('Should follow a channel', async function () {
339 this.timeout(15000)
340
341 await servers[0].follows.follow({
342 handles: [ 'root_channel@' + servers[1].host ]
343 })
344
345 await waitJobs(servers)
346
347 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
348 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
349
350 {
351 const { total, data } = await servers[0].follows.getFollowings()
352 expect(total).to.equal(1)
353 expect(data).to.have.lengthOf(1)
354 }
355
356 {
357 const { total, data } = await servers[0].videos.list()
358 expect(total).to.equal(1)
359 expect(data).to.have.lengthOf(1)
360 }
361 })
362 })
363
364 describe('Should propagate data on a new server follow', function () {
365 let video4: Video
366
367 before(async function () {
368 this.timeout(240000)
369
370 const video4Attributes = {
371 name: 'server3-4',
372 category: 2,
373 nsfw: true,
374 licence: 6,
375 tags: [ 'tag1', 'tag2', 'tag3' ]
376 }
377
378 await servers[2].videos.upload({ attributes: { name: 'server3-2' } })
379 await servers[2].videos.upload({ attributes: { name: 'server3-3' } })
380
381 const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes })
382
383 await servers[2].videos.upload({ attributes: { name: 'server3-5' } })
384 await servers[2].videos.upload({ attributes: { name: 'server3-6' } })
385
386 {
387 const userAccessToken = await servers[2].users.generateUserAndToken('captain')
388
389 await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' })
390 await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' })
391 }
392
393 {
394 await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' })
395
396 await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' })
397 await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' })
398 await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' })
399 }
400
401 {
402 const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' })
403 await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' })
404
405 const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' })
406
407 await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' })
408
409 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId })
410 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId })
411 }
412
413 await servers[2].captions.add({
414 language: 'ar',
415 videoId: video4CreateResult.id,
416 fixture: 'subtitle-good2.vtt'
417 })
418
419 await waitJobs(servers)
420
421 // Server 1 follows server 3
422 await servers[0].follows.follow({ hosts: [ servers[2].url ] })
423
424 await waitJobs(servers)
425 })
426
427 it('Should have the correct follows counts', async function () {
428 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
429 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
430 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
431 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
432
433 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
434 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
435 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
436 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
437
438 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
439 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
440 })
441
442 it('Should have propagated videos', async function () {
443 const { total, data } = await servers[0].videos.list()
444 expect(total).to.equal(7)
445
446 const video2 = data.find(v => v.name === 'server3-2')
447 video4 = data.find(v => v.name === 'server3-4')
448 const video6 = data.find(v => v.name === 'server3-6')
449
450 expect(video2).to.not.be.undefined
451 expect(video4).to.not.be.undefined
452 expect(video6).to.not.be.undefined
453
454 const isLocal = false
455 const checkAttributes = {
456 name: 'server3-4',
457 category: 2,
458 licence: 6,
459 language: 'zh',
460 nsfw: true,
461 description: 'my super description',
462 support: 'my super support text',
463 account: {
464 name: 'root',
465 host: servers[2].host
466 },
467 isLocal,
468 commentsEnabled: true,
469 downloadEnabled: true,
470 duration: 5,
471 tags: [ 'tag1', 'tag2', 'tag3' ],
472 privacy: VideoPrivacy.PUBLIC,
473 likes: 1,
474 dislikes: 1,
475 channel: {
476 displayName: 'Main root channel',
477 name: 'root_channel',
478 description: '',
479 isLocal
480 },
481 fixture: 'video_short.webm',
482 files: [
483 {
484 resolution: 720,
485 size: 218910
486 }
487 ]
488 }
489 await completeVideoCheck({
490 server: servers[0],
491 originServer: servers[2],
492 videoUUID: video4.uuid,
493 attributes: checkAttributes
494 })
495 })
496
497 it('Should have propagated comments', async function () {
498 const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' })
499
500 expect(total).to.equal(2)
501 expect(data).to.be.an('array')
502 expect(data).to.have.lengthOf(2)
503
504 {
505 const comment = data[0]
506 expect(comment.inReplyToCommentId).to.be.null
507 expect(comment.text).equal('my super first comment')
508 expect(comment.videoId).to.equal(video4.id)
509 expect(comment.id).to.equal(comment.threadId)
510 expect(comment.account.name).to.equal('root')
511 expect(comment.account.host).to.equal(servers[2].host)
512 expect(comment.totalReplies).to.equal(3)
513 expect(dateIsValid(comment.createdAt as string)).to.be.true
514 expect(dateIsValid(comment.updatedAt as string)).to.be.true
515
516 const threadId = comment.threadId
517
518 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId })
519 expect(tree.comment.text).equal('my super first comment')
520 expect(tree.children).to.have.lengthOf(2)
521
522 const firstChild = tree.children[0]
523 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
524 expect(firstChild.children).to.have.lengthOf(1)
525
526 const childOfFirstChild = firstChild.children[0]
527 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
528 expect(childOfFirstChild.children).to.have.lengthOf(0)
529
530 const secondChild = tree.children[1]
531 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
532 expect(secondChild.children).to.have.lengthOf(0)
533 }
534
535 {
536 const deletedComment = data[1]
537 expect(deletedComment).to.not.be.undefined
538 expect(deletedComment.isDeleted).to.be.true
539 expect(deletedComment.deletedAt).to.not.be.null
540 expect(deletedComment.text).to.equal('')
541 expect(deletedComment.inReplyToCommentId).to.be.null
542 expect(deletedComment.account).to.be.null
543 expect(deletedComment.totalReplies).to.equal(2)
544 expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
545
546 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId })
547 const [ commentRoot, deletedChildRoot ] = tree.children
548
549 expect(deletedChildRoot).to.not.be.undefined
550 expect(deletedChildRoot.comment.isDeleted).to.be.true
551 expect(deletedChildRoot.comment.deletedAt).to.not.be.null
552 expect(deletedChildRoot.comment.text).to.equal('')
553 expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
554 expect(deletedChildRoot.comment.account).to.be.null
555 expect(deletedChildRoot.children).to.have.lengthOf(1)
556
557 const answerToDeletedChild = deletedChildRoot.children[0]
558 expect(answerToDeletedChild.comment).to.not.be.undefined
559 expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id)
560 expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted')
561 expect(answerToDeletedChild.comment.account.name).to.equal('root')
562
563 expect(commentRoot.comment).to.not.be.undefined
564 expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
565 expect(commentRoot.comment.text).to.equal('answer to deleted')
566 expect(commentRoot.comment.account.name).to.equal('root')
567 }
568 })
569
570 it('Should have propagated captions', async function () {
571 const body = await servers[0].captions.list({ videoId: video4.id })
572 expect(body.total).to.equal(1)
573 expect(body.data).to.have.lengthOf(1)
574
575 const caption1 = body.data[0]
576 expect(caption1.language.id).to.equal('ar')
577 expect(caption1.language.label).to.equal('Arabic')
578 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$'))
579 await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.')
580 })
581
582 it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () {
583 this.timeout(5000)
584
585 await servers[0].follows.unfollow({ target: servers[2] })
586
587 await waitJobs(servers)
588
589 const { total } = await servers[0].videos.list()
590 expect(total).to.equal(1)
591 })
592 })
593
594 after(async function () {
595 await cleanupTests(servers)
596 })
597 })
598
599 describe('Simple data propagation propagate data on a new channel follow', function () {
600 let servers: PeerTubeServer[] = []
601
602 before(async function () {
603 this.timeout(120000)
604
605 servers = await createMultipleServers(3)
606 await setAccessTokensToServers(servers)
607
608 await servers[0].videos.upload({ attributes: { name: 'video to add' } })
609
610 await waitJobs(servers)
611
612 for (const server of [ servers[1], servers[2] ]) {
613 const video = await server.videos.find({ name: 'video to add' })
614 expect(video).to.not.exist
615 }
616 })
617
618 it('Should have propagated video after new channel follow', async function () {
619 this.timeout(60000)
620
621 await servers[1].follows.follow({ handles: [ 'root_channel@' + servers[0].host ] })
622
623 await waitJobs(servers)
624
625 const video = await servers[1].videos.find({ name: 'video to add' })
626 expect(video).to.exist
627 })
628
629 it('Should have propagated video after new account follow', async function () {
630 this.timeout(60000)
631
632 await servers[2].follows.follow({ handles: [ 'root@' + servers[0].host ] })
633
634 await waitJobs(servers)
635
636 const video = await servers[2].videos.find({ name: 'video to add' })
637 expect(video).to.exist
638 })
639
640 after(async function () {
641 await cleanupTests(servers)
642 })
643 })
644})
diff --git a/packages/tests/src/api/server/handle-down.ts b/packages/tests/src/api/server/handle-down.ts
new file mode 100644
index 000000000..604df129f
--- /dev/null
+++ b/packages/tests/src/api/server/handle-down.ts
@@ -0,0 +1,339 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, JobState, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 CommentsCommand,
9 createMultipleServers,
10 killallServers,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15import { SQLCommand } from '@tests/shared/sql-command.js'
16import { completeVideoCheck } from '@tests/shared/videos.js'
17
18describe('Test handle downs', function () {
19 let servers: PeerTubeServer[] = []
20 let sqlCommands: SQLCommand[] = []
21
22 let threadIdServer1: number
23 let threadIdServer2: number
24 let commentIdServer1: number
25 let commentIdServer2: number
26 let missedVideo1: VideoCreateResult
27 let missedVideo2: VideoCreateResult
28 let unlistedVideo: VideoCreateResult
29
30 const videoIdsServer1: string[] = []
31
32 const videoAttributes = {
33 name: 'my super name for server 1',
34 category: 5,
35 licence: 4,
36 language: 'ja',
37 nsfw: true,
38 privacy: VideoPrivacy.PUBLIC,
39 description: 'my super description for server 1',
40 support: 'my super support text for server 1',
41 tags: [ 'tag1p1', 'tag2p1' ],
42 fixture: 'video_short1.webm'
43 }
44
45 const unlistedVideoAttributes = { ...videoAttributes, privacy: VideoPrivacy.UNLISTED }
46
47 let checkAttributes: any
48 let unlistedCheckAttributes: any
49
50 let commentCommands: CommentsCommand[]
51
52 before(async function () {
53 this.timeout(120000)
54
55 servers = await createMultipleServers(3)
56 commentCommands = servers.map(s => s.comments)
57
58 checkAttributes = {
59 name: 'my super name for server 1',
60 category: 5,
61 licence: 4,
62 language: 'ja',
63 nsfw: true,
64 description: 'my super description for server 1',
65 support: 'my super support text for server 1',
66 account: {
67 name: 'root',
68 host: servers[0].host
69 },
70 isLocal: false,
71 duration: 10,
72 tags: [ 'tag1p1', 'tag2p1' ],
73 privacy: VideoPrivacy.PUBLIC,
74 commentsEnabled: true,
75 downloadEnabled: true,
76 channel: {
77 name: 'root_channel',
78 displayName: 'Main root channel',
79 description: '',
80 isLocal: false
81 },
82 fixture: 'video_short1.webm',
83 files: [
84 {
85 resolution: 720,
86 size: 572456
87 }
88 ]
89 }
90 unlistedCheckAttributes = { ...checkAttributes, privacy: VideoPrivacy.UNLISTED }
91
92 // Get the access tokens
93 await setAccessTokensToServers(servers)
94
95 sqlCommands = servers.map(s => new SQLCommand(s))
96 })
97
98 it('Should remove followers that are often down', async function () {
99 this.timeout(240000)
100
101 // Server 2 and 3 follow server 1
102 await servers[1].follows.follow({ hosts: [ servers[0].url ] })
103 await servers[2].follows.follow({ hosts: [ servers[0].url ] })
104
105 await waitJobs(servers)
106
107 // Upload a video to server 1
108 await servers[0].videos.upload({ attributes: videoAttributes })
109
110 await waitJobs(servers)
111
112 // And check all servers have this video
113 for (const server of servers) {
114 const { data } = await server.videos.list()
115 expect(data).to.be.an('array')
116 expect(data).to.have.lengthOf(1)
117 }
118
119 // Kill server 2
120 await killallServers([ servers[1] ])
121
122 // Remove server 2 follower
123 for (let i = 0; i < 10; i++) {
124 await servers[0].videos.upload({ attributes: videoAttributes })
125 }
126
127 await waitJobs([ servers[0], servers[2] ])
128
129 // Kill server 3
130 await killallServers([ servers[2] ])
131
132 missedVideo1 = await servers[0].videos.upload({ attributes: videoAttributes })
133
134 missedVideo2 = await servers[0].videos.upload({ attributes: videoAttributes })
135
136 // Unlisted video
137 unlistedVideo = await servers[0].videos.upload({ attributes: unlistedVideoAttributes })
138
139 // Add comments to video 2
140 {
141 const text = 'thread 1'
142 let comment = await commentCommands[0].createThread({ videoId: missedVideo2.uuid, text })
143 threadIdServer1 = comment.id
144
145 comment = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-1' })
146
147 const created = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-2' })
148 commentIdServer1 = created.id
149 }
150
151 await waitJobs(servers[0])
152 // Wait scheduler
153 await wait(11000)
154
155 // Only server 3 is still a follower of server 1
156 const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' })
157 expect(body.data).to.be.an('array')
158 expect(body.data).to.have.lengthOf(1)
159 expect(body.data[0].follower.host).to.equal(servers[2].host)
160 })
161
162 it('Should not have pending/processing jobs anymore', async function () {
163 const states: JobState[] = [ 'waiting', 'active' ]
164
165 for (const state of states) {
166 const body = await servers[0].jobs.list({
167 state,
168 start: 0,
169 count: 50,
170 sort: '-createdAt'
171 })
172 expect(body.data).to.have.length(0)
173 }
174 })
175
176 it('Should re-follow server 1', async function () {
177 this.timeout(70000)
178
179 await servers[1].run()
180 await servers[2].run()
181
182 await servers[1].follows.unfollow({ target: servers[0] })
183 await waitJobs(servers)
184
185 await servers[1].follows.follow({ hosts: [ servers[0].url ] })
186
187 await waitJobs(servers)
188
189 const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' })
190 expect(body.data).to.be.an('array')
191 expect(body.data).to.have.lengthOf(2)
192 })
193
194 it('Should send an update to server 3, and automatically fetch the video', async function () {
195 this.timeout(15000)
196
197 {
198 const { data } = await servers[2].videos.list()
199 expect(data).to.be.an('array')
200 expect(data).to.have.lengthOf(11)
201 }
202
203 await servers[0].videos.update({ id: missedVideo1.uuid })
204 await servers[0].videos.update({ id: unlistedVideo.uuid })
205
206 await waitJobs(servers)
207
208 {
209 const { data } = await servers[2].videos.list()
210 expect(data).to.be.an('array')
211 // 1 video is unlisted
212 expect(data).to.have.lengthOf(12)
213 }
214
215 // Check unlisted video
216 const video = await servers[2].videos.get({ id: unlistedVideo.uuid })
217 await completeVideoCheck({ server: servers[2], originServer: servers[0], videoUUID: video.uuid, attributes: unlistedCheckAttributes })
218 })
219
220 it('Should send comments on a video to server 3, and automatically fetch the video', async function () {
221 this.timeout(25000)
222
223 await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer1, text: 'comment 1-3' })
224
225 await waitJobs(servers)
226
227 await servers[2].videos.get({ id: missedVideo2.uuid })
228
229 {
230 const { data } = await servers[2].comments.listThreads({ videoId: missedVideo2.uuid })
231 expect(data).to.be.an('array')
232 expect(data).to.have.lengthOf(1)
233
234 threadIdServer2 = data[0].id
235
236 const tree = await servers[2].comments.getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer2 })
237 expect(tree.comment.text).equal('thread 1')
238 expect(tree.children).to.have.lengthOf(1)
239
240 const firstChild = tree.children[0]
241 expect(firstChild.comment.text).to.equal('comment 1-1')
242 expect(firstChild.children).to.have.lengthOf(1)
243
244 const childOfFirstChild = firstChild.children[0]
245 expect(childOfFirstChild.comment.text).to.equal('comment 1-2')
246 expect(childOfFirstChild.children).to.have.lengthOf(1)
247
248 const childOfChildFirstChild = childOfFirstChild.children[0]
249 expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3')
250 expect(childOfChildFirstChild.children).to.have.lengthOf(0)
251
252 commentIdServer2 = childOfChildFirstChild.comment.id
253 }
254 })
255
256 it('Should correctly reply to the comment', async function () {
257 this.timeout(15000)
258
259 await servers[2].comments.addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer2, text: 'comment 1-4' })
260
261 await waitJobs(servers)
262
263 const tree = await commentCommands[0].getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer1 })
264
265 expect(tree.comment.text).equal('thread 1')
266 expect(tree.children).to.have.lengthOf(1)
267
268 const firstChild = tree.children[0]
269 expect(firstChild.comment.text).to.equal('comment 1-1')
270 expect(firstChild.children).to.have.lengthOf(1)
271
272 const childOfFirstChild = firstChild.children[0]
273 expect(childOfFirstChild.comment.text).to.equal('comment 1-2')
274 expect(childOfFirstChild.children).to.have.lengthOf(1)
275
276 const childOfChildFirstChild = childOfFirstChild.children[0]
277 expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3')
278 expect(childOfChildFirstChild.children).to.have.lengthOf(1)
279
280 const childOfChildOfChildOfFirstChild = childOfChildFirstChild.children[0]
281 expect(childOfChildOfChildOfFirstChild.comment.text).to.equal('comment 1-4')
282 expect(childOfChildOfChildOfFirstChild.children).to.have.lengthOf(0)
283 })
284
285 it('Should upload many videos on server 1', async function () {
286 this.timeout(240000)
287
288 for (let i = 0; i < 10; i++) {
289 const uuid = (await servers[0].videos.quickUpload({ name: 'video ' + i })).uuid
290 videoIdsServer1.push(uuid)
291 }
292
293 await waitJobs(servers)
294
295 for (const id of videoIdsServer1) {
296 await servers[1].videos.get({ id })
297 }
298
299 await waitJobs(servers)
300 await sqlCommands[1].setActorFollowScores(20)
301
302 // Wait video expiration
303 await wait(11000)
304
305 // Refresh video -> score + 10 = 30
306 await servers[1].videos.get({ id: videoIdsServer1[0] })
307
308 await waitJobs(servers)
309 })
310
311 it('Should remove followings that are down', async function () {
312 this.timeout(120000)
313
314 await killallServers([ servers[0] ])
315
316 // Wait video expiration
317 await wait(11000)
318
319 for (let i = 0; i < 5; i++) {
320 try {
321 await servers[1].videos.get({ id: videoIdsServer1[i] })
322 await waitJobs([ servers[1] ])
323 await wait(1500)
324 } catch {}
325 }
326
327 for (const id of videoIdsServer1) {
328 await servers[1].videos.get({ id, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
329 }
330 })
331
332 after(async function () {
333 for (const sqlCommand of sqlCommands) {
334 await sqlCommand.cleanup()
335 }
336
337 await cleanupTests(servers)
338 })
339})
diff --git a/packages/tests/src/api/server/homepage.ts b/packages/tests/src/api/server/homepage.ts
new file mode 100644
index 000000000..082a2fb91
--- /dev/null
+++ b/packages/tests/src/api/server/homepage.ts
@@ -0,0 +1,81 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 CustomPagesCommand,
9 killallServers,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar
14} from '@peertube/peertube-server-commands'
15
16async function getHomepageState (server: PeerTubeServer) {
17 const config = await server.config.getConfig()
18
19 return config.homepage.enabled
20}
21
22describe('Test instance homepage actions', function () {
23 let server: PeerTubeServer
24 let command: CustomPagesCommand
25
26 before(async function () {
27 this.timeout(30000)
28
29 server = await createSingleServer(1)
30 await setAccessTokensToServers([ server ])
31 await setDefaultChannelAvatar(server)
32 await setDefaultAccountAvatar(server)
33
34 command = server.customPage
35 })
36
37 it('Should not have a homepage', async function () {
38 const state = await getHomepageState(server)
39 expect(state).to.be.false
40
41 await command.getInstanceHomepage({ expectedStatus: HttpStatusCode.NOT_FOUND_404 })
42 })
43
44 it('Should set a homepage', async function () {
45 await command.updateInstanceHomepage({ content: '<picsou-magazine></picsou-magazine>' })
46
47 const page = await command.getInstanceHomepage()
48 expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
49
50 const state = await getHomepageState(server)
51 expect(state).to.be.true
52 })
53
54 it('Should have the same homepage after a restart', async function () {
55 this.timeout(30000)
56
57 await killallServers([ server ])
58
59 await server.run()
60
61 const page = await command.getInstanceHomepage()
62 expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
63
64 const state = await getHomepageState(server)
65 expect(state).to.be.true
66 })
67
68 it('Should empty the homepage', async function () {
69 await command.updateInstanceHomepage({ content: '' })
70
71 const page = await command.getInstanceHomepage()
72 expect(page.content).to.be.empty
73
74 const state = await getHomepageState(server)
75 expect(state).to.be.false
76 })
77
78 after(async function () {
79 await cleanupTests([ server ])
80 })
81})
diff --git a/packages/tests/src/api/server/index.ts b/packages/tests/src/api/server/index.ts
new file mode 100644
index 000000000..5c80a5a37
--- /dev/null
+++ b/packages/tests/src/api/server/index.ts
@@ -0,0 +1,22 @@
1import './auto-follows.js'
2import './bulk.js'
3import './config-defaults.js'
4import './config.js'
5import './contact-form.js'
6import './email.js'
7import './follow-constraints.js'
8import './follows.js'
9import './follows-moderation.js'
10import './homepage.js'
11import './handle-down.js'
12import './jobs.js'
13import './logs.js'
14import './reverse-proxy.js'
15import './services.js'
16import './slow-follows.js'
17import './stats.js'
18import './tracker.js'
19import './no-client.js'
20import './open-telemetry.js'
21import './plugins.js'
22import './proxy.js'
diff --git a/packages/tests/src/api/server/jobs.ts b/packages/tests/src/api/server/jobs.ts
new file mode 100644
index 000000000..3d60b1431
--- /dev/null
+++ b/packages/tests/src/api/server/jobs.ts
@@ -0,0 +1,128 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { dateIsValid } from '@tests/shared/checks.js'
5import { wait } from '@peertube/peertube-core-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test jobs', function () {
16 let servers: PeerTubeServer[]
17
18 before(async function () {
19 this.timeout(240000)
20
21 servers = await createMultipleServers(2)
22
23 await setAccessTokensToServers(servers)
24
25 // Server 1 and server 2 follow each other
26 await doubleFollow(servers[0], servers[1])
27 })
28
29 it('Should create some jobs', async function () {
30 this.timeout(240000)
31
32 await servers[1].videos.upload({ attributes: { name: 'video1' } })
33 await servers[1].videos.upload({ attributes: { name: 'video2' } })
34
35 await waitJobs(servers)
36 })
37
38 it('Should list jobs', async function () {
39 const body = await servers[1].jobs.list({ state: 'completed' })
40 expect(body.total).to.be.above(2)
41 expect(body.data).to.have.length.above(2)
42 })
43
44 it('Should list jobs with sort, pagination and job type', async function () {
45 {
46 const body = await servers[1].jobs.list({
47 state: 'completed',
48 start: 1,
49 count: 2,
50 sort: 'createdAt'
51 })
52 expect(body.total).to.be.above(2)
53 expect(body.data).to.have.lengthOf(2)
54
55 let job = body.data[0]
56 // Skip repeat jobs
57 if (job.type === 'videos-views-stats') job = body.data[1]
58
59 expect(job.state).to.equal('completed')
60 expect(dateIsValid(job.createdAt as string)).to.be.true
61 expect(dateIsValid(job.processedOn as string)).to.be.true
62 expect(dateIsValid(job.finishedOn as string)).to.be.true
63 }
64
65 {
66 const body = await servers[1].jobs.list({
67 state: 'completed',
68 start: 0,
69 count: 100,
70 sort: 'createdAt',
71 jobType: 'activitypub-http-broadcast'
72 })
73 expect(body.total).to.be.above(2)
74
75 for (const j of body.data) {
76 expect(j.type).to.equal('activitypub-http-broadcast')
77 }
78 }
79 })
80
81 it('Should list all jobs', async function () {
82 const body = await servers[1].jobs.list()
83 expect(body.total).to.be.above(2)
84
85 const jobs = body.data
86 expect(jobs).to.have.length.above(2)
87
88 expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined
89 })
90
91 it('Should pause the job queue', async function () {
92 this.timeout(120000)
93
94 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video2' } })
95 await waitJobs(servers)
96
97 await servers[1].jobs.pauseJobQueue()
98 await servers[1].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
99
100 await wait(5000)
101
102 {
103 const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
104 // waiting includes waiting-children
105 expect(body.data).to.have.lengthOf(4)
106 }
107
108 {
109 const body = await servers[1].jobs.list({ state: 'waiting-children', jobType: 'video-transcoding' })
110 expect(body.data).to.have.lengthOf(1)
111 }
112 })
113
114 it('Should resume the job queue', async function () {
115 this.timeout(120000)
116
117 await servers[1].jobs.resumeJobQueue()
118
119 await waitJobs(servers)
120
121 const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
122 expect(body.data).to.have.lengthOf(0)
123 })
124
125 after(async function () {
126 await cleanupTests(servers)
127 })
128})
diff --git a/packages/tests/src/api/server/logs.ts b/packages/tests/src/api/server/logs.ts
new file mode 100644
index 000000000..11c86d694
--- /dev/null
+++ b/packages/tests/src/api/server/logs.ts
@@ -0,0 +1,265 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 killallServers,
9 LogsCommand,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test logs', function () {
16 let server: PeerTubeServer
17 let logsCommand: LogsCommand
18
19 before(async function () {
20 this.timeout(30000)
21
22 server = await createSingleServer(1)
23 await setAccessTokensToServers([ server ])
24
25 logsCommand = server.logs
26 })
27
28 describe('With the standard log file', function () {
29
30 it('Should get logs with a start date', async function () {
31 this.timeout(60000)
32
33 await server.videos.upload({ attributes: { name: 'video 1' } })
34 await waitJobs([ server ])
35
36 const now = new Date()
37
38 await server.videos.upload({ attributes: { name: 'video 2' } })
39 await waitJobs([ server ])
40
41 const body = await logsCommand.getLogs({ startDate: now })
42 const logsString = JSON.stringify(body)
43
44 expect(logsString.includes('Video with name video 1')).to.be.false
45 expect(logsString.includes('Video with name video 2')).to.be.true
46 })
47
48 it('Should get logs with an end date', async function () {
49 this.timeout(60000)
50
51 await server.videos.upload({ attributes: { name: 'video 3' } })
52 await waitJobs([ server ])
53
54 const now1 = new Date()
55
56 await server.videos.upload({ attributes: { name: 'video 4' } })
57 await waitJobs([ server ])
58
59 const now2 = new Date()
60
61 await server.videos.upload({ attributes: { name: 'video 5' } })
62 await waitJobs([ server ])
63
64 const body = await logsCommand.getLogs({ startDate: now1, endDate: now2 })
65 const logsString = JSON.stringify(body)
66
67 expect(logsString.includes('Video with name video 3')).to.be.false
68 expect(logsString.includes('Video with name video 4')).to.be.true
69 expect(logsString.includes('Video with name video 5')).to.be.false
70 })
71
72 it('Should filter by level', async function () {
73 this.timeout(60000)
74
75 const now = new Date()
76
77 await server.videos.upload({ attributes: { name: 'video 6' } })
78 await waitJobs([ server ])
79
80 {
81 const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
82 const logsString = JSON.stringify(body)
83
84 expect(logsString.includes('Video with name video 6')).to.be.true
85 }
86
87 {
88 const body = await logsCommand.getLogs({ startDate: now, level: 'warn' })
89 const logsString = JSON.stringify(body)
90
91 expect(logsString.includes('Video with name video 6')).to.be.false
92 }
93 })
94
95 it('Should filter by tag', async function () {
96 const now = new Date()
97
98 const { uuid } = await server.videos.upload({ attributes: { name: 'video 6' } })
99 await waitJobs([ server ])
100
101 {
102 const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ 'toto' ] })
103 expect(body).to.have.lengthOf(0)
104 }
105
106 {
107 const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ uuid ] })
108 expect(body).to.not.have.lengthOf(0)
109
110 for (const line of body) {
111 expect(line.tags).to.contain(uuid)
112 }
113 }
114 })
115
116 it('Should log ping requests', async function () {
117 const now = new Date()
118
119 await server.servers.ping()
120
121 const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
122 const logsString = JSON.stringify(body)
123
124 expect(logsString.includes('/api/v1/ping')).to.be.true
125 })
126
127 it('Should not log ping requests', async function () {
128 this.timeout(60000)
129
130 await killallServers([ server ])
131
132 await server.run({ log: { log_ping_requests: false } })
133
134 const now = new Date()
135
136 await server.servers.ping()
137
138 const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
139 const logsString = JSON.stringify(body)
140
141 expect(logsString.includes('/api/v1/ping')).to.be.false
142 })
143 })
144
145 describe('With the audit log', function () {
146
147 it('Should get logs with a start date', async function () {
148 this.timeout(60000)
149
150 await server.videos.upload({ attributes: { name: 'video 7' } })
151 await waitJobs([ server ])
152
153 const now = new Date()
154
155 await server.videos.upload({ attributes: { name: 'video 8' } })
156 await waitJobs([ server ])
157
158 const body = await logsCommand.getAuditLogs({ startDate: now })
159 const logsString = JSON.stringify(body)
160
161 expect(logsString.includes('video 7')).to.be.false
162 expect(logsString.includes('video 8')).to.be.true
163
164 expect(body).to.have.lengthOf(1)
165
166 const item = body[0]
167
168 const message = JSON.parse(item.message)
169 expect(message.domain).to.equal('videos')
170 expect(message.action).to.equal('create')
171 })
172
173 it('Should get logs with an end date', async function () {
174 this.timeout(60000)
175
176 await server.videos.upload({ attributes: { name: 'video 9' } })
177 await waitJobs([ server ])
178
179 const now1 = new Date()
180
181 await server.videos.upload({ attributes: { name: 'video 10' } })
182 await waitJobs([ server ])
183
184 const now2 = new Date()
185
186 await server.videos.upload({ attributes: { name: 'video 11' } })
187 await waitJobs([ server ])
188
189 const body = await logsCommand.getAuditLogs({ startDate: now1, endDate: now2 })
190 const logsString = JSON.stringify(body)
191
192 expect(logsString.includes('video 9')).to.be.false
193 expect(logsString.includes('video 10')).to.be.true
194 expect(logsString.includes('video 11')).to.be.false
195 })
196 })
197
198 describe('When creating log from the client', function () {
199
200 it('Should create a warn client log', async function () {
201 const now = new Date()
202
203 await server.logs.createLogClient({
204 payload: {
205 level: 'warn',
206 url: 'http://example.com',
207 message: 'my super client message'
208 },
209 token: null
210 })
211
212 const body = await logsCommand.getLogs({ startDate: now })
213 const logsString = JSON.stringify(body)
214
215 expect(logsString.includes('my super client message')).to.be.true
216 })
217
218 it('Should create an error authenticated client log', async function () {
219 const now = new Date()
220
221 await server.logs.createLogClient({
222 payload: {
223 url: 'https://example.com/page1',
224 level: 'error',
225 message: 'my super client message 2',
226 userAgent: 'super user agent',
227 meta: '{hello}',
228 stackTrace: 'super stack trace'
229 }
230 })
231
232 const body = await logsCommand.getLogs({ startDate: now })
233 const logsString = JSON.stringify(body)
234
235 expect(logsString.includes('my super client message 2')).to.be.true
236 expect(logsString.includes('super user agent')).to.be.true
237 expect(logsString.includes('super stack trace')).to.be.true
238 expect(logsString.includes('{hello}')).to.be.true
239 expect(logsString.includes('https://example.com/page1')).to.be.true
240 })
241
242 it('Should refuse to create client logs', async function () {
243 await server.kill()
244
245 await server.run({
246 log: {
247 accept_client_log: false
248 }
249 })
250
251 await server.logs.createLogClient({
252 payload: {
253 level: 'warn',
254 url: 'http://example.com',
255 message: 'my super client message'
256 },
257 expectedStatus: HttpStatusCode.FORBIDDEN_403
258 })
259 })
260 })
261
262 after(async function () {
263 await cleanupTests([ server ])
264 })
265})
diff --git a/packages/tests/src/api/server/no-client.ts b/packages/tests/src/api/server/no-client.ts
new file mode 100644
index 000000000..0f097d50b
--- /dev/null
+++ b/packages/tests/src/api/server/no-client.ts
@@ -0,0 +1,24 @@
1import request from 'supertest'
2import { HttpStatusCode } from '@peertube/peertube-models'
3import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands'
4
5describe('Start and stop server without web client routes', function () {
6 let server: PeerTubeServer
7
8 before(async function () {
9 this.timeout(30000)
10
11 server = await createSingleServer(1, {}, { peertubeArgs: [ '--no-client' ] })
12 })
13
14 it('Should fail getting the client', function () {
15 const req = request(server.url)
16 .get('/')
17
18 return req.expect(HttpStatusCode.NOT_FOUND_404)
19 })
20
21 after(async function () {
22 await cleanupTests([ server ])
23 })
24})
diff --git a/packages/tests/src/api/server/open-telemetry.ts b/packages/tests/src/api/server/open-telemetry.ts
new file mode 100644
index 000000000..8ed3801db
--- /dev/null
+++ b/packages/tests/src/api/server/open-telemetry.ts
@@ -0,0 +1,193 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode, PlaybackMetricCreate, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeRawRequest,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12import { expectLogDoesNotContain, expectLogContain } from '@tests/shared/checks.js'
13import { MockHTTP } from '@tests/shared/mock-servers/mock-http.js'
14
15describe('Open Telemetry', function () {
16 let server: PeerTubeServer
17
18 describe('Metrics', function () {
19 const metricsUrl = 'http://127.0.0.1:9092/metrics'
20
21 it('Should not enable open telemetry metrics', async function () {
22 this.timeout(60000)
23
24 server = await createSingleServer(1)
25
26 let hasError = false
27 try {
28 await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
29 } catch (err) {
30 hasError = err.message.includes('ECONNREFUSED')
31 }
32
33 expect(hasError).to.be.true
34
35 await server.kill()
36 })
37
38 it('Should enable open telemetry metrics', async function () {
39 this.timeout(120000)
40
41 await server.run({
42 open_telemetry: {
43 metrics: {
44 enabled: true
45 }
46 }
47 })
48
49 // Simulate a HTTP request
50 await server.videos.list()
51
52 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
53 expect(res.text).to.contain('peertube_job_queue_total{')
54 expect(res.text).to.contain('http_request_duration_ms_bucket{')
55 })
56
57 it('Should have playback metrics', async function () {
58 await setAccessTokensToServers([ server ])
59
60 const video = await server.videos.quickUpload({ name: 'video' })
61
62 await server.metrics.addPlaybackMetric({
63 metrics: {
64 playerMode: 'p2p-media-loader',
65 resolution: VideoResolution.H_1080P,
66 fps: 30,
67 resolutionChanges: 1,
68 errors: 2,
69 downloadedBytesP2P: 0,
70 downloadedBytesHTTP: 0,
71 uploadedBytesP2P: 5,
72 p2pPeers: 1,
73 p2pEnabled: false,
74 videoId: video.uuid
75 }
76 })
77
78 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
79
80 expect(res.text).to.contain('peertube_playback_http_downloaded_bytes_total{')
81 expect(res.text).to.contain('peertube_playback_p2p_peers{')
82 expect(res.text).to.contain('p2pEnabled="false"')
83 })
84
85 it('Should take the last playback metric', async function () {
86 await setAccessTokensToServers([ server ])
87
88 const video = await server.videos.quickUpload({ name: 'video' })
89
90 const metrics = {
91 playerMode: 'p2p-media-loader',
92 resolution: VideoResolution.H_1080P,
93 fps: 30,
94 resolutionChanges: 1,
95 errors: 2,
96 downloadedBytesP2P: 0,
97 downloadedBytesHTTP: 0,
98 uploadedBytesP2P: 5,
99 p2pPeers: 7,
100 p2pEnabled: false,
101 videoId: video.uuid
102 } as PlaybackMetricCreate
103
104 await server.metrics.addPlaybackMetric({ metrics })
105
106 metrics.p2pPeers = 42
107 await server.metrics.addPlaybackMetric({ metrics })
108
109 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
110
111 // eslint-disable-next-line max-len
112 const label = `{videoOrigin="local",playerMode="p2p-media-loader",resolution="1080",fps="30",p2pEnabled="false",videoUUID="${video.uuid}"}`
113 expect(res.text).to.contain(`peertube_playback_p2p_peers${label} 42`)
114 expect(res.text).to.not.contain(`peertube_playback_p2p_peers${label} 7`)
115 })
116
117 it('Should disable http request duration metrics', async function () {
118 await server.kill()
119
120 await server.run({
121 open_telemetry: {
122 metrics: {
123 enabled: true,
124 http_request_duration: {
125 enabled: false
126 }
127 }
128 }
129 })
130
131 // Simulate a HTTP request
132 await server.videos.list()
133
134 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
135 expect(res.text).to.not.contain('http_request_duration_ms_bucket{')
136 })
137
138 after(async function () {
139 await server.kill()
140 })
141 })
142
143 describe('Tracing', function () {
144 let mockHTTP: MockHTTP
145 let mockPort: number
146
147 before(async function () {
148 mockHTTP = new MockHTTP()
149 mockPort = await mockHTTP.initialize()
150 })
151
152 it('Should enable open telemetry tracing', async function () {
153 server = await createSingleServer(1)
154
155 await expectLogDoesNotContain(server, 'Registering Open Telemetry tracing')
156
157 await server.kill()
158 })
159
160 it('Should enable open telemetry metrics', async function () {
161 await server.run({
162 open_telemetry: {
163 tracing: {
164 enabled: true,
165 jaeger_exporter: {
166 endpoint: 'http://127.0.0.1:' + mockPort
167 }
168 }
169 }
170 })
171
172 await expectLogContain(server, 'Registering Open Telemetry tracing')
173 })
174
175 it('Should upload a video and correctly works', async function () {
176 await setAccessTokensToServers([ server ])
177
178 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC })
179
180 const video = await server.videos.get({ id: uuid })
181
182 expect(video.name).to.equal('video')
183 })
184
185 after(async function () {
186 await mockHTTP.terminate()
187 })
188 })
189
190 after(async function () {
191 await cleanupTests([ server ])
192 })
193})
diff --git a/packages/tests/src/api/server/plugins.ts b/packages/tests/src/api/server/plugins.ts
new file mode 100644
index 000000000..a78cea025
--- /dev/null
+++ b/packages/tests/src/api/server/plugins.ts
@@ -0,0 +1,410 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, remove } from 'fs-extra/esm'
5import { join } from 'path'
6import { wait } from '@peertube/peertube-core-utils'
7import { HttpStatusCode, PluginType } from '@peertube/peertube-models'
8import {
9 cleanupTests,
10 createSingleServer,
11 killallServers,
12 makeGetRequest,
13 PeerTubeServer,
14 PluginsCommand,
15 setAccessTokensToServers
16} from '@peertube/peertube-server-commands'
17import { SQLCommand } from '@tests/shared/sql-command.js'
18import { testHelloWorldRegisteredSettings } from '@tests/shared/plugins.js'
19
20describe('Test plugins', function () {
21 let server: PeerTubeServer
22 let sqlCommand: SQLCommand
23 let command: PluginsCommand
24
25 before(async function () {
26 this.timeout(30000)
27
28 const configOverride = {
29 plugins: {
30 index: { check_latest_versions_interval: '5 seconds' }
31 }
32 }
33 server = await createSingleServer(1, configOverride)
34 await setAccessTokensToServers([ server ])
35
36 command = server.plugins
37
38 sqlCommand = new SQLCommand(server)
39 })
40
41 it('Should list and search available plugins and themes', async function () {
42 this.timeout(30000)
43
44 {
45 const body = await command.listAvailable({
46 count: 1,
47 start: 0,
48 pluginType: PluginType.THEME,
49 search: 'background-red'
50 })
51
52 expect(body.total).to.be.at.least(1)
53 expect(body.data).to.have.lengthOf(1)
54 }
55
56 {
57 const body1 = await command.listAvailable({
58 count: 2,
59 start: 0,
60 sort: 'npmName'
61 })
62 expect(body1.total).to.be.at.least(2)
63
64 const data1 = body1.data
65 expect(data1).to.have.lengthOf(2)
66
67 const body2 = await command.listAvailable({
68 count: 2,
69 start: 0,
70 sort: '-npmName'
71 })
72 expect(body2.total).to.be.at.least(2)
73
74 const data2 = body2.data
75 expect(data2).to.have.lengthOf(2)
76
77 expect(data1[0].npmName).to.not.equal(data2[0].npmName)
78 }
79
80 {
81 const body = await command.listAvailable({
82 count: 10,
83 start: 0,
84 pluginType: PluginType.THEME,
85 search: 'background-red',
86 currentPeerTubeEngine: '1.0.0'
87 })
88
89 const p = body.data.find(p => p.npmName === 'peertube-theme-background-red')
90 expect(p).to.be.undefined
91 }
92 })
93
94 it('Should install a plugin and a theme', async function () {
95 this.timeout(30000)
96
97 await command.install({ npmName: 'peertube-plugin-hello-world' })
98 await command.install({ npmName: 'peertube-theme-background-red' })
99 })
100
101 it('Should have the plugin loaded in the configuration', async function () {
102 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
103 const theme = config.theme.registered.find(r => r.name === 'background-red')
104 expect(theme).to.not.be.undefined
105 expect(theme.npmName).to.equal('peertube-theme-background-red')
106
107 const plugin = config.plugin.registered.find(r => r.name === 'hello-world')
108 expect(plugin).to.not.be.undefined
109 expect(plugin.npmName).to.equal('peertube-plugin-hello-world')
110 }
111 })
112
113 it('Should update the default theme in the configuration', async function () {
114 await server.config.updateCustomSubConfig({
115 newConfig: {
116 theme: { default: 'background-red' }
117 }
118 })
119
120 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
121 expect(config.theme.default).to.equal('background-red')
122 }
123 })
124
125 it('Should update my default theme', async function () {
126 await server.users.updateMe({ theme: 'background-red' })
127
128 const user = await server.users.getMyInfo()
129 expect(user.theme).to.equal('background-red')
130 })
131
132 it('Should list plugins and themes', async function () {
133 {
134 const body = await command.list({
135 count: 1,
136 start: 0,
137 pluginType: PluginType.THEME
138 })
139 expect(body.total).to.be.at.least(1)
140
141 const data = body.data
142 expect(data).to.have.lengthOf(1)
143 expect(data[0].name).to.equal('background-red')
144 }
145
146 {
147 const { data } = await command.list({
148 count: 2,
149 start: 0,
150 sort: 'name'
151 })
152
153 expect(data[0].name).to.equal('background-red')
154 expect(data[1].name).to.equal('hello-world')
155 }
156
157 {
158 const body = await command.list({
159 count: 2,
160 start: 1,
161 sort: 'name'
162 })
163
164 expect(body.data[0].name).to.equal('hello-world')
165 }
166 })
167
168 it('Should get registered settings', async function () {
169 await testHelloWorldRegisteredSettings(server)
170 })
171
172 it('Should get public settings', async function () {
173 const body = await command.getPublicSettings({ npmName: 'peertube-plugin-hello-world' })
174 const publicSettings = body.publicSettings
175
176 expect(Object.keys(publicSettings)).to.have.lengthOf(1)
177 expect(Object.keys(publicSettings)).to.deep.equal([ 'user-name' ])
178 expect(publicSettings['user-name']).to.be.null
179 })
180
181 it('Should update the settings', async function () {
182 const settings = {
183 'admin-name': 'Cid'
184 }
185
186 await command.updateSettings({
187 npmName: 'peertube-plugin-hello-world',
188 settings
189 })
190 })
191
192 it('Should have watched settings changes', async function () {
193 await server.servers.waitUntilLog('Settings changed!')
194 })
195
196 it('Should get a plugin and a theme', async function () {
197 {
198 const plugin = await command.get({ npmName: 'peertube-plugin-hello-world' })
199
200 expect(plugin.type).to.equal(PluginType.PLUGIN)
201 expect(plugin.name).to.equal('hello-world')
202 expect(plugin.description).to.exist
203 expect(plugin.homepage).to.exist
204 expect(plugin.uninstalled).to.be.false
205 expect(plugin.enabled).to.be.true
206 expect(plugin.description).to.exist
207 expect(plugin.version).to.exist
208 expect(plugin.peertubeEngine).to.exist
209 expect(plugin.createdAt).to.exist
210
211 expect(plugin.settings).to.not.be.undefined
212 expect(plugin.settings['admin-name']).to.equal('Cid')
213 }
214
215 {
216 const plugin = await command.get({ npmName: 'peertube-theme-background-red' })
217
218 expect(plugin.type).to.equal(PluginType.THEME)
219 expect(plugin.name).to.equal('background-red')
220 expect(plugin.description).to.exist
221 expect(plugin.homepage).to.exist
222 expect(plugin.uninstalled).to.be.false
223 expect(plugin.enabled).to.be.true
224 expect(plugin.description).to.exist
225 expect(plugin.version).to.exist
226 expect(plugin.peertubeEngine).to.exist
227 expect(plugin.createdAt).to.exist
228
229 expect(plugin.settings).to.be.null
230 }
231 })
232
233 it('Should update the plugin and the theme', async function () {
234 this.timeout(180000)
235
236 // Wait the scheduler that get the latest plugins versions
237 await wait(6000)
238
239 async function testUpdate (type: 'plugin' | 'theme', name: string) {
240 // Fake update our plugin version
241 await sqlCommand.setPluginVersion(name, '0.0.1')
242
243 // Fake update package.json
244 const packageJSON = await command.getPackageJSON(`peertube-${type}-${name}`)
245 const oldVersion = packageJSON.version
246
247 packageJSON.version = '0.0.1'
248 await command.updatePackageJSON(`peertube-${type}-${name}`, packageJSON)
249
250 // Restart the server to take into account this change
251 await killallServers([ server ])
252 await server.run()
253
254 const checkConfig = async (version: string) => {
255 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
256 expect(config[type].registered.find(r => r.name === name).version).to.equal(version)
257 }
258 }
259
260 const getPluginFromAPI = async () => {
261 const body = await command.list({ pluginType: type === 'plugin' ? PluginType.PLUGIN : PluginType.THEME })
262
263 return body.data.find(p => p.name === name)
264 }
265
266 {
267 const plugin = await getPluginFromAPI()
268 expect(plugin.version).to.equal('0.0.1')
269 expect(plugin.latestVersion).to.exist
270 expect(plugin.latestVersion).to.not.equal('0.0.1')
271
272 await checkConfig('0.0.1')
273 }
274
275 {
276 await command.update({ npmName: `peertube-${type}-${name}` })
277
278 const plugin = await getPluginFromAPI()
279 expect(plugin.version).to.equal(oldVersion)
280
281 const updatedPackageJSON = await command.getPackageJSON(`peertube-${type}-${name}`)
282 expect(updatedPackageJSON.version).to.equal(oldVersion)
283
284 await checkConfig(oldVersion)
285 }
286 }
287
288 await testUpdate('theme', 'background-red')
289 await testUpdate('plugin', 'hello-world')
290 })
291
292 it('Should uninstall the plugin', async function () {
293 await command.uninstall({ npmName: 'peertube-plugin-hello-world' })
294
295 const body = await command.list({ pluginType: PluginType.PLUGIN })
296 expect(body.total).to.equal(0)
297 expect(body.data).to.have.lengthOf(0)
298 })
299
300 it('Should list uninstalled plugins', async function () {
301 const body = await command.list({ pluginType: PluginType.PLUGIN, uninstalled: true })
302 expect(body.total).to.equal(1)
303 expect(body.data).to.have.lengthOf(1)
304
305 const plugin = body.data[0]
306 expect(plugin.name).to.equal('hello-world')
307 expect(plugin.enabled).to.be.false
308 expect(plugin.uninstalled).to.be.true
309 })
310
311 it('Should uninstall the theme', async function () {
312 await command.uninstall({ npmName: 'peertube-theme-background-red' })
313 })
314
315 it('Should have updated the configuration', async function () {
316 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
317 expect(config.theme.default).to.equal('default')
318
319 const theme = config.theme.registered.find(r => r.name === 'background-red')
320 expect(theme).to.be.undefined
321
322 const plugin = config.plugin.registered.find(r => r.name === 'hello-world')
323 expect(plugin).to.be.undefined
324 }
325 })
326
327 it('Should have updated the user theme', async function () {
328 const user = await server.users.getMyInfo()
329 expect(user.theme).to.equal('instance-default')
330 })
331
332 it('Should not install a broken plugin', async function () {
333 this.timeout(60000)
334
335 async function check () {
336 const body = await command.list({ pluginType: PluginType.PLUGIN })
337 const plugins = body.data
338 expect(plugins.find(p => p.name === 'test-broken')).to.not.exist
339 }
340
341 await command.install({
342 path: PluginsCommand.getPluginTestPath('-broken'),
343 expectedStatus: HttpStatusCode.BAD_REQUEST_400
344 })
345
346 await check()
347
348 await killallServers([ server ])
349 await server.run()
350
351 await check()
352 })
353
354 it('Should rebuild native modules on Node ABI change', async function () {
355 this.timeout(60000)
356
357 const removeNativeModule = async () => {
358 await remove(join(baseNativeModule, 'build'))
359 await remove(join(baseNativeModule, 'prebuilds'))
360 }
361
362 await command.install({ path: PluginsCommand.getPluginTestPath('-native') })
363
364 await makeGetRequest({
365 url: server.url,
366 path: '/plugins/test-native/router',
367 expectedStatus: HttpStatusCode.NO_CONTENT_204
368 })
369
370 const query = `UPDATE "application" SET "nodeABIVersion" = 1`
371 await sqlCommand.updateQuery(query)
372
373 const baseNativeModule = server.servers.buildDirectory(join('plugins', 'node_modules', 'a-native-example'))
374
375 await removeNativeModule()
376 await server.kill()
377 await server.run()
378
379 await wait(3000)
380
381 expect(await pathExists(join(baseNativeModule, 'build'))).to.be.true
382 expect(await pathExists(join(baseNativeModule, 'prebuilds'))).to.be.true
383
384 await makeGetRequest({
385 url: server.url,
386 path: '/plugins/test-native/router',
387 expectedStatus: HttpStatusCode.NO_CONTENT_204
388 })
389
390 await removeNativeModule()
391
392 await server.kill()
393 await server.run()
394
395 expect(await pathExists(join(baseNativeModule, 'build'))).to.be.false
396 expect(await pathExists(join(baseNativeModule, 'prebuilds'))).to.be.false
397
398 await makeGetRequest({
399 url: server.url,
400 path: '/plugins/test-native/router',
401 expectedStatus: HttpStatusCode.NOT_FOUND_404
402 })
403 })
404
405 after(async function () {
406 await sqlCommand.cleanup()
407
408 await cleanupTests([ server ])
409 })
410})
diff --git a/packages/tests/src/api/server/proxy.ts b/packages/tests/src/api/server/proxy.ts
new file mode 100644
index 000000000..c7d13f4ab
--- /dev/null
+++ b/packages/tests/src/api/server/proxy.ts
@@ -0,0 +1,173 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models'
5import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 ObjectStorageCommand,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16import { FIXTURE_URLS } from '@tests/shared/tests.js'
17import { expectStartWith, expectNotStartWith } from '@tests/shared/checks.js'
18import { MockProxy } from '@tests/shared/mock-servers/mock-proxy.js'
19
20describe('Test proxy', function () {
21 let servers: PeerTubeServer[] = []
22 let proxy: MockProxy
23
24 const goodEnv = { HTTP_PROXY: '' }
25 const badEnv = { HTTP_PROXY: 'http://127.0.0.1:9000' }
26
27 before(async function () {
28 this.timeout(120000)
29
30 proxy = new MockProxy()
31
32 const proxyPort = await proxy.initialize()
33 servers = await createMultipleServers(2)
34
35 goodEnv.HTTP_PROXY = 'http://127.0.0.1:' + proxyPort
36
37 await setAccessTokensToServers(servers)
38 await setDefaultVideoChannel(servers)
39 await doubleFollow(servers[0], servers[1])
40 })
41
42 describe('Federation', function () {
43
44 it('Should succeed federation with the appropriate proxy config', async function () {
45 this.timeout(40000)
46
47 await servers[0].kill()
48 await servers[0].run({}, { env: goodEnv })
49
50 await servers[0].videos.quickUpload({ name: 'video 1' })
51
52 await waitJobs(servers)
53
54 for (const server of servers) {
55 const { total, data } = await server.videos.list()
56 expect(total).to.equal(1)
57 expect(data).to.have.lengthOf(1)
58 }
59 })
60
61 it('Should fail federation with a wrong proxy config', async function () {
62 this.timeout(40000)
63
64 await servers[0].kill()
65 await servers[0].run({}, { env: badEnv })
66
67 await servers[0].videos.quickUpload({ name: 'video 2' })
68
69 await waitJobs(servers)
70
71 {
72 const { total, data } = await servers[0].videos.list()
73 expect(total).to.equal(2)
74 expect(data).to.have.lengthOf(2)
75 }
76
77 {
78 const { total, data } = await servers[1].videos.list()
79 expect(total).to.equal(1)
80 expect(data).to.have.lengthOf(1)
81 }
82 })
83 })
84
85 describe('Videos import', async function () {
86
87 function quickImport (expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) {
88 return servers[0].imports.importVideo({
89 attributes: {
90 name: 'video import',
91 channelId: servers[0].store.channel.id,
92 privacy: VideoPrivacy.PUBLIC,
93 targetUrl: FIXTURE_URLS.peertube_long
94 },
95 expectedStatus
96 })
97 }
98
99 it('Should succeed import with the appropriate proxy config', async function () {
100 this.timeout(240000)
101
102 await servers[0].kill()
103 await servers[0].run({}, { env: goodEnv })
104
105 await quickImport()
106
107 await waitJobs(servers)
108
109 const { total, data } = await servers[0].videos.list()
110 expect(total).to.equal(3)
111 expect(data).to.have.lengthOf(3)
112 })
113
114 it('Should fail import with a wrong proxy config', async function () {
115 this.timeout(120000)
116
117 await servers[0].kill()
118 await servers[0].run({}, { env: badEnv })
119
120 await quickImport(HttpStatusCode.BAD_REQUEST_400)
121 })
122 })
123
124 describe('Object storage', function () {
125 if (areMockObjectStorageTestsDisabled()) return
126
127 const objectStorage = new ObjectStorageCommand()
128
129 before(async function () {
130 this.timeout(30000)
131
132 await objectStorage.prepareDefaultMockBuckets()
133 })
134
135 it('Should succeed to upload to object storage with the appropriate proxy config', async function () {
136 this.timeout(120000)
137
138 await servers[0].kill()
139 await servers[0].run(objectStorage.getDefaultMockConfig(), { env: goodEnv })
140
141 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
142 await waitJobs(servers)
143
144 const video = await servers[0].videos.get({ id: uuid })
145
146 expectStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
147 })
148
149 it('Should fail to upload to object storage with a wrong proxy config', async function () {
150 this.timeout(120000)
151
152 await servers[0].kill()
153 await servers[0].run(objectStorage.getDefaultMockConfig(), { env: badEnv })
154
155 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
156 await waitJobs(servers, { skipDelayed: true })
157
158 const video = await servers[0].videos.get({ id: uuid })
159
160 expectNotStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
161 })
162
163 after(async function () {
164 await objectStorage.cleanupMock()
165 })
166 })
167
168 after(async function () {
169 await proxy.terminate()
170
171 await cleanupTests(servers)
172 })
173})
diff --git a/packages/tests/src/api/server/reverse-proxy.ts b/packages/tests/src/api/server/reverse-proxy.ts
new file mode 100644
index 000000000..7e334cc3e
--- /dev/null
+++ b/packages/tests/src/api/server/reverse-proxy.ts
@@ -0,0 +1,156 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode } from '@peertube/peertube-models'
6import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
7
8describe('Test application behind a reverse proxy', function () {
9 let server: PeerTubeServer
10 let userAccessToken: string
11 let videoId: string
12
13 before(async function () {
14 this.timeout(60000)
15
16 const config = {
17 rates_limit: {
18 api: {
19 max: 50,
20 window: 5000
21 },
22 signup: {
23 max: 3,
24 window: 5000
25 },
26 login: {
27 max: 20
28 }
29 },
30 signup: {
31 limit: 20
32 }
33 }
34
35 server = await createSingleServer(1, config)
36 await setAccessTokensToServers([ server ])
37
38 userAccessToken = await server.users.generateUserAndToken('user')
39
40 const { uuid } = await server.videos.upload()
41 videoId = uuid
42 })
43
44 it('Should view a video only once with the same IP by default', async function () {
45 this.timeout(40000)
46
47 await server.views.simulateView({ id: videoId })
48 await server.views.simulateView({ id: videoId })
49
50 // Wait the repeatable job
51 await wait(8000)
52
53 const video = await server.videos.get({ id: videoId })
54 expect(video.views).to.equal(1)
55 })
56
57 it('Should view a video 2 times with the X-Forwarded-For header set', async function () {
58 this.timeout(20000)
59
60 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.1,127.0.0.1' })
61 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.2,127.0.0.1' })
62
63 // Wait the repeatable job
64 await wait(8000)
65
66 const video = await server.videos.get({ id: videoId })
67 expect(video.views).to.equal(3)
68 })
69
70 it('Should view a video only once with the same client IP in the X-Forwarded-For header', async function () {
71 this.timeout(20000)
72
73 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.4,0.0.0.3,::ffff:127.0.0.1' })
74 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.5,0.0.0.3,127.0.0.1' })
75
76 // Wait the repeatable job
77 await wait(8000)
78
79 const video = await server.videos.get({ id: videoId })
80 expect(video.views).to.equal(4)
81 })
82
83 it('Should view a video two times with a different client IP in the X-Forwarded-For header', async function () {
84 this.timeout(20000)
85
86 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.6,127.0.0.1' })
87 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.7,127.0.0.1' })
88
89 // Wait the repeatable job
90 await wait(8000)
91
92 const video = await server.videos.get({ id: videoId })
93 expect(video.views).to.equal(6)
94 })
95
96 it('Should rate limit logins', async function () {
97 const user = { username: 'root', password: 'fail' }
98
99 for (let i = 0; i < 18; i++) {
100 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
101 }
102
103 await server.login.login({ user, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
104 })
105
106 it('Should rate limit signup', async function () {
107 for (let i = 0; i < 10; i++) {
108 try {
109 await server.registrations.register({ username: 'test' + i })
110 } catch {
111 // empty
112 }
113 }
114
115 await server.registrations.register({ username: 'test42', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
116 })
117
118 it('Should not rate limit failed signup', async function () {
119 this.timeout(30000)
120
121 await wait(7000)
122
123 for (let i = 0; i < 3; i++) {
124 await server.registrations.register({ username: 'test' + i, expectedStatus: HttpStatusCode.CONFLICT_409 })
125 }
126
127 await server.registrations.register({ username: 'test43', expectedStatus: HttpStatusCode.NO_CONTENT_204 })
128
129 })
130
131 it('Should rate limit API calls', async function () {
132 this.timeout(30000)
133
134 await wait(7000)
135
136 for (let i = 0; i < 100; i++) {
137 try {
138 await server.videos.get({ id: videoId })
139 } catch {
140 // don't care if it fails
141 }
142 }
143
144 await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
145 })
146
147 it('Should rate limit API calls with a user but not with an admin', async function () {
148 await server.videos.get({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
149
150 await server.videos.get({ id: videoId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
151 })
152
153 after(async function () {
154 await cleanupTests([ server ])
155 })
156})
diff --git a/packages/tests/src/api/server/services.ts b/packages/tests/src/api/server/services.ts
new file mode 100644
index 000000000..349d29a58
--- /dev/null
+++ b/packages/tests/src/api/server/services.ts
@@ -0,0 +1,143 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { Video, VideoPlaylistPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@peertube/peertube-server-commands'
12
13describe('Test services', function () {
14 let server: PeerTubeServer = null
15 let playlistUUID: string
16 let playlistDisplayName: string
17 let video: Video
18
19 const urlSuffixes = [
20 {
21 input: '',
22 output: ''
23 },
24 {
25 input: '?param=1',
26 output: ''
27 },
28 {
29 input: '?muted=1&warningTitle=0&toto=1',
30 output: '?muted=1&warningTitle=0'
31 }
32 ]
33
34 before(async function () {
35 this.timeout(120000)
36
37 server = await createSingleServer(1)
38
39 await setAccessTokensToServers([ server ])
40 await setDefaultVideoChannel([ server ])
41
42 {
43 const attributes = { name: 'my super name' }
44 await server.videos.upload({ attributes })
45
46 const { data } = await server.videos.list()
47 video = data[0]
48 }
49
50 {
51 const created = await server.playlists.create({
52 attributes: {
53 displayName: 'The Life and Times of Scrooge McDuck',
54 privacy: VideoPlaylistPrivacy.PUBLIC,
55 videoChannelId: server.store.channel.id
56 }
57 })
58
59 playlistUUID = created.uuid
60 playlistDisplayName = 'The Life and Times of Scrooge McDuck'
61
62 await server.playlists.addElement({
63 playlistId: created.id,
64 attributes: {
65 videoId: video.id
66 }
67 })
68 }
69 })
70
71 it('Should have a valid oEmbed video response', async function () {
72 for (const basePath of [ '/videos/watch/', '/w/' ]) {
73 for (const suffix of urlSuffixes) {
74 const oembedUrl = server.url + basePath + video.uuid + suffix.input
75
76 const res = await server.services.getOEmbed({ oembedUrl })
77 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
78 `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}${suffix.output}" ` +
79 'frameborder="0" allowfullscreen></iframe>'
80
81 const expectedThumbnailUrl = 'http://' + server.host + video.previewPath
82
83 expect(res.body.html).to.equal(expectedHtml)
84 expect(res.body.title).to.equal(video.name)
85 expect(res.body.author_name).to.equal(server.store.channel.displayName)
86 expect(res.body.width).to.equal(560)
87 expect(res.body.height).to.equal(315)
88 expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
89 expect(res.body.thumbnail_width).to.equal(850)
90 expect(res.body.thumbnail_height).to.equal(480)
91 }
92 }
93 })
94
95 it('Should have a valid playlist oEmbed response', async function () {
96 for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) {
97 for (const suffix of urlSuffixes) {
98 const oembedUrl = server.url + basePath + playlistUUID + suffix.input
99
100 const res = await server.services.getOEmbed({ oembedUrl })
101 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
102 `title="${playlistDisplayName}" src="http://${server.host}/video-playlists/embed/${playlistUUID}${suffix.output}" ` +
103 'frameborder="0" allowfullscreen></iframe>'
104
105 expect(res.body.html).to.equal(expectedHtml)
106 expect(res.body.title).to.equal('The Life and Times of Scrooge McDuck')
107 expect(res.body.author_name).to.equal(server.store.channel.displayName)
108 expect(res.body.width).to.equal(560)
109 expect(res.body.height).to.equal(315)
110 expect(res.body.thumbnail_url).exist
111 expect(res.body.thumbnail_width).to.equal(280)
112 expect(res.body.thumbnail_height).to.equal(157)
113 }
114 }
115 })
116
117 it('Should have a valid oEmbed response with small max height query', async function () {
118 for (const basePath of [ '/videos/watch/', '/w/' ]) {
119 const oembedUrl = 'http://' + server.host + basePath + video.uuid
120 const format = 'json'
121 const maxHeight = 50
122 const maxWidth = 50
123
124 const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth })
125 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' +
126 `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}" ` +
127 'frameborder="0" allowfullscreen></iframe>'
128
129 expect(res.body.html).to.equal(expectedHtml)
130 expect(res.body.title).to.equal(video.name)
131 expect(res.body.author_name).to.equal(server.store.channel.displayName)
132 expect(res.body.height).to.equal(50)
133 expect(res.body.width).to.equal(50)
134 expect(res.body).to.not.have.property('thumbnail_url')
135 expect(res.body).to.not.have.property('thumbnail_width')
136 expect(res.body).to.not.have.property('thumbnail_height')
137 }
138 })
139
140 after(async function () {
141 await cleanupTests([ server ])
142 })
143})
diff --git a/packages/tests/src/api/server/slow-follows.ts b/packages/tests/src/api/server/slow-follows.ts
new file mode 100644
index 000000000..d03109001
--- /dev/null
+++ b/packages/tests/src/api/server/slow-follows.ts
@@ -0,0 +1,85 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { Job } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test slow follows', function () {
15 let servers: PeerTubeServer[] = []
16
17 let afterFollows: Date
18
19 before(async function () {
20 this.timeout(120000)
21
22 servers = await createMultipleServers(3)
23
24 // Get the access tokens
25 await setAccessTokensToServers(servers)
26
27 await doubleFollow(servers[0], servers[1])
28 await doubleFollow(servers[0], servers[2])
29
30 afterFollows = new Date()
31
32 for (let i = 0; i < 5; i++) {
33 await servers[0].videos.quickUpload({ name: 'video ' + i })
34 }
35
36 await waitJobs(servers)
37 })
38
39 it('Should only have broadcast jobs', async function () {
40 const { data } = await servers[0].jobs.list({ jobType: 'activitypub-http-unicast', sort: '-createdAt' })
41
42 for (const job of data) {
43 expect(new Date(job.createdAt)).below(afterFollows)
44 }
45 })
46
47 it('Should process bad follower', async function () {
48 this.timeout(30000)
49
50 await servers[1].kill()
51
52 // Set server 2 as bad follower
53 await servers[0].videos.quickUpload({ name: 'video 6' })
54 await waitJobs(servers[0])
55
56 afterFollows = new Date()
57 const filter = (job: Job) => new Date(job.createdAt) > afterFollows
58
59 // Resend another broadcast job
60 await servers[0].videos.quickUpload({ name: 'video 7' })
61 await waitJobs(servers[0])
62
63 const resBroadcast = await servers[0].jobs.list({ jobType: 'activitypub-http-broadcast', sort: '-createdAt' })
64 const resUnicast = await servers[0].jobs.list({ jobType: 'activitypub-http-unicast', sort: '-createdAt' })
65
66 const broadcast = resBroadcast.data.filter(filter)
67 const unicast = resUnicast.data.filter(filter)
68
69 expect(unicast).to.have.lengthOf(2)
70 expect(broadcast).to.have.lengthOf(2)
71
72 for (const u of unicast) {
73 expect(u.data.uri).to.equal(servers[1].url + '/inbox')
74 }
75
76 for (const b of broadcast) {
77 expect(b.data.uris).to.have.lengthOf(1)
78 expect(b.data.uris[0]).to.equal(servers[2].url + '/inbox')
79 }
80 })
81
82 after(async function () {
83 await cleanupTests(servers)
84 })
85})
diff --git a/packages/tests/src/api/server/stats.ts b/packages/tests/src/api/server/stats.ts
new file mode 100644
index 000000000..32ab323ce
--- /dev/null
+++ b/packages/tests/src/api/server/stats.ts
@@ -0,0 +1,279 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { ActivityType, VideoPlaylistPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test stats (excluding redundancy)', function () {
18 let servers: PeerTubeServer[] = []
19 let channelId
20 const user = {
21 username: 'user1',
22 password: 'super_password'
23 }
24
25 before(async function () {
26 this.timeout(120000)
27
28 servers = await createMultipleServers(3)
29
30 await setAccessTokensToServers(servers)
31 await setDefaultChannelAvatar(servers)
32 await setDefaultAccountAvatar(servers)
33
34 await doubleFollow(servers[0], servers[1])
35
36 await servers[0].users.create({ username: user.username, password: user.password })
37
38 const { uuid } = await servers[0].videos.upload({ attributes: { fixture: 'video_short.webm' } })
39
40 await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
41
42 await servers[0].views.simulateView({ id: uuid })
43
44 // Wait the video views repeatable job
45 await wait(8000)
46
47 await servers[2].follows.follow({ hosts: [ servers[0].url ] })
48 await waitJobs(servers)
49 })
50
51 it('Should have the correct stats on instance 1', async function () {
52 const data = await servers[0].stats.get()
53
54 expect(data.totalLocalVideoComments).to.equal(1)
55 expect(data.totalLocalVideos).to.equal(1)
56 expect(data.totalLocalVideoViews).to.equal(1)
57 expect(data.totalLocalVideoFilesSize).to.equal(218910)
58 expect(data.totalUsers).to.equal(2)
59 expect(data.totalVideoComments).to.equal(1)
60 expect(data.totalVideos).to.equal(1)
61 expect(data.totalInstanceFollowers).to.equal(2)
62 expect(data.totalInstanceFollowing).to.equal(1)
63 expect(data.totalLocalPlaylists).to.equal(0)
64 })
65
66 it('Should have the correct stats on instance 2', async function () {
67 const data = await servers[1].stats.get()
68
69 expect(data.totalLocalVideoComments).to.equal(0)
70 expect(data.totalLocalVideos).to.equal(0)
71 expect(data.totalLocalVideoViews).to.equal(0)
72 expect(data.totalLocalVideoFilesSize).to.equal(0)
73 expect(data.totalUsers).to.equal(1)
74 expect(data.totalVideoComments).to.equal(1)
75 expect(data.totalVideos).to.equal(1)
76 expect(data.totalInstanceFollowers).to.equal(1)
77 expect(data.totalInstanceFollowing).to.equal(1)
78 expect(data.totalLocalPlaylists).to.equal(0)
79 })
80
81 it('Should have the correct stats on instance 3', async function () {
82 const data = await servers[2].stats.get()
83
84 expect(data.totalLocalVideoComments).to.equal(0)
85 expect(data.totalLocalVideos).to.equal(0)
86 expect(data.totalLocalVideoViews).to.equal(0)
87 expect(data.totalUsers).to.equal(1)
88 expect(data.totalVideoComments).to.equal(1)
89 expect(data.totalVideos).to.equal(1)
90 expect(data.totalInstanceFollowing).to.equal(1)
91 expect(data.totalInstanceFollowers).to.equal(0)
92 expect(data.totalLocalPlaylists).to.equal(0)
93 })
94
95 it('Should have the correct total videos stats after an unfollow', async function () {
96 this.timeout(15000)
97
98 await servers[2].follows.unfollow({ target: servers[0] })
99 await waitJobs(servers)
100
101 const data = await servers[2].stats.get()
102
103 expect(data.totalVideos).to.equal(0)
104 })
105
106 it('Should have the correct active user stats', async function () {
107 const server = servers[0]
108
109 {
110 const data = await server.stats.get()
111
112 expect(data.totalDailyActiveUsers).to.equal(1)
113 expect(data.totalWeeklyActiveUsers).to.equal(1)
114 expect(data.totalMonthlyActiveUsers).to.equal(1)
115 }
116
117 {
118 await server.login.getAccessToken(user)
119
120 const data = await server.stats.get()
121
122 expect(data.totalDailyActiveUsers).to.equal(2)
123 expect(data.totalWeeklyActiveUsers).to.equal(2)
124 expect(data.totalMonthlyActiveUsers).to.equal(2)
125 }
126 })
127
128 it('Should have the correct active channel stats', async function () {
129 const server = servers[0]
130
131 {
132 const data = await server.stats.get()
133
134 expect(data.totalLocalVideoChannels).to.equal(2)
135 expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
136 expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
137 expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
138 }
139
140 {
141 const attributes = {
142 name: 'stats_channel',
143 displayName: 'My stats channel'
144 }
145 const created = await server.channels.create({ attributes })
146 channelId = created.id
147
148 const data = await server.stats.get()
149
150 expect(data.totalLocalVideoChannels).to.equal(3)
151 expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
152 expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
153 expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
154 }
155
156 {
157 await server.videos.upload({ attributes: { fixture: 'video_short.webm', channelId } })
158
159 const data = await server.stats.get()
160
161 expect(data.totalLocalVideoChannels).to.equal(3)
162 expect(data.totalLocalDailyActiveVideoChannels).to.equal(2)
163 expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2)
164 expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2)
165 }
166 })
167
168 it('Should have the correct playlist stats', async function () {
169 const server = servers[0]
170
171 {
172 const data = await server.stats.get()
173 expect(data.totalLocalPlaylists).to.equal(0)
174 }
175
176 {
177 await server.playlists.create({
178 attributes: {
179 displayName: 'playlist for count',
180 privacy: VideoPlaylistPrivacy.PUBLIC,
181 videoChannelId: channelId
182 }
183 })
184
185 const data = await server.stats.get()
186 expect(data.totalLocalPlaylists).to.equal(1)
187 }
188 })
189
190 it('Should correctly count video file sizes if transcoding is enabled', async function () {
191 this.timeout(120000)
192
193 await servers[0].config.updateCustomSubConfig({
194 newConfig: {
195 transcoding: {
196 enabled: true,
197 webVideos: {
198 enabled: true
199 },
200 hls: {
201 enabled: true
202 },
203 resolutions: {
204 '0p': false,
205 '144p': false,
206 '240p': false,
207 '360p': false,
208 '480p': false,
209 '720p': false,
210 '1080p': false,
211 '1440p': false,
212 '2160p': false
213 }
214 }
215 }
216 })
217
218 await servers[0].videos.upload({ attributes: { name: 'video', fixture: 'video_short.webm' } })
219
220 await waitJobs(servers)
221
222 {
223 const data = await servers[1].stats.get()
224 expect(data.totalLocalVideoFilesSize).to.equal(0)
225 }
226
227 {
228 const data = await servers[0].stats.get()
229 expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000)
230 expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000)
231 }
232 })
233
234 it('Should have the correct AP stats', async function () {
235 this.timeout(120000)
236
237 await servers[0].config.disableTranscoding()
238
239 const first = await servers[1].stats.get()
240
241 for (let i = 0; i < 10; i++) {
242 await servers[0].videos.upload({ attributes: { name: 'video' } })
243 }
244
245 await waitJobs(servers)
246
247 await wait(6000)
248
249 const second = await servers[1].stats.get()
250 expect(second.totalActivityPubMessagesProcessed).to.be.greaterThan(first.totalActivityPubMessagesProcessed)
251
252 const apTypes: ActivityType[] = [
253 'Create', 'Update', 'Delete', 'Follow', 'Accept', 'Announce', 'Undo', 'Like', 'Reject', 'View', 'Dislike', 'Flag'
254 ]
255
256 const processed = apTypes.reduce(
257 (previous, type) => previous + second['totalActivityPub' + type + 'MessagesSuccesses'],
258 0
259 )
260 expect(second.totalActivityPubMessagesProcessed).to.equal(processed)
261 expect(second.totalActivityPubMessagesSuccesses).to.equal(processed)
262
263 expect(second.totalActivityPubMessagesErrors).to.equal(0)
264
265 for (const apType of apTypes) {
266 expect(second['totalActivityPub' + apType + 'MessagesErrors']).to.equal(0)
267 }
268
269 await wait(6000)
270
271 const third = await servers[1].stats.get()
272 expect(third.totalActivityPubMessagesWaiting).to.equal(0)
273 expect(third.activityPubMessagesProcessedPerSecond).to.be.lessThan(second.activityPubMessagesProcessedPerSecond)
274 })
275
276 after(async function () {
277 await cleanupTests(servers)
278 })
279})
diff --git a/packages/tests/src/api/server/tracker.ts b/packages/tests/src/api/server/tracker.ts
new file mode 100644
index 000000000..4df4e4613
--- /dev/null
+++ b/packages/tests/src/api/server/tracker.ts
@@ -0,0 +1,110 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */
2
3import { decode as magnetUriDecode, encode as magnetUriEncode } from 'magnet-uri'
4import WebTorrent from 'webtorrent'
5import {
6 cleanupTests,
7 createSingleServer,
8 killallServers,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12
13describe('Test tracker', function () {
14 let server: PeerTubeServer
15 let badMagnet: string
16 let goodMagnet: string
17
18 before(async function () {
19 this.timeout(60000)
20 server = await createSingleServer(1)
21 await setAccessTokensToServers([ server ])
22
23 {
24 const { uuid } = await server.videos.upload()
25 const video = await server.videos.get({ id: uuid })
26 goodMagnet = video.files[0].magnetUri
27
28 const parsed = magnetUriDecode(goodMagnet)
29 parsed.infoHash = '010597bb88b1968a5693a4fa8267c592ca65f2e9'
30
31 badMagnet = magnetUriEncode(parsed)
32 }
33 })
34
35 it('Should succeed with the correct infohash', function (done) {
36 const webtorrent = new WebTorrent()
37
38 const torrent = webtorrent.add(goodMagnet)
39
40 torrent.on('error', done)
41 torrent.on('warning', warn => {
42 const message = typeof warn === 'string' ? warn : warn.message
43 if (message.includes('Unknown infoHash ')) return done(new Error('Error on infohash'))
44 })
45
46 torrent.on('done', done)
47 })
48
49 it('Should disable the tracker', function (done) {
50 this.timeout(20000)
51
52 const errCb = () => done(new Error('Tracker is enabled'))
53
54 killallServers([ server ])
55 .then(() => server.run({ tracker: { enabled: false } }))
56 .then(() => {
57 const webtorrent = new WebTorrent()
58
59 const torrent = webtorrent.add(goodMagnet)
60
61 torrent.on('error', done)
62 torrent.on('warning', warn => {
63 const message = typeof warn === 'string' ? warn : warn.message
64 if (message.includes('disabled ')) {
65 torrent.off('done', errCb)
66
67 return done()
68 }
69 })
70
71 torrent.on('done', errCb)
72 })
73 })
74
75 it('Should return an error when adding an incorrect infohash', function (done) {
76 this.timeout(20000)
77
78 killallServers([ server ])
79 .then(() => server.run())
80 .then(() => {
81 const webtorrent = new WebTorrent()
82
83 const torrent = webtorrent.add(badMagnet)
84
85 torrent.on('error', done)
86 torrent.on('warning', warn => {
87 const message = typeof warn === 'string' ? warn : warn.message
88 if (message.includes('Unknown infoHash ')) return done()
89 })
90
91 torrent.on('done', () => done(new Error('No error on infohash')))
92 })
93 })
94
95 it('Should block the IP after the failed infohash', function (done) {
96 const webtorrent = new WebTorrent()
97
98 const torrent = webtorrent.add(goodMagnet)
99
100 torrent.on('error', done)
101 torrent.on('warning', warn => {
102 const message = typeof warn === 'string' ? warn : warn.message
103 if (message.includes('Unsupported tracker protocol')) return done()
104 })
105 })
106
107 after(async function () {
108 await cleanupTests([ server ])
109 })
110})
diff --git a/packages/tests/src/api/transcoding/audio-only.ts b/packages/tests/src/api/transcoding/audio-only.ts
new file mode 100644
index 000000000..6d0410348
--- /dev/null
+++ b/packages/tests/src/api/transcoding/audio-only.ts
@@ -0,0 +1,104 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { getAudioStream, getVideoStreamDimensionsInfo } from '@peertube/peertube-ffmpeg'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test audio only video transcoding', function () {
15 let servers: PeerTubeServer[] = []
16 let videoUUID: string
17 let webVideoAudioFileUrl: string
18 let fragmentedAudioFileUrl: string
19
20 before(async function () {
21 this.timeout(120000)
22
23 const configOverride = {
24 transcoding: {
25 enabled: true,
26 resolutions: {
27 '0p': true,
28 '144p': false,
29 '240p': true,
30 '360p': false,
31 '480p': false,
32 '720p': false,
33 '1080p': false,
34 '1440p': false,
35 '2160p': false
36 },
37 hls: {
38 enabled: true
39 },
40 web_videos: {
41 enabled: true
42 }
43 }
44 }
45 servers = await createMultipleServers(2, configOverride)
46
47 // Get the access tokens
48 await setAccessTokensToServers(servers)
49
50 // Server 1 and server 2 follow each other
51 await doubleFollow(servers[0], servers[1])
52 })
53
54 it('Should upload a video and transcode it', async function () {
55 this.timeout(120000)
56
57 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'audio only' } })
58 videoUUID = uuid
59
60 await waitJobs(servers)
61
62 for (const server of servers) {
63 const video = await server.videos.get({ id: videoUUID })
64 expect(video.streamingPlaylists).to.have.lengthOf(1)
65
66 for (const files of [ video.files, video.streamingPlaylists[0].files ]) {
67 expect(files).to.have.lengthOf(3)
68 expect(files[0].resolution.id).to.equal(720)
69 expect(files[1].resolution.id).to.equal(240)
70 expect(files[2].resolution.id).to.equal(0)
71 }
72
73 if (server.serverNumber === 1) {
74 webVideoAudioFileUrl = video.files[2].fileUrl
75 fragmentedAudioFileUrl = video.streamingPlaylists[0].files[2].fileUrl
76 }
77 }
78 })
79
80 it('0p transcoded video should not have video', async function () {
81 const paths = [
82 servers[0].servers.buildWebVideoFilePath(webVideoAudioFileUrl),
83 servers[0].servers.buildFragmentedFilePath(videoUUID, fragmentedAudioFileUrl)
84 ]
85
86 for (const path of paths) {
87 const { audioStream } = await getAudioStream(path)
88 expect(audioStream['codec_name']).to.be.equal('aac')
89 expect(audioStream['bit_rate']).to.be.at.most(384 * 8000)
90
91 const size = await getVideoStreamDimensionsInfo(path)
92
93 expect(size.height).to.equal(0)
94 expect(size.width).to.equal(0)
95 expect(size.isPortraitMode).to.be.false
96 expect(size.ratio).to.equal(0)
97 expect(size.resolution).to.equal(0)
98 }
99 })
100
101 after(async function () {
102 await cleanupTests(servers)
103 })
104})
diff --git a/packages/tests/src/api/transcoding/create-transcoding.ts b/packages/tests/src/api/transcoding/create-transcoding.ts
new file mode 100644
index 000000000..b0a9c7556
--- /dev/null
+++ b/packages/tests/src/api/transcoding/create-transcoding.ts
@@ -0,0 +1,267 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
5import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 ConfigCommand,
9 createMultipleServers,
10 doubleFollow,
11 expectNoFailedTranscodingJob,
12 makeRawRequest,
13 ObjectStorageCommand,
14 PeerTubeServer,
15 setAccessTokensToServers,
16 waitJobs
17} from '@peertube/peertube-server-commands'
18import { expectStartWith } from '@tests/shared/checks.js'
19import { checkResolutionsInMasterPlaylist } from '@tests/shared/streaming-playlists.js'
20
21async function checkFilesInObjectStorage (objectStorage: ObjectStorageCommand, video: VideoDetails) {
22 for (const file of video.files) {
23 expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl())
24 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
25 }
26
27 if (video.streamingPlaylists.length === 0) return
28
29 const hlsPlaylist = video.streamingPlaylists[0]
30 for (const file of hlsPlaylist.files) {
31 expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
32 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
33 }
34
35 expectStartWith(hlsPlaylist.playlistUrl, objectStorage.getMockPlaylistBaseUrl())
36 await makeRawRequest({ url: hlsPlaylist.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
37
38 expectStartWith(hlsPlaylist.segmentsSha256Url, objectStorage.getMockPlaylistBaseUrl())
39 await makeRawRequest({ url: hlsPlaylist.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
40}
41
42function runTests (enableObjectStorage: boolean) {
43 let servers: PeerTubeServer[] = []
44 let videoUUID: string
45 let publishedAt: string
46
47 let shouldBeDeleted: string[]
48 const objectStorage = new ObjectStorageCommand()
49
50 before(async function () {
51 this.timeout(120000)
52
53 const config = enableObjectStorage
54 ? objectStorage.getDefaultMockConfig()
55 : {}
56
57 // Run server 2 to have transcoding enabled
58 servers = await createMultipleServers(2, config)
59 await setAccessTokensToServers(servers)
60
61 await servers[0].config.disableTranscoding()
62
63 await doubleFollow(servers[0], servers[1])
64
65 if (enableObjectStorage) await objectStorage.prepareDefaultMockBuckets()
66
67 const { shortUUID } = await servers[0].videos.quickUpload({ name: 'video' })
68 videoUUID = shortUUID
69
70 await waitJobs(servers)
71
72 const video = await servers[0].videos.get({ id: videoUUID })
73 publishedAt = video.publishedAt as string
74
75 await servers[0].config.enableTranscoding()
76 })
77
78 it('Should generate HLS', async function () {
79 this.timeout(60000)
80
81 await servers[0].videos.runTranscoding({
82 videoId: videoUUID,
83 transcodingType: 'hls'
84 })
85
86 await waitJobs(servers)
87 await expectNoFailedTranscodingJob(servers[0])
88
89 for (const server of servers) {
90 const videoDetails = await server.videos.get({ id: videoUUID })
91
92 expect(videoDetails.files).to.have.lengthOf(1)
93 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
94 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
95
96 if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
97 }
98 })
99
100 it('Should generate Web Video', async function () {
101 this.timeout(60000)
102
103 await servers[0].videos.runTranscoding({
104 videoId: videoUUID,
105 transcodingType: 'web-video'
106 })
107
108 await waitJobs(servers)
109
110 for (const server of servers) {
111 const videoDetails = await server.videos.get({ id: videoUUID })
112
113 expect(videoDetails.files).to.have.lengthOf(5)
114 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
115 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
116
117 if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
118 }
119 })
120
121 it('Should generate Web Video from HLS only video', async function () {
122 this.timeout(60000)
123
124 await servers[0].videos.removeAllWebVideoFiles({ videoId: videoUUID })
125 await waitJobs(servers)
126
127 await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' })
128 await waitJobs(servers)
129
130 for (const server of servers) {
131 const videoDetails = await server.videos.get({ id: videoUUID })
132
133 expect(videoDetails.files).to.have.lengthOf(5)
134 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
135 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
136
137 if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
138 }
139 })
140
141 it('Should only generate Web Video', async function () {
142 this.timeout(60000)
143
144 await servers[0].videos.removeHLSPlaylist({ videoId: videoUUID })
145 await waitJobs(servers)
146
147 await servers[0].videos.runTranscoding({ videoId: videoUUID, transcodingType: 'web-video' })
148 await waitJobs(servers)
149
150 for (const server of servers) {
151 const videoDetails = await server.videos.get({ id: videoUUID })
152
153 expect(videoDetails.files).to.have.lengthOf(5)
154 expect(videoDetails.streamingPlaylists).to.have.lengthOf(0)
155
156 if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
157 }
158 })
159
160 it('Should correctly update HLS playlist on resolution change', async function () {
161 this.timeout(120000)
162
163 await servers[0].config.updateExistingSubConfig({
164 newConfig: {
165 transcoding: {
166 enabled: true,
167 resolutions: ConfigCommand.getCustomConfigResolutions(false),
168
169 webVideos: {
170 enabled: true
171 },
172 hls: {
173 enabled: true
174 }
175 }
176 }
177 })
178
179 const { uuid } = await servers[0].videos.quickUpload({ name: 'quick' })
180
181 await waitJobs(servers)
182
183 for (const server of servers) {
184 const videoDetails = await server.videos.get({ id: uuid })
185
186 expect(videoDetails.files).to.have.lengthOf(1)
187 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
188 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1)
189
190 if (enableObjectStorage) await checkFilesInObjectStorage(objectStorage, videoDetails)
191
192 shouldBeDeleted = [
193 videoDetails.streamingPlaylists[0].files[0].fileUrl,
194 videoDetails.streamingPlaylists[0].playlistUrl,
195 videoDetails.streamingPlaylists[0].segmentsSha256Url
196 ]
197 }
198
199 await servers[0].config.updateExistingSubConfig({
200 newConfig: {
201 transcoding: {
202 enabled: true,
203 resolutions: ConfigCommand.getCustomConfigResolutions(true),
204
205 webVideos: {
206 enabled: true
207 },
208 hls: {
209 enabled: true
210 }
211 }
212 }
213 })
214
215 await servers[0].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
216 await waitJobs(servers)
217
218 for (const server of servers) {
219 const videoDetails = await server.videos.get({ id: uuid })
220
221 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
222 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(5)
223
224 if (enableObjectStorage) {
225 await checkFilesInObjectStorage(objectStorage, videoDetails)
226
227 const hlsPlaylist = videoDetails.streamingPlaylists[0]
228 const resolutions = hlsPlaylist.files.map(f => f.resolution.id)
229 await checkResolutionsInMasterPlaylist({ server: servers[0], playlistUrl: hlsPlaylist.playlistUrl, resolutions })
230
231 const shaBody = await servers[0].streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: true })
232 expect(Object.keys(shaBody)).to.have.lengthOf(5)
233 }
234 }
235 })
236
237 it('Should have correctly deleted previous files', async function () {
238 for (const fileUrl of shouldBeDeleted) {
239 await makeRawRequest({ url: fileUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
240 }
241 })
242
243 it('Should not have updated published at attributes', async function () {
244 const video = await servers[0].videos.get({ id: videoUUID })
245
246 expect(video.publishedAt).to.equal(publishedAt)
247 })
248
249 after(async function () {
250 if (objectStorage) await objectStorage.cleanupMock()
251
252 await cleanupTests(servers)
253 })
254}
255
256describe('Test create transcoding jobs from API', function () {
257
258 describe('On filesystem', function () {
259 runTests(false)
260 })
261
262 describe('On object storage', function () {
263 if (areMockObjectStorageTestsDisabled()) return
264
265 runTests(true)
266 })
267})
diff --git a/packages/tests/src/api/transcoding/hls.ts b/packages/tests/src/api/transcoding/hls.ts
new file mode 100644
index 000000000..884f98e87
--- /dev/null
+++ b/packages/tests/src/api/transcoding/hls.ts
@@ -0,0 +1,176 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { join } from 'path'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 ObjectStorageCommand,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15import { DEFAULT_AUDIO_RESOLUTION } from '@peertube/peertube-server/server/initializers/constants.js'
16import { checkDirectoryIsEmpty, checkTmpIsEmpty } from '@tests/shared/directories.js'
17import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
18
19describe('Test HLS videos', function () {
20 let servers: PeerTubeServer[] = []
21
22 function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) {
23 const videoUUIDs: string[] = []
24
25 it('Should upload a video and transcode it to HLS', async function () {
26 this.timeout(120000)
27
28 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video 1', fixture: 'video_short.webm' } })
29 videoUUIDs.push(uuid)
30
31 await waitJobs(servers)
32
33 await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl })
34 })
35
36 it('Should upload an audio file and transcode it to HLS', async function () {
37 this.timeout(120000)
38
39 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video audio', fixture: 'sample.ogg' } })
40 videoUUIDs.push(uuid)
41
42 await waitJobs(servers)
43
44 await completeCheckHlsPlaylist({
45 servers,
46 videoUUID: uuid,
47 hlsOnly,
48 resolutions: [ DEFAULT_AUDIO_RESOLUTION, 360, 240 ],
49 objectStorageBaseUrl
50 })
51 })
52
53 it('Should update the video', async function () {
54 this.timeout(30000)
55
56 await servers[0].videos.update({ id: videoUUIDs[0], attributes: { name: 'video 1 updated' } })
57
58 await waitJobs(servers)
59
60 await completeCheckHlsPlaylist({ servers, videoUUID: videoUUIDs[0], hlsOnly, objectStorageBaseUrl })
61 })
62
63 it('Should delete videos', async function () {
64 for (const uuid of videoUUIDs) {
65 await servers[0].videos.remove({ id: uuid })
66 }
67
68 await waitJobs(servers)
69
70 for (const server of servers) {
71 for (const uuid of videoUUIDs) {
72 await server.videos.get({ id: uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
73 }
74 }
75 })
76
77 it('Should have the playlists/segment deleted from the disk', async function () {
78 for (const server of servers) {
79 await checkDirectoryIsEmpty(server, 'web-videos', [ 'private' ])
80 await checkDirectoryIsEmpty(server, join('web-videos', 'private'))
81
82 await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'), [ 'private' ])
83 await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls', 'private'))
84 }
85 })
86
87 it('Should have an empty tmp directory', async function () {
88 for (const server of servers) {
89 await checkTmpIsEmpty(server)
90 }
91 })
92 }
93
94 before(async function () {
95 this.timeout(120000)
96
97 const configOverride = {
98 transcoding: {
99 enabled: true,
100 allow_audio_files: true,
101 hls: {
102 enabled: true
103 }
104 }
105 }
106 servers = await createMultipleServers(2, configOverride)
107
108 // Get the access tokens
109 await setAccessTokensToServers(servers)
110
111 // Server 1 and server 2 follow each other
112 await doubleFollow(servers[0], servers[1])
113 })
114
115 describe('With Web Video & HLS enabled', function () {
116 runTestSuite(false)
117 })
118
119 describe('With only HLS enabled', function () {
120
121 before(async function () {
122 await servers[0].config.updateCustomSubConfig({
123 newConfig: {
124 transcoding: {
125 enabled: true,
126 allowAudioFiles: true,
127 resolutions: {
128 '144p': false,
129 '240p': true,
130 '360p': true,
131 '480p': true,
132 '720p': true,
133 '1080p': true,
134 '1440p': true,
135 '2160p': true
136 },
137 hls: {
138 enabled: true
139 },
140 webVideos: {
141 enabled: false
142 }
143 }
144 }
145 })
146 })
147
148 runTestSuite(true)
149 })
150
151 describe('With object storage enabled', function () {
152 if (areMockObjectStorageTestsDisabled()) return
153
154 const objectStorage = new ObjectStorageCommand()
155
156 before(async function () {
157 this.timeout(120000)
158
159 const configOverride = objectStorage.getDefaultMockConfig()
160 await objectStorage.prepareDefaultMockBuckets()
161
162 await servers[0].kill()
163 await servers[0].run(configOverride)
164 })
165
166 runTestSuite(true, objectStorage.getMockPlaylistBaseUrl())
167
168 after(async function () {
169 await objectStorage.cleanupMock()
170 })
171 })
172
173 after(async function () {
174 await cleanupTests(servers)
175 })
176})
diff --git a/packages/tests/src/api/transcoding/index.ts b/packages/tests/src/api/transcoding/index.ts
new file mode 100644
index 000000000..c25cd51c3
--- /dev/null
+++ b/packages/tests/src/api/transcoding/index.ts
@@ -0,0 +1,6 @@
1export * from './audio-only.js'
2export * from './create-transcoding.js'
3export * from './hls.js'
4export * from './transcoder.js'
5export * from './update-while-transcoding.js'
6export * from './video-studio.js'
diff --git a/packages/tests/src/api/transcoding/transcoder.ts b/packages/tests/src/api/transcoding/transcoder.ts
new file mode 100644
index 000000000..8900491f5
--- /dev/null
+++ b/packages/tests/src/api/transcoding/transcoder.ts
@@ -0,0 +1,802 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate, omit } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, VideoFileMetadata, VideoState } from '@peertube/peertube-models'
6import { canDoQuickTranscode } from '@peertube/peertube-server/server/lib/transcoding/transcoding-quick-transcode.js'
7import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
8import {
9 ffprobePromise,
10 getAudioStream,
11 getVideoStreamBitrate,
12 getVideoStreamDimensionsInfo,
13 getVideoStreamFPS,
14 hasAudioStream
15} from '@peertube/peertube-ffmpeg'
16import {
17 cleanupTests,
18 createMultipleServers,
19 doubleFollow,
20 makeGetRequest,
21 PeerTubeServer,
22 setAccessTokensToServers,
23 waitJobs
24} from '@peertube/peertube-server-commands'
25import { generateVideoWithFramerate, generateHighBitrateVideo } from '@tests/shared/generate.js'
26import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js'
27
28function updateConfigForTranscoding (server: PeerTubeServer) {
29 return server.config.updateCustomSubConfig({
30 newConfig: {
31 transcoding: {
32 enabled: true,
33 allowAdditionalExtensions: true,
34 allowAudioFiles: true,
35 hls: { enabled: true },
36 webVideos: { enabled: true },
37 resolutions: {
38 '0p': false,
39 '144p': true,
40 '240p': true,
41 '360p': true,
42 '480p': true,
43 '720p': true,
44 '1080p': true,
45 '1440p': true,
46 '2160p': true
47 }
48 }
49 }
50 })
51}
52
53describe('Test video transcoding', function () {
54 let servers: PeerTubeServer[] = []
55 let video4k: string
56
57 before(async function () {
58 this.timeout(30_000)
59
60 // Run servers
61 servers = await createMultipleServers(2)
62
63 await setAccessTokensToServers(servers)
64
65 await doubleFollow(servers[0], servers[1])
66
67 await updateConfigForTranscoding(servers[1])
68 })
69
70 describe('Basic transcoding (or not)', function () {
71
72 it('Should not transcode video on server 1', async function () {
73 this.timeout(60_000)
74
75 const attributes = {
76 name: 'my super name for server 1',
77 description: 'my super description for server 1',
78 fixture: 'video_short.webm'
79 }
80 await servers[0].videos.upload({ attributes })
81
82 await waitJobs(servers)
83
84 for (const server of servers) {
85 const { data } = await server.videos.list()
86 const video = data[0]
87
88 const videoDetails = await server.videos.get({ id: video.id })
89 expect(videoDetails.files).to.have.lengthOf(1)
90
91 const magnetUri = videoDetails.files[0].magnetUri
92 expect(magnetUri).to.match(/\.webm/)
93
94 await checkWebTorrentWorks(magnetUri, /\.webm$/)
95 }
96 })
97
98 it('Should transcode video on server 2', async function () {
99 this.timeout(120_000)
100
101 const attributes = {
102 name: 'my super name for server 2',
103 description: 'my super description for server 2',
104 fixture: 'video_short.webm'
105 }
106 await servers[1].videos.upload({ attributes })
107
108 await waitJobs(servers)
109
110 for (const server of servers) {
111 const { data } = await server.videos.list()
112
113 const video = data.find(v => v.name === attributes.name)
114 const videoDetails = await server.videos.get({ id: video.id })
115
116 expect(videoDetails.files).to.have.lengthOf(5)
117
118 const magnetUri = videoDetails.files[0].magnetUri
119 expect(magnetUri).to.match(/\.mp4/)
120
121 await checkWebTorrentWorks(magnetUri, /\.mp4$/)
122 }
123 })
124
125 it('Should wait for transcoding before publishing the video', async function () {
126 this.timeout(160_000)
127
128 {
129 // Upload the video, but wait transcoding
130 const attributes = {
131 name: 'waiting video',
132 fixture: 'video_short1.webm',
133 waitTranscoding: true
134 }
135 const { uuid } = await servers[1].videos.upload({ attributes })
136 const videoId = uuid
137
138 // Should be in transcode state
139 const body = await servers[1].videos.get({ id: videoId })
140 expect(body.name).to.equal('waiting video')
141 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
142 expect(body.state.label).to.equal('To transcode')
143 expect(body.waitTranscoding).to.be.true
144
145 {
146 // Should have my video
147 const { data } = await servers[1].videos.listMyVideos()
148 const videoToFindInMine = data.find(v => v.name === attributes.name)
149 expect(videoToFindInMine).not.to.be.undefined
150 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
151 expect(videoToFindInMine.state.label).to.equal('To transcode')
152 expect(videoToFindInMine.waitTranscoding).to.be.true
153 }
154
155 {
156 // Should not list this video
157 const { data } = await servers[1].videos.list()
158 const videoToFindInList = data.find(v => v.name === attributes.name)
159 expect(videoToFindInList).to.be.undefined
160 }
161
162 // Server 1 should not have the video yet
163 await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
164 }
165
166 await waitJobs(servers)
167
168 for (const server of servers) {
169 const { data } = await server.videos.list()
170 const videoToFind = data.find(v => v.name === 'waiting video')
171 expect(videoToFind).not.to.be.undefined
172
173 const videoDetails = await server.videos.get({ id: videoToFind.id })
174
175 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
176 expect(videoDetails.state.label).to.equal('Published')
177 expect(videoDetails.waitTranscoding).to.be.true
178 }
179 })
180
181 it('Should accept and transcode additional extensions', async function () {
182 this.timeout(300_000)
183
184 for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) {
185 const attributes = {
186 name: fixture,
187 fixture
188 }
189
190 await servers[1].videos.upload({ attributes })
191
192 await waitJobs(servers)
193
194 for (const server of servers) {
195 const { data } = await server.videos.list()
196
197 const video = data.find(v => v.name === attributes.name)
198 const videoDetails = await server.videos.get({ id: video.id })
199 expect(videoDetails.files).to.have.lengthOf(5)
200
201 const magnetUri = videoDetails.files[0].magnetUri
202 expect(magnetUri).to.contain('.mp4')
203 }
204 }
205 })
206
207 it('Should transcode a 4k video', async function () {
208 this.timeout(200_000)
209
210 const attributes = {
211 name: '4k video',
212 fixture: 'video_short_4k.mp4'
213 }
214
215 const { uuid } = await servers[1].videos.upload({ attributes })
216 video4k = uuid
217
218 await waitJobs(servers)
219
220 const resolutions = [ 144, 240, 360, 480, 720, 1080, 1440, 2160 ]
221
222 for (const server of servers) {
223 const videoDetails = await server.videos.get({ id: video4k })
224 expect(videoDetails.files).to.have.lengthOf(resolutions.length)
225
226 for (const r of resolutions) {
227 expect(videoDetails.files.find(f => f.resolution.id === r)).to.not.be.undefined
228 expect(videoDetails.streamingPlaylists[0].files.find(f => f.resolution.id === r)).to.not.be.undefined
229 }
230 }
231 })
232 })
233
234 describe('Audio transcoding', function () {
235
236 it('Should transcode high bit rate mp3 to proper bit rate', async function () {
237 this.timeout(60_000)
238
239 const attributes = {
240 name: 'mp3_256k',
241 fixture: 'video_short_mp3_256k.mp4'
242 }
243 await servers[1].videos.upload({ attributes })
244
245 await waitJobs(servers)
246
247 for (const server of servers) {
248 const { data } = await server.videos.list()
249
250 const video = data.find(v => v.name === attributes.name)
251 const videoDetails = await server.videos.get({ id: video.id })
252
253 expect(videoDetails.files).to.have.lengthOf(5)
254
255 const file = videoDetails.files.find(f => f.resolution.id === 240)
256 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
257 const probe = await getAudioStream(path)
258
259 if (probe.audioStream) {
260 expect(probe.audioStream['codec_name']).to.be.equal('aac')
261 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
262 } else {
263 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
264 }
265 }
266 })
267
268 it('Should transcode video with no audio and have no audio itself', async function () {
269 this.timeout(60_000)
270
271 const attributes = {
272 name: 'no_audio',
273 fixture: 'video_short_no_audio.mp4'
274 }
275 await servers[1].videos.upload({ attributes })
276
277 await waitJobs(servers)
278
279 for (const server of servers) {
280 const { data } = await server.videos.list()
281
282 const video = data.find(v => v.name === attributes.name)
283 const videoDetails = await server.videos.get({ id: video.id })
284
285 const file = videoDetails.files.find(f => f.resolution.id === 240)
286 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
287
288 expect(await hasAudioStream(path)).to.be.false
289 }
290 })
291
292 it('Should leave the audio untouched, but properly transcode the video', async function () {
293 this.timeout(60_000)
294
295 const attributes = {
296 name: 'untouched_audio',
297 fixture: 'video_short.mp4'
298 }
299 await servers[1].videos.upload({ attributes })
300
301 await waitJobs(servers)
302
303 for (const server of servers) {
304 const { data } = await server.videos.list()
305
306 const video = data.find(v => v.name === attributes.name)
307 const videoDetails = await server.videos.get({ id: video.id })
308
309 expect(videoDetails.files).to.have.lengthOf(5)
310
311 const fixturePath = buildAbsoluteFixturePath(attributes.fixture)
312 const fixtureVideoProbe = await getAudioStream(fixturePath)
313
314 const file = videoDetails.files.find(f => f.resolution.id === 240)
315 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
316
317 const videoProbe = await getAudioStream(path)
318
319 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
320 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
321 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
322 } else {
323 this.fail('Could not retrieve the audio stream on ' + videoProbe.absolutePath)
324 }
325 }
326 })
327 })
328
329 describe('Audio upload', function () {
330
331 function runSuite (mode: 'legacy' | 'resumable') {
332
333 before(async function () {
334 await servers[1].config.updateCustomSubConfig({
335 newConfig: {
336 transcoding: {
337 hls: { enabled: true },
338 webVideos: { enabled: true },
339 resolutions: {
340 '0p': false,
341 '144p': false,
342 '240p': false,
343 '360p': false,
344 '480p': false,
345 '720p': false,
346 '1080p': false,
347 '1440p': false,
348 '2160p': false
349 }
350 }
351 }
352 })
353 })
354
355 it('Should merge an audio file with the preview file', async function () {
356 this.timeout(60_000)
357
358 const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
359 await servers[1].videos.upload({ attributes, mode })
360
361 await waitJobs(servers)
362
363 for (const server of servers) {
364 const { data } = await server.videos.list()
365
366 const video = data.find(v => v.name === 'audio_with_preview')
367 const videoDetails = await server.videos.get({ id: video.id })
368
369 expect(videoDetails.files).to.have.lengthOf(1)
370
371 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
372 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
373
374 const magnetUri = videoDetails.files[0].magnetUri
375 expect(magnetUri).to.contain('.mp4')
376 }
377 })
378
379 it('Should upload an audio file and choose a default background image', async function () {
380 this.timeout(60_000)
381
382 const attributes = { name: 'audio_without_preview', fixture: 'sample.ogg' }
383 await servers[1].videos.upload({ attributes, mode })
384
385 await waitJobs(servers)
386
387 for (const server of servers) {
388 const { data } = await server.videos.list()
389
390 const video = data.find(v => v.name === 'audio_without_preview')
391 const videoDetails = await server.videos.get({ id: video.id })
392
393 expect(videoDetails.files).to.have.lengthOf(1)
394
395 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
396 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, expectedStatus: HttpStatusCode.OK_200 })
397
398 const magnetUri = videoDetails.files[0].magnetUri
399 expect(magnetUri).to.contain('.mp4')
400 }
401 })
402
403 it('Should upload an audio file and create an audio version only', async function () {
404 this.timeout(60_000)
405
406 await servers[1].config.updateCustomSubConfig({
407 newConfig: {
408 transcoding: {
409 hls: { enabled: true },
410 webVideos: { enabled: true },
411 resolutions: {
412 '0p': true,
413 '144p': false,
414 '240p': false,
415 '360p': false
416 }
417 }
418 }
419 })
420
421 const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' }
422 const { id } = await servers[1].videos.upload({ attributes, mode })
423
424 await waitJobs(servers)
425
426 for (const server of servers) {
427 const videoDetails = await server.videos.get({ id })
428
429 for (const files of [ videoDetails.files, videoDetails.streamingPlaylists[0].files ]) {
430 expect(files).to.have.lengthOf(2)
431 expect(files.find(f => f.resolution.id === 0)).to.not.be.undefined
432 }
433 }
434
435 await updateConfigForTranscoding(servers[1])
436 })
437 }
438
439 describe('Legacy upload', function () {
440 runSuite('legacy')
441 })
442
443 describe('Resumable upload', function () {
444 runSuite('resumable')
445 })
446 })
447
448 describe('Framerate', function () {
449
450 it('Should transcode a 60 FPS video', async function () {
451 this.timeout(60_000)
452
453 const attributes = {
454 name: 'my super 30fps name for server 2',
455 description: 'my super 30fps description for server 2',
456 fixture: '60fps_720p_small.mp4'
457 }
458 await servers[1].videos.upload({ attributes })
459
460 await waitJobs(servers)
461
462 for (const server of servers) {
463 const { data } = await server.videos.list()
464
465 const video = data.find(v => v.name === attributes.name)
466 const videoDetails = await server.videos.get({ id: video.id })
467
468 expect(videoDetails.files).to.have.lengthOf(5)
469 expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
470 expect(videoDetails.files[1].fps).to.be.below(31)
471 expect(videoDetails.files[2].fps).to.be.below(31)
472 expect(videoDetails.files[3].fps).to.be.below(31)
473 expect(videoDetails.files[4].fps).to.be.below(31)
474
475 for (const resolution of [ 144, 240, 360, 480 ]) {
476 const file = videoDetails.files.find(f => f.resolution.id === resolution)
477 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
478 const fps = await getVideoStreamFPS(path)
479
480 expect(fps).to.be.below(31)
481 }
482
483 const file = videoDetails.files.find(f => f.resolution.id === 720)
484 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
485 const fps = await getVideoStreamFPS(path)
486
487 expect(fps).to.be.above(58).and.below(62)
488 }
489 })
490
491 it('Should downscale to the closest divisor standard framerate', async function () {
492 this.timeout(200_000)
493
494 let tempFixturePath: string
495
496 {
497 tempFixturePath = await generateVideoWithFramerate(59)
498
499 const fps = await getVideoStreamFPS(tempFixturePath)
500 expect(fps).to.be.equal(59)
501 }
502
503 const attributes = {
504 name: '59fps video',
505 description: '59fps video',
506 fixture: tempFixturePath
507 }
508
509 await servers[1].videos.upload({ attributes })
510
511 await waitJobs(servers)
512
513 for (const server of servers) {
514 const { data } = await server.videos.list()
515
516 const { id } = data.find(v => v.name === attributes.name)
517 const video = await server.videos.get({ id })
518
519 {
520 const file = video.files.find(f => f.resolution.id === 240)
521 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
522 const fps = await getVideoStreamFPS(path)
523 expect(fps).to.be.equal(25)
524 }
525
526 {
527 const file = video.files.find(f => f.resolution.id === 720)
528 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
529 const fps = await getVideoStreamFPS(path)
530 expect(fps).to.be.equal(59)
531 }
532 }
533 })
534 })
535
536 describe('Bitrate control', function () {
537
538 it('Should respect maximum bitrate values', async function () {
539 this.timeout(160_000)
540
541 const tempFixturePath = await generateHighBitrateVideo()
542
543 const attributes = {
544 name: 'high bitrate video',
545 description: 'high bitrate video',
546 fixture: tempFixturePath
547 }
548
549 await servers[1].videos.upload({ attributes })
550
551 await waitJobs(servers)
552
553 for (const server of servers) {
554 const { data } = await server.videos.list()
555
556 const { id } = data.find(v => v.name === attributes.name)
557 const video = await server.videos.get({ id })
558
559 for (const resolution of [ 240, 360, 480, 720, 1080 ]) {
560 const file = video.files.find(f => f.resolution.id === resolution)
561 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
562
563 const bitrate = await getVideoStreamBitrate(path)
564 const fps = await getVideoStreamFPS(path)
565 const dataResolution = await getVideoStreamDimensionsInfo(path)
566
567 expect(resolution).to.equal(resolution)
568
569 const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps })
570 expect(bitrate).to.be.below(maxBitrate)
571 }
572 }
573 })
574
575 it('Should not transcode to an higher bitrate than the original file but above our low limit', async function () {
576 this.timeout(160_000)
577
578 const newConfig = {
579 transcoding: {
580 enabled: true,
581 resolutions: {
582 '144p': true,
583 '240p': true,
584 '360p': true,
585 '480p': true,
586 '720p': true,
587 '1080p': true,
588 '1440p': true,
589 '2160p': true
590 },
591 webVideos: { enabled: true },
592 hls: { enabled: true }
593 }
594 }
595 await servers[1].config.updateCustomSubConfig({ newConfig })
596
597 const attributes = {
598 name: 'low bitrate',
599 fixture: 'low-bitrate.mp4'
600 }
601
602 const { id } = await servers[1].videos.upload({ attributes })
603
604 await waitJobs(servers)
605
606 const video = await servers[1].videos.get({ id })
607
608 const resolutions = [ 240, 360, 480, 720, 1080 ]
609 for (const r of resolutions) {
610 const file = video.files.find(f => f.resolution.id === r)
611
612 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
613 const bitrate = await getVideoStreamBitrate(path)
614
615 const inputBitrate = 60_000
616 const limit = getMinTheoreticalBitrate({ fps: 10, ratio: 1, resolution: r })
617 let belowValue = Math.max(inputBitrate, limit)
618 belowValue += belowValue * 0.20 // Apply 20% margin because bitrate control is not very precise
619
620 expect(bitrate, `${path} not below ${limit}`).to.be.below(belowValue)
621 }
622 })
623 })
624
625 describe('FFprobe', function () {
626
627 it('Should provide valid ffprobe data', async function () {
628 this.timeout(160_000)
629
630 const videoUUID = (await servers[1].videos.quickUpload({ name: 'ffprobe data' })).uuid
631 await waitJobs(servers)
632
633 {
634 const video = await servers[1].videos.get({ id: videoUUID })
635 const file = video.files.find(f => f.resolution.id === 240)
636 const path = servers[1].servers.buildWebVideoFilePath(file.fileUrl)
637
638 const probe = await ffprobePromise(path)
639 const metadata = new VideoFileMetadata(probe)
640
641 // expected format properties
642 for (const p of [
643 'tags.encoder',
644 'format_long_name',
645 'size',
646 'bit_rate'
647 ]) {
648 expect(metadata.format).to.have.nested.property(p)
649 }
650
651 // expected stream properties
652 for (const p of [
653 'codec_long_name',
654 'profile',
655 'width',
656 'height',
657 'display_aspect_ratio',
658 'avg_frame_rate',
659 'pix_fmt'
660 ]) {
661 expect(metadata.streams[0]).to.have.nested.property(p)
662 }
663
664 expect(metadata).to.not.have.nested.property('format.filename')
665 }
666
667 for (const server of servers) {
668 const videoDetails = await server.videos.get({ id: videoUUID })
669
670 const videoFiles = getAllFiles(videoDetails)
671 expect(videoFiles).to.have.lengthOf(10)
672
673 for (const file of videoFiles) {
674 expect(file.metadata).to.be.undefined
675 expect(file.metadataUrl).to.exist
676 expect(file.metadataUrl).to.contain(servers[1].url)
677 expect(file.metadataUrl).to.contain(videoUUID)
678
679 const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
680 expect(metadata).to.have.nested.property('format.size')
681 }
682 }
683 })
684
685 it('Should correctly detect if quick transcode is possible', async function () {
686 this.timeout(10_000)
687
688 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.mp4'))).to.be.true
689 expect(await canDoQuickTranscode(buildAbsoluteFixturePath('video_short.webm'))).to.be.false
690 })
691 })
692
693 describe('Transcoding job queue', function () {
694
695 it('Should have the appropriate priorities for transcoding jobs', async function () {
696 const body = await servers[1].jobs.list({
697 start: 0,
698 count: 100,
699 sort: 'createdAt',
700 jobType: 'video-transcoding'
701 })
702
703 const jobs = body.data
704 const transcodingJobs = jobs.filter(j => j.data.videoUUID === video4k)
705
706 expect(transcodingJobs).to.have.lengthOf(16)
707
708 const hlsJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-hls')
709 const webVideoJobs = transcodingJobs.filter(j => j.data.type === 'new-resolution-to-web-video')
710 const optimizeJobs = transcodingJobs.filter(j => j.data.type === 'optimize-to-web-video')
711
712 expect(hlsJobs).to.have.lengthOf(8)
713 expect(webVideoJobs).to.have.lengthOf(7)
714 expect(optimizeJobs).to.have.lengthOf(1)
715
716 for (const j of optimizeJobs.concat(hlsJobs.concat(webVideoJobs))) {
717 expect(j.priority).to.be.greaterThan(100)
718 expect(j.priority).to.be.lessThan(150)
719 }
720 })
721 })
722
723 describe('Bounded transcoding', function () {
724
725 it('Should not generate an upper resolution than original file', async function () {
726 this.timeout(120_000)
727
728 await servers[0].config.updateExistingSubConfig({
729 newConfig: {
730 transcoding: {
731 enabled: true,
732 hls: { enabled: true },
733 webVideos: { enabled: true },
734 resolutions: {
735 '0p': false,
736 '144p': false,
737 '240p': true,
738 '360p': false,
739 '480p': true,
740 '720p': false,
741 '1080p': false,
742 '1440p': false,
743 '2160p': false
744 },
745 alwaysTranscodeOriginalResolution: false
746 }
747 }
748 })
749
750 const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
751 await waitJobs(servers)
752
753 const video = await servers[0].videos.get({ id: uuid })
754 const hlsFiles = video.streamingPlaylists[0].files
755
756 expect(video.files).to.have.lengthOf(2)
757 expect(hlsFiles).to.have.lengthOf(2)
758
759 // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
760 const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
761 expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
762 })
763
764 it('Should only keep the original resolution if all resolutions are disabled', async function () {
765 this.timeout(120_000)
766
767 await servers[0].config.updateExistingSubConfig({
768 newConfig: {
769 transcoding: {
770 resolutions: {
771 '0p': false,
772 '144p': false,
773 '240p': false,
774 '360p': false,
775 '480p': false,
776 '720p': false,
777 '1080p': false,
778 '1440p': false,
779 '2160p': false
780 }
781 }
782 }
783 })
784
785 const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
786 await waitJobs(servers)
787
788 const video = await servers[0].videos.get({ id: uuid })
789 const hlsFiles = video.streamingPlaylists[0].files
790
791 expect(video.files).to.have.lengthOf(1)
792 expect(hlsFiles).to.have.lengthOf(1)
793
794 expect(video.files[0].resolution.id).to.equal(720)
795 expect(hlsFiles[0].resolution.id).to.equal(720)
796 })
797 })
798
799 after(async function () {
800 await cleanupTests(servers)
801 })
802})
diff --git a/packages/tests/src/api/transcoding/update-while-transcoding.ts b/packages/tests/src/api/transcoding/update-while-transcoding.ts
new file mode 100644
index 000000000..9990bc745
--- /dev/null
+++ b/packages/tests/src/api/transcoding/update-while-transcoding.ts
@@ -0,0 +1,161 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { wait } from '@peertube/peertube-core-utils'
4import { VideoPrivacy } from '@peertube/peertube-models'
5import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 ObjectStorageCommand,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js'
16
17describe('Test update video privacy while transcoding', function () {
18 let servers: PeerTubeServer[] = []
19
20 const videoUUIDs: string[] = []
21
22 function runTestSuite (hlsOnly: boolean, objectStorageBaseUrl?: string) {
23
24 it('Should not have an error while quickly updating a private video to public after upload #1', async function () {
25 this.timeout(360_000)
26
27 const attributes = {
28 name: 'quick update',
29 privacy: VideoPrivacy.PRIVATE
30 }
31
32 const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: false })
33 await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
34 videoUUIDs.push(uuid)
35
36 await waitJobs(servers)
37
38 await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl })
39 })
40
41 it('Should not have an error while quickly updating a private video to public after upload #2', async function () {
42 this.timeout(60000)
43
44 {
45 const attributes = {
46 name: 'quick update 2',
47 privacy: VideoPrivacy.PRIVATE
48 }
49
50 const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true })
51 await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
52 videoUUIDs.push(uuid)
53
54 await waitJobs(servers)
55
56 await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl })
57 }
58 })
59
60 it('Should not have an error while quickly updating a private video to public after upload #3', async function () {
61 this.timeout(60000)
62
63 const attributes = {
64 name: 'quick update 3',
65 privacy: VideoPrivacy.PRIVATE
66 }
67
68 const { uuid } = await servers[0].videos.upload({ attributes, waitTorrentGeneration: true })
69 await wait(1000)
70 await servers[0].videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
71 videoUUIDs.push(uuid)
72
73 await waitJobs(servers)
74
75 await completeCheckHlsPlaylist({ servers, videoUUID: uuid, hlsOnly, objectStorageBaseUrl })
76 })
77 }
78
79 before(async function () {
80 this.timeout(120000)
81
82 const configOverride = {
83 transcoding: {
84 enabled: true,
85 allow_audio_files: true,
86 hls: {
87 enabled: true
88 }
89 }
90 }
91 servers = await createMultipleServers(2, configOverride)
92
93 // Get the access tokens
94 await setAccessTokensToServers(servers)
95
96 // Server 1 and server 2 follow each other
97 await doubleFollow(servers[0], servers[1])
98 })
99
100 describe('With Web Video & HLS enabled', function () {
101 runTestSuite(false)
102 })
103
104 describe('With only HLS enabled', function () {
105
106 before(async function () {
107 await servers[0].config.updateCustomSubConfig({
108 newConfig: {
109 transcoding: {
110 enabled: true,
111 allowAudioFiles: true,
112 resolutions: {
113 '144p': false,
114 '240p': true,
115 '360p': true,
116 '480p': true,
117 '720p': true,
118 '1080p': true,
119 '1440p': true,
120 '2160p': true
121 },
122 hls: {
123 enabled: true
124 },
125 webVideos: {
126 enabled: false
127 }
128 }
129 }
130 })
131 })
132
133 runTestSuite(true)
134 })
135
136 describe('With object storage enabled', function () {
137 if (areMockObjectStorageTestsDisabled()) return
138
139 const objectStorage = new ObjectStorageCommand()
140
141 before(async function () {
142 this.timeout(120000)
143
144 const configOverride = objectStorage.getDefaultMockConfig()
145 await objectStorage.prepareDefaultMockBuckets()
146
147 await servers[0].kill()
148 await servers[0].run(configOverride)
149 })
150
151 runTestSuite(true, objectStorage.getMockPlaylistBaseUrl())
152
153 after(async function () {
154 await objectStorage.cleanupMock()
155 })
156 })
157
158 after(async function () {
159 await cleanupTests(servers)
160 })
161})
diff --git a/packages/tests/src/api/transcoding/video-studio.ts b/packages/tests/src/api/transcoding/video-studio.ts
new file mode 100644
index 000000000..8a3788aa6
--- /dev/null
+++ b/packages/tests/src/api/transcoding/video-studio.ts
@@ -0,0 +1,379 @@
1import { expect } from 'chai'
2import { getAllFiles } from '@peertube/peertube-core-utils'
3import { VideoStudioTask } from '@peertube/peertube-models'
4import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 ObjectStorageCommand,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 VideoStudioCommand,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16import { checkVideoDuration, expectStartWith } from '@tests/shared/checks.js'
17import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js'
18
19describe('Test video studio', function () {
20 let servers: PeerTubeServer[] = []
21 let videoUUID: string
22
23 async function renewVideo (fixture = 'video_short.webm') {
24 const video = await servers[0].videos.quickUpload({ name: 'video', fixture })
25 videoUUID = video.uuid
26
27 await waitJobs(servers)
28 }
29
30 async function createTasks (tasks: VideoStudioTask[]) {
31 await servers[0].videoStudio.createEditionTasks({ videoId: videoUUID, tasks })
32 await waitJobs(servers)
33 }
34
35 before(async function () {
36 this.timeout(120_000)
37
38 servers = await createMultipleServers(2)
39
40 await setAccessTokensToServers(servers)
41 await setDefaultVideoChannel(servers)
42
43 await doubleFollow(servers[0], servers[1])
44
45 await servers[0].config.enableMinimumTranscoding()
46
47 await servers[0].config.enableStudio()
48 })
49
50 describe('Cutting', function () {
51
52 it('Should cut the beginning of the video', async function () {
53 this.timeout(120_000)
54
55 await renewVideo()
56 await waitJobs(servers)
57
58 const beforeTasks = new Date()
59
60 await createTasks([
61 {
62 name: 'cut',
63 options: {
64 start: 2
65 }
66 }
67 ])
68
69 for (const server of servers) {
70 await checkVideoDuration(server, videoUUID, 3)
71
72 const video = await server.videos.get({ id: videoUUID })
73 expect(new Date(video.publishedAt)).to.be.below(beforeTasks)
74 }
75 })
76
77 it('Should cut the end of the video', async function () {
78 this.timeout(120_000)
79 await renewVideo()
80
81 await createTasks([
82 {
83 name: 'cut',
84 options: {
85 end: 2
86 }
87 }
88 ])
89
90 for (const server of servers) {
91 await checkVideoDuration(server, videoUUID, 2)
92 }
93 })
94
95 it('Should cut start/end of the video', async function () {
96 this.timeout(120_000)
97 await renewVideo('video_short1.webm') // 10 seconds video duration
98
99 await createTasks([
100 {
101 name: 'cut',
102 options: {
103 start: 2,
104 end: 6
105 }
106 }
107 ])
108
109 for (const server of servers) {
110 await checkVideoDuration(server, videoUUID, 4)
111 }
112 })
113 })
114
115 describe('Intro/Outro', function () {
116
117 it('Should add an intro', async function () {
118 this.timeout(120_000)
119 await renewVideo()
120
121 await createTasks([
122 {
123 name: 'add-intro',
124 options: {
125 file: 'video_short.webm'
126 }
127 }
128 ])
129
130 for (const server of servers) {
131 await checkVideoDuration(server, videoUUID, 10)
132 }
133 })
134
135 it('Should add an outro', async function () {
136 this.timeout(120_000)
137 await renewVideo()
138
139 await createTasks([
140 {
141 name: 'add-outro',
142 options: {
143 file: 'video_very_short_240p.mp4'
144 }
145 }
146 ])
147
148 for (const server of servers) {
149 await checkVideoDuration(server, videoUUID, 7)
150 }
151 })
152
153 it('Should add an intro/outro', async function () {
154 this.timeout(120_000)
155 await renewVideo()
156
157 await createTasks([
158 {
159 name: 'add-intro',
160 options: {
161 file: 'video_very_short_240p.mp4'
162 }
163 },
164 {
165 name: 'add-outro',
166 options: {
167 // Different frame rate
168 file: 'video_short2.webm'
169 }
170 }
171 ])
172
173 for (const server of servers) {
174 await checkVideoDuration(server, videoUUID, 12)
175 }
176 })
177
178 it('Should add an intro to a video without audio', async function () {
179 this.timeout(120_000)
180 await renewVideo('video_short_no_audio.mp4')
181
182 await createTasks([
183 {
184 name: 'add-intro',
185 options: {
186 file: 'video_very_short_240p.mp4'
187 }
188 }
189 ])
190
191 for (const server of servers) {
192 await checkVideoDuration(server, videoUUID, 7)
193 }
194 })
195
196 it('Should add an outro without audio to a video with audio', async function () {
197 this.timeout(120_000)
198 await renewVideo()
199
200 await createTasks([
201 {
202 name: 'add-outro',
203 options: {
204 file: 'video_short_no_audio.mp4'
205 }
206 }
207 ])
208
209 for (const server of servers) {
210 await checkVideoDuration(server, videoUUID, 10)
211 }
212 })
213
214 it('Should add an outro without audio to a video with audio', async function () {
215 this.timeout(120_000)
216 await renewVideo('video_short_no_audio.mp4')
217
218 await createTasks([
219 {
220 name: 'add-outro',
221 options: {
222 file: 'video_short_no_audio.mp4'
223 }
224 }
225 ])
226
227 for (const server of servers) {
228 await checkVideoDuration(server, videoUUID, 10)
229 }
230 })
231 })
232
233 describe('Watermark', function () {
234
235 it('Should add a watermark to the video', async function () {
236 this.timeout(120_000)
237 await renewVideo()
238
239 const video = await servers[0].videos.get({ id: videoUUID })
240 const oldFileUrls = getAllFiles(video).map(f => f.fileUrl)
241
242 await createTasks([
243 {
244 name: 'add-watermark',
245 options: {
246 file: 'custom-thumbnail.png'
247 }
248 }
249 ])
250
251 for (const server of servers) {
252 const video = await server.videos.get({ id: videoUUID })
253 const fileUrls = getAllFiles(video).map(f => f.fileUrl)
254
255 for (const oldUrl of oldFileUrls) {
256 expect(fileUrls).to.not.include(oldUrl)
257 }
258 }
259 })
260 })
261
262 describe('Complex tasks', function () {
263 it('Should run a complex task', async function () {
264 this.timeout(240_000)
265 await renewVideo()
266
267 await createTasks(VideoStudioCommand.getComplexTask())
268
269 for (const server of servers) {
270 await checkVideoDuration(server, videoUUID, 9)
271 }
272 })
273 })
274
275 describe('HLS only studio edition', function () {
276
277 before(async function () {
278 // Disable Web Videos
279 await servers[0].config.updateExistingSubConfig({
280 newConfig: {
281 transcoding: {
282 webVideos: {
283 enabled: false
284 }
285 }
286 }
287 })
288 })
289
290 it('Should run a complex task on HLS only video', async function () {
291 this.timeout(240_000)
292 await renewVideo()
293
294 await createTasks(VideoStudioCommand.getComplexTask())
295
296 for (const server of servers) {
297 const video = await server.videos.get({ id: videoUUID })
298 expect(video.files).to.have.lengthOf(0)
299
300 await checkVideoDuration(server, videoUUID, 9)
301 }
302 })
303 })
304
305 describe('Server restart', function () {
306
307 it('Should still be able to run video edition after a server restart', async function () {
308 this.timeout(240_000)
309
310 await renewVideo()
311 await servers[0].videoStudio.createEditionTasks({ videoId: videoUUID, tasks: VideoStudioCommand.getComplexTask() })
312
313 await servers[0].kill()
314 await servers[0].run()
315
316 await waitJobs(servers)
317
318 for (const server of servers) {
319 await checkVideoDuration(server, videoUUID, 9)
320 }
321 })
322
323 it('Should have an empty persistent tmp directory', async function () {
324 await checkPersistentTmpIsEmpty(servers[0])
325 })
326 })
327
328 describe('Object storage studio edition', function () {
329 if (areMockObjectStorageTestsDisabled()) return
330
331 const objectStorage = new ObjectStorageCommand()
332
333 before(async function () {
334 await objectStorage.prepareDefaultMockBuckets()
335
336 await servers[0].kill()
337 await servers[0].run(objectStorage.getDefaultMockConfig())
338
339 await servers[0].config.enableMinimumTranscoding()
340 })
341
342 it('Should run a complex task on a video in object storage', async function () {
343 this.timeout(240_000)
344 await renewVideo()
345
346 const video = await servers[0].videos.get({ id: videoUUID })
347 const oldFileUrls = getAllFiles(video).map(f => f.fileUrl)
348
349 await createTasks(VideoStudioCommand.getComplexTask())
350
351 for (const server of servers) {
352 const video = await server.videos.get({ id: videoUUID })
353 const files = getAllFiles(video)
354
355 for (const f of files) {
356 expect(oldFileUrls).to.not.include(f.fileUrl)
357 }
358
359 for (const webVideoFile of video.files) {
360 expectStartWith(webVideoFile.fileUrl, objectStorage.getMockWebVideosBaseUrl())
361 }
362
363 for (const hlsFile of video.streamingPlaylists[0].files) {
364 expectStartWith(hlsFile.fileUrl, objectStorage.getMockPlaylistBaseUrl())
365 }
366
367 await checkVideoDuration(server, videoUUID, 9)
368 }
369 })
370
371 after(async function () {
372 await objectStorage.cleanupMock()
373 })
374 })
375
376 after(async function () {
377 await cleanupTests(servers)
378 })
379})
diff --git a/packages/tests/src/api/users/index.ts b/packages/tests/src/api/users/index.ts
new file mode 100644
index 000000000..830d4da62
--- /dev/null
+++ b/packages/tests/src/api/users/index.ts
@@ -0,0 +1,8 @@
1import './oauth.js'
2import './registrations`.js'
3import './two-factor.js'
4import './user-subscriptions.js'
5import './user-videos.js'
6import './users.js'
7import './users-multiple-servers.js'
8import './users-email-verification.js'
diff --git a/packages/tests/src/api/users/oauth.ts b/packages/tests/src/api/users/oauth.ts
new file mode 100644
index 000000000..fe50872cb
--- /dev/null
+++ b/packages/tests/src/api/users/oauth.ts
@@ -0,0 +1,203 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@peertube/peertube-models'
6import { SQLCommand } from '@tests/shared/sql-command.js'
7import {
8 cleanupTests,
9 createSingleServer,
10 killallServers,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15describe('Test oauth', function () {
16 let server: PeerTubeServer
17 let sqlCommand: SQLCommand
18
19 before(async function () {
20 this.timeout(30000)
21
22 server = await createSingleServer(1, {
23 rates_limit: {
24 login: {
25 max: 30
26 }
27 }
28 })
29
30 await setAccessTokensToServers([ server ])
31
32 sqlCommand = new SQLCommand(server)
33 })
34
35 describe('OAuth client', function () {
36
37 function expectInvalidClient (body: PeerTubeProblemDocument) {
38 expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
39 expect(body.error).to.contain('client is invalid')
40 expect(body.type.startsWith('https://')).to.be.true
41 expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
42 }
43
44 it('Should create a new client')
45
46 it('Should return the first client')
47
48 it('Should remove the last client')
49
50 it('Should not login with an invalid client id', async function () {
51 const client = { id: 'client', secret: server.store.client.secret }
52 const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
53
54 expectInvalidClient(body)
55 })
56
57 it('Should not login with an invalid client secret', async function () {
58 const client = { id: server.store.client.id, secret: 'coucou' }
59 const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
60
61 expectInvalidClient(body)
62 })
63 })
64
65 describe('Login', function () {
66
67 function expectInvalidCredentials (body: PeerTubeProblemDocument) {
68 expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
69 expect(body.error).to.contain('credentials are invalid')
70 expect(body.type.startsWith('https://')).to.be.true
71 expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
72 }
73
74 it('Should not login with an invalid username', async function () {
75 const user = { username: 'captain crochet', password: server.store.user.password }
76 const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
77
78 expectInvalidCredentials(body)
79 })
80
81 it('Should not login with an invalid password', async function () {
82 const user = { username: server.store.user.username, password: 'mew_three' }
83 const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
84
85 expectInvalidCredentials(body)
86 })
87
88 it('Should be able to login', async function () {
89 await server.login.login({ expectedStatus: HttpStatusCode.OK_200 })
90 })
91
92 it('Should be able to login with an insensitive username', async function () {
93 const user = { username: 'RoOt', password: server.store.user.password }
94 await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 })
95
96 const user2 = { username: 'rOoT', password: server.store.user.password }
97 await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 })
98
99 const user3 = { username: 'ROOt', password: server.store.user.password }
100 await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 })
101 })
102 })
103
104 describe('Logout', function () {
105
106 it('Should logout (revoke token)', async function () {
107 await server.login.logout({ token: server.accessToken })
108 })
109
110 it('Should not be able to get the user information', async function () {
111 await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
112 })
113
114 it('Should not be able to upload a video', async function () {
115 await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
116 })
117
118 it('Should be able to login again', async function () {
119 const body = await server.login.login()
120 server.accessToken = body.access_token
121 server.refreshToken = body.refresh_token
122 })
123
124 it('Should be able to get my user information again', async function () {
125 await server.users.getMyInfo()
126 })
127
128 it('Should have an expired access token', async function () {
129 this.timeout(60000)
130
131 await sqlCommand.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
132 await sqlCommand.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
133
134 await killallServers([ server ])
135 await server.run()
136
137 await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
138 })
139
140 it('Should not be able to refresh an access token with an expired refresh token', async function () {
141 await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
142 })
143
144 it('Should refresh the token', async function () {
145 this.timeout(50000)
146
147 const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
148 await sqlCommand.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate)
149
150 await killallServers([ server ])
151 await server.run()
152
153 const res = await server.login.refreshToken({ refreshToken: server.refreshToken })
154 server.accessToken = res.body.access_token
155 server.refreshToken = res.body.refresh_token
156 })
157
158 it('Should be able to get my user information again', async function () {
159 await server.users.getMyInfo()
160 })
161 })
162
163 describe('Custom token lifetime', function () {
164 before(async function () {
165 this.timeout(120_000)
166
167 await server.kill()
168 await server.run({
169 oauth2: {
170 token_lifetime: {
171 access_token: '2 seconds',
172 refresh_token: '2 seconds'
173 }
174 }
175 })
176 })
177
178 it('Should have a very short access token lifetime', async function () {
179 this.timeout(50000)
180
181 const { access_token: accessToken } = await server.login.login()
182 await server.users.getMyInfo({ token: accessToken })
183
184 await wait(3000)
185 await server.users.getMyInfo({ token: accessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
186 })
187
188 it('Should have a very short refresh token lifetime', async function () {
189 this.timeout(50000)
190
191 const { refresh_token: refreshToken } = await server.login.login()
192 await server.login.refreshToken({ refreshToken })
193
194 await wait(3000)
195 await server.login.refreshToken({ refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
196 })
197 })
198
199 after(async function () {
200 await sqlCommand.cleanup()
201 await cleanupTests([ server ])
202 })
203})
diff --git a/packages/tests/src/api/users/registrations.ts b/packages/tests/src/api/users/registrations.ts
new file mode 100644
index 000000000..dbe1bc4f5
--- /dev/null
+++ b/packages/tests/src/api/users/registrations.ts
@@ -0,0 +1,415 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { UserRegistrationState, UserRole } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 ConfigCommand,
9 createSingleServer,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test registrations', function () {
16 let server: PeerTubeServer
17
18 const emails: object[] = []
19 let emailPort: number
20
21 before(async function () {
22 this.timeout(30000)
23
24 emailPort = await MockSmtpServer.Instance.collectEmails(emails)
25
26 server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(emailPort))
27
28 await setAccessTokensToServers([ server ])
29 await server.config.enableSignup(false)
30 })
31
32 describe('Direct registrations of a new user', function () {
33 let user1Token: string
34
35 it('Should register a new user', async function () {
36 const user = { displayName: 'super user 1', username: 'user_1', password: 'my super password' }
37 const channel = { name: 'my_user_1_channel', displayName: 'my channel rocks' }
38
39 await server.registrations.register({ ...user, channel })
40 })
41
42 it('Should be able to login with this registered user', async function () {
43 const user1 = { username: 'user_1', password: 'my super password' }
44
45 user1Token = await server.login.getAccessToken(user1)
46 })
47
48 it('Should have the correct display name', async function () {
49 const user = await server.users.getMyInfo({ token: user1Token })
50 expect(user.account.displayName).to.equal('super user 1')
51 })
52
53 it('Should have the correct video quota', async function () {
54 const user = await server.users.getMyInfo({ token: user1Token })
55 expect(user.videoQuota).to.equal(5 * 1024 * 1024)
56 })
57
58 it('Should have created the channel', async function () {
59 const { displayName } = await server.channels.get({ channelName: 'my_user_1_channel' })
60
61 expect(displayName).to.equal('my channel rocks')
62 })
63
64 it('Should remove me', async function () {
65 {
66 const { data } = await server.users.list()
67 expect(data.find(u => u.username === 'user_1')).to.not.be.undefined
68 }
69
70 await server.users.deleteMe({ token: user1Token })
71
72 {
73 const { data } = await server.users.list()
74 expect(data.find(u => u.username === 'user_1')).to.be.undefined
75 }
76 })
77 })
78
79 describe('Registration requests', function () {
80 let id2: number
81 let id3: number
82 let id4: number
83
84 let user2Token: string
85 let user3Token: string
86
87 before(async function () {
88 this.timeout(60000)
89
90 await server.config.enableSignup(true)
91
92 {
93 const { id } = await server.registrations.requestRegistration({
94 username: 'user4',
95 registrationReason: 'registration reason 4'
96 })
97
98 id4 = id
99 }
100 })
101
102 it('Should request a registration without a channel', async function () {
103 {
104 const { id } = await server.registrations.requestRegistration({
105 username: 'user2',
106 displayName: 'my super user 2',
107 email: 'user2@example.com',
108 password: 'user2password',
109 registrationReason: 'registration reason 2'
110 })
111
112 id2 = id
113 }
114 })
115
116 it('Should request a registration with a channel', async function () {
117 const { id } = await server.registrations.requestRegistration({
118 username: 'user3',
119 displayName: 'my super user 3',
120 channel: {
121 displayName: 'my user 3 channel',
122 name: 'super_user3_channel'
123 },
124 email: 'user3@example.com',
125 password: 'user3password',
126 registrationReason: 'registration reason 3'
127 })
128
129 id3 = id
130 })
131
132 it('Should list these registration requests', async function () {
133 {
134 const { total, data } = await server.registrations.list({ sort: '-createdAt' })
135 expect(total).to.equal(3)
136 expect(data).to.have.lengthOf(3)
137
138 {
139 expect(data[0].id).to.equal(id3)
140 expect(data[0].username).to.equal('user3')
141 expect(data[0].accountDisplayName).to.equal('my super user 3')
142
143 expect(data[0].channelDisplayName).to.equal('my user 3 channel')
144 expect(data[0].channelHandle).to.equal('super_user3_channel')
145
146 expect(data[0].createdAt).to.exist
147 expect(data[0].updatedAt).to.exist
148
149 expect(data[0].email).to.equal('user3@example.com')
150 expect(data[0].emailVerified).to.be.null
151
152 expect(data[0].moderationResponse).to.be.null
153 expect(data[0].registrationReason).to.equal('registration reason 3')
154 expect(data[0].state.id).to.equal(UserRegistrationState.PENDING)
155 expect(data[0].state.label).to.equal('Pending')
156 expect(data[0].user).to.be.null
157 }
158
159 {
160 expect(data[1].id).to.equal(id2)
161 expect(data[1].username).to.equal('user2')
162 expect(data[1].accountDisplayName).to.equal('my super user 2')
163
164 expect(data[1].channelDisplayName).to.be.null
165 expect(data[1].channelHandle).to.be.null
166
167 expect(data[1].createdAt).to.exist
168 expect(data[1].updatedAt).to.exist
169
170 expect(data[1].email).to.equal('user2@example.com')
171 expect(data[1].emailVerified).to.be.null
172
173 expect(data[1].moderationResponse).to.be.null
174 expect(data[1].registrationReason).to.equal('registration reason 2')
175 expect(data[1].state.id).to.equal(UserRegistrationState.PENDING)
176 expect(data[1].state.label).to.equal('Pending')
177 expect(data[1].user).to.be.null
178 }
179
180 {
181 expect(data[2].username).to.equal('user4')
182 }
183 }
184
185 {
186 const { total, data } = await server.registrations.list({ count: 1, start: 1, sort: 'createdAt' })
187
188 expect(total).to.equal(3)
189 expect(data).to.have.lengthOf(1)
190 expect(data[0].id).to.equal(id2)
191 }
192
193 {
194 const { total, data } = await server.registrations.list({ search: 'user3' })
195 expect(total).to.equal(1)
196 expect(data).to.have.lengthOf(1)
197 expect(data[0].id).to.equal(id3)
198 }
199 })
200
201 it('Should reject a registration request', async function () {
202 await server.registrations.reject({ id: id4, moderationResponse: 'I do not want id 4 on this instance' })
203 })
204
205 it('Should have sent an email to the user explanining the registration has been rejected', async function () {
206 this.timeout(50000)
207
208 await waitJobs([ server ])
209
210 const email = emails.find(e => e['to'][0]['address'] === 'user4@example.com')
211 expect(email).to.exist
212
213 expect(email['subject']).to.contain('been rejected')
214 expect(email['text']).to.contain('been rejected')
215 expect(email['text']).to.contain('I do not want id 4 on this instance')
216 })
217
218 it('Should accept registration requests', async function () {
219 await server.registrations.accept({ id: id2, moderationResponse: 'Welcome id 2' })
220 await server.registrations.accept({ id: id3, moderationResponse: 'Welcome id 3' })
221 })
222
223 it('Should have sent an email to the user explanining the registration has been accepted', async function () {
224 this.timeout(50000)
225
226 await waitJobs([ server ])
227
228 {
229 const email = emails.find(e => e['to'][0]['address'] === 'user2@example.com')
230 expect(email).to.exist
231
232 expect(email['subject']).to.contain('been accepted')
233 expect(email['text']).to.contain('been accepted')
234 expect(email['text']).to.contain('Welcome id 2')
235 }
236
237 {
238 const email = emails.find(e => e['to'][0]['address'] === 'user3@example.com')
239 expect(email).to.exist
240
241 expect(email['subject']).to.contain('been accepted')
242 expect(email['text']).to.contain('been accepted')
243 expect(email['text']).to.contain('Welcome id 3')
244 }
245 })
246
247 it('Should login with these users', async function () {
248 user2Token = await server.login.getAccessToken({ username: 'user2', password: 'user2password' })
249 user3Token = await server.login.getAccessToken({ username: 'user3', password: 'user3password' })
250 })
251
252 it('Should have created the appropriate attributes for user 2', async function () {
253 const me = await server.users.getMyInfo({ token: user2Token })
254
255 expect(me.username).to.equal('user2')
256 expect(me.account.displayName).to.equal('my super user 2')
257 expect(me.videoQuota).to.equal(5 * 1024 * 1024)
258 expect(me.videoChannels[0].name).to.equal('user2_channel')
259 expect(me.videoChannels[0].displayName).to.equal('Main user2 channel')
260 expect(me.role.id).to.equal(UserRole.USER)
261 expect(me.email).to.equal('user2@example.com')
262 })
263
264 it('Should have created the appropriate attributes for user 3', async function () {
265 const me = await server.users.getMyInfo({ token: user3Token })
266
267 expect(me.username).to.equal('user3')
268 expect(me.account.displayName).to.equal('my super user 3')
269 expect(me.videoQuota).to.equal(5 * 1024 * 1024)
270 expect(me.videoChannels[0].name).to.equal('super_user3_channel')
271 expect(me.videoChannels[0].displayName).to.equal('my user 3 channel')
272 expect(me.role.id).to.equal(UserRole.USER)
273 expect(me.email).to.equal('user3@example.com')
274 })
275
276 it('Should list these accepted/rejected registration requests', async function () {
277 const { data } = await server.registrations.list({ sort: 'createdAt' })
278 const { data: users } = await server.users.list()
279
280 {
281 expect(data[0].id).to.equal(id4)
282 expect(data[0].state.id).to.equal(UserRegistrationState.REJECTED)
283 expect(data[0].state.label).to.equal('Rejected')
284
285 expect(data[0].moderationResponse).to.equal('I do not want id 4 on this instance')
286 expect(data[0].user).to.be.null
287
288 expect(users.find(u => u.username === 'user4')).to.not.exist
289 }
290
291 {
292 expect(data[1].id).to.equal(id2)
293 expect(data[1].state.id).to.equal(UserRegistrationState.ACCEPTED)
294 expect(data[1].state.label).to.equal('Accepted')
295
296 expect(data[1].moderationResponse).to.equal('Welcome id 2')
297 expect(data[1].user).to.exist
298
299 const user2 = users.find(u => u.username === 'user2')
300 expect(data[1].user.id).to.equal(user2.id)
301 }
302
303 {
304 expect(data[2].id).to.equal(id3)
305 expect(data[2].state.id).to.equal(UserRegistrationState.ACCEPTED)
306 expect(data[2].state.label).to.equal('Accepted')
307
308 expect(data[2].moderationResponse).to.equal('Welcome id 3')
309 expect(data[2].user).to.exist
310
311 const user3 = users.find(u => u.username === 'user3')
312 expect(data[2].user.id).to.equal(user3.id)
313 }
314 })
315
316 it('Shoulde delete a registration', async function () {
317 await server.registrations.delete({ id: id2 })
318 await server.registrations.delete({ id: id3 })
319
320 const { total, data } = await server.registrations.list()
321 expect(total).to.equal(1)
322 expect(data).to.have.lengthOf(1)
323 expect(data[0].id).to.equal(id4)
324
325 const { data: users } = await server.users.list()
326
327 for (const username of [ 'user2', 'user3' ]) {
328 expect(users.find(u => u.username === username)).to.exist
329 }
330 })
331
332 it('Should be able to prevent email delivery on accept/reject', async function () {
333 this.timeout(50000)
334
335 let id1: number
336 let id2: number
337
338 {
339 const { id } = await server.registrations.requestRegistration({
340 username: 'user7',
341 email: 'user7@example.com',
342 registrationReason: 'tt'
343 })
344 id1 = id
345 }
346 {
347 const { id } = await server.registrations.requestRegistration({
348 username: 'user8',
349 email: 'user8@example.com',
350 registrationReason: 'tt'
351 })
352 id2 = id
353 }
354
355 await server.registrations.accept({ id: id1, moderationResponse: 'tt', preventEmailDelivery: true })
356 await server.registrations.reject({ id: id2, moderationResponse: 'tt', preventEmailDelivery: true })
357
358 await waitJobs([ server ])
359
360 const filtered = emails.filter(e => {
361 const address = e['to'][0]['address']
362 return address === 'user7@example.com' || address === 'user8@example.com'
363 })
364
365 expect(filtered).to.have.lengthOf(0)
366 })
367
368 it('Should request a registration without a channel, that will conflict with an already existing channel', async function () {
369 let id1: number
370 let id2: number
371
372 {
373 const { id } = await server.registrations.requestRegistration({
374 registrationReason: 'tt',
375 username: 'user5',
376 password: 'user5password',
377 channel: {
378 displayName: 'channel 6',
379 name: 'user6_channel'
380 }
381 })
382
383 id1 = id
384 }
385
386 {
387 const { id } = await server.registrations.requestRegistration({
388 registrationReason: 'tt',
389 username: 'user6',
390 password: 'user6password'
391 })
392
393 id2 = id
394 }
395
396 await server.registrations.accept({ id: id1, moderationResponse: 'tt' })
397 await server.registrations.accept({ id: id2, moderationResponse: 'tt' })
398
399 const user5Token = await server.login.getAccessToken('user5', 'user5password')
400 const user6Token = await server.login.getAccessToken('user6', 'user6password')
401
402 const user5 = await server.users.getMyInfo({ token: user5Token })
403 const user6 = await server.users.getMyInfo({ token: user6Token })
404
405 expect(user5.videoChannels[0].name).to.equal('user6_channel')
406 expect(user6.videoChannels[0].name).to.equal('user6_channel-1')
407 })
408 })
409
410 after(async function () {
411 MockSmtpServer.Instance.kill()
412
413 await cleanupTests([ server ])
414 })
415})
diff --git a/packages/tests/src/api/users/two-factor.ts b/packages/tests/src/api/users/two-factor.ts
new file mode 100644
index 000000000..fda125d20
--- /dev/null
+++ b/packages/tests/src/api/users/two-factor.ts
@@ -0,0 +1,206 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode, HttpStatusCodeType } from '@peertube/peertube-models'
5import { expectStartWith } from '@tests/shared/checks.js'
6import {
7 cleanupTests,
8 createSingleServer,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 TwoFactorCommand
12} from '@peertube/peertube-server-commands'
13
14async function login (options: {
15 server: PeerTubeServer
16 username: string
17 password: string
18 otpToken?: string
19 expectedStatus?: HttpStatusCodeType
20}) {
21 const { server, username, password, otpToken, expectedStatus } = options
22
23 const user = { username, password }
24 const { res, body: { access_token: token } } = await server.login.loginAndGetResponse({ user, otpToken, expectedStatus })
25
26 return { res, token }
27}
28
29describe('Test users', function () {
30 let server: PeerTubeServer
31 let otpSecret: string
32 let requestToken: string
33
34 const userUsername = 'user1'
35 let userId: number
36 let userPassword: string
37 let userToken: string
38
39 before(async function () {
40 this.timeout(30000)
41
42 server = await createSingleServer(1)
43
44 await setAccessTokensToServers([ server ])
45 const res = await server.users.generate(userUsername)
46 userId = res.userId
47 userPassword = res.password
48 userToken = res.token
49 })
50
51 it('Should not add the header on login if two factor is not enabled', async function () {
52 const { res, token } = await login({ server, username: userUsername, password: userPassword })
53
54 expect(res.header['x-peertube-otp']).to.not.exist
55
56 await server.users.getMyInfo({ token })
57 })
58
59 it('Should request two factor and get the secret and uri', async function () {
60 const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword })
61
62 expect(otpRequest.requestToken).to.exist
63
64 expect(otpRequest.secret).to.exist
65 expect(otpRequest.secret).to.have.lengthOf(32)
66
67 expect(otpRequest.uri).to.exist
68 expectStartWith(otpRequest.uri, 'otpauth://')
69 expect(otpRequest.uri).to.include(otpRequest.secret)
70
71 requestToken = otpRequest.requestToken
72 otpSecret = otpRequest.secret
73 })
74
75 it('Should not have two factor confirmed yet', async function () {
76 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
77 expect(twoFactorEnabled).to.be.false
78 })
79
80 it('Should confirm two factor', async function () {
81 await server.twoFactor.confirmRequest({
82 userId,
83 token: userToken,
84 otpToken: TwoFactorCommand.buildOTP({ secret: otpSecret }).generate(),
85 requestToken
86 })
87 })
88
89 it('Should not add the header on login if two factor is enabled and password is incorrect', async function () {
90 const { res, token } = await login({ server, username: userUsername, password: 'fake', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
91
92 expect(res.header['x-peertube-otp']).to.not.exist
93 expect(token).to.not.exist
94 })
95
96 it('Should add the header on login if two factor is enabled and password is correct', async function () {
97 const { res, token } = await login({
98 server,
99 username: userUsername,
100 password: userPassword,
101 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
102 })
103
104 expect(res.header['x-peertube-otp']).to.exist
105 expect(token).to.not.exist
106
107 await server.users.getMyInfo({ token })
108 })
109
110 it('Should not login with correct password and incorrect otp secret', async function () {
111 const otp = TwoFactorCommand.buildOTP({ secret: 'a'.repeat(32) })
112
113 const { res, token } = await login({
114 server,
115 username: userUsername,
116 password: userPassword,
117 otpToken: otp.generate(),
118 expectedStatus: HttpStatusCode.BAD_REQUEST_400
119 })
120
121 expect(res.header['x-peertube-otp']).to.not.exist
122 expect(token).to.not.exist
123 })
124
125 it('Should not login with correct password and incorrect otp code', async function () {
126 const { res, token } = await login({
127 server,
128 username: userUsername,
129 password: userPassword,
130 otpToken: '123456',
131 expectedStatus: HttpStatusCode.BAD_REQUEST_400
132 })
133
134 expect(res.header['x-peertube-otp']).to.not.exist
135 expect(token).to.not.exist
136 })
137
138 it('Should not login with incorrect password and correct otp code', async function () {
139 const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
140
141 const { res, token } = await login({
142 server,
143 username: userUsername,
144 password: 'fake',
145 otpToken,
146 expectedStatus: HttpStatusCode.BAD_REQUEST_400
147 })
148
149 expect(res.header['x-peertube-otp']).to.not.exist
150 expect(token).to.not.exist
151 })
152
153 it('Should correctly login with correct password and otp code', async function () {
154 const otpToken = TwoFactorCommand.buildOTP({ secret: otpSecret }).generate()
155
156 const { res, token } = await login({ server, username: userUsername, password: userPassword, otpToken })
157
158 expect(res.header['x-peertube-otp']).to.not.exist
159 expect(token).to.exist
160
161 await server.users.getMyInfo({ token })
162 })
163
164 it('Should have two factor enabled when getting my info', async function () {
165 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
166 expect(twoFactorEnabled).to.be.true
167 })
168
169 it('Should disable two factor and be able to login without otp token', async function () {
170 await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword })
171
172 const { res, token } = await login({ server, username: userUsername, password: userPassword })
173 expect(res.header['x-peertube-otp']).to.not.exist
174
175 await server.users.getMyInfo({ token })
176 })
177
178 it('Should have two factor disabled when getting my info', async function () {
179 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
180 expect(twoFactorEnabled).to.be.false
181 })
182
183 it('Should enable two factor auth without password from an admin', async function () {
184 const { otpRequest } = await server.twoFactor.request({ userId })
185
186 await server.twoFactor.confirmRequest({
187 userId,
188 otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate(),
189 requestToken: otpRequest.requestToken
190 })
191
192 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
193 expect(twoFactorEnabled).to.be.true
194 })
195
196 it('Should disable two factor auth without password from an admin', async function () {
197 await server.twoFactor.disable({ userId })
198
199 const { twoFactorEnabled } = await server.users.getMyInfo({ token: userToken })
200 expect(twoFactorEnabled).to.be.false
201 })
202
203 after(async function () {
204 await cleanupTests([ server ])
205 })
206})
diff --git a/packages/tests/src/api/users/user-subscriptions.ts b/packages/tests/src/api/users/user-subscriptions.ts
new file mode 100644
index 000000000..eb4ea9539
--- /dev/null
+++ b/packages/tests/src/api/users/user-subscriptions.ts
@@ -0,0 +1,614 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { VideoPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultAccountAvatar,
12 setDefaultChannelAvatar,
13 SubscriptionsCommand,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test users subscriptions', function () {
18 let servers: PeerTubeServer[] = []
19 const users: { accessToken: string }[] = []
20 let video3UUID: string
21
22 let command: SubscriptionsCommand
23
24 before(async function () {
25 this.timeout(240000)
26
27 servers = await createMultipleServers(3)
28
29 // Get the access tokens
30 await setAccessTokensToServers(servers)
31 await setDefaultChannelAvatar(servers)
32 await setDefaultAccountAvatar(servers)
33
34 // Server 1 and server 2 follow each other
35 await doubleFollow(servers[0], servers[1])
36
37 for (const server of servers) {
38 const user = { username: 'user' + server.serverNumber, password: 'password' }
39 await server.users.create({ username: user.username, password: user.password })
40
41 const accessToken = await server.login.getAccessToken(user)
42 users.push({ accessToken })
43
44 const videoName1 = 'video 1-' + server.serverNumber
45 await server.videos.upload({ token: accessToken, attributes: { name: videoName1 } })
46
47 const videoName2 = 'video 2-' + server.serverNumber
48 await server.videos.upload({ token: accessToken, attributes: { name: videoName2 } })
49 }
50
51 await waitJobs(servers)
52
53 command = servers[0].subscriptions
54 })
55
56 describe('Destinction between server videos and user videos', function () {
57 it('Should display videos of server 2 on server 1', async function () {
58 const { total } = await servers[0].videos.list()
59
60 expect(total).to.equal(4)
61 })
62
63 it('User of server 1 should follow user of server 3 and root of server 1', async function () {
64 this.timeout(60000)
65
66 await command.add({ token: users[0].accessToken, targetUri: 'user3_channel@' + servers[2].host })
67 await command.add({ token: users[0].accessToken, targetUri: 'root_channel@' + servers[0].host })
68
69 await waitJobs(servers)
70
71 const attributes = { name: 'video server 3 added after follow' }
72 const { uuid } = await servers[2].videos.upload({ token: users[2].accessToken, attributes })
73 video3UUID = uuid
74
75 await waitJobs(servers)
76 })
77
78 it('Should not display videos of server 3 on server 1', async function () {
79 const { total, data } = await servers[0].videos.list()
80 expect(total).to.equal(4)
81
82 for (const video of data) {
83 expect(video.name).to.not.contain('1-3')
84 expect(video.name).to.not.contain('2-3')
85 expect(video.name).to.not.contain('video server 3 added after follow')
86 }
87 })
88 })
89
90 describe('Subscription endpoints', function () {
91
92 it('Should list subscriptions', async function () {
93 {
94 const body = await command.list()
95 expect(body.total).to.equal(0)
96 expect(body.data).to.be.an('array')
97 expect(body.data).to.have.lengthOf(0)
98 }
99
100 {
101 const body = await command.list({ token: users[0].accessToken, sort: 'createdAt' })
102 expect(body.total).to.equal(2)
103
104 const subscriptions = body.data
105 expect(subscriptions).to.be.an('array')
106 expect(subscriptions).to.have.lengthOf(2)
107
108 expect(subscriptions[0].name).to.equal('user3_channel')
109 expect(subscriptions[1].name).to.equal('root_channel')
110 }
111 })
112
113 it('Should get subscription', async function () {
114 {
115 const videoChannel = await command.get({ token: users[0].accessToken, uri: 'user3_channel@' + servers[2].host })
116
117 expect(videoChannel.name).to.equal('user3_channel')
118 expect(videoChannel.host).to.equal(servers[2].host)
119 expect(videoChannel.displayName).to.equal('Main user3 channel')
120 expect(videoChannel.followingCount).to.equal(0)
121 expect(videoChannel.followersCount).to.equal(1)
122 }
123
124 {
125 const videoChannel = await command.get({ token: users[0].accessToken, uri: 'root_channel@' + servers[0].host })
126
127 expect(videoChannel.name).to.equal('root_channel')
128 expect(videoChannel.host).to.equal(servers[0].host)
129 expect(videoChannel.displayName).to.equal('Main root channel')
130 expect(videoChannel.followingCount).to.equal(0)
131 expect(videoChannel.followersCount).to.equal(1)
132 }
133 })
134
135 it('Should return the existing subscriptions', async function () {
136 const uris = [
137 'user3_channel@' + servers[2].host,
138 'root2_channel@' + servers[0].host,
139 'root_channel@' + servers[0].host,
140 'user3_channel@' + servers[0].host
141 ]
142
143 const body = await command.exist({ token: users[0].accessToken, uris })
144
145 expect(body['user3_channel@' + servers[2].host]).to.be.true
146 expect(body['root2_channel@' + servers[0].host]).to.be.false
147 expect(body['root_channel@' + servers[0].host]).to.be.true
148 expect(body['user3_channel@' + servers[0].host]).to.be.false
149 })
150
151 it('Should search among subscriptions', async function () {
152 {
153 const body = await command.list({ token: users[0].accessToken, sort: '-createdAt', search: 'user3_channel' })
154 expect(body.total).to.equal(1)
155 expect(body.data).to.have.lengthOf(1)
156 }
157
158 {
159 const body = await command.list({ token: users[0].accessToken, sort: '-createdAt', search: 'toto' })
160 expect(body.total).to.equal(0)
161 expect(body.data).to.have.lengthOf(0)
162 }
163 })
164 })
165
166 describe('Subscription videos', function () {
167
168 it('Should list subscription videos', async function () {
169 {
170 const body = await servers[0].videos.listMySubscriptionVideos()
171 expect(body.total).to.equal(0)
172 expect(body.data).to.be.an('array')
173 expect(body.data).to.have.lengthOf(0)
174 }
175
176 {
177 const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' })
178 expect(body.total).to.equal(3)
179
180 const videos = body.data
181 expect(videos).to.be.an('array')
182 expect(videos).to.have.lengthOf(3)
183
184 expect(videos[0].name).to.equal('video 1-3')
185 expect(videos[1].name).to.equal('video 2-3')
186 expect(videos[2].name).to.equal('video server 3 added after follow')
187 }
188
189 {
190 const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, count: 1, start: 1 })
191 expect(body.total).to.equal(3)
192
193 const videos = body.data
194 expect(videos).to.be.an('array')
195 expect(videos).to.have.lengthOf(1)
196
197 expect(videos[0].name).to.equal('video 2-3')
198 }
199 })
200
201 it('Should upload a video by root on server 1 and see it in the subscription videos', async function () {
202 this.timeout(60000)
203
204 const videoName = 'video server 1 added after follow'
205 await servers[0].videos.upload({ attributes: { name: videoName } })
206
207 await waitJobs(servers)
208
209 {
210 const body = await servers[0].videos.listMySubscriptionVideos()
211 expect(body.total).to.equal(0)
212 expect(body.data).to.be.an('array')
213 expect(body.data).to.have.lengthOf(0)
214 }
215
216 {
217 const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' })
218 expect(body.total).to.equal(4)
219
220 const videos = body.data
221 expect(videos).to.be.an('array')
222 expect(videos).to.have.lengthOf(4)
223
224 expect(videos[0].name).to.equal('video 1-3')
225 expect(videos[1].name).to.equal('video 2-3')
226 expect(videos[2].name).to.equal('video server 3 added after follow')
227 expect(videos[3].name).to.equal('video server 1 added after follow')
228 }
229
230 {
231 const { data, total } = await servers[0].videos.list()
232 expect(total).to.equal(5)
233
234 for (const video of data) {
235 expect(video.name).to.not.contain('1-3')
236 expect(video.name).to.not.contain('2-3')
237 expect(video.name).to.not.contain('video server 3 added after follow')
238 }
239 }
240 })
241
242 it('Should have server 1 following server 3 and display server 3 videos', async function () {
243 this.timeout(60000)
244
245 await servers[0].follows.follow({ hosts: [ servers[2].url ] })
246
247 await waitJobs(servers)
248
249 const { data, total } = await servers[0].videos.list()
250 expect(total).to.equal(8)
251
252 const names = [ '1-3', '2-3', 'video server 3 added after follow' ]
253 for (const name of names) {
254 const video = data.find(v => v.name.includes(name))
255 expect(video).to.not.be.undefined
256 }
257 })
258
259 it('Should remove follow server 1 -> server 3 and hide server 3 videos', async function () {
260 this.timeout(60000)
261
262 await servers[0].follows.unfollow({ target: servers[2] })
263
264 await waitJobs(servers)
265
266 const { total, data } = await servers[0].videos.list()
267 expect(total).to.equal(5)
268
269 for (const video of data) {
270 expect(video.name).to.not.contain('1-3')
271 expect(video.name).to.not.contain('2-3')
272 expect(video.name).to.not.contain('video server 3 added after follow')
273 }
274 })
275
276 it('Should still list subscription videos', async function () {
277 {
278 const body = await servers[0].videos.listMySubscriptionVideos()
279 expect(body.total).to.equal(0)
280 expect(body.data).to.be.an('array')
281 expect(body.data).to.have.lengthOf(0)
282 }
283
284 {
285 const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' })
286 expect(body.total).to.equal(4)
287
288 const videos = body.data
289 expect(videos).to.be.an('array')
290 expect(videos).to.have.lengthOf(4)
291
292 expect(videos[0].name).to.equal('video 1-3')
293 expect(videos[1].name).to.equal('video 2-3')
294 expect(videos[2].name).to.equal('video server 3 added after follow')
295 expect(videos[3].name).to.equal('video server 1 added after follow')
296 }
297 })
298 })
299
300 describe('Existing subscription video update', function () {
301
302 it('Should update a video of server 3 and see the updated video on server 1', async function () {
303 this.timeout(30000)
304
305 await servers[2].videos.update({ id: video3UUID, attributes: { name: 'video server 3 added after follow updated' } })
306
307 await waitJobs(servers)
308
309 const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' })
310 expect(body.data[2].name).to.equal('video server 3 added after follow updated')
311 })
312 })
313
314 describe('Subscription removal', function () {
315
316 it('Should remove user of server 3 subscription', async function () {
317 this.timeout(30000)
318
319 await command.remove({ token: users[0].accessToken, uri: 'user3_channel@' + servers[2].host })
320
321 await waitJobs(servers)
322 })
323
324 it('Should not display its videos anymore', async function () {
325 const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' })
326 expect(body.total).to.equal(1)
327
328 const videos = body.data
329 expect(videos).to.be.an('array')
330 expect(videos).to.have.lengthOf(1)
331
332 expect(videos[0].name).to.equal('video server 1 added after follow')
333 })
334
335 it('Should remove the root subscription and not display the videos anymore', async function () {
336 this.timeout(30000)
337
338 await command.remove({ token: users[0].accessToken, uri: 'root_channel@' + servers[0].host })
339
340 await waitJobs(servers)
341
342 {
343 const body = await command.list({ token: users[0].accessToken, sort: 'createdAt' })
344 expect(body.total).to.equal(0)
345
346 const videos = body.data
347 expect(videos).to.be.an('array')
348 expect(videos).to.have.lengthOf(0)
349 }
350 })
351
352 it('Should correctly display public videos on server 1', async function () {
353 const { total, data } = await servers[0].videos.list()
354 expect(total).to.equal(5)
355
356 for (const video of data) {
357 expect(video.name).to.not.contain('1-3')
358 expect(video.name).to.not.contain('2-3')
359 expect(video.name).to.not.contain('video server 3 added after follow updated')
360 }
361 })
362 })
363
364 describe('Re-follow', function () {
365
366 it('Should follow user of server 3 again', async function () {
367 this.timeout(60000)
368
369 await command.add({ token: users[0].accessToken, targetUri: 'user3_channel@' + servers[2].host })
370
371 await waitJobs(servers)
372
373 {
374 const body = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken, sort: 'createdAt' })
375 expect(body.total).to.equal(3)
376
377 const videos = body.data
378 expect(videos).to.be.an('array')
379 expect(videos).to.have.lengthOf(3)
380
381 expect(videos[0].name).to.equal('video 1-3')
382 expect(videos[1].name).to.equal('video 2-3')
383 expect(videos[2].name).to.equal('video server 3 added after follow updated')
384 }
385
386 {
387 const { total, data } = await servers[0].videos.list()
388 expect(total).to.equal(5)
389
390 for (const video of data) {
391 expect(video.name).to.not.contain('1-3')
392 expect(video.name).to.not.contain('2-3')
393 expect(video.name).to.not.contain('video server 3 added after follow updated')
394 }
395 }
396 })
397
398 it('Should follow user channels of server 3 by root of server 3', async function () {
399 this.timeout(60000)
400
401 await servers[2].channels.create({ token: users[2].accessToken, attributes: { name: 'user3_channel2' } })
402
403 await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel@' + servers[2].host })
404 await servers[2].subscriptions.add({ token: servers[2].accessToken, targetUri: 'user3_channel2@' + servers[2].host })
405
406 await waitJobs(servers)
407 })
408 })
409
410 describe('Followers listing', function () {
411
412 it('Should list user 3 followers', async function () {
413 {
414 const { total, data } = await servers[2].accounts.listFollowers({
415 token: users[2].accessToken,
416 accountName: 'user3',
417 start: 0,
418 count: 5,
419 sort: 'createdAt'
420 })
421
422 expect(total).to.equal(3)
423 expect(data).to.have.lengthOf(3)
424
425 expect(data[0].following.host).to.equal(servers[2].host)
426 expect(data[0].following.name).to.equal('user3_channel')
427 expect(data[0].follower.host).to.equal(servers[0].host)
428 expect(data[0].follower.name).to.equal('user1')
429
430 expect(data[1].following.host).to.equal(servers[2].host)
431 expect(data[1].following.name).to.equal('user3_channel')
432 expect(data[1].follower.host).to.equal(servers[2].host)
433 expect(data[1].follower.name).to.equal('root')
434
435 expect(data[2].following.host).to.equal(servers[2].host)
436 expect(data[2].following.name).to.equal('user3_channel2')
437 expect(data[2].follower.host).to.equal(servers[2].host)
438 expect(data[2].follower.name).to.equal('root')
439 }
440
441 {
442 const { total, data } = await servers[2].accounts.listFollowers({
443 token: users[2].accessToken,
444 accountName: 'user3',
445 start: 0,
446 count: 1,
447 sort: '-createdAt'
448 })
449
450 expect(total).to.equal(3)
451 expect(data).to.have.lengthOf(1)
452
453 expect(data[0].following.host).to.equal(servers[2].host)
454 expect(data[0].following.name).to.equal('user3_channel2')
455 expect(data[0].follower.host).to.equal(servers[2].host)
456 expect(data[0].follower.name).to.equal('root')
457 }
458
459 {
460 const { total, data } = await servers[2].accounts.listFollowers({
461 token: users[2].accessToken,
462 accountName: 'user3',
463 start: 1,
464 count: 1,
465 sort: '-createdAt'
466 })
467
468 expect(total).to.equal(3)
469 expect(data).to.have.lengthOf(1)
470
471 expect(data[0].following.host).to.equal(servers[2].host)
472 expect(data[0].following.name).to.equal('user3_channel')
473 expect(data[0].follower.host).to.equal(servers[2].host)
474 expect(data[0].follower.name).to.equal('root')
475 }
476
477 {
478 const { total, data } = await servers[2].accounts.listFollowers({
479 token: users[2].accessToken,
480 accountName: 'user3',
481 search: 'user1',
482 sort: '-createdAt'
483 })
484
485 expect(total).to.equal(1)
486 expect(data).to.have.lengthOf(1)
487
488 expect(data[0].following.host).to.equal(servers[2].host)
489 expect(data[0].following.name).to.equal('user3_channel')
490 expect(data[0].follower.host).to.equal(servers[0].host)
491 expect(data[0].follower.name).to.equal('user1')
492 }
493 })
494
495 it('Should list user3_channel followers', async function () {
496 {
497 const { total, data } = await servers[2].channels.listFollowers({
498 token: users[2].accessToken,
499 channelName: 'user3_channel',
500 start: 0,
501 count: 5,
502 sort: 'createdAt'
503 })
504
505 expect(total).to.equal(2)
506 expect(data).to.have.lengthOf(2)
507
508 expect(data[0].following.host).to.equal(servers[2].host)
509 expect(data[0].following.name).to.equal('user3_channel')
510 expect(data[0].follower.host).to.equal(servers[0].host)
511 expect(data[0].follower.name).to.equal('user1')
512
513 expect(data[1].following.host).to.equal(servers[2].host)
514 expect(data[1].following.name).to.equal('user3_channel')
515 expect(data[1].follower.host).to.equal(servers[2].host)
516 expect(data[1].follower.name).to.equal('root')
517 }
518
519 {
520 const { total, data } = await servers[2].channels.listFollowers({
521 token: users[2].accessToken,
522 channelName: 'user3_channel',
523 start: 0,
524 count: 1,
525 sort: '-createdAt'
526 })
527
528 expect(total).to.equal(2)
529 expect(data).to.have.lengthOf(1)
530
531 expect(data[0].following.host).to.equal(servers[2].host)
532 expect(data[0].following.name).to.equal('user3_channel')
533 expect(data[0].follower.host).to.equal(servers[2].host)
534 expect(data[0].follower.name).to.equal('root')
535 }
536
537 {
538 const { total, data } = await servers[2].channels.listFollowers({
539 token: users[2].accessToken,
540 channelName: 'user3_channel',
541 start: 1,
542 count: 1,
543 sort: '-createdAt'
544 })
545
546 expect(total).to.equal(2)
547 expect(data).to.have.lengthOf(1)
548
549 expect(data[0].following.host).to.equal(servers[2].host)
550 expect(data[0].following.name).to.equal('user3_channel')
551 expect(data[0].follower.host).to.equal(servers[0].host)
552 expect(data[0].follower.name).to.equal('user1')
553 }
554
555 {
556 const { total, data } = await servers[2].channels.listFollowers({
557 token: users[2].accessToken,
558 channelName: 'user3_channel',
559 search: 'user1',
560 sort: '-createdAt'
561 })
562
563 expect(total).to.equal(1)
564 expect(data).to.have.lengthOf(1)
565
566 expect(data[0].following.host).to.equal(servers[2].host)
567 expect(data[0].following.name).to.equal('user3_channel')
568 expect(data[0].follower.host).to.equal(servers[0].host)
569 expect(data[0].follower.name).to.equal('user1')
570 }
571 })
572 })
573
574 describe('Subscription videos privacy', function () {
575
576 it('Should update video as internal and not see from remote server', async function () {
577 this.timeout(30000)
578
579 await servers[2].videos.update({ id: video3UUID, attributes: { name: 'internal', privacy: VideoPrivacy.INTERNAL } })
580 await waitJobs(servers)
581
582 {
583 const { data } = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken })
584 expect(data.find(v => v.name === 'internal')).to.not.exist
585 }
586 })
587
588 it('Should see internal from local user', async function () {
589 const { data } = await servers[2].videos.listMySubscriptionVideos({ token: servers[2].accessToken })
590 expect(data.find(v => v.name === 'internal')).to.exist
591 })
592
593 it('Should update video as private and not see from anyone server', async function () {
594 this.timeout(30000)
595
596 await servers[2].videos.update({ id: video3UUID, attributes: { name: 'private', privacy: VideoPrivacy.PRIVATE } })
597 await waitJobs(servers)
598
599 {
600 const { data } = await servers[0].videos.listMySubscriptionVideos({ token: users[0].accessToken })
601 expect(data.find(v => v.name === 'private')).to.not.exist
602 }
603
604 {
605 const { data } = await servers[2].videos.listMySubscriptionVideos({ token: servers[2].accessToken })
606 expect(data.find(v => v.name === 'private')).to.not.exist
607 }
608 })
609 })
610
611 after(async function () {
612 await cleanupTests(servers)
613 })
614})
diff --git a/packages/tests/src/api/users/user-videos.ts b/packages/tests/src/api/users/user-videos.ts
new file mode 100644
index 000000000..7b075d040
--- /dev/null
+++ b/packages/tests/src/api/users/user-videos.ts
@@ -0,0 +1,219 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultAccountAvatar,
11 setDefaultChannelAvatar,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test user videos', function () {
16 let server: PeerTubeServer
17 let videoId: number
18 let videoId2: number
19 let token: string
20 let anotherUserToken: string
21
22 before(async function () {
23 this.timeout(120000)
24
25 server = await createSingleServer(1)
26
27 await setAccessTokensToServers([ server ])
28 await setDefaultChannelAvatar([ server ])
29 await setDefaultAccountAvatar([ server ])
30
31 await server.videos.quickUpload({ name: 'root video' })
32 await server.videos.quickUpload({ name: 'root video 2' })
33
34 token = await server.users.generateUserAndToken('user')
35 anotherUserToken = await server.users.generateUserAndToken('user2')
36 })
37
38 describe('List my videos', function () {
39
40 it('Should list my videos', async function () {
41 const { data, total } = await server.videos.listMyVideos()
42
43 expect(total).to.equal(2)
44 expect(data).to.have.lengthOf(2)
45 })
46 })
47
48 describe('Upload', function () {
49
50 it('Should upload the video with the correct token', async function () {
51 await server.videos.upload({ token })
52 const { data } = await server.videos.list()
53 const video = data[0]
54
55 expect(video.account.name).to.equal('user')
56 videoId = video.id
57 })
58
59 it('Should upload the video again with the correct token', async function () {
60 const { id } = await server.videos.upload({ token })
61 videoId2 = id
62 })
63 })
64
65 describe('Ratings', function () {
66
67 it('Should retrieve a video rating', async function () {
68 await server.videos.rate({ id: videoId, token, rating: 'like' })
69 const rating = await server.users.getMyRating({ token, videoId })
70
71 expect(rating.videoId).to.equal(videoId)
72 expect(rating.rating).to.equal('like')
73 })
74
75 it('Should retrieve ratings list', async function () {
76 await server.videos.rate({ id: videoId, token, rating: 'like' })
77
78 const body = await server.accounts.listRatings({ accountName: 'user', token })
79
80 expect(body.total).to.equal(1)
81 expect(body.data[0].video.id).to.equal(videoId)
82 expect(body.data[0].rating).to.equal('like')
83 })
84
85 it('Should retrieve ratings list by rating type', async function () {
86 {
87 const body = await server.accounts.listRatings({ accountName: 'user', token, rating: 'like' })
88 expect(body.data.length).to.equal(1)
89 }
90
91 {
92 const body = await server.accounts.listRatings({ accountName: 'user', token, rating: 'dislike' })
93 expect(body.data.length).to.equal(0)
94 }
95 })
96 })
97
98 describe('Remove video', function () {
99
100 it('Should not be able to remove the video with an incorrect token', async function () {
101 await server.videos.remove({ token: 'bad_token', id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
102 })
103
104 it('Should not be able to remove the video with the token of another account', async function () {
105 await server.videos.remove({ token: anotherUserToken, id: videoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
106 })
107
108 it('Should be able to remove the video with the correct token', async function () {
109 await server.videos.remove({ token, id: videoId })
110 await server.videos.remove({ token, id: videoId2 })
111 })
112 })
113
114 describe('My videos & quotas', function () {
115
116 it('Should be able to upload a video with a user', async function () {
117 this.timeout(30000)
118
119 const attributes = {
120 name: 'super user video',
121 fixture: 'video_short.webm'
122 }
123 await server.videos.upload({ token, attributes })
124
125 await server.channels.create({ token, attributes: { name: 'other_channel' } })
126 })
127
128 it('Should have video quota updated', async function () {
129 const quota = await server.users.getMyQuotaUsed({ token })
130 expect(quota.videoQuotaUsed).to.equal(218910)
131 expect(quota.videoQuotaUsedDaily).to.equal(218910)
132
133 const { data } = await server.users.list()
134 const tmpUser = data.find(u => u.username === 'user')
135 expect(tmpUser.videoQuotaUsed).to.equal(218910)
136 expect(tmpUser.videoQuotaUsedDaily).to.equal(218910)
137 })
138
139 it('Should be able to list my videos', async function () {
140 const { total, data } = await server.videos.listMyVideos({ token })
141 expect(total).to.equal(1)
142 expect(data).to.have.lengthOf(1)
143
144 const video = data[0]
145 expect(video.name).to.equal('super user video')
146 expect(video.thumbnailPath).to.not.be.null
147 expect(video.previewPath).to.not.be.null
148 })
149
150 it('Should be able to filter by channel in my videos', async function () {
151 const myInfo = await server.users.getMyInfo({ token })
152 const mainChannel = myInfo.videoChannels.find(c => c.name !== 'other_channel')
153 const otherChannel = myInfo.videoChannels.find(c => c.name === 'other_channel')
154
155 {
156 const { total, data } = await server.videos.listMyVideos({ token, channelId: mainChannel.id })
157 expect(total).to.equal(1)
158 expect(data).to.have.lengthOf(1)
159
160 const video = data[0]
161 expect(video.name).to.equal('super user video')
162 expect(video.thumbnailPath).to.not.be.null
163 expect(video.previewPath).to.not.be.null
164 }
165
166 {
167 const { total, data } = await server.videos.listMyVideos({ token, channelId: otherChannel.id })
168 expect(total).to.equal(0)
169 expect(data).to.have.lengthOf(0)
170 }
171 })
172
173 it('Should be able to search in my videos', async function () {
174 {
175 const { total, data } = await server.videos.listMyVideos({ token, sort: '-createdAt', search: 'user video' })
176 expect(total).to.equal(1)
177 expect(data).to.have.lengthOf(1)
178 }
179
180 {
181 const { total, data } = await server.videos.listMyVideos({ token, sort: '-createdAt', search: 'toto' })
182 expect(total).to.equal(0)
183 expect(data).to.have.lengthOf(0)
184 }
185 })
186
187 it('Should disable web videos, enable HLS, and update my quota', async function () {
188 this.timeout(160000)
189
190 {
191 const config = await server.config.getCustomConfig()
192 config.transcoding.webVideos.enabled = false
193 config.transcoding.hls.enabled = true
194 config.transcoding.enabled = true
195 await server.config.updateCustomSubConfig({ newConfig: config })
196 }
197
198 {
199 const attributes = {
200 name: 'super user video 2',
201 fixture: 'video_short.webm'
202 }
203 await server.videos.upload({ token, attributes })
204
205 await waitJobs([ server ])
206 }
207
208 {
209 const data = await server.users.getMyQuotaUsed({ token })
210 expect(data.videoQuotaUsed).to.be.greaterThan(220000)
211 expect(data.videoQuotaUsedDaily).to.be.greaterThan(220000)
212 }
213 })
214 })
215
216 after(async function () {
217 await cleanupTests([ server ])
218 })
219})
diff --git a/packages/tests/src/api/users/users-email-verification.ts b/packages/tests/src/api/users/users-email-verification.ts
new file mode 100644
index 000000000..689e3c4bb
--- /dev/null
+++ b/packages/tests/src/api/users/users-email-verification.ts
@@ -0,0 +1,165 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { HttpStatusCode } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 ConfigCommand,
9 createSingleServer,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test users email verification', function () {
16 let server: PeerTubeServer
17 let userId: number
18 let userAccessToken: string
19 let verificationString: string
20 let expectedEmailsLength = 0
21 const user1 = {
22 username: 'user_1',
23 password: 'super password'
24 }
25 const user2 = {
26 username: 'user_2',
27 password: 'super password'
28 }
29 const emails: object[] = []
30
31 before(async function () {
32 this.timeout(30000)
33
34 const port = await MockSmtpServer.Instance.collectEmails(emails)
35 server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(port))
36
37 await setAccessTokensToServers([ server ])
38 })
39
40 it('Should register user and send verification email if verification required', async function () {
41 this.timeout(30000)
42
43 await server.config.updateExistingSubConfig({
44 newConfig: {
45 signup: {
46 enabled: true,
47 requiresApproval: false,
48 requiresEmailVerification: true,
49 limit: 10
50 }
51 }
52 })
53
54 await server.registrations.register(user1)
55
56 await waitJobs(server)
57 expectedEmailsLength++
58 expect(emails).to.have.lengthOf(expectedEmailsLength)
59
60 const email = emails[expectedEmailsLength - 1]
61
62 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
63 expect(verificationStringMatches).not.to.be.null
64
65 verificationString = verificationStringMatches[1]
66 expect(verificationString).to.have.length.above(2)
67
68 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
69 expect(userIdMatches).not.to.be.null
70
71 userId = parseInt(userIdMatches[1], 10)
72
73 const body = await server.users.get({ userId })
74 expect(body.emailVerified).to.be.false
75 })
76
77 it('Should not allow login for user with unverified email', async function () {
78 const { detail } = await server.login.login({ user: user1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
79 expect(detail).to.contain('User email is not verified.')
80 })
81
82 it('Should verify the user via email and allow login', async function () {
83 await server.users.verifyEmail({ userId, verificationString })
84
85 const body = await server.login.login({ user: user1 })
86 userAccessToken = body.access_token
87
88 const user = await server.users.get({ userId })
89 expect(user.emailVerified).to.be.true
90 })
91
92 it('Should be able to change the user email', async function () {
93 let updateVerificationString: string
94
95 {
96 await server.users.updateMe({
97 token: userAccessToken,
98 email: 'updated@example.com',
99 currentPassword: user1.password
100 })
101
102 await waitJobs(server)
103 expectedEmailsLength++
104 expect(emails).to.have.lengthOf(expectedEmailsLength)
105
106 const email = emails[expectedEmailsLength - 1]
107
108 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
109 updateVerificationString = verificationStringMatches[1]
110 }
111
112 {
113 const me = await server.users.getMyInfo({ token: userAccessToken })
114 expect(me.email).to.equal('user_1@example.com')
115 expect(me.pendingEmail).to.equal('updated@example.com')
116 }
117
118 {
119 await server.users.verifyEmail({ userId, verificationString: updateVerificationString, isPendingEmail: true })
120
121 const me = await server.users.getMyInfo({ token: userAccessToken })
122 expect(me.email).to.equal('updated@example.com')
123 expect(me.pendingEmail).to.be.null
124 }
125 })
126
127 it('Should register user not requiring email verification if setting not enabled', async function () {
128 this.timeout(5000)
129 await server.config.updateExistingSubConfig({
130 newConfig: {
131 signup: {
132 requiresEmailVerification: false
133 }
134 }
135 })
136
137 await server.registrations.register(user2)
138
139 await waitJobs(server)
140 expect(emails).to.have.lengthOf(expectedEmailsLength)
141
142 const accessToken = await server.login.getAccessToken(user2)
143
144 const user = await server.users.getMyInfo({ token: accessToken })
145 expect(user.emailVerified).to.be.null
146 })
147
148 it('Should allow login for user with unverified email when setting later enabled', async function () {
149 await server.config.updateCustomSubConfig({
150 newConfig: {
151 signup: {
152 requiresEmailVerification: true
153 }
154 }
155 })
156
157 await server.login.getAccessToken(user2)
158 })
159
160 after(async function () {
161 MockSmtpServer.Instance.kill()
162
163 await cleanupTests([ server ])
164 })
165})
diff --git a/packages/tests/src/api/users/users-multiple-servers.ts b/packages/tests/src/api/users/users-multiple-servers.ts
new file mode 100644
index 000000000..61e3aa001
--- /dev/null
+++ b/packages/tests/src/api/users/users-multiple-servers.ts
@@ -0,0 +1,213 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MyUser } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultChannelAvatar,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14import { checkActorFilesWereRemoved } from '@tests/shared/actors.js'
15import { testImage } from '@tests/shared/checks.js'
16import { checkTmpIsEmpty } from '@tests/shared/directories.js'
17import { saveVideoInServers, checkVideoFilesWereRemoved } from '@tests/shared/videos.js'
18
19describe('Test users with multiple servers', function () {
20 let servers: PeerTubeServer[] = []
21
22 let user: MyUser
23 let userId: number
24
25 let videoUUID: string
26 let userAccessToken: string
27 let userAvatarFilenames: string[]
28
29 before(async function () {
30 this.timeout(120_000)
31
32 servers = await createMultipleServers(3)
33
34 // Get the access tokens
35 await setAccessTokensToServers(servers)
36 await setDefaultChannelAvatar(servers)
37
38 // Server 1 and server 2 follow each other
39 await doubleFollow(servers[0], servers[1])
40 // Server 1 and server 3 follow each other
41 await doubleFollow(servers[0], servers[2])
42 // Server 2 and server 3 follow each other
43 await doubleFollow(servers[1], servers[2])
44
45 // The root user of server 1 is propagated to servers 2 and 3
46 await servers[0].videos.upload()
47
48 {
49 const username = 'user1'
50 const created = await servers[0].users.create({ username })
51 userId = created.id
52 userAccessToken = await servers[0].login.getAccessToken(username)
53 }
54
55 {
56 const { uuid } = await servers[0].videos.upload({ token: userAccessToken })
57 videoUUID = uuid
58
59 await waitJobs(servers)
60
61 await saveVideoInServers(servers, videoUUID)
62 }
63 })
64
65 it('Should be able to update my display name', async function () {
66 await servers[0].users.updateMe({ displayName: 'my super display name' })
67
68 user = await servers[0].users.getMyInfo()
69 expect(user.account.displayName).to.equal('my super display name')
70
71 await waitJobs(servers)
72 })
73
74 it('Should be able to update my description', async function () {
75 this.timeout(10_000)
76
77 await servers[0].users.updateMe({ description: 'my super description updated' })
78
79 user = await servers[0].users.getMyInfo()
80 expect(user.account.displayName).to.equal('my super display name')
81 expect(user.account.description).to.equal('my super description updated')
82
83 await waitJobs(servers)
84 })
85
86 it('Should be able to update my avatar', async function () {
87 this.timeout(10_000)
88
89 const fixture = 'avatar2.png'
90
91 await servers[0].users.updateMyAvatar({ fixture })
92
93 user = await servers[0].users.getMyInfo()
94 userAvatarFilenames = user.account.avatars.map(({ path }) => path)
95
96 for (const avatar of user.account.avatars) {
97 await testImage(servers[0].url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png')
98 }
99
100 await waitJobs(servers)
101 })
102
103 it('Should have updated my profile on other servers too', async function () {
104 let createdAt: string | Date
105
106 for (const server of servers) {
107 const body = await server.accounts.list({ sort: '-createdAt' })
108
109 const resList = body.data.find(a => a.name === 'root' && a.host === servers[0].host)
110 expect(resList).not.to.be.undefined
111
112 const account = await server.accounts.get({ accountName: resList.name + '@' + resList.host })
113
114 if (!createdAt) createdAt = account.createdAt
115
116 expect(account.name).to.equal('root')
117 expect(account.host).to.equal(servers[0].host)
118 expect(account.displayName).to.equal('my super display name')
119 expect(account.description).to.equal('my super description updated')
120 expect(createdAt).to.equal(account.createdAt)
121
122 if (server.serverNumber === 1) {
123 expect(account.userId).to.be.a('number')
124 } else {
125 expect(account.userId).to.be.undefined
126 }
127
128 for (const avatar of account.avatars) {
129 await testImage(server.url, `avatar2-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png')
130 }
131 }
132 })
133
134 it('Should list account videos', async function () {
135 for (const server of servers) {
136 const { total, data } = await server.videos.listByAccount({ handle: 'user1@' + servers[0].host })
137
138 expect(total).to.equal(1)
139 expect(data).to.be.an('array')
140 expect(data).to.have.lengthOf(1)
141 expect(data[0].uuid).to.equal(videoUUID)
142 }
143 })
144
145 it('Should search through account videos', async function () {
146 const created = await servers[0].videos.upload({ token: userAccessToken, attributes: { name: 'Kami no chikara' } })
147
148 await waitJobs(servers)
149
150 for (const server of servers) {
151 const { total, data } = await server.videos.listByAccount({ handle: 'user1@' + servers[0].host, search: 'Kami' })
152
153 expect(total).to.equal(1)
154 expect(data).to.be.an('array')
155 expect(data).to.have.lengthOf(1)
156 expect(data[0].uuid).to.equal(created.uuid)
157 }
158 })
159
160 it('Should remove the user', async function () {
161 this.timeout(10_000)
162
163 for (const server of servers) {
164 const body = await server.accounts.list({ sort: '-createdAt' })
165
166 const accountDeleted = body.data.find(a => a.name === 'user1' && a.host === servers[0].host)
167 expect(accountDeleted).not.to.be.undefined
168
169 const { data } = await server.channels.list()
170 const videoChannelDeleted = data.find(a => a.displayName === 'Main user1 channel' && a.host === servers[0].host)
171 expect(videoChannelDeleted).not.to.be.undefined
172 }
173
174 await servers[0].users.remove({ userId })
175
176 await waitJobs(servers)
177
178 for (const server of servers) {
179 const body = await server.accounts.list({ sort: '-createdAt' })
180
181 const accountDeleted = body.data.find(a => a.name === 'user1' && a.host === servers[0].host)
182 expect(accountDeleted).to.be.undefined
183
184 const { data } = await server.channels.list()
185 const videoChannelDeleted = data.find(a => a.name === 'Main user1 channel' && a.host === servers[0].host)
186 expect(videoChannelDeleted).to.be.undefined
187 }
188 })
189
190 it('Should not have actor files', async () => {
191 for (const server of servers) {
192 for (const userAvatarFilename of userAvatarFilenames) {
193 await checkActorFilesWereRemoved(userAvatarFilename, server)
194 }
195 }
196 })
197
198 it('Should not have video files', async () => {
199 for (const server of servers) {
200 await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
201 }
202 })
203
204 it('Should have an empty tmp directory', async function () {
205 for (const server of servers) {
206 await checkTmpIsEmpty(server)
207 }
208 })
209
210 after(async function () {
211 await cleanupTests(servers)
212 })
213})
diff --git a/packages/tests/src/api/users/users.ts b/packages/tests/src/api/users/users.ts
new file mode 100644
index 000000000..a0090a463
--- /dev/null
+++ b/packages/tests/src/api/users/users.ts
@@ -0,0 +1,529 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { testImageSize } from '@tests/shared/checks.js'
5import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@peertube/peertube-models'
6import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
7
8describe('Test users', function () {
9 let server: PeerTubeServer
10 let token: string
11 let userToken: string
12 let videoId: number
13 let userId: number
14 const user = {
15 username: 'user_1',
16 password: 'super password'
17 }
18
19 before(async function () {
20 this.timeout(30000)
21
22 server = await createSingleServer(1, {
23 rates_limit: {
24 login: {
25 max: 30
26 }
27 }
28 })
29
30 await setAccessTokensToServers([ server ])
31
32 await server.plugins.install({ npmName: 'peertube-theme-background-red' })
33 })
34
35 describe('Creating a user', function () {
36
37 it('Should be able to create a new user', async function () {
38 await server.users.create({ ...user, videoQuota: 2 * 1024 * 1024, adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST })
39 })
40
41 it('Should be able to login with this user', async function () {
42 userToken = await server.login.getAccessToken(user)
43 })
44
45 it('Should be able to get user information', async function () {
46 const userMe = await server.users.getMyInfo({ token: userToken })
47
48 const userGet = await server.users.get({ userId: userMe.id, withStats: true })
49
50 for (const user of [ userMe, userGet ]) {
51 expect(user.username).to.equal('user_1')
52 expect(user.email).to.equal('user_1@example.com')
53 expect(user.nsfwPolicy).to.equal('display')
54 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
55 expect(user.role.label).to.equal('User')
56 expect(user.id).to.be.a('number')
57 expect(user.account.displayName).to.equal('user_1')
58 expect(user.account.description).to.be.null
59 }
60
61 expect(userMe.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST)
62 expect(userGet.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST)
63
64 expect(userMe.specialPlaylists).to.have.lengthOf(1)
65 expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER)
66
67 // Check stats are included with withStats
68 expect(userGet.videosCount).to.be.a('number')
69 expect(userGet.videosCount).to.equal(0)
70 expect(userGet.videoCommentsCount).to.be.a('number')
71 expect(userGet.videoCommentsCount).to.equal(0)
72 expect(userGet.abusesCount).to.be.a('number')
73 expect(userGet.abusesCount).to.equal(0)
74 expect(userGet.abusesAcceptedCount).to.be.a('number')
75 expect(userGet.abusesAcceptedCount).to.equal(0)
76 })
77 })
78
79 describe('Users listing', function () {
80
81 it('Should list all the users', async function () {
82 const { data, total } = await server.users.list()
83
84 expect(total).to.equal(2)
85 expect(data).to.be.an('array')
86 expect(data.length).to.equal(2)
87
88 const user = data[0]
89 expect(user.username).to.equal('user_1')
90 expect(user.email).to.equal('user_1@example.com')
91 expect(user.nsfwPolicy).to.equal('display')
92
93 const rootUser = data[1]
94 expect(rootUser.username).to.equal('root')
95 expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com')
96 expect(user.nsfwPolicy).to.equal('display')
97
98 expect(rootUser.lastLoginDate).to.exist
99 expect(user.lastLoginDate).to.exist
100
101 userId = user.id
102 })
103
104 it('Should list only the first user by username asc', async function () {
105 const { total, data } = await server.users.list({ start: 0, count: 1, sort: 'username' })
106
107 expect(total).to.equal(2)
108 expect(data.length).to.equal(1)
109
110 const user = data[0]
111 expect(user.username).to.equal('root')
112 expect(user.email).to.equal('admin' + server.internalServerNumber + '@example.com')
113 expect(user.role.label).to.equal('Administrator')
114 expect(user.nsfwPolicy).to.equal('display')
115 })
116
117 it('Should list only the first user by username desc', async function () {
118 const { total, data } = await server.users.list({ start: 0, count: 1, sort: '-username' })
119
120 expect(total).to.equal(2)
121 expect(data.length).to.equal(1)
122
123 const user = data[0]
124 expect(user.username).to.equal('user_1')
125 expect(user.email).to.equal('user_1@example.com')
126 expect(user.nsfwPolicy).to.equal('display')
127 })
128
129 it('Should list only the second user by createdAt desc', async function () {
130 const { data, total } = await server.users.list({ start: 0, count: 1, sort: '-createdAt' })
131 expect(total).to.equal(2)
132
133 expect(data.length).to.equal(1)
134
135 const user = data[0]
136 expect(user.username).to.equal('user_1')
137 expect(user.email).to.equal('user_1@example.com')
138 expect(user.nsfwPolicy).to.equal('display')
139 })
140
141 it('Should list all the users by createdAt asc', async function () {
142 const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt' })
143
144 expect(total).to.equal(2)
145 expect(data.length).to.equal(2)
146
147 expect(data[0].username).to.equal('root')
148 expect(data[0].email).to.equal('admin' + server.internalServerNumber + '@example.com')
149 expect(data[0].nsfwPolicy).to.equal('display')
150
151 expect(data[1].username).to.equal('user_1')
152 expect(data[1].email).to.equal('user_1@example.com')
153 expect(data[1].nsfwPolicy).to.equal('display')
154 })
155
156 it('Should search user by username', async function () {
157 const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'oot' })
158 expect(total).to.equal(1)
159 expect(data.length).to.equal(1)
160 expect(data[0].username).to.equal('root')
161 })
162
163 it('Should search user by email', async function () {
164 {
165 const { total, data } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'r_1@exam' })
166 expect(total).to.equal(1)
167 expect(data.length).to.equal(1)
168 expect(data[0].username).to.equal('user_1')
169 expect(data[0].email).to.equal('user_1@example.com')
170 }
171
172 {
173 const { total, data } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', search: 'example' })
174 expect(total).to.equal(2)
175 expect(data.length).to.equal(2)
176 expect(data[0].username).to.equal('root')
177 expect(data[1].username).to.equal('user_1')
178 }
179 })
180 })
181
182 describe('Update my account', function () {
183
184 it('Should update my password', async function () {
185 await server.users.updateMe({
186 token: userToken,
187 currentPassword: 'super password',
188 password: 'new password'
189 })
190 user.password = 'new password'
191
192 await server.login.login({ user })
193 })
194
195 it('Should be able to change the NSFW display attribute', async function () {
196 await server.users.updateMe({
197 token: userToken,
198 nsfwPolicy: 'do_not_list'
199 })
200
201 const user = await server.users.getMyInfo({ token: userToken })
202 expect(user.username).to.equal('user_1')
203 expect(user.email).to.equal('user_1@example.com')
204 expect(user.nsfwPolicy).to.equal('do_not_list')
205 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
206 expect(user.id).to.be.a('number')
207 expect(user.account.displayName).to.equal('user_1')
208 expect(user.account.description).to.be.null
209 })
210
211 it('Should be able to change the autoPlayVideo attribute', async function () {
212 await server.users.updateMe({
213 token: userToken,
214 autoPlayVideo: false
215 })
216
217 const user = await server.users.getMyInfo({ token: userToken })
218 expect(user.autoPlayVideo).to.be.false
219 })
220
221 it('Should be able to change the autoPlayNextVideo attribute', async function () {
222 await server.users.updateMe({
223 token: userToken,
224 autoPlayNextVideo: true
225 })
226
227 const user = await server.users.getMyInfo({ token: userToken })
228 expect(user.autoPlayNextVideo).to.be.true
229 })
230
231 it('Should be able to change the p2p attribute', async function () {
232 await server.users.updateMe({
233 token: userToken,
234 p2pEnabled: true
235 })
236
237 const user = await server.users.getMyInfo({ token: userToken })
238 expect(user.p2pEnabled).to.be.true
239 })
240
241 it('Should be able to change the email attribute', async function () {
242 await server.users.updateMe({
243 token: userToken,
244 currentPassword: 'new password',
245 email: 'updated@example.com'
246 })
247
248 const user = await server.users.getMyInfo({ token: userToken })
249 expect(user.username).to.equal('user_1')
250 expect(user.email).to.equal('updated@example.com')
251 expect(user.nsfwPolicy).to.equal('do_not_list')
252 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
253 expect(user.id).to.be.a('number')
254 expect(user.account.displayName).to.equal('user_1')
255 expect(user.account.description).to.be.null
256 })
257
258 it('Should be able to update my avatar with a gif', async function () {
259 const fixture = 'avatar.gif'
260
261 await server.users.updateMyAvatar({ token: userToken, fixture })
262
263 const user = await server.users.getMyInfo({ token: userToken })
264 for (const avatar of user.account.avatars) {
265 await testImageSize(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.gif')
266 }
267 })
268
269 it('Should be able to update my avatar with a gif, and then a png', async function () {
270 for (const extension of [ '.png', '.gif' ]) {
271 const fixture = 'avatar' + extension
272
273 await server.users.updateMyAvatar({ token: userToken, fixture })
274
275 const user = await server.users.getMyInfo({ token: userToken })
276 for (const avatar of user.account.avatars) {
277 await testImageSize(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, extension)
278 }
279 }
280 })
281
282 it('Should be able to update my display name', async function () {
283 await server.users.updateMe({ token: userToken, displayName: 'new display name' })
284
285 const user = await server.users.getMyInfo({ token: userToken })
286 expect(user.username).to.equal('user_1')
287 expect(user.email).to.equal('updated@example.com')
288 expect(user.nsfwPolicy).to.equal('do_not_list')
289 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
290 expect(user.id).to.be.a('number')
291 expect(user.account.displayName).to.equal('new display name')
292 expect(user.account.description).to.be.null
293 })
294
295 it('Should be able to update my description', async function () {
296 await server.users.updateMe({ token: userToken, description: 'my super description updated' })
297
298 const user = await server.users.getMyInfo({ token: userToken })
299 expect(user.username).to.equal('user_1')
300 expect(user.email).to.equal('updated@example.com')
301 expect(user.nsfwPolicy).to.equal('do_not_list')
302 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
303 expect(user.id).to.be.a('number')
304 expect(user.account.displayName).to.equal('new display name')
305 expect(user.account.description).to.equal('my super description updated')
306 expect(user.noWelcomeModal).to.be.false
307 expect(user.noInstanceConfigWarningModal).to.be.false
308 expect(user.noAccountSetupWarningModal).to.be.false
309 })
310
311 it('Should be able to update my theme', async function () {
312 for (const theme of [ 'background-red', 'default', 'instance-default' ]) {
313 await server.users.updateMe({ token: userToken, theme })
314
315 const user = await server.users.getMyInfo({ token: userToken })
316 expect(user.theme).to.equal(theme)
317 }
318 })
319
320 it('Should be able to update my modal preferences', async function () {
321 await server.users.updateMe({
322 token: userToken,
323 noInstanceConfigWarningModal: true,
324 noWelcomeModal: true,
325 noAccountSetupWarningModal: true
326 })
327
328 const user = await server.users.getMyInfo({ token: userToken })
329 expect(user.noWelcomeModal).to.be.true
330 expect(user.noInstanceConfigWarningModal).to.be.true
331 expect(user.noAccountSetupWarningModal).to.be.true
332 })
333 })
334
335 describe('Updating another user', function () {
336
337 it('Should be able to update another user', async function () {
338 await server.users.update({
339 userId,
340 token,
341 email: 'updated2@example.com',
342 emailVerified: true,
343 videoQuota: 42,
344 role: UserRole.MODERATOR,
345 adminFlags: UserAdminFlag.NONE,
346 pluginAuth: 'toto'
347 })
348
349 const user = await server.users.get({ token, userId })
350
351 expect(user.username).to.equal('user_1')
352 expect(user.email).to.equal('updated2@example.com')
353 expect(user.emailVerified).to.be.true
354 expect(user.nsfwPolicy).to.equal('do_not_list')
355 expect(user.videoQuota).to.equal(42)
356 expect(user.role.label).to.equal('Moderator')
357 expect(user.id).to.be.a('number')
358 expect(user.adminFlags).to.equal(UserAdminFlag.NONE)
359 expect(user.pluginAuth).to.equal('toto')
360 })
361
362 it('Should reset the auth plugin', async function () {
363 await server.users.update({ userId, token, pluginAuth: null })
364
365 const user = await server.users.get({ token, userId })
366 expect(user.pluginAuth).to.be.null
367 })
368
369 it('Should have removed the user token', async function () {
370 await server.users.getMyQuotaUsed({ token: userToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
371
372 userToken = await server.login.getAccessToken(user)
373 })
374
375 it('Should be able to update another user password', async function () {
376 await server.users.update({ userId, token, password: 'password updated' })
377
378 await server.users.getMyQuotaUsed({ token: userToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
379
380 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
381
382 user.password = 'password updated'
383 userToken = await server.login.getAccessToken(user)
384 })
385 })
386
387 describe('Remove a user', function () {
388
389 before(async function () {
390 await server.users.update({
391 userId,
392 token,
393 videoQuota: 2 * 1024 * 1024
394 })
395
396 await server.videos.quickUpload({ name: 'user video', token: userToken, fixture: 'video_short.webm' })
397 await server.videos.quickUpload({ name: 'root video' })
398
399 const { total } = await server.videos.list()
400 expect(total).to.equal(2)
401 })
402
403 it('Should be able to remove this user', async function () {
404 await server.users.remove({ userId, token })
405 })
406
407 it('Should not be able to login with this user', async function () {
408 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
409 })
410
411 it('Should not have videos of this user', async function () {
412 const { data, total } = await server.videos.list()
413 expect(total).to.equal(1)
414
415 const video = data[0]
416 expect(video.account.name).to.equal('root')
417 })
418 })
419
420 describe('User blocking', function () {
421 let user16Id: number
422 let user16AccessToken: string
423
424 const user16 = {
425 username: 'user_16',
426 password: 'my super password'
427 }
428
429 it('Should block a user', async function () {
430 const user = await server.users.create({ ...user16 })
431 user16Id = user.id
432
433 user16AccessToken = await server.login.getAccessToken(user16)
434
435 await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.OK_200 })
436 await server.users.banUser({ userId: user16Id })
437
438 await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
439 await server.login.login({ user: user16, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
440 })
441
442 it('Should search user by banned status', async function () {
443 {
444 const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', blocked: true })
445 expect(total).to.equal(1)
446 expect(data.length).to.equal(1)
447
448 expect(data[0].username).to.equal(user16.username)
449 }
450
451 {
452 const { data, total } = await server.users.list({ start: 0, count: 2, sort: 'createdAt', blocked: false })
453 expect(total).to.equal(1)
454 expect(data.length).to.equal(1)
455
456 expect(data[0].username).to.not.equal(user16.username)
457 }
458 })
459
460 it('Should unblock a user', async function () {
461 await server.users.unbanUser({ userId: user16Id })
462 user16AccessToken = await server.login.getAccessToken(user16)
463 await server.users.getMyInfo({ token: user16AccessToken, expectedStatus: HttpStatusCode.OK_200 })
464 })
465 })
466
467 describe('User stats', function () {
468 let user17Id: number
469 let user17AccessToken: string
470
471 it('Should report correct initial statistics about a user', async function () {
472 const user17 = {
473 username: 'user_17',
474 password: 'my super password'
475 }
476 const created = await server.users.create({ ...user17 })
477
478 user17Id = created.id
479 user17AccessToken = await server.login.getAccessToken(user17)
480
481 const user = await server.users.get({ userId: user17Id, withStats: true })
482 expect(user.videosCount).to.equal(0)
483 expect(user.videoCommentsCount).to.equal(0)
484 expect(user.abusesCount).to.equal(0)
485 expect(user.abusesCreatedCount).to.equal(0)
486 expect(user.abusesAcceptedCount).to.equal(0)
487 })
488
489 it('Should report correct videos count', async function () {
490 const attributes = { name: 'video to test user stats' }
491 await server.videos.upload({ token: user17AccessToken, attributes })
492
493 const { data } = await server.videos.list()
494 videoId = data.find(video => video.name === attributes.name).id
495
496 const user = await server.users.get({ userId: user17Id, withStats: true })
497 expect(user.videosCount).to.equal(1)
498 })
499
500 it('Should report correct video comments for user', async function () {
501 const text = 'super comment'
502 await server.comments.createThread({ token: user17AccessToken, videoId, text })
503
504 const user = await server.users.get({ userId: user17Id, withStats: true })
505 expect(user.videoCommentsCount).to.equal(1)
506 })
507
508 it('Should report correct abuses counts', async function () {
509 const reason = 'my super bad reason'
510 await server.abuses.report({ token: user17AccessToken, videoId, reason })
511
512 const body1 = await server.abuses.getAdminList()
513 const abuseId = body1.data[0].id
514
515 const user2 = await server.users.get({ userId: user17Id, withStats: true })
516 expect(user2.abusesCount).to.equal(1) // number of incriminations
517 expect(user2.abusesCreatedCount).to.equal(1) // number of reports created
518
519 await server.abuses.update({ abuseId, body: { state: AbuseState.ACCEPTED } })
520
521 const user3 = await server.users.get({ userId: user17Id, withStats: true })
522 expect(user3.abusesAcceptedCount).to.equal(1) // number of reports created accepted
523 })
524 })
525
526 after(async function () {
527 await cleanupTests([ server ])
528 })
529})
diff --git a/packages/tests/src/api/videos/channel-import-videos.ts b/packages/tests/src/api/videos/channel-import-videos.ts
new file mode 100644
index 000000000..d0e47fe95
--- /dev/null
+++ b/packages/tests/src/api/videos/channel-import-videos.ts
@@ -0,0 +1,161 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FIXTURE_URLS } from '@tests/shared/tests.js'
5import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
6import {
7 createSingleServer,
8 getServerImportConfig,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultVideoChannel,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test videos import in a channel', function () {
16 if (areHttpImportTestsDisabled()) return
17
18 function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
19
20 describe('Import using ' + mode, function () {
21 let server: PeerTubeServer
22
23 before(async function () {
24 this.timeout(120_000)
25
26 server = await createSingleServer(1, getServerImportConfig(mode))
27
28 await setAccessTokensToServers([ server ])
29 await setDefaultVideoChannel([ server ])
30
31 await server.config.enableChannelSync()
32 })
33
34 it('Should import a whole channel without specifying the sync id', async function () {
35 this.timeout(240_000)
36
37 await server.channels.importVideos({ channelName: server.store.channel.name, externalChannelUrl: FIXTURE_URLS.youtubeChannel })
38 await waitJobs(server)
39
40 const videos = await server.videos.listByChannel({ handle: server.store.channel.name })
41 expect(videos.total).to.equal(2)
42 })
43
44 it('These imports should not have a sync id', async function () {
45 const { total, data } = await server.imports.getMyVideoImports()
46
47 expect(total).to.equal(2)
48 expect(data).to.have.lengthOf(2)
49
50 for (const videoImport of data) {
51 expect(videoImport.videoChannelSync).to.not.exist
52 }
53 })
54
55 it('Should import a whole channel and specifying the sync id', async function () {
56 this.timeout(240_000)
57
58 {
59 server.store.channel.name = 'channel2'
60 const { id } = await server.channels.create({ attributes: { name: server.store.channel.name } })
61 server.store.channel.id = id
62 }
63
64 {
65 const attributes = {
66 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
67 videoChannelId: server.store.channel.id
68 }
69
70 const { videoChannelSync } = await server.channelSyncs.create({ attributes })
71 server.store.videoChannelSync = videoChannelSync
72
73 await waitJobs(server)
74 }
75
76 await server.channels.importVideos({
77 channelName: server.store.channel.name,
78 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
79 videoChannelSyncId: server.store.videoChannelSync.id
80 })
81
82 await waitJobs(server)
83 })
84
85 it('These imports should have a sync id', async function () {
86 const { total, data } = await server.imports.getMyVideoImports()
87
88 expect(total).to.equal(4)
89 expect(data).to.have.lengthOf(4)
90
91 const importsWithSyncId = data.filter(i => !!i.videoChannelSync)
92 expect(importsWithSyncId).to.have.lengthOf(2)
93
94 for (const videoImport of importsWithSyncId) {
95 expect(videoImport.videoChannelSync).to.exist
96 expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
97 }
98 })
99
100 it('Should be able to filter imports by this sync id', async function () {
101 const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: server.store.videoChannelSync.id })
102
103 expect(total).to.equal(2)
104 expect(data).to.have.lengthOf(2)
105
106 for (const videoImport of data) {
107 expect(videoImport.videoChannelSync).to.exist
108 expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
109 }
110 })
111
112 it('Should limit max amount of videos synced on full sync', async function () {
113 this.timeout(240_000)
114
115 await server.kill()
116 await server.run({
117 import: {
118 video_channel_synchronization: {
119 full_sync_videos_limit: 1
120 }
121 }
122 })
123
124 const { id } = await server.channels.create({ attributes: { name: 'channel3' } })
125 const channel3Id = id
126
127 const { videoChannelSync } = await server.channelSyncs.create({
128 attributes: {
129 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
130 videoChannelId: channel3Id
131 }
132 })
133 const syncId = videoChannelSync.id
134
135 await waitJobs(server)
136
137 await server.channels.importVideos({
138 channelName: 'channel3',
139 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
140 videoChannelSyncId: syncId
141 })
142
143 await waitJobs(server)
144
145 const { total, data } = await server.videos.listByChannel({ handle: 'channel3' })
146
147 expect(total).to.equal(1)
148 expect(data).to.have.lengthOf(1)
149 })
150
151 after(async function () {
152 await server?.kill()
153 })
154 })
155 }
156
157 runSuite('yt-dlp')
158
159 // FIXME: With recent changes on youtube, youtube-dl doesn't fetch live replays which means the test suite fails
160 // runSuite('youtube-dl')
161})
diff --git a/packages/tests/src/api/videos/index.ts b/packages/tests/src/api/videos/index.ts
new file mode 100644
index 000000000..fcb1d5a81
--- /dev/null
+++ b/packages/tests/src/api/videos/index.ts
@@ -0,0 +1,23 @@
1import './multiple-servers.js'
2import './resumable-upload.js'
3import './single-server.js'
4import './video-captions.js'
5import './video-change-ownership.js'
6import './video-channels.js'
7import './channel-import-videos.js'
8import './video-channel-syncs.js'
9import './video-comments.js'
10import './video-description.js'
11import './video-files.js'
12import './video-imports.js'
13import './video-nsfw.js'
14import './video-playlists.js'
15import './video-playlist-thumbnails.js'
16import './video-source.js'
17import './video-privacy.js'
18import './video-schedule-update.js'
19import './videos-common-filters.js'
20import './videos-history.js'
21import './videos-overview.js'
22import './video-static-file-privacy.js'
23import './video-storyboard.js'
diff --git a/packages/tests/src/api/videos/multiple-servers.ts b/packages/tests/src/api/videos/multiple-servers.ts
new file mode 100644
index 000000000..03afd7cbb
--- /dev/null
+++ b/packages/tests/src/api/videos/multiple-servers.ts
@@ -0,0 +1,1095 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import request from 'supertest'
5import { wait } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@peertube/peertube-models'
7import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
8import {
9 cleanupTests,
10 createMultipleServers,
11 doubleFollow,
12 makeGetRequest,
13 PeerTubeServer,
14 setAccessTokensToServers,
15 setDefaultAccountAvatar,
16 setDefaultChannelAvatar,
17 waitJobs
18} from '@peertube/peertube-server-commands'
19import { testImageGeneratedByFFmpeg, dateIsValid } from '@tests/shared/checks.js'
20import { checkTmpIsEmpty } from '@tests/shared/directories.js'
21import { completeVideoCheck, saveVideoInServers, checkVideoFilesWereRemoved } from '@tests/shared/videos.js'
22import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js'
23
24describe('Test multiple servers', function () {
25 let servers: PeerTubeServer[] = []
26 const toRemove = []
27 let videoUUID = ''
28 let videoChannelId: number
29
30 before(async function () {
31 this.timeout(120000)
32
33 servers = await createMultipleServers(3)
34
35 // Get the access tokens
36 await setAccessTokensToServers(servers)
37
38 {
39 const videoChannel = {
40 name: 'super_channel_name',
41 displayName: 'my channel',
42 description: 'super channel'
43 }
44 await servers[0].channels.create({ attributes: videoChannel })
45 await setDefaultChannelAvatar(servers[0], videoChannel.name)
46 await setDefaultAccountAvatar(servers)
47
48 const { data } = await servers[0].channels.list({ start: 0, count: 1 })
49 videoChannelId = data[0].id
50 }
51
52 // Server 1 and server 2 follow each other
53 await doubleFollow(servers[0], servers[1])
54 // Server 1 and server 3 follow each other
55 await doubleFollow(servers[0], servers[2])
56 // Server 2 and server 3 follow each other
57 await doubleFollow(servers[1], servers[2])
58 })
59
60 it('Should not have videos for all servers', async function () {
61 for (const server of servers) {
62 const { data } = await server.videos.list()
63 expect(data).to.be.an('array')
64 expect(data.length).to.equal(0)
65 }
66 })
67
68 describe('Should upload the video and propagate on each server', function () {
69
70 it('Should upload the video on server 1 and propagate on each server', async function () {
71 this.timeout(60000)
72
73 const attributes = {
74 name: 'my super name for server 1',
75 category: 5,
76 licence: 4,
77 language: 'ja',
78 nsfw: true,
79 description: 'my super description for server 1',
80 support: 'my super support text for server 1',
81 originallyPublishedAt: '2019-02-10T13:38:14.449Z',
82 tags: [ 'tag1p1', 'tag2p1' ],
83 channelId: videoChannelId,
84 fixture: 'video_short1.webm'
85 }
86 await servers[0].videos.upload({ attributes })
87
88 await waitJobs(servers)
89
90 // All servers should have this video
91 let publishedAt: string = null
92 for (const server of servers) {
93 const isLocal = server.port === servers[0].port
94 const checkAttributes = {
95 name: 'my super name for server 1',
96 category: 5,
97 licence: 4,
98 language: 'ja',
99 nsfw: true,
100 description: 'my super description for server 1',
101 support: 'my super support text for server 1',
102 originallyPublishedAt: '2019-02-10T13:38:14.449Z',
103 account: {
104 name: 'root',
105 host: servers[0].host
106 },
107 isLocal,
108 publishedAt,
109 duration: 10,
110 tags: [ 'tag1p1', 'tag2p1' ],
111 privacy: VideoPrivacy.PUBLIC,
112 commentsEnabled: true,
113 downloadEnabled: true,
114 channel: {
115 displayName: 'my channel',
116 name: 'super_channel_name',
117 description: 'super channel',
118 isLocal
119 },
120 fixture: 'video_short1.webm',
121 files: [
122 {
123 resolution: 720,
124 size: 572456
125 }
126 ]
127 }
128
129 const { data } = await server.videos.list()
130 expect(data).to.be.an('array')
131 expect(data.length).to.equal(1)
132 const video = data[0]
133
134 await completeVideoCheck({ server, originServer: servers[0], videoUUID: video.uuid, attributes: checkAttributes })
135 publishedAt = video.publishedAt as string
136
137 expect(video.channel.avatars).to.have.lengthOf(2)
138 expect(video.account.avatars).to.have.lengthOf(2)
139
140 for (const image of [ ...video.channel.avatars, ...video.account.avatars ]) {
141 expect(image.createdAt).to.exist
142 expect(image.updatedAt).to.exist
143 expect(image.width).to.be.above(20).and.below(1000)
144 expect(image.path).to.exist
145
146 await makeGetRequest({
147 url: server.url,
148 path: image.path,
149 expectedStatus: HttpStatusCode.OK_200
150 })
151 }
152 }
153 })
154
155 it('Should upload the video on server 2 and propagate on each server', async function () {
156 this.timeout(240000)
157
158 const user = {
159 username: 'user1',
160 password: 'super_password'
161 }
162 await servers[1].users.create({ username: user.username, password: user.password })
163 const userAccessToken = await servers[1].login.getAccessToken(user)
164
165 const attributes = {
166 name: 'my super name for server 2',
167 category: 4,
168 licence: 3,
169 language: 'de',
170 nsfw: true,
171 description: 'my super description for server 2',
172 support: 'my super support text for server 2',
173 tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
174 fixture: 'video_short2.webm',
175 thumbnailfile: 'custom-thumbnail.jpg',
176 previewfile: 'custom-preview.jpg'
177 }
178 await servers[1].videos.upload({ token: userAccessToken, attributes, mode: 'resumable' })
179
180 // Transcoding
181 await waitJobs(servers)
182
183 // All servers should have this video
184 for (const server of servers) {
185 const isLocal = server.url === servers[1].url
186 const checkAttributes = {
187 name: 'my super name for server 2',
188 category: 4,
189 licence: 3,
190 language: 'de',
191 nsfw: true,
192 description: 'my super description for server 2',
193 support: 'my super support text for server 2',
194 account: {
195 name: 'user1',
196 host: servers[1].host
197 },
198 isLocal,
199 commentsEnabled: true,
200 downloadEnabled: true,
201 duration: 5,
202 tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
203 privacy: VideoPrivacy.PUBLIC,
204 channel: {
205 displayName: 'Main user1 channel',
206 name: 'user1_channel',
207 description: 'super channel',
208 isLocal
209 },
210 fixture: 'video_short2.webm',
211 files: [
212 {
213 resolution: 240,
214 size: 270000
215 },
216 {
217 resolution: 360,
218 size: 359000
219 },
220 {
221 resolution: 480,
222 size: 465000
223 },
224 {
225 resolution: 720,
226 size: 750000
227 }
228 ],
229 thumbnailfile: 'custom-thumbnail',
230 previewfile: 'custom-preview'
231 }
232
233 const { data } = await server.videos.list()
234 expect(data).to.be.an('array')
235 expect(data.length).to.equal(2)
236 const video = data[1]
237
238 await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes })
239 }
240 })
241
242 it('Should upload two videos on server 3 and propagate on each server', async function () {
243 this.timeout(45000)
244
245 {
246 const attributes = {
247 name: 'my super name for server 3',
248 category: 6,
249 licence: 5,
250 language: 'de',
251 nsfw: true,
252 description: 'my super description for server 3',
253 support: 'my super support text for server 3',
254 tags: [ 'tag1p3' ],
255 fixture: 'video_short3.webm'
256 }
257 await servers[2].videos.upload({ attributes })
258 }
259
260 {
261 const attributes = {
262 name: 'my super name for server 3-2',
263 category: 7,
264 licence: 6,
265 language: 'ko',
266 nsfw: false,
267 description: 'my super description for server 3-2',
268 support: 'my super support text for server 3-2',
269 tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
270 fixture: 'video_short.webm'
271 }
272 await servers[2].videos.upload({ attributes })
273 }
274
275 await waitJobs(servers)
276
277 // All servers should have this video
278 for (const server of servers) {
279 const isLocal = server.url === servers[2].url
280 const { data } = await server.videos.list()
281
282 expect(data).to.be.an('array')
283 expect(data.length).to.equal(4)
284
285 // We not sure about the order of the two last uploads
286 let video1 = null
287 let video2 = null
288 if (data[2].name === 'my super name for server 3') {
289 video1 = data[2]
290 video2 = data[3]
291 } else {
292 video1 = data[3]
293 video2 = data[2]
294 }
295
296 const checkAttributesVideo1 = {
297 name: 'my super name for server 3',
298 category: 6,
299 licence: 5,
300 language: 'de',
301 nsfw: true,
302 description: 'my super description for server 3',
303 support: 'my super support text for server 3',
304 account: {
305 name: 'root',
306 host: servers[2].host
307 },
308 isLocal,
309 duration: 5,
310 commentsEnabled: true,
311 downloadEnabled: true,
312 tags: [ 'tag1p3' ],
313 privacy: VideoPrivacy.PUBLIC,
314 channel: {
315 displayName: 'Main root channel',
316 name: 'root_channel',
317 description: '',
318 isLocal
319 },
320 fixture: 'video_short3.webm',
321 files: [
322 {
323 resolution: 720,
324 size: 292677
325 }
326 ]
327 }
328 await completeVideoCheck({ server, originServer: servers[2], videoUUID: video1.uuid, attributes: checkAttributesVideo1 })
329
330 const checkAttributesVideo2 = {
331 name: 'my super name for server 3-2',
332 category: 7,
333 licence: 6,
334 language: 'ko',
335 nsfw: false,
336 description: 'my super description for server 3-2',
337 support: 'my super support text for server 3-2',
338 account: {
339 name: 'root',
340 host: servers[2].host
341 },
342 commentsEnabled: true,
343 downloadEnabled: true,
344 isLocal,
345 duration: 5,
346 tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
347 privacy: VideoPrivacy.PUBLIC,
348 channel: {
349 displayName: 'Main root channel',
350 name: 'root_channel',
351 description: '',
352 isLocal
353 },
354 fixture: 'video_short.webm',
355 files: [
356 {
357 resolution: 720,
358 size: 218910
359 }
360 ]
361 }
362 await completeVideoCheck({ server, originServer: servers[2], videoUUID: video2.uuid, attributes: checkAttributesVideo2 })
363 }
364 })
365 })
366
367 describe('It should list local videos', function () {
368 it('Should list only local videos on server 1', async function () {
369 const { data, total } = await servers[0].videos.list({ isLocal: true })
370
371 expect(total).to.equal(1)
372 expect(data).to.be.an('array')
373 expect(data.length).to.equal(1)
374 expect(data[0].name).to.equal('my super name for server 1')
375 })
376
377 it('Should list only local videos on server 2', async function () {
378 const { data, total } = await servers[1].videos.list({ isLocal: true })
379
380 expect(total).to.equal(1)
381 expect(data).to.be.an('array')
382 expect(data.length).to.equal(1)
383 expect(data[0].name).to.equal('my super name for server 2')
384 })
385
386 it('Should list only local videos on server 3', async function () {
387 const { data, total } = await servers[2].videos.list({ isLocal: true })
388
389 expect(total).to.equal(2)
390 expect(data).to.be.an('array')
391 expect(data.length).to.equal(2)
392 expect(data[0].name).to.equal('my super name for server 3')
393 expect(data[1].name).to.equal('my super name for server 3-2')
394 })
395 })
396
397 describe('Should seed the uploaded video', function () {
398
399 it('Should add the file 1 by asking server 3', async function () {
400 this.retries(2)
401 this.timeout(30000)
402
403 const { data } = await servers[2].videos.list()
404
405 const video = data[0]
406 toRemove.push(data[2])
407 toRemove.push(data[3])
408
409 const videoDetails = await servers[2].videos.get({ id: video.id })
410
411 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
412 })
413
414 it('Should add the file 2 by asking server 1', async function () {
415 this.retries(2)
416 this.timeout(30000)
417
418 const { data } = await servers[0].videos.list()
419
420 const video = data[1]
421 const videoDetails = await servers[0].videos.get({ id: video.id })
422
423 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
424 })
425
426 it('Should add the file 3 by asking server 2', async function () {
427 this.retries(2)
428 this.timeout(30000)
429
430 const { data } = await servers[1].videos.list()
431
432 const video = data[2]
433 const videoDetails = await servers[1].videos.get({ id: video.id })
434
435 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
436 })
437
438 it('Should add the file 3-2 by asking server 1', async function () {
439 this.retries(2)
440 this.timeout(30000)
441
442 const { data } = await servers[0].videos.list()
443
444 const video = data[3]
445 const videoDetails = await servers[0].videos.get({ id: video.id })
446
447 await checkWebTorrentWorks(videoDetails.files[0].magnetUri)
448 })
449
450 it('Should add the file 2 in 360p by asking server 1', async function () {
451 this.retries(2)
452 this.timeout(30000)
453
454 const { data } = await servers[0].videos.list()
455
456 const video = data.find(v => v.name === 'my super name for server 2')
457 const videoDetails = await servers[0].videos.get({ id: video.id })
458
459 const file = videoDetails.files.find(f => f.resolution.id === 360)
460 expect(file).not.to.be.undefined
461
462 await checkWebTorrentWorks(file.magnetUri)
463 })
464 })
465
466 describe('Should update video views, likes and dislikes', function () {
467 let localVideosServer3 = []
468 let remoteVideosServer1 = []
469 let remoteVideosServer2 = []
470 let remoteVideosServer3 = []
471
472 before(async function () {
473 {
474 const { data } = await servers[0].videos.list()
475 remoteVideosServer1 = data.filter(video => video.isLocal === false).map(video => video.uuid)
476 }
477
478 {
479 const { data } = await servers[1].videos.list()
480 remoteVideosServer2 = data.filter(video => video.isLocal === false).map(video => video.uuid)
481 }
482
483 {
484 const { data } = await servers[2].videos.list()
485 localVideosServer3 = data.filter(video => video.isLocal === true).map(video => video.uuid)
486 remoteVideosServer3 = data.filter(video => video.isLocal === false).map(video => video.uuid)
487 }
488 })
489
490 it('Should view multiple videos on owned servers', async function () {
491 this.timeout(30000)
492
493 await servers[2].views.simulateView({ id: localVideosServer3[0] })
494 await wait(1000)
495
496 await servers[2].views.simulateView({ id: localVideosServer3[0] })
497 await servers[2].views.simulateView({ id: localVideosServer3[1] })
498
499 await wait(1000)
500
501 await servers[2].views.simulateView({ id: localVideosServer3[0] })
502 await servers[2].views.simulateView({ id: localVideosServer3[0] })
503
504 await waitJobs(servers)
505
506 for (const server of servers) {
507 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
508 }
509
510 await waitJobs(servers)
511
512 for (const server of servers) {
513 const { data } = await server.videos.list()
514
515 const video0 = data.find(v => v.uuid === localVideosServer3[0])
516 const video1 = data.find(v => v.uuid === localVideosServer3[1])
517
518 expect(video0.views).to.equal(3)
519 expect(video1.views).to.equal(1)
520 }
521 })
522
523 it('Should view multiple videos on each servers', async function () {
524 this.timeout(45000)
525
526 const tasks: Promise<any>[] = []
527 tasks.push(servers[0].views.simulateView({ id: remoteVideosServer1[0] }))
528 tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] }))
529 tasks.push(servers[1].views.simulateView({ id: remoteVideosServer2[0] }))
530 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[0] }))
531 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
532 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
533 tasks.push(servers[2].views.simulateView({ id: remoteVideosServer3[1] }))
534 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
535 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
536 tasks.push(servers[2].views.simulateView({ id: localVideosServer3[1] }))
537
538 await Promise.all(tasks)
539
540 await waitJobs(servers)
541
542 for (const server of servers) {
543 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
544 }
545
546 await waitJobs(servers)
547
548 let baseVideos = null
549
550 for (const server of servers) {
551 const { data } = await server.videos.list()
552
553 // Initialize base videos for future comparisons
554 if (baseVideos === null) {
555 baseVideos = data
556 continue
557 }
558
559 for (const baseVideo of baseVideos) {
560 const sameVideo = data.find(video => video.name === baseVideo.name)
561 expect(baseVideo.views).to.equal(sameVideo.views)
562 }
563 }
564 })
565
566 it('Should like and dislikes videos on different services', async function () {
567 this.timeout(50000)
568
569 await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' })
570 await wait(500)
571 await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'dislike' })
572 await wait(500)
573 await servers[0].videos.rate({ id: remoteVideosServer1[0], rating: 'like' })
574 await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'like' })
575 await wait(500)
576 await servers[2].videos.rate({ id: localVideosServer3[1], rating: 'dislike' })
577 await servers[2].videos.rate({ id: remoteVideosServer3[1], rating: 'dislike' })
578 await wait(500)
579 await servers[2].videos.rate({ id: remoteVideosServer3[0], rating: 'like' })
580
581 await waitJobs(servers)
582 await wait(5000)
583 await waitJobs(servers)
584
585 let baseVideos = null
586 for (const server of servers) {
587 const { data } = await server.videos.list()
588
589 // Initialize base videos for future comparisons
590 if (baseVideos === null) {
591 baseVideos = data
592 continue
593 }
594
595 for (const baseVideo of baseVideos) {
596 const sameVideo = data.find(video => video.name === baseVideo.name)
597 expect(baseVideo.likes).to.equal(sameVideo.likes, `Likes of ${sameVideo.uuid} do not correspond`)
598 expect(baseVideo.dislikes).to.equal(sameVideo.dislikes, `Dislikes of ${sameVideo.uuid} do not correspond`)
599 }
600 }
601 })
602 })
603
604 describe('Should manipulate these videos', function () {
605 let updatedAtMin: Date
606
607 it('Should update video 3', async function () {
608 this.timeout(30000)
609
610 const attributes = {
611 name: 'my super video updated',
612 category: 10,
613 licence: 7,
614 language: 'fr',
615 nsfw: true,
616 description: 'my super description updated',
617 support: 'my super support text updated',
618 tags: [ 'tag_up_1', 'tag_up_2' ],
619 thumbnailfile: 'custom-thumbnail.jpg',
620 originallyPublishedAt: '2019-02-11T13:38:14.449Z',
621 previewfile: 'custom-preview.jpg'
622 }
623
624 updatedAtMin = new Date()
625 await servers[2].videos.update({ id: toRemove[0].id, attributes })
626
627 await waitJobs(servers)
628 })
629
630 it('Should have the video 3 updated on each server', async function () {
631 this.timeout(30000)
632
633 for (const server of servers) {
634 const { data } = await server.videos.list()
635
636 const videoUpdated = data.find(video => video.name === 'my super video updated')
637 expect(!!videoUpdated).to.be.true
638
639 expect(new Date(videoUpdated.updatedAt)).to.be.greaterThan(updatedAtMin)
640
641 const isLocal = server.url === servers[2].url
642 const checkAttributes = {
643 name: 'my super video updated',
644 category: 10,
645 licence: 7,
646 language: 'fr',
647 nsfw: true,
648 description: 'my super description updated',
649 support: 'my super support text updated',
650 originallyPublishedAt: '2019-02-11T13:38:14.449Z',
651 account: {
652 name: 'root',
653 host: servers[2].host
654 },
655 isLocal,
656 duration: 5,
657 commentsEnabled: true,
658 downloadEnabled: true,
659 tags: [ 'tag_up_1', 'tag_up_2' ],
660 privacy: VideoPrivacy.PUBLIC,
661 channel: {
662 displayName: 'Main root channel',
663 name: 'root_channel',
664 description: '',
665 isLocal
666 },
667 fixture: 'video_short3.webm',
668 files: [
669 {
670 resolution: 720,
671 size: 292677
672 }
673 ],
674 thumbnailfile: 'custom-thumbnail',
675 previewfile: 'custom-preview'
676 }
677 await completeVideoCheck({ server, originServer: servers[2], videoUUID: videoUpdated.uuid, attributes: checkAttributes })
678 }
679 })
680
681 it('Should only update thumbnail and update updatedAt attribute', async function () {
682 this.timeout(30000)
683
684 const attributes = {
685 thumbnailfile: 'custom-thumbnail.jpg'
686 }
687
688 updatedAtMin = new Date()
689 await servers[2].videos.update({ id: toRemove[0].id, attributes })
690
691 await waitJobs(servers)
692
693 for (const server of servers) {
694 const { data } = await server.videos.list()
695
696 const videoUpdated = data.find(video => video.name === 'my super video updated')
697 expect(new Date(videoUpdated.updatedAt)).to.be.greaterThan(updatedAtMin)
698 }
699 })
700
701 it('Should remove the videos 3 and 3-2 by asking server 3 and correctly delete files', async function () {
702 this.timeout(30000)
703
704 for (const id of [ toRemove[0].id, toRemove[1].id ]) {
705 await saveVideoInServers(servers, id)
706
707 await servers[2].videos.remove({ id })
708
709 await waitJobs(servers)
710
711 for (const server of servers) {
712 await checkVideoFilesWereRemoved({ server, video: server.store.videoDetails })
713 }
714 }
715 })
716
717 it('Should have videos 1 and 3 on each server', async function () {
718 for (const server of servers) {
719 const { data } = await server.videos.list()
720
721 expect(data).to.be.an('array')
722 expect(data.length).to.equal(2)
723 expect(data[0].name).not.to.equal(data[1].name)
724 expect(data[0].name).not.to.equal(toRemove[0].name)
725 expect(data[1].name).not.to.equal(toRemove[0].name)
726 expect(data[0].name).not.to.equal(toRemove[1].name)
727 expect(data[1].name).not.to.equal(toRemove[1].name)
728
729 videoUUID = data.find(video => video.name === 'my super name for server 1').uuid
730 }
731 })
732
733 it('Should get the same video by UUID on each server', async function () {
734 let baseVideo = null
735 for (const server of servers) {
736 const video = await server.videos.get({ id: videoUUID })
737
738 if (baseVideo === null) {
739 baseVideo = video
740 continue
741 }
742
743 expect(baseVideo.name).to.equal(video.name)
744 expect(baseVideo.uuid).to.equal(video.uuid)
745 expect(baseVideo.category.id).to.equal(video.category.id)
746 expect(baseVideo.language.id).to.equal(video.language.id)
747 expect(baseVideo.licence.id).to.equal(video.licence.id)
748 expect(baseVideo.nsfw).to.equal(video.nsfw)
749 expect(baseVideo.account.name).to.equal(video.account.name)
750 expect(baseVideo.account.displayName).to.equal(video.account.displayName)
751 expect(baseVideo.account.url).to.equal(video.account.url)
752 expect(baseVideo.account.host).to.equal(video.account.host)
753 expect(baseVideo.tags).to.deep.equal(video.tags)
754 }
755 })
756
757 it('Should get the preview from each server', async function () {
758 for (const server of servers) {
759 const video = await server.videos.get({ id: videoUUID })
760
761 await testImageGeneratedByFFmpeg(server.url, 'video_short1-preview.webm', video.previewPath)
762 }
763 })
764 })
765
766 describe('Should comment these videos', function () {
767 let childOfFirstChild: VideoCommentThreadTree
768
769 it('Should add comment (threads and replies)', async function () {
770 this.timeout(25000)
771
772 {
773 const text = 'my super first comment'
774 await servers[0].comments.createThread({ videoId: videoUUID, text })
775 }
776
777 {
778 const text = 'my super second comment'
779 await servers[2].comments.createThread({ videoId: videoUUID, text })
780 }
781
782 await waitJobs(servers)
783
784 {
785 const threadId = await servers[1].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' })
786
787 const text = 'my super answer to thread 1'
788 await servers[1].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text })
789 }
790
791 await waitJobs(servers)
792
793 {
794 const threadId = await servers[2].comments.findCommentId({ videoId: videoUUID, text: 'my super first comment' })
795
796 const body = await servers[2].comments.getThread({ videoId: videoUUID, threadId })
797 const childCommentId = body.children[0].comment.id
798
799 const text3 = 'my second answer to thread 1'
800 await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: text3 })
801
802 const text2 = 'my super answer to answer of thread 1'
803 await servers[2].comments.addReply({ videoId: videoUUID, toCommentId: childCommentId, text: text2 })
804 }
805
806 await waitJobs(servers)
807 })
808
809 it('Should have these threads', async function () {
810 for (const server of servers) {
811 const body = await server.comments.listThreads({ videoId: videoUUID })
812
813 expect(body.total).to.equal(2)
814 expect(body.data).to.be.an('array')
815 expect(body.data).to.have.lengthOf(2)
816
817 {
818 const comment = body.data.find(c => c.text === 'my super first comment')
819 expect(comment).to.not.be.undefined
820 expect(comment.inReplyToCommentId).to.be.null
821 expect(comment.account.name).to.equal('root')
822 expect(comment.account.host).to.equal(servers[0].host)
823 expect(comment.totalReplies).to.equal(3)
824 expect(dateIsValid(comment.createdAt as string)).to.be.true
825 expect(dateIsValid(comment.updatedAt as string)).to.be.true
826 }
827
828 {
829 const comment = body.data.find(c => c.text === 'my super second comment')
830 expect(comment).to.not.be.undefined
831 expect(comment.inReplyToCommentId).to.be.null
832 expect(comment.account.name).to.equal('root')
833 expect(comment.account.host).to.equal(servers[2].host)
834 expect(comment.totalReplies).to.equal(0)
835 expect(dateIsValid(comment.createdAt as string)).to.be.true
836 expect(dateIsValid(comment.updatedAt as string)).to.be.true
837 }
838 }
839 })
840
841 it('Should have these comments', async function () {
842 for (const server of servers) {
843 const body = await server.comments.listThreads({ videoId: videoUUID })
844 const threadId = body.data.find(c => c.text === 'my super first comment').id
845
846 const tree = await server.comments.getThread({ videoId: videoUUID, threadId })
847
848 expect(tree.comment.text).equal('my super first comment')
849 expect(tree.comment.account.name).equal('root')
850 expect(tree.comment.account.host).equal(servers[0].host)
851 expect(tree.children).to.have.lengthOf(2)
852
853 const firstChild = tree.children[0]
854 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
855 expect(firstChild.comment.account.name).equal('root')
856 expect(firstChild.comment.account.host).equal(servers[1].host)
857 expect(firstChild.children).to.have.lengthOf(1)
858
859 childOfFirstChild = firstChild.children[0]
860 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
861 expect(childOfFirstChild.comment.account.name).equal('root')
862 expect(childOfFirstChild.comment.account.host).equal(servers[2].host)
863 expect(childOfFirstChild.children).to.have.lengthOf(0)
864
865 const secondChild = tree.children[1]
866 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
867 expect(secondChild.comment.account.name).equal('root')
868 expect(secondChild.comment.account.host).equal(servers[2].host)
869 expect(secondChild.children).to.have.lengthOf(0)
870 }
871 })
872
873 it('Should delete a reply', async function () {
874 this.timeout(30000)
875
876 await servers[2].comments.delete({ videoId: videoUUID, commentId: childOfFirstChild.comment.id })
877
878 await waitJobs(servers)
879 })
880
881 it('Should have this comment marked as deleted', async function () {
882 for (const server of servers) {
883 const { data } = await server.comments.listThreads({ videoId: videoUUID })
884 const threadId = data.find(c => c.text === 'my super first comment').id
885
886 const tree = await server.comments.getThread({ videoId: videoUUID, threadId })
887 expect(tree.comment.text).equal('my super first comment')
888
889 const firstChild = tree.children[0]
890 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
891 expect(firstChild.children).to.have.lengthOf(1)
892
893 const deletedComment = firstChild.children[0].comment
894 expect(deletedComment.isDeleted).to.be.true
895 expect(deletedComment.deletedAt).to.not.be.null
896 expect(deletedComment.account).to.be.null
897 expect(deletedComment.text).to.equal('')
898
899 const secondChild = tree.children[1]
900 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
901 }
902 })
903
904 it('Should delete the thread comments', async function () {
905 this.timeout(30000)
906
907 const { data } = await servers[0].comments.listThreads({ videoId: videoUUID })
908 const commentId = data.find(c => c.text === 'my super first comment').id
909 await servers[0].comments.delete({ videoId: videoUUID, commentId })
910
911 await waitJobs(servers)
912 })
913
914 it('Should have the threads marked as deleted on other servers too', async function () {
915 for (const server of servers) {
916 const body = await server.comments.listThreads({ videoId: videoUUID })
917
918 expect(body.total).to.equal(2)
919 expect(body.data).to.be.an('array')
920 expect(body.data).to.have.lengthOf(2)
921
922 {
923 const comment = body.data[0]
924 expect(comment).to.not.be.undefined
925 expect(comment.inReplyToCommentId).to.be.null
926 expect(comment.account.name).to.equal('root')
927 expect(comment.account.host).to.equal(servers[2].host)
928 expect(comment.totalReplies).to.equal(0)
929 expect(dateIsValid(comment.createdAt as string)).to.be.true
930 expect(dateIsValid(comment.updatedAt as string)).to.be.true
931 }
932
933 {
934 const deletedComment = body.data[1]
935 expect(deletedComment).to.not.be.undefined
936 expect(deletedComment.isDeleted).to.be.true
937 expect(deletedComment.deletedAt).to.not.be.null
938 expect(deletedComment.text).to.equal('')
939 expect(deletedComment.inReplyToCommentId).to.be.null
940 expect(deletedComment.account).to.be.null
941 expect(deletedComment.totalReplies).to.equal(2)
942 expect(dateIsValid(deletedComment.createdAt as string)).to.be.true
943 expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true
944 expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
945 }
946 }
947 })
948
949 it('Should delete a remote thread by the origin server', async function () {
950 this.timeout(5000)
951
952 const { data } = await servers[0].comments.listThreads({ videoId: videoUUID })
953 const commentId = data.find(c => c.text === 'my super second comment').id
954 await servers[0].comments.delete({ videoId: videoUUID, commentId })
955
956 await waitJobs(servers)
957 })
958
959 it('Should have the threads marked as deleted on other servers too', async function () {
960 for (const server of servers) {
961 const body = await server.comments.listThreads({ videoId: videoUUID })
962
963 expect(body.total).to.equal(2)
964 expect(body.data).to.have.lengthOf(2)
965
966 {
967 const comment = body.data[0]
968 expect(comment.text).to.equal('')
969 expect(comment.isDeleted).to.be.true
970 expect(comment.createdAt).to.not.be.null
971 expect(comment.deletedAt).to.not.be.null
972 expect(comment.account).to.be.null
973 expect(comment.totalReplies).to.equal(0)
974 }
975
976 {
977 const comment = body.data[1]
978 expect(comment.text).to.equal('')
979 expect(comment.isDeleted).to.be.true
980 expect(comment.createdAt).to.not.be.null
981 expect(comment.deletedAt).to.not.be.null
982 expect(comment.account).to.be.null
983 expect(comment.totalReplies).to.equal(2)
984 }
985 }
986 })
987
988 it('Should disable comments and download', async function () {
989 this.timeout(20000)
990
991 const attributes = {
992 commentsEnabled: false,
993 downloadEnabled: false
994 }
995
996 await servers[0].videos.update({ id: videoUUID, attributes })
997
998 await waitJobs(servers)
999
1000 for (const server of servers) {
1001 const video = await server.videos.get({ id: videoUUID })
1002 expect(video.commentsEnabled).to.be.false
1003 expect(video.downloadEnabled).to.be.false
1004
1005 const text = 'my super forbidden comment'
1006 await server.comments.createThread({ videoId: videoUUID, text, expectedStatus: HttpStatusCode.CONFLICT_409 })
1007 }
1008 })
1009 })
1010
1011 describe('With minimum parameters', function () {
1012 it('Should upload and propagate the video', async function () {
1013 this.timeout(120000)
1014
1015 const path = '/api/v1/videos/upload'
1016
1017 const req = request(servers[1].url)
1018 .post(path)
1019 .set('Accept', 'application/json')
1020 .set('Authorization', 'Bearer ' + servers[1].accessToken)
1021 .field('name', 'minimum parameters')
1022 .field('privacy', '1')
1023 .field('channelId', '1')
1024
1025 await req.attach('videofile', buildAbsoluteFixturePath('video_short.webm'))
1026 .expect(HttpStatusCode.OK_200)
1027
1028 await waitJobs(servers)
1029
1030 for (const server of servers) {
1031 const { data } = await server.videos.list()
1032 const video = data.find(v => v.name === 'minimum parameters')
1033
1034 const isLocal = server.url === servers[1].url
1035 const checkAttributes = {
1036 name: 'minimum parameters',
1037 category: null,
1038 licence: null,
1039 language: null,
1040 nsfw: false,
1041 description: null,
1042 support: null,
1043 account: {
1044 name: 'root',
1045 host: servers[1].host
1046 },
1047 isLocal,
1048 duration: 5,
1049 commentsEnabled: true,
1050 downloadEnabled: true,
1051 tags: [],
1052 privacy: VideoPrivacy.PUBLIC,
1053 channel: {
1054 displayName: 'Main root channel',
1055 name: 'root_channel',
1056 description: '',
1057 isLocal
1058 },
1059 fixture: 'video_short.webm',
1060 files: [
1061 {
1062 resolution: 720,
1063 size: 61000
1064 },
1065 {
1066 resolution: 480,
1067 size: 40000
1068 },
1069 {
1070 resolution: 360,
1071 size: 32000
1072 },
1073 {
1074 resolution: 240,
1075 size: 23000
1076 }
1077 ]
1078 }
1079 await completeVideoCheck({ server, originServer: servers[1], videoUUID: video.uuid, attributes: checkAttributes })
1080 }
1081 })
1082 })
1083
1084 describe('TMP directory', function () {
1085 it('Should have an empty tmp directory', async function () {
1086 for (const server of servers) {
1087 await checkTmpIsEmpty(server)
1088 }
1089 })
1090 })
1091
1092 after(async function () {
1093 await cleanupTests(servers)
1094 })
1095})
diff --git a/packages/tests/src/api/videos/resumable-upload.ts b/packages/tests/src/api/videos/resumable-upload.ts
new file mode 100644
index 000000000..628e0298c
--- /dev/null
+++ b/packages/tests/src/api/videos/resumable-upload.ts
@@ -0,0 +1,316 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists } from 'fs-extra/esm'
5import { readdir, stat } from 'fs/promises'
6import { join } from 'path'
7import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models'
8import { buildAbsoluteFixturePath, sha1 } from '@peertube/peertube-node-utils'
9import {
10 cleanupTests,
11 createSingleServer,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 setDefaultVideoChannel
15} from '@peertube/peertube-server-commands'
16
17// Most classic resumable upload tests are done in other test suites
18
19describe('Test resumable upload', function () {
20 const path = '/api/v1/videos/upload-resumable'
21 const defaultFixture = 'video_short.mp4'
22 let server: PeerTubeServer
23 let rootId: number
24 let userAccessToken: string
25 let userChannelId: number
26
27 async function buildSize (fixture: string, size?: number) {
28 if (size !== undefined) return size
29
30 const baseFixture = buildAbsoluteFixturePath(fixture)
31 return (await stat(baseFixture)).size
32 }
33
34 async function prepareUpload (options: {
35 channelId?: number
36 token?: string
37 size?: number
38 originalName?: string
39 lastModified?: number
40 } = {}) {
41 const { token, originalName, lastModified } = options
42
43 const size = await buildSize(defaultFixture, options.size)
44
45 const attributes = {
46 name: 'video',
47 channelId: options.channelId ?? server.store.channel.id,
48 privacy: VideoPrivacy.PUBLIC,
49 fixture: defaultFixture
50 }
51
52 const mimetype = 'video/mp4'
53
54 const res = await server.videos.prepareResumableUpload({ path, token, attributes, size, mimetype, originalName, lastModified })
55
56 return res.header['location'].split('?')[1]
57 }
58
59 async function sendChunks (options: {
60 token?: string
61 pathUploadId: string
62 size?: number
63 expectedStatus?: HttpStatusCodeType
64 contentLength?: number
65 contentRange?: string
66 contentRangeBuilder?: (start: number, chunk: any) => string
67 digestBuilder?: (chunk: any) => string
68 }) {
69 const { token, pathUploadId, expectedStatus, contentLength, contentRangeBuilder, digestBuilder } = options
70
71 const size = await buildSize(defaultFixture, options.size)
72 const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture)
73
74 return server.videos.sendResumableChunks({
75 token,
76 path,
77 pathUploadId,
78 videoFilePath: absoluteFilePath,
79 size,
80 contentLength,
81 contentRangeBuilder,
82 digestBuilder,
83 expectedStatus
84 })
85 }
86
87 async function checkFileSize (uploadIdArg: string, expectedSize: number | null) {
88 const uploadId = uploadIdArg.replace(/^upload_id=/, '')
89
90 const subPath = join('tmp', 'resumable-uploads', `${rootId}-${uploadId}.mp4`)
91 const filePath = server.servers.buildDirectory(subPath)
92 const exists = await pathExists(filePath)
93
94 if (expectedSize === null) {
95 expect(exists).to.be.false
96 return
97 }
98
99 expect(exists).to.be.true
100
101 expect((await stat(filePath)).size).to.equal(expectedSize)
102 }
103
104 async function countResumableUploads (wait?: number) {
105 const subPath = join('tmp', 'resumable-uploads')
106 const filePath = server.servers.buildDirectory(subPath)
107 await new Promise(resolve => setTimeout(resolve, wait))
108 const files = await readdir(filePath)
109 return files.length
110 }
111
112 before(async function () {
113 this.timeout(30000)
114
115 server = await createSingleServer(1)
116 await setAccessTokensToServers([ server ])
117 await setDefaultVideoChannel([ server ])
118
119 const body = await server.users.getMyInfo()
120 rootId = body.id
121
122 {
123 userAccessToken = await server.users.generateUserAndToken('user1')
124 const { videoChannels } = await server.users.getMyInfo({ token: userAccessToken })
125 userChannelId = videoChannels[0].id
126 }
127
128 await server.users.update({ userId: rootId, videoQuota: 10_000_000 })
129 })
130
131 describe('Directory cleaning', function () {
132
133 it('Should correctly delete files after an upload', async function () {
134 const uploadId = await prepareUpload()
135 await sendChunks({ pathUploadId: uploadId })
136 await server.videos.endResumableUpload({ path, pathUploadId: uploadId })
137
138 expect(await countResumableUploads()).to.equal(0)
139 })
140
141 it('Should correctly delete corrupt files', async function () {
142 const uploadId = await prepareUpload({ size: 8 * 1024 })
143 await sendChunks({ pathUploadId: uploadId, size: 8 * 1024, expectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 })
144
145 expect(await countResumableUploads(2000)).to.equal(0)
146 })
147
148 it('Should not delete files after an unfinished upload', async function () {
149 await prepareUpload()
150
151 expect(await countResumableUploads()).to.equal(2)
152 })
153
154 it('Should not delete recent uploads', async function () {
155 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
156
157 expect(await countResumableUploads()).to.equal(2)
158 })
159
160 it('Should delete old uploads', async function () {
161 await server.debug.sendCommand({ body: { command: 'remove-dandling-resumable-uploads' } })
162
163 expect(await countResumableUploads()).to.equal(0)
164 })
165 })
166
167 describe('Resumable upload and chunks', function () {
168
169 it('Should accept the same amount of chunks', async function () {
170 const uploadId = await prepareUpload()
171 await sendChunks({ pathUploadId: uploadId })
172
173 await checkFileSize(uploadId, null)
174 })
175
176 it('Should not accept more chunks than expected', async function () {
177 const uploadId = await prepareUpload({ size: 100 })
178
179 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409 })
180 await checkFileSize(uploadId, 0)
181 })
182
183 it('Should not accept more chunks than expected with an invalid content length/content range', async function () {
184 const uploadId = await prepareUpload({ size: 1500 })
185
186 // Content length check can be different depending on the node version
187 try {
188 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.CONFLICT_409, contentLength: 1000 })
189 await checkFileSize(uploadId, 0)
190 } catch {
191 await sendChunks({ pathUploadId: uploadId, expectedStatus: HttpStatusCode.BAD_REQUEST_400, contentLength: 1000 })
192 await checkFileSize(uploadId, 0)
193 }
194 })
195
196 it('Should not accept more chunks than expected with an invalid content length', async function () {
197 const uploadId = await prepareUpload({ size: 500 })
198
199 const size = 1000
200
201 // Content length check seems to have changed in v16
202 const expectedStatus = process.version.startsWith('v16')
203 ? HttpStatusCode.CONFLICT_409
204 : HttpStatusCode.BAD_REQUEST_400
205
206 const contentRangeBuilder = (start: number) => `bytes ${start}-${start + size - 1}/${size}`
207 await sendChunks({ pathUploadId: uploadId, expectedStatus, contentRangeBuilder, contentLength: size })
208 await checkFileSize(uploadId, 0)
209 })
210
211 it('Should be able to accept 2 PUT requests', async function () {
212 const uploadId = await prepareUpload()
213
214 const result1 = await sendChunks({ pathUploadId: uploadId })
215 const result2 = await sendChunks({ pathUploadId: uploadId })
216
217 expect(result1.body.video.uuid).to.exist
218 expect(result1.body.video.uuid).to.equal(result2.body.video.uuid)
219
220 expect(result1.headers['x-resumable-upload-cached']).to.not.exist
221 expect(result2.headers['x-resumable-upload-cached']).to.equal('true')
222
223 await checkFileSize(uploadId, null)
224 })
225
226 it('Should not have the same upload id with 2 different users', async function () {
227 const originalName = 'toto.mp4'
228 const lastModified = new Date().getTime()
229
230 const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
231 const uploadId2 = await prepareUpload({ originalName, lastModified, channelId: userChannelId, token: userAccessToken })
232
233 expect(uploadId1).to.not.equal(uploadId2)
234 })
235
236 it('Should have the same upload id with the same user', async function () {
237 const originalName = 'toto.mp4'
238 const lastModified = new Date().getTime()
239
240 const uploadId1 = await prepareUpload({ originalName, lastModified })
241 const uploadId2 = await prepareUpload({ originalName, lastModified })
242
243 expect(uploadId1).to.equal(uploadId2)
244 })
245
246 it('Should not cache a request with 2 different users', async function () {
247 const originalName = 'toto.mp4'
248 const lastModified = new Date().getTime()
249
250 const uploadId = await prepareUpload({ originalName, lastModified, token: server.accessToken })
251
252 await sendChunks({ pathUploadId: uploadId, token: server.accessToken })
253 await sendChunks({ pathUploadId: uploadId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
254 })
255
256 it('Should not cache a request after a delete', async function () {
257 const originalName = 'toto.mp4'
258 const lastModified = new Date().getTime()
259 const uploadId1 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
260
261 await sendChunks({ pathUploadId: uploadId1 })
262 await server.videos.endResumableUpload({ path, pathUploadId: uploadId1 })
263
264 const uploadId2 = await prepareUpload({ originalName, lastModified, token: server.accessToken })
265 expect(uploadId1).to.equal(uploadId2)
266
267 const result2 = await sendChunks({ pathUploadId: uploadId1 })
268 expect(result2.headers['x-resumable-upload-cached']).to.not.exist
269 })
270
271 it('Should not cache after video deletion', async function () {
272 const originalName = 'toto.mp4'
273 const lastModified = new Date().getTime()
274
275 const uploadId1 = await prepareUpload({ originalName, lastModified })
276 const result1 = await sendChunks({ pathUploadId: uploadId1 })
277 await server.videos.remove({ id: result1.body.video.uuid })
278
279 const uploadId2 = await prepareUpload({ originalName, lastModified })
280 const result2 = await sendChunks({ pathUploadId: uploadId2 })
281 expect(result1.body.video.uuid).to.not.equal(result2.body.video.uuid)
282
283 expect(result2.headers['x-resumable-upload-cached']).to.not.exist
284
285 await checkFileSize(uploadId1, null)
286 await checkFileSize(uploadId2, null)
287 })
288
289 it('Should refuse an invalid digest', async function () {
290 const uploadId = await prepareUpload({ token: server.accessToken })
291
292 await sendChunks({
293 pathUploadId: uploadId,
294 token: server.accessToken,
295 digestBuilder: () => 'sha=' + 'a'.repeat(40),
296 expectedStatus: 460 as any
297 })
298 })
299
300 it('Should accept an appropriate digest', async function () {
301 const uploadId = await prepareUpload({ token: server.accessToken })
302
303 await sendChunks({
304 pathUploadId: uploadId,
305 token: server.accessToken,
306 digestBuilder: (chunk: Buffer) => {
307 return 'sha1=' + sha1(chunk, 'base64')
308 }
309 })
310 })
311 })
312
313 after(async function () {
314 await cleanupTests([ server ])
315 })
316})
diff --git a/packages/tests/src/api/videos/single-server.ts b/packages/tests/src/api/videos/single-server.ts
new file mode 100644
index 000000000..b87192a57
--- /dev/null
+++ b/packages/tests/src/api/videos/single-server.ts
@@ -0,0 +1,461 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { Video, VideoPrivacy } from '@peertube/peertube-models'
6import { checkVideoFilesWereRemoved, completeVideoCheck } from '@tests/shared/videos.js'
7import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
8import {
9 cleanupTests,
10 createSingleServer,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultAccountAvatar,
14 setDefaultChannelAvatar,
15 waitJobs
16} from '@peertube/peertube-server-commands'
17
18describe('Test a single server', function () {
19
20 function runSuite (mode: 'legacy' | 'resumable') {
21 let server: PeerTubeServer = null
22 let videoId: number | string
23 let videoId2: string
24 let videoUUID = ''
25 let videosListBase: any[] = null
26
27 const getCheckAttributes = () => ({
28 name: 'my super name',
29 category: 2,
30 licence: 6,
31 language: 'zh',
32 nsfw: true,
33 description: 'my super description',
34 support: 'my super support text',
35 account: {
36 name: 'root',
37 host: server.host
38 },
39 isLocal: true,
40 duration: 5,
41 tags: [ 'tag1', 'tag2', 'tag3' ],
42 privacy: VideoPrivacy.PUBLIC,
43 commentsEnabled: true,
44 downloadEnabled: true,
45 channel: {
46 displayName: 'Main root channel',
47 name: 'root_channel',
48 description: '',
49 isLocal: true
50 },
51 fixture: 'video_short.webm',
52 files: [
53 {
54 resolution: 720,
55 size: 218910
56 }
57 ]
58 })
59
60 const updateCheckAttributes = () => ({
61 name: 'my super video updated',
62 category: 4,
63 licence: 2,
64 language: 'ar',
65 nsfw: false,
66 description: 'my super description updated',
67 support: 'my super support text updated',
68 account: {
69 name: 'root',
70 host: server.host
71 },
72 isLocal: true,
73 tags: [ 'tagup1', 'tagup2' ],
74 privacy: VideoPrivacy.PUBLIC,
75 duration: 5,
76 commentsEnabled: false,
77 downloadEnabled: false,
78 channel: {
79 name: 'root_channel',
80 displayName: 'Main root channel',
81 description: '',
82 isLocal: true
83 },
84 fixture: 'video_short3.webm',
85 files: [
86 {
87 resolution: 720,
88 size: 292677
89 }
90 ]
91 })
92
93 before(async function () {
94 this.timeout(30000)
95
96 server = await createSingleServer(1, {})
97
98 await setAccessTokensToServers([ server ])
99 await setDefaultChannelAvatar(server)
100 await setDefaultAccountAvatar(server)
101 })
102
103 it('Should list video categories', async function () {
104 const categories = await server.videos.getCategories()
105 expect(Object.keys(categories)).to.have.length.above(10)
106
107 expect(categories[11]).to.equal('News & Politics')
108 })
109
110 it('Should list video licences', async function () {
111 const licences = await server.videos.getLicences()
112 expect(Object.keys(licences)).to.have.length.above(5)
113
114 expect(licences[3]).to.equal('Attribution - No Derivatives')
115 })
116
117 it('Should list video languages', async function () {
118 const languages = await server.videos.getLanguages()
119 expect(Object.keys(languages)).to.have.length.above(5)
120
121 expect(languages['ru']).to.equal('Russian')
122 })
123
124 it('Should list video privacies', async function () {
125 const privacies = await server.videos.getPrivacies()
126 expect(Object.keys(privacies)).to.have.length.at.least(3)
127
128 expect(privacies[3]).to.equal('Private')
129 })
130
131 it('Should not have videos', async function () {
132 const { data, total } = await server.videos.list()
133
134 expect(total).to.equal(0)
135 expect(data).to.be.an('array')
136 expect(data.length).to.equal(0)
137 })
138
139 it('Should upload the video', async function () {
140 const attributes = {
141 name: 'my super name',
142 category: 2,
143 nsfw: true,
144 licence: 6,
145 tags: [ 'tag1', 'tag2', 'tag3' ]
146 }
147 const video = await server.videos.upload({ attributes, mode })
148 expect(video).to.not.be.undefined
149 expect(video.id).to.equal(1)
150 expect(video.uuid).to.have.length.above(5)
151
152 videoId = video.id
153 videoUUID = video.uuid
154 })
155
156 it('Should get and seed the uploaded video', async function () {
157 this.timeout(5000)
158
159 const { data, total } = await server.videos.list()
160
161 expect(total).to.equal(1)
162 expect(data).to.be.an('array')
163 expect(data.length).to.equal(1)
164
165 const video = data[0]
166 await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() })
167 })
168
169 it('Should get the video by UUID', async function () {
170 this.timeout(5000)
171
172 const video = await server.videos.get({ id: videoUUID })
173 await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: getCheckAttributes() })
174 })
175
176 it('Should have the views updated', async function () {
177 this.timeout(20000)
178
179 await server.views.simulateView({ id: videoId })
180 await server.views.simulateView({ id: videoId })
181 await server.views.simulateView({ id: videoId })
182
183 await wait(1500)
184
185 await server.views.simulateView({ id: videoId })
186 await server.views.simulateView({ id: videoId })
187
188 await wait(1500)
189
190 await server.views.simulateView({ id: videoId })
191 await server.views.simulateView({ id: videoId })
192
193 await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
194
195 const video = await server.videos.get({ id: videoId })
196 expect(video.views).to.equal(3)
197 })
198
199 it('Should remove the video', async function () {
200 const video = await server.videos.get({ id: videoId })
201 await server.videos.remove({ id: videoId })
202
203 await checkVideoFilesWereRemoved({ video, server })
204 })
205
206 it('Should not have videos', async function () {
207 const { total, data } = await server.videos.list()
208
209 expect(total).to.equal(0)
210 expect(data).to.be.an('array')
211 expect(data).to.have.lengthOf(0)
212 })
213
214 it('Should upload 6 videos', async function () {
215 this.timeout(120000)
216
217 const videos = new Set([
218 'video_short.mp4', 'video_short.ogv', 'video_short.webm',
219 'video_short1.webm', 'video_short2.webm', 'video_short3.webm'
220 ])
221
222 for (const video of videos) {
223 const attributes = {
224 name: video + ' name',
225 description: video + ' description',
226 category: 2,
227 licence: 1,
228 language: 'en',
229 nsfw: true,
230 tags: [ 'tag1', 'tag2', 'tag3' ],
231 fixture: video
232 }
233
234 await server.videos.upload({ attributes, mode })
235 }
236 })
237
238 it('Should have the correct durations', async function () {
239 const { total, data } = await server.videos.list()
240
241 expect(total).to.equal(6)
242 expect(data).to.be.an('array')
243 expect(data).to.have.lengthOf(6)
244
245 const videosByName: { [ name: string ]: Video } = {}
246 data.forEach(v => { videosByName[v.name] = v })
247
248 expect(videosByName['video_short.mp4 name'].duration).to.equal(5)
249 expect(videosByName['video_short.ogv name'].duration).to.equal(5)
250 expect(videosByName['video_short.webm name'].duration).to.equal(5)
251 expect(videosByName['video_short1.webm name'].duration).to.equal(10)
252 expect(videosByName['video_short2.webm name'].duration).to.equal(5)
253 expect(videosByName['video_short3.webm name'].duration).to.equal(5)
254 })
255
256 it('Should have the correct thumbnails', async function () {
257 const { data } = await server.videos.list()
258
259 // For the next test
260 videosListBase = data
261
262 for (const video of data) {
263 const videoName = video.name.replace(' name', '')
264 await testImageGeneratedByFFmpeg(server.url, videoName, video.thumbnailPath)
265 }
266 })
267
268 it('Should list only the two first videos', async function () {
269 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: 'name' })
270
271 expect(total).to.equal(6)
272 expect(data.length).to.equal(2)
273 expect(data[0].name).to.equal(videosListBase[0].name)
274 expect(data[1].name).to.equal(videosListBase[1].name)
275 })
276
277 it('Should list only the next three videos', async function () {
278 const { total, data } = await server.videos.list({ start: 2, count: 3, sort: 'name' })
279
280 expect(total).to.equal(6)
281 expect(data.length).to.equal(3)
282 expect(data[0].name).to.equal(videosListBase[2].name)
283 expect(data[1].name).to.equal(videosListBase[3].name)
284 expect(data[2].name).to.equal(videosListBase[4].name)
285 })
286
287 it('Should list the last video', async function () {
288 const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name' })
289
290 expect(total).to.equal(6)
291 expect(data.length).to.equal(1)
292 expect(data[0].name).to.equal(videosListBase[5].name)
293 })
294
295 it('Should not have the total field', async function () {
296 const { total, data } = await server.videos.list({ start: 5, count: 6, sort: 'name', skipCount: true })
297
298 expect(total).to.not.exist
299 expect(data.length).to.equal(1)
300 expect(data[0].name).to.equal(videosListBase[5].name)
301 })
302
303 it('Should list and sort by name in descending order', async function () {
304 const { total, data } = await server.videos.list({ sort: '-name' })
305
306 expect(total).to.equal(6)
307 expect(data.length).to.equal(6)
308 expect(data[0].name).to.equal('video_short.webm name')
309 expect(data[1].name).to.equal('video_short.ogv name')
310 expect(data[2].name).to.equal('video_short.mp4 name')
311 expect(data[3].name).to.equal('video_short3.webm name')
312 expect(data[4].name).to.equal('video_short2.webm name')
313 expect(data[5].name).to.equal('video_short1.webm name')
314
315 videoId = data[3].uuid
316 videoId2 = data[5].uuid
317 })
318
319 it('Should list and sort by trending in descending order', async function () {
320 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-trending' })
321
322 expect(total).to.equal(6)
323 expect(data.length).to.equal(2)
324 })
325
326 it('Should list and sort by hotness in descending order', async function () {
327 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-hot' })
328
329 expect(total).to.equal(6)
330 expect(data.length).to.equal(2)
331 })
332
333 it('Should list and sort by best in descending order', async function () {
334 const { total, data } = await server.videos.list({ start: 0, count: 2, sort: '-best' })
335
336 expect(total).to.equal(6)
337 expect(data.length).to.equal(2)
338 })
339
340 it('Should update a video', async function () {
341 const attributes = {
342 name: 'my super video updated',
343 category: 4,
344 licence: 2,
345 language: 'ar',
346 nsfw: false,
347 description: 'my super description updated',
348 commentsEnabled: false,
349 downloadEnabled: false,
350 tags: [ 'tagup1', 'tagup2' ]
351 }
352 await server.videos.update({ id: videoId, attributes })
353 })
354
355 it('Should have the video updated', async function () {
356 this.timeout(60000)
357
358 await waitJobs([ server ])
359
360 const video = await server.videos.get({ id: videoId })
361
362 await completeVideoCheck({ server, originServer: server, videoUUID: video.uuid, attributes: updateCheckAttributes() })
363 })
364
365 it('Should update only the tags of a video', async function () {
366 const attributes = {
367 tags: [ 'supertag', 'tag1', 'tag2' ]
368 }
369 await server.videos.update({ id: videoId, attributes })
370
371 const video = await server.videos.get({ id: videoId })
372
373 await completeVideoCheck({
374 server,
375 originServer: server,
376 videoUUID: video.uuid,
377 attributes: Object.assign(updateCheckAttributes(), attributes)
378 })
379 })
380
381 it('Should update only the description of a video', async function () {
382 const attributes = {
383 description: 'hello everybody'
384 }
385 await server.videos.update({ id: videoId, attributes })
386
387 const video = await server.videos.get({ id: videoId })
388
389 await completeVideoCheck({
390 server,
391 originServer: server,
392 videoUUID: video.uuid,
393 attributes: Object.assign(updateCheckAttributes(), { tags: [ 'supertag', 'tag1', 'tag2' ] }, attributes)
394 })
395 })
396
397 it('Should like a video', async function () {
398 await server.videos.rate({ id: videoId, rating: 'like' })
399
400 const video = await server.videos.get({ id: videoId })
401
402 expect(video.likes).to.equal(1)
403 expect(video.dislikes).to.equal(0)
404 })
405
406 it('Should dislike the same video', async function () {
407 await server.videos.rate({ id: videoId, rating: 'dislike' })
408
409 const video = await server.videos.get({ id: videoId })
410
411 expect(video.likes).to.equal(0)
412 expect(video.dislikes).to.equal(1)
413 })
414
415 it('Should sort by originallyPublishedAt', async function () {
416 {
417 const now = new Date()
418 const attributes = { originallyPublishedAt: now.toISOString() }
419 await server.videos.update({ id: videoId, attributes })
420
421 const { data } = await server.videos.list({ sort: '-originallyPublishedAt' })
422 const names = data.map(v => v.name)
423
424 expect(names[0]).to.equal('my super video updated')
425 expect(names[1]).to.equal('video_short2.webm name')
426 expect(names[2]).to.equal('video_short1.webm name')
427 expect(names[3]).to.equal('video_short.webm name')
428 expect(names[4]).to.equal('video_short.ogv name')
429 expect(names[5]).to.equal('video_short.mp4 name')
430 }
431
432 {
433 const now = new Date()
434 const attributes = { originallyPublishedAt: now.toISOString() }
435 await server.videos.update({ id: videoId2, attributes })
436
437 const { data } = await server.videos.list({ sort: '-originallyPublishedAt' })
438 const names = data.map(v => v.name)
439
440 expect(names[0]).to.equal('video_short1.webm name')
441 expect(names[1]).to.equal('my super video updated')
442 expect(names[2]).to.equal('video_short2.webm name')
443 expect(names[3]).to.equal('video_short.webm name')
444 expect(names[4]).to.equal('video_short.ogv name')
445 expect(names[5]).to.equal('video_short.mp4 name')
446 }
447 })
448
449 after(async function () {
450 await cleanupTests([ server ])
451 })
452 }
453
454 describe('Legacy upload', function () {
455 runSuite('legacy')
456 })
457
458 describe('Resumable upload', function () {
459 runSuite('resumable')
460 })
461})
diff --git a/packages/tests/src/api/videos/video-captions.ts b/packages/tests/src/api/videos/video-captions.ts
new file mode 100644
index 000000000..027022549
--- /dev/null
+++ b/packages/tests/src/api/videos/video-captions.ts
@@ -0,0 +1,189 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13import { testCaptionFile } from '@tests/shared/captions.js'
14import { checkVideoFilesWereRemoved } from '@tests/shared/videos.js'
15
16describe('Test video captions', function () {
17 const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
18
19 let servers: PeerTubeServer[]
20 let videoUUID: string
21
22 before(async function () {
23 this.timeout(60000)
24
25 servers = await createMultipleServers(2)
26
27 await setAccessTokensToServers(servers)
28 await doubleFollow(servers[0], servers[1])
29
30 await waitJobs(servers)
31
32 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video name' } })
33 videoUUID = uuid
34
35 await waitJobs(servers)
36 })
37
38 it('Should list the captions and return an empty list', async function () {
39 for (const server of servers) {
40 const body = await server.captions.list({ videoId: videoUUID })
41 expect(body.total).to.equal(0)
42 expect(body.data).to.have.lengthOf(0)
43 }
44 })
45
46 it('Should create two new captions', async function () {
47 this.timeout(30000)
48
49 await servers[0].captions.add({
50 language: 'ar',
51 videoId: videoUUID,
52 fixture: 'subtitle-good1.vtt'
53 })
54
55 await servers[0].captions.add({
56 language: 'zh',
57 videoId: videoUUID,
58 fixture: 'subtitle-good2.vtt',
59 mimeType: 'application/octet-stream'
60 })
61
62 await waitJobs(servers)
63 })
64
65 it('Should list these uploaded captions', async function () {
66 for (const server of servers) {
67 const body = await server.captions.list({ videoId: videoUUID })
68 expect(body.total).to.equal(2)
69 expect(body.data).to.have.lengthOf(2)
70
71 const caption1 = body.data[0]
72 expect(caption1.language.id).to.equal('ar')
73 expect(caption1.language.label).to.equal('Arabic')
74 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
75 await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 1.')
76
77 const caption2 = body.data[1]
78 expect(caption2.language.id).to.equal('zh')
79 expect(caption2.language.label).to.equal('Chinese')
80 expect(caption2.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$'))
81 await testCaptionFile(server.url, caption2.captionPath, 'Subtitle good 2.')
82 }
83 })
84
85 it('Should replace an existing caption', async function () {
86 this.timeout(30000)
87
88 await servers[0].captions.add({
89 language: 'ar',
90 videoId: videoUUID,
91 fixture: 'subtitle-good2.vtt'
92 })
93
94 await waitJobs(servers)
95 })
96
97 it('Should have this caption updated', async function () {
98 for (const server of servers) {
99 const body = await server.captions.list({ videoId: videoUUID })
100 expect(body.total).to.equal(2)
101 expect(body.data).to.have.lengthOf(2)
102
103 const caption1 = body.data[0]
104 expect(caption1.language.id).to.equal('ar')
105 expect(caption1.language.label).to.equal('Arabic')
106 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
107 await testCaptionFile(server.url, caption1.captionPath, 'Subtitle good 2.')
108 }
109 })
110
111 it('Should replace an existing caption with a srt file and convert it', async function () {
112 this.timeout(30000)
113
114 await servers[0].captions.add({
115 language: 'ar',
116 videoId: videoUUID,
117 fixture: 'subtitle-good.srt'
118 })
119
120 await waitJobs(servers)
121
122 // Cache invalidation
123 await wait(3000)
124 })
125
126 it('Should have this caption updated and converted', async function () {
127 for (const server of servers) {
128 const body = await server.captions.list({ videoId: videoUUID })
129 expect(body.total).to.equal(2)
130 expect(body.data).to.have.lengthOf(2)
131
132 const caption1 = body.data[0]
133 expect(caption1.language.id).to.equal('ar')
134 expect(caption1.language.label).to.equal('Arabic')
135 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-ar.vtt$'))
136
137 const expected = 'WEBVTT FILE\r\n' +
138 '\r\n' +
139 '1\r\n' +
140 '00:00:01.600 --> 00:00:04.200\r\n' +
141 'English (US)\r\n' +
142 '\r\n' +
143 '2\r\n' +
144 '00:00:05.900 --> 00:00:07.999\r\n' +
145 'This is a subtitle in American English\r\n' +
146 '\r\n' +
147 '3\r\n' +
148 '00:00:10.000 --> 00:00:14.000\r\n' +
149 'Adding subtitles is very easy to do\r\n'
150 await testCaptionFile(server.url, caption1.captionPath, expected)
151 }
152 })
153
154 it('Should remove one caption', async function () {
155 this.timeout(30000)
156
157 await servers[0].captions.delete({ videoId: videoUUID, language: 'ar' })
158
159 await waitJobs(servers)
160 })
161
162 it('Should only list the caption that was not deleted', async function () {
163 for (const server of servers) {
164 const body = await server.captions.list({ videoId: videoUUID })
165 expect(body.total).to.equal(1)
166 expect(body.data).to.have.lengthOf(1)
167
168 const caption = body.data[0]
169
170 expect(caption.language.id).to.equal('zh')
171 expect(caption.language.label).to.equal('Chinese')
172 expect(caption.captionPath).to.match(new RegExp('^/lazy-static/video-captions/' + uuidRegex + '-zh.vtt$'))
173 await testCaptionFile(server.url, caption.captionPath, 'Subtitle good 2.')
174 }
175 })
176
177 it('Should remove the video, and thus all video captions', async function () {
178 const video = await servers[0].videos.get({ id: videoUUID })
179 const { data: captions } = await servers[0].captions.list({ videoId: videoUUID })
180
181 await servers[0].videos.remove({ id: videoUUID })
182
183 await checkVideoFilesWereRemoved({ server: servers[0], video, captions })
184 })
185
186 after(async function () {
187 await cleanupTests(servers)
188 })
189})
diff --git a/packages/tests/src/api/videos/video-change-ownership.ts b/packages/tests/src/api/videos/video-change-ownership.ts
new file mode 100644
index 000000000..717c37469
--- /dev/null
+++ b/packages/tests/src/api/videos/video-change-ownership.ts
@@ -0,0 +1,314 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 ChangeOwnershipCommand,
6 cleanupTests,
7 createMultipleServers,
8 createSingleServer,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
16
17describe('Test video change ownership - nominal', function () {
18 let servers: PeerTubeServer[] = []
19
20 const firstUser = 'first'
21 const secondUser = 'second'
22
23 let firstUserToken = ''
24 let firstUserChannelId: number
25
26 let secondUserToken = ''
27 let secondUserChannelId: number
28
29 let lastRequestId: number
30
31 let liveId: number
32
33 let command: ChangeOwnershipCommand
34
35 before(async function () {
36 this.timeout(240000)
37
38 servers = await createMultipleServers(2)
39 await setAccessTokensToServers(servers)
40 await setDefaultVideoChannel(servers)
41
42 await servers[0].config.updateCustomSubConfig({
43 newConfig: {
44 transcoding: {
45 enabled: false
46 },
47 live: {
48 enabled: true
49 }
50 }
51 })
52
53 firstUserToken = await servers[0].users.generateUserAndToken(firstUser)
54 secondUserToken = await servers[0].users.generateUserAndToken(secondUser)
55
56 {
57 const { videoChannels } = await servers[0].users.getMyInfo({ token: firstUserToken })
58 firstUserChannelId = videoChannels[0].id
59 }
60
61 {
62 const { videoChannels } = await servers[0].users.getMyInfo({ token: secondUserToken })
63 secondUserChannelId = videoChannels[0].id
64 }
65
66 {
67 const attributes = {
68 name: 'my super name',
69 description: 'my super description'
70 }
71 const { id } = await servers[0].videos.upload({ token: firstUserToken, attributes })
72
73 servers[0].store.videoCreated = await servers[0].videos.get({ id })
74 }
75
76 {
77 const attributes = { name: 'live', channelId: firstUserChannelId, privacy: VideoPrivacy.PUBLIC }
78 const video = await servers[0].live.create({ token: firstUserToken, fields: attributes })
79
80 liveId = video.id
81 }
82
83 command = servers[0].changeOwnership
84
85 await doubleFollow(servers[0], servers[1])
86 })
87
88 it('Should not have video change ownership', async function () {
89 {
90 const body = await command.list({ token: firstUserToken })
91
92 expect(body.total).to.equal(0)
93 expect(body.data).to.be.an('array')
94 expect(body.data.length).to.equal(0)
95 }
96
97 {
98 const body = await command.list({ token: secondUserToken })
99
100 expect(body.total).to.equal(0)
101 expect(body.data).to.be.an('array')
102 expect(body.data.length).to.equal(0)
103 }
104 })
105
106 it('Should send a request to change ownership of a video', async function () {
107 this.timeout(15000)
108
109 await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
110 })
111
112 it('Should only return a request to change ownership for the second user', async function () {
113 {
114 const body = await command.list({ token: firstUserToken })
115
116 expect(body.total).to.equal(0)
117 expect(body.data).to.be.an('array')
118 expect(body.data.length).to.equal(0)
119 }
120
121 {
122 const body = await command.list({ token: secondUserToken })
123
124 expect(body.total).to.equal(1)
125 expect(body.data).to.be.an('array')
126 expect(body.data.length).to.equal(1)
127
128 lastRequestId = body.data[0].id
129 }
130 })
131
132 it('Should accept the same change ownership request without crashing', async function () {
133 await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
134 })
135
136 it('Should not create multiple change ownership requests while one is waiting', async function () {
137 const body = await command.list({ token: secondUserToken })
138
139 expect(body.total).to.equal(1)
140 expect(body.data).to.be.an('array')
141 expect(body.data.length).to.equal(1)
142 })
143
144 it('Should not be possible to refuse the change of ownership from first user', async function () {
145 await command.refuse({ token: firstUserToken, ownershipId: lastRequestId, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
146 })
147
148 it('Should be possible to refuse the change of ownership from second user', async function () {
149 await command.refuse({ token: secondUserToken, ownershipId: lastRequestId })
150 })
151
152 it('Should send a new request to change ownership of a video', async function () {
153 this.timeout(15000)
154
155 await command.create({ token: firstUserToken, videoId: servers[0].store.videoCreated.id, username: secondUser })
156 })
157
158 it('Should return two requests to change ownership for the second user', async function () {
159 {
160 const body = await command.list({ token: firstUserToken })
161
162 expect(body.total).to.equal(0)
163 expect(body.data).to.be.an('array')
164 expect(body.data.length).to.equal(0)
165 }
166
167 {
168 const body = await command.list({ token: secondUserToken })
169
170 expect(body.total).to.equal(2)
171 expect(body.data).to.be.an('array')
172 expect(body.data.length).to.equal(2)
173
174 lastRequestId = body.data[0].id
175 }
176 })
177
178 it('Should not be possible to accept the change of ownership from first user', async function () {
179 await command.accept({
180 token: firstUserToken,
181 ownershipId: lastRequestId,
182 channelId: secondUserChannelId,
183 expectedStatus: HttpStatusCode.FORBIDDEN_403
184 })
185 })
186
187 it('Should be possible to accept the change of ownership from second user', async function () {
188 await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId })
189
190 await waitJobs(servers)
191 })
192
193 it('Should have the channel of the video updated', async function () {
194 for (const server of servers) {
195 const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid })
196
197 expect(video.name).to.equal('my super name')
198 expect(video.channel.displayName).to.equal('Main second channel')
199 expect(video.channel.name).to.equal('second_channel')
200 }
201 })
202
203 it('Should send a request to change ownership of a live', async function () {
204 this.timeout(15000)
205
206 await command.create({ token: firstUserToken, videoId: liveId, username: secondUser })
207
208 const body = await command.list({ token: secondUserToken })
209
210 expect(body.total).to.equal(3)
211 expect(body.data.length).to.equal(3)
212
213 lastRequestId = body.data[0].id
214 })
215
216 it('Should accept a live ownership change', async function () {
217 this.timeout(20000)
218
219 await command.accept({ token: secondUserToken, ownershipId: lastRequestId, channelId: secondUserChannelId })
220
221 await waitJobs(servers)
222
223 for (const server of servers) {
224 const video = await server.videos.get({ id: servers[0].store.videoCreated.uuid })
225
226 expect(video.name).to.equal('my super name')
227 expect(video.channel.displayName).to.equal('Main second channel')
228 expect(video.channel.name).to.equal('second_channel')
229 }
230 })
231
232 after(async function () {
233 await cleanupTests(servers)
234 })
235})
236
237describe('Test video change ownership - quota too small', function () {
238 let server: PeerTubeServer
239 const firstUser = 'first'
240 const secondUser = 'second'
241
242 let firstUserToken = ''
243 let secondUserToken = ''
244 let lastRequestId: number
245
246 before(async function () {
247 this.timeout(50000)
248
249 // Run one server
250 server = await createSingleServer(1)
251 await setAccessTokensToServers([ server ])
252
253 await server.users.create({ username: secondUser, videoQuota: 10 })
254
255 firstUserToken = await server.users.generateUserAndToken(firstUser)
256 secondUserToken = await server.login.getAccessToken(secondUser)
257
258 // Upload some videos on the server
259 const attributes = {
260 name: 'my super name',
261 description: 'my super description'
262 }
263 await server.videos.upload({ token: firstUserToken, attributes })
264
265 await waitJobs(server)
266
267 const { data } = await server.videos.list()
268 expect(data.length).to.equal(1)
269
270 server.store.videoCreated = data.find(video => video.name === 'my super name')
271 })
272
273 it('Should send a request to change ownership of a video', async function () {
274 this.timeout(15000)
275
276 await server.changeOwnership.create({ token: firstUserToken, videoId: server.store.videoCreated.id, username: secondUser })
277 })
278
279 it('Should only return a request to change ownership for the second user', async function () {
280 {
281 const body = await server.changeOwnership.list({ token: firstUserToken })
282
283 expect(body.total).to.equal(0)
284 expect(body.data).to.be.an('array')
285 expect(body.data.length).to.equal(0)
286 }
287
288 {
289 const body = await server.changeOwnership.list({ token: secondUserToken })
290
291 expect(body.total).to.equal(1)
292 expect(body.data).to.be.an('array')
293 expect(body.data.length).to.equal(1)
294
295 lastRequestId = body.data[0].id
296 }
297 })
298
299 it('Should not be possible to accept the change of ownership from second user because of exceeded quota', async function () {
300 const { videoChannels } = await server.users.getMyInfo({ token: secondUserToken })
301 const channelId = videoChannels[0].id
302
303 await server.changeOwnership.accept({
304 token: secondUserToken,
305 ownershipId: lastRequestId,
306 channelId,
307 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
308 })
309 })
310
311 after(async function () {
312 await cleanupTests([ server ])
313 })
314})
diff --git a/packages/tests/src/api/videos/video-channel-syncs.ts b/packages/tests/src/api/videos/video-channel-syncs.ts
new file mode 100644
index 000000000..54212bcb5
--- /dev/null
+++ b/packages/tests/src/api/videos/video-channel-syncs.ts
@@ -0,0 +1,321 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
5import { VideoChannelSyncState, VideoInclude, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 getServerImportConfig,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar,
14 setDefaultVideoChannel,
15 waitJobs
16} from '@peertube/peertube-server-commands'
17import { SQLCommand } from '@tests/shared/sql-command.js'
18import { FIXTURE_URLS } from '@tests/shared/tests.js'
19
20describe('Test channel synchronizations', function () {
21 if (areHttpImportTestsDisabled()) return
22
23 function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
24
25 describe('Sync using ' + mode, function () {
26 let servers: PeerTubeServer[]
27 let sqlCommands: SQLCommand[] = []
28
29 let startTestDate: Date
30
31 let rootChannelSyncId: number
32 const userInfo = {
33 accessToken: '',
34 username: 'user1',
35 channelName: 'user1_channel',
36 channelId: -1,
37 syncId: -1
38 }
39
40 async function changeDateForSync (channelSyncId: number, newDate: string) {
41 await sqlCommands[0].updateQuery(
42 `UPDATE "videoChannelSync" ` +
43 `SET "createdAt"='${newDate}', "lastSyncAt"='${newDate}' ` +
44 `WHERE id=${channelSyncId}`
45 )
46 }
47
48 async function listAllVideosOfChannel (channelName: string) {
49 return servers[0].videos.listByChannel({
50 handle: channelName,
51 include: VideoInclude.NOT_PUBLISHED_STATE
52 })
53 }
54
55 async function forceSyncAll (videoChannelSyncId: number, fromDate = '1970-01-01') {
56 await changeDateForSync(videoChannelSyncId, fromDate)
57
58 await servers[0].debug.sendCommand({
59 body: {
60 command: 'process-video-channel-sync-latest'
61 }
62 })
63
64 await waitJobs(servers)
65 }
66
67 before(async function () {
68 this.timeout(240_000)
69
70 startTestDate = new Date()
71
72 servers = await createMultipleServers(2, getServerImportConfig(mode))
73
74 await setAccessTokensToServers(servers)
75 await setDefaultVideoChannel(servers)
76 await setDefaultChannelAvatar(servers)
77 await setDefaultAccountAvatar(servers)
78
79 await servers[0].config.enableChannelSync()
80
81 {
82 userInfo.accessToken = await servers[0].users.generateUserAndToken(userInfo.username)
83
84 const { videoChannels } = await servers[0].users.getMyInfo({ token: userInfo.accessToken })
85 userInfo.channelId = videoChannels[0].id
86 }
87
88 sqlCommands = servers.map(s => new SQLCommand(s))
89 })
90
91 it('Should fetch the latest channel videos of a remote channel', async function () {
92 this.timeout(120_000)
93
94 {
95 const { video } = await servers[0].imports.importVideo({
96 attributes: {
97 channelId: servers[0].store.channel.id,
98 privacy: VideoPrivacy.PUBLIC,
99 targetUrl: FIXTURE_URLS.youtube
100 }
101 })
102
103 expect(video.name).to.equal('small video - youtube')
104 expect(video.waitTranscoding).to.be.true
105
106 const { total } = await listAllVideosOfChannel('root_channel')
107 expect(total).to.equal(1)
108 }
109
110 const { videoChannelSync } = await servers[0].channelSyncs.create({
111 attributes: {
112 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
113 videoChannelId: servers[0].store.channel.id
114 }
115 })
116 rootChannelSyncId = videoChannelSync.id
117
118 await forceSyncAll(rootChannelSyncId)
119
120 {
121 const { total, data } = await listAllVideosOfChannel('root_channel')
122 expect(total).to.equal(2)
123 expect(data[0].name).to.equal('test')
124 expect(data[0].waitTranscoding).to.be.true
125 }
126 })
127
128 it('Should add another synchronization', async function () {
129 const externalChannelUrl = FIXTURE_URLS.youtubeChannel + '?foo=bar'
130
131 const { videoChannelSync } = await servers[0].channelSyncs.create({
132 attributes: {
133 externalChannelUrl,
134 videoChannelId: servers[0].store.channel.id
135 }
136 })
137
138 expect(videoChannelSync.externalChannelUrl).to.equal(externalChannelUrl)
139 expect(videoChannelSync.channel.id).to.equal(servers[0].store.channel.id)
140 expect(videoChannelSync.channel.name).to.equal('root_channel')
141 expect(videoChannelSync.state.id).to.equal(VideoChannelSyncState.WAITING_FIRST_RUN)
142 expect(new Date(videoChannelSync.createdAt)).to.be.above(startTestDate).and.to.be.at.most(new Date())
143 })
144
145 it('Should add a synchronization for another user', async function () {
146 const { videoChannelSync } = await servers[0].channelSyncs.create({
147 attributes: {
148 externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux',
149 videoChannelId: userInfo.channelId
150 },
151 token: userInfo.accessToken
152 })
153 userInfo.syncId = videoChannelSync.id
154 })
155
156 it('Should not import a channel if not asked', async function () {
157 await waitJobs(servers)
158
159 const { data } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username })
160
161 expect(data[0].state).to.contain({
162 id: VideoChannelSyncState.WAITING_FIRST_RUN,
163 label: 'Waiting first run'
164 })
165 })
166
167 it('Should only fetch the videos newer than the creation date', async function () {
168 this.timeout(120_000)
169
170 await forceSyncAll(userInfo.syncId, '2019-03-01')
171
172 const { data, total } = await listAllVideosOfChannel(userInfo.channelName)
173
174 expect(total).to.equal(1)
175 expect(data[0].name).to.equal('test')
176 })
177
178 it('Should list channel synchronizations', async function () {
179 // Root
180 {
181 const { total, data } = await servers[0].channelSyncs.listByAccount({ accountName: 'root' })
182 expect(total).to.equal(2)
183
184 expect(data[0]).to.deep.contain({
185 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
186 state: {
187 id: VideoChannelSyncState.SYNCED,
188 label: 'Synchronized'
189 }
190 })
191
192 expect(new Date(data[0].lastSyncAt)).to.be.greaterThan(startTestDate)
193
194 expect(data[0].channel).to.contain({ id: servers[0].store.channel.id })
195 expect(data[1]).to.contain({ externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?foo=bar' })
196 }
197
198 // User
199 {
200 const { total, data } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username })
201 expect(total).to.equal(1)
202 expect(data[0]).to.deep.contain({
203 externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux',
204 state: {
205 id: VideoChannelSyncState.SYNCED,
206 label: 'Synchronized'
207 }
208 })
209 }
210 })
211
212 it('Should list imports of a channel synchronization', async function () {
213 const { total, data } = await servers[0].imports.getMyVideoImports({ videoChannelSyncId: rootChannelSyncId })
214
215 expect(total).to.equal(1)
216 expect(data).to.have.lengthOf(1)
217 expect(data[0].video.name).to.equal('test')
218 })
219
220 it('Should remove user\'s channel synchronizations', async function () {
221 await servers[0].channelSyncs.delete({ channelSyncId: userInfo.syncId })
222
223 const { total } = await servers[0].channelSyncs.listByAccount({ accountName: userInfo.username })
224 expect(total).to.equal(0)
225 })
226
227 // FIXME: youtube-dl/yt-dlp doesn't work when speicifying a port after the hostname
228 // it('Should import a remote PeerTube channel', async function () {
229 // this.timeout(240_000)
230
231 // await servers[1].videos.quickUpload({ name: 'remote 1' })
232 // await waitJobs(servers)
233
234 // const { videoChannelSync } = await servers[0].channelSyncs.create({
235 // attributes: {
236 // externalChannelUrl: servers[1].url + '/c/root_channel',
237 // videoChannelId: userInfo.channelId
238 // },
239 // token: userInfo.accessToken
240 // })
241 // await servers[0].channels.importVideos({
242 // channelName: userInfo.channelName,
243 // externalChannelUrl: servers[1].url + '/c/root_channel',
244 // videoChannelSyncId: videoChannelSync.id,
245 // token: userInfo.accessToken
246 // })
247
248 // await waitJobs(servers)
249
250 // const { data, total } = await servers[0].videos.listByChannel({
251 // handle: userInfo.channelName,
252 // include: VideoInclude.NOT_PUBLISHED_STATE
253 // })
254
255 // expect(total).to.equal(2)
256 // expect(data[0].name).to.equal('remote 1')
257 // })
258
259 // it('Should keep synced a remote PeerTube channel', async function () {
260 // this.timeout(240_000)
261
262 // await servers[1].videos.quickUpload({ name: 'remote 2' })
263 // await waitJobs(servers)
264
265 // await servers[0].debug.sendCommand({
266 // body: {
267 // command: 'process-video-channel-sync-latest'
268 // }
269 // })
270
271 // await waitJobs(servers)
272
273 // const { data, total } = await servers[0].videos.listByChannel({
274 // handle: userInfo.channelName,
275 // include: VideoInclude.NOT_PUBLISHED_STATE
276 // })
277 // expect(total).to.equal(2)
278 // expect(data[0].name).to.equal('remote 2')
279 // })
280
281 it('Should fetch the latest videos of a youtube playlist', async function () {
282 this.timeout(120_000)
283
284 const { id: channelId } = await servers[0].channels.create({
285 attributes: {
286 name: 'channel2'
287 }
288 })
289
290 const { videoChannelSync: { id: videoChannelSyncId } } = await servers[0].channelSyncs.create({
291 attributes: {
292 externalChannelUrl: FIXTURE_URLS.youtubePlaylist,
293 videoChannelId: channelId
294 }
295 })
296
297 await forceSyncAll(videoChannelSyncId)
298
299 {
300
301 const { total, data } = await listAllVideosOfChannel('channel2')
302 expect(total).to.equal(2)
303 expect(data[0].name).to.equal('test')
304 expect(data[1].name).to.equal('small video - youtube')
305 }
306 })
307
308 after(async function () {
309 for (const sqlCommand of sqlCommands) {
310 await sqlCommand.cleanup()
311 }
312
313 await cleanupTests(servers)
314 })
315 })
316 }
317
318 // FIXME: suite is broken with youtube-dl
319 // runSuite('youtube-dl')
320 runSuite('yt-dlp')
321})
diff --git a/packages/tests/src/api/videos/video-channels.ts b/packages/tests/src/api/videos/video-channels.ts
new file mode 100644
index 000000000..64b1b9315
--- /dev/null
+++ b/packages/tests/src/api/videos/video-channels.ts
@@ -0,0 +1,556 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { basename } from 'path'
5import { ACTOR_IMAGES_SIZE } from '@peertube/peertube-server/server/initializers/constants.js'
6import { testFileExistsOrNot, testImage } from '@tests/shared/checks.js'
7import { SQLCommand } from '@tests/shared/sql-command.js'
8import { wait } from '@peertube/peertube-core-utils'
9import { ActorImageType, User, VideoChannel } from '@peertube/peertube-models'
10import {
11 cleanupTests,
12 createMultipleServers,
13 doubleFollow,
14 PeerTubeServer,
15 setAccessTokensToServers,
16 setDefaultAccountAvatar,
17 setDefaultVideoChannel,
18 waitJobs
19} from '@peertube/peertube-server-commands'
20
21async function findChannel (server: PeerTubeServer, channelId: number) {
22 const body = await server.channels.list({ sort: '-name' })
23
24 return body.data.find(c => c.id === channelId)
25}
26
27describe('Test video channels', function () {
28 let servers: PeerTubeServer[]
29 let sqlCommands: SQLCommand[] = []
30
31 let userInfo: User
32 let secondVideoChannelId: number
33 let totoChannel: number
34 let videoUUID: string
35 let accountName: string
36 let secondUserChannelName: string
37
38 const avatarPaths: { [ port: number ]: string } = {}
39 const bannerPaths: { [ port: number ]: string } = {}
40
41 before(async function () {
42 this.timeout(60000)
43
44 servers = await createMultipleServers(2)
45
46 await setAccessTokensToServers(servers)
47 await setDefaultVideoChannel(servers)
48 await setDefaultAccountAvatar(servers)
49
50 await doubleFollow(servers[0], servers[1])
51
52 sqlCommands = servers.map(s => new SQLCommand(s))
53 })
54
55 it('Should have one video channel (created with root)', async () => {
56 const body = await servers[0].channels.list({ start: 0, count: 2 })
57
58 expect(body.total).to.equal(1)
59 expect(body.data).to.be.an('array')
60 expect(body.data).to.have.lengthOf(1)
61 })
62
63 it('Should create another video channel', async function () {
64 this.timeout(30000)
65
66 {
67 const videoChannel = {
68 name: 'second_video_channel',
69 displayName: 'second video channel',
70 description: 'super video channel description',
71 support: 'super video channel support text'
72 }
73 const created = await servers[0].channels.create({ attributes: videoChannel })
74 secondVideoChannelId = created.id
75 }
76
77 // The channel is 1 is propagated to servers 2
78 {
79 const attributes = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' }
80 const { uuid } = await servers[0].videos.upload({ attributes })
81 videoUUID = uuid
82 }
83
84 await waitJobs(servers)
85 })
86
87 it('Should have two video channels when getting my information', async () => {
88 userInfo = await servers[0].users.getMyInfo()
89
90 expect(userInfo.videoChannels).to.be.an('array')
91 expect(userInfo.videoChannels).to.have.lengthOf(2)
92
93 const videoChannels = userInfo.videoChannels
94 expect(videoChannels[0].name).to.equal('root_channel')
95 expect(videoChannels[0].displayName).to.equal('Main root channel')
96
97 expect(videoChannels[1].name).to.equal('second_video_channel')
98 expect(videoChannels[1].displayName).to.equal('second video channel')
99 expect(videoChannels[1].description).to.equal('super video channel description')
100 expect(videoChannels[1].support).to.equal('super video channel support text')
101
102 accountName = userInfo.account.name + '@' + userInfo.account.host
103 })
104
105 it('Should have two video channels when getting account channels on server 1', async function () {
106 const body = await servers[0].channels.listByAccount({ accountName })
107 expect(body.total).to.equal(2)
108
109 const videoChannels = body.data
110
111 expect(videoChannels).to.be.an('array')
112 expect(videoChannels).to.have.lengthOf(2)
113
114 expect(videoChannels[0].name).to.equal('root_channel')
115 expect(videoChannels[0].displayName).to.equal('Main root channel')
116
117 expect(videoChannels[1].name).to.equal('second_video_channel')
118 expect(videoChannels[1].displayName).to.equal('second video channel')
119 expect(videoChannels[1].description).to.equal('super video channel description')
120 expect(videoChannels[1].support).to.equal('super video channel support text')
121 })
122
123 it('Should paginate and sort account channels', async function () {
124 {
125 const body = await servers[0].channels.listByAccount({
126 accountName,
127 start: 0,
128 count: 1,
129 sort: 'createdAt'
130 })
131
132 expect(body.total).to.equal(2)
133 expect(body.data).to.have.lengthOf(1)
134
135 const videoChannel: VideoChannel = body.data[0]
136 expect(videoChannel.name).to.equal('root_channel')
137 }
138
139 {
140 const body = await servers[0].channels.listByAccount({
141 accountName,
142 start: 0,
143 count: 1,
144 sort: '-createdAt'
145 })
146
147 expect(body.total).to.equal(2)
148 expect(body.data).to.have.lengthOf(1)
149 expect(body.data[0].name).to.equal('second_video_channel')
150 }
151
152 {
153 const body = await servers[0].channels.listByAccount({
154 accountName,
155 start: 1,
156 count: 1,
157 sort: '-createdAt'
158 })
159
160 expect(body.total).to.equal(2)
161 expect(body.data).to.have.lengthOf(1)
162 expect(body.data[0].name).to.equal('root_channel')
163 }
164 })
165
166 it('Should have one video channel when getting account channels on server 2', async function () {
167 const body = await servers[1].channels.listByAccount({ accountName })
168
169 expect(body.total).to.equal(1)
170 expect(body.data).to.be.an('array')
171 expect(body.data).to.have.lengthOf(1)
172
173 const videoChannel = body.data[0]
174 expect(videoChannel.name).to.equal('second_video_channel')
175 expect(videoChannel.displayName).to.equal('second video channel')
176 expect(videoChannel.description).to.equal('super video channel description')
177 expect(videoChannel.support).to.equal('super video channel support text')
178 })
179
180 it('Should list video channels', async function () {
181 const body = await servers[0].channels.list({ start: 1, count: 1, sort: '-name' })
182
183 expect(body.total).to.equal(2)
184 expect(body.data).to.be.an('array')
185 expect(body.data).to.have.lengthOf(1)
186 expect(body.data[0].name).to.equal('root_channel')
187 expect(body.data[0].displayName).to.equal('Main root channel')
188 })
189
190 it('Should update video channel', async function () {
191 this.timeout(15000)
192
193 const videoChannelAttributes = {
194 displayName: 'video channel updated',
195 description: 'video channel description updated',
196 support: 'support updated'
197 }
198
199 await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes })
200
201 await waitJobs(servers)
202 })
203
204 it('Should have video channel updated', async function () {
205 for (const server of servers) {
206 const body = await server.channels.list({ start: 0, count: 1, sort: '-name' })
207
208 expect(body.total).to.equal(2)
209 expect(body.data).to.be.an('array')
210 expect(body.data).to.have.lengthOf(1)
211
212 expect(body.data[0].name).to.equal('second_video_channel')
213 expect(body.data[0].displayName).to.equal('video channel updated')
214 expect(body.data[0].description).to.equal('video channel description updated')
215 expect(body.data[0].support).to.equal('support updated')
216 }
217 })
218
219 it('Should not have updated the video support field', async function () {
220 for (const server of servers) {
221 const video = await server.videos.get({ id: videoUUID })
222 expect(video.support).to.equal('video support field')
223 }
224 })
225
226 it('Should update another accounts video channel', async function () {
227 this.timeout(15000)
228
229 const result = await servers[0].users.generate('second_user')
230 secondUserChannelName = result.userChannelName
231
232 await servers[0].videos.quickUpload({ name: 'video', token: result.token })
233
234 const videoChannelAttributes = {
235 displayName: 'video channel updated',
236 description: 'video channel description updated',
237 support: 'support updated'
238 }
239
240 await servers[0].channels.update({ channelName: secondUserChannelName, attributes: videoChannelAttributes })
241
242 await waitJobs(servers)
243 })
244
245 it('Should have another accounts video channel updated', async function () {
246 for (const server of servers) {
247 const body = await server.channels.get({ channelName: `${secondUserChannelName}@${servers[0].host}` })
248
249 expect(body.displayName).to.equal('video channel updated')
250 expect(body.description).to.equal('video channel description updated')
251 expect(body.support).to.equal('support updated')
252 }
253 })
254
255 it('Should update the channel support field and update videos too', async function () {
256 this.timeout(35000)
257
258 const videoChannelAttributes = {
259 support: 'video channel support text updated',
260 bulkVideosSupportUpdate: true
261 }
262
263 await servers[0].channels.update({ channelName: 'second_video_channel', attributes: videoChannelAttributes })
264
265 await waitJobs(servers)
266
267 for (const server of servers) {
268 const video = await server.videos.get({ id: videoUUID })
269 expect(video.support).to.equal(videoChannelAttributes.support)
270 }
271 })
272
273 it('Should update video channel avatar', async function () {
274 this.timeout(15000)
275
276 const fixture = 'avatar.png'
277
278 await servers[0].channels.updateImage({
279 channelName: 'second_video_channel',
280 fixture,
281 type: 'avatar'
282 })
283
284 await waitJobs(servers)
285
286 for (let i = 0; i < servers.length; i++) {
287 const server = servers[i]
288
289 const videoChannel = await findChannel(server, secondVideoChannelId)
290 const expectedSizes = ACTOR_IMAGES_SIZE[ActorImageType.AVATAR]
291
292 expect(videoChannel.avatars.length).to.equal(expectedSizes.length, 'Expected avatars to be generated in all sizes')
293
294 for (const avatar of videoChannel.avatars) {
295 avatarPaths[server.port] = avatar.path
296 await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatarPaths[server.port], '.png')
297 await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), true)
298
299 const row = await sqlCommands[i].getActorImage(basename(avatarPaths[server.port]))
300
301 expect(expectedSizes.some(({ height, width }) => row.height === height && row.width === width)).to.equal(true)
302 }
303 }
304 })
305
306 it('Should update video channel banner', async function () {
307 this.timeout(15000)
308
309 const fixture = 'banner.jpg'
310
311 await servers[0].channels.updateImage({
312 channelName: 'second_video_channel',
313 fixture,
314 type: 'banner'
315 })
316
317 await waitJobs(servers)
318
319 for (let i = 0; i < servers.length; i++) {
320 const server = servers[i]
321
322 const videoChannel = await server.channels.get({ channelName: 'second_video_channel@' + servers[0].host })
323
324 bannerPaths[server.port] = videoChannel.banners[0].path
325 await testImage(server.url, 'banner-resized', bannerPaths[server.port])
326 await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), true)
327
328 const row = await sqlCommands[i].getActorImage(basename(bannerPaths[server.port]))
329 expect(row.height).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].height)
330 expect(row.width).to.equal(ACTOR_IMAGES_SIZE[ActorImageType.BANNER][0].width)
331 }
332 })
333
334 it('Should still correctly list channels', async function () {
335 {
336 const body = await servers[0].channels.list({ start: 1, count: 1, sort: 'createdAt' })
337
338 expect(body.total).to.equal(3)
339 expect(body.data).to.have.lengthOf(1)
340 expect(body.data[0].name).to.equal('second_video_channel')
341 }
342
343 {
344 const body = await servers[0].channels.listByAccount({ accountName, start: 1, count: 1, sort: 'createdAt' })
345
346 expect(body.total).to.equal(2)
347 expect(body.data).to.have.lengthOf(1)
348 expect(body.data[0].name).to.equal('second_video_channel')
349 }
350 })
351
352 it('Should delete the video channel avatar', async function () {
353 this.timeout(15000)
354 await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'avatar' })
355
356 await waitJobs(servers)
357
358 for (const server of servers) {
359 const videoChannel = await findChannel(server, secondVideoChannelId)
360 await testFileExistsOrNot(server, 'avatars', basename(avatarPaths[server.port]), false)
361
362 expect(videoChannel.avatars).to.be.empty
363 }
364 })
365
366 it('Should delete the video channel banner', async function () {
367 this.timeout(15000)
368
369 await servers[0].channels.deleteImage({ channelName: 'second_video_channel', type: 'banner' })
370
371 await waitJobs(servers)
372
373 for (const server of servers) {
374 const videoChannel = await findChannel(server, secondVideoChannelId)
375 await testFileExistsOrNot(server, 'avatars', basename(bannerPaths[server.port]), false)
376
377 expect(videoChannel.banners).to.be.empty
378 }
379 })
380
381 it('Should list the second video channel videos', async function () {
382 for (const server of servers) {
383 const channelURI = 'second_video_channel@' + servers[0].host
384 const { total, data } = await server.videos.listByChannel({ handle: channelURI })
385
386 expect(total).to.equal(1)
387 expect(data).to.be.an('array')
388 expect(data).to.have.lengthOf(1)
389 expect(data[0].name).to.equal('my video name')
390 }
391 })
392
393 it('Should change the video channel of a video', async function () {
394 await servers[0].videos.update({ id: videoUUID, attributes: { channelId: servers[0].store.channel.id } })
395
396 await waitJobs(servers)
397 })
398
399 it('Should list the first video channel videos', async function () {
400 for (const server of servers) {
401 {
402 const secondChannelURI = 'second_video_channel@' + servers[0].host
403 const { total } = await server.videos.listByChannel({ handle: secondChannelURI })
404 expect(total).to.equal(0)
405 }
406
407 {
408 const channelURI = 'root_channel@' + servers[0].host
409 const { total, data } = await server.videos.listByChannel({ handle: channelURI })
410 expect(total).to.equal(1)
411
412 expect(data).to.be.an('array')
413 expect(data).to.have.lengthOf(1)
414 expect(data[0].name).to.equal('my video name')
415 }
416 }
417 })
418
419 it('Should delete video channel', async function () {
420 await servers[0].channels.delete({ channelName: 'second_video_channel' })
421 })
422
423 it('Should have video channel deleted', async function () {
424 const body = await servers[0].channels.list({ start: 0, count: 10, sort: 'createdAt' })
425
426 expect(body.total).to.equal(2)
427 expect(body.data).to.be.an('array')
428 expect(body.data).to.have.lengthOf(2)
429 expect(body.data[0].displayName).to.equal('Main root channel')
430 expect(body.data[1].displayName).to.equal('video channel updated')
431 })
432
433 it('Should create the main channel with a suffix if there is a conflict', async function () {
434 {
435 const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' }
436 const created = await servers[0].channels.create({ attributes: videoChannel })
437 totoChannel = created.id
438 }
439
440 {
441 await servers[0].users.create({ username: 'toto', password: 'password' })
442 const accessToken = await servers[0].login.getAccessToken({ username: 'toto', password: 'password' })
443
444 const { videoChannels } = await servers[0].users.getMyInfo({ token: accessToken })
445 expect(videoChannels[0].name).to.equal('toto_channel-1')
446 }
447 })
448
449 it('Should report correct channel views per days', async function () {
450 {
451 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
452
453 for (const channel of data) {
454 expect(channel).to.haveOwnProperty('viewsPerDay')
455 expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today
456
457 for (const v of channel.viewsPerDay) {
458 expect(v.date).to.be.an('string')
459 expect(v.views).to.equal(0)
460 }
461 }
462 }
463
464 {
465 // video has been posted on channel servers[0].store.videoChannel.id since last update
466 await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1' })
467 await servers[0].views.simulateView({ id: videoUUID, xForwardedFor: '0.0.0.2,127.0.0.1' })
468
469 // Wait the repeatable job
470 await wait(8000)
471
472 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
473 const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id)
474 expect(channelWithView.viewsPerDay.slice(-1)[0].views).to.equal(2)
475 }
476 })
477
478 it('Should report correct total views count', async function () {
479 // check if there's the property
480 {
481 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
482
483 for (const channel of data) {
484 expect(channel).to.haveOwnProperty('totalViews')
485 expect(channel.totalViews).to.be.a('number')
486 }
487 }
488
489 // Check if the totalViews count can be updated
490 {
491 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
492 const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id)
493 expect(channelWithView.totalViews).to.equal(2)
494 }
495 })
496
497 it('Should report correct videos count', async function () {
498 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
499
500 const totoChannel = data.find(c => c.name === 'toto_channel')
501 const rootChannel = data.find(c => c.name === 'root_channel')
502
503 expect(rootChannel.videosCount).to.equal(1)
504 expect(totoChannel.videosCount).to.equal(0)
505 })
506
507 it('Should search among account video channels', async function () {
508 {
509 const body = await servers[0].channels.listByAccount({ accountName, search: 'root' })
510 expect(body.total).to.equal(1)
511
512 const channels = body.data
513 expect(channels).to.have.lengthOf(1)
514 }
515
516 {
517 const body = await servers[0].channels.listByAccount({ accountName, search: 'does not exist' })
518 expect(body.total).to.equal(0)
519
520 const channels = body.data
521 expect(channels).to.have.lengthOf(0)
522 }
523 })
524
525 it('Should list channels by updatedAt desc if a video has been uploaded', async function () {
526 this.timeout(30000)
527
528 await servers[0].videos.upload({ attributes: { channelId: totoChannel } })
529 await waitJobs(servers)
530
531 for (const server of servers) {
532 const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' })
533
534 expect(data[0].name).to.equal('toto_channel')
535 expect(data[1].name).to.equal('root_channel')
536 }
537
538 await servers[0].videos.upload({ attributes: { channelId: servers[0].store.channel.id } })
539 await waitJobs(servers)
540
541 for (const server of servers) {
542 const { data } = await server.channels.listByAccount({ accountName, sort: '-updatedAt' })
543
544 expect(data[0].name).to.equal('root_channel')
545 expect(data[1].name).to.equal('toto_channel')
546 }
547 })
548
549 after(async function () {
550 for (const sqlCommand of sqlCommands) {
551 await sqlCommand.cleanup()
552 }
553
554 await cleanupTests(servers)
555 })
556})
diff --git a/packages/tests/src/api/videos/video-comments.ts b/packages/tests/src/api/videos/video-comments.ts
new file mode 100644
index 000000000..f17db9979
--- /dev/null
+++ b/packages/tests/src/api/videos/video-comments.ts
@@ -0,0 +1,335 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { dateIsValid, testImage } from '@tests/shared/checks.js'
5import {
6 cleanupTests,
7 CommentsCommand,
8 createSingleServer,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 setDefaultAccountAvatar,
12 setDefaultChannelAvatar
13} from '@peertube/peertube-server-commands'
14
15describe('Test video comments', function () {
16 let server: PeerTubeServer
17 let videoId: number
18 let videoUUID: string
19 let threadId: number
20 let replyToDeleteId: number
21
22 let userAccessTokenServer1: string
23
24 let command: CommentsCommand
25
26 before(async function () {
27 this.timeout(120000)
28
29 server = await createSingleServer(1)
30
31 await setAccessTokensToServers([ server ])
32
33 const { id, uuid } = await server.videos.upload()
34 videoUUID = uuid
35 videoId = id
36
37 await setDefaultChannelAvatar(server)
38 await setDefaultAccountAvatar(server)
39
40 userAccessTokenServer1 = await server.users.generateUserAndToken('user1')
41 await setDefaultChannelAvatar(server, 'user1_channel')
42 await setDefaultAccountAvatar(server, userAccessTokenServer1)
43
44 command = server.comments
45 })
46
47 describe('User comments', function () {
48
49 it('Should not have threads on this video', async function () {
50 const body = await command.listThreads({ videoId: videoUUID })
51
52 expect(body.total).to.equal(0)
53 expect(body.totalNotDeletedComments).to.equal(0)
54 expect(body.data).to.be.an('array')
55 expect(body.data).to.have.lengthOf(0)
56 })
57
58 it('Should create a thread in this video', async function () {
59 const text = 'my super first comment'
60
61 const comment = await command.createThread({ videoId: videoUUID, text })
62
63 expect(comment.inReplyToCommentId).to.be.null
64 expect(comment.text).equal('my super first comment')
65 expect(comment.videoId).to.equal(videoId)
66 expect(comment.id).to.equal(comment.threadId)
67 expect(comment.account.name).to.equal('root')
68 expect(comment.account.host).to.equal(server.host)
69 expect(comment.account.url).to.equal(server.url + '/accounts/root')
70 expect(comment.totalReplies).to.equal(0)
71 expect(comment.totalRepliesFromVideoAuthor).to.equal(0)
72 expect(dateIsValid(comment.createdAt as string)).to.be.true
73 expect(dateIsValid(comment.updatedAt as string)).to.be.true
74 })
75
76 it('Should list threads of this video', async function () {
77 const body = await command.listThreads({ videoId: videoUUID })
78
79 expect(body.total).to.equal(1)
80 expect(body.totalNotDeletedComments).to.equal(1)
81 expect(body.data).to.be.an('array')
82 expect(body.data).to.have.lengthOf(1)
83
84 const comment = body.data[0]
85 expect(comment.inReplyToCommentId).to.be.null
86 expect(comment.text).equal('my super first comment')
87 expect(comment.videoId).to.equal(videoId)
88 expect(comment.id).to.equal(comment.threadId)
89 expect(comment.account.name).to.equal('root')
90 expect(comment.account.host).to.equal(server.host)
91
92 for (const avatar of comment.account.avatars) {
93 await testImage(server.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png')
94 }
95
96 expect(comment.totalReplies).to.equal(0)
97 expect(comment.totalRepliesFromVideoAuthor).to.equal(0)
98 expect(dateIsValid(comment.createdAt as string)).to.be.true
99 expect(dateIsValid(comment.updatedAt as string)).to.be.true
100
101 threadId = comment.threadId
102 })
103
104 it('Should get all the thread created', async function () {
105 const body = await command.getThread({ videoId: videoUUID, threadId })
106
107 const rootComment = body.comment
108 expect(rootComment.inReplyToCommentId).to.be.null
109 expect(rootComment.text).equal('my super first comment')
110 expect(rootComment.videoId).to.equal(videoId)
111 expect(dateIsValid(rootComment.createdAt as string)).to.be.true
112 expect(dateIsValid(rootComment.updatedAt as string)).to.be.true
113 })
114
115 it('Should create multiple replies in this thread', async function () {
116 const text1 = 'my super answer to thread 1'
117 const created = await command.addReply({ videoId, toCommentId: threadId, text: text1 })
118 const childCommentId = created.id
119
120 const text2 = 'my super answer to answer of thread 1'
121 await command.addReply({ videoId, toCommentId: childCommentId, text: text2 })
122
123 const text3 = 'my second answer to thread 1'
124 await command.addReply({ videoId, toCommentId: threadId, text: text3 })
125 })
126
127 it('Should get correctly the replies', async function () {
128 const tree = await command.getThread({ videoId: videoUUID, threadId })
129
130 expect(tree.comment.text).equal('my super first comment')
131 expect(tree.children).to.have.lengthOf(2)
132
133 const firstChild = tree.children[0]
134 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
135 expect(firstChild.children).to.have.lengthOf(1)
136
137 const childOfFirstChild = firstChild.children[0]
138 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
139 expect(childOfFirstChild.children).to.have.lengthOf(0)
140
141 const secondChild = tree.children[1]
142 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
143 expect(secondChild.children).to.have.lengthOf(0)
144
145 replyToDeleteId = secondChild.comment.id
146 })
147
148 it('Should create other threads', async function () {
149 const text1 = 'super thread 2'
150 await command.createThread({ videoId: videoUUID, text: text1 })
151
152 const text2 = 'super thread 3'
153 await command.createThread({ videoId: videoUUID, text: text2 })
154 })
155
156 it('Should list the threads', async function () {
157 const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
158
159 expect(body.total).to.equal(3)
160 expect(body.totalNotDeletedComments).to.equal(6)
161 expect(body.data).to.be.an('array')
162 expect(body.data).to.have.lengthOf(3)
163
164 expect(body.data[0].text).to.equal('my super first comment')
165 expect(body.data[0].totalReplies).to.equal(3)
166 expect(body.data[1].text).to.equal('super thread 2')
167 expect(body.data[1].totalReplies).to.equal(0)
168 expect(body.data[2].text).to.equal('super thread 3')
169 expect(body.data[2].totalReplies).to.equal(0)
170 })
171
172 it('Should list the and sort them by total replies', async function () {
173 const body = await command.listThreads({ videoId: videoUUID, sort: 'totalReplies' })
174
175 expect(body.data[2].text).to.equal('my super first comment')
176 expect(body.data[2].totalReplies).to.equal(3)
177 })
178
179 it('Should delete a reply', async function () {
180 await command.delete({ videoId, commentId: replyToDeleteId })
181
182 {
183 const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
184
185 expect(body.total).to.equal(3)
186 expect(body.totalNotDeletedComments).to.equal(5)
187 }
188
189 {
190 const tree = await command.getThread({ videoId: videoUUID, threadId })
191
192 expect(tree.comment.text).equal('my super first comment')
193 expect(tree.children).to.have.lengthOf(2)
194
195 const firstChild = tree.children[0]
196 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
197 expect(firstChild.children).to.have.lengthOf(1)
198
199 const childOfFirstChild = firstChild.children[0]
200 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
201 expect(childOfFirstChild.children).to.have.lengthOf(0)
202
203 const deletedChildOfFirstChild = tree.children[1]
204 expect(deletedChildOfFirstChild.comment.text).to.equal('')
205 expect(deletedChildOfFirstChild.comment.isDeleted).to.be.true
206 expect(deletedChildOfFirstChild.comment.deletedAt).to.not.be.null
207 expect(deletedChildOfFirstChild.comment.account).to.be.null
208 expect(deletedChildOfFirstChild.children).to.have.lengthOf(0)
209 }
210 })
211
212 it('Should delete a complete thread', async function () {
213 await command.delete({ videoId, commentId: threadId })
214
215 const body = await command.listThreads({ videoId: videoUUID, sort: 'createdAt' })
216 expect(body.total).to.equal(3)
217 expect(body.data).to.be.an('array')
218 expect(body.data).to.have.lengthOf(3)
219
220 expect(body.data[0].text).to.equal('')
221 expect(body.data[0].isDeleted).to.be.true
222 expect(body.data[0].deletedAt).to.not.be.null
223 expect(body.data[0].account).to.be.null
224 expect(body.data[0].totalReplies).to.equal(2)
225 expect(body.data[1].text).to.equal('super thread 2')
226 expect(body.data[1].totalReplies).to.equal(0)
227 expect(body.data[2].text).to.equal('super thread 3')
228 expect(body.data[2].totalReplies).to.equal(0)
229 })
230
231 it('Should count replies from the video author correctly', async function () {
232 await command.createThread({ videoId: videoUUID, text: 'my super first comment' })
233
234 const { data } = await command.listThreads({ videoId: videoUUID })
235 const threadId2 = data[0].threadId
236
237 const text2 = 'a first answer to thread 4 by a third party'
238 await command.addReply({ token: userAccessTokenServer1, videoId, toCommentId: threadId2, text: text2 })
239
240 const text3 = 'my second answer to thread 4'
241 await command.addReply({ videoId, toCommentId: threadId2, text: text3 })
242
243 const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 })
244 expect(tree.comment.totalRepliesFromVideoAuthor).to.equal(1)
245 expect(tree.comment.totalReplies).to.equal(2)
246 })
247 })
248
249 describe('All instance comments', function () {
250
251 it('Should list instance comments as admin', async function () {
252 {
253 const { data, total } = await command.listForAdmin({ start: 0, count: 1 })
254
255 expect(total).to.equal(7)
256 expect(data).to.have.lengthOf(1)
257 expect(data[0].text).to.equal('my second answer to thread 4')
258 expect(data[0].account.name).to.equal('root')
259 expect(data[0].account.displayName).to.equal('root')
260 expect(data[0].account.avatars).to.have.lengthOf(2)
261 }
262
263 {
264 const { data, total } = await command.listForAdmin({ start: 1, count: 2 })
265
266 expect(total).to.equal(7)
267 expect(data).to.have.lengthOf(2)
268
269 expect(data[0].account.avatars).to.have.lengthOf(2)
270 expect(data[1].account.avatars).to.have.lengthOf(2)
271 }
272 })
273
274 it('Should filter instance comments by isLocal', async function () {
275 const { total, data } = await command.listForAdmin({ isLocal: false })
276
277 expect(data).to.have.lengthOf(0)
278 expect(total).to.equal(0)
279 })
280
281 it('Should filter instance comments by onLocalVideo', async function () {
282 {
283 const { total, data } = await command.listForAdmin({ onLocalVideo: false })
284
285 expect(data).to.have.lengthOf(0)
286 expect(total).to.equal(0)
287 }
288
289 {
290 const { total, data } = await command.listForAdmin({ onLocalVideo: true })
291
292 expect(data).to.not.have.lengthOf(0)
293 expect(total).to.not.equal(0)
294 }
295 })
296
297 it('Should search instance comments by account', async function () {
298 const { total, data } = await command.listForAdmin({ searchAccount: 'user' })
299
300 expect(data).to.have.lengthOf(1)
301 expect(total).to.equal(1)
302
303 expect(data[0].text).to.equal('a first answer to thread 4 by a third party')
304 })
305
306 it('Should search instance comments by video', async function () {
307 {
308 const { total, data } = await command.listForAdmin({ searchVideo: 'video' })
309
310 expect(data).to.have.lengthOf(7)
311 expect(total).to.equal(7)
312 }
313
314 {
315 const { total, data } = await command.listForAdmin({ searchVideo: 'hello' })
316
317 expect(data).to.have.lengthOf(0)
318 expect(total).to.equal(0)
319 }
320 })
321
322 it('Should search instance comments', async function () {
323 const { total, data } = await command.listForAdmin({ search: 'super thread 3' })
324
325 expect(total).to.equal(1)
326
327 expect(data).to.have.lengthOf(1)
328 expect(data[0].text).to.equal('super thread 3')
329 })
330 })
331
332 after(async function () {
333 await cleanupTests([ server ])
334 })
335})
diff --git a/packages/tests/src/api/videos/video-description.ts b/packages/tests/src/api/videos/video-description.ts
new file mode 100644
index 000000000..eb41cd71c
--- /dev/null
+++ b/packages/tests/src/api/videos/video-description.ts
@@ -0,0 +1,103 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createMultipleServers,
7 doubleFollow,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 waitJobs
11} from '@peertube/peertube-server-commands'
12
13describe('Test video description', function () {
14 let servers: PeerTubeServer[] = []
15 let videoUUID = ''
16 let videoId: number
17
18 const longDescription = 'my super description for server 1'.repeat(50)
19
20 // 30 characters * 6 -> 240 characters
21 const truncatedDescription = 'my super description for server 1'.repeat(7) + 'my super descrip...'
22
23 before(async function () {
24 this.timeout(40000)
25
26 // Run servers
27 servers = await createMultipleServers(2)
28
29 // Get the access tokens
30 await setAccessTokensToServers(servers)
31
32 // Server 1 and server 2 follow each other
33 await doubleFollow(servers[0], servers[1])
34 })
35
36 it('Should upload video with long description', async function () {
37 this.timeout(30000)
38
39 const attributes = {
40 description: longDescription
41 }
42 await servers[0].videos.upload({ attributes })
43
44 await waitJobs(servers)
45
46 const { data } = await servers[0].videos.list()
47
48 videoId = data[0].id
49 videoUUID = data[0].uuid
50 })
51
52 it('Should have a truncated description on each server when listing videos', async function () {
53 for (const server of servers) {
54 const { data } = await server.videos.list()
55 const video = data.find(v => v.uuid === videoUUID)
56
57 expect(video.description).to.equal(truncatedDescription)
58 expect(video.truncatedDescription).to.equal(truncatedDescription)
59 }
60 })
61
62 it('Should not have a truncated description on each server when getting videos', async function () {
63 for (const server of servers) {
64 const video = await server.videos.get({ id: videoUUID })
65
66 expect(video.description).to.equal(longDescription)
67 expect(video.truncatedDescription).to.equal(truncatedDescription)
68 }
69 })
70
71 it('Should fetch long description on each server', async function () {
72 for (const server of servers) {
73 const video = await server.videos.get({ id: videoUUID })
74
75 const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath })
76 expect(description).to.equal(longDescription)
77 }
78 })
79
80 it('Should update with a short description', async function () {
81 const attributes = {
82 description: 'short description'
83 }
84 await servers[0].videos.update({ id: videoId, attributes })
85
86 await waitJobs(servers)
87 })
88
89 it('Should have a small description on each server', async function () {
90 for (const server of servers) {
91 const video = await server.videos.get({ id: videoUUID })
92
93 expect(video.description).to.equal('short description')
94
95 const { description } = await server.videos.getDescription({ descriptionPath: video.descriptionPath })
96 expect(description).to.equal('short description')
97 }
98 })
99
100 after(async function () {
101 await cleanupTests(servers)
102 })
103})
diff --git a/packages/tests/src/api/videos/video-files.ts b/packages/tests/src/api/videos/video-files.ts
new file mode 100644
index 000000000..1d7c218a4
--- /dev/null
+++ b/packages/tests/src/api/videos/video-files.ts
@@ -0,0 +1,202 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 makeRawRequest,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test videos files', function () {
16 let servers: PeerTubeServer[]
17
18 // ---------------------------------------------------------------
19
20 before(async function () {
21 this.timeout(150_000)
22
23 servers = await createMultipleServers(2)
24 await setAccessTokensToServers(servers)
25
26 await doubleFollow(servers[0], servers[1])
27
28 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
29 })
30
31 describe('When deleting all files', function () {
32 let validId1: string
33 let validId2: string
34
35 before(async function () {
36 this.timeout(360_000)
37
38 {
39 const { uuid } = await servers[0].videos.quickUpload({ name: 'video 1' })
40 validId1 = uuid
41 }
42
43 {
44 const { uuid } = await servers[0].videos.quickUpload({ name: 'video 2' })
45 validId2 = uuid
46 }
47
48 await waitJobs(servers)
49 })
50
51 it('Should delete web video files', async function () {
52 this.timeout(30_000)
53
54 await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1 })
55
56 await waitJobs(servers)
57
58 for (const server of servers) {
59 const video = await server.videos.get({ id: validId1 })
60
61 expect(video.files).to.have.lengthOf(0)
62 expect(video.streamingPlaylists).to.have.lengthOf(1)
63 }
64 })
65
66 it('Should delete HLS files', async function () {
67 this.timeout(30_000)
68
69 await servers[0].videos.removeHLSPlaylist({ videoId: validId2 })
70
71 await waitJobs(servers)
72
73 for (const server of servers) {
74 const video = await server.videos.get({ id: validId2 })
75
76 expect(video.files).to.have.length.above(0)
77 expect(video.streamingPlaylists).to.have.lengthOf(0)
78 }
79 })
80 })
81
82 describe('When deleting a specific file', function () {
83 let webVideoId: string
84 let hlsId: string
85
86 before(async function () {
87 this.timeout(120_000)
88
89 {
90 const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' })
91 webVideoId = uuid
92 }
93
94 {
95 const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' })
96 hlsId = uuid
97 }
98
99 await waitJobs(servers)
100 })
101
102 it('Shoulde delete a web video file', async function () {
103 this.timeout(30_000)
104
105 const video = await servers[0].videos.get({ id: webVideoId })
106 const files = video.files
107
108 await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: files[0].id })
109
110 await waitJobs(servers)
111
112 for (const server of servers) {
113 const video = await server.videos.get({ id: webVideoId })
114
115 expect(video.files).to.have.lengthOf(files.length - 1)
116 expect(video.files.find(f => f.id === files[0].id)).to.not.exist
117 }
118 })
119
120 it('Should delete all web video files', async function () {
121 this.timeout(30_000)
122
123 const video = await servers[0].videos.get({ id: webVideoId })
124 const files = video.files
125
126 for (const file of files) {
127 await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: file.id })
128 }
129
130 await waitJobs(servers)
131
132 for (const server of servers) {
133 const video = await server.videos.get({ id: webVideoId })
134
135 expect(video.files).to.have.lengthOf(0)
136 }
137 })
138
139 it('Should delete a hls file', async function () {
140 this.timeout(30_000)
141
142 const video = await servers[0].videos.get({ id: hlsId })
143 const files = video.streamingPlaylists[0].files
144 const toDelete = files[0]
145
146 await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: toDelete.id })
147
148 await waitJobs(servers)
149
150 for (const server of servers) {
151 const video = await server.videos.get({ id: hlsId })
152
153 expect(video.streamingPlaylists[0].files).to.have.lengthOf(files.length - 1)
154 expect(video.streamingPlaylists[0].files.find(f => f.id === toDelete.id)).to.not.exist
155
156 const { text } = await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
157
158 expect(text.includes(`-${toDelete.resolution.id}.m3u8`)).to.be.false
159 expect(text.includes(`-${video.streamingPlaylists[0].files[0].resolution.id}.m3u8`)).to.be.true
160 }
161 })
162
163 it('Should delete all hls files', async function () {
164 this.timeout(30_000)
165
166 const video = await servers[0].videos.get({ id: hlsId })
167 const files = video.streamingPlaylists[0].files
168
169 for (const file of files) {
170 await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: file.id })
171 }
172
173 await waitJobs(servers)
174
175 for (const server of servers) {
176 const video = await server.videos.get({ id: hlsId })
177
178 expect(video.streamingPlaylists).to.have.lengthOf(0)
179 }
180 })
181
182 it('Should not delete last file of a video', async function () {
183 this.timeout(60_000)
184
185 const webVideoOnly = await servers[0].videos.get({ id: hlsId })
186 const hlsOnly = await servers[0].videos.get({ id: webVideoId })
187
188 for (let i = 0; i < 4; i++) {
189 await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[i].id })
190 await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[i].id })
191 }
192
193 const expectedStatus = HttpStatusCode.BAD_REQUEST_400
194 await servers[0].videos.removeWebVideoFile({ videoId: webVideoOnly.id, fileId: webVideoOnly.files[4].id, expectedStatus })
195 await servers[0].videos.removeHLSFile({ videoId: hlsOnly.id, fileId: hlsOnly.streamingPlaylists[0].files[4].id, expectedStatus })
196 })
197 })
198
199 after(async function () {
200 await cleanupTests(servers)
201 })
202})
diff --git a/packages/tests/src/api/videos/video-imports.ts b/packages/tests/src/api/videos/video-imports.ts
new file mode 100644
index 000000000..09efe9931
--- /dev/null
+++ b/packages/tests/src/api/videos/video-imports.ts
@@ -0,0 +1,634 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, remove } from 'fs-extra/esm'
5import { readdir } from 'fs/promises'
6import { join } from 'path'
7import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
8import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@peertube/peertube-models'
9import {
10 cleanupTests,
11 createMultipleServers,
12 createSingleServer,
13 doubleFollow,
14 getServerImportConfig,
15 PeerTubeServer,
16 setAccessTokensToServers,
17 setDefaultVideoChannel,
18 waitJobs
19} from '@peertube/peertube-server-commands'
20import { DeepPartial } from '@peertube/peertube-typescript-utils'
21import { testCaptionFile } from '@tests/shared/captions.js'
22import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
23import { FIXTURE_URLS } from '@tests/shared/tests.js'
24
25async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) {
26 const videoHttp = await server.videos.get({ id: idHttp })
27
28 expect(videoHttp.name).to.equal('small video - youtube')
29 expect(videoHttp.category.label).to.equal('News & Politics')
30 expect(videoHttp.licence.label).to.equal('Attribution')
31 expect(videoHttp.language.label).to.equal('Unknown')
32 expect(videoHttp.nsfw).to.be.false
33 expect(videoHttp.description).to.equal('this is a super description')
34 expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ])
35 expect(videoHttp.files).to.have.lengthOf(1)
36
37 const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt)
38 expect(originallyPublishedAt.getDate()).to.equal(14)
39 expect(originallyPublishedAt.getMonth()).to.equal(0)
40 expect(originallyPublishedAt.getFullYear()).to.equal(2019)
41
42 const videoMagnet = await server.videos.get({ id: idMagnet })
43 const videoTorrent = await server.videos.get({ id: idTorrent })
44
45 for (const video of [ videoMagnet, videoTorrent ]) {
46 expect(video.category.label).to.equal('Unknown')
47 expect(video.licence.label).to.equal('Unknown')
48 expect(video.language.label).to.equal('Unknown')
49 expect(video.nsfw).to.be.false
50 expect(video.description).to.equal('this is a super torrent description')
51 expect(video.tags).to.deep.equal([ 'tag_torrent1', 'tag_torrent2' ])
52 expect(video.files).to.have.lengthOf(1)
53 }
54
55 expect(videoTorrent.name).to.contain('你好 世界 720p.mp4')
56 expect(videoMagnet.name).to.contain('super peertube2 video')
57
58 const bodyCaptions = await server.captions.list({ videoId: idHttp })
59 expect(bodyCaptions.total).to.equal(2)
60}
61
62async function checkVideoServer2 (server: PeerTubeServer, id: number | string) {
63 const video = await server.videos.get({ id })
64
65 expect(video.name).to.equal('my super name')
66 expect(video.category.label).to.equal('Entertainment')
67 expect(video.licence.label).to.equal('Public Domain Dedication')
68 expect(video.language.label).to.equal('English')
69 expect(video.nsfw).to.be.false
70 expect(video.description).to.equal('my super description')
71 expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ])
72
73 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', video.thumbnailPath)
74
75 expect(video.files).to.have.lengthOf(1)
76
77 const bodyCaptions = await server.captions.list({ videoId: id })
78 expect(bodyCaptions.total).to.equal(2)
79}
80
81describe('Test video imports', function () {
82
83 if (areHttpImportTestsDisabled()) return
84
85 function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
86
87 describe('Import ' + mode, function () {
88 let servers: PeerTubeServer[] = []
89
90 before(async function () {
91 this.timeout(60_000)
92
93 servers = await createMultipleServers(2, getServerImportConfig(mode))
94
95 await setAccessTokensToServers(servers)
96 await setDefaultVideoChannel(servers)
97
98 for (const server of servers) {
99 await server.config.updateExistingSubConfig({
100 newConfig: {
101 transcoding: {
102 alwaysTranscodeOriginalResolution: false
103 }
104 }
105 })
106 }
107
108 await doubleFollow(servers[0], servers[1])
109 })
110
111 it('Should import videos on server 1', async function () {
112 this.timeout(60_000)
113
114 const baseAttributes = {
115 channelId: servers[0].store.channel.id,
116 privacy: VideoPrivacy.PUBLIC
117 }
118
119 {
120 const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube }
121 const { video } = await servers[0].imports.importVideo({ attributes })
122 expect(video.name).to.equal('small video - youtube')
123
124 {
125 expect(video.thumbnailPath).to.match(new RegExp(`^/lazy-static/thumbnails/.+.jpg$`))
126 expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`))
127
128 const suffix = mode === 'yt-dlp'
129 ? '_yt_dlp'
130 : ''
131
132 await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath)
133 await testImageGeneratedByFFmpeg(servers[0].url, 'video_import_preview' + suffix, video.previewPath)
134 }
135
136 const bodyCaptions = await servers[0].captions.list({ videoId: video.id })
137 const videoCaptions = bodyCaptions.data
138 expect(videoCaptions).to.have.lengthOf(2)
139
140 {
141 const enCaption = videoCaptions.find(caption => caption.language.id === 'en')
142 expect(enCaption).to.exist
143 expect(enCaption.language.label).to.equal('English')
144 expect(enCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-en.vtt$`))
145
146 const regex = `WEBVTT[ \n]+Kind: captions[ \n]+` +
147 `(Language: en[ \n]+)?` +
148 `00:00:01.600 --> 00:00:04.200( position:\\d+% line:\\d+%)?[ \n]+English \\(US\\)[ \n]+` +
149 `00:00:05.900 --> 00:00:07.999( position:\\d+% line:\\d+%)?[ \n]+This is a subtitle in American English[ \n]+` +
150 `00:00:10.000 --> 00:00:14.000( position:\\d+% line:\\d+%)?[ \n]+Adding subtitles is very easy to do`
151 await testCaptionFile(servers[0].url, enCaption.captionPath, new RegExp(regex))
152 }
153
154 {
155 const frCaption = videoCaptions.find(caption => caption.language.id === 'fr')
156 expect(frCaption).to.exist
157 expect(frCaption.language.label).to.equal('French')
158 expect(frCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-fr.vtt`))
159
160 const regex = `WEBVTT[ \n]+Kind: captions[ \n]+` +
161 `(Language: fr[ \n]+)?` +
162 `00:00:01.600 --> 00:00:04.200( position:\\d+% line:\\d+%)?[ \n]+Français \\(FR\\)[ \n]+` +
163 `00:00:05.900 --> 00:00:07.999( position:\\d+% line:\\d+%)?[ \n]+C'est un sous-titre français[ \n]+` +
164 `00:00:10.000 --> 00:00:14.000( position:\\d+% line:\\d+%)?[ \n]+Ajouter un sous-titre est vraiment facile`
165
166 await testCaptionFile(servers[0].url, frCaption.captionPath, new RegExp(regex))
167 }
168 }
169
170 {
171 const attributes = {
172 ...baseAttributes,
173 magnetUri: FIXTURE_URLS.magnet,
174 description: 'this is a super torrent description',
175 tags: [ 'tag_torrent1', 'tag_torrent2' ]
176 }
177 const { video } = await servers[0].imports.importVideo({ attributes })
178 expect(video.name).to.equal('super peertube2 video')
179 }
180
181 {
182 const attributes = {
183 ...baseAttributes,
184 torrentfile: 'video-720p.torrent' as any,
185 description: 'this is a super torrent description',
186 tags: [ 'tag_torrent1', 'tag_torrent2' ]
187 }
188 const { video } = await servers[0].imports.importVideo({ attributes })
189 expect(video.name).to.equal('你好 世界 720p.mp4')
190 }
191 })
192
193 it('Should list the videos to import in my videos on server 1', async function () {
194 const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' })
195
196 expect(total).to.equal(3)
197
198 expect(data).to.have.lengthOf(3)
199 expect(data[0].name).to.equal('small video - youtube')
200 expect(data[1].name).to.equal('super peertube2 video')
201 expect(data[2].name).to.equal('你好 世界 720p.mp4')
202 })
203
204 it('Should list the videos to import in my imports on server 1', async function () {
205 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' })
206 expect(total).to.equal(3)
207
208 expect(videoImports).to.have.lengthOf(3)
209
210 expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube)
211 expect(videoImports[2].magnetUri).to.be.null
212 expect(videoImports[2].torrentName).to.be.null
213 expect(videoImports[2].video.name).to.equal('small video - youtube')
214
215 expect(videoImports[1].targetUrl).to.be.null
216 expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet)
217 expect(videoImports[1].torrentName).to.be.null
218 expect(videoImports[1].video.name).to.equal('super peertube2 video')
219
220 expect(videoImports[0].targetUrl).to.be.null
221 expect(videoImports[0].magnetUri).to.be.null
222 expect(videoImports[0].torrentName).to.equal('video-720p.torrent')
223 expect(videoImports[0].video.name).to.equal('你好 世界 720p.mp4')
224 })
225
226 it('Should filter my imports on target URL', async function () {
227 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ targetUrl: FIXTURE_URLS.youtube })
228 expect(total).to.equal(1)
229 expect(videoImports).to.have.lengthOf(1)
230
231 expect(videoImports[0].targetUrl).to.equal(FIXTURE_URLS.youtube)
232 })
233
234 it('Should search in my imports', async function () {
235 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ search: 'peertube2' })
236 expect(total).to.equal(1)
237 expect(videoImports).to.have.lengthOf(1)
238
239 expect(videoImports[0].magnetUri).to.equal(FIXTURE_URLS.magnet)
240 expect(videoImports[0].video.name).to.equal('super peertube2 video')
241 })
242
243 it('Should have the video listed on the two instances', async function () {
244 this.timeout(120_000)
245
246 await waitJobs(servers)
247
248 for (const server of servers) {
249 const { total, data } = await server.videos.list()
250 expect(total).to.equal(3)
251 expect(data).to.have.lengthOf(3)
252
253 const [ videoHttp, videoMagnet, videoTorrent ] = data
254 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
255 }
256 })
257
258 it('Should import a video on server 2 with some fields', async function () {
259 this.timeout(60_000)
260
261 const { video } = await servers[1].imports.importVideo({
262 attributes: {
263 targetUrl: FIXTURE_URLS.youtube,
264 channelId: servers[1].store.channel.id,
265 privacy: VideoPrivacy.PUBLIC,
266 category: 10,
267 licence: 7,
268 language: 'en',
269 name: 'my super name',
270 description: 'my super description',
271 tags: [ 'supertag1', 'supertag2' ],
272 thumbnailfile: 'custom-thumbnail.jpg'
273 }
274 })
275 expect(video.name).to.equal('my super name')
276 })
277
278 it('Should have the videos listed on the two instances', async function () {
279 this.timeout(120_000)
280
281 await waitJobs(servers)
282
283 for (const server of servers) {
284 const { total, data } = await server.videos.list()
285 expect(total).to.equal(4)
286 expect(data).to.have.lengthOf(4)
287
288 await checkVideoServer2(server, data[0].uuid)
289
290 const [ , videoHttp, videoMagnet, videoTorrent ] = data
291 await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
292 }
293 })
294
295 it('Should import a video that will be transcoded', async function () {
296 this.timeout(240_000)
297
298 const attributes = {
299 name: 'transcoded video',
300 magnetUri: FIXTURE_URLS.magnet,
301 channelId: servers[1].store.channel.id,
302 privacy: VideoPrivacy.PUBLIC
303 }
304 const { video } = await servers[1].imports.importVideo({ attributes })
305 const videoUUID = video.uuid
306
307 await waitJobs(servers)
308
309 for (const server of servers) {
310 const video = await server.videos.get({ id: videoUUID })
311
312 expect(video.name).to.equal('transcoded video')
313 expect(video.files).to.have.lengthOf(4)
314 }
315 })
316
317 it('Should import no HDR version on a HDR video', async function () {
318 this.timeout(300_000)
319
320 const config: DeepPartial<CustomConfig> = {
321 transcoding: {
322 enabled: true,
323 resolutions: {
324 '0p': false,
325 '144p': true,
326 '240p': true,
327 '360p': false,
328 '480p': false,
329 '720p': false,
330 '1080p': false, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01
331 '1440p': false,
332 '2160p': false
333 },
334 webVideos: { enabled: true },
335 hls: { enabled: false }
336 }
337 }
338 await servers[0].config.updateExistingSubConfig({ newConfig: config })
339
340 const attributes = {
341 name: 'hdr video',
342 targetUrl: FIXTURE_URLS.youtubeHDR,
343 channelId: servers[0].store.channel.id,
344 privacy: VideoPrivacy.PUBLIC
345 }
346 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
347 const videoUUID = videoImported.uuid
348
349 await waitJobs(servers)
350
351 // test resolution
352 const video = await servers[0].videos.get({ id: videoUUID })
353 expect(video.name).to.equal('hdr video')
354 const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id }))
355 expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_240P)
356 })
357
358 it('Should not import resolution higher than enabled transcoding resolution', async function () {
359 this.timeout(300_000)
360
361 const config: DeepPartial<CustomConfig> = {
362 transcoding: {
363 enabled: true,
364 resolutions: {
365 '0p': false,
366 '144p': true,
367 '240p': false,
368 '360p': false,
369 '480p': false,
370 '720p': false,
371 '1080p': false,
372 '1440p': false,
373 '2160p': false
374 },
375 alwaysTranscodeOriginalResolution: false
376 }
377 }
378 await servers[0].config.updateExistingSubConfig({ newConfig: config })
379
380 const attributes = {
381 name: 'small resolution video',
382 targetUrl: FIXTURE_URLS.youtube,
383 channelId: servers[0].store.channel.id,
384 privacy: VideoPrivacy.PUBLIC
385 }
386 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
387 const videoUUID = videoImported.uuid
388
389 await waitJobs(servers)
390
391 // test resolution
392 const video = await servers[0].videos.get({ id: videoUUID })
393 expect(video.name).to.equal('small resolution video')
394 expect(video.files).to.have.lengthOf(1)
395 expect(video.files[0].resolution.id).to.equal(144)
396 })
397
398 it('Should import resolution higher than enabled transcoding resolution', async function () {
399 this.timeout(300_000)
400
401 const config: DeepPartial<CustomConfig> = {
402 transcoding: {
403 alwaysTranscodeOriginalResolution: true
404 }
405 }
406 await servers[0].config.updateExistingSubConfig({ newConfig: config })
407
408 const attributes = {
409 name: 'bigger resolution video',
410 targetUrl: FIXTURE_URLS.youtube,
411 channelId: servers[0].store.channel.id,
412 privacy: VideoPrivacy.PUBLIC
413 }
414 const { video: videoImported } = await servers[0].imports.importVideo({ attributes })
415 const videoUUID = videoImported.uuid
416
417 await waitJobs(servers)
418
419 // test resolution
420 const video = await servers[0].videos.get({ id: videoUUID })
421 expect(video.name).to.equal('bigger resolution video')
422
423 expect(video.files).to.have.lengthOf(2)
424 expect(video.files.find(f => f.resolution.id === 240)).to.exist
425 expect(video.files.find(f => f.resolution.id === 144)).to.exist
426 })
427
428 it('Should import a peertube video', async function () {
429 this.timeout(120_000)
430
431 const toTest = [ FIXTURE_URLS.peertube_long ]
432
433 // TODO: include peertube_short when https://github.com/ytdl-org/youtube-dl/pull/29475 is merged
434 if (mode === 'yt-dlp') {
435 toTest.push(FIXTURE_URLS.peertube_short)
436 }
437
438 for (const targetUrl of toTest) {
439 await servers[0].config.disableTranscoding()
440
441 const attributes = {
442 targetUrl,
443 channelId: servers[0].store.channel.id,
444 privacy: VideoPrivacy.PUBLIC
445 }
446 const { video } = await servers[0].imports.importVideo({ attributes })
447 const videoUUID = video.uuid
448
449 await waitJobs(servers)
450
451 for (const server of servers) {
452 const video = await server.videos.get({ id: videoUUID })
453
454 expect(video.name).to.equal('E2E tests')
455
456 const { data: captions } = await server.captions.list({ videoId: videoUUID })
457 expect(captions).to.have.lengthOf(1)
458 expect(captions[0].language.id).to.equal('fr')
459
460 const str = `WEBVTT FILE\r?\n\r?\n` +
461 `1\r?\n` +
462 `00:00:04.000 --> 00:00:09.000\r?\n` +
463 `January 1, 1994. The North American`
464 await testCaptionFile(server.url, captions[0].captionPath, new RegExp(str))
465 }
466 }
467 })
468
469 after(async function () {
470 await cleanupTests(servers)
471 })
472 })
473 }
474
475 // FIXME: youtube-dl seems broken
476 // runSuite('youtube-dl')
477
478 runSuite('yt-dlp')
479
480 describe('Delete/cancel an import', function () {
481 let server: PeerTubeServer
482
483 let finishedImportId: number
484 let finishedVideo: Video
485 let pendingImportId: number
486
487 async function importVideo (name: string) {
488 const attributes = { name, channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo }
489 const res = await server.imports.importVideo({ attributes })
490
491 return res.id
492 }
493
494 before(async function () {
495 this.timeout(120_000)
496
497 server = await createSingleServer(1)
498
499 await setAccessTokensToServers([ server ])
500 await setDefaultVideoChannel([ server ])
501
502 finishedImportId = await importVideo('finished')
503 await waitJobs([ server ])
504
505 await server.jobs.pauseJobQueue()
506 pendingImportId = await importVideo('pending')
507
508 const { data } = await server.imports.getMyVideoImports()
509 expect(data).to.have.lengthOf(2)
510
511 finishedVideo = data.find(i => i.id === finishedImportId).video
512 })
513
514 it('Should delete a video import', async function () {
515 await server.imports.delete({ importId: finishedImportId })
516
517 const { data } = await server.imports.getMyVideoImports()
518 expect(data).to.have.lengthOf(1)
519 expect(data[0].id).to.equal(pendingImportId)
520 expect(data[0].state.id).to.equal(VideoImportState.PENDING)
521 })
522
523 it('Should not have deleted the associated video', async function () {
524 const video = await server.videos.get({ id: finishedVideo.id, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
525 expect(video.name).to.equal('finished')
526 expect(video.state.id).to.equal(VideoState.PUBLISHED)
527 })
528
529 it('Should cancel a video import', async function () {
530 await server.imports.cancel({ importId: pendingImportId })
531
532 const { data } = await server.imports.getMyVideoImports()
533 expect(data).to.have.lengthOf(1)
534 expect(data[0].id).to.equal(pendingImportId)
535 expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
536 })
537
538 it('Should not have processed the cancelled video import', async function () {
539 this.timeout(60_000)
540
541 await server.jobs.resumeJobQueue()
542
543 await waitJobs([ server ])
544
545 const { data } = await server.imports.getMyVideoImports()
546 expect(data).to.have.lengthOf(1)
547 expect(data[0].id).to.equal(pendingImportId)
548 expect(data[0].state.id).to.equal(VideoImportState.CANCELLED)
549 expect(data[0].video.state.id).to.equal(VideoState.TO_IMPORT)
550 })
551
552 it('Should delete the cancelled video import', async function () {
553 await server.imports.delete({ importId: pendingImportId })
554 const { data } = await server.imports.getMyVideoImports()
555 expect(data).to.have.lengthOf(0)
556 })
557
558 after(async function () {
559 await cleanupTests([ server ])
560 })
561 })
562
563 describe('Auto update', function () {
564 let server: PeerTubeServer
565
566 function quickPeerTubeImport () {
567 const attributes = {
568 targetUrl: FIXTURE_URLS.peertube_long,
569 channelId: server.store.channel.id,
570 privacy: VideoPrivacy.PUBLIC
571 }
572
573 return server.imports.importVideo({ attributes })
574 }
575
576 async function testBinaryUpdate (releaseUrl: string, releaseName: string) {
577 await remove(join(server.servers.buildDirectory('bin'), releaseName))
578
579 await server.kill()
580 await server.run({
581 import: {
582 videos: {
583 http: {
584 youtube_dl_release: {
585 url: releaseUrl,
586 name: releaseName
587 }
588 }
589 }
590 }
591 })
592
593 await quickPeerTubeImport()
594
595 const base = server.servers.buildDirectory('bin')
596 const content = await readdir(base)
597 const binaryPath = join(base, releaseName)
598
599 expect(await pathExists(binaryPath), `${binaryPath} does not exist in ${base} (${content.join(', ')})`).to.be.true
600 }
601
602 before(async function () {
603 this.timeout(30_000)
604
605 // Run servers
606 server = await createSingleServer(1)
607
608 await setAccessTokensToServers([ server ])
609 await setDefaultVideoChannel([ server ])
610 })
611
612 it('Should update youtube-dl from github URL', async function () {
613 this.timeout(120_000)
614
615 await testBinaryUpdate('https://api.github.com/repos/ytdl-org/youtube-dl/releases', 'youtube-dl')
616 })
617
618 it('Should update youtube-dl from raw URL', async function () {
619 this.timeout(120_000)
620
621 await testBinaryUpdate('https://yt-dl.org/downloads/latest/youtube-dl', 'youtube-dl')
622 })
623
624 it('Should update youtube-dl from youtube-dl fork', async function () {
625 this.timeout(120_000)
626
627 await testBinaryUpdate('https://api.github.com/repos/yt-dlp/yt-dlp/releases', 'yt-dlp')
628 })
629
630 after(async function () {
631 await cleanupTests([ server ])
632 })
633 })
634})
diff --git a/packages/tests/src/api/videos/video-nsfw.ts b/packages/tests/src/api/videos/video-nsfw.ts
new file mode 100644
index 000000000..fc5225dd2
--- /dev/null
+++ b/packages/tests/src/api/videos/video-nsfw.ts
@@ -0,0 +1,227 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
5import { BooleanBothQuery, CustomConfig, ResultList, Video, VideosOverview } from '@peertube/peertube-models'
6
7function createOverviewRes (overview: VideosOverview) {
8 const videos = overview.categories[0].videos
9 return { data: videos, total: videos.length }
10}
11
12describe('Test video NSFW policy', function () {
13 let server: PeerTubeServer
14 let userAccessToken: string
15 let customConfig: CustomConfig
16
17 async function getVideosFunctions (token?: string, query: { nsfw?: BooleanBothQuery } = {}) {
18 const user = await server.users.getMyInfo()
19
20 const channelName = user.videoChannels[0].name
21 const accountName = user.account.name + '@' + user.account.host
22
23 const hasQuery = Object.keys(query).length !== 0
24 let promises: Promise<ResultList<Video>>[]
25
26 if (token) {
27 promises = [
28 server.search.advancedVideoSearch({ token, search: { search: 'n', sort: '-publishedAt', ...query } }),
29 server.videos.listWithToken({ token, ...query }),
30 server.videos.listByAccount({ token, handle: accountName, ...query }),
31 server.videos.listByChannel({ token, handle: channelName, ...query })
32 ]
33
34 // Overviews do not support video filters
35 if (!hasQuery) {
36 const p = server.overviews.getVideos({ page: 1, token })
37 .then(res => createOverviewRes(res))
38 promises.push(p)
39 }
40
41 return Promise.all(promises)
42 }
43
44 promises = [
45 server.search.searchVideos({ search: 'n', sort: '-publishedAt' }),
46 server.videos.list(),
47 server.videos.listByAccount({ token: null, handle: accountName }),
48 server.videos.listByChannel({ token: null, handle: channelName })
49 ]
50
51 // Overviews do not support video filters
52 if (!hasQuery) {
53 const p = server.overviews.getVideos({ page: 1 })
54 .then(res => createOverviewRes(res))
55 promises.push(p)
56 }
57
58 return Promise.all(promises)
59 }
60
61 before(async function () {
62 this.timeout(50000)
63 server = await createSingleServer(1)
64
65 // Get the access tokens
66 await setAccessTokensToServers([ server ])
67
68 {
69 const attributes = { name: 'nsfw', nsfw: true, category: 1 }
70 await server.videos.upload({ attributes })
71 }
72
73 {
74 const attributes = { name: 'normal', nsfw: false, category: 1 }
75 await server.videos.upload({ attributes })
76 }
77
78 customConfig = await server.config.getCustomConfig()
79 })
80
81 describe('Instance default NSFW policy', function () {
82
83 it('Should display NSFW videos with display default NSFW policy', async function () {
84 const serverConfig = await server.config.getConfig()
85 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display')
86
87 for (const body of await getVideosFunctions()) {
88 expect(body.total).to.equal(2)
89
90 const videos = body.data
91 expect(videos).to.have.lengthOf(2)
92 expect(videos[0].name).to.equal('normal')
93 expect(videos[1].name).to.equal('nsfw')
94 }
95 })
96
97 it('Should not display NSFW videos with do_not_list default NSFW policy', async function () {
98 customConfig.instance.defaultNSFWPolicy = 'do_not_list'
99 await server.config.updateCustomConfig({ newCustomConfig: customConfig })
100
101 const serverConfig = await server.config.getConfig()
102 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list')
103
104 for (const body of await getVideosFunctions()) {
105 expect(body.total).to.equal(1)
106
107 const videos = body.data
108 expect(videos).to.have.lengthOf(1)
109 expect(videos[0].name).to.equal('normal')
110 }
111 })
112
113 it('Should display NSFW videos with blur default NSFW policy', async function () {
114 customConfig.instance.defaultNSFWPolicy = 'blur'
115 await server.config.updateCustomConfig({ newCustomConfig: customConfig })
116
117 const serverConfig = await server.config.getConfig()
118 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur')
119
120 for (const body of await getVideosFunctions()) {
121 expect(body.total).to.equal(2)
122
123 const videos = body.data
124 expect(videos).to.have.lengthOf(2)
125 expect(videos[0].name).to.equal('normal')
126 expect(videos[1].name).to.equal('nsfw')
127 }
128 })
129 })
130
131 describe('User NSFW policy', function () {
132
133 it('Should create a user having the default nsfw policy', async function () {
134 const username = 'user1'
135 const password = 'my super password'
136 await server.users.create({ username, password })
137
138 userAccessToken = await server.login.getAccessToken({ username, password })
139
140 const user = await server.users.getMyInfo({ token: userAccessToken })
141 expect(user.nsfwPolicy).to.equal('blur')
142 })
143
144 it('Should display NSFW videos with blur user NSFW policy', async function () {
145 customConfig.instance.defaultNSFWPolicy = 'do_not_list'
146 await server.config.updateCustomConfig({ newCustomConfig: customConfig })
147
148 for (const body of await getVideosFunctions(userAccessToken)) {
149 expect(body.total).to.equal(2)
150
151 const videos = body.data
152 expect(videos).to.have.lengthOf(2)
153 expect(videos[0].name).to.equal('normal')
154 expect(videos[1].name).to.equal('nsfw')
155 }
156 })
157
158 it('Should display NSFW videos with display user NSFW policy', async function () {
159 await server.users.updateMe({ nsfwPolicy: 'display' })
160
161 for (const body of await getVideosFunctions(server.accessToken)) {
162 expect(body.total).to.equal(2)
163
164 const videos = body.data
165 expect(videos).to.have.lengthOf(2)
166 expect(videos[0].name).to.equal('normal')
167 expect(videos[1].name).to.equal('nsfw')
168 }
169 })
170
171 it('Should not display NSFW videos with do_not_list user NSFW policy', async function () {
172 await server.users.updateMe({ nsfwPolicy: 'do_not_list' })
173
174 for (const body of await getVideosFunctions(server.accessToken)) {
175 expect(body.total).to.equal(1)
176
177 const videos = body.data
178 expect(videos).to.have.lengthOf(1)
179 expect(videos[0].name).to.equal('normal')
180 }
181 })
182
183 it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () {
184 const { total, data } = await server.videos.listMyVideos()
185 expect(total).to.equal(2)
186
187 expect(data).to.have.lengthOf(2)
188 expect(data[0].name).to.equal('normal')
189 expect(data[1].name).to.equal('nsfw')
190 })
191
192 it('Should display NSFW videos when the nsfw param === true', async function () {
193 for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'true' })) {
194 expect(body.total).to.equal(1)
195
196 const videos = body.data
197 expect(videos).to.have.lengthOf(1)
198 expect(videos[0].name).to.equal('nsfw')
199 }
200 })
201
202 it('Should hide NSFW videos when the nsfw param === true', async function () {
203 for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'false' })) {
204 expect(body.total).to.equal(1)
205
206 const videos = body.data
207 expect(videos).to.have.lengthOf(1)
208 expect(videos[0].name).to.equal('normal')
209 }
210 })
211
212 it('Should display both videos when the nsfw param === both', async function () {
213 for (const body of await getVideosFunctions(server.accessToken, { nsfw: 'both' })) {
214 expect(body.total).to.equal(2)
215
216 const videos = body.data
217 expect(videos).to.have.lengthOf(2)
218 expect(videos[0].name).to.equal('normal')
219 expect(videos[1].name).to.equal('nsfw')
220 }
221 })
222 })
223
224 after(async function () {
225 await cleanupTests([ server ])
226 })
227})
diff --git a/packages/tests/src/api/videos/video-passwords.ts b/packages/tests/src/api/videos/video-passwords.ts
new file mode 100644
index 000000000..60e0e28bd
--- /dev/null
+++ b/packages/tests/src/api/videos/video-passwords.ts
@@ -0,0 +1,97 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createSingleServer,
7 VideoPasswordsCommand,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultAccountAvatar,
11 setDefaultChannelAvatar
12} from '@peertube/peertube-server-commands'
13import { VideoPrivacy } from '@peertube/peertube-models'
14
15describe('Test video passwords', function () {
16 let server: PeerTubeServer
17 let videoUUID: string
18
19 let userAccessTokenServer1: string
20
21 let videoPasswords: string[] = []
22 let command: VideoPasswordsCommand
23
24 before(async function () {
25 this.timeout(30000)
26
27 server = await createSingleServer(1)
28
29 await setAccessTokensToServers([ server ])
30
31 for (let i = 0; i < 10; i++) {
32 videoPasswords.push(`password ${i + 1}`)
33 }
34 const { uuid } = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords } })
35 videoUUID = uuid
36
37 await setDefaultChannelAvatar(server)
38 await setDefaultAccountAvatar(server)
39
40 userAccessTokenServer1 = await server.users.generateUserAndToken('user1')
41 await setDefaultChannelAvatar(server, 'user1_channel')
42 await setDefaultAccountAvatar(server, userAccessTokenServer1)
43
44 command = server.videoPasswords
45 })
46
47 it('Should list video passwords', async function () {
48 const body = await command.list({ videoId: videoUUID })
49
50 expect(body.total).to.equal(10)
51 expect(body.data).to.be.an('array')
52 expect(body.data).to.have.lengthOf(10)
53 })
54
55 it('Should filter passwords on this video', async function () {
56 const body = await command.list({ videoId: videoUUID, count: 2, start: 3, sort: 'createdAt' })
57
58 expect(body.total).to.equal(10)
59 expect(body.data).to.be.an('array')
60 expect(body.data).to.have.lengthOf(2)
61 expect(body.data[0].password).to.equal('password 4')
62 expect(body.data[1].password).to.equal('password 5')
63 })
64
65 it('Should update password for this video', async function () {
66 videoPasswords = [ 'my super new password 1', 'my super new password 2' ]
67
68 await command.updateAll({ videoId: videoUUID, passwords: videoPasswords })
69 const body = await command.list({ videoId: videoUUID })
70 expect(body.total).to.equal(2)
71 expect(body.data).to.be.an('array')
72 expect(body.data).to.have.lengthOf(2)
73 expect(body.data[0].password).to.equal('my super new password 2')
74 expect(body.data[1].password).to.equal('my super new password 1')
75 })
76
77 it('Should delete one password', async function () {
78 {
79 const body = await command.list({ videoId: videoUUID })
80 expect(body.total).to.equal(2)
81 expect(body.data).to.be.an('array')
82 expect(body.data).to.have.lengthOf(2)
83 await command.remove({ id: body.data[0].id, videoId: videoUUID })
84 }
85 {
86 const body = await command.list({ videoId: videoUUID })
87
88 expect(body.total).to.equal(1)
89 expect(body.data).to.be.an('array')
90 expect(body.data).to.have.lengthOf(1)
91 }
92 })
93
94 after(async function () {
95 await cleanupTests([ server ])
96 })
97})
diff --git a/packages/tests/src/api/videos/video-playlist-thumbnails.ts b/packages/tests/src/api/videos/video-playlist-thumbnails.ts
new file mode 100644
index 000000000..d79c92f72
--- /dev/null
+++ b/packages/tests/src/api/videos/video-playlist-thumbnails.ts
@@ -0,0 +1,234 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
5import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15
16describe('Playlist thumbnail', function () {
17 let servers: PeerTubeServer[] = []
18
19 let playlistWithoutThumbnailId: number
20 let playlistWithThumbnailId: number
21
22 let withThumbnailE1: number
23 let withThumbnailE2: number
24 let withoutThumbnailE1: number
25 let withoutThumbnailE2: number
26
27 let video1: number
28 let video2: number
29
30 async function getPlaylistWithoutThumbnail (server: PeerTubeServer) {
31 const body = await server.playlists.list({ start: 0, count: 10 })
32
33 return body.data.find(p => p.displayName === 'playlist without thumbnail')
34 }
35
36 async function getPlaylistWithThumbnail (server: PeerTubeServer) {
37 const body = await server.playlists.list({ start: 0, count: 10 })
38
39 return body.data.find(p => p.displayName === 'playlist with thumbnail')
40 }
41
42 before(async function () {
43 this.timeout(120000)
44
45 servers = await createMultipleServers(2)
46
47 // Get the access tokens
48 await setAccessTokensToServers(servers)
49 await setDefaultVideoChannel(servers)
50
51 for (const server of servers) {
52 await server.config.disableTranscoding()
53 }
54
55 // Server 1 and server 2 follow each other
56 await doubleFollow(servers[0], servers[1])
57
58 video1 = (await servers[0].videos.quickUpload({ name: 'video 1' })).id
59 video2 = (await servers[0].videos.quickUpload({ name: 'video 2' })).id
60
61 await waitJobs(servers)
62 })
63
64 it('Should automatically update the thumbnail when adding an element', async function () {
65 this.timeout(30000)
66
67 const created = await servers[1].playlists.create({
68 attributes: {
69 displayName: 'playlist without thumbnail',
70 privacy: VideoPlaylistPrivacy.PUBLIC,
71 videoChannelId: servers[1].store.channel.id
72 }
73 })
74 playlistWithoutThumbnailId = created.id
75
76 const added = await servers[1].playlists.addElement({
77 playlistId: playlistWithoutThumbnailId,
78 attributes: { videoId: video1 }
79 })
80 withoutThumbnailE1 = added.id
81
82 await waitJobs(servers)
83
84 for (const server of servers) {
85 const p = await getPlaylistWithoutThumbnail(server)
86 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath)
87 }
88 })
89
90 it('Should not update the thumbnail if we explicitly uploaded a thumbnail', async function () {
91 this.timeout(30000)
92
93 const created = await servers[1].playlists.create({
94 attributes: {
95 displayName: 'playlist with thumbnail',
96 privacy: VideoPlaylistPrivacy.PUBLIC,
97 videoChannelId: servers[1].store.channel.id,
98 thumbnailfile: 'custom-thumbnail.jpg'
99 }
100 })
101 playlistWithThumbnailId = created.id
102
103 const added = await servers[1].playlists.addElement({
104 playlistId: playlistWithThumbnailId,
105 attributes: { videoId: video1 }
106 })
107 withThumbnailE1 = added.id
108
109 await waitJobs(servers)
110
111 for (const server of servers) {
112 const p = await getPlaylistWithThumbnail(server)
113 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
114 }
115 })
116
117 it('Should automatically update the thumbnail when moving the first element', async function () {
118 this.timeout(30000)
119
120 const added = await servers[1].playlists.addElement({
121 playlistId: playlistWithoutThumbnailId,
122 attributes: { videoId: video2 }
123 })
124 withoutThumbnailE2 = added.id
125
126 await servers[1].playlists.reorderElements({
127 playlistId: playlistWithoutThumbnailId,
128 attributes: {
129 startPosition: 1,
130 insertAfterPosition: 2
131 }
132 })
133
134 await waitJobs(servers)
135
136 for (const server of servers) {
137 const p = await getPlaylistWithoutThumbnail(server)
138 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath)
139 }
140 })
141
142 it('Should not update the thumbnail when moving the first element if we explicitly uploaded a thumbnail', async function () {
143 this.timeout(30000)
144
145 const added = await servers[1].playlists.addElement({
146 playlistId: playlistWithThumbnailId,
147 attributes: { videoId: video2 }
148 })
149 withThumbnailE2 = added.id
150
151 await servers[1].playlists.reorderElements({
152 playlistId: playlistWithThumbnailId,
153 attributes: {
154 startPosition: 1,
155 insertAfterPosition: 2
156 }
157 })
158
159 await waitJobs(servers)
160
161 for (const server of servers) {
162 const p = await getPlaylistWithThumbnail(server)
163 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
164 }
165 })
166
167 it('Should automatically update the thumbnail when deleting the first element', async function () {
168 this.timeout(30000)
169
170 await servers[1].playlists.removeElement({
171 playlistId: playlistWithoutThumbnailId,
172 elementId: withoutThumbnailE1
173 })
174
175 await waitJobs(servers)
176
177 for (const server of servers) {
178 const p = await getPlaylistWithoutThumbnail(server)
179 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', p.thumbnailPath)
180 }
181 })
182
183 it('Should not update the thumbnail when deleting the first element if we explicitly uploaded a thumbnail', async function () {
184 this.timeout(30000)
185
186 await servers[1].playlists.removeElement({
187 playlistId: playlistWithThumbnailId,
188 elementId: withThumbnailE1
189 })
190
191 await waitJobs(servers)
192
193 for (const server of servers) {
194 const p = await getPlaylistWithThumbnail(server)
195 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
196 }
197 })
198
199 it('Should the thumbnail when we delete the last element', async function () {
200 this.timeout(30000)
201
202 await servers[1].playlists.removeElement({
203 playlistId: playlistWithoutThumbnailId,
204 elementId: withoutThumbnailE2
205 })
206
207 await waitJobs(servers)
208
209 for (const server of servers) {
210 const p = await getPlaylistWithoutThumbnail(server)
211 expect(p.thumbnailPath).to.be.null
212 }
213 })
214
215 it('Should not update the thumbnail when we delete the last element if we explicitly uploaded a thumbnail', async function () {
216 this.timeout(30000)
217
218 await servers[1].playlists.removeElement({
219 playlistId: playlistWithThumbnailId,
220 elementId: withThumbnailE2
221 })
222
223 await waitJobs(servers)
224
225 for (const server of servers) {
226 const p = await getPlaylistWithThumbnail(server)
227 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', p.thumbnailPath)
228 }
229 })
230
231 after(async function () {
232 await cleanupTests(servers)
233 })
234})
diff --git a/packages/tests/src/api/videos/video-playlists.ts b/packages/tests/src/api/videos/video-playlists.ts
new file mode 100644
index 000000000..578d01093
--- /dev/null
+++ b/packages/tests/src/api/videos/video-playlists.ts
@@ -0,0 +1,1210 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import {
6 HttpStatusCode,
7 VideoPlaylist,
8 VideoPlaylistCreateResult,
9 VideoPlaylistElementType,
10 VideoPlaylistElementType_Type,
11 VideoPlaylistPrivacy,
12 VideoPlaylistType,
13 VideoPrivacy
14} from '@peertube/peertube-models'
15import { uuidToShort } from '@peertube/peertube-node-utils'
16import {
17 cleanupTests,
18 createMultipleServers,
19 doubleFollow,
20 PeerTubeServer,
21 PlaylistsCommand,
22 setAccessTokensToServers,
23 setDefaultAccountAvatar,
24 setDefaultVideoChannel,
25 waitJobs
26} from '@peertube/peertube-server-commands'
27import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js'
28import { checkPlaylistFilesWereRemoved } from '@tests/shared/video-playlists.js'
29
30async function checkPlaylistElementType (
31 servers: PeerTubeServer[],
32 playlistId: string,
33 type: VideoPlaylistElementType_Type,
34 position: number,
35 name: string,
36 total: number
37) {
38 for (const server of servers) {
39 const body = await server.playlists.listVideos({ token: server.accessToken, playlistId, start: 0, count: 10 })
40 expect(body.total).to.equal(total)
41
42 const videoElement = body.data.find(e => e.position === position)
43 expect(videoElement.type).to.equal(type, 'On server ' + server.url)
44
45 if (type === VideoPlaylistElementType.REGULAR) {
46 expect(videoElement.video).to.not.be.null
47 expect(videoElement.video.name).to.equal(name)
48 } else {
49 expect(videoElement.video).to.be.null
50 }
51 }
52}
53
54describe('Test video playlists', function () {
55 let servers: PeerTubeServer[] = []
56
57 let playlistServer2Id1: number
58 let playlistServer2Id2: number
59 let playlistServer2UUID2: string
60
61 let playlistServer1Id: number
62 let playlistServer1DisplayName: string
63 let playlistServer1UUID: string
64 let playlistServer1UUID2: string
65
66 let playlistElementServer1Video4: number
67 let playlistElementServer1Video5: number
68 let playlistElementNSFW: number
69
70 let nsfwVideoServer1: number
71
72 let userTokenServer1: string
73
74 let commands: PlaylistsCommand[]
75
76 before(async function () {
77 this.timeout(240000)
78
79 servers = await createMultipleServers(3)
80
81 // Get the access tokens
82 await setAccessTokensToServers(servers)
83 await setDefaultVideoChannel(servers)
84 await setDefaultAccountAvatar(servers)
85
86 for (const server of servers) {
87 await server.config.disableTranscoding()
88 }
89
90 // Server 1 and server 2 follow each other
91 await doubleFollow(servers[0], servers[1])
92 // Server 1 and server 3 follow each other
93 await doubleFollow(servers[0], servers[2])
94
95 commands = servers.map(s => s.playlists)
96
97 {
98 servers[0].store.videos = []
99 servers[1].store.videos = []
100 servers[2].store.videos = []
101
102 for (const server of servers) {
103 for (let i = 0; i < 7; i++) {
104 const name = `video ${i} server ${server.serverNumber}`
105 const video = await server.videos.upload({ attributes: { name, nsfw: false } })
106
107 server.store.videos.push(video)
108 }
109 }
110 }
111
112 nsfwVideoServer1 = (await servers[0].videos.quickUpload({ name: 'NSFW video', nsfw: true })).id
113
114 userTokenServer1 = await servers[0].users.generateUserAndToken('user1')
115
116 await waitJobs(servers)
117 })
118
119 describe('Check playlists filters and privacies', function () {
120
121 it('Should list video playlist privacies', async function () {
122 const privacies = await commands[0].getPrivacies()
123
124 expect(Object.keys(privacies)).to.have.length.at.least(3)
125 expect(privacies[3]).to.equal('Private')
126 })
127
128 it('Should filter on playlist type', async function () {
129 this.timeout(30000)
130
131 const token = servers[0].accessToken
132
133 await commands[0].create({
134 attributes: {
135 displayName: 'my super playlist',
136 privacy: VideoPlaylistPrivacy.PUBLIC,
137 description: 'my super description',
138 thumbnailfile: 'custom-thumbnail.jpg',
139 videoChannelId: servers[0].store.channel.id
140 }
141 })
142
143 {
144 const body = await commands[0].listByAccount({ token, handle: 'root', playlistType: VideoPlaylistType.WATCH_LATER })
145
146 expect(body.total).to.equal(1)
147 expect(body.data).to.have.lengthOf(1)
148
149 const playlist = body.data[0]
150 expect(playlist.displayName).to.equal('Watch later')
151 expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
152 expect(playlist.type.label).to.equal('Watch later')
153 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
154 }
155
156 {
157 const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.WATCH_LATER })
158 const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.WATCH_LATER })
159
160 for (const body of [ bodyList, bodyChannel ]) {
161 expect(body.total).to.equal(0)
162 expect(body.data).to.have.lengthOf(0)
163 }
164 }
165
166 {
167 const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.REGULAR })
168 const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.REGULAR })
169
170 let playlist: VideoPlaylist = null
171 for (const body of [ bodyList, bodyChannel ]) {
172
173 expect(body.total).to.equal(1)
174 expect(body.data).to.have.lengthOf(1)
175
176 playlist = body.data[0]
177 expect(playlist.displayName).to.equal('my super playlist')
178 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
179 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
180 }
181
182 await commands[0].update({
183 playlistId: playlist.id,
184 attributes: {
185 privacy: VideoPlaylistPrivacy.PRIVATE
186 }
187 })
188 }
189
190 {
191 const bodyList = await commands[0].list({ playlistType: VideoPlaylistType.REGULAR })
192 const bodyChannel = await commands[0].listByChannel({ handle: 'root_channel', playlistType: VideoPlaylistType.REGULAR })
193
194 for (const body of [ bodyList, bodyChannel ]) {
195 expect(body.total).to.equal(0)
196 expect(body.data).to.have.lengthOf(0)
197 }
198 }
199
200 {
201 const body = await commands[0].listByAccount({ handle: 'root' })
202 expect(body.total).to.equal(0)
203 expect(body.data).to.have.lengthOf(0)
204 }
205 })
206
207 it('Should get private playlist for a classic user', async function () {
208 const token = await servers[0].users.generateUserAndToken('toto')
209
210 const body = await commands[0].listByAccount({ token, handle: 'toto' })
211
212 expect(body.total).to.equal(1)
213 expect(body.data).to.have.lengthOf(1)
214
215 const playlistId = body.data[0].id
216 await commands[0].listVideos({ token, playlistId })
217 })
218 })
219
220 describe('Create and federate playlists', function () {
221
222 it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
223 this.timeout(30000)
224
225 await commands[0].create({
226 attributes: {
227 displayName: 'my super playlist',
228 privacy: VideoPlaylistPrivacy.PUBLIC,
229 description: 'my super description',
230 thumbnailfile: 'custom-thumbnail.jpg',
231 videoChannelId: servers[0].store.channel.id
232 }
233 })
234
235 await waitJobs(servers)
236 // Processing a playlist by the receiver could be long
237 await wait(3000)
238
239 for (const server of servers) {
240 const body = await server.playlists.list({ start: 0, count: 5 })
241 expect(body.total).to.equal(1)
242 expect(body.data).to.have.lengthOf(1)
243
244 const playlistFromList = body.data[0]
245
246 const playlistFromGet = await server.playlists.get({ playlistId: playlistFromList.uuid })
247
248 for (const playlist of [ playlistFromGet, playlistFromList ]) {
249 expect(playlist.id).to.be.a('number')
250 expect(playlist.uuid).to.be.a('string')
251
252 expect(playlist.isLocal).to.equal(server.serverNumber === 1)
253
254 expect(playlist.displayName).to.equal('my super playlist')
255 expect(playlist.description).to.equal('my super description')
256 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
257 expect(playlist.privacy.label).to.equal('Public')
258 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
259 expect(playlist.type.label).to.equal('Regular')
260 expect(playlist.embedPath).to.equal('/video-playlists/embed/' + playlist.uuid)
261
262 expect(playlist.videosLength).to.equal(0)
263
264 expect(playlist.ownerAccount.name).to.equal('root')
265 expect(playlist.ownerAccount.displayName).to.equal('root')
266 expect(playlist.videoChannel.name).to.equal('root_channel')
267 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
268 }
269 }
270 })
271
272 it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () {
273 this.timeout(30000)
274
275 {
276 const playlist = await servers[1].playlists.create({
277 attributes: {
278 displayName: 'playlist 2',
279 privacy: VideoPlaylistPrivacy.PUBLIC,
280 videoChannelId: servers[1].store.channel.id
281 }
282 })
283 playlistServer2Id1 = playlist.id
284 }
285
286 {
287 const playlist = await servers[1].playlists.create({
288 attributes: {
289 displayName: 'playlist 3',
290 privacy: VideoPlaylistPrivacy.PUBLIC,
291 thumbnailfile: 'custom-thumbnail.jpg',
292 videoChannelId: servers[1].store.channel.id
293 }
294 })
295
296 playlistServer2Id2 = playlist.id
297 playlistServer2UUID2 = playlist.uuid
298 }
299
300 for (const id of [ playlistServer2Id1, playlistServer2Id2 ]) {
301 await servers[1].playlists.addElement({
302 playlistId: id,
303 attributes: { videoId: servers[1].store.videos[0].id, startTimestamp: 1, stopTimestamp: 2 }
304 })
305 await servers[1].playlists.addElement({
306 playlistId: id,
307 attributes: { videoId: servers[1].store.videos[1].id }
308 })
309 }
310
311 await waitJobs(servers)
312 await wait(3000)
313
314 for (const server of [ servers[0], servers[1] ]) {
315 const body = await server.playlists.list({ start: 0, count: 5 })
316
317 const playlist2 = body.data.find(p => p.displayName === 'playlist 2')
318 expect(playlist2).to.not.be.undefined
319 await testImageGeneratedByFFmpeg(server.url, 'thumbnail-playlist', playlist2.thumbnailPath)
320
321 const playlist3 = body.data.find(p => p.displayName === 'playlist 3')
322 expect(playlist3).to.not.be.undefined
323 await testImageGeneratedByFFmpeg(server.url, 'custom-thumbnail', playlist3.thumbnailPath)
324 }
325
326 const body = await servers[2].playlists.list({ start: 0, count: 5 })
327 expect(body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
328 expect(body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
329 })
330
331 it('Should have the playlist on server 3 after a new follow', async function () {
332 this.timeout(30000)
333
334 // Server 2 and server 3 follow each other
335 await doubleFollow(servers[1], servers[2])
336
337 const body = await servers[2].playlists.list({ start: 0, count: 5 })
338
339 const playlist2 = body.data.find(p => p.displayName === 'playlist 2')
340 expect(playlist2).to.not.be.undefined
341 await testImageGeneratedByFFmpeg(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
342
343 expect(body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
344 })
345 })
346
347 describe('List playlists', function () {
348
349 it('Should correctly list the playlists', async function () {
350 this.timeout(30000)
351
352 {
353 const body = await servers[2].playlists.list({ start: 1, count: 2, sort: 'createdAt' })
354 expect(body.total).to.equal(3)
355
356 const data = body.data
357 expect(data).to.have.lengthOf(2)
358 expect(data[0].displayName).to.equal('playlist 2')
359 expect(data[1].displayName).to.equal('playlist 3')
360 }
361
362 {
363 const body = await servers[2].playlists.list({ start: 1, count: 2, sort: '-createdAt' })
364 expect(body.total).to.equal(3)
365
366 const data = body.data
367 expect(data).to.have.lengthOf(2)
368 expect(data[0].displayName).to.equal('playlist 2')
369 expect(data[1].displayName).to.equal('my super playlist')
370 }
371 })
372
373 it('Should list video channel playlists', async function () {
374 this.timeout(30000)
375
376 {
377 const body = await commands[0].listByChannel({ handle: 'root_channel', start: 0, count: 2, sort: '-createdAt' })
378 expect(body.total).to.equal(1)
379
380 const data = body.data
381 expect(data).to.have.lengthOf(1)
382 expect(data[0].displayName).to.equal('my super playlist')
383 }
384 })
385
386 it('Should list account playlists', async function () {
387 this.timeout(30000)
388
389 {
390 const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: '-createdAt' })
391 expect(body.total).to.equal(2)
392
393 const data = body.data
394 expect(data).to.have.lengthOf(1)
395 expect(data[0].displayName).to.equal('playlist 2')
396 }
397
398 {
399 const body = await servers[1].playlists.listByAccount({ handle: 'root', start: 1, count: 2, sort: 'createdAt' })
400 expect(body.total).to.equal(2)
401
402 const data = body.data
403 expect(data).to.have.lengthOf(1)
404 expect(data[0].displayName).to.equal('playlist 3')
405 }
406
407 {
408 const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '3' })
409 expect(body.total).to.equal(1)
410
411 const data = body.data
412 expect(data).to.have.lengthOf(1)
413 expect(data[0].displayName).to.equal('playlist 3')
414 }
415
416 {
417 const body = await servers[1].playlists.listByAccount({ handle: 'root', sort: 'createdAt', search: '4' })
418 expect(body.total).to.equal(0)
419
420 const data = body.data
421 expect(data).to.have.lengthOf(0)
422 }
423 })
424 })
425
426 describe('Playlist rights', function () {
427 let unlistedPlaylist: VideoPlaylistCreateResult
428 let privatePlaylist: VideoPlaylistCreateResult
429
430 before(async function () {
431 this.timeout(30000)
432
433 {
434 unlistedPlaylist = await servers[1].playlists.create({
435 attributes: {
436 displayName: 'playlist unlisted',
437 privacy: VideoPlaylistPrivacy.UNLISTED,
438 videoChannelId: servers[1].store.channel.id
439 }
440 })
441 }
442
443 {
444 privatePlaylist = await servers[1].playlists.create({
445 attributes: {
446 displayName: 'playlist private',
447 privacy: VideoPlaylistPrivacy.PRIVATE
448 }
449 })
450 }
451
452 await waitJobs(servers)
453 await wait(3000)
454 })
455
456 it('Should not list unlisted or private playlists', async function () {
457 for (const server of servers) {
458 const results = [
459 await server.playlists.listByAccount({ handle: 'root@' + servers[1].host, sort: '-createdAt' }),
460 await server.playlists.list({ start: 0, count: 2, sort: '-createdAt' })
461 ]
462
463 expect(results[0].total).to.equal(2)
464 expect(results[1].total).to.equal(3)
465
466 for (const body of results) {
467 const data = body.data
468 expect(data).to.have.lengthOf(2)
469 expect(data[0].displayName).to.equal('playlist 3')
470 expect(data[1].displayName).to.equal('playlist 2')
471 }
472 }
473 })
474
475 it('Should not get unlisted playlist using only the id', async function () {
476 await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 })
477 })
478
479 it('Should get unlisted playlist using uuid or shortUUID', async function () {
480 await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid })
481 await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID })
482 })
483
484 it('Should not get private playlist without token', async function () {
485 for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) {
486 await servers[1].playlists.get({ playlistId: id, expectedStatus: 401 })
487 }
488 })
489
490 it('Should get private playlist with a token', async function () {
491 for (const id of [ privatePlaylist.id, privatePlaylist.uuid, privatePlaylist.shortUUID ]) {
492 await servers[1].playlists.get({ token: servers[1].accessToken, playlistId: id })
493 }
494 })
495 })
496
497 describe('Update playlists', function () {
498
499 it('Should update a playlist', async function () {
500 this.timeout(30000)
501
502 await servers[1].playlists.update({
503 attributes: {
504 displayName: 'playlist 3 updated',
505 description: 'description updated',
506 privacy: VideoPlaylistPrivacy.UNLISTED,
507 thumbnailfile: 'custom-thumbnail.jpg',
508 videoChannelId: servers[1].store.channel.id
509 },
510 playlistId: playlistServer2Id2
511 })
512
513 await waitJobs(servers)
514
515 for (const server of servers) {
516 const playlist = await server.playlists.get({ playlistId: playlistServer2UUID2 })
517
518 expect(playlist.displayName).to.equal('playlist 3 updated')
519 expect(playlist.description).to.equal('description updated')
520
521 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED)
522 expect(playlist.privacy.label).to.equal('Unlisted')
523
524 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
525 expect(playlist.type.label).to.equal('Regular')
526
527 expect(playlist.videosLength).to.equal(2)
528
529 expect(playlist.ownerAccount.name).to.equal('root')
530 expect(playlist.ownerAccount.displayName).to.equal('root')
531 expect(playlist.videoChannel.name).to.equal('root_channel')
532 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
533 }
534 })
535 })
536
537 describe('Element timestamps', function () {
538
539 it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
540 this.timeout(30000)
541
542 const addVideo = (attributes: any) => {
543 return commands[0].addElement({ playlistId: playlistServer1Id, attributes })
544 }
545
546 const playlistDisplayName = 'playlist 4'
547 const playlist = await commands[0].create({
548 attributes: {
549 displayName: playlistDisplayName,
550 privacy: VideoPlaylistPrivacy.PUBLIC,
551 videoChannelId: servers[0].store.channel.id
552 }
553 })
554
555 playlistServer1Id = playlist.id
556 playlistServer1DisplayName = playlistDisplayName
557 playlistServer1UUID = playlist.uuid
558
559 await addVideo({ videoId: servers[0].store.videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
560 await addVideo({ videoId: servers[2].store.videos[1].uuid, startTimestamp: 35 })
561 await addVideo({ videoId: servers[2].store.videos[2].uuid })
562 {
563 const element = await addVideo({ videoId: servers[0].store.videos[3].uuid, stopTimestamp: 35 })
564 playlistElementServer1Video4 = element.id
565 }
566
567 {
568 const element = await addVideo({ videoId: servers[0].store.videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
569 playlistElementServer1Video5 = element.id
570 }
571
572 {
573 const element = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
574 playlistElementNSFW = element.id
575
576 await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 4 })
577 await addVideo({ videoId: nsfwVideoServer1 })
578 }
579
580 await waitJobs(servers)
581 })
582
583 it('Should correctly list playlist videos', async function () {
584 this.timeout(30000)
585
586 for (const server of servers) {
587 {
588 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
589
590 expect(body.total).to.equal(8)
591
592 const videoElements = body.data
593 expect(videoElements).to.have.lengthOf(8)
594
595 expect(videoElements[0].video.name).to.equal('video 0 server 1')
596 expect(videoElements[0].position).to.equal(1)
597 expect(videoElements[0].startTimestamp).to.equal(15)
598 expect(videoElements[0].stopTimestamp).to.equal(28)
599
600 expect(videoElements[1].video.name).to.equal('video 1 server 3')
601 expect(videoElements[1].position).to.equal(2)
602 expect(videoElements[1].startTimestamp).to.equal(35)
603 expect(videoElements[1].stopTimestamp).to.be.null
604
605 expect(videoElements[2].video.name).to.equal('video 2 server 3')
606 expect(videoElements[2].position).to.equal(3)
607 expect(videoElements[2].startTimestamp).to.be.null
608 expect(videoElements[2].stopTimestamp).to.be.null
609
610 expect(videoElements[3].video.name).to.equal('video 3 server 1')
611 expect(videoElements[3].position).to.equal(4)
612 expect(videoElements[3].startTimestamp).to.be.null
613 expect(videoElements[3].stopTimestamp).to.equal(35)
614
615 expect(videoElements[4].video.name).to.equal('video 4 server 1')
616 expect(videoElements[4].position).to.equal(5)
617 expect(videoElements[4].startTimestamp).to.equal(45)
618 expect(videoElements[4].stopTimestamp).to.equal(60)
619
620 expect(videoElements[5].video.name).to.equal('NSFW video')
621 expect(videoElements[5].position).to.equal(6)
622 expect(videoElements[5].startTimestamp).to.equal(5)
623 expect(videoElements[5].stopTimestamp).to.be.null
624
625 expect(videoElements[6].video.name).to.equal('NSFW video')
626 expect(videoElements[6].position).to.equal(7)
627 expect(videoElements[6].startTimestamp).to.equal(4)
628 expect(videoElements[6].stopTimestamp).to.be.null
629
630 expect(videoElements[7].video.name).to.equal('NSFW video')
631 expect(videoElements[7].position).to.equal(8)
632 expect(videoElements[7].startTimestamp).to.be.null
633 expect(videoElements[7].stopTimestamp).to.be.null
634 }
635
636 {
637 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 2 })
638 expect(body.data).to.have.lengthOf(2)
639 }
640 }
641 })
642 })
643
644 describe('Element type', function () {
645 let groupUser1: PeerTubeServer[]
646 let groupWithoutToken1: PeerTubeServer[]
647 let group1: PeerTubeServer[]
648 let group2: PeerTubeServer[]
649
650 let video1: string
651 let video2: string
652 let video3: string
653
654 before(async function () {
655 this.timeout(60000)
656
657 groupUser1 = [ Object.assign({}, servers[0], { accessToken: userTokenServer1 }) ]
658 groupWithoutToken1 = [ Object.assign({}, servers[0], { accessToken: undefined }) ]
659 group1 = [ servers[0] ]
660 group2 = [ servers[1], servers[2] ]
661
662 const playlist = await commands[0].create({
663 token: userTokenServer1,
664 attributes: {
665 displayName: 'playlist 56',
666 privacy: VideoPlaylistPrivacy.PUBLIC,
667 videoChannelId: servers[0].store.channel.id
668 }
669 })
670
671 const playlistServer1Id2 = playlist.id
672 playlistServer1UUID2 = playlist.uuid
673
674 const addVideo = (attributes: any) => {
675 return commands[0].addElement({ token: userTokenServer1, playlistId: playlistServer1Id2, attributes })
676 }
677
678 video1 = (await servers[0].videos.quickUpload({ name: 'video 89', token: userTokenServer1 })).uuid
679 video2 = (await servers[1].videos.quickUpload({ name: 'video 90' })).uuid
680 video3 = (await servers[0].videos.quickUpload({ name: 'video 91', nsfw: true })).uuid
681
682 await waitJobs(servers)
683
684 await addVideo({ videoId: video1, startTimestamp: 15, stopTimestamp: 28 })
685 await addVideo({ videoId: video2, startTimestamp: 35 })
686 await addVideo({ videoId: video3 })
687
688 await waitJobs(servers)
689 })
690
691 it('Should update the element type if the video is private/password protected', async function () {
692 this.timeout(20000)
693
694 const name = 'video 89'
695 const position = 1
696
697 {
698 await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PRIVATE } })
699 await waitJobs(servers)
700
701 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
702 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
703 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
704 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
705 }
706
707 {
708 await servers[0].videos.update({
709 id: video1,
710 attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] }
711 })
712 await waitJobs(servers)
713
714 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
715 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
716 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3)
717 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
718 }
719
720 {
721 await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } })
722 await waitJobs(servers)
723
724 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
725 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
726 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
727 // We deleted the video, so even if we recreated it, the old entry is still deleted
728 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
729 }
730 })
731
732 it('Should update the element type if the video is blacklisted', async function () {
733 this.timeout(20000)
734
735 const name = 'video 89'
736 const position = 1
737
738 {
739 await servers[0].blacklist.add({ videoId: video1, reason: 'reason', unfederate: true })
740 await waitJobs(servers)
741
742 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
743 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
744 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
745 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
746 }
747
748 {
749 await servers[0].blacklist.remove({ videoId: video1 })
750 await waitJobs(servers)
751
752 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
753 await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
754 await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
755 // We deleted the video (because unfederated), so even if we recreated it, the old entry is still deleted
756 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3)
757 }
758 })
759
760 it('Should update the element type if the account or server of the video is blocked', async function () {
761 this.timeout(90000)
762
763 const command = servers[0].blocklist
764
765 const name = 'video 90'
766 const position = 2
767
768 {
769 await command.addToMyBlocklist({ token: userTokenServer1, account: 'root@' + servers[1].host })
770 await waitJobs(servers)
771
772 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
773 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
774
775 await command.removeFromMyBlocklist({ token: userTokenServer1, account: 'root@' + servers[1].host })
776 await waitJobs(servers)
777
778 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
779 }
780
781 {
782 await command.addToMyBlocklist({ token: userTokenServer1, server: servers[1].host })
783 await waitJobs(servers)
784
785 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
786 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
787
788 await command.removeFromMyBlocklist({ token: userTokenServer1, server: servers[1].host })
789 await waitJobs(servers)
790
791 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
792 }
793
794 {
795 await command.addToServerBlocklist({ account: 'root@' + servers[1].host })
796 await waitJobs(servers)
797
798 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
799 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
800
801 await command.removeFromServerBlocklist({ account: 'root@' + servers[1].host })
802 await waitJobs(servers)
803
804 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
805 }
806
807 {
808 await command.addToServerBlocklist({ server: servers[1].host })
809 await waitJobs(servers)
810
811 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
812 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
813
814 await command.removeFromServerBlocklist({ server: servers[1].host })
815 await waitJobs(servers)
816
817 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
818 }
819 })
820 })
821
822 describe('Managing playlist elements', function () {
823
824 it('Should reorder the playlist', async function () {
825 this.timeout(30000)
826
827 {
828 await commands[0].reorderElements({
829 playlistId: playlistServer1Id,
830 attributes: {
831 startPosition: 2,
832 insertAfterPosition: 3
833 }
834 })
835
836 await waitJobs(servers)
837
838 for (const server of servers) {
839 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
840 const names = body.data.map(v => v.video.name)
841
842 expect(names).to.deep.equal([
843 'video 0 server 1',
844 'video 2 server 3',
845 'video 1 server 3',
846 'video 3 server 1',
847 'video 4 server 1',
848 'NSFW video',
849 'NSFW video',
850 'NSFW video'
851 ])
852 }
853 }
854
855 {
856 await commands[0].reorderElements({
857 playlistId: playlistServer1Id,
858 attributes: {
859 startPosition: 1,
860 reorderLength: 3,
861 insertAfterPosition: 4
862 }
863 })
864
865 await waitJobs(servers)
866
867 for (const server of servers) {
868 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
869 const names = body.data.map(v => v.video.name)
870
871 expect(names).to.deep.equal([
872 'video 3 server 1',
873 'video 0 server 1',
874 'video 2 server 3',
875 'video 1 server 3',
876 'video 4 server 1',
877 'NSFW video',
878 'NSFW video',
879 'NSFW video'
880 ])
881 }
882 }
883
884 {
885 await commands[0].reorderElements({
886 playlistId: playlistServer1Id,
887 attributes: {
888 startPosition: 6,
889 insertAfterPosition: 3
890 }
891 })
892
893 await waitJobs(servers)
894
895 for (const server of servers) {
896 const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
897 const names = elements.map(v => v.video.name)
898
899 expect(names).to.deep.equal([
900 'video 3 server 1',
901 'video 0 server 1',
902 'video 2 server 3',
903 'NSFW video',
904 'video 1 server 3',
905 'video 4 server 1',
906 'NSFW video',
907 'NSFW video'
908 ])
909
910 for (let i = 1; i <= elements.length; i++) {
911 expect(elements[i - 1].position).to.equal(i)
912 }
913 }
914 }
915 })
916
917 it('Should update startTimestamp/endTimestamp of some elements', async function () {
918 this.timeout(30000)
919
920 await commands[0].updateElement({
921 playlistId: playlistServer1Id,
922 elementId: playlistElementServer1Video4,
923 attributes: {
924 startTimestamp: 1
925 }
926 })
927
928 await commands[0].updateElement({
929 playlistId: playlistServer1Id,
930 elementId: playlistElementServer1Video5,
931 attributes: {
932 stopTimestamp: null
933 }
934 })
935
936 await waitJobs(servers)
937
938 for (const server of servers) {
939 const { data: elements } = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
940
941 expect(elements[0].video.name).to.equal('video 3 server 1')
942 expect(elements[0].position).to.equal(1)
943 expect(elements[0].startTimestamp).to.equal(1)
944 expect(elements[0].stopTimestamp).to.equal(35)
945
946 expect(elements[5].video.name).to.equal('video 4 server 1')
947 expect(elements[5].position).to.equal(6)
948 expect(elements[5].startTimestamp).to.equal(45)
949 expect(elements[5].stopTimestamp).to.be.null
950 }
951 })
952
953 it('Should check videos existence in my playlist', async function () {
954 const videoIds = [
955 servers[0].store.videos[0].id,
956 42000,
957 servers[0].store.videos[3].id,
958 43000,
959 servers[0].store.videos[4].id
960 ]
961 const obj = await commands[0].videosExist({ videoIds })
962
963 {
964 const elem = obj[servers[0].store.videos[0].id]
965 expect(elem).to.have.lengthOf(1)
966 expect(elem[0].playlistElementId).to.exist
967 expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName)
968 expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID))
969 expect(elem[0].playlistId).to.equal(playlistServer1Id)
970 expect(elem[0].startTimestamp).to.equal(15)
971 expect(elem[0].stopTimestamp).to.equal(28)
972 }
973
974 {
975 const elem = obj[servers[0].store.videos[3].id]
976 expect(elem).to.have.lengthOf(1)
977 expect(elem[0].playlistElementId).to.equal(playlistElementServer1Video4)
978 expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName)
979 expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID))
980 expect(elem[0].playlistId).to.equal(playlistServer1Id)
981 expect(elem[0].startTimestamp).to.equal(1)
982 expect(elem[0].stopTimestamp).to.equal(35)
983 }
984
985 {
986 const elem = obj[servers[0].store.videos[4].id]
987 expect(elem).to.have.lengthOf(1)
988 expect(elem[0].playlistId).to.equal(playlistServer1Id)
989 expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName)
990 expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID))
991 expect(elem[0].startTimestamp).to.equal(45)
992 expect(elem[0].stopTimestamp).to.equal(null)
993 }
994
995 expect(obj[42000]).to.have.lengthOf(0)
996 expect(obj[43000]).to.have.lengthOf(0)
997 })
998
999 it('Should automatically update updatedAt field of playlists', async function () {
1000 const server = servers[1]
1001 const videoId = servers[1].store.videos[5].id
1002
1003 async function getPlaylistNames () {
1004 const { data } = await server.playlists.listByAccount({ token: server.accessToken, handle: 'root', sort: '-updatedAt' })
1005
1006 return data.map(p => p.displayName)
1007 }
1008
1009 const attributes = { videoId }
1010 const element1 = await server.playlists.addElement({ playlistId: playlistServer2Id1, attributes })
1011 const element2 = await server.playlists.addElement({ playlistId: playlistServer2Id2, attributes })
1012
1013 const names1 = await getPlaylistNames()
1014 expect(names1[0]).to.equal('playlist 3 updated')
1015 expect(names1[1]).to.equal('playlist 2')
1016
1017 await server.playlists.removeElement({ playlistId: playlistServer2Id1, elementId: element1.id })
1018
1019 const names2 = await getPlaylistNames()
1020 expect(names2[0]).to.equal('playlist 2')
1021 expect(names2[1]).to.equal('playlist 3 updated')
1022
1023 await server.playlists.removeElement({ playlistId: playlistServer2Id2, elementId: element2.id })
1024
1025 const names3 = await getPlaylistNames()
1026 expect(names3[0]).to.equal('playlist 3 updated')
1027 expect(names3[1]).to.equal('playlist 2')
1028 })
1029
1030 it('Should delete some elements', async function () {
1031 this.timeout(30000)
1032
1033 await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementServer1Video4 })
1034 await commands[0].removeElement({ playlistId: playlistServer1Id, elementId: playlistElementNSFW })
1035
1036 await waitJobs(servers)
1037
1038 for (const server of servers) {
1039 const body = await server.playlists.listVideos({ playlistId: playlistServer1UUID, start: 0, count: 10 })
1040 expect(body.total).to.equal(6)
1041
1042 const elements = body.data
1043 expect(elements).to.have.lengthOf(6)
1044
1045 expect(elements[0].video.name).to.equal('video 0 server 1')
1046 expect(elements[0].position).to.equal(1)
1047
1048 expect(elements[1].video.name).to.equal('video 2 server 3')
1049 expect(elements[1].position).to.equal(2)
1050
1051 expect(elements[2].video.name).to.equal('video 1 server 3')
1052 expect(elements[2].position).to.equal(3)
1053
1054 expect(elements[3].video.name).to.equal('video 4 server 1')
1055 expect(elements[3].position).to.equal(4)
1056
1057 expect(elements[4].video.name).to.equal('NSFW video')
1058 expect(elements[4].position).to.equal(5)
1059
1060 expect(elements[5].video.name).to.equal('NSFW video')
1061 expect(elements[5].position).to.equal(6)
1062 }
1063 })
1064
1065 it('Should be able to create a public playlist, and set it to private', async function () {
1066 this.timeout(30000)
1067
1068 const videoPlaylistIds = await commands[0].create({
1069 attributes: {
1070 displayName: 'my super public playlist',
1071 privacy: VideoPlaylistPrivacy.PUBLIC,
1072 videoChannelId: servers[0].store.channel.id
1073 }
1074 })
1075
1076 await waitJobs(servers)
1077
1078 for (const server of servers) {
1079 await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 })
1080 }
1081
1082 const attributes = { privacy: VideoPlaylistPrivacy.PRIVATE }
1083 await commands[0].update({ playlistId: videoPlaylistIds.id, attributes })
1084
1085 await waitJobs(servers)
1086
1087 for (const server of [ servers[1], servers[2] ]) {
1088 await server.playlists.get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
1089 }
1090
1091 await commands[0].get({ playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
1092 await commands[0].get({ token: servers[0].accessToken, playlistId: videoPlaylistIds.uuid, expectedStatus: HttpStatusCode.OK_200 })
1093 })
1094 })
1095
1096 describe('Playlist deletion', function () {
1097
1098 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
1099 this.timeout(30000)
1100
1101 await commands[0].delete({ playlistId: playlistServer1Id })
1102
1103 await waitJobs(servers)
1104
1105 for (const server of servers) {
1106 await server.playlists.get({ playlistId: playlistServer1UUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
1107 }
1108 })
1109
1110 it('Should have deleted the thumbnail on server 1, 2 and 3', async function () {
1111 this.timeout(30000)
1112
1113 for (const server of servers) {
1114 await checkPlaylistFilesWereRemoved(playlistServer1UUID, server)
1115 }
1116 })
1117
1118 it('Should unfollow servers 1 and 2 and hide their playlists', async function () {
1119 this.timeout(30000)
1120
1121 const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'my super playlist')
1122
1123 {
1124 const body = await servers[2].playlists.list({ start: 0, count: 5 })
1125 expect(body.total).to.equal(3)
1126
1127 expect(finder(body.data)).to.not.be.undefined
1128 }
1129
1130 await servers[2].follows.unfollow({ target: servers[0] })
1131
1132 {
1133 const body = await servers[2].playlists.list({ start: 0, count: 5 })
1134 expect(body.total).to.equal(1)
1135
1136 expect(finder(body.data)).to.be.undefined
1137 }
1138 })
1139
1140 it('Should delete a channel and put the associated playlist in private mode', async function () {
1141 this.timeout(30000)
1142
1143 const channel = await servers[0].channels.create({ attributes: { name: 'super_channel', displayName: 'super channel' } })
1144
1145 const playlistCreated = await commands[0].create({
1146 attributes: {
1147 displayName: 'channel playlist',
1148 privacy: VideoPlaylistPrivacy.PUBLIC,
1149 videoChannelId: channel.id
1150 }
1151 })
1152
1153 await waitJobs(servers)
1154
1155 await servers[0].channels.delete({ channelName: 'super_channel' })
1156
1157 await waitJobs(servers)
1158
1159 const body = await commands[0].get({ token: servers[0].accessToken, playlistId: playlistCreated.uuid })
1160 expect(body.displayName).to.equal('channel playlist')
1161 expect(body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
1162
1163 await servers[1].playlists.get({ playlistId: playlistCreated.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
1164 })
1165
1166 it('Should delete an account and delete its playlists', async function () {
1167 this.timeout(30000)
1168
1169 const { userId, token } = await servers[0].users.generate('user_1')
1170
1171 const { videoChannels } = await servers[0].users.getMyInfo({ token })
1172 const userChannel = videoChannels[0]
1173
1174 await commands[0].create({
1175 attributes: {
1176 displayName: 'playlist to be deleted',
1177 privacy: VideoPlaylistPrivacy.PUBLIC,
1178 videoChannelId: userChannel.id
1179 }
1180 })
1181
1182 await waitJobs(servers)
1183
1184 const finder = (data: VideoPlaylist[]) => data.find(p => p.displayName === 'playlist to be deleted')
1185
1186 {
1187 for (const server of [ servers[0], servers[1] ]) {
1188 const body = await server.playlists.list({ start: 0, count: 15 })
1189
1190 expect(finder(body.data)).to.not.be.undefined
1191 }
1192 }
1193
1194 await servers[0].users.remove({ userId })
1195 await waitJobs(servers)
1196
1197 {
1198 for (const server of [ servers[0], servers[1] ]) {
1199 const body = await server.playlists.list({ start: 0, count: 15 })
1200
1201 expect(finder(body.data)).to.be.undefined
1202 }
1203 }
1204 })
1205 })
1206
1207 after(async function () {
1208 await cleanupTests(servers)
1209 })
1210})
diff --git a/packages/tests/src/api/videos/video-privacy.ts b/packages/tests/src/api/videos/video-privacy.ts
new file mode 100644
index 000000000..9171463a4
--- /dev/null
+++ b/packages/tests/src/api/videos/video-privacy.ts
@@ -0,0 +1,294 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test video privacy', function () {
16 const servers: PeerTubeServer[] = []
17 let anotherUserToken: string
18
19 let privateVideoId: number
20 let privateVideoUUID: string
21
22 let internalVideoId: number
23 let internalVideoUUID: string
24
25 let unlistedVideo: VideoCreateResult
26 let nonFederatedUnlistedVideoUUID: string
27
28 let now: number
29
30 const dontFederateUnlistedConfig = {
31 federation: {
32 videos: {
33 federate_unlisted: false
34 }
35 }
36 }
37
38 before(async function () {
39 this.timeout(50000)
40
41 // Run servers
42 servers.push(await createSingleServer(1, dontFederateUnlistedConfig))
43 servers.push(await createSingleServer(2))
44
45 // Get the access tokens
46 await setAccessTokensToServers(servers)
47
48 // Server 1 and server 2 follow each other
49 await doubleFollow(servers[0], servers[1])
50 })
51
52 describe('Private and internal videos', function () {
53
54 it('Should upload a private and internal videos on server 1', async function () {
55 this.timeout(50000)
56
57 for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
58 const attributes = { privacy }
59 await servers[0].videos.upload({ attributes })
60 }
61
62 await waitJobs(servers)
63 })
64
65 it('Should not have these private and internal videos on server 2', async function () {
66 const { total, data } = await servers[1].videos.list()
67
68 expect(total).to.equal(0)
69 expect(data).to.have.lengthOf(0)
70 })
71
72 it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () {
73 const { total, data } = await servers[0].videos.list()
74
75 expect(total).to.equal(0)
76 expect(data).to.have.lengthOf(0)
77 })
78
79 it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () {
80 const { total, data } = await servers[0].videos.listWithToken()
81
82 expect(total).to.equal(1)
83 expect(data).to.have.lengthOf(1)
84
85 expect(data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL)
86 })
87
88 it('Should list my (private and internal) videos', async function () {
89 const { total, data } = await servers[0].videos.listMyVideos()
90
91 expect(total).to.equal(2)
92 expect(data).to.have.lengthOf(2)
93
94 const privateVideo = data.find(v => v.privacy.id === VideoPrivacy.PRIVATE)
95 privateVideoId = privateVideo.id
96 privateVideoUUID = privateVideo.uuid
97
98 const internalVideo = data.find(v => v.privacy.id === VideoPrivacy.INTERNAL)
99 internalVideoId = internalVideo.id
100 internalVideoUUID = internalVideo.uuid
101 })
102
103 it('Should not be able to watch the private/internal video with non authenticated user', async function () {
104 await servers[0].videos.get({ id: privateVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
105 await servers[0].videos.get({ id: internalVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
106 })
107
108 it('Should not be able to watch the private video with another user', async function () {
109 const user = {
110 username: 'hello',
111 password: 'super password'
112 }
113 await servers[0].users.create({ username: user.username, password: user.password })
114
115 anotherUserToken = await servers[0].login.getAccessToken(user)
116
117 await servers[0].videos.getWithToken({
118 token: anotherUserToken,
119 id: privateVideoUUID,
120 expectedStatus: HttpStatusCode.FORBIDDEN_403
121 })
122 })
123
124 it('Should be able to watch the internal video with another user', async function () {
125 await servers[0].videos.getWithToken({ token: anotherUserToken, id: internalVideoUUID })
126 })
127
128 it('Should be able to watch the private video with the correct user', async function () {
129 await servers[0].videos.getWithToken({ id: privateVideoUUID })
130 })
131 })
132
133 describe('Unlisted videos', function () {
134
135 it('Should upload an unlisted video on server 2', async function () {
136 this.timeout(120000)
137
138 const attributes = {
139 name: 'unlisted video',
140 privacy: VideoPrivacy.UNLISTED
141 }
142 await servers[1].videos.upload({ attributes })
143
144 // Server 2 has transcoding enabled
145 await waitJobs(servers)
146 })
147
148 it('Should not have this unlisted video listed on server 1 and 2', async function () {
149 for (const server of servers) {
150 const { total, data } = await server.videos.list()
151
152 expect(total).to.equal(0)
153 expect(data).to.have.lengthOf(0)
154 }
155 })
156
157 it('Should list my (unlisted) videos', async function () {
158 const { total, data } = await servers[1].videos.listMyVideos()
159
160 expect(total).to.equal(1)
161 expect(data).to.have.lengthOf(1)
162
163 unlistedVideo = data[0]
164 })
165
166 it('Should not be able to get this unlisted video using its id', async function () {
167 await servers[1].videos.get({ id: unlistedVideo.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
168 })
169
170 it('Should be able to get this unlisted video using its uuid/shortUUID', async function () {
171 for (const server of servers) {
172 for (const id of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) {
173 const video = await server.videos.get({ id })
174
175 expect(video.name).to.equal('unlisted video')
176 }
177 }
178 })
179
180 it('Should upload a non-federating unlisted video to server 1', async function () {
181 this.timeout(30000)
182
183 const attributes = {
184 name: 'unlisted video',
185 privacy: VideoPrivacy.UNLISTED
186 }
187 await servers[0].videos.upload({ attributes })
188
189 await waitJobs(servers)
190 })
191
192 it('Should list my new unlisted video', async function () {
193 const { total, data } = await servers[0].videos.listMyVideos()
194
195 expect(total).to.equal(3)
196 expect(data).to.have.lengthOf(3)
197
198 nonFederatedUnlistedVideoUUID = data[0].uuid
199 })
200
201 it('Should be able to get non-federated unlisted video from origin', async function () {
202 const video = await servers[0].videos.get({ id: nonFederatedUnlistedVideoUUID })
203
204 expect(video.name).to.equal('unlisted video')
205 })
206
207 it('Should not be able to get non-federated unlisted video from federated server', async function () {
208 await servers[1].videos.get({ id: nonFederatedUnlistedVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
209 })
210 })
211
212 describe('Privacy update', function () {
213
214 it('Should update the private and internal videos to public on server 1', async function () {
215 this.timeout(100000)
216
217 now = Date.now()
218
219 {
220 const attributes = {
221 name: 'private video becomes public',
222 privacy: VideoPrivacy.PUBLIC
223 }
224
225 await servers[0].videos.update({ id: privateVideoId, attributes })
226 }
227
228 {
229 const attributes = {
230 name: 'internal video becomes public',
231 privacy: VideoPrivacy.PUBLIC
232 }
233 await servers[0].videos.update({ id: internalVideoId, attributes })
234 }
235
236 await wait(10000)
237 await waitJobs(servers)
238 })
239
240 it('Should have this new public video listed on server 1 and 2', async function () {
241 for (const server of servers) {
242 const { total, data } = await server.videos.list()
243 expect(total).to.equal(2)
244 expect(data).to.have.lengthOf(2)
245
246 const privateVideo = data.find(v => v.name === 'private video becomes public')
247 const internalVideo = data.find(v => v.name === 'internal video becomes public')
248
249 expect(privateVideo).to.not.be.undefined
250 expect(internalVideo).to.not.be.undefined
251
252 expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now)
253 // We don't change the publish date of internal videos
254 expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now)
255
256 expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
257 expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC)
258 }
259 })
260
261 it('Should set these videos as private and internal', async function () {
262 await servers[0].videos.update({ id: internalVideoId, attributes: { privacy: VideoPrivacy.PRIVATE } })
263 await servers[0].videos.update({ id: privateVideoId, attributes: { privacy: VideoPrivacy.INTERNAL } })
264
265 await waitJobs(servers)
266
267 for (const server of servers) {
268 const { total, data } = await server.videos.list()
269
270 expect(total).to.equal(0)
271 expect(data).to.have.lengthOf(0)
272 }
273
274 {
275 const { total, data } = await servers[0].videos.listMyVideos()
276 expect(total).to.equal(3)
277 expect(data).to.have.lengthOf(3)
278
279 const privateVideo = data.find(v => v.name === 'private video becomes public')
280 const internalVideo = data.find(v => v.name === 'internal video becomes public')
281
282 expect(privateVideo).to.not.be.undefined
283 expect(internalVideo).to.not.be.undefined
284
285 expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL)
286 expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE)
287 }
288 })
289 })
290
291 after(async function () {
292 await cleanupTests(servers)
293 })
294})
diff --git a/packages/tests/src/api/videos/video-schedule-update.ts b/packages/tests/src/api/videos/video-schedule-update.ts
new file mode 100644
index 000000000..96d71933e
--- /dev/null
+++ b/packages/tests/src/api/videos/video-schedule-update.ts
@@ -0,0 +1,155 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15function in10Seconds () {
16 const now = new Date()
17 now.setSeconds(now.getSeconds() + 10)
18
19 return now
20}
21
22describe('Test video update scheduler', function () {
23 let servers: PeerTubeServer[] = []
24 let video2UUID: string
25
26 before(async function () {
27 this.timeout(30000)
28
29 // Run servers
30 servers = await createMultipleServers(2)
31
32 await setAccessTokensToServers(servers)
33
34 await doubleFollow(servers[0], servers[1])
35 })
36
37 it('Should upload a video and schedule an update in 10 seconds', async function () {
38 const attributes = {
39 name: 'video 1',
40 privacy: VideoPrivacy.PRIVATE,
41 scheduleUpdate: {
42 updateAt: in10Seconds().toISOString(),
43 privacy: VideoPrivacy.PUBLIC
44 }
45 }
46
47 await servers[0].videos.upload({ attributes })
48
49 await waitJobs(servers)
50 })
51
52 it('Should not list the video (in privacy mode)', async function () {
53 for (const server of servers) {
54 const { total } = await server.videos.list()
55
56 expect(total).to.equal(0)
57 }
58 })
59
60 it('Should have my scheduled video in my account videos', async function () {
61 const { total, data } = await servers[0].videos.listMyVideos()
62 expect(total).to.equal(1)
63
64 const videoFromList = data[0]
65 const videoFromGet = await servers[0].videos.getWithToken({ id: videoFromList.uuid })
66
67 for (const video of [ videoFromList, videoFromGet ]) {
68 expect(video.name).to.equal('video 1')
69 expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
70 expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
71 expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
72 }
73 })
74
75 it('Should wait some seconds and have the video in public privacy', async function () {
76 this.timeout(50000)
77
78 await wait(15000)
79 await waitJobs(servers)
80
81 for (const server of servers) {
82 const { total, data } = await server.videos.list()
83
84 expect(total).to.equal(1)
85 expect(data[0].name).to.equal('video 1')
86 }
87 })
88
89 it('Should upload a video without scheduling an update', async function () {
90 const attributes = {
91 name: 'video 2',
92 privacy: VideoPrivacy.PRIVATE
93 }
94
95 const { uuid } = await servers[0].videos.upload({ attributes })
96 video2UUID = uuid
97
98 await waitJobs(servers)
99 })
100
101 it('Should update a video by scheduling an update', async function () {
102 const attributes = {
103 name: 'video 2 updated',
104 scheduleUpdate: {
105 updateAt: in10Seconds().toISOString(),
106 privacy: VideoPrivacy.PUBLIC
107 }
108 }
109
110 await servers[0].videos.update({ id: video2UUID, attributes })
111 await waitJobs(servers)
112 })
113
114 it('Should not display the updated video', async function () {
115 for (const server of servers) {
116 const { total } = await server.videos.list()
117
118 expect(total).to.equal(1)
119 }
120 })
121
122 it('Should have my scheduled updated video in my account videos', async function () {
123 const { total, data } = await servers[0].videos.listMyVideos()
124 expect(total).to.equal(2)
125
126 const video = data.find(v => v.uuid === video2UUID)
127 expect(video).not.to.be.undefined
128
129 expect(video.name).to.equal('video 2 updated')
130 expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
131
132 expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
133 expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
134 })
135
136 it('Should wait some seconds and have the updated video in public privacy', async function () {
137 this.timeout(20000)
138
139 await wait(15000)
140 await waitJobs(servers)
141
142 for (const server of servers) {
143 const { total, data } = await server.videos.list()
144 expect(total).to.equal(2)
145
146 const video = data.find(v => v.uuid === video2UUID)
147 expect(video).not.to.be.undefined
148 expect(video.name).to.equal('video 2 updated')
149 }
150 })
151
152 after(async function () {
153 await cleanupTests(servers)
154 })
155})
diff --git a/packages/tests/src/api/videos/video-source.ts b/packages/tests/src/api/videos/video-source.ts
new file mode 100644
index 000000000..efe8c3802
--- /dev/null
+++ b/packages/tests/src/api/videos/video-source.ts
@@ -0,0 +1,448 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2import { expect } from 'chai'
3import { getAllFiles } from '@peertube/peertube-core-utils'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import { expectStartWith } from '@tests/shared/checks.js'
6import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
7import {
8 cleanupTests,
9 createMultipleServers,
10 doubleFollow,
11 makeGetRequest,
12 makeRawRequest,
13 ObjectStorageCommand,
14 PeerTubeServer,
15 setAccessTokensToServers,
16 setDefaultAccountAvatar,
17 setDefaultVideoChannel,
18 waitJobs
19} from '@peertube/peertube-server-commands'
20
21describe('Test a video file replacement', function () {
22 let servers: PeerTubeServer[] = []
23
24 let replaceDate: Date
25 let userToken: string
26 let uuid: string
27
28 before(async function () {
29 this.timeout(50000)
30
31 servers = await createMultipleServers(2)
32
33 // Get the access tokens
34 await setAccessTokensToServers(servers)
35 await setDefaultVideoChannel(servers)
36 await setDefaultAccountAvatar(servers)
37
38 await servers[0].config.enableFileUpdate()
39
40 userToken = await servers[0].users.generateUserAndToken('user1')
41
42 // Server 1 and server 2 follow each other
43 await doubleFollow(servers[0], servers[1])
44 })
45
46 describe('Getting latest video source', () => {
47 const fixture = 'video_short.webm'
48 const uuids: string[] = []
49
50 it('Should get the source filename with legacy upload', async function () {
51 this.timeout(30000)
52
53 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'legacy' })
54 uuids.push(uuid)
55
56 const source = await servers[0].videos.getSource({ id: uuid })
57 expect(source.filename).to.equal(fixture)
58 })
59
60 it('Should get the source filename with resumable upload', async function () {
61 this.timeout(30000)
62
63 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'my video', fixture }, mode: 'resumable' })
64 uuids.push(uuid)
65
66 const source = await servers[0].videos.getSource({ id: uuid })
67 expect(source.filename).to.equal(fixture)
68 })
69
70 after(async function () {
71 this.timeout(60000)
72
73 for (const uuid of uuids) {
74 await servers[0].videos.remove({ id: uuid })
75 }
76
77 await waitJobs(servers)
78 })
79 })
80
81 describe('Updating video source', function () {
82
83 describe('Filesystem', function () {
84
85 it('Should replace a video file with transcoding disabled', async function () {
86 this.timeout(120000)
87
88 await servers[0].config.disableTranscoding()
89
90 const { uuid } = await servers[0].videos.quickUpload({ name: 'fs without transcoding', fixture: 'video_short_720p.mp4' })
91 await waitJobs(servers)
92
93 for (const server of servers) {
94 const video = await server.videos.get({ id: uuid })
95
96 const files = getAllFiles(video)
97 expect(files).to.have.lengthOf(1)
98 expect(files[0].resolution.id).to.equal(720)
99 }
100
101 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
102 await waitJobs(servers)
103
104 for (const server of servers) {
105 const video = await server.videos.get({ id: uuid })
106
107 const files = getAllFiles(video)
108 expect(files).to.have.lengthOf(1)
109 expect(files[0].resolution.id).to.equal(360)
110 }
111 })
112
113 it('Should replace a video file with transcoding enabled', async function () {
114 this.timeout(120000)
115
116 const previousPaths: string[] = []
117
118 await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
119
120 const { uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'fs with transcoding', fixture: 'video_short_720p.mp4' })
121 uuid = videoUUID
122
123 await waitJobs(servers)
124
125 for (const server of servers) {
126 const video = await server.videos.get({ id: uuid })
127 expect(video.inputFileUpdatedAt).to.be.null
128
129 const files = getAllFiles(video)
130 expect(files).to.have.lengthOf(6 * 2)
131
132 // Grab old paths to ensure we'll regenerate
133
134 previousPaths.push(video.previewPath)
135 previousPaths.push(video.thumbnailPath)
136
137 for (const file of files) {
138 previousPaths.push(file.fileUrl)
139 previousPaths.push(file.torrentUrl)
140 previousPaths.push(file.metadataUrl)
141
142 const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
143 previousPaths.push(JSON.stringify(metadata))
144 }
145
146 const { storyboards } = await server.storyboard.list({ id: uuid })
147 for (const s of storyboards) {
148 previousPaths.push(s.storyboardPath)
149 }
150 }
151
152 replaceDate = new Date()
153
154 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
155 await waitJobs(servers)
156
157 for (const server of servers) {
158 const video = await server.videos.get({ id: uuid })
159
160 expect(video.inputFileUpdatedAt).to.not.be.null
161 expect(new Date(video.inputFileUpdatedAt)).to.be.above(replaceDate)
162
163 const files = getAllFiles(video)
164 expect(files).to.have.lengthOf(4 * 2)
165
166 expect(previousPaths).to.not.include(video.previewPath)
167 expect(previousPaths).to.not.include(video.thumbnailPath)
168
169 await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
170 await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
171
172 for (const file of files) {
173 expect(previousPaths).to.not.include(file.fileUrl)
174 expect(previousPaths).to.not.include(file.torrentUrl)
175 expect(previousPaths).to.not.include(file.metadataUrl)
176
177 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
178 await makeRawRequest({ url: file.torrentUrl, expectedStatus: HttpStatusCode.OK_200 })
179
180 const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
181 expect(previousPaths).to.not.include(JSON.stringify(metadata))
182 }
183
184 const { storyboards } = await server.storyboard.list({ id: uuid })
185 for (const s of storyboards) {
186 expect(previousPaths).to.not.include(s.storyboardPath)
187
188 await makeGetRequest({ url: server.url, path: s.storyboardPath, expectedStatus: HttpStatusCode.OK_200 })
189 }
190 }
191
192 await servers[0].config.enableMinimumTranscoding()
193 })
194
195 it('Should have cleaned up old files', async function () {
196 {
197 const count = await servers[0].servers.countFiles('storyboards')
198 expect(count).to.equal(2)
199 }
200
201 {
202 const count = await servers[0].servers.countFiles('web-videos')
203 expect(count).to.equal(5 + 1) // +1 for private directory
204 }
205
206 {
207 const count = await servers[0].servers.countFiles('streaming-playlists/hls')
208 expect(count).to.equal(1 + 1) // +1 for private directory
209 }
210
211 {
212 const count = await servers[0].servers.countFiles('torrents')
213 expect(count).to.equal(9)
214 }
215 })
216
217 it('Should have the correct source input', async function () {
218 const source = await servers[0].videos.getSource({ id: uuid })
219
220 expect(source.filename).to.equal('video_short_360p.mp4')
221 expect(new Date(source.createdAt)).to.be.above(replaceDate)
222 })
223
224 it('Should not have regenerated miniatures that were previously uploaded', async function () {
225 this.timeout(120000)
226
227 const { uuid } = await servers[0].videos.upload({
228 attributes: {
229 name: 'custom miniatures',
230 thumbnailfile: 'custom-thumbnail.jpg',
231 previewfile: 'custom-preview.jpg'
232 }
233 })
234
235 await waitJobs(servers)
236
237 const previousPaths: string[] = []
238
239 for (const server of servers) {
240 const video = await server.videos.get({ id: uuid })
241
242 previousPaths.push(video.previewPath)
243 previousPaths.push(video.thumbnailPath)
244
245 await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
246 await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
247 }
248
249 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
250 await waitJobs(servers)
251
252 for (const server of servers) {
253 const video = await server.videos.get({ id: uuid })
254
255 expect(previousPaths).to.include(video.previewPath)
256 expect(previousPaths).to.include(video.thumbnailPath)
257
258 await makeGetRequest({ url: server.url, path: video.previewPath, expectedStatus: HttpStatusCode.OK_200 })
259 await makeGetRequest({ url: server.url, path: video.thumbnailPath, expectedStatus: HttpStatusCode.OK_200 })
260 }
261 })
262 })
263
264 describe('Autoblacklist', function () {
265
266 function updateAutoBlacklist (enabled: boolean) {
267 return servers[0].config.updateExistingSubConfig({
268 newConfig: {
269 autoBlacklist: {
270 videos: {
271 ofUsers: {
272 enabled
273 }
274 }
275 }
276 }
277 })
278 }
279
280 async function expectBlacklist (uuid: string, value: boolean) {
281 const video = await servers[0].videos.getWithToken({ id: uuid })
282
283 expect(video.blacklisted).to.equal(value)
284 }
285
286 before(async function () {
287 await updateAutoBlacklist(true)
288 })
289
290 it('Should auto blacklist an unblacklisted video after file replacement', async function () {
291 this.timeout(120000)
292
293 const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' })
294 await waitJobs(servers)
295 await expectBlacklist(uuid, true)
296
297 await servers[0].blacklist.remove({ videoId: uuid })
298 await expectBlacklist(uuid, false)
299
300 await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short_360p.mp4' })
301 await waitJobs(servers)
302
303 await expectBlacklist(uuid, true)
304 })
305
306 it('Should auto blacklist an already blacklisted video after file replacement', async function () {
307 this.timeout(120000)
308
309 const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' })
310 await waitJobs(servers)
311 await expectBlacklist(uuid, true)
312
313 await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short_360p.mp4' })
314 await waitJobs(servers)
315
316 await expectBlacklist(uuid, true)
317 })
318
319 it('Should not auto blacklist if auto blacklist has been disabled between the upload and the replacement', async function () {
320 this.timeout(120000)
321
322 const { uuid } = await servers[0].videos.quickUpload({ token: userToken, name: 'user video' })
323 await waitJobs(servers)
324 await expectBlacklist(uuid, true)
325
326 await servers[0].blacklist.remove({ videoId: uuid })
327 await expectBlacklist(uuid, false)
328
329 await updateAutoBlacklist(false)
330
331 await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short1.webm' })
332 await waitJobs(servers)
333
334 await expectBlacklist(uuid, false)
335 })
336 })
337
338 describe('With object storage enabled', function () {
339 if (areMockObjectStorageTestsDisabled()) return
340
341 const objectStorage = new ObjectStorageCommand()
342
343 before(async function () {
344 this.timeout(120000)
345
346 const configOverride = objectStorage.getDefaultMockConfig()
347 await objectStorage.prepareDefaultMockBuckets()
348
349 await servers[0].kill()
350 await servers[0].run(configOverride)
351 })
352
353 it('Should replace a video file with transcoding disabled', async function () {
354 this.timeout(120000)
355
356 await servers[0].config.disableTranscoding()
357
358 const { uuid } = await servers[0].videos.quickUpload({
359 name: 'object storage without transcoding',
360 fixture: 'video_short_720p.mp4'
361 })
362 await waitJobs(servers)
363
364 for (const server of servers) {
365 const video = await server.videos.get({ id: uuid })
366
367 const files = getAllFiles(video)
368 expect(files).to.have.lengthOf(1)
369 expect(files[0].resolution.id).to.equal(720)
370 expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
371 }
372
373 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_360p.mp4' })
374 await waitJobs(servers)
375
376 for (const server of servers) {
377 const video = await server.videos.get({ id: uuid })
378
379 const files = getAllFiles(video)
380 expect(files).to.have.lengthOf(1)
381 expect(files[0].resolution.id).to.equal(360)
382 expectStartWith(files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
383 }
384 })
385
386 it('Should replace a video file with transcoding enabled', async function () {
387 this.timeout(120000)
388
389 const previousPaths: string[] = []
390
391 await servers[0].config.enableTranscoding({ hls: true, webVideo: true, with0p: true })
392
393 const { uuid: videoUUID } = await servers[0].videos.quickUpload({
394 name: 'object storage with transcoding',
395 fixture: 'video_short_360p.mp4'
396 })
397 uuid = videoUUID
398
399 await waitJobs(servers)
400
401 for (const server of servers) {
402 const video = await server.videos.get({ id: uuid })
403
404 const files = getAllFiles(video)
405 expect(files).to.have.lengthOf(4 * 2)
406
407 for (const file of files) {
408 previousPaths.push(file.fileUrl)
409 }
410
411 for (const file of video.files) {
412 expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl())
413 }
414
415 for (const file of video.streamingPlaylists[0].files) {
416 expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
417 }
418 }
419
420 await servers[0].videos.replaceSourceFile({ videoId: uuid, fixture: 'video_short_240p.mp4' })
421 await waitJobs(servers)
422
423 for (const server of servers) {
424 const video = await server.videos.get({ id: uuid })
425
426 const files = getAllFiles(video)
427 expect(files).to.have.lengthOf(3 * 2)
428
429 for (const file of files) {
430 expect(previousPaths).to.not.include(file.fileUrl)
431 }
432
433 for (const file of video.files) {
434 expectStartWith(file.fileUrl, objectStorage.getMockWebVideosBaseUrl())
435 }
436
437 for (const file of video.streamingPlaylists[0].files) {
438 expectStartWith(file.fileUrl, objectStorage.getMockPlaylistBaseUrl())
439 }
440 }
441 })
442 })
443 })
444
445 after(async function () {
446 await cleanupTests(servers)
447 })
448})
diff --git a/packages/tests/src/api/videos/video-static-file-privacy.ts b/packages/tests/src/api/videos/video-static-file-privacy.ts
new file mode 100644
index 000000000..7c8d14815
--- /dev/null
+++ b/packages/tests/src/api/videos/video-static-file-privacy.ts
@@ -0,0 +1,602 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { decode } from 'magnet-uri'
5import { getAllFiles, wait } from '@peertube/peertube-core-utils'
6import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 createSingleServer,
10 findExternalSavedVideo,
11 makeRawRequest,
12 PeerTubeServer,
13 sendRTMPStream,
14 setAccessTokensToServers,
15 setDefaultVideoChannel,
16 stopFfmpeg,
17 waitJobs
18} from '@peertube/peertube-server-commands'
19import { expectStartWith } from '@tests/shared/checks.js'
20import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js'
21import { parseTorrentVideo } from '@tests/shared/webtorrent.js'
22
23describe('Test video static file privacy', function () {
24 let server: PeerTubeServer
25 let userToken: string
26
27 before(async function () {
28 this.timeout(50000)
29
30 server = await createSingleServer(1)
31 await setAccessTokensToServers([ server ])
32 await setDefaultVideoChannel([ server ])
33
34 userToken = await server.users.generateUserAndToken('user1')
35 })
36
37 describe('VOD static file path', function () {
38
39 function runSuite () {
40
41 async function checkPrivateFiles (uuid: string) {
42 const video = await server.videos.getWithToken({ id: uuid })
43
44 for (const file of video.files) {
45 expect(file.fileDownloadUrl).to.not.include('/private/')
46 expectStartWith(file.fileUrl, server.url + '/static/web-videos/private/')
47
48 const torrent = await parseTorrentVideo(server, file)
49 expect(torrent.urlList).to.have.lengthOf(0)
50
51 const magnet = decode(file.magnetUri)
52 expect(magnet.urlList).to.have.lengthOf(0)
53
54 await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
55 }
56
57 const hls = video.streamingPlaylists[0]
58 if (hls) {
59 expectStartWith(hls.playlistUrl, server.url + '/static/streaming-playlists/hls/private/')
60 expectStartWith(hls.segmentsSha256Url, server.url + '/static/streaming-playlists/hls/private/')
61
62 await makeRawRequest({ url: hls.playlistUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
63 await makeRawRequest({ url: hls.segmentsSha256Url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
64 }
65 }
66
67 async function checkPublicFiles (uuid: string) {
68 const video = await server.videos.get({ id: uuid })
69
70 for (const file of getAllFiles(video)) {
71 expect(file.fileDownloadUrl).to.not.include('/private/')
72 expect(file.fileUrl).to.not.include('/private/')
73
74 const torrent = await parseTorrentVideo(server, file)
75 expect(torrent.urlList[0]).to.not.include('private')
76
77 const magnet = decode(file.magnetUri)
78 expect(magnet.urlList[0]).to.not.include('private')
79
80 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
81 await makeRawRequest({ url: torrent.urlList[0], expectedStatus: HttpStatusCode.OK_200 })
82 await makeRawRequest({ url: magnet.urlList[0], expectedStatus: HttpStatusCode.OK_200 })
83 }
84
85 const hls = video.streamingPlaylists[0]
86 if (hls) {
87 expect(hls.playlistUrl).to.not.include('private')
88 expect(hls.segmentsSha256Url).to.not.include('private')
89
90 await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
91 await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
92 }
93 }
94
95 it('Should upload a private/internal/password protected video and have a private static path', async function () {
96 this.timeout(120000)
97
98 for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
99 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy })
100 await waitJobs([ server ])
101
102 await checkPrivateFiles(uuid)
103 }
104
105 const { uuid } = await server.videos.quickUpload({
106 name: 'video',
107 privacy: VideoPrivacy.PASSWORD_PROTECTED,
108 videoPasswords: [ 'my super password' ]
109 })
110 await waitJobs([ server ])
111
112 await checkPrivateFiles(uuid)
113 })
114
115 it('Should upload a public video and update it as private/internal to have a private static path', async function () {
116 this.timeout(120000)
117
118 for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) {
119 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC })
120 await waitJobs([ server ])
121
122 await server.videos.update({ id: uuid, attributes: { privacy } })
123 await waitJobs([ server ])
124
125 await checkPrivateFiles(uuid)
126 }
127 })
128
129 it('Should upload a private video and update it to unlisted to have a public static path', async function () {
130 this.timeout(120000)
131
132 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
133 await waitJobs([ server ])
134
135 await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.UNLISTED } })
136 await waitJobs([ server ])
137
138 await checkPublicFiles(uuid)
139 })
140
141 it('Should upload an internal video and update it to public to have a public static path', async function () {
142 this.timeout(120000)
143
144 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL })
145 await waitJobs([ server ])
146
147 await server.videos.update({ id: uuid, attributes: { privacy: VideoPrivacy.PUBLIC } })
148 await waitJobs([ server ])
149
150 await checkPublicFiles(uuid)
151 })
152
153 it('Should upload an internal video and schedule a public publish', async function () {
154 this.timeout(120000)
155
156 const attributes = {
157 name: 'video',
158 privacy: VideoPrivacy.PRIVATE,
159 scheduleUpdate: {
160 updateAt: new Date(Date.now() + 1000).toISOString(),
161 privacy: VideoPrivacy.PUBLIC
162 }
163 }
164
165 const { uuid } = await server.videos.upload({ attributes })
166
167 await waitJobs([ server ])
168 await wait(1000)
169 await server.debug.sendCommand({ body: { command: 'process-update-videos-scheduler' } })
170
171 await waitJobs([ server ])
172
173 await checkPublicFiles(uuid)
174 })
175 }
176
177 describe('Without transcoding', function () {
178 runSuite()
179 })
180
181 describe('With transcoding', function () {
182
183 before(async function () {
184 await server.config.enableMinimumTranscoding()
185 })
186
187 runSuite()
188 })
189 })
190
191 describe('VOD static file right check', function () {
192 let unrelatedFileToken: string
193
194 async function checkVideoFiles (options: {
195 id: string
196 expectedStatus: HttpStatusCodeType
197 token: string
198 videoFileToken: string
199 videoPassword?: string
200 }) {
201 const { id, expectedStatus, token, videoFileToken, videoPassword } = options
202
203 const video = await server.videos.getWithToken({ id })
204
205 for (const file of getAllFiles(video)) {
206 await makeRawRequest({ url: file.fileUrl, token, expectedStatus })
207 await makeRawRequest({ url: file.fileDownloadUrl, token, expectedStatus })
208
209 await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus })
210 await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus })
211
212 if (videoPassword) {
213 const headers = { 'x-peertube-video-password': videoPassword }
214 await makeRawRequest({ url: file.fileUrl, headers, expectedStatus })
215 await makeRawRequest({ url: file.fileDownloadUrl, headers, expectedStatus })
216 }
217 }
218
219 const hls = video.streamingPlaylists[0]
220 await makeRawRequest({ url: hls.playlistUrl, token, expectedStatus })
221 await makeRawRequest({ url: hls.segmentsSha256Url, token, expectedStatus })
222
223 await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus })
224 await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus })
225
226 if (videoPassword) {
227 const headers = { 'x-peertube-video-password': videoPassword }
228 await makeRawRequest({ url: hls.playlistUrl, token: null, headers, expectedStatus })
229 await makeRawRequest({ url: hls.segmentsSha256Url, token: null, headers, expectedStatus })
230 }
231 }
232
233 before(async function () {
234 await server.config.enableMinimumTranscoding()
235
236 const { uuid } = await server.videos.quickUpload({ name: 'another video' })
237 unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
238 })
239
240 it('Should not be able to access a private video files without OAuth token and file token', async function () {
241 this.timeout(120000)
242
243 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
244 await waitJobs([ server ])
245
246 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null })
247 })
248
249 it('Should not be able to access password protected video files without OAuth token, file token and password', async function () {
250 this.timeout(120000)
251 const videoPassword = 'my super password'
252
253 const { uuid } = await server.videos.quickUpload({
254 name: 'password protected video',
255 privacy: VideoPrivacy.PASSWORD_PROTECTED,
256 videoPasswords: [ videoPassword ]
257 })
258 await waitJobs([ server ])
259
260 await checkVideoFiles({
261 id: uuid,
262 expectedStatus: HttpStatusCode.FORBIDDEN_403,
263 token: null,
264 videoFileToken: null,
265 videoPassword: null
266 })
267 })
268
269 it('Should not be able to access an password video files with incorrect OAuth token, file token and password', async function () {
270 this.timeout(120000)
271 const videoPassword = 'my super password'
272
273 const { uuid } = await server.videos.quickUpload({
274 name: 'password protected video',
275 privacy: VideoPrivacy.PASSWORD_PROTECTED,
276 videoPasswords: [ videoPassword ]
277 })
278 await waitJobs([ server ])
279
280 await checkVideoFiles({
281 id: uuid,
282 expectedStatus: HttpStatusCode.FORBIDDEN_403,
283 token: userToken,
284 videoFileToken: unrelatedFileToken,
285 videoPassword: 'incorrectPassword'
286 })
287 })
288
289 it('Should not be able to access an private video files without appropriate OAuth token and file token', async function () {
290 this.timeout(120000)
291
292 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
293 await waitJobs([ server ])
294
295 await checkVideoFiles({
296 id: uuid,
297 expectedStatus: HttpStatusCode.FORBIDDEN_403,
298 token: userToken,
299 videoFileToken: unrelatedFileToken
300 })
301 })
302
303 it('Should be able to access a private video files with appropriate OAuth token or file token', async function () {
304 this.timeout(120000)
305
306 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
307 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
308
309 await waitJobs([ server ])
310
311 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken })
312 })
313
314 it('Should be able to access a password protected video files with appropriate OAuth token or file token', async function () {
315 this.timeout(120000)
316 const videoPassword = 'my super password'
317
318 const { uuid } = await server.videos.quickUpload({
319 name: 'video',
320 privacy: VideoPrivacy.PASSWORD_PROTECTED,
321 videoPasswords: [ videoPassword ]
322 })
323
324 const videoFileToken = await server.videoToken.getVideoFileToken({ token: null, videoId: uuid, videoPassword })
325
326 await waitJobs([ server ])
327
328 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken, videoPassword })
329 })
330
331 it('Should reinject video file token', async function () {
332 this.timeout(120000)
333
334 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE })
335
336 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
337 await waitJobs([ server ])
338
339 {
340 const video = await server.videos.getWithToken({ id: uuid })
341 const hls = video.streamingPlaylists[0]
342 const query = { videoFileToken }
343 const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 })
344
345 expect(text).to.not.include(videoFileToken)
346 }
347
348 {
349 await checkVideoFileTokenReinjection({
350 server,
351 videoUUID: uuid,
352 videoFileToken,
353 resolutions: [ 240, 720 ],
354 isLive: false
355 })
356 }
357 })
358
359 it('Should be able to access a private video of another user with an admin OAuth token or file token', async function () {
360 this.timeout(120000)
361
362 const { uuid } = await server.videos.quickUpload({ name: 'video', token: userToken, privacy: VideoPrivacy.PRIVATE })
363 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
364
365 await waitJobs([ server ])
366
367 await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken })
368 })
369 })
370
371 describe('Live static file path and check', function () {
372 let normalLiveId: string
373 let normalLive: LiveVideo
374
375 let permanentLiveId: string
376 let permanentLive: LiveVideo
377
378 let passwordProtectedLiveId: string
379 let passwordProtectedLive: LiveVideo
380
381 const correctPassword = 'my super password'
382
383 let unrelatedFileToken: string
384
385 async function checkLiveFiles (options: { live: LiveVideo, liveId: string, videoPassword?: string }) {
386 const { live, liveId, videoPassword } = options
387 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
388 await server.live.waitUntilPublished({ videoId: liveId })
389
390 const video = await server.videos.getWithToken({ id: liveId })
391
392 const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid })
393
394 const hls = video.streamingPlaylists[0]
395
396 for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
397 expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/')
398
399 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
400 await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
401
402 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
403 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
404 await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
405
406 if (videoPassword) {
407 await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 })
408 await makeRawRequest({
409 url,
410 headers: { 'x-peertube-video-password': 'incorrectPassword' },
411 expectedStatus: HttpStatusCode.FORBIDDEN_403
412 })
413 }
414
415 }
416
417 await stopFfmpeg(ffmpegCommand)
418 }
419
420 async function checkReplay (replay: VideoDetails) {
421 const fileToken = await server.videoToken.getVideoFileToken({ videoId: replay.uuid })
422
423 const hls = replay.streamingPlaylists[0]
424 expect(hls.files).to.not.have.lengthOf(0)
425
426 for (const file of hls.files) {
427 await makeRawRequest({ url: file.fileUrl, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
428 await makeRawRequest({ url: file.fileUrl, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
429
430 await makeRawRequest({ url: file.fileUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
431 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
432 await makeRawRequest({
433 url: file.fileUrl,
434 query: { videoFileToken: unrelatedFileToken },
435 expectedStatus: HttpStatusCode.FORBIDDEN_403
436 })
437 }
438
439 for (const url of [ hls.playlistUrl, hls.segmentsSha256Url ]) {
440 expectStartWith(url, server.url + '/static/streaming-playlists/hls/private/')
441
442 await makeRawRequest({ url, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
443 await makeRawRequest({ url, query: { videoFileToken: fileToken }, expectedStatus: HttpStatusCode.OK_200 })
444
445 await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
446 await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
447 await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
448 }
449 }
450
451 before(async function () {
452 await server.config.enableMinimumTranscoding()
453
454 const { uuid } = await server.videos.quickUpload({ name: 'another video' })
455 unrelatedFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid })
456
457 await server.config.enableLive({
458 allowReplay: true,
459 transcoding: true,
460 resolutions: 'min'
461 })
462
463 {
464 const { video, live } = await server.live.quickCreate({
465 saveReplay: true,
466 permanentLive: false,
467 privacy: VideoPrivacy.PRIVATE
468 })
469 normalLiveId = video.uuid
470 normalLive = live
471 }
472
473 {
474 const { video, live } = await server.live.quickCreate({
475 saveReplay: true,
476 permanentLive: true,
477 privacy: VideoPrivacy.PRIVATE
478 })
479 permanentLiveId = video.uuid
480 permanentLive = live
481 }
482
483 {
484 const { video, live } = await server.live.quickCreate({
485 saveReplay: false,
486 permanentLive: false,
487 privacy: VideoPrivacy.PASSWORD_PROTECTED,
488 videoPasswords: [ correctPassword ]
489 })
490 passwordProtectedLiveId = video.uuid
491 passwordProtectedLive = live
492 }
493 })
494
495 it('Should create a private normal live and have a private static path', async function () {
496 this.timeout(240000)
497
498 await checkLiveFiles({ live: normalLive, liveId: normalLiveId })
499 })
500
501 it('Should create a private permanent live and have a private static path', async function () {
502 this.timeout(240000)
503
504 await checkLiveFiles({ live: permanentLive, liveId: permanentLiveId })
505 })
506
507 it('Should create a password protected live and have a private static path', async function () {
508 this.timeout(240000)
509
510 await checkLiveFiles({ live: passwordProtectedLive, liveId: passwordProtectedLiveId, videoPassword: correctPassword })
511 })
512
513 it('Should reinject video file token on permanent live', async function () {
514 this.timeout(240000)
515
516 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey })
517 await server.live.waitUntilPublished({ videoId: permanentLiveId })
518
519 const video = await server.videos.getWithToken({ id: permanentLiveId })
520 const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid })
521 const hls = video.streamingPlaylists[0]
522
523 {
524 const query = { videoFileToken }
525 const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 })
526
527 expect(text).to.not.include(videoFileToken)
528 }
529
530 {
531 await checkVideoFileTokenReinjection({
532 server,
533 videoUUID: permanentLiveId,
534 videoFileToken,
535 resolutions: [ 720 ],
536 isLive: true
537 })
538 }
539
540 await stopFfmpeg(ffmpegCommand)
541 })
542
543 it('Should have created a replay of the normal live with a private static path', async function () {
544 this.timeout(240000)
545
546 await server.live.waitUntilReplacedByReplay({ videoId: normalLiveId })
547
548 const replay = await server.videos.getWithToken({ id: normalLiveId })
549 await checkReplay(replay)
550 })
551
552 it('Should have created a replay of the permanent live with a private static path', async function () {
553 this.timeout(240000)
554
555 await server.live.waitUntilWaiting({ videoId: permanentLiveId })
556 await waitJobs([ server ])
557
558 const live = await server.videos.getWithToken({ id: permanentLiveId })
559 const replayFromList = await findExternalSavedVideo(server, live)
560 const replay = await server.videos.getWithToken({ id: replayFromList.id })
561
562 await checkReplay(replay)
563 })
564 })
565
566 describe('With static file right check disabled', function () {
567 let videoUUID: string
568
569 before(async function () {
570 this.timeout(240000)
571
572 await server.kill()
573
574 await server.run({
575 static_files: {
576 private_files_require_auth: false
577 }
578 })
579
580 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL })
581 videoUUID = uuid
582
583 await waitJobs([ server ])
584 })
585
586 it('Should not check auth for private static files', async function () {
587 const video = await server.videos.getWithToken({ id: videoUUID })
588
589 for (const file of getAllFiles(video)) {
590 await makeRawRequest({ url: file.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
591 }
592
593 const hls = video.streamingPlaylists[0]
594 await makeRawRequest({ url: hls.playlistUrl, expectedStatus: HttpStatusCode.OK_200 })
595 await makeRawRequest({ url: hls.segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 })
596 })
597 })
598
599 after(async function () {
600 await cleanupTests([ server ])
601 })
602})
diff --git a/packages/tests/src/api/videos/video-storyboard.ts b/packages/tests/src/api/videos/video-storyboard.ts
new file mode 100644
index 000000000..7d156aa7f
--- /dev/null
+++ b/packages/tests/src/api/videos/video-storyboard.ts
@@ -0,0 +1,213 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { readdir } from 'fs/promises'
5import { basename } from 'path'
6import { FIXTURE_URLS } from '@tests/shared/tests.js'
7import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
8import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
9import {
10 cleanupTests,
11 createMultipleServers,
12 doubleFollow,
13 makeGetRequest,
14 PeerTubeServer,
15 sendRTMPStream,
16 setAccessTokensToServers,
17 setDefaultVideoChannel,
18 stopFfmpeg,
19 waitJobs
20} from '@peertube/peertube-server-commands'
21
22async function checkStoryboard (options: {
23 server: PeerTubeServer
24 uuid: string
25 tilesCount?: number
26 minSize?: number
27}) {
28 const { server, uuid, tilesCount, minSize = 1000 } = options
29
30 const { storyboards } = await server.storyboard.list({ id: uuid })
31
32 expect(storyboards).to.have.lengthOf(1)
33
34 const storyboard = storyboards[0]
35
36 expect(storyboard.spriteDuration).to.equal(1)
37 expect(storyboard.spriteHeight).to.equal(108)
38 expect(storyboard.spriteWidth).to.equal(192)
39 expect(storyboard.storyboardPath).to.exist
40
41 if (tilesCount) {
42 expect(storyboard.totalWidth).to.equal(192 * Math.min(tilesCount, 10))
43 expect(storyboard.totalHeight).to.equal(108 * Math.max((tilesCount / 10), 1))
44 }
45
46 const { body } = await makeGetRequest({ url: server.url, path: storyboard.storyboardPath, expectedStatus: HttpStatusCode.OK_200 })
47 expect(body.length).to.be.above(minSize)
48}
49
50describe('Test video storyboard', function () {
51 let servers: PeerTubeServer[]
52
53 let baseUUID: string
54
55 before(async function () {
56 this.timeout(120000)
57
58 servers = await createMultipleServers(2)
59 await setAccessTokensToServers(servers)
60 await setDefaultVideoChannel(servers)
61
62 await doubleFollow(servers[0], servers[1])
63 })
64
65 it('Should generate a storyboard after upload without transcoding', async function () {
66 this.timeout(120000)
67
68 // 5s video
69 const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' })
70 baseUUID = uuid
71 await waitJobs(servers)
72
73 for (const server of servers) {
74 await checkStoryboard({ server, uuid, tilesCount: 5 })
75 }
76 })
77
78 it('Should generate a storyboard after upload without transcoding with a long video', async function () {
79 this.timeout(120000)
80
81 // 124s video
82 const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_very_long_10p.mp4' })
83 await waitJobs(servers)
84
85 for (const server of servers) {
86 await checkStoryboard({ server, uuid, tilesCount: 100 })
87 }
88 })
89
90 it('Should generate a storyboard after upload with transcoding', async function () {
91 this.timeout(120000)
92
93 await servers[0].config.enableMinimumTranscoding()
94
95 // 5s video
96 const { uuid } = await servers[0].videos.quickUpload({ name: 'upload', fixture: 'video_short.webm' })
97 await waitJobs(servers)
98
99 for (const server of servers) {
100 await checkStoryboard({ server, uuid, tilesCount: 5 })
101 }
102 })
103
104 it('Should generate a storyboard after an audio upload', async function () {
105 this.timeout(120000)
106
107 // 6s audio
108 const attributes = { name: 'audio', fixture: 'sample.ogg' }
109 const { uuid } = await servers[0].videos.upload({ attributes, mode: 'legacy' })
110 await waitJobs(servers)
111
112 for (const server of servers) {
113 try {
114 await checkStoryboard({ server, uuid, tilesCount: 6, minSize: 250 })
115 } catch { // FIXME: to remove after ffmpeg CI upgrade, ffmpeg CI version (4.3) generates a 7.6s length video
116 await checkStoryboard({ server, uuid, tilesCount: 8, minSize: 250 })
117 }
118 }
119 })
120
121 it('Should generate a storyboard after HTTP import', async function () {
122 this.timeout(120000)
123
124 if (areHttpImportTestsDisabled()) return
125
126 // 3s video
127 const { video } = await servers[0].imports.importVideo({
128 attributes: {
129 targetUrl: FIXTURE_URLS.goodVideo,
130 channelId: servers[0].store.channel.id,
131 privacy: VideoPrivacy.PUBLIC
132 }
133 })
134 await waitJobs(servers)
135
136 for (const server of servers) {
137 await checkStoryboard({ server, uuid: video.uuid, tilesCount: 3 })
138 }
139 })
140
141 it('Should generate a storyboard after torrent import', async function () {
142 this.timeout(120000)
143
144 if (areHttpImportTestsDisabled()) return
145
146 // 10s video
147 const { video } = await servers[0].imports.importVideo({
148 attributes: {
149 magnetUri: FIXTURE_URLS.magnet,
150 channelId: servers[0].store.channel.id,
151 privacy: VideoPrivacy.PUBLIC
152 }
153 })
154 await waitJobs(servers)
155
156 for (const server of servers) {
157 await checkStoryboard({ server, uuid: video.uuid, tilesCount: 10 })
158 }
159 })
160
161 it('Should generate a storyboard after a live', async function () {
162 this.timeout(240000)
163
164 await servers[0].config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' })
165
166 const { live, video } = await servers[0].live.quickCreate({
167 saveReplay: true,
168 permanentLive: false,
169 privacy: VideoPrivacy.PUBLIC
170 })
171
172 const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey })
173 await servers[0].live.waitUntilPublished({ videoId: video.id })
174
175 await stopFfmpeg(ffmpegCommand)
176
177 await servers[0].live.waitUntilReplacedByReplay({ videoId: video.id })
178 await waitJobs(servers)
179
180 for (const server of servers) {
181 await checkStoryboard({ server, uuid: video.uuid })
182 }
183 })
184
185 it('Should cleanup storyboards on video deletion', async function () {
186 this.timeout(60000)
187
188 const { storyboards } = await servers[0].storyboard.list({ id: baseUUID })
189 const storyboardName = basename(storyboards[0].storyboardPath)
190
191 const listFiles = () => {
192 const storyboardPath = servers[0].getDirectoryPath('storyboards')
193 return readdir(storyboardPath)
194 }
195
196 {
197 const storyboads = await listFiles()
198 expect(storyboads).to.include(storyboardName)
199 }
200
201 await servers[0].videos.remove({ id: baseUUID })
202 await waitJobs(servers)
203
204 {
205 const storyboads = await listFiles()
206 expect(storyboads).to.not.include(storyboardName)
207 }
208 })
209
210 after(async function () {
211 await cleanupTests(servers)
212 })
213})
diff --git a/packages/tests/src/api/videos/videos-common-filters.ts b/packages/tests/src/api/videos/videos-common-filters.ts
new file mode 100644
index 000000000..9e75bd6ca
--- /dev/null
+++ b/packages/tests/src/api/videos/videos-common-filters.ts
@@ -0,0 +1,499 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pick } from '@peertube/peertube-core-utils'
5import {
6 HttpStatusCode,
7 HttpStatusCodeType,
8 UserRole,
9 Video,
10 VideoDetails,
11 VideoInclude,
12 VideoIncludeType,
13 VideoPrivacy,
14 VideoPrivacyType
15} from '@peertube/peertube-models'
16import {
17 cleanupTests,
18 createMultipleServers,
19 doubleFollow,
20 makeGetRequest,
21 PeerTubeServer,
22 setAccessTokensToServers,
23 setDefaultAccountAvatar,
24 setDefaultVideoChannel,
25 waitJobs
26} from '@peertube/peertube-server-commands'
27
28describe('Test videos filter', function () {
29 let servers: PeerTubeServer[]
30 let paths: string[]
31 let remotePaths: string[]
32
33 const subscriptionVideosPath = '/api/v1/users/me/subscriptions/videos'
34
35 // ---------------------------------------------------------------
36
37 before(async function () {
38 this.timeout(240000)
39
40 servers = await createMultipleServers(2)
41
42 await setAccessTokensToServers(servers)
43 await setDefaultVideoChannel(servers)
44 await setDefaultAccountAvatar(servers)
45
46 await servers[1].config.enableMinimumTranscoding()
47
48 for (const server of servers) {
49 const moderator = { username: 'moderator', password: 'my super password' }
50 await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR })
51 server['moderatorAccessToken'] = await server.login.getAccessToken(moderator)
52
53 await server.videos.upload({ attributes: { name: 'public ' + server.serverNumber } })
54
55 {
56 const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
57 await server.videos.upload({ attributes })
58 }
59
60 {
61 const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
62 await server.videos.upload({ attributes })
63 }
64
65 // Subscribing to itself
66 await server.subscriptions.add({ targetUri: 'root_channel@' + server.host })
67 }
68
69 await doubleFollow(servers[0], servers[1])
70
71 paths = [
72 `/api/v1/video-channels/root_channel/videos`,
73 `/api/v1/accounts/root/videos`,
74 '/api/v1/videos',
75 '/api/v1/search/videos',
76 subscriptionVideosPath
77 ]
78
79 remotePaths = [
80 `/api/v1/video-channels/root_channel@${servers[1].host}/videos`,
81 `/api/v1/accounts/root@${servers[1].host}/videos`,
82 '/api/v1/videos',
83 '/api/v1/search/videos'
84 ]
85 })
86
87 describe('Check videos filters', function () {
88
89 async function listVideos (options: {
90 server: PeerTubeServer
91 path: string
92 isLocal?: boolean
93 hasWebVideoFiles?: boolean
94 hasHLSFiles?: boolean
95 include?: VideoIncludeType
96 privacyOneOf?: VideoPrivacyType[]
97 category?: number
98 tagsAllOf?: string[]
99 token?: string
100 expectedStatus?: HttpStatusCodeType
101 excludeAlreadyWatched?: boolean
102 }) {
103 const res = await makeGetRequest({
104 url: options.server.url,
105 path: options.path,
106 token: options.token ?? options.server.accessToken,
107 query: {
108 ...pick(options, [
109 'isLocal',
110 'include',
111 'category',
112 'tagsAllOf',
113 'hasWebVideoFiles',
114 'hasHLSFiles',
115 'privacyOneOf',
116 'excludeAlreadyWatched'
117 ]),
118
119 sort: 'createdAt'
120 },
121 expectedStatus: options.expectedStatus ?? HttpStatusCode.OK_200
122 })
123
124 return res.body.data as Video[]
125 }
126
127 async function getVideosNames (
128 options: {
129 server: PeerTubeServer
130 isLocal?: boolean
131 include?: VideoIncludeType
132 privacyOneOf?: VideoPrivacyType[]
133 token?: string
134 expectedStatus?: HttpStatusCodeType
135 skipSubscription?: boolean
136 excludeAlreadyWatched?: boolean
137 }
138 ) {
139 const { skipSubscription = false } = options
140 const videosResults: string[][] = []
141
142 for (const path of paths) {
143 if (skipSubscription && path === subscriptionVideosPath) continue
144
145 const videos = await listVideos({ ...options, path })
146
147 videosResults.push(videos.map(v => v.name))
148 }
149
150 return videosResults
151 }
152
153 it('Should display local videos', async function () {
154 for (const server of servers) {
155 const namesResults = await getVideosNames({ server, isLocal: true })
156
157 for (const names of namesResults) {
158 expect(names).to.have.lengthOf(1)
159 expect(names[0]).to.equal('public ' + server.serverNumber)
160 }
161 }
162 })
163
164 it('Should display local videos with hidden privacy by the admin or the moderator', async function () {
165 for (const server of servers) {
166 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
167
168 const namesResults = await getVideosNames(
169 {
170 server,
171 token,
172 isLocal: true,
173 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ],
174 skipSubscription: true
175 }
176 )
177
178 for (const names of namesResults) {
179 expect(names).to.have.lengthOf(3)
180
181 expect(names[0]).to.equal('public ' + server.serverNumber)
182 expect(names[1]).to.equal('unlisted ' + server.serverNumber)
183 expect(names[2]).to.equal('private ' + server.serverNumber)
184 }
185 }
186 }
187 })
188
189 it('Should display all videos by the admin or the moderator', async function () {
190 for (const server of servers) {
191 for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
192
193 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({
194 server,
195 token,
196 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
197 })
198
199 expect(channelVideos).to.have.lengthOf(3)
200 expect(accountVideos).to.have.lengthOf(3)
201
202 expect(videos).to.have.lengthOf(5)
203 expect(searchVideos).to.have.lengthOf(5)
204 }
205 }
206 })
207
208 it('Should display only remote videos', async function () {
209 this.timeout(120000)
210
211 await servers[1].videos.upload({ attributes: { name: 'remote video' } })
212
213 await waitJobs(servers)
214
215 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
216
217 for (const path of remotePaths) {
218 {
219 const videos = await listVideos({ server: servers[0], path })
220 const video = finder(videos)
221 expect(video).to.exist
222 }
223
224 {
225 const videos = await listVideos({ server: servers[0], path, isLocal: false })
226 const video = finder(videos)
227 expect(video).to.exist
228 }
229
230 {
231 const videos = await listVideos({ server: servers[0], path, isLocal: true })
232 const video = finder(videos)
233 expect(video).to.not.exist
234 }
235 }
236 })
237
238 it('Should include not published videos', async function () {
239 await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
240 await servers[0].live.create({ fields: { name: 'live video', channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } })
241
242 const finder = (videos: Video[]) => videos.find(v => v.name === 'live video')
243
244 for (const path of paths) {
245 {
246 const videos = await listVideos({ server: servers[0], path })
247 const video = finder(videos)
248 expect(video).to.not.exist
249 expect(videos[0].state).to.not.exist
250 expect(videos[0].waitTranscoding).to.not.exist
251 }
252
253 {
254 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.NOT_PUBLISHED_STATE })
255 const video = finder(videos)
256 expect(video).to.exist
257 expect(video.state).to.exist
258 }
259 }
260 })
261
262 it('Should include blacklisted videos', async function () {
263 const { id } = await servers[0].videos.upload({ attributes: { name: 'blacklisted' } })
264
265 await servers[0].blacklist.add({ videoId: id })
266
267 const finder = (videos: Video[]) => videos.find(v => v.name === 'blacklisted')
268
269 for (const path of paths) {
270 {
271 const videos = await listVideos({ server: servers[0], path })
272 const video = finder(videos)
273 expect(video).to.not.exist
274 expect(videos[0].blacklisted).to.not.exist
275 }
276
277 {
278 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLACKLISTED })
279 const video = finder(videos)
280 expect(video).to.exist
281 expect(video.blacklisted).to.be.true
282 }
283 }
284 })
285
286 it('Should include videos from muted account', async function () {
287 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
288
289 await servers[0].blocklist.addToServerBlocklist({ account: 'root@' + servers[1].host })
290
291 for (const path of remotePaths) {
292 {
293 const videos = await listVideos({ server: servers[0], path })
294 const video = finder(videos)
295 expect(video).to.not.exist
296
297 // Some paths won't have videos
298 if (videos[0]) {
299 expect(videos[0].blockedOwner).to.not.exist
300 expect(videos[0].blockedServer).to.not.exist
301 }
302 }
303
304 {
305 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
306
307 const video = finder(videos)
308 expect(video).to.exist
309 expect(video.blockedServer).to.be.false
310 expect(video.blockedOwner).to.be.true
311 }
312 }
313
314 await servers[0].blocklist.removeFromServerBlocklist({ account: 'root@' + servers[1].host })
315 })
316
317 it('Should include videos from muted server', async function () {
318 const finder = (videos: Video[]) => videos.find(v => v.name === 'remote video')
319
320 await servers[0].blocklist.addToServerBlocklist({ server: servers[1].host })
321
322 for (const path of remotePaths) {
323 {
324 const videos = await listVideos({ server: servers[0], path })
325 const video = finder(videos)
326 expect(video).to.not.exist
327
328 // Some paths won't have videos
329 if (videos[0]) {
330 expect(videos[0].blockedOwner).to.not.exist
331 expect(videos[0].blockedServer).to.not.exist
332 }
333 }
334
335 {
336 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.BLOCKED_OWNER })
337 const video = finder(videos)
338 expect(video).to.exist
339 expect(video.blockedServer).to.be.true
340 expect(video.blockedOwner).to.be.false
341 }
342 }
343
344 await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host })
345 })
346
347 it('Should include video files', async function () {
348 for (const path of paths) {
349 {
350 const videos = await listVideos({ server: servers[0], path })
351
352 for (const video of videos) {
353 const videoWithFiles = video as VideoDetails
354
355 expect(videoWithFiles.files).to.not.exist
356 expect(videoWithFiles.streamingPlaylists).to.not.exist
357 }
358 }
359
360 {
361 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.FILES })
362
363 for (const video of videos) {
364 const videoWithFiles = video as VideoDetails
365
366 expect(videoWithFiles.files).to.exist
367 expect(videoWithFiles.files).to.have.length.at.least(1)
368 }
369 }
370 }
371 })
372
373 it('Should filter by tags and category', async function () {
374 await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } })
375 await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } })
376
377 for (const path of paths) {
378 {
379 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] })
380 expect(videos).to.have.lengthOf(1)
381 expect(videos[0].name).to.equal('tag filter')
382 }
383
384 {
385 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag3' ] })
386 expect(videos).to.have.lengthOf(0)
387 }
388
389 {
390 const { data, total } = await servers[0].videos.list({ tagsAllOf: [ 'tag3' ], categoryOneOf: [ 4 ] })
391 expect(total).to.equal(1)
392 expect(data[0].name).to.equal('tag filter with category')
393 }
394
395 {
396 const { total } = await servers[0].videos.list({ tagsAllOf: [ 'tag4' ], categoryOneOf: [ 4 ] })
397 expect(total).to.equal(0)
398 }
399 }
400 })
401
402 it('Should filter by HLS or Web Video files', async function () {
403 this.timeout(360000)
404
405 const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name)
406
407 await servers[0].config.enableTranscoding({ hls: false, webVideo: true })
408 await servers[0].videos.upload({ attributes: { name: 'web video' } })
409 const hasWebVideo = finderFactory('web video')
410
411 await waitJobs(servers)
412
413 await servers[0].config.enableTranscoding({ hls: true, webVideo: false })
414 await servers[0].videos.upload({ attributes: { name: 'hls video' } })
415 const hasHLS = finderFactory('hls video')
416
417 await waitJobs(servers)
418
419 await servers[0].config.enableTranscoding({ hls: true, webVideo: true })
420 await servers[0].videos.upload({ attributes: { name: 'hls and web video' } })
421 const hasBoth = finderFactory('hls and web video')
422
423 await waitJobs(servers)
424
425 for (const path of paths) {
426 {
427 const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: true })
428
429 expect(hasWebVideo(videos)).to.be.true
430 expect(hasHLS(videos)).to.be.false
431 expect(hasBoth(videos)).to.be.true
432 }
433
434 {
435 const videos = await listVideos({ server: servers[0], path, hasWebVideoFiles: false })
436
437 expect(hasWebVideo(videos)).to.be.false
438 expect(hasHLS(videos)).to.be.true
439 expect(hasBoth(videos)).to.be.false
440 }
441
442 {
443 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true })
444
445 expect(hasWebVideo(videos)).to.be.false
446 expect(hasHLS(videos)).to.be.true
447 expect(hasBoth(videos)).to.be.true
448 }
449
450 {
451 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false })
452
453 expect(hasWebVideo(videos)).to.be.true
454 expect(hasHLS(videos)).to.be.false
455 expect(hasBoth(videos)).to.be.false
456 }
457
458 {
459 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebVideoFiles: false })
460
461 expect(hasWebVideo(videos)).to.be.false
462 expect(hasHLS(videos)).to.be.false
463 expect(hasBoth(videos)).to.be.false
464 }
465
466 {
467 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebVideoFiles: true })
468
469 expect(hasWebVideo(videos)).to.be.false
470 expect(hasHLS(videos)).to.be.false
471 expect(hasBoth(videos)).to.be.true
472 }
473 }
474 })
475
476 it('Should filter already watched videos by the user', async function () {
477 const { id } = await servers[0].videos.upload({ attributes: { name: 'video for history' } })
478
479 for (const path of paths) {
480 const videos = await listVideos({ server: servers[0], path, isLocal: true, excludeAlreadyWatched: true })
481 const foundVideo = videos.find(video => video.id === id)
482
483 expect(foundVideo).to.not.be.undefined
484 }
485 await servers[0].views.view({ id, currentTime: 1, token: servers[0].accessToken })
486
487 for (const path of paths) {
488 const videos = await listVideos({ server: servers[0], path, excludeAlreadyWatched: true })
489 const foundVideo = videos.find(video => video.id === id)
490
491 expect(foundVideo).to.be.undefined
492 }
493 })
494 })
495
496 after(async function () {
497 await cleanupTests(servers)
498 })
499})
diff --git a/packages/tests/src/api/videos/videos-history.ts b/packages/tests/src/api/videos/videos-history.ts
new file mode 100644
index 000000000..75c0fcebd
--- /dev/null
+++ b/packages/tests/src/api/videos/videos-history.ts
@@ -0,0 +1,230 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { Video } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 killallServers,
10 PeerTubeServer,
11 setAccessTokensToServers
12} from '@peertube/peertube-server-commands'
13
14describe('Test videos history', function () {
15 let server: PeerTubeServer = null
16 let video1Id: number
17 let video1UUID: string
18 let video2UUID: string
19 let video3UUID: string
20 let video3WatchedDate: Date
21 let userAccessToken: string
22
23 before(async function () {
24 this.timeout(120000)
25
26 server = await createSingleServer(1)
27
28 await setAccessTokensToServers([ server ])
29
30 // 10 seconds long
31 const fixture = 'video_short1.webm'
32
33 {
34 const { id, uuid } = await server.videos.upload({ attributes: { name: 'video 1', fixture } })
35 video1UUID = uuid
36 video1Id = id
37 }
38
39 {
40 const { uuid } = await server.videos.upload({ attributes: { name: 'video 2', fixture } })
41 video2UUID = uuid
42 }
43
44 {
45 const { uuid } = await server.videos.upload({ attributes: { name: 'video 3', fixture } })
46 video3UUID = uuid
47 }
48
49 userAccessToken = await server.users.generateUserAndToken('user_1')
50 })
51
52 it('Should get videos, without watching history', async function () {
53 const { data } = await server.videos.listWithToken()
54
55 for (const video of data) {
56 const videoDetails = await server.videos.getWithToken({ id: video.id })
57
58 expect(video.userHistory).to.be.undefined
59 expect(videoDetails.userHistory).to.be.undefined
60 }
61 })
62
63 it('Should watch the first and second video', async function () {
64 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
65 await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 3 })
66 })
67
68 it('Should return the correct history when listing, searching and getting videos', async function () {
69 const videosOfVideos: Video[][] = []
70
71 {
72 const { data } = await server.videos.listWithToken()
73 videosOfVideos.push(data)
74 }
75
76 {
77 const body = await server.search.searchVideos({ token: server.accessToken, search: 'video' })
78 videosOfVideos.push(body.data)
79 }
80
81 for (const videos of videosOfVideos) {
82 const video1 = videos.find(v => v.uuid === video1UUID)
83 const video2 = videos.find(v => v.uuid === video2UUID)
84 const video3 = videos.find(v => v.uuid === video3UUID)
85
86 expect(video1.userHistory).to.not.be.undefined
87 expect(video1.userHistory.currentTime).to.equal(3)
88
89 expect(video2.userHistory).to.not.be.undefined
90 expect(video2.userHistory.currentTime).to.equal(8)
91
92 expect(video3.userHistory).to.be.undefined
93 }
94
95 {
96 const videoDetails = await server.videos.getWithToken({ id: video1UUID })
97
98 expect(videoDetails.userHistory).to.not.be.undefined
99 expect(videoDetails.userHistory.currentTime).to.equal(3)
100 }
101
102 {
103 const videoDetails = await server.videos.getWithToken({ id: video2UUID })
104
105 expect(videoDetails.userHistory).to.not.be.undefined
106 expect(videoDetails.userHistory.currentTime).to.equal(8)
107 }
108
109 {
110 const videoDetails = await server.videos.getWithToken({ id: video3UUID })
111
112 expect(videoDetails.userHistory).to.be.undefined
113 }
114 })
115
116 it('Should have these videos when listing my history', async function () {
117 video3WatchedDate = new Date()
118 await server.views.view({ id: video3UUID, token: server.accessToken, currentTime: 2 })
119
120 const body = await server.history.list()
121
122 expect(body.total).to.equal(3)
123
124 const videos = body.data
125 expect(videos[0].name).to.equal('video 3')
126 expect(videos[1].name).to.equal('video 1')
127 expect(videos[2].name).to.equal('video 2')
128 })
129
130 it('Should not have videos history on another user', async function () {
131 const body = await server.history.list({ token: userAccessToken })
132
133 expect(body.total).to.equal(0)
134 expect(body.data).to.have.lengthOf(0)
135 })
136
137 it('Should be able to search through videos in my history', async function () {
138 const body = await server.history.list({ search: '2' })
139 expect(body.total).to.equal(1)
140
141 const videos = body.data
142 expect(videos[0].name).to.equal('video 2')
143 })
144
145 it('Should clear my history', async function () {
146 await server.history.removeAll({ beforeDate: video3WatchedDate.toISOString() })
147 })
148
149 it('Should have my history cleared', async function () {
150 const body = await server.history.list()
151 expect(body.total).to.equal(1)
152
153 const videos = body.data
154 expect(videos[0].name).to.equal('video 3')
155 })
156
157 it('Should disable videos history', async function () {
158 await server.users.updateMe({
159 videosHistoryEnabled: false
160 })
161
162 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
163
164 const { data } = await server.history.list()
165 expect(data[0].name).to.not.equal('video 2')
166 })
167
168 it('Should re-enable videos history', async function () {
169 await server.users.updateMe({
170 videosHistoryEnabled: true
171 })
172
173 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
174
175 const { data } = await server.history.list()
176 expect(data[0].name).to.equal('video 2')
177 })
178
179 it('Should not clean old history', async function () {
180 this.timeout(50000)
181
182 await killallServers([ server ])
183
184 await server.run({ history: { videos: { max_age: '10 days' } } })
185
186 await wait(6000)
187
188 // Should still have history
189
190 const body = await server.history.list()
191 expect(body.total).to.equal(2)
192 })
193
194 it('Should clean old history', async function () {
195 this.timeout(50000)
196
197 await killallServers([ server ])
198
199 await server.run({ history: { videos: { max_age: '5 seconds' } } })
200
201 await wait(6000)
202
203 const body = await server.history.list()
204 expect(body.total).to.equal(0)
205 })
206
207 it('Should delete a specific history element', async function () {
208 {
209 await server.views.view({ id: video1UUID, token: server.accessToken, currentTime: 4 })
210 await server.views.view({ id: video2UUID, token: server.accessToken, currentTime: 8 })
211 }
212
213 {
214 const body = await server.history.list()
215 expect(body.total).to.equal(2)
216 }
217
218 {
219 await server.history.removeElement({ videoId: video1Id })
220
221 const body = await server.history.list()
222 expect(body.total).to.equal(1)
223 expect(body.data[0].uuid).to.equal(video2UUID)
224 }
225 })
226
227 after(async function () {
228 await cleanupTests([ server ])
229 })
230})
diff --git a/packages/tests/src/api/videos/videos-overview.ts b/packages/tests/src/api/videos/videos-overview.ts
new file mode 100644
index 000000000..7d74d6db2
--- /dev/null
+++ b/packages/tests/src/api/videos/videos-overview.ts
@@ -0,0 +1,129 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { VideosOverview } from '@peertube/peertube-models'
6import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
7
8describe('Test a videos overview', function () {
9 let server: PeerTubeServer = null
10
11 function testOverviewCount (overview: VideosOverview, expected: number) {
12 expect(overview.tags).to.have.lengthOf(expected)
13 expect(overview.categories).to.have.lengthOf(expected)
14 expect(overview.channels).to.have.lengthOf(expected)
15 }
16
17 before(async function () {
18 this.timeout(30000)
19
20 server = await createSingleServer(1)
21
22 await setAccessTokensToServers([ server ])
23 })
24
25 it('Should send empty overview', async function () {
26 const body = await server.overviews.getVideos({ page: 1 })
27
28 testOverviewCount(body, 0)
29 })
30
31 it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () {
32 this.timeout(60000)
33
34 await wait(3000)
35
36 await server.videos.upload({
37 attributes: {
38 name: 'video 0',
39 category: 3,
40 tags: [ 'coucou1', 'coucou2' ]
41 }
42 })
43
44 const body = await server.overviews.getVideos({ page: 1 })
45
46 testOverviewCount(body, 0)
47 })
48
49 it('Should upload another video and include all videos in the overview', async function () {
50 this.timeout(120000)
51
52 {
53 for (let i = 1; i < 6; i++) {
54 await server.videos.upload({
55 attributes: {
56 name: 'video ' + i,
57 category: 3,
58 tags: [ 'coucou1', 'coucou2' ]
59 }
60 })
61 }
62
63 await wait(3000)
64 }
65
66 {
67 const body = await server.overviews.getVideos({ page: 1 })
68
69 testOverviewCount(body, 1)
70 }
71
72 {
73 const overview = await server.overviews.getVideos({ page: 2 })
74
75 expect(overview.tags).to.have.lengthOf(1)
76 expect(overview.categories).to.have.lengthOf(0)
77 expect(overview.channels).to.have.lengthOf(0)
78 }
79 })
80
81 it('Should have the correct overview', async function () {
82 const overview1 = await server.overviews.getVideos({ page: 1 })
83 const overview2 = await server.overviews.getVideos({ page: 2 })
84
85 for (const arr of [ overview1.tags, overview1.categories, overview1.channels, overview2.tags ]) {
86 expect(arr).to.have.lengthOf(1)
87
88 const obj = arr[0]
89
90 expect(obj.videos).to.have.lengthOf(6)
91 expect(obj.videos[0].name).to.equal('video 5')
92 expect(obj.videos[1].name).to.equal('video 4')
93 expect(obj.videos[2].name).to.equal('video 3')
94 expect(obj.videos[3].name).to.equal('video 2')
95 expect(obj.videos[4].name).to.equal('video 1')
96 expect(obj.videos[5].name).to.equal('video 0')
97 }
98
99 const tags = [ overview1.tags[0].tag, overview2.tags[0].tag ]
100 expect(tags.find(t => t === 'coucou1')).to.not.be.undefined
101 expect(tags.find(t => t === 'coucou2')).to.not.be.undefined
102
103 expect(overview1.categories[0].category.id).to.equal(3)
104
105 expect(overview1.channels[0].channel.name).to.equal('root_channel')
106 })
107
108 it('Should hide muted accounts', async function () {
109 const token = await server.users.generateUserAndToken('choco')
110
111 await server.blocklist.addToMyBlocklist({ token, account: 'root@' + server.host })
112
113 {
114 const body = await server.overviews.getVideos({ page: 1 })
115
116 testOverviewCount(body, 1)
117 }
118
119 {
120 const body = await server.overviews.getVideos({ page: 1, token })
121
122 testOverviewCount(body, 0)
123 }
124 })
125
126 after(async function () {
127 await cleanupTests([ server ])
128 })
129})
diff --git a/packages/tests/src/api/views/index.ts b/packages/tests/src/api/views/index.ts
new file mode 100644
index 000000000..2b7334d1a
--- /dev/null
+++ b/packages/tests/src/api/views/index.ts
@@ -0,0 +1,5 @@
1export * from './video-views-counter.js'
2export * from './video-views-overall-stats.js'
3export * from './video-views-retention-stats.js'
4export * from './video-views-timeserie-stats.js'
5export * from './videos-views-cleaner.js'
diff --git a/packages/tests/src/api/views/video-views-counter.ts b/packages/tests/src/api/views/video-views-counter.ts
new file mode 100644
index 000000000..d9afb0f18
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-counter.ts
@@ -0,0 +1,153 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@tests/shared/views.js'
6import { wait } from '@peertube/peertube-core-utils'
7import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
8
9describe('Test video views/viewers counters', function () {
10 let servers: PeerTubeServer[]
11
12 async function checkCounter (field: 'views' | 'viewers', id: string, expected: number) {
13 for (const server of servers) {
14 const video = await server.videos.get({ id })
15
16 const messageSuffix = video.isLive
17 ? 'live video'
18 : 'vod video'
19
20 expect(video[field]).to.equal(expected, `${field} not valid on server ${server.serverNumber} for ${messageSuffix} ${video.uuid}`)
21 }
22 }
23
24 before(async function () {
25 this.timeout(120000)
26
27 servers = await prepareViewsServers()
28 })
29
30 describe('Test views counter on VOD', function () {
31 let videoUUID: string
32
33 before(async function () {
34 this.timeout(120000)
35
36 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
37 videoUUID = uuid
38
39 await waitJobs(servers)
40 })
41
42 it('Should not view a video if watch time is below the threshold', async function () {
43 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 2 ] })
44 await processViewsBuffer(servers)
45
46 await checkCounter('views', videoUUID, 0)
47 })
48
49 it('Should view a video if watch time is above the threshold', async function () {
50 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
51 await processViewsBuffer(servers)
52
53 await checkCounter('views', videoUUID, 1)
54 })
55
56 it('Should not view again this video with the same IP', async function () {
57 await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] })
58 await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] })
59 await processViewsBuffer(servers)
60
61 await checkCounter('views', videoUUID, 2)
62 })
63
64 it('Should view the video from server 2 and send the event', async function () {
65 await servers[1].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
66 await waitJobs(servers)
67 await processViewsBuffer(servers)
68
69 await checkCounter('views', videoUUID, 3)
70 })
71 })
72
73 describe('Test views and viewers counters on live and VOD', function () {
74 let liveVideoId: string
75 let vodVideoId: string
76 let command: FfmpegCommand
77
78 before(async function () {
79 this.timeout(240000);
80
81 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
82 })
83
84 it('Should display no views and viewers', async function () {
85 await checkCounter('views', liveVideoId, 0)
86 await checkCounter('viewers', liveVideoId, 0)
87
88 await checkCounter('views', vodVideoId, 0)
89 await checkCounter('viewers', vodVideoId, 0)
90 })
91
92 it('Should view twice and display 1 view/viewer', async function () {
93 this.timeout(30000)
94
95 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
96 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
97 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
98 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
99
100 await waitJobs(servers)
101 await checkCounter('viewers', liveVideoId, 1)
102 await checkCounter('viewers', vodVideoId, 1)
103
104 await processViewsBuffer(servers)
105
106 await checkCounter('views', liveVideoId, 1)
107 await checkCounter('views', vodVideoId, 1)
108 })
109
110 it('Should wait and display 0 viewers but still have 1 view', async function () {
111 this.timeout(30000)
112
113 await wait(12000)
114 await waitJobs(servers)
115
116 await checkCounter('views', liveVideoId, 1)
117 await checkCounter('viewers', liveVideoId, 0)
118
119 await checkCounter('views', vodVideoId, 1)
120 await checkCounter('viewers', vodVideoId, 0)
121 })
122
123 it('Should view on a remote and on local and display 2 viewers and 3 views', async function () {
124 this.timeout(30000)
125
126 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
127 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
128 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
129
130 await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
131 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
132 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
133
134 await waitJobs(servers)
135
136 await checkCounter('viewers', liveVideoId, 2)
137 await checkCounter('viewers', vodVideoId, 2)
138
139 await processViewsBuffer(servers)
140
141 await checkCounter('views', liveVideoId, 3)
142 await checkCounter('views', vodVideoId, 3)
143 })
144
145 after(async function () {
146 await stopFfmpeg(command)
147 })
148 })
149
150 after(async function () {
151 await cleanupTests(servers)
152 })
153})
diff --git a/packages/tests/src/api/views/video-views-overall-stats.ts b/packages/tests/src/api/views/video-views-overall-stats.ts
new file mode 100644
index 000000000..6ea0da2d9
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-overall-stats.ts
@@ -0,0 +1,368 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
6import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
7import { wait } from '@peertube/peertube-core-utils'
8import { VideoStatsOverall } from '@peertube/peertube-models'
9
10/**
11 *
12 * Simulate 5 sections of viewers
13 * * user0 started and ended before start date
14 * * user1 started before start date and ended in the interval
15 * * user2 started started in the interval and ended after end date
16 * * user3 started and ended in the interval
17 * * user4 started and ended after end date
18 */
19async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: string) {
20 const user0 = '8.8.8.8,127.0.0.1'
21 const user1 = '8.8.8.8,127.0.0.1'
22 const user2 = '8.8.8.9,127.0.0.1'
23 const user3 = '8.8.8.10,127.0.0.1'
24 const user4 = '8.8.8.11,127.0.0.1'
25
26 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user0 }) // User 0 starts
27 await wait(500)
28
29 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user1 }) // User 1 starts
30 await servers[0].views.view({ id: videoUUID, currentTime: 2, xForwardedFor: user0 }) // User 0 ends
31 await wait(500)
32
33 const startDate = new Date().toISOString()
34 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user2 }) // User 2 starts
35 await wait(500)
36
37 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user3 }) // User 3 starts
38 await wait(500)
39
40 await servers[0].views.view({ id: videoUUID, currentTime: 4, xForwardedFor: user1 }) // User 1 ends
41 await wait(500)
42
43 await servers[0].views.view({ id: videoUUID, currentTime: 3, xForwardedFor: user3 }) // User 3 ends
44 await wait(500)
45
46 const endDate = new Date().toISOString()
47 await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user4 }) // User 4 starts
48 await servers[0].views.view({ id: videoUUID, currentTime: 5, xForwardedFor: user2 }) // User 2 ends
49 await wait(500)
50
51 await servers[0].views.view({ id: videoUUID, currentTime: 1, xForwardedFor: user4 }) // User 4 ends
52
53 await processViewersStats(servers)
54
55 return { startDate, endDate }
56}
57
58describe('Test views overall stats', function () {
59 let servers: PeerTubeServer[]
60
61 before(async function () {
62 this.timeout(120000)
63
64 servers = await prepareViewsServers()
65 })
66
67 describe('Test watch time stats of local videos on live and VOD', function () {
68 let vodVideoId: string
69 let liveVideoId: string
70 let command: FfmpegCommand
71
72 before(async function () {
73 this.timeout(240000);
74
75 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
76 })
77
78 it('Should display overall stats of a video with no viewers', async function () {
79 for (const videoId of [ liveVideoId, vodVideoId ]) {
80 const stats = await servers[0].videoStats.getOverallStats({ videoId })
81 const video = await servers[0].videos.get({ id: videoId })
82
83 expect(video.views).to.equal(0)
84 expect(stats.averageWatchTime).to.equal(0)
85 expect(stats.totalWatchTime).to.equal(0)
86 expect(stats.totalViewers).to.equal(0)
87 }
88 })
89
90 it('Should display overall stats with 1 viewer below the watch time limit', async function () {
91 this.timeout(60000)
92
93 for (const videoId of [ liveVideoId, vodVideoId ]) {
94 await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
95 }
96
97 await processViewersStats(servers)
98
99 for (const videoId of [ liveVideoId, vodVideoId ]) {
100 const stats = await servers[0].videoStats.getOverallStats({ videoId })
101 const video = await servers[0].videos.get({ id: videoId })
102
103 expect(video.views).to.equal(0)
104 expect(stats.averageWatchTime).to.equal(1)
105 expect(stats.totalWatchTime).to.equal(1)
106 expect(stats.totalViewers).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 const video = await servers[0].videos.get({ id: vodVideoId })
122
123 expect(video.views).to.equal(1)
124 expect(stats.averageWatchTime).to.equal(2)
125 expect(stats.totalWatchTime).to.equal(4)
126 expect(stats.totalViewers).to.equal(2)
127 }
128
129 {
130 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
131 const video = await servers[0].videos.get({ id: liveVideoId })
132
133 expect(video.views).to.equal(1)
134 expect(stats.averageWatchTime).to.equal(21)
135 expect(stats.totalWatchTime).to.equal(41)
136 expect(stats.totalViewers).to.equal(2)
137 }
138 }
139 })
140
141 it('Should display overall stats with a remote viewer below the watch time limit', async function () {
142 this.timeout(60000)
143
144 for (const videoId of [ liveVideoId, vodVideoId ]) {
145 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 2 ] })
146 }
147
148 await processViewersStats(servers)
149
150 {
151 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
152 const video = await servers[0].videos.get({ id: vodVideoId })
153
154 expect(video.views).to.equal(1)
155 expect(stats.averageWatchTime).to.equal(2)
156 expect(stats.totalWatchTime).to.equal(6)
157 expect(stats.totalViewers).to.equal(3)
158 }
159
160 {
161 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
162 const video = await servers[0].videos.get({ id: liveVideoId })
163
164 expect(video.views).to.equal(1)
165 expect(stats.averageWatchTime).to.equal(14)
166 expect(stats.totalWatchTime).to.equal(43)
167 expect(stats.totalViewers).to.equal(3)
168 }
169 })
170
171 it('Should display overall stats with a remote viewer above the watch time limit', async function () {
172 this.timeout(60000)
173
174 await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
175 await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 45 ] })
176 await processViewersStats(servers)
177
178 {
179 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
180 const video = await servers[0].videos.get({ id: vodVideoId })
181
182 expect(video.views).to.equal(2)
183 expect(stats.averageWatchTime).to.equal(3)
184 expect(stats.totalWatchTime).to.equal(11)
185 expect(stats.totalViewers).to.equal(4)
186 }
187
188 {
189 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
190 const video = await servers[0].videos.get({ id: liveVideoId })
191
192 expect(video.views).to.equal(2)
193 expect(stats.averageWatchTime).to.equal(22)
194 expect(stats.totalWatchTime).to.equal(88)
195 expect(stats.totalViewers).to.equal(4)
196 }
197 })
198
199 it('Should filter overall stats by date', async function () {
200 this.timeout(60000)
201
202 const beforeView = new Date()
203
204 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
205 await processViewersStats(servers)
206
207 {
208 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId, startDate: beforeView.toISOString() })
209 expect(stats.averageWatchTime).to.equal(3)
210 expect(stats.totalWatchTime).to.equal(3)
211 expect(stats.totalViewers).to.equal(1)
212 }
213
214 {
215 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId, endDate: beforeView.toISOString() })
216 expect(stats.averageWatchTime).to.equal(22)
217 expect(stats.totalWatchTime).to.equal(88)
218 expect(stats.totalViewers).to.equal(4)
219 }
220 })
221
222 after(async function () {
223 await stopFfmpeg(command)
224 })
225 })
226
227 describe('Test watchers peak stats of local videos on VOD', function () {
228 let videoUUID: string
229 let before2Watchers: Date
230
231 before(async function () {
232 this.timeout(240000);
233
234 ({ vodVideoId: videoUUID } = await prepareViewsVideos({ servers, live: true, vod: true }))
235 })
236
237 it('Should not have watchers peak', async function () {
238 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
239
240 expect(stats.viewersPeak).to.equal(0)
241 expect(stats.viewersPeakDate).to.be.null
242 })
243
244 it('Should have watcher peak with 1 watcher', async function () {
245 this.timeout(60000)
246
247 const before = new Date()
248 await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 0, 2 ] })
249 const after = new Date()
250
251 await processViewersStats(servers)
252
253 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
254
255 expect(stats.viewersPeak).to.equal(1)
256 expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after)
257 })
258
259 it('Should have watcher peak with 2 watchers', async function () {
260 this.timeout(60000)
261
262 before2Watchers = new Date()
263 await servers[0].views.view({ id: videoUUID, currentTime: 0 })
264 await servers[1].views.view({ id: videoUUID, currentTime: 0 })
265 await servers[0].views.view({ id: videoUUID, currentTime: 2 })
266 await servers[1].views.view({ id: videoUUID, currentTime: 2 })
267 const after = new Date()
268
269 await processViewersStats(servers)
270
271 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
272
273 expect(stats.viewersPeak).to.equal(2)
274 expect(new Date(stats.viewersPeakDate)).to.be.above(before2Watchers).and.below(after)
275 })
276
277 it('Should filter peak viewers stats by date', async function () {
278 {
279 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
280 expect(stats.viewersPeak).to.equal(0)
281 expect(stats.viewersPeakDate).to.not.exist
282 }
283
284 {
285 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate: before2Watchers.toISOString() })
286 expect(stats.viewersPeak).to.equal(1)
287 expect(new Date(stats.viewersPeakDate)).to.be.below(before2Watchers)
288 }
289 })
290
291 it('Should complex filter peak viewers by date', async function () {
292 this.timeout(60000)
293
294 const { startDate, endDate } = await simulateComplexViewers(servers, videoUUID)
295
296 const expectCorrect = (stats: VideoStatsOverall) => {
297 expect(stats.viewersPeak).to.equal(3)
298 expect(new Date(stats.viewersPeakDate)).to.be.above(new Date(startDate)).and.below(new Date(endDate))
299 }
300
301 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate, endDate }))
302 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate }))
303 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate }))
304 expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID }))
305 })
306 })
307
308 describe('Test countries', function () {
309 let videoUUID: string
310
311 it('Should not report countries if geoip is disabled', async function () {
312 this.timeout(120000)
313
314 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
315 await waitJobs(servers)
316
317 await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
318
319 await processViewersStats(servers)
320
321 const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
322 expect(stats.countries).to.have.lengthOf(0)
323 })
324
325 it('Should report countries if geoip is enabled', async function () {
326 this.timeout(240000)
327
328 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
329 videoUUID = uuid
330 await waitJobs(servers)
331
332 await Promise.all([
333 servers[0].kill(),
334 servers[1].kill()
335 ])
336
337 const config = { geo_ip: { enabled: true } }
338 await Promise.all([
339 servers[0].run(config),
340 servers[1].run(config)
341 ])
342
343 await servers[0].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
344 await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.4,127.0.0.1', currentTime: 3 })
345 await servers[1].views.view({ id: uuid, xForwardedFor: '80.67.169.12,127.0.0.1', currentTime: 2 })
346
347 await processViewersStats(servers)
348
349 const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
350 expect(stats.countries).to.have.lengthOf(2)
351
352 expect(stats.countries[0].isoCode).to.equal('US')
353 expect(stats.countries[0].viewers).to.equal(2)
354
355 expect(stats.countries[1].isoCode).to.equal('FR')
356 expect(stats.countries[1].viewers).to.equal(1)
357 })
358
359 it('Should filter countries stats by date', async function () {
360 const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
361 expect(stats.countries).to.have.lengthOf(0)
362 })
363 })
364
365 after(async function () {
366 await cleanupTests(servers)
367 })
368})
diff --git a/packages/tests/src/api/views/video-views-retention-stats.ts b/packages/tests/src/api/views/video-views-retention-stats.ts
new file mode 100644
index 000000000..4cd0c7da9
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-retention-stats.ts
@@ -0,0 +1,53 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
5import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
6
7describe('Test views retention stats', function () {
8 let servers: PeerTubeServer[]
9
10 before(async function () {
11 this.timeout(120000)
12
13 servers = await prepareViewsServers()
14 })
15
16 describe('Test retention stats on VOD', function () {
17 let vodVideoId: string
18
19 before(async function () {
20 this.timeout(240000);
21
22 ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
23 })
24
25 it('Should display empty retention', async function () {
26 const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId })
27 expect(data).to.have.lengthOf(6)
28
29 for (let i = 0; i < 6; i++) {
30 expect(data[i].second).to.equal(i)
31 expect(data[i].retentionPercent).to.equal(0)
32 }
33 })
34
35 it('Should display appropriate retention metrics', async function () {
36 await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
37 await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 3 ] })
38 await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 4 ] })
39 await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
40
41 await processViewersStats(servers)
42
43 const { data } = await servers[0].videoStats.getRetentionStats({ videoId: vodVideoId })
44 expect(data).to.have.lengthOf(6)
45
46 expect(data.map(d => d.retentionPercent)).to.deep.equal([ 50, 75, 25, 25, 25, 0 ])
47 })
48 })
49
50 after(async function () {
51 await cleanupTests(servers)
52 })
53})
diff --git a/packages/tests/src/api/views/video-views-timeserie-stats.ts b/packages/tests/src/api/views/video-views-timeserie-stats.ts
new file mode 100644
index 000000000..44fccb644
--- /dev/null
+++ b/packages/tests/src/api/views/video-views-timeserie-stats.ts
@@ -0,0 +1,253 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { FfmpegCommand } from 'fluent-ffmpeg'
5import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
6import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models'
7import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@peertube/peertube-server-commands'
8
9function buildOneMonthAgo () {
10 const monthAgo = new Date()
11 monthAgo.setHours(0, 0, 0, 0)
12
13 monthAgo.setDate(monthAgo.getDate() - 29)
14
15 return monthAgo
16}
17
18describe('Test views timeserie stats', function () {
19 const availableMetrics: VideoStatsTimeserieMetric[] = [ 'viewers' ]
20
21 let servers: PeerTubeServer[]
22
23 before(async function () {
24 this.timeout(120000)
25
26 servers = await prepareViewsServers()
27 })
28
29 describe('Common metric tests', function () {
30 let vodVideoId: string
31
32 before(async function () {
33 this.timeout(240000);
34
35 ({ vodVideoId } = await prepareViewsVideos({ servers, live: false, vod: true }))
36 })
37
38 it('Should display empty metric stats', async function () {
39 for (const metric of availableMetrics) {
40 const { data } = await servers[0].videoStats.getTimeserieStats({ videoId: vodVideoId, metric })
41
42 expect(data).to.have.length.at.least(1)
43
44 for (const d of data) {
45 expect(d.value).to.equal(0)
46 }
47 }
48 })
49 })
50
51 describe('Test viewer and watch time metrics on live and VOD', function () {
52 let vodVideoId: string
53 let liveVideoId: string
54 let command: FfmpegCommand
55
56 function expectTodayLastValue (result: VideoStatsTimeserie, lastValue?: number) {
57 const { data } = result
58
59 const last = data[data.length - 1]
60 const today = new Date().getDate()
61 expect(new Date(last.date).getDate()).to.equal(today)
62
63 if (lastValue) expect(last.value).to.equal(lastValue)
64 }
65
66 function expectTimeserieData (result: VideoStatsTimeserie, lastValue: number) {
67 const { data } = result
68 expect(data).to.have.length.at.least(25)
69
70 expectTodayLastValue(result, lastValue)
71
72 for (let i = 0; i < data.length - 2; i++) {
73 expect(data[i].value).to.equal(0)
74 }
75 }
76
77 function expectInterval (result: VideoStatsTimeserie, intervalMs: number) {
78 const first = result.data[0]
79 const second = result.data[1]
80 expect(new Date(second.date).getTime() - new Date(first.date).getTime()).to.equal(intervalMs)
81 }
82
83 before(async function () {
84 this.timeout(240000);
85
86 ({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
87 })
88
89 it('Should display appropriate viewers metrics', async function () {
90 for (const videoId of [ vodVideoId, liveVideoId ]) {
91 await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 3 ] })
92 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 5 ] })
93 }
94
95 await processViewersStats(servers)
96
97 for (const videoId of [ vodVideoId, liveVideoId ]) {
98 const result = await servers[0].videoStats.getTimeserieStats({
99 videoId,
100 startDate: buildOneMonthAgo(),
101 endDate: new Date(),
102 metric: 'viewers'
103 })
104 expectTimeserieData(result, 2)
105 }
106 })
107
108 it('Should display appropriate watch time metrics', async function () {
109 for (const videoId of [ vodVideoId, liveVideoId ]) {
110 const result = await servers[0].videoStats.getTimeserieStats({
111 videoId,
112 startDate: buildOneMonthAgo(),
113 endDate: new Date(),
114 metric: 'aggregateWatchTime'
115 })
116 expectTimeserieData(result, 8)
117
118 await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
119 }
120
121 await processViewersStats(servers)
122
123 for (const videoId of [ vodVideoId, liveVideoId ]) {
124 const result = await servers[0].videoStats.getTimeserieStats({
125 videoId,
126 startDate: buildOneMonthAgo(),
127 endDate: new Date(),
128 metric: 'aggregateWatchTime'
129 })
130 expectTimeserieData(result, 9)
131 }
132 })
133
134 it('Should use a custom start/end date', async function () {
135 const now = new Date()
136 const twentyDaysAgo = new Date()
137 twentyDaysAgo.setDate(twentyDaysAgo.getDate() - 19)
138
139 const result = await servers[0].videoStats.getTimeserieStats({
140 videoId: vodVideoId,
141 metric: 'aggregateWatchTime',
142 startDate: twentyDaysAgo,
143 endDate: now
144 })
145
146 expect(result.groupInterval).to.equal('1 day')
147 expect(result.data).to.have.lengthOf(20)
148
149 const first = result.data[0]
150 expect(new Date(first.date).toLocaleDateString()).to.equal(twentyDaysAgo.toLocaleDateString())
151
152 expectInterval(result, 24 * 3600 * 1000)
153 expectTodayLastValue(result, 9)
154 })
155
156 it('Should automatically group by months', async function () {
157 const now = new Date()
158 const heightYearsAgo = new Date()
159 heightYearsAgo.setFullYear(heightYearsAgo.getFullYear() - 7)
160
161 const result = await servers[0].videoStats.getTimeserieStats({
162 videoId: vodVideoId,
163 metric: 'aggregateWatchTime',
164 startDate: heightYearsAgo,
165 endDate: now
166 })
167
168 expect(result.groupInterval).to.equal('6 months')
169 expect(result.data).to.have.length.above(10).and.below(200)
170 })
171
172 it('Should automatically group by days', async function () {
173 const now = new Date()
174 const threeMonthsAgo = new Date()
175 threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3)
176
177 const result = await servers[0].videoStats.getTimeserieStats({
178 videoId: vodVideoId,
179 metric: 'aggregateWatchTime',
180 startDate: threeMonthsAgo,
181 endDate: now
182 })
183
184 expect(result.groupInterval).to.equal('2 days')
185 expect(result.data).to.have.length.above(10).and.below(200)
186 })
187
188 it('Should automatically group by hours', async function () {
189 const now = new Date()
190 const twoDaysAgo = new Date()
191 twoDaysAgo.setDate(twoDaysAgo.getDate() - 1)
192
193 const result = await servers[0].videoStats.getTimeserieStats({
194 videoId: vodVideoId,
195 metric: 'aggregateWatchTime',
196 startDate: twoDaysAgo,
197 endDate: now
198 })
199
200 expect(result.groupInterval).to.equal('1 hour')
201 expect(result.data).to.have.length.above(24).and.below(50)
202
203 expectInterval(result, 3600 * 1000)
204 expectTodayLastValue(result, 9)
205 })
206
207 it('Should automatically group by ten minutes', async function () {
208 const now = new Date()
209 const twoHoursAgo = new Date()
210 twoHoursAgo.setHours(twoHoursAgo.getHours() - 4)
211
212 const result = await servers[0].videoStats.getTimeserieStats({
213 videoId: vodVideoId,
214 metric: 'aggregateWatchTime',
215 startDate: twoHoursAgo,
216 endDate: now
217 })
218
219 expect(result.groupInterval).to.equal('10 minutes')
220 expect(result.data).to.have.length.above(20).and.below(30)
221
222 expectInterval(result, 60 * 10 * 1000)
223 expectTodayLastValue(result)
224 })
225
226 it('Should automatically group by one minute', async function () {
227 const now = new Date()
228 const thirtyAgo = new Date()
229 thirtyAgo.setMinutes(thirtyAgo.getMinutes() - 30)
230
231 const result = await servers[0].videoStats.getTimeserieStats({
232 videoId: vodVideoId,
233 metric: 'aggregateWatchTime',
234 startDate: thirtyAgo,
235 endDate: now
236 })
237
238 expect(result.groupInterval).to.equal('1 minute')
239 expect(result.data).to.have.length.above(20).and.below(40)
240
241 expectInterval(result, 60 * 1000)
242 expectTodayLastValue(result)
243 })
244
245 after(async function () {
246 await stopFfmpeg(command)
247 })
248 })
249
250 after(async function () {
251 await cleanupTests(servers)
252 })
253})
diff --git a/packages/tests/src/api/views/videos-views-cleaner.ts b/packages/tests/src/api/views/videos-views-cleaner.ts
new file mode 100644
index 000000000..521dd9b5e
--- /dev/null
+++ b/packages/tests/src/api/views/videos-views-cleaner.ts
@@ -0,0 +1,98 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { SQLCommand } from '@tests/shared/sql-command.js'
5import { wait } from '@peertube/peertube-core-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 killallServers,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15
16describe('Test video views cleaner', function () {
17 let servers: PeerTubeServer[]
18 let sqlCommands: SQLCommand[] = []
19
20 let videoIdServer1: string
21 let videoIdServer2: string
22
23 before(async function () {
24 this.timeout(240000)
25
26 servers = await createMultipleServers(2)
27 await setAccessTokensToServers(servers)
28
29 await doubleFollow(servers[0], servers[1])
30
31 videoIdServer1 = (await servers[0].videos.quickUpload({ name: 'video server 1' })).uuid
32 videoIdServer2 = (await servers[1].videos.quickUpload({ name: 'video server 2' })).uuid
33
34 await waitJobs(servers)
35
36 await servers[0].views.simulateView({ id: videoIdServer1 })
37 await servers[1].views.simulateView({ id: videoIdServer1 })
38 await servers[0].views.simulateView({ id: videoIdServer2 })
39 await servers[1].views.simulateView({ id: videoIdServer2 })
40
41 await waitJobs(servers)
42
43 sqlCommands = servers.map(s => new SQLCommand(s))
44 })
45
46 it('Should not clean old video views', async function () {
47 this.timeout(50000)
48
49 await killallServers([ servers[0] ])
50
51 await servers[0].run({ views: { videos: { remote: { max_age: '10 days' } } } })
52
53 await wait(6000)
54
55 // Should still have views
56
57 for (let i = 0; i < servers.length; i++) {
58 const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
59 expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
60 }
61
62 for (let i = 0; i < servers.length; i++) {
63 const total = await sqlCommands[i].countVideoViewsOf(videoIdServer2)
64 expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
65 }
66 })
67
68 it('Should clean old video views', async function () {
69 this.timeout(50000)
70
71 await killallServers([ servers[0] ])
72
73 await servers[0].run({ views: { videos: { remote: { max_age: '5 seconds' } } } })
74
75 await wait(6000)
76
77 // Should still have views
78
79 for (let i = 0; i < servers.length; i++) {
80 const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
81 expect(total).to.equal(2)
82 }
83
84 const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2)
85 expect(totalServer1).to.equal(0)
86
87 const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2)
88 expect(totalServer2).to.equal(2)
89 })
90
91 after(async function () {
92 for (const sqlCommand of sqlCommands) {
93 await sqlCommand.cleanup()
94 }
95
96 await cleanupTests(servers)
97 })
98})