aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/tests/src/plugins
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/tests/src/plugins
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'packages/tests/src/plugins')
-rw-r--r--packages/tests/src/plugins/action-hooks.ts298
-rw-r--r--packages/tests/src/plugins/external-auth.ts436
-rw-r--r--packages/tests/src/plugins/filter-hooks.ts909
-rw-r--r--packages/tests/src/plugins/html-injection.ts73
-rw-r--r--packages/tests/src/plugins/id-and-pass-auth.ts248
-rw-r--r--packages/tests/src/plugins/index.ts13
-rw-r--r--packages/tests/src/plugins/plugin-helpers.ts383
-rw-r--r--packages/tests/src/plugins/plugin-router.ts105
-rw-r--r--packages/tests/src/plugins/plugin-storage.ts95
-rw-r--r--packages/tests/src/plugins/plugin-transcoding.ts279
-rw-r--r--packages/tests/src/plugins/plugin-unloading.ts75
-rw-r--r--packages/tests/src/plugins/plugin-websocket.ts76
-rw-r--r--packages/tests/src/plugins/translations.ts80
-rw-r--r--packages/tests/src/plugins/video-constants.ts180
14 files changed, 3250 insertions, 0 deletions
diff --git a/packages/tests/src/plugins/action-hooks.ts b/packages/tests/src/plugins/action-hooks.ts
new file mode 100644
index 000000000..136c7671b
--- /dev/null
+++ b/packages/tests/src/plugins/action-hooks.ts
@@ -0,0 +1,298 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
4import {
5 cleanupTests,
6 createMultipleServers,
7 doubleFollow,
8 killallServers,
9 PeerTubeServer,
10 PluginsCommand,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 stopFfmpeg,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test plugin action hooks', function () {
18 let servers: PeerTubeServer[]
19 let videoUUID: string
20 let threadId: number
21
22 function checkHook (hook: ServerHookName, strictCount = true, count = 1) {
23 return servers[0].servers.waitUntilLog('Run hook ' + hook, count, strictCount)
24 }
25
26 before(async function () {
27 this.timeout(120000)
28
29 servers = await createMultipleServers(2)
30 await setAccessTokensToServers(servers)
31 await setDefaultVideoChannel(servers)
32
33 await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() })
34
35 await killallServers([ servers[0] ])
36
37 await servers[0].run({
38 live: {
39 enabled: true
40 }
41 })
42
43 await servers[0].config.enableFileUpdate()
44
45 await doubleFollow(servers[0], servers[1])
46 })
47
48 describe('Application hooks', function () {
49 it('Should run action:application.listening', async function () {
50 await checkHook('action:application.listening')
51 })
52 })
53
54 describe('Videos hooks', function () {
55
56 it('Should run action:api.video.uploaded', async function () {
57 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } })
58 videoUUID = uuid
59
60 await checkHook('action:api.video.uploaded')
61 })
62
63 it('Should run action:api.video.updated', async function () {
64 await servers[0].videos.update({ id: videoUUID, attributes: { name: 'video updated' } })
65
66 await checkHook('action:api.video.updated')
67 })
68
69 it('Should run action:api.video.viewed', async function () {
70 await servers[0].views.simulateView({ id: videoUUID })
71
72 await checkHook('action:api.video.viewed')
73 })
74
75 it('Should run action:api.video.file-updated', async function () {
76 await servers[0].videos.replaceSourceFile({ videoId: videoUUID, fixture: 'video_short.mp4' })
77
78 await checkHook('action:api.video.file-updated')
79 })
80
81 it('Should run action:api.video.deleted', async function () {
82 await servers[0].videos.remove({ id: videoUUID })
83
84 await checkHook('action:api.video.deleted')
85 })
86
87 after(async function () {
88 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
89 videoUUID = uuid
90 })
91 })
92
93 describe('Video channel hooks', function () {
94 const channelName = 'my_super_channel'
95
96 it('Should run action:api.video-channel.created', async function () {
97 await servers[0].channels.create({ attributes: { name: channelName } })
98
99 await checkHook('action:api.video-channel.created')
100 })
101
102 it('Should run action:api.video-channel.updated', async function () {
103 await servers[0].channels.update({ channelName, attributes: { displayName: 'my display name' } })
104
105 await checkHook('action:api.video-channel.updated')
106 })
107
108 it('Should run action:api.video-channel.deleted', async function () {
109 await servers[0].channels.delete({ channelName })
110
111 await checkHook('action:api.video-channel.deleted')
112 })
113 })
114
115 describe('Live hooks', function () {
116
117 it('Should run action:api.live-video.created', async function () {
118 const attributes = {
119 name: 'live',
120 privacy: VideoPrivacy.PUBLIC,
121 channelId: servers[0].store.channel.id
122 }
123
124 await servers[0].live.create({ fields: attributes })
125
126 await checkHook('action:api.live-video.created')
127 })
128
129 it('Should run action:live.video.state.updated', async function () {
130 this.timeout(60000)
131
132 const attributes = {
133 name: 'live',
134 privacy: VideoPrivacy.PUBLIC,
135 channelId: servers[0].store.channel.id
136 }
137
138 const { uuid: liveVideoId } = await servers[0].live.create({ fields: attributes })
139 const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId })
140 await servers[0].live.waitUntilPublished({ videoId: liveVideoId })
141 await waitJobs(servers)
142
143 await checkHook('action:live.video.state.updated', true, 1)
144
145 await stopFfmpeg(ffmpegCommand)
146 await servers[0].live.waitUntilEnded({ videoId: liveVideoId })
147 await waitJobs(servers)
148
149 await checkHook('action:live.video.state.updated', true, 2)
150 })
151 })
152
153 describe('Comments hooks', function () {
154 it('Should run action:api.video-thread.created', async function () {
155 const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' })
156 threadId = created.id
157
158 await checkHook('action:api.video-thread.created')
159 })
160
161 it('Should run action:api.video-comment-reply.created', async function () {
162 await servers[0].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: 'reply' })
163
164 await checkHook('action:api.video-comment-reply.created')
165 })
166
167 it('Should run action:api.video-comment.deleted', async function () {
168 await servers[0].comments.delete({ videoId: videoUUID, commentId: threadId })
169
170 await checkHook('action:api.video-comment.deleted')
171 })
172 })
173
174 describe('Captions hooks', function () {
175 it('Should run action:api.video-caption.created', async function () {
176 await servers[0].captions.add({ videoId: videoUUID, language: 'en', fixture: 'subtitle-good.srt' })
177
178 await checkHook('action:api.video-caption.created')
179 })
180
181 it('Should run action:api.video-caption.deleted', async function () {
182 await servers[0].captions.delete({ videoId: videoUUID, language: 'en' })
183
184 await checkHook('action:api.video-caption.deleted')
185 })
186 })
187
188 describe('Users hooks', function () {
189 let userId: number
190
191 it('Should run action:api.user.registered', async function () {
192 await servers[0].registrations.register({ username: 'registered_user' })
193
194 await checkHook('action:api.user.registered')
195 })
196
197 it('Should run action:api.user.created', async function () {
198 const user = await servers[0].users.create({ username: 'created_user' })
199 userId = user.id
200
201 await checkHook('action:api.user.created')
202 })
203
204 it('Should run action:api.user.oauth2-got-token', async function () {
205 await servers[0].login.login({ user: { username: 'created_user' } })
206
207 await checkHook('action:api.user.oauth2-got-token')
208 })
209
210 it('Should run action:api.user.blocked', async function () {
211 await servers[0].users.banUser({ userId })
212
213 await checkHook('action:api.user.blocked')
214 })
215
216 it('Should run action:api.user.unblocked', async function () {
217 await servers[0].users.unbanUser({ userId })
218
219 await checkHook('action:api.user.unblocked')
220 })
221
222 it('Should run action:api.user.updated', async function () {
223 await servers[0].users.update({ userId, videoQuota: 50 })
224
225 await checkHook('action:api.user.updated')
226 })
227
228 it('Should run action:api.user.deleted', async function () {
229 await servers[0].users.remove({ userId })
230
231 await checkHook('action:api.user.deleted')
232 })
233 })
234
235 describe('Playlist hooks', function () {
236 let playlistId: number
237 let videoId: number
238
239 before(async function () {
240 {
241 const { id } = await servers[0].playlists.create({
242 attributes: {
243 displayName: 'My playlist',
244 privacy: VideoPlaylistPrivacy.PRIVATE
245 }
246 })
247 playlistId = id
248 }
249
250 {
251 const { id } = await servers[0].videos.upload({ attributes: { name: 'my super name' } })
252 videoId = id
253 }
254 })
255
256 it('Should run action:api.video-playlist-element.created', async function () {
257 await servers[0].playlists.addElement({ playlistId, attributes: { videoId } })
258
259 await checkHook('action:api.video-playlist-element.created')
260 })
261 })
262
263 describe('Notification hook', function () {
264
265 it('Should run action:notifier.notification.created', async function () {
266 await checkHook('action:notifier.notification.created', false)
267 })
268 })
269
270 describe('Activity Pub hooks', function () {
271 let videoUUID: string
272
273 it('Should run action:activity-pub.remote-video.created', async function () {
274 this.timeout(30000)
275
276 const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' })
277 videoUUID = uuid
278
279 await servers[0].servers.waitUntilLog('action:activity-pub.remote-video.created - AP remote video - video remote video')
280 })
281
282 it('Should run action:activity-pub.remote-video.updated', async function () {
283 this.timeout(30000)
284
285 await servers[1].videos.update({ id: videoUUID, attributes: { name: 'remote video updated' } })
286
287 await servers[0].servers.waitUntilLog(
288 'action:activity-pub.remote-video.updated - AP remote video updated - video remote video updated',
289 1,
290 false
291 )
292 })
293 })
294
295 after(async function () {
296 await cleanupTests(servers)
297 })
298})
diff --git a/packages/tests/src/plugins/external-auth.ts b/packages/tests/src/plugins/external-auth.ts
new file mode 100644
index 000000000..c7fe22185
--- /dev/null
+++ b/packages/tests/src/plugins/external-auth.ts
@@ -0,0 +1,436 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, HttpStatusCodeType, UserAdminFlag, UserRole } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 decodeQueryString,
10 PeerTubeServer,
11 PluginsCommand,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15async function loginExternal (options: {
16 server: PeerTubeServer
17 npmName: string
18 authName: string
19 username: string
20 query?: any
21 expectedStatus?: HttpStatusCodeType
22 expectedStatusStep2?: HttpStatusCodeType
23}) {
24 const res = await options.server.plugins.getExternalAuth({
25 npmName: options.npmName,
26 npmVersion: '0.0.1',
27 authName: options.authName,
28 query: options.query,
29 expectedStatus: options.expectedStatus || HttpStatusCode.FOUND_302
30 })
31
32 if (res.status !== HttpStatusCode.FOUND_302) return
33
34 const location = res.header.location
35 const { externalAuthToken } = decodeQueryString(location)
36
37 const resLogin = await options.server.login.loginUsingExternalToken({
38 username: options.username,
39 externalAuthToken: externalAuthToken as string,
40 expectedStatus: options.expectedStatusStep2
41 })
42
43 return resLogin.body
44}
45
46describe('Test external auth plugins', function () {
47 let server: PeerTubeServer
48
49 let cyanAccessToken: string
50 let cyanRefreshToken: string
51
52 let kefkaAccessToken: string
53 let kefkaRefreshToken: string
54 let kefkaId: number
55
56 let externalAuthToken: string
57
58 before(async function () {
59 this.timeout(30000)
60
61 server = await createSingleServer(1, {
62 rates_limit: {
63 login: {
64 max: 30
65 }
66 }
67 })
68
69 await setAccessTokensToServers([ server ])
70
71 for (const suffix of [ 'one', 'two', 'three' ]) {
72 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-external-auth-' + suffix) })
73 }
74 })
75
76 it('Should display the correct configuration', async function () {
77 const config = await server.config.getConfig()
78
79 const auths = config.plugin.registeredExternalAuths
80 expect(auths).to.have.lengthOf(9)
81
82 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
83 expect(auth2).to.exist
84 expect(auth2.authDisplayName).to.equal('External Auth 2')
85 expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one')
86 })
87
88 it('Should redirect for a Cyan login', async function () {
89 const res = await server.plugins.getExternalAuth({
90 npmName: 'test-external-auth-one',
91 npmVersion: '0.0.1',
92 authName: 'external-auth-1',
93 query: {
94 username: 'cyan'
95 },
96 expectedStatus: HttpStatusCode.FOUND_302
97 })
98
99 const location = res.header.location
100 expect(location.startsWith('/login?')).to.be.true
101
102 const searchParams = decodeQueryString(location)
103
104 expect(searchParams.externalAuthToken).to.exist
105 expect(searchParams.username).to.equal('cyan')
106
107 externalAuthToken = searchParams.externalAuthToken as string
108 })
109
110 it('Should reject auto external login with a missing or invalid token', async function () {
111 const command = server.login
112
113 await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
114 await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
115 })
116
117 it('Should reject auto external login with a missing or invalid username', async function () {
118 const command = server.login
119
120 await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
121 await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
122 })
123
124 it('Should reject auto external login with an expired token', async function () {
125 this.timeout(15000)
126
127 await wait(5000)
128
129 await server.login.loginUsingExternalToken({
130 username: 'cyan',
131 externalAuthToken,
132 expectedStatus: HttpStatusCode.BAD_REQUEST_400
133 })
134
135 await server.servers.waitUntilLog('expired external auth token', 4)
136 })
137
138 it('Should auto login Cyan, create the user and use the token', async function () {
139 {
140 const res = await loginExternal({
141 server,
142 npmName: 'test-external-auth-one',
143 authName: 'external-auth-1',
144 query: {
145 username: 'cyan'
146 },
147 username: 'cyan'
148 })
149
150 cyanAccessToken = res.access_token
151 cyanRefreshToken = res.refresh_token
152 }
153
154 {
155 const body = await server.users.getMyInfo({ token: cyanAccessToken })
156 expect(body.username).to.equal('cyan')
157 expect(body.account.displayName).to.equal('cyan')
158 expect(body.email).to.equal('cyan@example.com')
159 expect(body.role.id).to.equal(UserRole.USER)
160 expect(body.adminFlags).to.equal(UserAdminFlag.NONE)
161 expect(body.videoQuota).to.equal(5242880)
162 expect(body.videoQuotaDaily).to.equal(-1)
163 }
164 })
165
166 it('Should auto login Kefka, create the user and use the token', async function () {
167 {
168 const res = await loginExternal({
169 server,
170 npmName: 'test-external-auth-one',
171 authName: 'external-auth-2',
172 username: 'kefka'
173 })
174
175 kefkaAccessToken = res.access_token
176 kefkaRefreshToken = res.refresh_token
177 }
178
179 {
180 const body = await server.users.getMyInfo({ token: kefkaAccessToken })
181 expect(body.username).to.equal('kefka')
182 expect(body.account.displayName).to.equal('Kefka Palazzo')
183 expect(body.email).to.equal('kefka@example.com')
184 expect(body.role.id).to.equal(UserRole.ADMINISTRATOR)
185 expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST)
186 expect(body.videoQuota).to.equal(42000)
187 expect(body.videoQuotaDaily).to.equal(42100)
188
189 kefkaId = body.id
190 }
191 })
192
193 it('Should refresh Cyan token, but not Kefka token', async function () {
194 {
195 const resRefresh = await server.login.refreshToken({ refreshToken: cyanRefreshToken })
196 cyanAccessToken = resRefresh.body.access_token
197 cyanRefreshToken = resRefresh.body.refresh_token
198
199 const body = await server.users.getMyInfo({ token: cyanAccessToken })
200 expect(body.username).to.equal('cyan')
201 }
202
203 {
204 await server.login.refreshToken({ refreshToken: kefkaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
205 }
206 })
207
208 it('Should update Cyan profile', async function () {
209 await server.users.updateMe({
210 token: cyanAccessToken,
211 displayName: 'Cyan Garamonde',
212 description: 'Retainer to the king of Doma'
213 })
214
215 const body = await server.users.getMyInfo({ token: cyanAccessToken })
216 expect(body.account.displayName).to.equal('Cyan Garamonde')
217 expect(body.account.description).to.equal('Retainer to the king of Doma')
218 })
219
220 it('Should logout Cyan', async function () {
221 await server.login.logout({ token: cyanAccessToken })
222 })
223
224 it('Should have logged out Cyan', async function () {
225 await server.servers.waitUntilLog('On logout cyan')
226
227 await server.users.getMyInfo({ token: cyanAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
228 })
229
230 it('Should login Cyan and keep the old existing profile', async function () {
231 {
232 const res = await loginExternal({
233 server,
234 npmName: 'test-external-auth-one',
235 authName: 'external-auth-1',
236 query: {
237 username: 'cyan'
238 },
239 username: 'cyan'
240 })
241
242 cyanAccessToken = res.access_token
243 }
244
245 const body = await server.users.getMyInfo({ token: cyanAccessToken })
246 expect(body.username).to.equal('cyan')
247 expect(body.account.displayName).to.equal('Cyan Garamonde')
248 expect(body.account.description).to.equal('Retainer to the king of Doma')
249 expect(body.role.id).to.equal(UserRole.USER)
250 })
251
252 it('Should login Kefka and update the profile', async function () {
253 {
254 await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 })
255 await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' })
256
257 const body = await server.users.getMyInfo({ token: kefkaAccessToken })
258 expect(body.username).to.equal('kefka')
259 expect(body.account.displayName).to.equal('kefka updated')
260 expect(body.videoQuota).to.equal(43000)
261 expect(body.videoQuotaDaily).to.equal(43100)
262 }
263
264 {
265 const res = await loginExternal({
266 server,
267 npmName: 'test-external-auth-one',
268 authName: 'external-auth-2',
269 username: 'kefka'
270 })
271
272 kefkaAccessToken = res.access_token
273 kefkaRefreshToken = res.refresh_token
274
275 const body = await server.users.getMyInfo({ token: kefkaAccessToken })
276 expect(body.username).to.equal('kefka')
277 expect(body.account.displayName).to.equal('Kefka Palazzo')
278 expect(body.videoQuota).to.equal(42000)
279 expect(body.videoQuotaDaily).to.equal(43100)
280 }
281 })
282
283 it('Should not update an external auth email', async function () {
284 await server.users.updateMe({
285 token: cyanAccessToken,
286 email: 'toto@example.com',
287 currentPassword: 'toto',
288 expectedStatus: HttpStatusCode.BAD_REQUEST_400
289 })
290 })
291
292 it('Should reject token of Kefka by the plugin hook', async function () {
293 await wait(5000)
294
295 await server.users.getMyInfo({ token: kefkaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
296 })
297
298 it('Should unregister external-auth-2 and do not login existing Kefka', async function () {
299 await server.plugins.updateSettings({
300 npmName: 'peertube-plugin-test-external-auth-one',
301 settings: { disableKefka: true }
302 })
303
304 await server.login.login({ user: { username: 'kefka', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
305
306 await loginExternal({
307 server,
308 npmName: 'test-external-auth-one',
309 authName: 'external-auth-2',
310 query: {
311 username: 'kefka'
312 },
313 username: 'kefka',
314 expectedStatus: HttpStatusCode.NOT_FOUND_404
315 })
316 })
317
318 it('Should have disabled this auth', async function () {
319 const config = await server.config.getConfig()
320
321 const auths = config.plugin.registeredExternalAuths
322 expect(auths).to.have.lengthOf(8)
323
324 const auth1 = auths.find(a => a.authName === 'external-auth-2')
325 expect(auth1).to.not.exist
326 })
327
328 it('Should uninstall the plugin one and do not login Cyan', async function () {
329 await server.plugins.uninstall({ npmName: 'peertube-plugin-test-external-auth-one' })
330
331 await loginExternal({
332 server,
333 npmName: 'test-external-auth-one',
334 authName: 'external-auth-1',
335 query: {
336 username: 'cyan'
337 },
338 username: 'cyan',
339 expectedStatus: HttpStatusCode.NOT_FOUND_404
340 })
341
342 await server.login.login({ user: { username: 'cyan', password: null }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
343 await server.login.login({ user: { username: 'cyan', password: '' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
344 await server.login.login({ user: { username: 'cyan', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
345 })
346
347 it('Should not login kefka with another plugin', async function () {
348 await loginExternal({
349 server,
350 npmName: 'test-external-auth-two',
351 authName: 'external-auth-4',
352 username: 'kefka2',
353 expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
354 })
355
356 await loginExternal({
357 server,
358 npmName: 'test-external-auth-two',
359 authName: 'external-auth-4',
360 username: 'kefka',
361 expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
362 })
363 })
364
365 it('Should not login an existing user email', async function () {
366 await server.users.create({ username: 'existing_user', password: 'super_password' })
367
368 await loginExternal({
369 server,
370 npmName: 'test-external-auth-two',
371 authName: 'external-auth-6',
372 username: 'existing_user',
373 expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400
374 })
375 })
376
377 it('Should be able to login an existing user username and channel', async function () {
378 await server.users.create({ username: 'existing_user2' })
379 await server.users.create({ username: 'existing_user2-1_channel' })
380
381 // Test twice to ensure we don't generate a username on every login
382 for (let i = 0; i < 2; i++) {
383 const res = await loginExternal({
384 server,
385 npmName: 'test-external-auth-two',
386 authName: 'external-auth-7',
387 username: 'existing_user2'
388 })
389
390 const token = res.access_token
391
392 const myInfo = await server.users.getMyInfo({ token })
393 expect(myInfo.username).to.equal('existing_user2-1')
394
395 expect(myInfo.videoChannels[0].name).to.equal('existing_user2-1_channel-1')
396 }
397 })
398
399 it('Should display the correct configuration', async function () {
400 const config = await server.config.getConfig()
401
402 const auths = config.plugin.registeredExternalAuths
403 expect(auths).to.have.lengthOf(7)
404
405 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
406 expect(auth2).to.not.exist
407 })
408
409 after(async function () {
410 await cleanupTests([ server ])
411 })
412
413 it('Should forward the redirectUrl if the plugin returns one', async function () {
414 const resLogin = await loginExternal({
415 server,
416 npmName: 'test-external-auth-three',
417 authName: 'external-auth-7',
418 username: 'cid'
419 })
420
421 const { redirectUrl } = await server.login.logout({ token: resLogin.access_token })
422 expect(redirectUrl).to.equal('https://example.com/redirectUrl')
423 })
424
425 it('Should call the plugin\'s onLogout method with the request', async function () {
426 const resLogin = await loginExternal({
427 server,
428 npmName: 'test-external-auth-three',
429 authName: 'external-auth-8',
430 username: 'cid'
431 })
432
433 const { redirectUrl } = await server.login.logout({ token: resLogin.access_token })
434 expect(redirectUrl).to.equal('https://example.com/redirectUrl?access_token=' + resLogin.access_token)
435 })
436})
diff --git a/packages/tests/src/plugins/filter-hooks.ts b/packages/tests/src/plugins/filter-hooks.ts
new file mode 100644
index 000000000..88cfee631
--- /dev/null
+++ b/packages/tests/src/plugins/filter-hooks.ts
@@ -0,0 +1,909 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 HttpStatusCode,
6 PeerTubeProblemDocument,
7 VideoDetails,
8 VideoImportState,
9 VideoPlaylist,
10 VideoPlaylistPrivacy,
11 VideoPrivacy
12} from '@peertube/peertube-models'
13import {
14 cleanupTests,
15 createMultipleServers,
16 doubleFollow,
17 makeActivityPubGetRequest,
18 makeGetRequest,
19 makeRawRequest,
20 PeerTubeServer,
21 PluginsCommand,
22 setAccessTokensToServers,
23 setDefaultVideoChannel,
24 waitJobs
25} from '@peertube/peertube-server-commands'
26import { FIXTURE_URLS } from '../shared/tests.js'
27
28describe('Test plugin filter hooks', function () {
29 let servers: PeerTubeServer[]
30 let videoUUID: string
31 let threadId: number
32 let videoPlaylistUUID: string
33
34 before(async function () {
35 this.timeout(120000)
36
37 servers = await createMultipleServers(2)
38 await setAccessTokensToServers(servers)
39 await setDefaultVideoChannel(servers)
40 await doubleFollow(servers[0], servers[1])
41
42 await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() })
43 await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') })
44 {
45 ({ uuid: videoPlaylistUUID } = await servers[0].playlists.create({
46 attributes: {
47 displayName: 'my super playlist',
48 privacy: VideoPlaylistPrivacy.PUBLIC,
49 description: 'my super description',
50 videoChannelId: servers[0].store.channel.id
51 }
52 }))
53 }
54
55 for (let i = 0; i < 10; i++) {
56 const video = await servers[0].videos.upload({ attributes: { name: 'default video ' + i } })
57 await servers[0].playlists.addElement({ playlistId: videoPlaylistUUID, attributes: { videoId: video.id } })
58 }
59
60 const { data } = await servers[0].videos.list()
61 videoUUID = data[0].uuid
62
63 await servers[0].config.updateCustomSubConfig({
64 newConfig: {
65 live: { enabled: true },
66 signup: { enabled: true },
67 videoFile: {
68 update: {
69 enabled: true
70 }
71 },
72 import: {
73 videos: {
74 http: { enabled: true },
75 torrent: { enabled: true }
76 }
77 }
78 }
79 })
80
81 // Root subscribes to itself
82 await servers[0].subscriptions.add({ targetUri: 'root_channel@' + servers[0].host })
83 })
84
85 describe('Videos', function () {
86
87 it('Should run filter:api.videos.list.params', async function () {
88 const { data } = await servers[0].videos.list({ start: 0, count: 2 })
89
90 // 2 plugins do +1 to the count parameter
91 expect(data).to.have.lengthOf(4)
92 })
93
94 it('Should run filter:api.videos.list.result', async function () {
95 const { total } = await servers[0].videos.list({ start: 0, count: 0 })
96
97 // Plugin do +1 to the total result
98 expect(total).to.equal(11)
99 })
100
101 it('Should run filter:api.video-playlist.videos.list.params', async function () {
102 const { data } = await servers[0].playlists.listVideos({
103 count: 2,
104 playlistId: videoPlaylistUUID
105 })
106
107 // 1 plugin do +1 to the count parameter
108 expect(data).to.have.lengthOf(3)
109 })
110
111 it('Should run filter:api.video-playlist.videos.list.result', async function () {
112 const { total } = await servers[0].playlists.listVideos({
113 count: 0,
114 playlistId: videoPlaylistUUID
115 })
116
117 // Plugin do +1 to the total result
118 expect(total).to.equal(11)
119 })
120
121 it('Should run filter:api.accounts.videos.list.params', async function () {
122 const { data } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 })
123
124 // 1 plugin do +1 to the count parameter
125 expect(data).to.have.lengthOf(3)
126 })
127
128 it('Should run filter:api.accounts.videos.list.result', async function () {
129 const { total } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 })
130
131 // Plugin do +2 to the total result
132 expect(total).to.equal(12)
133 })
134
135 it('Should run filter:api.video-channels.videos.list.params', async function () {
136 const { data } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 })
137
138 // 1 plugin do +3 to the count parameter
139 expect(data).to.have.lengthOf(5)
140 })
141
142 it('Should run filter:api.video-channels.videos.list.result', async function () {
143 const { total } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 })
144
145 // Plugin do +3 to the total result
146 expect(total).to.equal(13)
147 })
148
149 it('Should run filter:api.user.me.videos.list.params', async function () {
150 const { data } = await servers[0].videos.listMyVideos({ start: 0, count: 2 })
151
152 // 1 plugin do +4 to the count parameter
153 expect(data).to.have.lengthOf(6)
154 })
155
156 it('Should run filter:api.user.me.videos.list.result', async function () {
157 const { total } = await servers[0].videos.listMyVideos({ start: 0, count: 2 })
158
159 // Plugin do +4 to the total result
160 expect(total).to.equal(14)
161 })
162
163 it('Should run filter:api.user.me.subscription-videos.list.params', async function () {
164 const { data } = await servers[0].videos.listMySubscriptionVideos({ start: 0, count: 2 })
165
166 // 1 plugin do +1 to the count parameter
167 expect(data).to.have.lengthOf(3)
168 })
169
170 it('Should run filter:api.user.me.subscription-videos.list.result', async function () {
171 const { total } = await servers[0].videos.listMySubscriptionVideos({ start: 0, count: 2 })
172
173 // Plugin do +4 to the total result
174 expect(total).to.equal(14)
175 })
176
177 it('Should run filter:api.video.get.result', async function () {
178 const video = await servers[0].videos.get({ id: videoUUID })
179 expect(video.name).to.contain('<3')
180 })
181 })
182
183 describe('Video/live/import accept', function () {
184
185 it('Should run filter:api.video.upload.accept.result', async function () {
186 const options = { attributes: { name: 'video with bad word' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }
187 await servers[0].videos.upload({ mode: 'legacy', ...options })
188 await servers[0].videos.upload({ mode: 'resumable', ...options })
189 })
190
191 it('Should run filter:api.video.update-file.accept.result', async function () {
192 const res = await servers[0].videos.replaceSourceFile({
193 videoId: videoUUID,
194 fixture: 'video_short1.webm',
195 completedExpectedStatus: HttpStatusCode.FORBIDDEN_403
196 })
197
198 expect((res as any)?.error).to.equal('no webm')
199 })
200
201 it('Should run filter:api.live-video.create.accept.result', async function () {
202 const attributes = {
203 name: 'video with bad word',
204 privacy: VideoPrivacy.PUBLIC,
205 channelId: servers[0].store.channel.id
206 }
207
208 await servers[0].live.create({ fields: attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
209 })
210
211 it('Should run filter:api.video.pre-import-url.accept.result', async function () {
212 const attributes = {
213 name: 'normal title',
214 privacy: VideoPrivacy.PUBLIC,
215 channelId: servers[0].store.channel.id,
216 targetUrl: FIXTURE_URLS.goodVideo + 'bad'
217 }
218 await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
219 })
220
221 it('Should run filter:api.video.pre-import-torrent.accept.result', async function () {
222 const attributes = {
223 name: 'bad torrent',
224 privacy: VideoPrivacy.PUBLIC,
225 channelId: servers[0].store.channel.id,
226 torrentfile: 'video-720p.torrent' as any
227 }
228 await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
229 })
230
231 it('Should run filter:api.video.post-import-url.accept.result', async function () {
232 this.timeout(60000)
233
234 let videoImportId: number
235
236 {
237 const attributes = {
238 name: 'title with bad word',
239 privacy: VideoPrivacy.PUBLIC,
240 channelId: servers[0].store.channel.id,
241 targetUrl: FIXTURE_URLS.goodVideo
242 }
243 const body = await servers[0].imports.importVideo({ attributes })
244 videoImportId = body.id
245 }
246
247 await waitJobs(servers)
248
249 {
250 const body = await servers[0].imports.getMyVideoImports()
251 const videoImports = body.data
252
253 const videoImport = videoImports.find(i => i.id === videoImportId)
254
255 expect(videoImport.state.id).to.equal(VideoImportState.REJECTED)
256 expect(videoImport.state.label).to.equal('Rejected')
257 }
258 })
259
260 it('Should run filter:api.video.post-import-torrent.accept.result', async function () {
261 this.timeout(60000)
262
263 let videoImportId: number
264
265 {
266 const attributes = {
267 name: 'title with bad word',
268 privacy: VideoPrivacy.PUBLIC,
269 channelId: servers[0].store.channel.id,
270 torrentfile: 'video-720p.torrent' as any
271 }
272 const body = await servers[0].imports.importVideo({ attributes })
273 videoImportId = body.id
274 }
275
276 await waitJobs(servers)
277
278 {
279 const { data: videoImports } = await servers[0].imports.getMyVideoImports()
280
281 const videoImport = videoImports.find(i => i.id === videoImportId)
282
283 expect(videoImport.state.id).to.equal(VideoImportState.REJECTED)
284 expect(videoImport.state.label).to.equal('Rejected')
285 }
286 })
287 })
288
289 describe('Video comments accept', function () {
290
291 it('Should run filter:api.video-thread.create.accept.result', async function () {
292 await servers[0].comments.createThread({
293 videoId: videoUUID,
294 text: 'comment with bad word',
295 expectedStatus: HttpStatusCode.FORBIDDEN_403
296 })
297 })
298
299 it('Should run filter:api.video-comment-reply.create.accept.result', async function () {
300 const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' })
301 threadId = created.id
302
303 await servers[0].comments.addReply({
304 videoId: videoUUID,
305 toCommentId: threadId,
306 text: 'comment with bad word',
307 expectedStatus: HttpStatusCode.FORBIDDEN_403
308 })
309 await servers[0].comments.addReply({
310 videoId: videoUUID,
311 toCommentId: threadId,
312 text: 'comment with good word',
313 expectedStatus: HttpStatusCode.OK_200
314 })
315 })
316
317 it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a thread creation', async function () {
318 this.timeout(30000)
319
320 await servers[1].comments.createThread({ videoId: videoUUID, text: 'comment with bad word' })
321
322 await waitJobs(servers)
323
324 {
325 const thread = await servers[0].comments.listThreads({ videoId: videoUUID })
326 expect(thread.data).to.have.lengthOf(1)
327 expect(thread.data[0].text).to.not.include(' bad ')
328 }
329
330 {
331 const thread = await servers[1].comments.listThreads({ videoId: videoUUID })
332 expect(thread.data).to.have.lengthOf(2)
333 }
334 })
335
336 it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a reply creation', async function () {
337 this.timeout(30000)
338
339 const { data } = await servers[1].comments.listThreads({ videoId: videoUUID })
340 const threadIdServer2 = data.find(t => t.text === 'thread').id
341
342 await servers[1].comments.addReply({
343 videoId: videoUUID,
344 toCommentId: threadIdServer2,
345 text: 'comment with bad word'
346 })
347
348 await waitJobs(servers)
349
350 {
351 const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId })
352 expect(tree.children).to.have.lengthOf(1)
353 expect(tree.children[0].comment.text).to.not.include(' bad ')
354 }
355
356 {
357 const tree = await servers[1].comments.getThread({ videoId: videoUUID, threadId: threadIdServer2 })
358 expect(tree.children).to.have.lengthOf(2)
359 }
360 })
361 })
362
363 describe('Video comments', function () {
364
365 it('Should run filter:api.video-threads.list.params', async function () {
366 const { data } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 })
367
368 // our plugin do +1 to the count parameter
369 expect(data).to.have.lengthOf(1)
370 })
371
372 it('Should run filter:api.video-threads.list.result', async function () {
373 const { total } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 })
374
375 // Plugin do +1 to the total result
376 expect(total).to.equal(2)
377 })
378
379 it('Should run filter:api.video-thread-comments.list.params')
380
381 it('Should run filter:api.video-thread-comments.list.result', async function () {
382 const thread = await servers[0].comments.getThread({ videoId: videoUUID, threadId })
383
384 expect(thread.comment.text.endsWith(' <3')).to.be.true
385 })
386
387 it('Should run filter:api.overviews.videos.list.{params,result}', async function () {
388 await servers[0].overviews.getVideos({ page: 1 })
389
390 // 3 because we get 3 samples per page
391 await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.params', 3)
392 await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.result', 3)
393 })
394 })
395
396 describe('filter:video.auto-blacklist.result', function () {
397
398 async function checkIsBlacklisted (id: number | string, value: boolean) {
399 const video = await servers[0].videos.getWithToken({ id })
400 expect(video.blacklisted).to.equal(value)
401 }
402
403 it('Should blacklist on upload', async function () {
404 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video please blacklist me' } })
405 await checkIsBlacklisted(uuid, true)
406 })
407
408 it('Should blacklist on import', async function () {
409 this.timeout(15000)
410
411 const attributes = {
412 name: 'video please blacklist me',
413 targetUrl: FIXTURE_URLS.goodVideo,
414 channelId: servers[0].store.channel.id
415 }
416 const body = await servers[0].imports.importVideo({ attributes })
417 await checkIsBlacklisted(body.video.uuid, true)
418 })
419
420 it('Should blacklist on update', async function () {
421 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } })
422 await checkIsBlacklisted(uuid, false)
423
424 await servers[0].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } })
425 await checkIsBlacklisted(uuid, true)
426 })
427
428 it('Should blacklist on remote upload', async function () {
429 this.timeout(120000)
430
431 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'remote please blacklist me' } })
432 await waitJobs(servers)
433
434 await checkIsBlacklisted(uuid, true)
435 })
436
437 it('Should blacklist on remote update', async function () {
438 this.timeout(120000)
439
440 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video' } })
441 await waitJobs(servers)
442
443 await checkIsBlacklisted(uuid, false)
444
445 await servers[1].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } })
446 await waitJobs(servers)
447
448 await checkIsBlacklisted(uuid, true)
449 })
450 })
451
452 describe('Should run filter:api.user.signup.allowed.result', function () {
453
454 before(async function () {
455 await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: false } } })
456 })
457
458 it('Should run on config endpoint', async function () {
459 const body = await servers[0].config.getConfig()
460 expect(body.signup.allowed).to.be.true
461 })
462
463 it('Should allow a signup', async function () {
464 await servers[0].registrations.register({ username: 'john1' })
465 })
466
467 it('Should not allow a signup', async function () {
468 const res = await servers[0].registrations.register({
469 username: 'jma 1',
470 expectedStatus: HttpStatusCode.FORBIDDEN_403
471 })
472
473 expect(res.body.error).to.equal('No jma 1')
474 })
475 })
476
477 describe('Should run filter:api.user.request-signup.allowed.result', function () {
478
479 before(async function () {
480 await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: true } } })
481 })
482
483 it('Should run on config endpoint', async function () {
484 const body = await servers[0].config.getConfig()
485 expect(body.signup.allowed).to.be.true
486 })
487
488 it('Should allow a signup request', async function () {
489 await servers[0].registrations.requestRegistration({ username: 'john2', registrationReason: 'tt' })
490 })
491
492 it('Should not allow a signup request', async function () {
493 const body = await servers[0].registrations.requestRegistration({
494 username: 'jma 2',
495 registrationReason: 'tt',
496 expectedStatus: HttpStatusCode.FORBIDDEN_403
497 })
498
499 expect((body as unknown as PeerTubeProblemDocument).error).to.equal('No jma 2')
500 })
501 })
502
503 describe('Download hooks', function () {
504 const downloadVideos: VideoDetails[] = []
505 let downloadVideo2Token: string
506
507 before(async function () {
508 this.timeout(120000)
509
510 await servers[0].config.updateCustomSubConfig({
511 newConfig: {
512 transcoding: {
513 webVideos: {
514 enabled: true
515 },
516 hls: {
517 enabled: true
518 }
519 }
520 }
521 })
522
523 const uuids: string[] = []
524
525 for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
526 const uuid = (await servers[0].videos.quickUpload({ name })).uuid
527 uuids.push(uuid)
528 }
529
530 await waitJobs(servers)
531
532 for (const uuid of uuids) {
533 downloadVideos.push(await servers[0].videos.get({ id: uuid }))
534 }
535
536 downloadVideo2Token = await servers[0].videoToken.getVideoFileToken({ videoId: downloadVideos[2].uuid })
537 })
538
539 it('Should run filter:api.download.torrent.allowed.result', async function () {
540 const res = await makeRawRequest({ url: downloadVideos[0].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
541 expect(res.body.error).to.equal('Liu Bei')
542
543 await makeRawRequest({ url: downloadVideos[1].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 })
544 await makeRawRequest({ url: downloadVideos[2].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 })
545 })
546
547 it('Should run filter:api.download.video.allowed.result', async function () {
548 {
549 const refused = downloadVideos[1].files[0].fileDownloadUrl
550 const allowed = [
551 downloadVideos[0].files[0].fileDownloadUrl,
552 downloadVideos[2].files[0].fileDownloadUrl
553 ]
554
555 const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
556 expect(res.body.error).to.equal('Cao Cao')
557
558 for (const url of allowed) {
559 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
560 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
561 }
562 }
563
564 {
565 const refused = downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl
566
567 const allowed = [
568 downloadVideos[2].files[0].fileDownloadUrl,
569 downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl,
570 downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl
571 ]
572
573 // Only streaming playlist is refuse
574 const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
575 expect(res.body.error).to.equal('Sun Jian')
576
577 // But not we there is a user in res
578 await makeRawRequest({ url: refused, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 })
579 await makeRawRequest({ url: refused, query: { videoFileToken: downloadVideo2Token }, expectedStatus: HttpStatusCode.OK_200 })
580
581 // Other files work
582 for (const url of allowed) {
583 await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
584 }
585 }
586 })
587 })
588
589 describe('Embed filters', function () {
590 const embedVideos: VideoDetails[] = []
591 const embedPlaylists: VideoPlaylist[] = []
592
593 before(async function () {
594 this.timeout(60000)
595
596 await servers[0].config.disableTranscoding()
597
598 for (const name of [ 'bad embed', 'good embed' ]) {
599 {
600 const uuid = (await servers[0].videos.quickUpload({ name })).uuid
601 embedVideos.push(await servers[0].videos.get({ id: uuid }))
602 }
603
604 {
605 const attributes = { displayName: name, videoChannelId: servers[0].store.channel.id, privacy: VideoPlaylistPrivacy.PUBLIC }
606 const { id } = await servers[0].playlists.create({ attributes })
607
608 const playlist = await servers[0].playlists.get({ playlistId: id })
609 embedPlaylists.push(playlist)
610 }
611 }
612 })
613
614 it('Should run filter:html.embed.video.allowed.result', async function () {
615 const res = await makeGetRequest({ url: servers[0].url, path: embedVideos[0].embedPath, expectedStatus: HttpStatusCode.OK_200 })
616 expect(res.text).to.equal('Lu Bu')
617 })
618
619 it('Should run filter:html.embed.video-playlist.allowed.result', async function () {
620 const res = await makeGetRequest({ url: servers[0].url, path: embedPlaylists[0].embedPath, expectedStatus: HttpStatusCode.OK_200 })
621 expect(res.text).to.equal('Diao Chan')
622 })
623 })
624
625 describe('Client HTML filters', function () {
626 let videoUUID: string
627
628 before(async function () {
629 this.timeout(60000)
630
631 const { uuid } = await servers[0].videos.quickUpload({ name: 'html video' })
632 videoUUID = uuid
633 })
634
635 it('Should run filter:html.client.json-ld.result', async function () {
636 const res = await makeGetRequest({ url: servers[0].url, path: '/w/' + videoUUID, expectedStatus: HttpStatusCode.OK_200 })
637 expect(res.text).to.contain('"recordedAt":"http://example.com/recordedAt"')
638 })
639
640 it('Should not run filter:html.client.json-ld.result with an account', async function () {
641 const res = await makeGetRequest({ url: servers[0].url, path: '/a/root', expectedStatus: HttpStatusCode.OK_200 })
642 expect(res.text).not.to.contain('"recordedAt":"http://example.com/recordedAt"')
643 })
644 })
645
646 describe('Search filters', function () {
647
648 before(async function () {
649 await servers[0].config.updateCustomSubConfig({
650 newConfig: {
651 search: {
652 searchIndex: {
653 enabled: true,
654 isDefaultSearch: false,
655 disableLocalSearch: false
656 }
657 }
658 }
659 })
660 })
661
662 it('Should run filter:api.search.videos.local.list.{params,result}', async function () {
663 await servers[0].search.advancedVideoSearch({
664 search: {
665 search: 'Sun Quan'
666 }
667 })
668
669 await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1)
670 await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1)
671 })
672
673 it('Should run filter:api.search.videos.index.list.{params,result}', async function () {
674 await servers[0].search.advancedVideoSearch({
675 search: {
676 search: 'Sun Quan',
677 searchTarget: 'search-index'
678 }
679 })
680
681 await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1)
682 await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1)
683 await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.params', 1)
684 await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.result', 1)
685 })
686
687 it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () {
688 await servers[0].search.advancedChannelSearch({
689 search: {
690 search: 'Sun Ce'
691 }
692 })
693
694 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1)
695 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1)
696 })
697
698 it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () {
699 await servers[0].search.advancedChannelSearch({
700 search: {
701 search: 'Sun Ce',
702 searchTarget: 'search-index'
703 }
704 })
705
706 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1)
707 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1)
708 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.params', 1)
709 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.result', 1)
710 })
711
712 it('Should run filter:api.search.video-playlists.local.list.{params,result}', async function () {
713 await servers[0].search.advancedPlaylistSearch({
714 search: {
715 search: 'Sun Jian'
716 }
717 })
718
719 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1)
720 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1)
721 })
722
723 it('Should run filter:api.search.video-playlists.index.list.{params,result}', async function () {
724 await servers[0].search.advancedPlaylistSearch({
725 search: {
726 search: 'Sun Jian',
727 searchTarget: 'search-index'
728 }
729 })
730
731 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1)
732 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1)
733 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.params', 1)
734 await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.result', 1)
735 })
736 })
737
738 describe('Upload/import/live attributes filters', function () {
739
740 before(async function () {
741 await servers[0].config.enableLive({ transcoding: false, allowReplay: false })
742 await servers[0].config.enableImports()
743 await servers[0].config.disableTranscoding()
744 })
745
746 it('Should run filter:api.video.upload.video-attribute.result', async function () {
747 for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) {
748 const { id } = await servers[0].videos.upload({ attributes: { name: 'video', description: 'upload' }, mode })
749
750 const video = await servers[0].videos.get({ id })
751 expect(video.description).to.equal('upload - filter:api.video.upload.video-attribute.result')
752 }
753 })
754
755 it('Should run filter:api.video.import-url.video-attribute.result', async function () {
756 const attributes = {
757 name: 'video',
758 description: 'import url',
759 channelId: servers[0].store.channel.id,
760 targetUrl: FIXTURE_URLS.goodVideo,
761 privacy: VideoPrivacy.PUBLIC
762 }
763 const { video: { id } } = await servers[0].imports.importVideo({ attributes })
764
765 const video = await servers[0].videos.get({ id })
766 expect(video.description).to.equal('import url - filter:api.video.import-url.video-attribute.result')
767 })
768
769 it('Should run filter:api.video.import-torrent.video-attribute.result', async function () {
770 const attributes = {
771 name: 'video',
772 description: 'import torrent',
773 channelId: servers[0].store.channel.id,
774 magnetUri: FIXTURE_URLS.magnet,
775 privacy: VideoPrivacy.PUBLIC
776 }
777 const { video: { id } } = await servers[0].imports.importVideo({ attributes })
778
779 const video = await servers[0].videos.get({ id })
780 expect(video.description).to.equal('import torrent - filter:api.video.import-torrent.video-attribute.result')
781 })
782
783 it('Should run filter:api.video.live.video-attribute.result', async function () {
784 const fields = {
785 name: 'live',
786 description: 'live',
787 channelId: servers[0].store.channel.id,
788 privacy: VideoPrivacy.PUBLIC
789 }
790 const { id } = await servers[0].live.create({ fields })
791
792 const video = await servers[0].videos.get({ id })
793 expect(video.description).to.equal('live - filter:api.video.live.video-attribute.result')
794 })
795 })
796
797 describe('Stats filters', function () {
798
799 it('Should run filter:api.server.stats.get.result', async function () {
800 const data = await servers[0].stats.get()
801
802 expect((data as any).customStats).to.equal(14)
803 })
804
805 })
806
807 describe('Job queue filters', function () {
808 let videoUUID: string
809
810 before(async function () {
811 this.timeout(120_000)
812
813 await servers[0].config.enableMinimumTranscoding()
814 const { uuid } = await servers[0].videos.quickUpload({ name: 'studio' })
815
816 const video = await servers[0].videos.get({ id: uuid })
817 expect(video.duration).at.least(2)
818 videoUUID = video.uuid
819
820 await waitJobs(servers)
821
822 await servers[0].config.enableStudio()
823 })
824
825 it('Should run filter:job-queue.process.params', async function () {
826 this.timeout(120_000)
827
828 await servers[0].videoStudio.createEditionTasks({
829 videoId: videoUUID,
830 tasks: [
831 {
832 name: 'add-intro',
833 options: {
834 file: 'video_very_short_240p.mp4'
835 }
836 }
837 ]
838 })
839
840 await waitJobs(servers)
841
842 await servers[0].servers.waitUntilLog('Run hook filter:job-queue.process.params', 1, false)
843
844 const video = await servers[0].videos.get({ id: videoUUID })
845 expect(video.duration).at.most(2)
846 })
847
848 it('Should run filter:job-queue.process.result', async function () {
849 await servers[0].servers.waitUntilLog('Run hook filter:job-queue.process.result', 1, false)
850 })
851 })
852
853 describe('Transcoding filters', async function () {
854
855 it('Should run filter:transcoding.auto.resolutions-to-transcode.result', async function () {
856 const { uuid } = await servers[0].videos.quickUpload({ name: 'transcode-filter' })
857
858 await waitJobs(servers)
859
860 const video = await servers[0].videos.get({ id: uuid })
861 expect(video.files).to.have.lengthOf(2)
862 expect(video.files.find(f => f.resolution.id === 100 as any)).to.exist
863 })
864 })
865
866 describe('Video channel filters', async function () {
867
868 it('Should run filter:api.video-channels.list.params', async function () {
869 const { data } = await servers[0].channels.list({ start: 0, count: 0 })
870
871 // plugin do +1 to the count parameter
872 expect(data).to.have.lengthOf(1)
873 })
874
875 it('Should run filter:api.video-channels.list.result', async function () {
876 const { total } = await servers[0].channels.list({ start: 0, count: 1 })
877
878 // plugin do +1 to the total parameter
879 expect(total).to.equal(4)
880 })
881
882 it('Should run filter:api.video-channel.get.result', async function () {
883 const channel = await servers[0].channels.get({ channelName: 'root_channel' })
884 expect(channel.displayName).to.equal('Main root channel <3')
885 })
886 })
887
888 describe('Activity Pub', function () {
889
890 it('Should run filter:activity-pub.activity.context.build.result', async function () {
891 const { body } = await makeActivityPubGetRequest(servers[0].url, '/w/' + videoUUID)
892 expect(body.type).to.equal('Video')
893
894 expect(body['@context'].some(c => {
895 return typeof c === 'object' && c.recordedAt === 'https://schema.org/recordedAt'
896 })).to.be.true
897 })
898
899 it('Should run filter:activity-pub.video.json-ld.build.result', async function () {
900 const { body } = await makeActivityPubGetRequest(servers[0].url, '/w/' + videoUUID)
901 expect(body.name).to.equal('default video 0')
902 expect(body.videoName).to.equal('default video 0')
903 })
904 })
905
906 after(async function () {
907 await cleanupTests(servers)
908 })
909})
diff --git a/packages/tests/src/plugins/html-injection.ts b/packages/tests/src/plugins/html-injection.ts
new file mode 100644
index 000000000..269a45b98
--- /dev/null
+++ b/packages/tests/src/plugins/html-injection.ts
@@ -0,0 +1,73 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeHTMLRequest,
8 PeerTubeServer,
9 PluginsCommand,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12
13describe('Test plugins HTML injection', function () {
14 let server: PeerTubeServer = null
15 let command: PluginsCommand
16
17 before(async function () {
18 this.timeout(30000)
19
20 server = await createSingleServer(1)
21 await setAccessTokensToServers([ server ])
22
23 command = server.plugins
24 })
25
26 it('Should not inject global css file in HTML', async function () {
27 {
28 const text = await command.getCSS()
29 expect(text).to.be.empty
30 }
31
32 for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) {
33 const res = await makeHTMLRequest(server.url, path)
34 expect(res.text).to.not.include('link rel="stylesheet" href="/plugins/global.css')
35 }
36 })
37
38 it('Should install a plugin and a theme', async function () {
39 this.timeout(30000)
40
41 await command.install({ npmName: 'peertube-plugin-hello-world' })
42 })
43
44 it('Should have the correct global css', async function () {
45 {
46 const text = await command.getCSS()
47 expect(text).to.contain('background-color: red')
48 }
49
50 for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) {
51 const res = await makeHTMLRequest(server.url, path)
52 expect(res.text).to.include('link rel="stylesheet" href="/plugins/global.css')
53 }
54 })
55
56 it('Should have an empty global css on uninstall', async function () {
57 await command.uninstall({ npmName: 'peertube-plugin-hello-world' })
58
59 {
60 const text = await command.getCSS()
61 expect(text).to.be.empty
62 }
63
64 for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) {
65 const res = await makeHTMLRequest(server.url, path)
66 expect(res.text).to.not.include('link rel="stylesheet" href="/plugins/global.css')
67 }
68 })
69
70 after(async function () {
71 await cleanupTests([ server ])
72 })
73})
diff --git a/packages/tests/src/plugins/id-and-pass-auth.ts b/packages/tests/src/plugins/id-and-pass-auth.ts
new file mode 100644
index 000000000..a332f0eec
--- /dev/null
+++ b/packages/tests/src/plugins/id-and-pass-auth.ts
@@ -0,0 +1,248 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, UserRole } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 PeerTubeServer,
10 PluginsCommand,
11 setAccessTokensToServers
12} from '@peertube/peertube-server-commands'
13
14describe('Test id and pass auth plugins', function () {
15 let server: PeerTubeServer
16
17 let crashAccessToken: string
18 let crashRefreshToken: string
19
20 let lagunaAccessToken: string
21 let lagunaRefreshToken: string
22 let lagunaId: number
23
24 before(async function () {
25 this.timeout(30000)
26
27 server = await createSingleServer(1)
28 await setAccessTokensToServers([ server ])
29
30 for (const suffix of [ 'one', 'two', 'three' ]) {
31 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-id-pass-auth-' + suffix) })
32 }
33 })
34
35 it('Should display the correct configuration', async function () {
36 const config = await server.config.getConfig()
37
38 const auths = config.plugin.registeredIdAndPassAuths
39 expect(auths).to.have.lengthOf(8)
40
41 const crashAuth = auths.find(a => a.authName === 'crash-auth')
42 expect(crashAuth).to.exist
43 expect(crashAuth.npmName).to.equal('peertube-plugin-test-id-pass-auth-one')
44 expect(crashAuth.weight).to.equal(50)
45 })
46
47 it('Should not login', async function () {
48 await server.login.login({ user: { username: 'toto', password: 'password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
49 })
50
51 it('Should login Spyro, create the user and use the token', async function () {
52 const accessToken = await server.login.getAccessToken({ username: 'spyro', password: 'spyro password' })
53
54 const body = await server.users.getMyInfo({ token: accessToken })
55
56 expect(body.username).to.equal('spyro')
57 expect(body.account.displayName).to.equal('Spyro the Dragon')
58 expect(body.role.id).to.equal(UserRole.USER)
59 })
60
61 it('Should login Crash, create the user and use the token', async function () {
62 {
63 const body = await server.login.login({ user: { username: 'crash', password: 'crash password' } })
64 crashAccessToken = body.access_token
65 crashRefreshToken = body.refresh_token
66 }
67
68 {
69 const body = await server.users.getMyInfo({ token: crashAccessToken })
70
71 expect(body.username).to.equal('crash')
72 expect(body.account.displayName).to.equal('Crash Bandicoot')
73 expect(body.role.id).to.equal(UserRole.MODERATOR)
74 }
75 })
76
77 it('Should login the first Laguna, create the user and use the token', async function () {
78 {
79 const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } })
80 lagunaAccessToken = body.access_token
81 lagunaRefreshToken = body.refresh_token
82 }
83
84 {
85 const body = await server.users.getMyInfo({ token: lagunaAccessToken })
86
87 expect(body.username).to.equal('laguna')
88 expect(body.account.displayName).to.equal('Laguna Loire')
89 expect(body.role.id).to.equal(UserRole.USER)
90
91 lagunaId = body.id
92 }
93 })
94
95 it('Should refresh crash token, but not laguna token', async function () {
96 {
97 const resRefresh = await server.login.refreshToken({ refreshToken: crashRefreshToken })
98 crashAccessToken = resRefresh.body.access_token
99 crashRefreshToken = resRefresh.body.refresh_token
100
101 const body = await server.users.getMyInfo({ token: crashAccessToken })
102 expect(body.username).to.equal('crash')
103 }
104
105 {
106 await server.login.refreshToken({ refreshToken: lagunaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
107 }
108 })
109
110 it('Should update Crash profile', async function () {
111 await server.users.updateMe({
112 token: crashAccessToken,
113 displayName: 'Beautiful Crash',
114 description: 'Mutant eastern barred bandicoot'
115 })
116
117 const body = await server.users.getMyInfo({ token: crashAccessToken })
118
119 expect(body.account.displayName).to.equal('Beautiful Crash')
120 expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
121 })
122
123 it('Should logout Crash', async function () {
124 await server.login.logout({ token: crashAccessToken })
125 })
126
127 it('Should have logged out Crash', async function () {
128 await server.servers.waitUntilLog('On logout for auth 1 - 2')
129
130 await server.users.getMyInfo({ token: crashAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
131 })
132
133 it('Should login Crash and keep the old existing profile', async function () {
134 crashAccessToken = await server.login.getAccessToken({ username: 'crash', password: 'crash password' })
135
136 const body = await server.users.getMyInfo({ token: crashAccessToken })
137
138 expect(body.username).to.equal('crash')
139 expect(body.account.displayName).to.equal('Beautiful Crash')
140 expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
141 expect(body.role.id).to.equal(UserRole.MODERATOR)
142 })
143
144 it('Should login Laguna and update the profile', async function () {
145 {
146 await server.users.update({ userId: lagunaId, videoQuota: 43000, videoQuotaDaily: 43100 })
147 await server.users.updateMe({ token: lagunaAccessToken, displayName: 'laguna updated' })
148
149 const body = await server.users.getMyInfo({ token: lagunaAccessToken })
150 expect(body.username).to.equal('laguna')
151 expect(body.account.displayName).to.equal('laguna updated')
152 expect(body.videoQuota).to.equal(43000)
153 expect(body.videoQuotaDaily).to.equal(43100)
154 }
155
156 {
157 const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } })
158 lagunaAccessToken = body.access_token
159 lagunaRefreshToken = body.refresh_token
160 }
161
162 {
163 const body = await server.users.getMyInfo({ token: lagunaAccessToken })
164 expect(body.username).to.equal('laguna')
165 expect(body.account.displayName).to.equal('Laguna Loire')
166 expect(body.videoQuota).to.equal(42000)
167 expect(body.videoQuotaDaily).to.equal(43100)
168 }
169 })
170
171 it('Should reject token of laguna by the plugin hook', async function () {
172 await wait(5000)
173
174 await server.users.getMyInfo({ token: lagunaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
175 })
176
177 it('Should reject an invalid username, email, role or display name', async function () {
178 const command = server.login
179
180 await command.login({ user: { username: 'ward', password: 'ward password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
181 await server.servers.waitUntilLog('valid username')
182
183 await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
184 await server.servers.waitUntilLog('valid displayName')
185
186 await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
187 await server.servers.waitUntilLog('valid role')
188
189 await command.login({ user: { username: 'ellone', password: 'elonne password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
190 await server.servers.waitUntilLog('valid email')
191 })
192
193 it('Should unregister spyro-auth and do not login existing Spyro', async function () {
194 await server.plugins.updateSettings({
195 npmName: 'peertube-plugin-test-id-pass-auth-one',
196 settings: { disableSpyro: true }
197 })
198
199 const command = server.login
200 await command.login({ user: { username: 'spyro', password: 'spyro password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
201 await command.login({ user: { username: 'spyro', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
202 })
203
204 it('Should have disabled this auth', async function () {
205 const config = await server.config.getConfig()
206
207 const auths = config.plugin.registeredIdAndPassAuths
208 expect(auths).to.have.lengthOf(7)
209
210 const spyroAuth = auths.find(a => a.authName === 'spyro-auth')
211 expect(spyroAuth).to.not.exist
212 })
213
214 it('Should uninstall the plugin one and do not login existing Crash', async function () {
215 await server.plugins.uninstall({ npmName: 'peertube-plugin-test-id-pass-auth-one' })
216
217 await server.login.login({
218 user: { username: 'crash', password: 'crash password' },
219 expectedStatus: HttpStatusCode.BAD_REQUEST_400
220 })
221 })
222
223 it('Should display the correct configuration', async function () {
224 const config = await server.config.getConfig()
225
226 const auths = config.plugin.registeredIdAndPassAuths
227 expect(auths).to.have.lengthOf(6)
228
229 const crashAuth = auths.find(a => a.authName === 'crash-auth')
230 expect(crashAuth).to.not.exist
231 })
232
233 it('Should display plugin auth information in users list', async function () {
234 const { data } = await server.users.list()
235
236 const root = data.find(u => u.username === 'root')
237 const crash = data.find(u => u.username === 'crash')
238 const laguna = data.find(u => u.username === 'laguna')
239
240 expect(root.pluginAuth).to.be.null
241 expect(crash.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-one')
242 expect(laguna.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-two')
243 })
244
245 after(async function () {
246 await cleanupTests([ server ])
247 })
248})
diff --git a/packages/tests/src/plugins/index.ts b/packages/tests/src/plugins/index.ts
new file mode 100644
index 000000000..210af7236
--- /dev/null
+++ b/packages/tests/src/plugins/index.ts
@@ -0,0 +1,13 @@
1import './action-hooks'
2import './external-auth'
3import './filter-hooks'
4import './html-injection'
5import './id-and-pass-auth'
6import './plugin-helpers'
7import './plugin-router'
8import './plugin-storage'
9import './plugin-transcoding'
10import './plugin-unloading'
11import './plugin-websocket'
12import './translations'
13import './video-constants'
diff --git a/packages/tests/src/plugins/plugin-helpers.ts b/packages/tests/src/plugins/plugin-helpers.ts
new file mode 100644
index 000000000..d2bd8596e
--- /dev/null
+++ b/packages/tests/src/plugins/plugin-helpers.ts
@@ -0,0 +1,383 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists } from 'fs-extra/esm'
5import { HttpStatusCode, ThumbnailType } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 makeGetRequest,
11 makePostBodyRequest,
12 makeRawRequest,
13 PeerTubeServer,
14 PluginsCommand,
15 setAccessTokensToServers,
16 waitJobs
17} from '@peertube/peertube-server-commands'
18import { checkVideoFilesWereRemoved } from '@tests/shared/videos.js'
19
20function postCommand (server: PeerTubeServer, command: string, bodyArg?: object) {
21 const body = { command }
22 if (bodyArg) Object.assign(body, bodyArg)
23
24 return makePostBodyRequest({
25 url: server.url,
26 path: '/plugins/test-four/router/commander',
27 fields: body,
28 expectedStatus: HttpStatusCode.NO_CONTENT_204
29 })
30}
31
32describe('Test plugin helpers', function () {
33 let servers: PeerTubeServer[]
34
35 before(async function () {
36 this.timeout(60000)
37
38 servers = await createMultipleServers(2)
39 await setAccessTokensToServers(servers)
40
41 await doubleFollow(servers[0], servers[1])
42
43 await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-four') })
44 })
45
46 describe('Logger', function () {
47
48 it('Should have logged things', async function () {
49 await servers[0].servers.waitUntilLog(servers[0].host + ' peertube-plugin-test-four', 1, false)
50 await servers[0].servers.waitUntilLog('Hello world from plugin four', 1)
51 })
52 })
53
54 describe('Database', function () {
55
56 it('Should have made a query', async function () {
57 await servers[0].servers.waitUntilLog(`root email is admin${servers[0].internalServerNumber}@example.com`)
58 })
59 })
60
61 describe('Config', function () {
62
63 it('Should have the correct webserver url', async function () {
64 await servers[0].servers.waitUntilLog(`server url is ${servers[0].url}`)
65 })
66
67 it('Should have the correct listening config', async function () {
68 const res = await makeGetRequest({
69 url: servers[0].url,
70 path: '/plugins/test-four/router/server-listening-config',
71 expectedStatus: HttpStatusCode.OK_200
72 })
73
74 expect(res.body.config).to.exist
75 expect(res.body.config.hostname).to.equal('::')
76 expect(res.body.config.port).to.equal(servers[0].port)
77 })
78
79 it('Should have the correct config', async function () {
80 const res = await makeGetRequest({
81 url: servers[0].url,
82 path: '/plugins/test-four/router/server-config',
83 expectedStatus: HttpStatusCode.OK_200
84 })
85
86 expect(res.body.serverConfig).to.exist
87 expect(res.body.serverConfig.instance.name).to.equal('PeerTube')
88 })
89 })
90
91 describe('Server', function () {
92
93 it('Should get the server actor', async function () {
94 await servers[0].servers.waitUntilLog('server actor name is peertube')
95 })
96 })
97
98 describe('Socket', function () {
99
100 it('Should sendNotification without any exceptions', async () => {
101 const user = await servers[0].users.create({ username: 'notis_redding', password: 'secret1234?' })
102 await makePostBodyRequest({
103 url: servers[0].url,
104 path: '/plugins/test-four/router/send-notification',
105 fields: {
106 userId: user.id
107 },
108 expectedStatus: HttpStatusCode.CREATED_201
109 })
110 })
111
112 it('Should sendVideoLiveNewState without any exceptions', async () => {
113 const res = await servers[0].videos.quickUpload({ name: 'video server 1' })
114
115 await makePostBodyRequest({
116 url: servers[0].url,
117 path: '/plugins/test-four/router/send-video-live-new-state/' + res.uuid,
118 expectedStatus: HttpStatusCode.CREATED_201
119 })
120
121 await servers[0].videos.remove({ id: res.uuid })
122 })
123 })
124
125 describe('Plugin', function () {
126
127 it('Should get the base static route', async function () {
128 const res = await makeGetRequest({
129 url: servers[0].url,
130 path: '/plugins/test-four/router/static-route',
131 expectedStatus: HttpStatusCode.OK_200
132 })
133
134 expect(res.body.staticRoute).to.equal('/plugins/test-four/0.0.1/static/')
135 })
136
137 it('Should get the base static route', async function () {
138 const baseRouter = '/plugins/test-four/0.0.1/router/'
139
140 const res = await makeGetRequest({
141 url: servers[0].url,
142 path: baseRouter + 'router-route',
143 expectedStatus: HttpStatusCode.OK_200
144 })
145
146 expect(res.body.routerRoute).to.equal(baseRouter)
147 })
148 })
149
150 describe('User', function () {
151 let rootId: number
152
153 it('Should not get a user if not authenticated', async function () {
154 await makeGetRequest({
155 url: servers[0].url,
156 path: '/plugins/test-four/router/user',
157 expectedStatus: HttpStatusCode.NOT_FOUND_404
158 })
159 })
160
161 it('Should get a user if authenticated', async function () {
162 const res = await makeGetRequest({
163 url: servers[0].url,
164 token: servers[0].accessToken,
165 path: '/plugins/test-four/router/user',
166 expectedStatus: HttpStatusCode.OK_200
167 })
168
169 expect(res.body.username).to.equal('root')
170 expect(res.body.displayName).to.equal('root')
171 expect(res.body.isAdmin).to.be.true
172 expect(res.body.isModerator).to.be.false
173 expect(res.body.isUser).to.be.false
174
175 rootId = res.body.id
176 })
177
178 it('Should load a user by id', async function () {
179 {
180 const res = await makeGetRequest({
181 url: servers[0].url,
182 path: '/plugins/test-four/router/user/' + rootId,
183 expectedStatus: HttpStatusCode.OK_200
184 })
185
186 expect(res.body.username).to.equal('root')
187 }
188
189 {
190 await makeGetRequest({
191 url: servers[0].url,
192 path: '/plugins/test-four/router/user/42',
193 expectedStatus: HttpStatusCode.NOT_FOUND_404
194 })
195 }
196 })
197 })
198
199 describe('Moderation', function () {
200 let videoUUIDServer1: string
201
202 before(async function () {
203 this.timeout(60000)
204
205 {
206 const res = await servers[0].videos.quickUpload({ name: 'video server 1' })
207 videoUUIDServer1 = res.uuid
208 }
209
210 {
211 await servers[1].videos.quickUpload({ name: 'video server 2' })
212 }
213
214 await waitJobs(servers)
215
216 const { data } = await servers[0].videos.list()
217
218 expect(data).to.have.lengthOf(2)
219 })
220
221 it('Should mute server 2', async function () {
222 await postCommand(servers[0], 'blockServer', { hostToBlock: servers[1].host })
223
224 const { data } = await servers[0].videos.list()
225
226 expect(data).to.have.lengthOf(1)
227 expect(data[0].name).to.equal('video server 1')
228 })
229
230 it('Should unmute server 2', async function () {
231 await postCommand(servers[0], 'unblockServer', { hostToUnblock: servers[1].host })
232
233 const { data } = await servers[0].videos.list()
234
235 expect(data).to.have.lengthOf(2)
236 })
237
238 it('Should mute account of server 2', async function () {
239 await postCommand(servers[0], 'blockAccount', { handleToBlock: `root@${servers[1].host}` })
240
241 const { data } = await servers[0].videos.list()
242
243 expect(data).to.have.lengthOf(1)
244 expect(data[0].name).to.equal('video server 1')
245 })
246
247 it('Should unmute account of server 2', async function () {
248 await postCommand(servers[0], 'unblockAccount', { handleToUnblock: `root@${servers[1].host}` })
249
250 const { data } = await servers[0].videos.list()
251
252 expect(data).to.have.lengthOf(2)
253 })
254
255 it('Should blacklist video', async function () {
256 await postCommand(servers[0], 'blacklist', { videoUUID: videoUUIDServer1, unfederate: true })
257
258 await waitJobs(servers)
259
260 for (const server of servers) {
261 const { data } = await server.videos.list()
262
263 expect(data).to.have.lengthOf(1)
264 expect(data[0].name).to.equal('video server 2')
265 }
266 })
267
268 it('Should unblacklist video', async function () {
269 await postCommand(servers[0], 'unblacklist', { videoUUID: videoUUIDServer1 })
270
271 await waitJobs(servers)
272
273 for (const server of servers) {
274 const { data } = await server.videos.list()
275
276 expect(data).to.have.lengthOf(2)
277 }
278 })
279 })
280
281 describe('Videos', function () {
282 let videoUUID: string
283 let videoPath: string
284
285 before(async function () {
286 this.timeout(240000)
287
288 await servers[0].config.enableTranscoding()
289
290 const res = await servers[0].videos.quickUpload({ name: 'video1' })
291 videoUUID = res.uuid
292
293 await waitJobs(servers)
294 })
295
296 it('Should get video files', async function () {
297 const { body } = await makeGetRequest({
298 url: servers[0].url,
299 path: '/plugins/test-four/router/video-files/' + videoUUID,
300 expectedStatus: HttpStatusCode.OK_200
301 })
302
303 // Video files check
304 {
305 expect(body.webVideo.videoFiles).to.be.an('array')
306 expect(body.hls.videoFiles).to.be.an('array')
307
308 for (const resolution of [ 144, 240, 360, 480, 720 ]) {
309 for (const files of [ body.webVideo.videoFiles, body.hls.videoFiles ]) {
310 const file = files.find(f => f.resolution === resolution)
311 expect(file).to.exist
312
313 expect(file.size).to.be.a('number')
314 expect(file.fps).to.equal(25)
315
316 expect(await pathExists(file.path)).to.be.true
317 await makeRawRequest({ url: file.url, expectedStatus: HttpStatusCode.OK_200 })
318 }
319 }
320
321 videoPath = body.webVideo.videoFiles[0].path
322 }
323
324 // Thumbnails check
325 {
326 expect(body.thumbnails).to.be.an('array')
327
328 const miniature = body.thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
329 expect(miniature).to.exist
330 expect(await pathExists(miniature.path)).to.be.true
331 await makeRawRequest({ url: miniature.url, expectedStatus: HttpStatusCode.OK_200 })
332
333 const preview = body.thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
334 expect(preview).to.exist
335 expect(await pathExists(preview.path)).to.be.true
336 await makeRawRequest({ url: preview.url, expectedStatus: HttpStatusCode.OK_200 })
337 }
338 })
339
340 it('Should probe a file', async function () {
341 const { body } = await makeGetRequest({
342 url: servers[0].url,
343 path: '/plugins/test-four/router/ffprobe',
344 query: {
345 path: videoPath
346 },
347 expectedStatus: HttpStatusCode.OK_200
348 })
349
350 expect(body.streams).to.be.an('array')
351 expect(body.streams).to.have.lengthOf(2)
352 })
353
354 it('Should remove a video after a view', async function () {
355 this.timeout(40000)
356
357 // Should not throw -> video exists
358 const video = await servers[0].videos.get({ id: videoUUID })
359 // Should delete the video
360 await servers[0].views.simulateView({ id: videoUUID })
361
362 await servers[0].servers.waitUntilLog('Video deleted by plugin four.')
363
364 try {
365 // Should throw because the video should have been deleted
366 await servers[0].videos.get({ id: videoUUID })
367 throw new Error('Video exists')
368 } catch (err) {
369 if (err.message.includes('exists')) throw err
370 }
371
372 await checkVideoFilesWereRemoved({ server: servers[0], video })
373 })
374
375 it('Should have fetched the video by URL', async function () {
376 await servers[0].servers.waitUntilLog(`video from DB uuid is ${videoUUID}`)
377 })
378 })
379
380 after(async function () {
381 await cleanupTests(servers)
382 })
383})
diff --git a/packages/tests/src/plugins/plugin-router.ts b/packages/tests/src/plugins/plugin-router.ts
new file mode 100644
index 000000000..6f3571c05
--- /dev/null
+++ b/packages/tests/src/plugins/plugin-router.ts
@@ -0,0 +1,105 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeGetRequest,
8 makePostBodyRequest,
9 PeerTubeServer,
10 PluginsCommand,
11 setAccessTokensToServers
12} from '@peertube/peertube-server-commands'
13import { HttpStatusCode } from '@peertube/peertube-models'
14
15describe('Test plugin helpers', function () {
16 let server: PeerTubeServer
17 const basePaths = [
18 '/plugins/test-five/router/',
19 '/plugins/test-five/0.0.1/router/'
20 ]
21
22 before(async function () {
23 this.timeout(30000)
24
25 server = await createSingleServer(1)
26 await setAccessTokensToServers([ server ])
27
28 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-five') })
29 })
30
31 it('Should answer "pong"', async function () {
32 for (const path of basePaths) {
33 const res = await makeGetRequest({
34 url: server.url,
35 path: path + 'ping',
36 expectedStatus: HttpStatusCode.OK_200
37 })
38
39 expect(res.body.message).to.equal('pong')
40 }
41 })
42
43 it('Should check if authenticated', async function () {
44 for (const path of basePaths) {
45 const res = await makeGetRequest({
46 url: server.url,
47 path: path + 'is-authenticated',
48 token: server.accessToken,
49 expectedStatus: 200
50 })
51
52 expect(res.body.isAuthenticated).to.equal(true)
53
54 const secRes = await makeGetRequest({
55 url: server.url,
56 path: path + 'is-authenticated',
57 expectedStatus: 200
58 })
59
60 expect(secRes.body.isAuthenticated).to.equal(false)
61 }
62 })
63
64 it('Should mirror post body', async function () {
65 const body = {
66 hello: 'world',
67 riri: 'fifi',
68 loulou: 'picsou'
69 }
70
71 for (const path of basePaths) {
72 const res = await makePostBodyRequest({
73 url: server.url,
74 path: path + 'form/post/mirror',
75 fields: body,
76 expectedStatus: HttpStatusCode.OK_200
77 })
78
79 expect(res.body).to.deep.equal(body)
80 }
81 })
82
83 it('Should remove the plugin and remove the routes', async function () {
84 await server.plugins.uninstall({ npmName: 'peertube-plugin-test-five' })
85
86 for (const path of basePaths) {
87 await makeGetRequest({
88 url: server.url,
89 path: path + 'ping',
90 expectedStatus: HttpStatusCode.NOT_FOUND_404
91 })
92
93 await makePostBodyRequest({
94 url: server.url,
95 path: path + 'ping',
96 fields: {},
97 expectedStatus: HttpStatusCode.NOT_FOUND_404
98 })
99 }
100 })
101
102 after(async function () {
103 await cleanupTests([ server ])
104 })
105})
diff --git a/packages/tests/src/plugins/plugin-storage.ts b/packages/tests/src/plugins/plugin-storage.ts
new file mode 100644
index 000000000..f9b0ead0c
--- /dev/null
+++ b/packages/tests/src/plugins/plugin-storage.ts
@@ -0,0 +1,95 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists } from 'fs-extra/esm'
5import { readdir, readFile } from 'fs/promises'
6import { join } from 'path'
7import { HttpStatusCode } from '@peertube/peertube-models'
8import {
9 cleanupTests,
10 createSingleServer,
11 makeGetRequest,
12 PeerTubeServer,
13 PluginsCommand,
14 setAccessTokensToServers
15} from '@peertube/peertube-server-commands'
16
17describe('Test plugin storage', function () {
18 let server: PeerTubeServer
19
20 before(async function () {
21 this.timeout(30000)
22
23 server = await createSingleServer(1)
24 await setAccessTokensToServers([ server ])
25
26 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') })
27 })
28
29 describe('DB storage', function () {
30 it('Should correctly store a subkey', async function () {
31 await server.servers.waitUntilLog('superkey stored value is toto')
32 })
33
34 it('Should correctly retrieve an array as array from the storage.', async function () {
35 await server.servers.waitUntilLog('storedArrayKey isArray is true')
36 await server.servers.waitUntilLog('storedArrayKey stored value is toto, toto2')
37 })
38 })
39
40 describe('Disk storage', function () {
41 let dataPath: string
42 let pluginDataPath: string
43
44 async function getFileContent () {
45 const files = await readdir(pluginDataPath)
46 expect(files).to.have.lengthOf(1)
47
48 return readFile(join(pluginDataPath, files[0]), 'utf8')
49 }
50
51 before(function () {
52 dataPath = server.servers.buildDirectory('plugins/data')
53 pluginDataPath = join(dataPath, 'peertube-plugin-test-six')
54 })
55
56 it('Should have created the directory on install', async function () {
57 const dataPath = server.servers.buildDirectory('plugins/data')
58 const pluginDataPath = join(dataPath, 'peertube-plugin-test-six')
59
60 expect(await pathExists(dataPath)).to.be.true
61 expect(await pathExists(pluginDataPath)).to.be.true
62 expect(await readdir(pluginDataPath)).to.have.lengthOf(0)
63 })
64
65 it('Should have created a file', async function () {
66 await makeGetRequest({
67 url: server.url,
68 token: server.accessToken,
69 path: '/plugins/test-six/router/create-file',
70 expectedStatus: HttpStatusCode.OK_200
71 })
72
73 const content = await getFileContent()
74 expect(content).to.equal('Prince Ali')
75 })
76
77 it('Should still have the file after an uninstallation', async function () {
78 await server.plugins.uninstall({ npmName: 'peertube-plugin-test-six' })
79
80 const content = await getFileContent()
81 expect(content).to.equal('Prince Ali')
82 })
83
84 it('Should still have the file after the reinstallation', async function () {
85 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') })
86
87 const content = await getFileContent()
88 expect(content).to.equal('Prince Ali')
89 })
90 })
91
92 after(async function () {
93 await cleanupTests([ server ])
94 })
95})
diff --git a/packages/tests/src/plugins/plugin-transcoding.ts b/packages/tests/src/plugins/plugin-transcoding.ts
new file mode 100644
index 000000000..2f50f65ff
--- /dev/null
+++ b/packages/tests/src/plugins/plugin-transcoding.ts
@@ -0,0 +1,279 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { getAudioStream, getVideoStream, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
5import { VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 PeerTubeServer,
10 PluginsCommand,
11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 testFfmpegStreamError,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17async function createLiveWrapper (server: PeerTubeServer) {
18 const liveAttributes = {
19 name: 'live video',
20 channelId: server.store.channel.id,
21 privacy: VideoPrivacy.PUBLIC
22 }
23
24 const { uuid } = await server.live.create({ fields: liveAttributes })
25
26 return uuid
27}
28
29function updateConf (server: PeerTubeServer, vodProfile: string, liveProfile: string) {
30 return server.config.updateCustomSubConfig({
31 newConfig: {
32 transcoding: {
33 enabled: true,
34 profile: vodProfile,
35 hls: {
36 enabled: true
37 },
38 webVideos: {
39 enabled: true
40 },
41 resolutions: {
42 '240p': true,
43 '360p': false,
44 '480p': false,
45 '720p': true
46 }
47 },
48 live: {
49 transcoding: {
50 profile: liveProfile,
51 enabled: true,
52 resolutions: {
53 '240p': true,
54 '360p': false,
55 '480p': false,
56 '720p': true
57 }
58 }
59 }
60 }
61 })
62}
63
64describe('Test transcoding plugins', function () {
65 let server: PeerTubeServer
66
67 before(async function () {
68 this.timeout(60000)
69
70 server = await createSingleServer(1)
71 await setAccessTokensToServers([ server ])
72 await setDefaultVideoChannel([ server ])
73
74 await updateConf(server, 'default', 'default')
75 })
76
77 describe('When using a plugin adding profiles to existing encoders', function () {
78
79 async function checkVideoFPS (uuid: string, type: 'above' | 'below', fps: number) {
80 const video = await server.videos.get({ id: uuid })
81 const files = video.files.concat(...video.streamingPlaylists.map(p => p.files))
82
83 for (const file of files) {
84 if (type === 'above') {
85 expect(file.fps).to.be.above(fps)
86 } else {
87 expect(file.fps).to.be.below(fps)
88 }
89 }
90 }
91
92 async function checkLiveFPS (uuid: string, type: 'above' | 'below', fps: number) {
93 const playlistUrl = `${server.url}/static/streaming-playlists/hls/${uuid}/0.m3u8`
94 const videoFPS = await getVideoStreamFPS(playlistUrl)
95
96 if (type === 'above') {
97 expect(videoFPS).to.be.above(fps)
98 } else {
99 expect(videoFPS).to.be.below(fps)
100 }
101 }
102
103 before(async function () {
104 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-one') })
105 })
106
107 it('Should have the appropriate available profiles', async function () {
108 const config = await server.config.getConfig()
109
110 expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ])
111 expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'high-live', 'input-options-live', 'bad-scale-live' ])
112 })
113
114 describe('VOD', function () {
115
116 it('Should not use the plugin profile if not chosen by the admin', async function () {
117 this.timeout(240000)
118
119 const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
120 await waitJobs([ server ])
121
122 await checkVideoFPS(videoUUID, 'above', 20)
123 })
124
125 it('Should use the vod profile', async function () {
126 this.timeout(240000)
127
128 await updateConf(server, 'low-vod', 'default')
129
130 const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
131 await waitJobs([ server ])
132
133 await checkVideoFPS(videoUUID, 'below', 12)
134 })
135
136 it('Should apply input options in vod profile', async function () {
137 this.timeout(240000)
138
139 await updateConf(server, 'input-options-vod', 'default')
140
141 const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
142 await waitJobs([ server ])
143
144 await checkVideoFPS(videoUUID, 'below', 6)
145 })
146
147 it('Should apply the scale filter in vod profile', async function () {
148 this.timeout(240000)
149
150 await updateConf(server, 'bad-scale-vod', 'default')
151
152 const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid
153 await waitJobs([ server ])
154
155 // Transcoding failed
156 const video = await server.videos.get({ id: videoUUID })
157 expect(video.files).to.have.lengthOf(1)
158 expect(video.streamingPlaylists).to.have.lengthOf(0)
159 })
160 })
161
162 describe('Live', function () {
163
164 it('Should not use the plugin profile if not chosen by the admin', async function () {
165 this.timeout(240000)
166
167 const liveVideoId = await createLiveWrapper(server)
168
169 await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
170 await server.live.waitUntilPublished({ videoId: liveVideoId })
171 await waitJobs([ server ])
172
173 await checkLiveFPS(liveVideoId, 'above', 20)
174 })
175
176 it('Should use the live profile', async function () {
177 this.timeout(240000)
178
179 await updateConf(server, 'low-vod', 'high-live')
180
181 const liveVideoId = await createLiveWrapper(server)
182
183 await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
184 await server.live.waitUntilPublished({ videoId: liveVideoId })
185 await waitJobs([ server ])
186
187 await checkLiveFPS(liveVideoId, 'above', 45)
188 })
189
190 it('Should apply the input options on live profile', async function () {
191 this.timeout(240000)
192
193 await updateConf(server, 'low-vod', 'input-options-live')
194
195 const liveVideoId = await createLiveWrapper(server)
196
197 await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
198 await server.live.waitUntilPublished({ videoId: liveVideoId })
199 await waitJobs([ server ])
200
201 await checkLiveFPS(liveVideoId, 'above', 45)
202 })
203
204 it('Should apply the scale filter name on live profile', async function () {
205 this.timeout(240000)
206
207 await updateConf(server, 'low-vod', 'bad-scale-live')
208
209 const liveVideoId = await createLiveWrapper(server)
210
211 const command = await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' })
212 await testFfmpegStreamError(command, true)
213 })
214
215 it('Should default to the default profile if the specified profile does not exist', async function () {
216 this.timeout(240000)
217
218 await server.plugins.uninstall({ npmName: 'peertube-plugin-test-transcoding-one' })
219
220 const config = await server.config.getConfig()
221
222 expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ])
223 expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ])
224
225 const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid
226 await waitJobs([ server ])
227
228 await checkVideoFPS(videoUUID, 'above', 20)
229 })
230 })
231
232 })
233
234 describe('When using a plugin adding new encoders', function () {
235
236 before(async function () {
237 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-two') })
238
239 await updateConf(server, 'test-vod-profile', 'test-live-profile')
240 })
241
242 it('Should use the new vod encoders', async function () {
243 this.timeout(240000)
244
245 const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid
246 await waitJobs([ server ])
247
248 const video = await server.videos.get({ id: videoUUID })
249
250 const path = server.servers.buildWebVideoFilePath(video.files[0].fileUrl)
251 const audioProbe = await getAudioStream(path)
252 expect(audioProbe.audioStream.codec_name).to.equal('opus')
253
254 const videoProbe = await getVideoStream(path)
255 expect(videoProbe.codec_name).to.equal('vp9')
256 })
257
258 it('Should use the new live encoders', async function () {
259 this.timeout(240000)
260
261 const liveVideoId = await createLiveWrapper(server)
262
263 await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
264 await server.live.waitUntilPublished({ videoId: liveVideoId })
265 await waitJobs([ server ])
266
267 const playlistUrl = `${server.url}/static/streaming-playlists/hls/${liveVideoId}/0.m3u8`
268 const audioProbe = await getAudioStream(playlistUrl)
269 expect(audioProbe.audioStream.codec_name).to.equal('opus')
270
271 const videoProbe = await getVideoStream(playlistUrl)
272 expect(videoProbe.codec_name).to.equal('h264')
273 })
274 })
275
276 after(async function () {
277 await cleanupTests([ server ])
278 })
279})
diff --git a/packages/tests/src/plugins/plugin-unloading.ts b/packages/tests/src/plugins/plugin-unloading.ts
new file mode 100644
index 000000000..70310bc8c
--- /dev/null
+++ b/packages/tests/src/plugins/plugin-unloading.ts
@@ -0,0 +1,75 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeGetRequest,
8 PeerTubeServer,
9 PluginsCommand,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12import { HttpStatusCode } from '@peertube/peertube-models'
13
14describe('Test plugins module unloading', function () {
15 let server: PeerTubeServer = null
16 const requestPath = '/plugins/test-unloading/router/get'
17 let value: string = null
18
19 before(async function () {
20 this.timeout(30000)
21
22 server = await createSingleServer(1)
23 await setAccessTokensToServers([ server ])
24
25 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') })
26 })
27
28 it('Should return a numeric value', async function () {
29 const res = await makeGetRequest({
30 url: server.url,
31 path: requestPath,
32 expectedStatus: HttpStatusCode.OK_200
33 })
34
35 expect(res.body.message).to.match(/^\d+$/)
36 value = res.body.message
37 })
38
39 it('Should return the same value the second time', async function () {
40 const res = await makeGetRequest({
41 url: server.url,
42 path: requestPath,
43 expectedStatus: HttpStatusCode.OK_200
44 })
45
46 expect(res.body.message).to.be.equal(value)
47 })
48
49 it('Should uninstall the plugin and free the route', async function () {
50 await server.plugins.uninstall({ npmName: 'peertube-plugin-test-unloading' })
51
52 await makeGetRequest({
53 url: server.url,
54 path: requestPath,
55 expectedStatus: HttpStatusCode.NOT_FOUND_404
56 })
57 })
58
59 it('Should return a different numeric value', async function () {
60 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') })
61
62 const res = await makeGetRequest({
63 url: server.url,
64 path: requestPath,
65 expectedStatus: HttpStatusCode.OK_200
66 })
67
68 expect(res.body.message).to.match(/^\d+$/)
69 expect(res.body.message).to.be.not.equal(value)
70 })
71
72 after(async function () {
73 await cleanupTests([ server ])
74 })
75})
diff --git a/packages/tests/src/plugins/plugin-websocket.ts b/packages/tests/src/plugins/plugin-websocket.ts
new file mode 100644
index 000000000..832dcebd0
--- /dev/null
+++ b/packages/tests/src/plugins/plugin-websocket.ts
@@ -0,0 +1,76 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import WebSocket from 'ws'
4import {
5 cleanupTests,
6 createSingleServer,
7 PeerTubeServer,
8 PluginsCommand,
9 setAccessTokensToServers
10} from '@peertube/peertube-server-commands'
11
12function buildWebSocket (server: PeerTubeServer, path: string) {
13 return new WebSocket('ws://' + server.host + path)
14}
15
16function expectErrorOrTimeout (server: PeerTubeServer, path: string, expectedTimeout: number) {
17 return new Promise<void>((res, rej) => {
18 const ws = buildWebSocket(server, path)
19 ws.on('error', () => res())
20
21 const timeout = setTimeout(() => res(), expectedTimeout)
22
23 ws.on('open', () => {
24 clearTimeout(timeout)
25
26 return rej(new Error('Connect did not timeout'))
27 })
28 })
29}
30
31describe('Test plugin websocket', function () {
32 let server: PeerTubeServer
33 const basePaths = [
34 '/plugins/test-websocket/ws/',
35 '/plugins/test-websocket/0.0.1/ws/'
36 ]
37
38 before(async function () {
39 this.timeout(30000)
40
41 server = await createSingleServer(1)
42 await setAccessTokensToServers([ server ])
43
44 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-websocket') })
45 })
46
47 it('Should not connect to the websocket without the appropriate path', async function () {
48 const paths = [
49 '/plugins/unknown/ws/',
50 '/plugins/unknown/0.0.1/ws/'
51 ]
52
53 for (const path of paths) {
54 await expectErrorOrTimeout(server, path, 1000)
55 }
56 })
57
58 it('Should not connect to the websocket without the appropriate sub path', async function () {
59 for (const path of basePaths) {
60 await expectErrorOrTimeout(server, path + '/unknown', 1000)
61 }
62 })
63
64 it('Should connect to the websocket and receive pong', function (done) {
65 const ws = buildWebSocket(server, basePaths[0])
66
67 ws.on('open', () => ws.send('ping'))
68 ws.on('message', data => {
69 if (data.toString() === 'pong') return done()
70 })
71 })
72
73 after(async function () {
74 await cleanupTests([ server ])
75 })
76})
diff --git a/packages/tests/src/plugins/translations.ts b/packages/tests/src/plugins/translations.ts
new file mode 100644
index 000000000..a69e14134
--- /dev/null
+++ b/packages/tests/src/plugins/translations.ts
@@ -0,0 +1,80 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createSingleServer,
7 PeerTubeServer,
8 PluginsCommand,
9 setAccessTokensToServers
10} from '@peertube/peertube-server-commands'
11
12describe('Test plugin translations', function () {
13 let server: PeerTubeServer
14 let command: PluginsCommand
15
16 before(async function () {
17 this.timeout(30000)
18
19 server = await createSingleServer(1)
20 await setAccessTokensToServers([ server ])
21
22 command = server.plugins
23
24 await command.install({ path: PluginsCommand.getPluginTestPath() })
25 await command.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') })
26 })
27
28 it('Should not have translations for locale pt', async function () {
29 const body = await command.getTranslations({ locale: 'pt' })
30
31 expect(body).to.deep.equal({})
32 })
33
34 it('Should have translations for locale fr', async function () {
35 const body = await command.getTranslations({ locale: 'fr-FR' })
36
37 expect(body).to.deep.equal({
38 'peertube-plugin-test': {
39 Hi: 'Coucou'
40 },
41 'peertube-plugin-test-filter-translations': {
42 'Hello world': 'Bonjour le monde'
43 }
44 })
45 })
46
47 it('Should have translations of locale it', async function () {
48 const body = await command.getTranslations({ locale: 'it-IT' })
49
50 expect(body).to.deep.equal({
51 'peertube-plugin-test-filter-translations': {
52 'Hello world': 'Ciao, mondo!'
53 }
54 })
55 })
56
57 it('Should remove the plugin and remove the locales', async function () {
58 await command.uninstall({ npmName: 'peertube-plugin-test-filter-translations' })
59
60 {
61 const body = await command.getTranslations({ locale: 'fr-FR' })
62
63 expect(body).to.deep.equal({
64 'peertube-plugin-test': {
65 Hi: 'Coucou'
66 }
67 })
68 }
69
70 {
71 const body = await command.getTranslations({ locale: 'it-IT' })
72
73 expect(body).to.deep.equal({})
74 }
75 })
76
77 after(async function () {
78 await cleanupTests([ server ])
79 })
80})
diff --git a/packages/tests/src/plugins/video-constants.ts b/packages/tests/src/plugins/video-constants.ts
new file mode 100644
index 000000000..b81240a64
--- /dev/null
+++ b/packages/tests/src/plugins/video-constants.ts
@@ -0,0 +1,180 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 cleanupTests,
6 createSingleServer,
7 makeGetRequest,
8 PeerTubeServer,
9 PluginsCommand,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
13
14describe('Test plugin altering video constants', function () {
15 let server: PeerTubeServer
16
17 before(async function () {
18 this.timeout(30000)
19
20 server = await createSingleServer(1)
21 await setAccessTokensToServers([ server ])
22
23 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') })
24 })
25
26 it('Should have updated languages', async function () {
27 const languages = await server.videos.getLanguages()
28
29 expect(languages['en']).to.not.exist
30 expect(languages['fr']).to.not.exist
31
32 expect(languages['al_bhed']).to.equal('Al Bhed')
33 expect(languages['al_bhed2']).to.equal('Al Bhed 2')
34 expect(languages['al_bhed3']).to.not.exist
35 })
36
37 it('Should have updated categories', async function () {
38 const categories = await server.videos.getCategories()
39
40 expect(categories[1]).to.not.exist
41 expect(categories[2]).to.not.exist
42
43 expect(categories[42]).to.equal('Best category')
44 expect(categories[43]).to.equal('High best category')
45 })
46
47 it('Should have updated licences', async function () {
48 const licences = await server.videos.getLicences()
49
50 expect(licences[1]).to.not.exist
51 expect(licences[7]).to.not.exist
52
53 expect(licences[42]).to.equal('Best licence')
54 expect(licences[43]).to.equal('High best licence')
55 })
56
57 it('Should have updated video privacies', async function () {
58 const privacies = await server.videos.getPrivacies()
59
60 expect(privacies[1]).to.exist
61 expect(privacies[2]).to.not.exist
62 expect(privacies[3]).to.exist
63 expect(privacies[4]).to.exist
64 })
65
66 it('Should have updated playlist privacies', async function () {
67 const playlistPrivacies = await server.playlists.getPrivacies()
68
69 expect(playlistPrivacies[1]).to.exist
70 expect(playlistPrivacies[2]).to.exist
71 expect(playlistPrivacies[3]).to.not.exist
72 })
73
74 it('Should not be able to create a video with this privacy', async function () {
75 const attributes = { name: 'video', privacy: VideoPrivacy.UNLISTED }
76 await server.videos.upload({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
77 })
78
79 it('Should not be able to create a video with this privacy', async function () {
80 const attributes = { displayName: 'video playlist', privacy: VideoPlaylistPrivacy.PRIVATE }
81 await server.playlists.create({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
82 })
83
84 it('Should be able to upload a video with these values', async function () {
85 const attributes = { name: 'video', category: 42, licence: 42, language: 'al_bhed2' }
86 const { uuid } = await server.videos.upload({ attributes })
87
88 const video = await server.videos.get({ id: uuid })
89 expect(video.language.label).to.equal('Al Bhed 2')
90 expect(video.licence.label).to.equal('Best licence')
91 expect(video.category.label).to.equal('Best category')
92 })
93
94 it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () {
95 await server.plugins.uninstall({ npmName: 'peertube-plugin-test-video-constants' })
96
97 {
98 const languages = await server.videos.getLanguages()
99
100 expect(languages['en']).to.equal('English')
101 expect(languages['fr']).to.equal('French')
102
103 expect(languages['al_bhed']).to.not.exist
104 expect(languages['al_bhed2']).to.not.exist
105 expect(languages['al_bhed3']).to.not.exist
106 }
107
108 {
109 const categories = await server.videos.getCategories()
110
111 expect(categories[1]).to.equal('Music')
112 expect(categories[2]).to.equal('Films')
113
114 expect(categories[42]).to.not.exist
115 expect(categories[43]).to.not.exist
116 }
117
118 {
119 const licences = await server.videos.getLicences()
120
121 expect(licences[1]).to.equal('Attribution')
122 expect(licences[7]).to.equal('Public Domain Dedication')
123
124 expect(licences[42]).to.not.exist
125 expect(licences[43]).to.not.exist
126 }
127
128 {
129 const privacies = await server.videos.getPrivacies()
130
131 expect(privacies[1]).to.exist
132 expect(privacies[2]).to.exist
133 expect(privacies[3]).to.exist
134 expect(privacies[4]).to.exist
135 }
136
137 {
138 const playlistPrivacies = await server.playlists.getPrivacies()
139
140 expect(playlistPrivacies[1]).to.exist
141 expect(playlistPrivacies[2]).to.exist
142 expect(playlistPrivacies[3]).to.exist
143 }
144 })
145
146 it('Should be able to reset categories', async function () {
147 await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') })
148
149 {
150 const categories = await server.videos.getCategories()
151
152 expect(categories[1]).to.not.exist
153 expect(categories[2]).to.not.exist
154
155 expect(categories[42]).to.exist
156 expect(categories[43]).to.exist
157 }
158
159 await makeGetRequest({
160 url: server.url,
161 token: server.accessToken,
162 path: '/plugins/test-video-constants/router/reset-categories',
163 expectedStatus: HttpStatusCode.NO_CONTENT_204
164 })
165
166 {
167 const categories = await server.videos.getCategories()
168
169 expect(categories[1]).to.exist
170 expect(categories[2]).to.exist
171
172 expect(categories[42]).to.not.exist
173 expect(categories[43]).to.not.exist
174 }
175 })
176
177 after(async function () {
178 await cleanupTests([ server ])
179 })
180})