diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/tests/src/api | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-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')
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 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { processViewersStats } from '@tests/shared/views.js' | ||
5 | import { HttpStatusCode, VideoPlaylistPrivacy, WatchActionObject } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | makeActivityPubGetRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 @@ | |||
1 | import './cleaner.js' | ||
2 | import './client.js' | ||
3 | import './fetch.js' | ||
4 | import './refresher.js' | ||
5 | import './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 | |||
3 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
6 | import { PeerTubeServer, cleanupTests, createMultipleServers, killallServers } from '@peertube/peertube-server-commands' | ||
7 | import { | ||
8 | activityPubContextify, | ||
9 | buildGlobalHTTPHeaders, | ||
10 | signAndContextify | ||
11 | } from '@peertube/peertube-server/server/helpers/activity-pub-utils.js' | ||
12 | import { buildDigest } from '@peertube/peertube-server/server/helpers/peertube-crypto.js' | ||
13 | import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@peertube/peertube-server/server/initializers/constants.js' | ||
14 | import { makePOSTAPRequest } from '@tests/shared/requests.js' | ||
15 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
16 | import { expect } from 'chai' | ||
17 | import { readJsonSync } from 'fs-extra/esm' | ||
18 | |||
19 | function fakeFilter () { | ||
20 | return (data: any) => Promise.resolve(data) | ||
21 | } | ||
22 | |||
23 | function 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 | |||
32 | function 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 | |||
41 | function 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 | |||
56 | async 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 | |||
82 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { AbuseCreate, AbuseState, HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
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 | |||
17 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
6 | |||
7 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeDeleteRequest, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makePostBodyRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | describe('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 | |||
3 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
4 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | ChannelsCommand, | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 */ | ||
2 | import merge from 'lodash-es/merge.js' | ||
3 | import { omit } from '@peertube/peertube-core-utils' | ||
4 | import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makePutBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | ConfigCommand, | ||
8 | ContactFormCommand, | ||
9 | createSingleServer, | ||
10 | killallServers, | ||
11 | PeerTubeServer | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeGetRequest, | ||
8 | makePutBodyRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makeGetRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makePostBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 @@ | |||
1 | import './abuses.js' | ||
2 | import './accounts.js' | ||
3 | import './blocklist.js' | ||
4 | import './bulk.js' | ||
5 | import './channel-import-videos.js' | ||
6 | import './config.js' | ||
7 | import './contact-form.js' | ||
8 | import './custom-pages.js' | ||
9 | import './debug.js' | ||
10 | import './follows.js' | ||
11 | import './jobs.js' | ||
12 | import './live.js' | ||
13 | import './logs.js' | ||
14 | import './metrics.js' | ||
15 | import './my-user.js' | ||
16 | import './plugins.js' | ||
17 | import './redundancy.js' | ||
18 | import './registrations.js' | ||
19 | import './runners.js' | ||
20 | import './search.js' | ||
21 | import './services.js' | ||
22 | import './transcoding.js' | ||
23 | import './two-factor.js' | ||
24 | import './upload-quota.js' | ||
25 | import './user-notifications.js' | ||
26 | import './user-subscriptions.js' | ||
27 | import './users-admin.js' | ||
28 | import './users-emails.js' | ||
29 | import './video-blacklist.js' | ||
30 | import './video-captions.js' | ||
31 | import './video-channel-syncs.js' | ||
32 | import './video-channels.js' | ||
33 | import './video-comments.js' | ||
34 | import './video-files.js' | ||
35 | import './video-imports.js' | ||
36 | import './video-playlists.js' | ||
37 | import './video-storyboards.js' | ||
38 | import './video-source.js' | ||
39 | import './video-studio.js' | ||
40 | import './video-token.js' | ||
41 | import './videos-common-filters.js' | ||
42 | import './videos-history.js' | ||
43 | import './videos-overviews.js' | ||
44 | import './videos.js' | ||
45 | import './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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | makePostBodyRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { omit } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
7 | import { | ||
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 | |||
19 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { omit } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode, PlaybackMetricCreate, VideoResolution } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makePostBodyRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
6 | import { HttpStatusCode, UserRole, VideoCreateResult } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | makeGetRequest, | ||
11 | makePutBodyRequest, | ||
12 | makeUploadRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | UsersCommand | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode, PeerTubePlugin, PluginType } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | makePostBodyRequest, | ||
10 | makePutBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode, VideoCreateResult } from '@peertube/peertube-models' | ||
5 | import { | ||
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 | |||
18 | describe('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 @@ | |||
1 | import { omit } from '@peertube/peertube-core-utils' | ||
2 | import { HttpStatusCode, HttpStatusCodeType, UserRole } from '@peertube/peertube-models' | ||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | makePostBodyRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 */ | ||
2 | import { basename } from 'path' | ||
3 | import { | ||
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' | ||
15 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
16 | import { | ||
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 | |||
29 | const badUUID = '910ec12a-d9e6-458b-a274-0abb655f9464' | ||
30 | |||
31 | describe('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 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeGetRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | function 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 | |||
26 | describe('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 | |||
3 | import { | ||
4 | HttpStatusCode, | ||
5 | HttpStatusCodeType, | ||
6 | VideoCreateResult, | ||
7 | VideoPlaylistCreateResult, | ||
8 | VideoPlaylistPrivacy, | ||
9 | VideoPrivacy | ||
10 | } from '@peertube/peertube-models' | ||
11 | import { | ||
12 | cleanupTests, | ||
13 | createSingleServer, | ||
14 | makeGetRequest, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | |||
20 | describe('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 | |||
193 | function 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 | |||
3 | import { HttpStatusCode, UserRole } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | waitJobs | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers, | ||
9 | TwoFactorCommand | ||
10 | } from '@peertube/peertube-server-commands' | ||
11 | |||
12 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
5 | import { randomInt } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, VideoImportState, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | VideosCommand, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { io } from 'socket.io-client' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | makePutBodyRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { | ||
4 | cleanupTests, | ||
5 | createSingleServer, | ||
6 | makeDeleteRequest, | ||
7 | makeGetRequest, | ||
8 | makePostBodyRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
14 | import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js' | ||
15 | |||
16 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { omit } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, UserAdminFlag, UserRole } from '@peertube/peertube-models' | ||
7 | import { | ||
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 | |||
19 | describe('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 */ | ||
2 | import { HttpStatusCode, UserRole } from '@peertube/peertube-models' | ||
3 | import { | ||
4 | cleanupTests, | ||
5 | createSingleServer, | ||
6 | makePostBodyRequest, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers | ||
9 | } from '@peertube/peertube-server-commands' | ||
10 | |||
11 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { HttpStatusCode, VideoBlacklistType } from '@peertube/peertube-models' | ||
6 | import { | ||
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 | |||
18 | describe('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 | |||
3 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
4 | import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makeUploadRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 @@ | |||
1 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
2 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
3 | import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | ChannelSyncsCommand, | ||
6 | createSingleServer, | ||
7 | makePostBodyRequest, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { omit } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models' | ||
6 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
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 | |||
20 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
5 | import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | makeDeleteRequest, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { getAllFiles } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode, UserRole, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeRawRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { omit } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
6 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
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 | |||
20 | describe('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 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { | ||
3 | HttpStatusCode, | ||
4 | HttpStatusCodeType, | ||
5 | PeerTubeProblemDocument, | ||
6 | ServerErrorCode, | ||
7 | VideoCreateResult, | ||
8 | VideoPrivacy | ||
9 | } from '@peertube/peertube-models' | ||
10 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
11 | import { | ||
12 | cleanupTests, | ||
13 | createSingleServer, | ||
14 | makePostBodyRequest, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
19 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
20 | import { checkUploadVideoParam } from '@tests/shared/videos.js' | ||
21 | |||
22 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { | ||
5 | HttpStatusCode, | ||
6 | VideoPlaylistCreate, | ||
7 | VideoPlaylistCreateResult, | ||
8 | VideoPlaylistElementCreate, | ||
9 | VideoPlaylistElementUpdate, | ||
10 | VideoPlaylistPrivacy, | ||
11 | VideoPlaylistReorder, | ||
12 | VideoPlaylistType | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { | ||
15 | cleanupTests, | ||
16 | createSingleServer, | ||
17 | makeGetRequest, | ||
18 | PeerTubeServer, | ||
19 | PlaylistsCommand, | ||
20 | setAccessTokensToServers, | ||
21 | setDefaultVideoChannel | ||
22 | } from '@peertube/peertube-server-commands' | ||
23 | |||
24 | describe('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 @@ | |||
1 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
2 | import { | ||
3 | cleanupTests, | ||
4 | createSingleServer, | ||
5 | PeerTubeServer, | ||
6 | setAccessTokensToServers, | ||
7 | setDefaultVideoChannel, | ||
8 | waitJobs | ||
9 | } from '@peertube/peertube-server-commands' | ||
10 | |||
11 | describe('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 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
5 | |||
6 | describe('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 | |||
3 | import { HttpStatusCode, HttpStatusCodeType, VideoStudioTask } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | PeerTubeServer, | ||
8 | setAccessTokensToServers, | ||
9 | VideoStudioCommand, | ||
10 | waitJobs | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
5 | |||
6 | describe('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 | |||
3 | import { | ||
4 | HttpStatusCode, | ||
5 | HttpStatusCodeType, | ||
6 | UserRole, | ||
7 | VideoInclude, | ||
8 | VideoIncludeType, | ||
9 | VideoPrivacy, | ||
10 | VideoPrivacyType | ||
11 | } from '@peertube/peertube-models' | ||
12 | import { | ||
13 | cleanupTests, | ||
14 | createSingleServer, | ||
15 | makeGetRequest, | ||
16 | PeerTubeServer, | ||
17 | setAccessTokensToServers, | ||
18 | setDefaultVideoChannel | ||
19 | } from '@peertube/peertube-server-commands' | ||
20 | |||
21 | describe('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 | |||
3 | import { checkBadCountPagination, checkBadStartPagination } from '@tests/shared/checks.js' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeDeleteRequest, | ||
9 | makeGetRequest, | ||
10 | makePostBodyRequest, | ||
11 | makePutBodyRequest, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
4 | |||
5 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { join } from 'path' | ||
5 | import { omit, randomInt } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, PeerTubeProblemDocument, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | makeDeleteRequest, | ||
12 | makeGetRequest, | ||
13 | makePutBodyRequest, | ||
14 | makeUploadRequest, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js' | ||
19 | import { checkUploadVideoParam } from '@tests/shared/videos.js' | ||
20 | |||
21 | describe('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 | |||
3 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 @@ | |||
1 | import './live-constraints.js' | ||
2 | import './live-fast-restream.js' | ||
3 | import './live-socket-messages.js' | ||
4 | import './live-permanent.js' | ||
5 | import './live-rtmps.js' | ||
6 | import './live-save-replay.js' | ||
7 | import './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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoError, UserVideoQuota, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
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' | ||
19 | import { checkLiveCleanup } from '../../shared/live.js' | ||
20 | |||
21 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultVideoChannel, | ||
12 | stopFfmpeg, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoCreate, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models' | ||
6 | import { checkLiveCleanup } from '@tests/shared/live.js' | ||
7 | import { | ||
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 | |||
19 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
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 | |||
18 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | HttpStatusCode, | ||
8 | HttpStatusCodeType, | ||
9 | LiveVideoCreate, | ||
10 | LiveVideoError, | ||
11 | VideoPrivacy, | ||
12 | VideoPrivacyType, | ||
13 | VideoState, | ||
14 | VideoStateType | ||
15 | } from '@peertube/peertube-models' | ||
16 | import { checkLiveCleanup } from '@tests/shared/live.js' | ||
17 | import { | ||
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 | |||
34 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { LiveVideoEventPayload, VideoPrivacy, VideoState, VideoStateType } from '@peertube/peertube-models' | ||
6 | import { | ||
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 | |||
18 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename, join } from 'path' | ||
5 | import { getAllFiles, wait } from '@peertube/peertube-core-utils' | ||
6 | import { ffprobePromise, getVideoStream } from '@peertube/peertube-ffmpeg' | ||
7 | import { | ||
8 | HttpStatusCode, | ||
9 | LiveVideo, | ||
10 | LiveVideoCreate, | ||
11 | LiveVideoLatencyMode, | ||
12 | VideoDetails, | ||
13 | VideoPrivacy, | ||
14 | VideoState, | ||
15 | VideoStreamingPlaylistType | ||
16 | } from '@peertube/peertube-models' | ||
17 | import { | ||
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' | ||
34 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
35 | import { testLiveVideoResolutions } from '@tests/shared/live.js' | ||
36 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
37 | |||
38 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { AbuseMessage, AbusePredefinedReasonsString, AbuseState, AdminAbuse, UserAbuse } from '@peertube/peertube-models' | ||
5 | import { | ||
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 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotificationType, UserNotificationType_Type } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | async 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 | |||
23 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotificationType } from '@peertube/peertube-models' | ||
5 | import { | ||
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 | |||
17 | async 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 | |||
29 | async 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 | |||
41 | async 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 | |||
63 | describe('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 @@ | |||
1 | export * from './abuses.js' | ||
2 | export * from './blocklist-notification.js' | ||
3 | export * from './blocklist.js' | ||
4 | export * 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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
5 | import { sortObjectComparator } from '@peertube/peertube-core-utils' | ||
6 | import { UserAdminFlag, UserRole, VideoBlacklist, VideoBlacklistType } from '@peertube/peertube-models' | ||
7 | import { | ||
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 | |||
19 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
7 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
8 | import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js' | ||
9 | import { CheckerBaseParams, prepareNotificationsTest, checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications.js' | ||
10 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
11 | |||
12 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotification } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
6 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
7 | import { prepareNotificationsTest, CheckerBaseParams, checkNewCommentOnMyVideo, checkCommentMention } from '@tests/shared/notifications.js' | ||
8 | |||
9 | describe('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 @@ | |||
1 | import './admin-notifications.js' | ||
2 | import './comments-notifications.js' | ||
3 | import './moderation-notifications.js' | ||
4 | import './notifications-api.js' | ||
5 | import './registrations-notifications.js' | ||
6 | import './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 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { AbuseState, CustomConfig, UserNotification, UserRole, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
6 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
7 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
8 | import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js' | ||
9 | import { | ||
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 | |||
25 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { UserNotification, UserNotificationSettingValue } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
6 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
7 | import { | ||
8 | prepareNotificationsTest, | ||
9 | CheckerBaseParams, | ||
10 | getAllNotificationsSettings, | ||
11 | checkNewVideoFromSubscription | ||
12 | } from '@tests/shared/notifications.js' | ||
13 | |||
14 | describe('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 | |||
3 | import { UserNotification } from '@peertube/peertube-models' | ||
4 | import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' | ||
5 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
6 | import { CheckerBaseParams, prepareNotificationsTest, checkUserRegistered, checkRegistrationRequest } from '@tests/shared/notifications.js' | ||
7 | |||
8 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { UserNotification, UserNotificationType, VideoPrivacy, VideoStudioTask } from '@peertube/peertube-models' | ||
6 | import { buildUUID } from '@peertube/peertube-node-utils' | ||
7 | import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
8 | import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' | ||
9 | import { | ||
10 | prepareNotificationsTest, | ||
11 | CheckerBaseParams, | ||
12 | checkNewVideoFromSubscription, | ||
13 | checkVideoIsPublished, | ||
14 | checkVideoStudioEditionIsFinished, | ||
15 | checkMyVideoImportIsFinished, | ||
16 | checkNewActorFollow | ||
17 | } from '@tests/shared/notifications.js' | ||
18 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
19 | import { uploadRandomVideoOnServers } from '@tests/shared/videos.js' | ||
20 | |||
21 | describe('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 @@ | |||
1 | export * from './live.js' | ||
2 | export * from './video-imports.js' | ||
3 | export * from './video-static-file-privacy.js' | ||
4 | export * 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 | |||
3 | import { expect } from 'chai' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { HttpStatusCode, LiveVideoCreate, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
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' | ||
22 | import { expectStartWith } from '@tests/shared/checks.js' | ||
23 | import { testLiveVideoResolutions } from '@tests/shared/live.js' | ||
24 | import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js' | ||
25 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
26 | |||
27 | async 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 | |||
42 | async 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 | |||
67 | async 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 | |||
95 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { expectStartWith } from '@tests/shared/checks.js' | ||
5 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
6 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
7 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | makeRawRequest, | ||
12 | ObjectStorageCommand, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | waitJobs | ||
17 | } from '@peertube/peertube-server-commands' | ||
18 | |||
19 | async 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 | |||
32 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename } from 'path' | ||
5 | import { getAllFiles, getHLS } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { areScalewayObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
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' | ||
21 | import { expectStartWith } from '@tests/shared/checks.js' | ||
22 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
23 | import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js' | ||
24 | |||
25 | function extractFilenameFromUrl (url: string) { | ||
26 | const parts = basename(url).split(':') | ||
27 | |||
28 | return parts[parts.length - 1] | ||
29 | } | ||
30 | |||
31 | describe('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 | |||
3 | import bytes from 'bytes' | ||
4 | import { expect } from 'chai' | ||
5 | import { stat } from 'fs/promises' | ||
6 | import merge from 'lodash-es/merge.js' | ||
7 | import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models' | ||
8 | import { areMockObjectStorageTestsDisabled, sha1 } from '@peertube/peertube-node-utils' | ||
9 | import { | ||
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' | ||
21 | import { expectStartWith, expectLogDoesNotContain } from '@tests/shared/checks.js' | ||
22 | import { checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
23 | import { generateHighBitrateVideo } from '@tests/shared/generate.js' | ||
24 | import { MockObjectStorageProxy } from '@tests/shared/mock-servers/mock-object-storage.js' | ||
25 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
26 | import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js' | ||
27 | |||
28 | async 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 | |||
125 | function 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 | |||
285 | describe('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 @@ | |||
1 | import './redundancy-constraints.js' | ||
2 | import './redundancy.js' | ||
3 | import './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 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | RedundancyCommand, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { VideoPrivacy, VideoRedundanciesTarget } from '@peertube/peertube-models' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | killallServers, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir } from 'fs/promises' | ||
5 | import { decode as magnetUriDecode } from 'magnet-uri' | ||
6 | import { basename, join } from 'path' | ||
7 | import { wait } from '@peertube/peertube-core-utils' | ||
8 | import { | ||
9 | HttpStatusCode, | ||
10 | VideoDetails, | ||
11 | VideoFile, | ||
12 | VideoPrivacy, | ||
13 | VideoRedundancyStrategy, | ||
14 | VideoRedundancyStrategyWithManual | ||
15 | } from '@peertube/peertube-models' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | killallServers, | ||
21 | makeRawRequest, | ||
22 | PeerTubeServer, | ||
23 | setAccessTokensToServers, | ||
24 | waitJobs | ||
25 | } from '@peertube/peertube-server-commands' | ||
26 | import { checkSegmentHash } from '@tests/shared/streaming-playlists.js' | ||
27 | import { checkVideoFilesWereRemoved, saveVideoInServers } from '@tests/shared/videos.js' | ||
28 | |||
29 | let servers: PeerTubeServer[] = [] | ||
30 | let video1Server2: VideoDetails | ||
31 | |||
32 | async 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 | |||
47 | async 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 | |||
103 | async 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 | |||
125 | async 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 | |||
144 | async 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 | |||
176 | async 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 | |||
191 | async 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 | |||
231 | async 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 | |||
250 | async 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 | |||
258 | async 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 | |||
266 | async 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 | |||
275 | async 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 | |||
287 | async 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 | |||
299 | describe('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 @@ | |||
1 | export * from './runner-common.js' | ||
2 | export * from './runner-live-transcoding.js' | ||
3 | export * from './runner-socket.js' | ||
4 | export * from './runner-studio-transcoding.js' | ||
5 | export * 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 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { | ||
5 | HttpStatusCode, | ||
6 | Runner, | ||
7 | RunnerJob, | ||
8 | RunnerJobAdmin, | ||
9 | RunnerJobState, | ||
10 | RunnerJobStateType, | ||
11 | RunnerJobVODWebVideoTranscodingPayload, | ||
12 | RunnerRegistrationToken | ||
13 | } from '@peertube/peertube-models' | ||
14 | import { | ||
15 | PeerTubeServer, | ||
16 | cleanupTests, | ||
17 | createSingleServer, | ||
18 | setAccessTokensToServers, | ||
19 | setDefaultVideoChannel, | ||
20 | waitJobs | ||
21 | } from '@peertube/peertube-server-commands' | ||
22 | import { expect } from 'chai' | ||
23 | |||
24 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { readFile } from 'fs/promises' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { | ||
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' | ||
19 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
20 | import { | ||
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 | |||
33 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { readFile } from 'fs/promises' | ||
5 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | RunnerJobStudioTranscodingPayload, | ||
8 | VideoStudioTranscodingSuccess, | ||
9 | VideoState, | ||
10 | VideoStudioTask, | ||
11 | VideoStudioTaskIntro | ||
12 | } from '@peertube/peertube-models' | ||
13 | import { | ||
14 | cleanupTests, | ||
15 | createMultipleServers, | ||
16 | doubleFollow, | ||
17 | PeerTubeServer, | ||
18 | setAccessTokensToServers, | ||
19 | setDefaultVideoChannel, | ||
20 | VideoStudioCommand, | ||
21 | waitJobs | ||
22 | } from '@peertube/peertube-server-commands' | ||
23 | import { checkVideoDuration } from '@tests/shared/checks.js' | ||
24 | import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js' | ||
25 | |||
26 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { readFile } from 'fs/promises' | ||
5 | import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' | ||
6 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
7 | import { | ||
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' | ||
19 | import { | ||
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 | |||
31 | async 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 | |||
48 | describe('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 @@ | |||
1 | import './search-activitypub-video-playlists.js' | ||
2 | import './search-activitypub-video-channels.js' | ||
3 | import './search-activitypub-videos.js' | ||
4 | import './search-channels.js' | ||
5 | import './search-index.js' | ||
6 | import './search-playlists.js' | ||
7 | import './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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoChannel } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoChannel } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | BooleanBothQuery, | ||
6 | VideoChannelsSearchQuery, | ||
7 | VideoPlaylistPrivacy, | ||
8 | VideoPlaylistsSearchQuery, | ||
9 | VideoPlaylistType, | ||
10 | VideosSearchQuery | ||
11 | } from '@peertube/peertube-models' | ||
12 | import { | ||
13 | cleanupTests, | ||
14 | createSingleServer, | ||
15 | PeerTubeServer, | ||
16 | SearchCommand, | ||
17 | setAccessTokensToServers | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | |||
20 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
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 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
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 | |||
19 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockInstancesIndex } from '@tests/shared/mock-servers/index.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | async 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 | |||
26 | async function server1Follows2 (servers: PeerTubeServer[]) { | ||
27 | await servers[0].follows.follow({ hosts: [ servers[1].host ] }) | ||
28 | |||
29 | await waitJobs(servers) | ||
30 | } | ||
31 | |||
32 | async 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 | |||
45 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | BulkCommand, | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { parallelTests } from '@peertube/peertube-node-utils' | ||
5 | import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | killallServers, | ||
10 | makeGetRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | function 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 | |||
128 | function 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 | |||
241 | const 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 | |||
465 | describe('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 | |||
490 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | ConfigCommand, | ||
10 | ContactFormCommand, | ||
11 | createSingleServer, | ||
12 | PeerTubeServer, | ||
13 | setAccessTokensToServers, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, PeerTubeProblemDocument, ServerErrorCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { expectStartWith } from '@tests/shared/checks.js' | ||
5 | import { ActorFollow, FollowState } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | FollowsCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | async 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 | |||
32 | async 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 | |||
72 | async 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 | |||
84 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { Video, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands' | ||
6 | import { expectAccountFollows, expectChannelsFollows } from '@tests/shared/actors.js' | ||
7 | import { testCaptionFile } from '@tests/shared/captions.js' | ||
8 | import { dateIsValid } from '@tests/shared/checks.js' | ||
9 | import { completeVideoCheck } from '@tests/shared/videos.js' | ||
10 | |||
11 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, JobState, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | CommentsCommand, | ||
9 | createMultipleServers, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
16 | import { completeVideoCheck } from '@tests/shared/videos.js' | ||
17 | |||
18 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | CustomPagesCommand, | ||
9 | killallServers, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | async function getHomepageState (server: PeerTubeServer) { | ||
17 | const config = await server.config.getConfig() | ||
18 | |||
19 | return config.homepage.enabled | ||
20 | } | ||
21 | |||
22 | describe('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 @@ | |||
1 | import './auto-follows.js' | ||
2 | import './bulk.js' | ||
3 | import './config-defaults.js' | ||
4 | import './config.js' | ||
5 | import './contact-form.js' | ||
6 | import './email.js' | ||
7 | import './follow-constraints.js' | ||
8 | import './follows.js' | ||
9 | import './follows-moderation.js' | ||
10 | import './homepage.js' | ||
11 | import './handle-down.js' | ||
12 | import './jobs.js' | ||
13 | import './logs.js' | ||
14 | import './reverse-proxy.js' | ||
15 | import './services.js' | ||
16 | import './slow-follows.js' | ||
17 | import './stats.js' | ||
18 | import './tracker.js' | ||
19 | import './no-client.js' | ||
20 | import './open-telemetry.js' | ||
21 | import './plugins.js' | ||
22 | import './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 | |||
3 | import { expect } from 'chai' | ||
4 | import { dateIsValid } from '@tests/shared/checks.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | killallServers, | ||
9 | LogsCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 @@ | |||
1 | import request from 'supertest' | ||
2 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
3 | import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
4 | |||
5 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, PlaybackMetricCreate, VideoPrivacy, VideoResolution } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | makeRawRequest, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | import { expectLogDoesNotContain, expectLogContain } from '@tests/shared/checks.js' | ||
13 | import { MockHTTP } from '@tests/shared/mock-servers/mock-http.js' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists, remove } from 'fs-extra/esm' | ||
5 | import { join } from 'path' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { HttpStatusCode, PluginType } from '@peertube/peertube-models' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | killallServers, | ||
12 | makeGetRequest, | ||
13 | PeerTubeServer, | ||
14 | PluginsCommand, | ||
15 | setAccessTokensToServers | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
18 | import { testHelloWorldRegisteredSettings } from '@tests/shared/plugins.js' | ||
19 | |||
20 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultVideoChannel, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
17 | import { expectStartWith, expectNotStartWith } from '@tests/shared/checks.js' | ||
18 | import { MockProxy } from '@tests/shared/mock-servers/mock-proxy.js' | ||
19 | |||
20 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { Video, VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultVideoChannel | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { Job } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { ActivityType, VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | |||
17 | describe('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 | |||
3 | import { decode as magnetUriDecode, encode as magnetUriEncode } from 'magnet-uri' | ||
4 | import WebTorrent from 'webtorrent' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | killallServers, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { getAudioStream, getVideoStreamDimensionsInfo } from '@peertube/peertube-ffmpeg' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { HttpStatusCode, VideoDetails } from '@peertube/peertube-models' | ||
6 | import { | ||
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' | ||
18 | import { expectStartWith } from '@tests/shared/checks.js' | ||
19 | import { checkResolutionsInMasterPlaylist } from '@tests/shared/streaming-playlists.js' | ||
20 | |||
21 | async 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 | |||
42 | function 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 | |||
256 | describe('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 | |||
3 | import { join } from 'path' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { DEFAULT_AUDIO_RESOLUTION } from '@peertube/peertube-server/server/initializers/constants.js' | ||
16 | import { checkDirectoryIsEmpty, checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
17 | import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' | ||
18 | |||
19 | describe('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 @@ | |||
1 | export * from './audio-only.js' | ||
2 | export * from './create-transcoding.js' | ||
3 | export * from './hls.js' | ||
4 | export * from './transcoder.js' | ||
5 | export * from './update-while-transcoding.js' | ||
6 | export * 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 | |||
3 | import { expect } from 'chai' | ||
4 | import { getAllFiles, getMaxTheoreticalBitrate, getMinTheoreticalBitrate, omit } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoFileMetadata, VideoState } from '@peertube/peertube-models' | ||
6 | import { canDoQuickTranscode } from '@peertube/peertube-server/server/lib/transcoding/transcoding-quick-transcode.js' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | ffprobePromise, | ||
10 | getAudioStream, | ||
11 | getVideoStreamBitrate, | ||
12 | getVideoStreamDimensionsInfo, | ||
13 | getVideoStreamFPS, | ||
14 | hasAudioStream | ||
15 | } from '@peertube/peertube-ffmpeg' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | makeGetRequest, | ||
21 | PeerTubeServer, | ||
22 | setAccessTokensToServers, | ||
23 | waitJobs | ||
24 | } from '@peertube/peertube-server-commands' | ||
25 | import { generateVideoWithFramerate, generateHighBitrateVideo } from '@tests/shared/generate.js' | ||
26 | import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js' | ||
27 | |||
28 | function 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 | |||
53 | describe('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 | |||
3 | import { wait } from '@peertube/peertube-core-utils' | ||
4 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | ObjectStorageCommand, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' | ||
16 | |||
17 | describe('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 @@ | |||
1 | import { expect } from 'chai' | ||
2 | import { getAllFiles } from '@peertube/peertube-core-utils' | ||
3 | import { VideoStudioTask } from '@peertube/peertube-models' | ||
4 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | ObjectStorageCommand, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | VideoStudioCommand, | ||
14 | waitJobs | ||
15 | } from '@peertube/peertube-server-commands' | ||
16 | import { checkVideoDuration, expectStartWith } from '@tests/shared/checks.js' | ||
17 | import { checkPersistentTmpIsEmpty } from '@tests/shared/directories.js' | ||
18 | |||
19 | describe('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 @@ | |||
1 | import './oauth.js' | ||
2 | import './registrations`.js' | ||
3 | import './two-factor.js' | ||
4 | import './user-subscriptions.js' | ||
5 | import './user-videos.js' | ||
6 | import './users.js' | ||
7 | import './users-multiple-servers.js' | ||
8 | import './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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@peertube/peertube-models' | ||
6 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
7 | import { | ||
8 | cleanupTests, | ||
9 | createSingleServer, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { UserRegistrationState, UserRole } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode, HttpStatusCodeType } from '@peertube/peertube-models' | ||
5 | import { expectStartWith } from '@tests/shared/checks.js' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | TwoFactorCommand | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | async 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 | |||
29 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
5 | import { | ||
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 | |||
17 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' | ||
5 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | ConfigCommand, | ||
9 | createSingleServer, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { MyUser } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultChannelAvatar, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | import { checkActorFilesWereRemoved } from '@tests/shared/actors.js' | ||
15 | import { testImage } from '@tests/shared/checks.js' | ||
16 | import { checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
17 | import { saveVideoInServers, checkVideoFilesWereRemoved } from '@tests/shared/videos.js' | ||
18 | |||
19 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { testImageSize } from '@tests/shared/checks.js' | ||
5 | import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
5 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
6 | import { | ||
7 | createSingleServer, | ||
8 | getServerImportConfig, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultVideoChannel, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 @@ | |||
1 | import './multiple-servers.js' | ||
2 | import './resumable-upload.js' | ||
3 | import './single-server.js' | ||
4 | import './video-captions.js' | ||
5 | import './video-change-ownership.js' | ||
6 | import './video-channels.js' | ||
7 | import './channel-import-videos.js' | ||
8 | import './video-channel-syncs.js' | ||
9 | import './video-comments.js' | ||
10 | import './video-description.js' | ||
11 | import './video-files.js' | ||
12 | import './video-imports.js' | ||
13 | import './video-nsfw.js' | ||
14 | import './video-playlists.js' | ||
15 | import './video-playlist-thumbnails.js' | ||
16 | import './video-source.js' | ||
17 | import './video-privacy.js' | ||
18 | import './video-schedule-update.js' | ||
19 | import './videos-common-filters.js' | ||
20 | import './videos-history.js' | ||
21 | import './videos-overview.js' | ||
22 | import './video-static-file-privacy.js' | ||
23 | import './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 | |||
3 | import { expect } from 'chai' | ||
4 | import request from 'supertest' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, VideoCommentThreadTree, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createMultipleServers, | ||
11 | doubleFollow, | ||
12 | makeGetRequest, | ||
13 | PeerTubeServer, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultAccountAvatar, | ||
16 | setDefaultChannelAvatar, | ||
17 | waitJobs | ||
18 | } from '@peertube/peertube-server-commands' | ||
19 | import { testImageGeneratedByFFmpeg, dateIsValid } from '@tests/shared/checks.js' | ||
20 | import { checkTmpIsEmpty } from '@tests/shared/directories.js' | ||
21 | import { completeVideoCheck, saveVideoInServers, checkVideoFilesWereRemoved } from '@tests/shared/videos.js' | ||
22 | import { checkWebTorrentWorks } from '@tests/shared/webtorrent.js' | ||
23 | |||
24 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists } from 'fs-extra/esm' | ||
5 | import { readdir, stat } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models' | ||
8 | import { buildAbsoluteFixturePath, sha1 } from '@peertube/peertube-node-utils' | ||
9 | import { | ||
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 | |||
19 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { Video, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { checkVideoFilesWereRemoved, completeVideoCheck } from '@tests/shared/videos.js' | ||
7 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
8 | import { | ||
9 | cleanupTests, | ||
10 | createSingleServer, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | setDefaultAccountAvatar, | ||
14 | setDefaultChannelAvatar, | ||
15 | waitJobs | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | |||
18 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | waitJobs | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { testCaptionFile } from '@tests/shared/captions.js' | ||
14 | import { checkVideoFilesWereRemoved } from '@tests/shared/videos.js' | ||
15 | |||
16 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | ChangeOwnershipCommand, | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | createSingleServer, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
16 | |||
17 | describe('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 | |||
237 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
5 | import { VideoChannelSyncState, VideoInclude, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | getServerImportConfig, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultAccountAvatar, | ||
13 | setDefaultChannelAvatar, | ||
14 | setDefaultVideoChannel, | ||
15 | waitJobs | ||
16 | } from '@peertube/peertube-server-commands' | ||
17 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
18 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
19 | |||
20 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { basename } from 'path' | ||
5 | import { ACTOR_IMAGES_SIZE } from '@peertube/peertube-server/server/initializers/constants.js' | ||
6 | import { testFileExistsOrNot, testImage } from '@tests/shared/checks.js' | ||
7 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
8 | import { wait } from '@peertube/peertube-core-utils' | ||
9 | import { ActorImageType, User, VideoChannel } from '@peertube/peertube-models' | ||
10 | import { | ||
11 | cleanupTests, | ||
12 | createMultipleServers, | ||
13 | doubleFollow, | ||
14 | PeerTubeServer, | ||
15 | setAccessTokensToServers, | ||
16 | setDefaultAccountAvatar, | ||
17 | setDefaultVideoChannel, | ||
18 | waitJobs | ||
19 | } from '@peertube/peertube-server-commands' | ||
20 | |||
21 | async 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 | |||
27 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { dateIsValid, testImage } from '@tests/shared/checks.js' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | CommentsCommand, | ||
8 | createSingleServer, | ||
9 | PeerTubeServer, | ||
10 | setAccessTokensToServers, | ||
11 | setDefaultAccountAvatar, | ||
12 | setDefaultChannelAvatar | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createMultipleServers, | ||
7 | doubleFollow, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | waitJobs | ||
11 | } from '@peertube/peertube-server-commands' | ||
12 | |||
13 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { | ||
6 | cleanupTests, | ||
7 | createMultipleServers, | ||
8 | doubleFollow, | ||
9 | makeRawRequest, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { pathExists, remove } from 'fs-extra/esm' | ||
5 | import { readdir } from 'fs/promises' | ||
6 | import { join } from 'path' | ||
7 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
8 | import { CustomConfig, HttpStatusCode, Video, VideoImportState, VideoPrivacy, VideoResolution, VideoState } from '@peertube/peertube-models' | ||
9 | import { | ||
10 | cleanupTests, | ||
11 | createMultipleServers, | ||
12 | createSingleServer, | ||
13 | doubleFollow, | ||
14 | getServerImportConfig, | ||
15 | PeerTubeServer, | ||
16 | setAccessTokensToServers, | ||
17 | setDefaultVideoChannel, | ||
18 | waitJobs | ||
19 | } from '@peertube/peertube-server-commands' | ||
20 | import { DeepPartial } from '@peertube/peertube-typescript-utils' | ||
21 | import { testCaptionFile } from '@tests/shared/captions.js' | ||
22 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
23 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
24 | |||
25 | async 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 | |||
62 | async 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 | |||
81 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
5 | import { BooleanBothQuery, CustomConfig, ResultList, Video, VideosOverview } from '@peertube/peertube-models' | ||
6 | |||
7 | function createOverviewRes (overview: VideosOverview) { | ||
8 | const videos = overview.categories[0].videos | ||
9 | return { data: videos, total: videos.length } | ||
10 | } | ||
11 | |||
12 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | VideoPasswordsCommand, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
5 | import { VideoPlaylistPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | setDefaultVideoChannel, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | HttpStatusCode, | ||
7 | VideoPlaylist, | ||
8 | VideoPlaylistCreateResult, | ||
9 | VideoPlaylistElementType, | ||
10 | VideoPlaylistElementType_Type, | ||
11 | VideoPlaylistPrivacy, | ||
12 | VideoPlaylistType, | ||
13 | VideoPrivacy | ||
14 | } from '@peertube/peertube-models' | ||
15 | import { uuidToShort } from '@peertube/peertube-node-utils' | ||
16 | import { | ||
17 | cleanupTests, | ||
18 | createMultipleServers, | ||
19 | doubleFollow, | ||
20 | PeerTubeServer, | ||
21 | PlaylistsCommand, | ||
22 | setAccessTokensToServers, | ||
23 | setDefaultAccountAvatar, | ||
24 | setDefaultVideoChannel, | ||
25 | waitJobs | ||
26 | } from '@peertube/peertube-server-commands' | ||
27 | import { testImageGeneratedByFFmpeg } from '@tests/shared/checks.js' | ||
28 | import { checkPlaylistFilesWereRemoved } from '@tests/shared/video-playlists.js' | ||
29 | |||
30 | async 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 | |||
54 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideoPrivacy } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers, | ||
12 | waitJobs | ||
13 | } from '@peertube/peertube-server-commands' | ||
14 | |||
15 | function in10Seconds () { | ||
16 | const now = new Date() | ||
17 | now.setSeconds(now.getSeconds() + 10) | ||
18 | |||
19 | return now | ||
20 | } | ||
21 | |||
22 | describe('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 */ | ||
2 | import { expect } from 'chai' | ||
3 | import { getAllFiles } from '@peertube/peertube-core-utils' | ||
4 | import { HttpStatusCode } from '@peertube/peertube-models' | ||
5 | import { expectStartWith } from '@tests/shared/checks.js' | ||
6 | import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' | ||
7 | import { | ||
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 | |||
21 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { decode } from 'magnet-uri' | ||
5 | import { getAllFiles, wait } from '@peertube/peertube-core-utils' | ||
6 | import { HttpStatusCode, HttpStatusCodeType, LiveVideo, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' | ||
7 | import { | ||
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' | ||
19 | import { expectStartWith } from '@tests/shared/checks.js' | ||
20 | import { checkVideoFileTokenReinjection } from '@tests/shared/streaming-playlists.js' | ||
21 | import { parseTorrentVideo } from '@tests/shared/webtorrent.js' | ||
22 | |||
23 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { readdir } from 'fs/promises' | ||
5 | import { basename } from 'path' | ||
6 | import { FIXTURE_URLS } from '@tests/shared/tests.js' | ||
7 | import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' | ||
8 | import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' | ||
9 | import { | ||
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 | |||
22 | async 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 | |||
50 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { pick } from '@peertube/peertube-core-utils' | ||
5 | import { | ||
6 | HttpStatusCode, | ||
7 | HttpStatusCodeType, | ||
8 | UserRole, | ||
9 | Video, | ||
10 | VideoDetails, | ||
11 | VideoInclude, | ||
12 | VideoIncludeType, | ||
13 | VideoPrivacy, | ||
14 | VideoPrivacyType | ||
15 | } from '@peertube/peertube-models' | ||
16 | import { | ||
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 | |||
28 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { Video } from '@peertube/peertube-models' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | killallServers, | ||
10 | PeerTubeServer, | ||
11 | setAccessTokensToServers | ||
12 | } from '@peertube/peertube-server-commands' | ||
13 | |||
14 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@peertube/peertube-core-utils' | ||
5 | import { VideosOverview } from '@peertube/peertube-models' | ||
6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' | ||
7 | |||
8 | describe('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 @@ | |||
1 | export * from './video-views-counter.js' | ||
2 | export * from './video-views-overall-stats.js' | ||
3 | export * from './video-views-retention-stats.js' | ||
4 | export * from './video-views-timeserie-stats.js' | ||
5 | export * 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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@tests/shared/views.js' | ||
6 | import { wait } from '@peertube/peertube-core-utils' | ||
7 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
6 | import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' | ||
7 | import { wait } from '@peertube/peertube-core-utils' | ||
8 | import { 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 | */ | ||
19 | async 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 | |||
58 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
5 | import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands' | ||
6 | |||
7 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
5 | import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js' | ||
6 | import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models' | ||
7 | import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@peertube/peertube-server-commands' | ||
8 | |||
9 | function 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 | |||
18 | describe('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 | |||
3 | import { expect } from 'chai' | ||
4 | import { SQLCommand } from '@tests/shared/sql-command.js' | ||
5 | import { wait } from '@peertube/peertube-core-utils' | ||
6 | import { | ||
7 | cleanupTests, | ||
8 | createMultipleServers, | ||
9 | doubleFollow, | ||
10 | killallServers, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers, | ||
13 | waitJobs | ||
14 | } from '@peertube/peertube-server-commands' | ||
15 | |||
16 | describe('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 | }) | ||