diff options
Diffstat (limited to 'packages/tests/src/api/videos')
25 files changed, 9049 insertions, 0 deletions
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..6c874d713 --- /dev/null +++ b/packages/tests/src/api/videos/video-imports.ts | |||
@@ -0,0 +1,635 @@ | |||
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 | // FIXME: official instance is broken | ||
619 | // it('Should update youtube-dl from raw URL', async function () { | ||
620 | // this.timeout(120_000) | ||
621 | |||
622 | // await testBinaryUpdate('https://yt-dl.org/downloads/latest/youtube-dl', 'youtube-dl') | ||
623 | // }) | ||
624 | |||
625 | it('Should update youtube-dl from youtube-dl fork', async function () { | ||
626 | this.timeout(120_000) | ||
627 | |||
628 | await testBinaryUpdate('https://api.github.com/repos/yt-dlp/yt-dlp/releases', 'yt-dlp') | ||
629 | }) | ||
630 | |||
631 | after(async function () { | ||
632 | await cleanupTests([ server ]) | ||
633 | }) | ||
634 | }) | ||
635 | }) | ||
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 | }) | ||