aboutsummaryrefslogtreecommitdiffhomepage
path: root/packages/tests/src/api/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /packages/tests/src/api/server
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'packages/tests/src/api/server')
-rw-r--r--packages/tests/src/api/server/auto-follows.ts189
-rw-r--r--packages/tests/src/api/server/bulk.ts185
-rw-r--r--packages/tests/src/api/server/config-defaults.ts294
-rw-r--r--packages/tests/src/api/server/config.ts645
-rw-r--r--packages/tests/src/api/server/contact-form.ts101
-rw-r--r--packages/tests/src/api/server/email.ts371
-rw-r--r--packages/tests/src/api/server/follow-constraints.ts321
-rw-r--r--packages/tests/src/api/server/follows-moderation.ts364
-rw-r--r--packages/tests/src/api/server/follows.ts644
-rw-r--r--packages/tests/src/api/server/handle-down.ts339
-rw-r--r--packages/tests/src/api/server/homepage.ts81
-rw-r--r--packages/tests/src/api/server/index.ts22
-rw-r--r--packages/tests/src/api/server/jobs.ts128
-rw-r--r--packages/tests/src/api/server/logs.ts265
-rw-r--r--packages/tests/src/api/server/no-client.ts24
-rw-r--r--packages/tests/src/api/server/open-telemetry.ts193
-rw-r--r--packages/tests/src/api/server/plugins.ts410
-rw-r--r--packages/tests/src/api/server/proxy.ts173
-rw-r--r--packages/tests/src/api/server/reverse-proxy.ts156
-rw-r--r--packages/tests/src/api/server/services.ts143
-rw-r--r--packages/tests/src/api/server/slow-follows.ts85
-rw-r--r--packages/tests/src/api/server/stats.ts279
-rw-r--r--packages/tests/src/api/server/tracker.ts110
23 files changed, 5522 insertions, 0 deletions
diff --git a/packages/tests/src/api/server/auto-follows.ts b/packages/tests/src/api/server/auto-follows.ts
new file mode 100644
index 000000000..aa272ebcc
--- /dev/null
+++ b/packages/tests/src/api/server/auto-follows.ts
@@ -0,0 +1,189 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockInstancesIndex } from '@tests/shared/mock-servers/index.js'
5import { wait } from '@peertube/peertube-core-utils'
6import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands'
7
8async function checkFollow (follower: PeerTubeServer, following: PeerTubeServer, exists: boolean) {
9 {
10 const body = await following.follows.getFollowers({ start: 0, count: 5, sort: '-createdAt' })
11 const follow = body.data.find(f => f.follower.host === follower.host && f.state === 'accepted')
12
13 if (exists === true) expect(follow, `Follower ${follower.url} should exist on ${following.url}`).to.exist
14 else expect(follow, `Follower ${follower.url} should not exist on ${following.url}`).to.be.undefined
15 }
16
17 {
18 const body = await follower.follows.getFollowings({ start: 0, count: 5, sort: '-createdAt' })
19 const follow = body.data.find(f => f.following.host === following.host && f.state === 'accepted')
20
21 if (exists === true) expect(follow, `Following ${following.url} should exist on ${follower.url}`).to.exist
22 else expect(follow, `Following ${following.url} should not exist on ${follower.url}`).to.be.undefined
23 }
24}
25
26async function server1Follows2 (servers: PeerTubeServer[]) {
27 await servers[0].follows.follow({ hosts: [ servers[1].host ] })
28
29 await waitJobs(servers)
30}
31
32async function resetFollows (servers: PeerTubeServer[]) {
33 try {
34 await servers[0].follows.unfollow({ target: servers[1] })
35 await servers[1].follows.unfollow({ target: servers[0] })
36 } catch { /* empty */
37 }
38
39 await waitJobs(servers)
40
41 await checkFollow(servers[0], servers[1], false)
42 await checkFollow(servers[1], servers[0], false)
43}
44
45describe('Test auto follows', function () {
46 let servers: PeerTubeServer[] = []
47
48 before(async function () {
49 this.timeout(120000)
50
51 servers = await createMultipleServers(3)
52
53 // Get the access tokens
54 await setAccessTokensToServers(servers)
55 })
56
57 describe('Auto follow back', function () {
58
59 it('Should not auto follow back if the option is not enabled', async function () {
60 this.timeout(15000)
61
62 await server1Follows2(servers)
63
64 await checkFollow(servers[0], servers[1], true)
65 await checkFollow(servers[1], servers[0], false)
66
67 await resetFollows(servers)
68 })
69
70 it('Should auto follow back on auto accept if the option is enabled', async function () {
71 this.timeout(15000)
72
73 const config = {
74 followings: {
75 instance: {
76 autoFollowBack: { enabled: true }
77 }
78 }
79 }
80 await servers[1].config.updateCustomSubConfig({ newConfig: config })
81
82 await server1Follows2(servers)
83
84 await checkFollow(servers[0], servers[1], true)
85 await checkFollow(servers[1], servers[0], true)
86
87 await resetFollows(servers)
88 })
89
90 it('Should wait the acceptation before auto follow back', async function () {
91 this.timeout(30000)
92
93 const config = {
94 followings: {
95 instance: {
96 autoFollowBack: { enabled: true }
97 }
98 },
99 followers: {
100 instance: {
101 manualApproval: true
102 }
103 }
104 }
105 await servers[1].config.updateCustomSubConfig({ newConfig: config })
106
107 await server1Follows2(servers)
108
109 await checkFollow(servers[0], servers[1], false)
110 await checkFollow(servers[1], servers[0], false)
111
112 await servers[1].follows.acceptFollower({ follower: 'peertube@' + servers[0].host })
113 await waitJobs(servers)
114
115 await checkFollow(servers[0], servers[1], true)
116 await checkFollow(servers[1], servers[0], true)
117
118 await resetFollows(servers)
119
120 config.followings.instance.autoFollowBack.enabled = false
121 config.followers.instance.manualApproval = false
122 await servers[1].config.updateCustomSubConfig({ newConfig: config })
123 })
124 })
125
126 describe('Auto follow index', function () {
127 const instanceIndexServer = new MockInstancesIndex()
128 let port: number
129
130 before(async function () {
131 port = await instanceIndexServer.initialize()
132 })
133
134 it('Should not auto follow index if the option is not enabled', async function () {
135 this.timeout(30000)
136
137 await wait(5000)
138 await waitJobs(servers)
139
140 await checkFollow(servers[0], servers[1], false)
141 await checkFollow(servers[1], servers[0], false)
142 })
143
144 it('Should auto follow the index', async function () {
145 this.timeout(30000)
146
147 instanceIndexServer.addInstance(servers[1].host)
148
149 const config = {
150 followings: {
151 instance: {
152 autoFollowIndex: {
153 indexUrl: `http://127.0.0.1:${port}/api/v1/instances/hosts`,
154 enabled: true
155 }
156 }
157 }
158 }
159 await servers[0].config.updateCustomSubConfig({ newConfig: config })
160
161 await wait(5000)
162 await waitJobs(servers)
163
164 await checkFollow(servers[0], servers[1], true)
165
166 await resetFollows(servers)
167 })
168
169 it('Should follow new added instances in the index but not old ones', async function () {
170 this.timeout(30000)
171
172 instanceIndexServer.addInstance(servers[2].host)
173
174 await wait(5000)
175 await waitJobs(servers)
176
177 await checkFollow(servers[0], servers[1], false)
178 await checkFollow(servers[0], servers[2], true)
179 })
180
181 after(async function () {
182 await instanceIndexServer.terminate()
183 })
184 })
185
186 after(async function () {
187 await cleanupTests(servers)
188 })
189})
diff --git a/packages/tests/src/api/server/bulk.ts b/packages/tests/src/api/server/bulk.ts
new file mode 100644
index 000000000..725bcfef2
--- /dev/null
+++ b/packages/tests/src/api/server/bulk.ts
@@ -0,0 +1,185 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import {
5 BulkCommand,
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test bulk actions', function () {
15 const commentsUser3: { videoId: number, commentId: number }[] = []
16
17 let servers: PeerTubeServer[] = []
18 let user1Token: string
19 let user2Token: string
20 let user3Token: string
21
22 let bulkCommand: BulkCommand
23
24 before(async function () {
25 this.timeout(120000)
26
27 servers = await createMultipleServers(2)
28
29 // Get the access tokens
30 await setAccessTokensToServers(servers)
31
32 {
33 const user = { username: 'user1', password: 'password' }
34 await servers[0].users.create({ username: user.username, password: user.password })
35
36 user1Token = await servers[0].login.getAccessToken(user)
37 }
38
39 {
40 const user = { username: 'user2', password: 'password' }
41 await servers[0].users.create({ username: user.username, password: user.password })
42
43 user2Token = await servers[0].login.getAccessToken(user)
44 }
45
46 {
47 const user = { username: 'user3', password: 'password' }
48 await servers[1].users.create({ username: user.username, password: user.password })
49
50 user3Token = await servers[1].login.getAccessToken(user)
51 }
52
53 await doubleFollow(servers[0], servers[1])
54
55 bulkCommand = new BulkCommand(servers[0])
56 })
57
58 describe('Bulk remove comments', function () {
59 async function checkInstanceCommentsRemoved () {
60 {
61 const { data } = await servers[0].videos.list()
62
63 // Server 1 should not have these comments anymore
64 for (const video of data) {
65 const { data } = await servers[0].comments.listThreads({ videoId: video.id })
66 const comment = data.find(c => c.text === 'comment by user 3')
67
68 expect(comment).to.not.exist
69 }
70 }
71
72 {
73 const { data } = await servers[1].videos.list()
74
75 // Server 1 should not have these comments on videos of server 1
76 for (const video of data) {
77 const { data } = await servers[1].comments.listThreads({ videoId: video.id })
78 const comment = data.find(c => c.text === 'comment by user 3')
79
80 if (video.account.host === servers[0].host) {
81 expect(comment).to.not.exist
82 } else {
83 expect(comment).to.exist
84 }
85 }
86 }
87 }
88
89 before(async function () {
90 this.timeout(240000)
91
92 await servers[0].videos.upload({ attributes: { name: 'video 1 server 1' } })
93 await servers[0].videos.upload({ attributes: { name: 'video 2 server 1' } })
94 await servers[0].videos.upload({ token: user1Token, attributes: { name: 'video 3 server 1' } })
95
96 await servers[1].videos.upload({ attributes: { name: 'video 1 server 2' } })
97
98 await waitJobs(servers)
99
100 {
101 const { data } = await servers[0].videos.list()
102 for (const video of data) {
103 await servers[0].comments.createThread({ videoId: video.id, text: 'comment by root server 1' })
104 await servers[0].comments.createThread({ token: user1Token, videoId: video.id, text: 'comment by user 1' })
105 await servers[0].comments.createThread({ token: user2Token, videoId: video.id, text: 'comment by user 2' })
106 }
107 }
108
109 {
110 const { data } = await servers[1].videos.list()
111
112 for (const video of data) {
113 await servers[1].comments.createThread({ videoId: video.id, text: 'comment by root server 2' })
114
115 const comment = await servers[1].comments.createThread({ token: user3Token, videoId: video.id, text: 'comment by user 3' })
116 commentsUser3.push({ videoId: video.id, commentId: comment.id })
117 }
118 }
119
120 await waitJobs(servers)
121 })
122
123 it('Should delete comments of an account on my videos', async function () {
124 this.timeout(60000)
125
126 await bulkCommand.removeCommentsOf({
127 token: user1Token,
128 attributes: {
129 accountName: 'user2',
130 scope: 'my-videos'
131 }
132 })
133
134 await waitJobs(servers)
135
136 for (const server of servers) {
137 const { data } = await server.videos.list()
138
139 for (const video of data) {
140 const { data } = await server.comments.listThreads({ videoId: video.id })
141 const comment = data.find(c => c.text === 'comment by user 2')
142
143 if (video.name === 'video 3 server 1') expect(comment).to.not.exist
144 else expect(comment).to.exist
145 }
146 }
147 })
148
149 it('Should delete comments of an account on the instance', async function () {
150 this.timeout(60000)
151
152 await bulkCommand.removeCommentsOf({
153 attributes: {
154 accountName: 'user3@' + servers[1].host,
155 scope: 'instance'
156 }
157 })
158
159 await waitJobs(servers)
160
161 await checkInstanceCommentsRemoved()
162 })
163
164 it('Should not re create the comment on video update', async function () {
165 this.timeout(60000)
166
167 for (const obj of commentsUser3) {
168 await servers[1].comments.addReply({
169 token: user3Token,
170 videoId: obj.videoId,
171 toCommentId: obj.commentId,
172 text: 'comment by user 3 bis'
173 })
174 }
175
176 await waitJobs(servers)
177
178 await checkInstanceCommentsRemoved()
179 })
180 })
181
182 after(async function () {
183 await cleanupTests(servers)
184 })
185})
diff --git a/packages/tests/src/api/server/config-defaults.ts b/packages/tests/src/api/server/config-defaults.ts
new file mode 100644
index 000000000..e874af012
--- /dev/null
+++ b/packages/tests/src/api/server/config-defaults.ts
@@ -0,0 +1,294 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { VideoDetails, VideoPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@peertube/peertube-server-commands'
12import { FIXTURE_URLS } from '@tests/shared/tests.js'
13
14describe('Test config defaults', function () {
15 let server: PeerTubeServer
16 let channelId: number
17
18 before(async function () {
19 this.timeout(30000)
20
21 server = await createSingleServer(1)
22 await setAccessTokensToServers([ server ])
23 await setDefaultVideoChannel([ server ])
24
25 channelId = server.store.channel.id
26 })
27
28 describe('Default publish values', function () {
29
30 before(async function () {
31 const overrideConfig = {
32 defaults: {
33 publish: {
34 comments_enabled: false,
35 download_enabled: false,
36 privacy: VideoPrivacy.INTERNAL,
37 licence: 4
38 }
39 }
40 }
41
42 await server.kill()
43 await server.run(overrideConfig)
44 })
45
46 const attributes = {
47 name: 'video',
48 downloadEnabled: undefined,
49 commentsEnabled: undefined,
50 licence: undefined,
51 privacy: VideoPrivacy.PUBLIC // Privacy is mandatory for server
52 }
53
54 function checkVideo (video: VideoDetails) {
55 expect(video.downloadEnabled).to.be.false
56 expect(video.commentsEnabled).to.be.false
57 expect(video.licence.id).to.equal(4)
58 }
59
60 before(async function () {
61 await server.config.disableTranscoding()
62 await server.config.enableImports()
63 await server.config.enableLive({ allowReplay: false, transcoding: false })
64 })
65
66 it('Should have the correct server configuration', async function () {
67 const config = await server.config.getConfig()
68
69 expect(config.defaults.publish.commentsEnabled).to.be.false
70 expect(config.defaults.publish.downloadEnabled).to.be.false
71 expect(config.defaults.publish.licence).to.equal(4)
72 expect(config.defaults.publish.privacy).to.equal(VideoPrivacy.INTERNAL)
73 })
74
75 it('Should respect default values when uploading a video', async function () {
76 for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) {
77 const { id } = await server.videos.upload({ attributes, mode })
78
79 const video = await server.videos.get({ id })
80 checkVideo(video)
81 }
82 })
83
84 it('Should respect default values when importing a video using URL', async function () {
85 const { video: { id } } = await server.imports.importVideo({
86 attributes: {
87 ...attributes,
88 channelId,
89 targetUrl: FIXTURE_URLS.goodVideo
90 }
91 })
92
93 const video = await server.videos.get({ id })
94 checkVideo(video)
95 })
96
97 it('Should respect default values when importing a video using magnet URI', async function () {
98 const { video: { id } } = await server.imports.importVideo({
99 attributes: {
100 ...attributes,
101 channelId,
102 magnetUri: FIXTURE_URLS.magnet
103 }
104 })
105
106 const video = await server.videos.get({ id })
107 checkVideo(video)
108 })
109
110 it('Should respect default values when creating a live', async function () {
111 const { id } = await server.live.create({
112 fields: {
113 ...attributes,
114 channelId
115 }
116 })
117
118 const video = await server.videos.get({ id })
119 checkVideo(video)
120 })
121 })
122
123 describe('Default P2P values', function () {
124
125 describe('Webapp default value', function () {
126
127 before(async function () {
128 const overrideConfig = {
129 defaults: {
130 p2p: {
131 webapp: {
132 enabled: false
133 }
134 }
135 }
136 }
137
138 await server.kill()
139 await server.run(overrideConfig)
140 })
141
142 it('Should have appropriate P2P config', async function () {
143 const config = await server.config.getConfig()
144
145 expect(config.defaults.p2p.webapp.enabled).to.be.false
146 expect(config.defaults.p2p.embed.enabled).to.be.true
147 })
148
149 it('Should create a user with this default setting', async function () {
150 await server.users.create({ username: 'user_p2p_1' })
151 const userToken = await server.login.getAccessToken('user_p2p_1')
152
153 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
154 expect(p2pEnabled).to.be.false
155 })
156
157 it('Should register a user with this default setting', async function () {
158 await server.registrations.register({ username: 'user_p2p_2' })
159
160 const userToken = await server.login.getAccessToken('user_p2p_2')
161
162 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
163 expect(p2pEnabled).to.be.false
164 })
165 })
166
167 describe('Embed default value', function () {
168
169 before(async function () {
170 const overrideConfig = {
171 defaults: {
172 p2p: {
173 embed: {
174 enabled: false
175 }
176 }
177 },
178 signup: {
179 limit: 15
180 }
181 }
182
183 await server.kill()
184 await server.run(overrideConfig)
185 })
186
187 it('Should have appropriate P2P config', async function () {
188 const config = await server.config.getConfig()
189
190 expect(config.defaults.p2p.webapp.enabled).to.be.true
191 expect(config.defaults.p2p.embed.enabled).to.be.false
192 })
193
194 it('Should create a user with this default setting', async function () {
195 await server.users.create({ username: 'user_p2p_3' })
196 const userToken = await server.login.getAccessToken('user_p2p_3')
197
198 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
199 expect(p2pEnabled).to.be.true
200 })
201
202 it('Should register a user with this default setting', async function () {
203 await server.registrations.register({ username: 'user_p2p_4' })
204
205 const userToken = await server.login.getAccessToken('user_p2p_4')
206
207 const { p2pEnabled } = await server.users.getMyInfo({ token: userToken })
208 expect(p2pEnabled).to.be.true
209 })
210 })
211 })
212
213 describe('Default user attributes', function () {
214 it('Should create a user and register a user with the default config', async function () {
215 await server.config.updateCustomSubConfig({
216 newConfig: {
217 user: {
218 history: {
219 videos: {
220 enabled: true
221 }
222 },
223 videoQuota : -1,
224 videoQuotaDaily: -1
225 },
226 signup: {
227 enabled: true,
228 requiresApproval: false
229 }
230 }
231 })
232
233 const config = await server.config.getConfig()
234
235 expect(config.user.videoQuota).to.equal(-1)
236 expect(config.user.videoQuotaDaily).to.equal(-1)
237
238 const user1Token = await server.users.generateUserAndToken('user1')
239 const user1 = await server.users.getMyInfo({ token: user1Token })
240
241 const user = { displayName: 'super user 2', username: 'user2', password: 'super password' }
242 const channel = { name: 'my_user_2_channel', displayName: 'my channel' }
243 await server.registrations.register({ ...user, channel })
244 const user2Token = await server.login.getAccessToken(user)
245 const user2 = await server.users.getMyInfo({ token: user2Token })
246
247 for (const user of [ user1, user2 ]) {
248 expect(user.videosHistoryEnabled).to.be.true
249 expect(user.videoQuota).to.equal(-1)
250 expect(user.videoQuotaDaily).to.equal(-1)
251 }
252 })
253
254 it('Should update config and create a user and register a user with the new default config', async function () {
255 await server.config.updateCustomSubConfig({
256 newConfig: {
257 user: {
258 history: {
259 videos: {
260 enabled: false
261 }
262 },
263 videoQuota : 5242881,
264 videoQuotaDaily: 318742
265 },
266 signup: {
267 enabled: true,
268 requiresApproval: false
269 }
270 }
271 })
272
273 const user3Token = await server.users.generateUserAndToken('user3')
274 const user3 = await server.users.getMyInfo({ token: user3Token })
275
276 const user = { displayName: 'super user 4', username: 'user4', password: 'super password' }
277 const channel = { name: 'my_user_4_channel', displayName: 'my channel' }
278 await server.registrations.register({ ...user, channel })
279 const user4Token = await server.login.getAccessToken(user)
280 const user4 = await server.users.getMyInfo({ token: user4Token })
281
282 for (const user of [ user3, user4 ]) {
283 expect(user.videosHistoryEnabled).to.be.false
284 expect(user.videoQuota).to.equal(5242881)
285 expect(user.videoQuotaDaily).to.equal(318742)
286 }
287 })
288
289 })
290
291 after(async function () {
292 await cleanupTests([ server ])
293 })
294})
diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts
new file mode 100644
index 000000000..ce64668f8
--- /dev/null
+++ b/packages/tests/src/api/server/config.ts
@@ -0,0 +1,645 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { parallelTests } from '@peertube/peertube-node-utils'
5import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createSingleServer,
9 killallServers,
10 makeGetRequest,
11 PeerTubeServer,
12 setAccessTokensToServers
13} from '@peertube/peertube-server-commands'
14
15function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
16 expect(data.instance.name).to.equal('PeerTube')
17 expect(data.instance.shortDescription).to.equal(
18 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
19 )
20 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
21
22 expect(data.instance.terms).to.equal('No terms for now.')
23 expect(data.instance.creationReason).to.be.empty
24 expect(data.instance.codeOfConduct).to.be.empty
25 expect(data.instance.moderationInformation).to.be.empty
26 expect(data.instance.administrator).to.be.empty
27 expect(data.instance.maintenanceLifetime).to.be.empty
28 expect(data.instance.businessModel).to.be.empty
29 expect(data.instance.hardwareInformation).to.be.empty
30
31 expect(data.instance.languages).to.have.lengthOf(0)
32 expect(data.instance.categories).to.have.lengthOf(0)
33
34 expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
35 expect(data.instance.isNSFW).to.be.false
36 expect(data.instance.defaultNSFWPolicy).to.equal('display')
37 expect(data.instance.customizations.css).to.be.empty
38 expect(data.instance.customizations.javascript).to.be.empty
39
40 expect(data.services.twitter.username).to.equal('@Chocobozzz')
41 expect(data.services.twitter.whitelisted).to.be.false
42
43 expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false
44 expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false
45
46 expect(data.cache.previews.size).to.equal(1)
47 expect(data.cache.captions.size).to.equal(1)
48 expect(data.cache.torrents.size).to.equal(1)
49 expect(data.cache.storyboards.size).to.equal(1)
50
51 expect(data.signup.enabled).to.be.true
52 expect(data.signup.limit).to.equal(4)
53 expect(data.signup.minimumAge).to.equal(16)
54 expect(data.signup.requiresApproval).to.be.false
55 expect(data.signup.requiresEmailVerification).to.be.false
56
57 expect(data.admin.email).to.equal('admin' + server.internalServerNumber + '@example.com')
58 expect(data.contactForm.enabled).to.be.true
59
60 expect(data.user.history.videos.enabled).to.be.true
61 expect(data.user.videoQuota).to.equal(5242880)
62 expect(data.user.videoQuotaDaily).to.equal(-1)
63
64 expect(data.videoChannels.maxPerUser).to.equal(20)
65
66 expect(data.transcoding.enabled).to.be.false
67 expect(data.transcoding.remoteRunners.enabled).to.be.false
68 expect(data.transcoding.allowAdditionalExtensions).to.be.false
69 expect(data.transcoding.allowAudioFiles).to.be.false
70 expect(data.transcoding.threads).to.equal(2)
71 expect(data.transcoding.concurrency).to.equal(2)
72 expect(data.transcoding.profile).to.equal('default')
73 expect(data.transcoding.resolutions['144p']).to.be.false
74 expect(data.transcoding.resolutions['240p']).to.be.true
75 expect(data.transcoding.resolutions['360p']).to.be.true
76 expect(data.transcoding.resolutions['480p']).to.be.true
77 expect(data.transcoding.resolutions['720p']).to.be.true
78 expect(data.transcoding.resolutions['1080p']).to.be.true
79 expect(data.transcoding.resolutions['1440p']).to.be.true
80 expect(data.transcoding.resolutions['2160p']).to.be.true
81 expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
82 expect(data.transcoding.webVideos.enabled).to.be.true
83 expect(data.transcoding.hls.enabled).to.be.true
84
85 expect(data.live.enabled).to.be.false
86 expect(data.live.allowReplay).to.be.false
87 expect(data.live.latencySetting.enabled).to.be.true
88 expect(data.live.maxDuration).to.equal(-1)
89 expect(data.live.maxInstanceLives).to.equal(20)
90 expect(data.live.maxUserLives).to.equal(3)
91 expect(data.live.transcoding.enabled).to.be.false
92 expect(data.live.transcoding.remoteRunners.enabled).to.be.false
93 expect(data.live.transcoding.threads).to.equal(2)
94 expect(data.live.transcoding.profile).to.equal('default')
95 expect(data.live.transcoding.resolutions['144p']).to.be.false
96 expect(data.live.transcoding.resolutions['240p']).to.be.false
97 expect(data.live.transcoding.resolutions['360p']).to.be.false
98 expect(data.live.transcoding.resolutions['480p']).to.be.false
99 expect(data.live.transcoding.resolutions['720p']).to.be.false
100 expect(data.live.transcoding.resolutions['1080p']).to.be.false
101 expect(data.live.transcoding.resolutions['1440p']).to.be.false
102 expect(data.live.transcoding.resolutions['2160p']).to.be.false
103 expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
104
105 expect(data.videoStudio.enabled).to.be.false
106 expect(data.videoStudio.remoteRunners.enabled).to.be.false
107
108 expect(data.videoFile.update.enabled).to.be.false
109
110 expect(data.import.videos.concurrency).to.equal(2)
111 expect(data.import.videos.http.enabled).to.be.true
112 expect(data.import.videos.torrent.enabled).to.be.true
113 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
114
115 expect(data.followers.instance.enabled).to.be.true
116 expect(data.followers.instance.manualApproval).to.be.false
117
118 expect(data.followings.instance.autoFollowBack.enabled).to.be.false
119 expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
120 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('')
121
122 expect(data.broadcastMessage.enabled).to.be.false
123 expect(data.broadcastMessage.level).to.equal('info')
124 expect(data.broadcastMessage.message).to.equal('')
125 expect(data.broadcastMessage.dismissable).to.be.false
126}
127
128function checkUpdatedConfig (data: CustomConfig) {
129 expect(data.instance.name).to.equal('PeerTube updated')
130 expect(data.instance.shortDescription).to.equal('my short description')
131 expect(data.instance.description).to.equal('my super description')
132
133 expect(data.instance.terms).to.equal('my super terms')
134 expect(data.instance.creationReason).to.equal('my super creation reason')
135 expect(data.instance.codeOfConduct).to.equal('my super coc')
136 expect(data.instance.moderationInformation).to.equal('my super moderation information')
137 expect(data.instance.administrator).to.equal('Kuja')
138 expect(data.instance.maintenanceLifetime).to.equal('forever')
139 expect(data.instance.businessModel).to.equal('my super business model')
140 expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
141
142 expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
143 expect(data.instance.categories).to.deep.equal([ 1, 2 ])
144
145 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
146 expect(data.instance.isNSFW).to.be.true
147 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
148 expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
149 expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
150
151 expect(data.services.twitter.username).to.equal('@Kuja')
152 expect(data.services.twitter.whitelisted).to.be.true
153
154 expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.true
155 expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.true
156
157 expect(data.cache.previews.size).to.equal(2)
158 expect(data.cache.captions.size).to.equal(3)
159 expect(data.cache.torrents.size).to.equal(4)
160 expect(data.cache.storyboards.size).to.equal(5)
161
162 expect(data.signup.enabled).to.be.false
163 expect(data.signup.limit).to.equal(5)
164 expect(data.signup.requiresApproval).to.be.false
165 expect(data.signup.requiresEmailVerification).to.be.false
166 expect(data.signup.minimumAge).to.equal(10)
167
168 // We override admin email in parallel tests, so skip this exception
169 if (parallelTests() === false) {
170 expect(data.admin.email).to.equal('superadmin1@example.com')
171 }
172
173 expect(data.contactForm.enabled).to.be.false
174
175 expect(data.user.history.videos.enabled).to.be.false
176 expect(data.user.videoQuota).to.equal(5242881)
177 expect(data.user.videoQuotaDaily).to.equal(318742)
178
179 expect(data.videoChannels.maxPerUser).to.equal(24)
180
181 expect(data.transcoding.enabled).to.be.true
182 expect(data.transcoding.remoteRunners.enabled).to.be.true
183 expect(data.transcoding.threads).to.equal(1)
184 expect(data.transcoding.concurrency).to.equal(3)
185 expect(data.transcoding.allowAdditionalExtensions).to.be.true
186 expect(data.transcoding.allowAudioFiles).to.be.true
187 expect(data.transcoding.profile).to.equal('vod_profile')
188 expect(data.transcoding.resolutions['144p']).to.be.false
189 expect(data.transcoding.resolutions['240p']).to.be.false
190 expect(data.transcoding.resolutions['360p']).to.be.true
191 expect(data.transcoding.resolutions['480p']).to.be.true
192 expect(data.transcoding.resolutions['720p']).to.be.false
193 expect(data.transcoding.resolutions['1080p']).to.be.false
194 expect(data.transcoding.resolutions['2160p']).to.be.false
195 expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false
196 expect(data.transcoding.hls.enabled).to.be.false
197 expect(data.transcoding.webVideos.enabled).to.be.true
198
199 expect(data.live.enabled).to.be.true
200 expect(data.live.allowReplay).to.be.true
201 expect(data.live.latencySetting.enabled).to.be.false
202 expect(data.live.maxDuration).to.equal(5000)
203 expect(data.live.maxInstanceLives).to.equal(-1)
204 expect(data.live.maxUserLives).to.equal(10)
205 expect(data.live.transcoding.enabled).to.be.true
206 expect(data.live.transcoding.remoteRunners.enabled).to.be.true
207 expect(data.live.transcoding.threads).to.equal(4)
208 expect(data.live.transcoding.profile).to.equal('live_profile')
209 expect(data.live.transcoding.resolutions['144p']).to.be.true
210 expect(data.live.transcoding.resolutions['240p']).to.be.true
211 expect(data.live.transcoding.resolutions['360p']).to.be.true
212 expect(data.live.transcoding.resolutions['480p']).to.be.true
213 expect(data.live.transcoding.resolutions['720p']).to.be.true
214 expect(data.live.transcoding.resolutions['1080p']).to.be.true
215 expect(data.live.transcoding.resolutions['2160p']).to.be.true
216 expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false
217
218 expect(data.videoStudio.enabled).to.be.true
219 expect(data.videoStudio.remoteRunners.enabled).to.be.true
220
221 expect(data.videoFile.update.enabled).to.be.true
222
223 expect(data.import.videos.concurrency).to.equal(4)
224 expect(data.import.videos.http.enabled).to.be.false
225 expect(data.import.videos.torrent.enabled).to.be.false
226 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
227
228 expect(data.followers.instance.enabled).to.be.false
229 expect(data.followers.instance.manualApproval).to.be.true
230
231 expect(data.followings.instance.autoFollowBack.enabled).to.be.true
232 expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
233 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
234
235 expect(data.broadcastMessage.enabled).to.be.true
236 expect(data.broadcastMessage.level).to.equal('error')
237 expect(data.broadcastMessage.message).to.equal('super bad message')
238 expect(data.broadcastMessage.dismissable).to.be.true
239}
240
241const newCustomConfig: CustomConfig = {
242 instance: {
243 name: 'PeerTube updated',
244 shortDescription: 'my short description',
245 description: 'my super description',
246 terms: 'my super terms',
247 codeOfConduct: 'my super coc',
248
249 creationReason: 'my super creation reason',
250 moderationInformation: 'my super moderation information',
251 administrator: 'Kuja',
252 maintenanceLifetime: 'forever',
253 businessModel: 'my super business model',
254 hardwareInformation: '2vCore 3GB RAM',
255
256 languages: [ 'en', 'es' ],
257 categories: [ 1, 2 ],
258
259 isNSFW: true,
260 defaultNSFWPolicy: 'blur' as 'blur',
261
262 defaultClientRoute: '/videos/recently-added',
263
264 customizations: {
265 javascript: 'alert("coucou")',
266 css: 'body { background-color: red; }'
267 }
268 },
269 theme: {
270 default: 'default'
271 },
272 services: {
273 twitter: {
274 username: '@Kuja',
275 whitelisted: true
276 }
277 },
278 client: {
279 videos: {
280 miniature: {
281 preferAuthorDisplayName: true
282 }
283 },
284 menu: {
285 login: {
286 redirectOnSingleExternalAuth: true
287 }
288 }
289 },
290 cache: {
291 previews: {
292 size: 2
293 },
294 captions: {
295 size: 3
296 },
297 torrents: {
298 size: 4
299 },
300 storyboards: {
301 size: 5
302 }
303 },
304 signup: {
305 enabled: false,
306 limit: 5,
307 requiresApproval: false,
308 requiresEmailVerification: false,
309 minimumAge: 10
310 },
311 admin: {
312 email: 'superadmin1@example.com'
313 },
314 contactForm: {
315 enabled: false
316 },
317 user: {
318 history: {
319 videos: {
320 enabled: false
321 }
322 },
323 videoQuota: 5242881,
324 videoQuotaDaily: 318742
325 },
326 videoChannels: {
327 maxPerUser: 24
328 },
329 transcoding: {
330 enabled: true,
331 remoteRunners: {
332 enabled: true
333 },
334 allowAdditionalExtensions: true,
335 allowAudioFiles: true,
336 threads: 1,
337 concurrency: 3,
338 profile: 'vod_profile',
339 resolutions: {
340 '0p': false,
341 '144p': false,
342 '240p': false,
343 '360p': true,
344 '480p': true,
345 '720p': false,
346 '1080p': false,
347 '1440p': false,
348 '2160p': false
349 },
350 alwaysTranscodeOriginalResolution: false,
351 webVideos: {
352 enabled: true
353 },
354 hls: {
355 enabled: false
356 }
357 },
358 live: {
359 enabled: true,
360 allowReplay: true,
361 latencySetting: {
362 enabled: false
363 },
364 maxDuration: 5000,
365 maxInstanceLives: -1,
366 maxUserLives: 10,
367 transcoding: {
368 enabled: true,
369 remoteRunners: {
370 enabled: true
371 },
372 threads: 4,
373 profile: 'live_profile',
374 resolutions: {
375 '144p': true,
376 '240p': true,
377 '360p': true,
378 '480p': true,
379 '720p': true,
380 '1080p': true,
381 '1440p': true,
382 '2160p': true
383 },
384 alwaysTranscodeOriginalResolution: false
385 }
386 },
387 videoStudio: {
388 enabled: true,
389 remoteRunners: {
390 enabled: true
391 }
392 },
393 videoFile: {
394 update: {
395 enabled: true
396 }
397 },
398 import: {
399 videos: {
400 concurrency: 4,
401 http: {
402 enabled: false
403 },
404 torrent: {
405 enabled: false
406 }
407 },
408 videoChannelSynchronization: {
409 enabled: false,
410 maxPerUser: 10
411 }
412 },
413 trending: {
414 videos: {
415 algorithms: {
416 enabled: [ 'hot', 'most-viewed', 'most-liked' ],
417 default: 'hot'
418 }
419 }
420 },
421 autoBlacklist: {
422 videos: {
423 ofUsers: {
424 enabled: true
425 }
426 }
427 },
428 followers: {
429 instance: {
430 enabled: false,
431 manualApproval: true
432 }
433 },
434 followings: {
435 instance: {
436 autoFollowBack: {
437 enabled: true
438 },
439 autoFollowIndex: {
440 enabled: true,
441 indexUrl: 'https://updated.example.com'
442 }
443 }
444 },
445 broadcastMessage: {
446 enabled: true,
447 level: 'error',
448 message: 'super bad message',
449 dismissable: true
450 },
451 search: {
452 remoteUri: {
453 anonymous: true,
454 users: true
455 },
456 searchIndex: {
457 enabled: true,
458 url: 'https://search.joinpeertube.org',
459 disableLocalSearch: true,
460 isDefaultSearch: true
461 }
462 }
463}
464
465describe('Test static config', function () {
466 let server: PeerTubeServer = null
467
468 before(async function () {
469 this.timeout(30000)
470
471 server = await createSingleServer(1, { webadmin: { configuration: { edition: { allowed: false } } } })
472 await setAccessTokensToServers([ server ])
473 })
474
475 it('Should tell the client that edits are not allowed', async function () {
476 const data = await server.config.getConfig()
477
478 expect(data.webadmin.configuration.edition.allowed).to.be.false
479 })
480
481 it('Should error when client tries to update', async function () {
482 await server.config.updateCustomConfig({ newCustomConfig, expectedStatus: 405 })
483 })
484
485 after(async function () {
486 await cleanupTests([ server ])
487 })
488})
489
490describe('Test config', function () {
491 let server: PeerTubeServer = null
492
493 before(async function () {
494 this.timeout(30000)
495
496 server = await createSingleServer(1)
497 await setAccessTokensToServers([ server ])
498 })
499
500 it('Should have a correct config on a server with registration enabled', async function () {
501 const data = await server.config.getConfig()
502
503 expect(data.signup.allowed).to.be.true
504 })
505
506 it('Should have a correct config on a server with registration enabled and a users limit', async function () {
507 this.timeout(5000)
508
509 await Promise.all([
510 server.registrations.register({ username: 'user1' }),
511 server.registrations.register({ username: 'user2' }),
512 server.registrations.register({ username: 'user3' })
513 ])
514
515 const data = await server.config.getConfig()
516
517 expect(data.signup.allowed).to.be.false
518 })
519
520 it('Should have the correct video allowed extensions', async function () {
521 const data = await server.config.getConfig()
522
523 expect(data.video.file.extensions).to.have.lengthOf(3)
524 expect(data.video.file.extensions).to.contain('.mp4')
525 expect(data.video.file.extensions).to.contain('.webm')
526 expect(data.video.file.extensions).to.contain('.ogv')
527
528 await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
529 await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 })
530
531 expect(data.contactForm.enabled).to.be.true
532 })
533
534 it('Should get the customized configuration', async function () {
535 const data = await server.config.getCustomConfig()
536
537 checkInitialConfig(server, data)
538 })
539
540 it('Should update the customized configuration', async function () {
541 await server.config.updateCustomConfig({ newCustomConfig })
542
543 const data = await server.config.getCustomConfig()
544 checkUpdatedConfig(data)
545 })
546
547 it('Should have the correct updated video allowed extensions', async function () {
548 this.timeout(30000)
549
550 const data = await server.config.getConfig()
551
552 expect(data.video.file.extensions).to.have.length.above(4)
553 expect(data.video.file.extensions).to.contain('.mp4')
554 expect(data.video.file.extensions).to.contain('.webm')
555 expect(data.video.file.extensions).to.contain('.ogv')
556 expect(data.video.file.extensions).to.contain('.flv')
557 expect(data.video.file.extensions).to.contain('.wmv')
558 expect(data.video.file.extensions).to.contain('.mkv')
559 expect(data.video.file.extensions).to.contain('.mp3')
560 expect(data.video.file.extensions).to.contain('.ogg')
561 expect(data.video.file.extensions).to.contain('.flac')
562
563 await server.videos.upload({ attributes: { fixture: 'video_short.mkv' }, expectedStatus: HttpStatusCode.OK_200 })
564 await server.videos.upload({ attributes: { fixture: 'sample.ogg' }, expectedStatus: HttpStatusCode.OK_200 })
565 })
566
567 it('Should have the configuration updated after a restart', async function () {
568 this.timeout(30000)
569
570 await killallServers([ server ])
571
572 await server.run()
573
574 const data = await server.config.getCustomConfig()
575
576 checkUpdatedConfig(data)
577 })
578
579 it('Should fetch the about information', async function () {
580 const data = await server.config.getAbout()
581
582 expect(data.instance.name).to.equal('PeerTube updated')
583 expect(data.instance.shortDescription).to.equal('my short description')
584 expect(data.instance.description).to.equal('my super description')
585 expect(data.instance.terms).to.equal('my super terms')
586 expect(data.instance.codeOfConduct).to.equal('my super coc')
587
588 expect(data.instance.creationReason).to.equal('my super creation reason')
589 expect(data.instance.moderationInformation).to.equal('my super moderation information')
590 expect(data.instance.administrator).to.equal('Kuja')
591 expect(data.instance.maintenanceLifetime).to.equal('forever')
592 expect(data.instance.businessModel).to.equal('my super business model')
593 expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
594
595 expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
596 expect(data.instance.categories).to.deep.equal([ 1, 2 ])
597 })
598
599 it('Should remove the custom configuration', async function () {
600 await server.config.deleteCustomConfig()
601
602 const data = await server.config.getCustomConfig()
603 checkInitialConfig(server, data)
604 })
605
606 it('Should enable/disable security headers', async function () {
607 this.timeout(25000)
608
609 {
610 const res = await makeGetRequest({
611 url: server.url,
612 path: '/api/v1/config',
613 expectedStatus: 200
614 })
615
616 expect(res.headers['x-frame-options']).to.exist
617 expect(res.headers['x-powered-by']).to.equal('PeerTube')
618 }
619
620 await killallServers([ server ])
621
622 const config = {
623 security: {
624 frameguard: { enabled: false },
625 powered_by_header: { enabled: false }
626 }
627 }
628 await server.run(config)
629
630 {
631 const res = await makeGetRequest({
632 url: server.url,
633 path: '/api/v1/config',
634 expectedStatus: 200
635 })
636
637 expect(res.headers['x-frame-options']).to.not.exist
638 expect(res.headers['x-powered-by']).to.not.exist
639 }
640 })
641
642 after(async function () {
643 await cleanupTests([ server ])
644 })
645})
diff --git a/packages/tests/src/api/server/contact-form.ts b/packages/tests/src/api/server/contact-form.ts
new file mode 100644
index 000000000..03389aa64
--- /dev/null
+++ b/packages/tests/src/api/server/contact-form.ts
@@ -0,0 +1,101 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { wait } from '@peertube/peertube-core-utils'
6import { HttpStatusCode } from '@peertube/peertube-models'
7import {
8 cleanupTests,
9 ConfigCommand,
10 ContactFormCommand,
11 createSingleServer,
12 PeerTubeServer,
13 setAccessTokensToServers,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test contact form', function () {
18 let server: PeerTubeServer
19 const emails: object[] = []
20 let command: ContactFormCommand
21
22 before(async function () {
23 this.timeout(30000)
24
25 const port = await MockSmtpServer.Instance.collectEmails(emails)
26
27 server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(port))
28 await setAccessTokensToServers([ server ])
29
30 command = server.contactForm
31 })
32
33 it('Should send a contact form', async function () {
34 await command.send({
35 fromEmail: 'toto@example.com',
36 body: 'my super message',
37 subject: 'my subject',
38 fromName: 'Super toto'
39 })
40
41 await waitJobs(server)
42
43 expect(emails).to.have.lengthOf(1)
44
45 const email = emails[0]
46
47 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
48 expect(email['replyTo'][0]['address']).equal('toto@example.com')
49 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
50 expect(email['subject']).contains('my subject')
51 expect(email['text']).contains('my super message')
52 })
53
54 it('Should not have duplicated email address in text message', async function () {
55 const text = emails[0]['text'] as string
56
57 const matches = text.match(/toto@example.com/g)
58 expect(matches).to.have.lengthOf(1)
59 })
60
61 it('Should not be able to send another contact form because of the anti spam checker', async function () {
62 await wait(1000)
63
64 await command.send({
65 fromEmail: 'toto@example.com',
66 body: 'my super message',
67 subject: 'my subject',
68 fromName: 'Super toto'
69 })
70
71 await command.send({
72 fromEmail: 'toto@example.com',
73 body: 'my super message',
74 fromName: 'Super toto',
75 subject: 'my subject',
76 expectedStatus: HttpStatusCode.FORBIDDEN_403
77 })
78 })
79
80 it('Should be able to send another contact form after a while', async function () {
81 await wait(1000)
82
83 await command.send({
84 fromEmail: 'toto@example.com',
85 fromName: 'Super toto',
86 subject: 'my subject',
87 body: 'my super message'
88 })
89 })
90
91 it('Should not have the manage preferences link in the email', async function () {
92 const email = emails[0]
93 expect(email['text']).to.not.contain('Manage your notification preferences')
94 })
95
96 after(async function () {
97 MockSmtpServer.Instance.kill()
98
99 await cleanupTests([ server ])
100 })
101})
diff --git a/packages/tests/src/api/server/email.ts b/packages/tests/src/api/server/email.ts
new file mode 100644
index 000000000..6d3f3f3bb
--- /dev/null
+++ b/packages/tests/src/api/server/email.ts
@@ -0,0 +1,371 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { MockSmtpServer } from '@tests/shared/mock-servers/index.js'
5import { HttpStatusCode } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 ConfigCommand,
9 createSingleServer,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test emails', function () {
16 let server: PeerTubeServer
17 let userId: number
18 let userId2: number
19 let userAccessToken: string
20
21 let videoShortUUID: string
22 let videoId: number
23
24 let videoUserUUID: string
25
26 let verificationString: string
27 let verificationString2: string
28
29 const emails: object[] = []
30 const user = {
31 username: 'user_1',
32 password: 'super_password'
33 }
34
35 before(async function () {
36 this.timeout(120000)
37
38 const emailPort = await MockSmtpServer.Instance.collectEmails(emails)
39 server = await createSingleServer(1, ConfigCommand.getEmailOverrideConfig(emailPort))
40
41 await setAccessTokensToServers([ server ])
42 await server.config.enableSignup(true)
43
44 {
45 const created = await server.users.create({ username: user.username, password: user.password })
46 userId = created.id
47
48 userAccessToken = await server.login.getAccessToken(user)
49 }
50
51 {
52 const attributes = { name: 'my super user video' }
53 const { uuid } = await server.videos.upload({ token: userAccessToken, attributes })
54 videoUserUUID = uuid
55 }
56
57 {
58 const attributes = {
59 name: 'my super name'
60 }
61 const { shortUUID, id } = await server.videos.upload({ attributes })
62 videoShortUUID = shortUUID
63 videoId = id
64 }
65 })
66
67 describe('When resetting user password', function () {
68
69 it('Should ask to reset the password', async function () {
70 await server.users.askResetPassword({ email: 'user_1@example.com' })
71
72 await waitJobs(server)
73 expect(emails).to.have.lengthOf(1)
74
75 const email = emails[0]
76
77 expect(email['from'][0]['name']).equal('PeerTube')
78 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
79 expect(email['to'][0]['address']).equal('user_1@example.com')
80 expect(email['subject']).contains('password')
81
82 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
83 expect(verificationStringMatches).not.to.be.null
84
85 verificationString = verificationStringMatches[1]
86 expect(verificationString).to.have.length.above(2)
87
88 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
89 expect(userIdMatches).not.to.be.null
90
91 userId = parseInt(userIdMatches[1], 10)
92 expect(verificationString).to.not.be.undefined
93 })
94
95 it('Should not reset the password with an invalid verification string', async function () {
96 await server.users.resetPassword({
97 userId,
98 verificationString: verificationString + 'b',
99 password: 'super_password2',
100 expectedStatus: HttpStatusCode.FORBIDDEN_403
101 })
102 })
103
104 it('Should reset the password', async function () {
105 await server.users.resetPassword({ userId, verificationString, password: 'super_password2' })
106 })
107
108 it('Should not reset the password with the same verification string', async function () {
109 await server.users.resetPassword({
110 userId,
111 verificationString,
112 password: 'super_password3',
113 expectedStatus: HttpStatusCode.FORBIDDEN_403
114 })
115 })
116
117 it('Should login with this new password', async function () {
118 user.password = 'super_password2'
119
120 await server.login.getAccessToken(user)
121 })
122 })
123
124 describe('When creating a user without password', function () {
125
126 it('Should send a create password email', async function () {
127 await server.users.create({ username: 'create_password', password: '' })
128
129 await waitJobs(server)
130 expect(emails).to.have.lengthOf(2)
131
132 const email = emails[1]
133
134 expect(email['from'][0]['name']).equal('PeerTube')
135 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
136 expect(email['to'][0]['address']).equal('create_password@example.com')
137 expect(email['subject']).contains('account')
138 expect(email['subject']).contains('password')
139
140 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
141 expect(verificationStringMatches).not.to.be.null
142
143 verificationString2 = verificationStringMatches[1]
144 expect(verificationString2).to.have.length.above(2)
145
146 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
147 expect(userIdMatches).not.to.be.null
148
149 userId2 = parseInt(userIdMatches[1], 10)
150 })
151
152 it('Should not reset the password with an invalid verification string', async function () {
153 await server.users.resetPassword({
154 userId: userId2,
155 verificationString: verificationString2 + 'c',
156 password: 'newly_created_password',
157 expectedStatus: HttpStatusCode.FORBIDDEN_403
158 })
159 })
160
161 it('Should reset the password', async function () {
162 await server.users.resetPassword({
163 userId: userId2,
164 verificationString: verificationString2,
165 password: 'newly_created_password'
166 })
167 })
168
169 it('Should login with this new password', async function () {
170 await server.login.getAccessToken({
171 username: 'create_password',
172 password: 'newly_created_password'
173 })
174 })
175 })
176
177 describe('When creating an abuse', function () {
178
179 it('Should send the notification email', async function () {
180 const reason = 'my super bad reason'
181 await server.abuses.report({ token: userAccessToken, videoId, reason })
182
183 await waitJobs(server)
184 expect(emails).to.have.lengthOf(3)
185
186 const email = emails[2]
187
188 expect(email['from'][0]['name']).equal('PeerTube')
189 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
190 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
191 expect(email['subject']).contains('abuse')
192 expect(email['text']).contains(videoShortUUID)
193 })
194 })
195
196 describe('When blocking/unblocking user', function () {
197
198 it('Should send the notification email when blocking a user', async function () {
199 const reason = 'my super bad reason'
200 await server.users.banUser({ userId, reason })
201
202 await waitJobs(server)
203 expect(emails).to.have.lengthOf(4)
204
205 const email = emails[3]
206
207 expect(email['from'][0]['name']).equal('PeerTube')
208 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
209 expect(email['to'][0]['address']).equal('user_1@example.com')
210 expect(email['subject']).contains(' blocked')
211 expect(email['text']).contains(' blocked')
212 expect(email['text']).contains('bad reason')
213 })
214
215 it('Should send the notification email when unblocking a user', async function () {
216 await server.users.unbanUser({ userId })
217
218 await waitJobs(server)
219 expect(emails).to.have.lengthOf(5)
220
221 const email = emails[4]
222
223 expect(email['from'][0]['name']).equal('PeerTube')
224 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
225 expect(email['to'][0]['address']).equal('user_1@example.com')
226 expect(email['subject']).contains(' unblocked')
227 expect(email['text']).contains(' unblocked')
228 })
229 })
230
231 describe('When blacklisting a video', function () {
232 it('Should send the notification email', async function () {
233 const reason = 'my super reason'
234 await server.blacklist.add({ videoId: videoUserUUID, reason })
235
236 await waitJobs(server)
237 expect(emails).to.have.lengthOf(6)
238
239 const email = emails[5]
240
241 expect(email['from'][0]['name']).equal('PeerTube')
242 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
243 expect(email['to'][0]['address']).equal('user_1@example.com')
244 expect(email['subject']).contains(' blacklisted')
245 expect(email['text']).contains('my super user video')
246 expect(email['text']).contains('my super reason')
247 })
248
249 it('Should send the notification email', async function () {
250 await server.blacklist.remove({ videoId: videoUserUUID })
251
252 await waitJobs(server)
253 expect(emails).to.have.lengthOf(7)
254
255 const email = emails[6]
256
257 expect(email['from'][0]['name']).equal('PeerTube')
258 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
259 expect(email['to'][0]['address']).equal('user_1@example.com')
260 expect(email['subject']).contains(' unblacklisted')
261 expect(email['text']).contains('my super user video')
262 })
263
264 it('Should have the manage preferences link in the email', async function () {
265 const email = emails[6]
266 expect(email['text']).to.contain('Manage your notification preferences')
267 })
268 })
269
270 describe('When verifying a user email', function () {
271
272 it('Should ask to send the verification email', async function () {
273 await server.users.askSendVerifyEmail({ email: 'user_1@example.com' })
274
275 await waitJobs(server)
276 expect(emails).to.have.lengthOf(8)
277
278 const email = emails[7]
279
280 expect(email['from'][0]['name']).equal('PeerTube')
281 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
282 expect(email['to'][0]['address']).equal('user_1@example.com')
283 expect(email['subject']).contains('Verify')
284
285 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
286 expect(verificationStringMatches).not.to.be.null
287
288 verificationString = verificationStringMatches[1]
289 expect(verificationString).to.not.be.undefined
290 expect(verificationString).to.have.length.above(2)
291
292 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
293 expect(userIdMatches).not.to.be.null
294
295 userId = parseInt(userIdMatches[1], 10)
296 })
297
298 it('Should not verify the email with an invalid verification string', async function () {
299 await server.users.verifyEmail({
300 userId,
301 verificationString: verificationString + 'b',
302 isPendingEmail: false,
303 expectedStatus: HttpStatusCode.FORBIDDEN_403
304 })
305 })
306
307 it('Should verify the email', async function () {
308 await server.users.verifyEmail({ userId, verificationString })
309 })
310 })
311
312 describe('When verifying a registration email', function () {
313 let registrationId: number
314 let registrationIdEmail: number
315
316 before(async function () {
317 const { id } = await server.registrations.requestRegistration({
318 username: 'request_1',
319 email: 'request_1@example.com',
320 registrationReason: 'tt'
321 })
322 registrationId = id
323 })
324
325 it('Should ask to send the verification email', async function () {
326 await server.registrations.askSendVerifyEmail({ email: 'request_1@example.com' })
327
328 await waitJobs(server)
329 expect(emails).to.have.lengthOf(9)
330
331 const email = emails[8]
332
333 expect(email['from'][0]['name']).equal('PeerTube')
334 expect(email['from'][0]['address']).equal('test-admin@127.0.0.1')
335 expect(email['to'][0]['address']).equal('request_1@example.com')
336 expect(email['subject']).contains('Verify')
337
338 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
339 expect(verificationStringMatches).not.to.be.null
340
341 verificationString = verificationStringMatches[1]
342 expect(verificationString).to.not.be.undefined
343 expect(verificationString).to.have.length.above(2)
344
345 const registrationIdMatches = /registrationId=([0-9]+)/.exec(email['text'])
346 expect(registrationIdMatches).not.to.be.null
347
348 registrationIdEmail = parseInt(registrationIdMatches[1], 10)
349
350 expect(registrationId).to.equal(registrationIdEmail)
351 })
352
353 it('Should not verify the email with an invalid verification string', async function () {
354 await server.registrations.verifyEmail({
355 registrationId: registrationIdEmail,
356 verificationString: verificationString + 'b',
357 expectedStatus: HttpStatusCode.FORBIDDEN_403
358 })
359 })
360
361 it('Should verify the email', async function () {
362 await server.registrations.verifyEmail({ registrationId: registrationIdEmail, verificationString })
363 })
364 })
365
366 after(async function () {
367 MockSmtpServer.Instance.kill()
368
369 await cleanupTests([ server ])
370 })
371})
diff --git a/packages/tests/src/api/server/follow-constraints.ts b/packages/tests/src/api/server/follow-constraints.ts
new file mode 100644
index 000000000..8d277c906
--- /dev/null
+++ b/packages/tests/src/api/server/follow-constraints.ts
@@ -0,0 +1,321 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode, PeerTubeProblemDocument, ServerErrorCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test follow constraints', function () {
15 let servers: PeerTubeServer[] = []
16 let video1UUID: string
17 let video2UUID: string
18 let userToken: string
19
20 before(async function () {
21 this.timeout(240000)
22
23 servers = await createMultipleServers(2)
24
25 // Get the access tokens
26 await setAccessTokensToServers(servers)
27
28 {
29 const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video server 1' } })
30 video1UUID = uuid
31 }
32 {
33 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video server 2' } })
34 video2UUID = uuid
35 }
36
37 const user = {
38 username: 'user1',
39 password: 'super_password'
40 }
41 await servers[0].users.create({ username: user.username, password: user.password })
42 userToken = await servers[0].login.getAccessToken(user)
43
44 await doubleFollow(servers[0], servers[1])
45 })
46
47 describe('With a followed instance', function () {
48
49 describe('With an unlogged user', function () {
50
51 it('Should get the local video', async function () {
52 await servers[0].videos.get({ id: video1UUID })
53 })
54
55 it('Should get the remote video', async function () {
56 await servers[0].videos.get({ id: video2UUID })
57 })
58
59 it('Should list local account videos', async function () {
60 const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@' + servers[0].host })
61
62 expect(total).to.equal(1)
63 expect(data).to.have.lengthOf(1)
64 })
65
66 it('Should list remote account videos', async function () {
67 const { total, data } = await servers[0].videos.listByAccount({ handle: 'root@' + servers[1].host })
68
69 expect(total).to.equal(1)
70 expect(data).to.have.lengthOf(1)
71 })
72
73 it('Should list local channel videos', async function () {
74 const handle = 'root_channel@' + servers[0].host
75 const { total, data } = await servers[0].videos.listByChannel({ handle })
76
77 expect(total).to.equal(1)
78 expect(data).to.have.lengthOf(1)
79 })
80
81 it('Should list remote channel videos', async function () {
82 const handle = 'root_channel@' + servers[1].host
83 const { total, data } = await servers[0].videos.listByChannel({ handle })
84
85 expect(total).to.equal(1)
86 expect(data).to.have.lengthOf(1)
87 })
88 })
89
90 describe('With a logged user', function () {
91 it('Should get the local video', async function () {
92 await servers[0].videos.getWithToken({ token: userToken, id: video1UUID })
93 })
94
95 it('Should get the remote video', async function () {
96 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
97 })
98
99 it('Should list local account videos', async function () {
100 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[0].host })
101
102 expect(total).to.equal(1)
103 expect(data).to.have.lengthOf(1)
104 })
105
106 it('Should list remote account videos', async function () {
107 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[1].host })
108
109 expect(total).to.equal(1)
110 expect(data).to.have.lengthOf(1)
111 })
112
113 it('Should list local channel videos', async function () {
114 const handle = 'root_channel@' + servers[0].host
115 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
116
117 expect(total).to.equal(1)
118 expect(data).to.have.lengthOf(1)
119 })
120
121 it('Should list remote channel videos', async function () {
122 const handle = 'root_channel@' + servers[1].host
123 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
124
125 expect(total).to.equal(1)
126 expect(data).to.have.lengthOf(1)
127 })
128 })
129 })
130
131 describe('With a non followed instance', function () {
132
133 before(async function () {
134 this.timeout(30000)
135
136 await servers[0].follows.unfollow({ target: servers[1] })
137 })
138
139 describe('With an unlogged user', function () {
140
141 it('Should get the local video', async function () {
142 await servers[0].videos.get({ id: video1UUID })
143 })
144
145 it('Should not get the remote video', async function () {
146 const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
147 const error = body as unknown as PeerTubeProblemDocument
148
149 const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints'
150 expect(error.type).to.equal(doc)
151 expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
152
153 expect(error.detail).to.equal('Cannot get this video regarding follow constraints')
154 expect(error.error).to.equal(error.detail)
155
156 expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
157
158 expect(error.originUrl).to.contains(servers[1].url)
159 })
160
161 it('Should list local account videos', async function () {
162 const { total, data } = await servers[0].videos.listByAccount({
163 token: null,
164 handle: 'root@' + servers[0].host
165 })
166
167 expect(total).to.equal(1)
168 expect(data).to.have.lengthOf(1)
169 })
170
171 it('Should not list remote account videos', async function () {
172 const { total, data } = await servers[0].videos.listByAccount({
173 token: null,
174 handle: 'root@' + servers[1].host
175 })
176
177 expect(total).to.equal(0)
178 expect(data).to.have.lengthOf(0)
179 })
180
181 it('Should list local channel videos', async function () {
182 const handle = 'root_channel@' + servers[0].host
183 const { total, data } = await servers[0].videos.listByChannel({ token: null, handle })
184
185 expect(total).to.equal(1)
186 expect(data).to.have.lengthOf(1)
187 })
188
189 it('Should not list remote channel videos', async function () {
190 const handle = 'root_channel@' + servers[1].host
191 const { total, data } = await servers[0].videos.listByChannel({ token: null, handle })
192
193 expect(total).to.equal(0)
194 expect(data).to.have.lengthOf(0)
195 })
196 })
197
198 describe('With a logged user', function () {
199
200 it('Should get the local video', async function () {
201 await servers[0].videos.getWithToken({ token: userToken, id: video1UUID })
202 })
203
204 it('Should get the remote video', async function () {
205 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
206 })
207
208 it('Should list local account videos', async function () {
209 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[0].host })
210
211 expect(total).to.equal(1)
212 expect(data).to.have.lengthOf(1)
213 })
214
215 it('Should list remote account videos', async function () {
216 const { total, data } = await servers[0].videos.listByAccount({ token: userToken, handle: 'root@' + servers[1].host })
217
218 expect(total).to.equal(1)
219 expect(data).to.have.lengthOf(1)
220 })
221
222 it('Should list local channel videos', async function () {
223 const handle = 'root_channel@' + servers[0].host
224 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
225
226 expect(total).to.equal(1)
227 expect(data).to.have.lengthOf(1)
228 })
229
230 it('Should list remote channel videos', async function () {
231 const handle = 'root_channel@' + servers[1].host
232 const { total, data } = await servers[0].videos.listByChannel({ token: userToken, handle })
233
234 expect(total).to.equal(1)
235 expect(data).to.have.lengthOf(1)
236 })
237 })
238 })
239
240 describe('When following a remote account', function () {
241
242 before(async function () {
243 this.timeout(60000)
244
245 await servers[0].follows.follow({ handles: [ 'root@' + servers[1].host ] })
246 await waitJobs(servers)
247 })
248
249 it('Should get the remote video with an unlogged user', async function () {
250 await servers[0].videos.get({ id: video2UUID })
251 })
252
253 it('Should get the remote video with a logged in user', async function () {
254 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
255 })
256 })
257
258 describe('When unfollowing a remote account', function () {
259
260 before(async function () {
261 this.timeout(60000)
262
263 await servers[0].follows.unfollow({ target: 'root@' + servers[1].host })
264 await waitJobs(servers)
265 })
266
267 it('Should not get the remote video with an unlogged user', async function () {
268 const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
269
270 const error = body as unknown as PeerTubeProblemDocument
271 expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
272 })
273
274 it('Should get the remote video with a logged in user', async function () {
275 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
276 })
277 })
278
279 describe('When following a remote channel', function () {
280
281 before(async function () {
282 this.timeout(60000)
283
284 await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[1].host ] })
285 await waitJobs(servers)
286 })
287
288 it('Should get the remote video with an unlogged user', async function () {
289 await servers[0].videos.get({ id: video2UUID })
290 })
291
292 it('Should get the remote video with a logged in user', async function () {
293 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
294 })
295 })
296
297 describe('When unfollowing a remote channel', function () {
298
299 before(async function () {
300 this.timeout(60000)
301
302 await servers[0].follows.unfollow({ target: 'root_channel@' + servers[1].host })
303 await waitJobs(servers)
304 })
305
306 it('Should not get the remote video with an unlogged user', async function () {
307 const body = await servers[0].videos.get({ id: video2UUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
308
309 const error = body as unknown as PeerTubeProblemDocument
310 expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
311 })
312
313 it('Should get the remote video with a logged in user', async function () {
314 await servers[0].videos.getWithToken({ token: userToken, id: video2UUID })
315 })
316 })
317
318 after(async function () {
319 await cleanupTests(servers)
320 })
321})
diff --git a/packages/tests/src/api/server/follows-moderation.ts b/packages/tests/src/api/server/follows-moderation.ts
new file mode 100644
index 000000000..811dd5c22
--- /dev/null
+++ b/packages/tests/src/api/server/follows-moderation.ts
@@ -0,0 +1,364 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { expectStartWith } from '@tests/shared/checks.js'
5import { ActorFollow, FollowState } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 FollowsCommand,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15async function checkServer1And2HasFollowers (servers: PeerTubeServer[], state = 'accepted') {
16 const fns = [
17 servers[0].follows.getFollowings.bind(servers[0].follows),
18 servers[1].follows.getFollowers.bind(servers[1].follows)
19 ]
20
21 for (const fn of fns) {
22 const body = await fn({ start: 0, count: 5, sort: 'createdAt' })
23 expect(body.total).to.equal(1)
24
25 const follow = body.data[0]
26 expect(follow.state).to.equal(state)
27 expect(follow.follower.url).to.equal(servers[0].url + '/accounts/peertube')
28 expect(follow.following.url).to.equal(servers[1].url + '/accounts/peertube')
29 }
30}
31
32async function checkFollows (options: {
33 follower: PeerTubeServer
34 followerState: FollowState | 'deleted'
35
36 following: PeerTubeServer
37 followingState: FollowState | 'deleted'
38}) {
39 const { follower, followerState, followingState, following } = options
40
41 const followerUrl = follower.url + '/accounts/peertube'
42 const followingUrl = following.url + '/accounts/peertube'
43 const finder = (d: ActorFollow) => d.follower.url === followerUrl && d.following.url === followingUrl
44
45 {
46 const { data } = await follower.follows.getFollowings()
47 const follow = data.find(finder)
48
49 if (followerState === 'deleted') {
50 expect(follow).to.not.exist
51 } else {
52 expect(follow.state).to.equal(followerState)
53 expect(follow.follower.url).to.equal(followerUrl)
54 expect(follow.following.url).to.equal(followingUrl)
55 }
56 }
57
58 {
59 const { data } = await following.follows.getFollowers()
60 const follow = data.find(finder)
61
62 if (followingState === 'deleted') {
63 expect(follow).to.not.exist
64 } else {
65 expect(follow.state).to.equal(followingState)
66 expect(follow.follower.url).to.equal(followerUrl)
67 expect(follow.following.url).to.equal(followingUrl)
68 }
69 }
70}
71
72async function checkNoFollowers (servers: PeerTubeServer[]) {
73 const fns = [
74 servers[0].follows.getFollowings.bind(servers[0].follows),
75 servers[1].follows.getFollowers.bind(servers[1].follows)
76 ]
77
78 for (const fn of fns) {
79 const body = await fn({ start: 0, count: 5, sort: 'createdAt', state: 'accepted' })
80 expect(body.total).to.equal(0)
81 }
82}
83
84describe('Test follows moderation', function () {
85 let servers: PeerTubeServer[] = []
86 let commands: FollowsCommand[]
87
88 before(async function () {
89 this.timeout(240000)
90
91 servers = await createMultipleServers(3)
92
93 // Get the access tokens
94 await setAccessTokensToServers(servers)
95
96 commands = servers.map(s => s.follows)
97 })
98
99 describe('Default behaviour', function () {
100
101 it('Should have server 1 following server 2', async function () {
102 this.timeout(30000)
103
104 await commands[0].follow({ hosts: [ servers[1].url ] })
105
106 await waitJobs(servers)
107 })
108
109 it('Should have correct follows', async function () {
110 await checkServer1And2HasFollowers(servers)
111 })
112
113 it('Should remove follower on server 2', async function () {
114 await commands[1].removeFollower({ follower: servers[0] })
115
116 await waitJobs(servers)
117 })
118
119 it('Should not not have follows anymore', async function () {
120 await checkNoFollowers(servers)
121 })
122 })
123
124 describe('Disabled/Enabled followers', function () {
125
126 it('Should disable followers on server 2', async function () {
127 const subConfig = {
128 followers: {
129 instance: {
130 enabled: false,
131 manualApproval: false
132 }
133 }
134 }
135
136 await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
137
138 await commands[0].follow({ hosts: [ servers[1].url ] })
139 await waitJobs(servers)
140
141 await checkNoFollowers(servers)
142 })
143
144 it('Should re enable followers on server 2', async function () {
145 const subConfig = {
146 followers: {
147 instance: {
148 enabled: true,
149 manualApproval: false
150 }
151 }
152 }
153
154 await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
155
156 await commands[0].follow({ hosts: [ servers[1].url ] })
157 await waitJobs(servers)
158
159 await checkServer1And2HasFollowers(servers)
160 })
161 })
162
163 describe('Manual approbation', function () {
164
165 it('Should manually approve followers', async function () {
166 this.timeout(20000)
167
168 await commands[0].unfollow({ target: servers[1] })
169 await waitJobs(servers)
170
171 const subConfig = {
172 followers: {
173 instance: {
174 enabled: true,
175 manualApproval: true
176 }
177 }
178 }
179
180 await servers[1].config.updateCustomSubConfig({ newConfig: subConfig })
181 await servers[2].config.updateCustomSubConfig({ newConfig: subConfig })
182
183 await commands[0].follow({ hosts: [ servers[1].url ] })
184 await waitJobs(servers)
185
186 await checkServer1And2HasFollowers(servers, 'pending')
187 })
188
189 it('Should accept a follower', async function () {
190 await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host })
191 await waitJobs(servers)
192
193 await checkServer1And2HasFollowers(servers)
194 })
195
196 it('Should reject another follower', async function () {
197 this.timeout(20000)
198
199 await commands[0].follow({ hosts: [ servers[2].url ] })
200 await waitJobs(servers)
201
202 {
203 const body = await commands[0].getFollowings()
204 expect(body.total).to.equal(2)
205 }
206
207 {
208 const body = await commands[1].getFollowers()
209 expect(body.total).to.equal(1)
210 }
211
212 {
213 const body = await commands[2].getFollowers()
214 expect(body.total).to.equal(1)
215 }
216
217 await commands[2].rejectFollower({ follower: 'peertube@' + servers[0].host })
218 await waitJobs(servers)
219
220 { // server 1
221 {
222 const { data } = await commands[0].getFollowings({ state: 'accepted' })
223 expect(data).to.have.lengthOf(1)
224 }
225
226 {
227 const { data } = await commands[0].getFollowings({ state: 'rejected' })
228 expect(data).to.have.lengthOf(1)
229 expectStartWith(data[0].following.url, servers[2].url)
230 }
231 }
232
233 { // server 3
234 {
235 const { data } = await commands[2].getFollowers({ state: 'accepted' })
236 expect(data).to.have.lengthOf(0)
237 }
238
239 {
240 const { data } = await commands[2].getFollowers({ state: 'rejected' })
241 expect(data).to.have.lengthOf(1)
242 expectStartWith(data[0].follower.url, servers[0].url)
243 }
244 }
245 })
246
247 it('Should still auto accept channel followers', async function () {
248 await commands[0].follow({ handles: [ 'root_channel@' + servers[1].host ] })
249
250 await waitJobs(servers)
251
252 const body = await commands[0].getFollowings()
253 const follow = body.data[0]
254 expect(follow.following.name).to.equal('root_channel')
255 expect(follow.state).to.equal('accepted')
256 })
257 })
258
259 describe('Accept/reject state', function () {
260
261 it('Should not change the follow on refollow with and without auto accept', async function () {
262 const run = async () => {
263 await commands[0].follow({ hosts: [ servers[2].url ] })
264 await waitJobs(servers)
265
266 await checkFollows({
267 follower: servers[0],
268 followerState: 'rejected',
269 following: servers[2],
270 followingState: 'rejected'
271 })
272 }
273
274 await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: false } } } })
275 await run()
276
277 await servers[2].config.updateExistingSubConfig({ newConfig: { followers: { instance: { manualApproval: true } } } })
278 await run()
279 })
280
281 it('Should not change the rejected status on unfollow', async function () {
282 await commands[0].unfollow({ target: servers[2] })
283 await waitJobs(servers)
284
285 await checkFollows({
286 follower: servers[0],
287 followerState: 'deleted',
288 following: servers[2],
289 followingState: 'rejected'
290 })
291 })
292
293 it('Should delete the follower and add again the follower', async function () {
294 await commands[2].removeFollower({ follower: servers[0] })
295 await waitJobs(servers)
296
297 await commands[0].follow({ hosts: [ servers[2].url ] })
298 await waitJobs(servers)
299
300 await checkFollows({
301 follower: servers[0],
302 followerState: 'pending',
303 following: servers[2],
304 followingState: 'pending'
305 })
306 })
307
308 it('Should be able to reject a previously accepted follower', async function () {
309 await commands[1].rejectFollower({ follower: 'peertube@' + servers[0].host })
310 await waitJobs(servers)
311
312 await checkFollows({
313 follower: servers[0],
314 followerState: 'rejected',
315 following: servers[1],
316 followingState: 'rejected'
317 })
318 })
319
320 it('Should be able to re accept a previously rejected follower', async function () {
321 await commands[1].acceptFollower({ follower: 'peertube@' + servers[0].host })
322 await waitJobs(servers)
323
324 await checkFollows({
325 follower: servers[0],
326 followerState: 'accepted',
327 following: servers[1],
328 followingState: 'accepted'
329 })
330 })
331 })
332
333 describe('Muted servers', function () {
334
335 it('Should ignore follow requests of muted servers', async function () {
336 await servers[1].blocklist.addToServerBlocklist({ server: servers[0].host })
337
338 await commands[0].unfollow({ target: servers[1] })
339
340 await waitJobs(servers)
341
342 await checkFollows({
343 follower: servers[0],
344 followerState: 'deleted',
345 following: servers[1],
346 followingState: 'deleted'
347 })
348
349 await commands[0].follow({ hosts: [ servers[1].host ] })
350 await waitJobs(servers)
351
352 await checkFollows({
353 follower: servers[0],
354 followerState: 'rejected',
355 following: servers[1],
356 followingState: 'deleted'
357 })
358 })
359 })
360
361 after(async function () {
362 await cleanupTests(servers)
363 })
364})
diff --git a/packages/tests/src/api/server/follows.ts b/packages/tests/src/api/server/follows.ts
new file mode 100644
index 000000000..fbe2e87da
--- /dev/null
+++ b/packages/tests/src/api/server/follows.ts
@@ -0,0 +1,644 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { Video, VideoPrivacy } from '@peertube/peertube-models'
5import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@peertube/peertube-server-commands'
6import { expectAccountFollows, expectChannelsFollows } from '@tests/shared/actors.js'
7import { testCaptionFile } from '@tests/shared/captions.js'
8import { dateIsValid } from '@tests/shared/checks.js'
9import { completeVideoCheck } from '@tests/shared/videos.js'
10
11describe('Test follows', function () {
12
13 describe('Complex follow', function () {
14 let servers: PeerTubeServer[] = []
15
16 before(async function () {
17 this.timeout(120000)
18
19 servers = await createMultipleServers(3)
20
21 // Get the access tokens
22 await setAccessTokensToServers(servers)
23 })
24
25 describe('Data propagation after follow', function () {
26
27 it('Should not have followers/followings', async function () {
28 for (const server of servers) {
29 const bodies = await Promise.all([
30 server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }),
31 server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
32 ])
33
34 for (const body of bodies) {
35 expect(body.total).to.equal(0)
36
37 const follows = body.data
38 expect(follows).to.be.an('array')
39 expect(follows).to.have.lengthOf(0)
40 }
41 }
42 })
43
44 it('Should have server 1 following root account of server 2 and server 3', async function () {
45 this.timeout(30000)
46
47 await servers[0].follows.follow({
48 hosts: [ servers[2].url ],
49 handles: [ 'root@' + servers[1].host ]
50 })
51
52 await waitJobs(servers)
53 })
54
55 it('Should have 2 followings on server 1', async function () {
56 const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' })
57 expect(body.total).to.equal(2)
58
59 let follows = body.data
60 expect(follows).to.be.an('array')
61 expect(follows).to.have.lengthOf(1)
62
63 const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' })
64 follows = follows.concat(body2.data)
65
66 const server2Follow = follows.find(f => f.following.host === servers[1].host)
67 const server3Follow = follows.find(f => f.following.host === servers[2].host)
68
69 expect(server2Follow).to.not.be.undefined
70 expect(server2Follow.following.name).to.equal('root')
71 expect(server2Follow.state).to.equal('accepted')
72
73 expect(server3Follow).to.not.be.undefined
74 expect(server3Follow.following.name).to.equal('peertube')
75 expect(server3Follow.state).to.equal('accepted')
76 })
77
78 it('Should have 0 followings on server 2 and 3', async function () {
79 for (const server of [ servers[1], servers[2] ]) {
80 const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' })
81 expect(body.total).to.equal(0)
82
83 const follows = body.data
84 expect(follows).to.be.an('array')
85 expect(follows).to.have.lengthOf(0)
86 }
87 })
88
89 it('Should have 1 followers on server 3', async function () {
90 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
91 expect(body.total).to.equal(1)
92
93 const follows = body.data
94 expect(follows).to.be.an('array')
95 expect(follows).to.have.lengthOf(1)
96 expect(follows[0].follower.host).to.equal(servers[0].host)
97 })
98
99 it('Should have 0 followers on server 1 and 2', async function () {
100 for (const server of [ servers[0], servers[1] ]) {
101 const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' })
102 expect(body.total).to.equal(0)
103
104 const follows = body.data
105 expect(follows).to.be.an('array')
106 expect(follows).to.have.lengthOf(0)
107 }
108 })
109
110 it('Should search/filter followings on server 1', async function () {
111 const sort = 'createdAt'
112 const start = 0
113 const count = 1
114
115 {
116 const search = ':' + servers[1].port
117
118 {
119 const body = await servers[0].follows.getFollowings({ start, count, sort, search })
120 expect(body.total).to.equal(1)
121
122 const follows = body.data
123 expect(follows).to.have.lengthOf(1)
124 expect(follows[0].following.host).to.equal(servers[1].host)
125 }
126
127 {
128 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' })
129 expect(body.total).to.equal(1)
130 expect(body.data).to.have.lengthOf(1)
131 }
132
133 {
134 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
135 expect(body.total).to.equal(1)
136 expect(body.data).to.have.lengthOf(1)
137 }
138
139 {
140 const body = await servers[0].follows.getFollowings({
141 start,
142 count,
143 sort,
144 search,
145 state: 'accepted',
146 actorType: 'Application'
147 })
148 expect(body.total).to.equal(0)
149 expect(body.data).to.have.lengthOf(0)
150 }
151
152 {
153 const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' })
154 expect(body.total).to.equal(0)
155 expect(body.data).to.have.lengthOf(0)
156 }
157 }
158
159 {
160 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' })
161 expect(body.total).to.equal(1)
162 expect(body.data).to.have.lengthOf(1)
163 }
164
165 {
166 const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' })
167 expect(body.total).to.equal(0)
168
169 expect(body.data).to.have.lengthOf(0)
170 }
171 })
172
173 it('Should search/filter followers on server 2', async function () {
174 const start = 0
175 const count = 5
176 const sort = 'createdAt'
177
178 {
179 const search = servers[0].port + ''
180
181 {
182 const body = await servers[2].follows.getFollowers({ start, count, sort, search })
183 expect(body.total).to.equal(1)
184
185 const follows = body.data
186 expect(follows).to.have.lengthOf(1)
187 expect(follows[0].following.host).to.equal(servers[2].host)
188 }
189
190 {
191 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' })
192 expect(body.total).to.equal(1)
193 expect(body.data).to.have.lengthOf(1)
194 }
195
196 {
197 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' })
198 expect(body.total).to.equal(0)
199 expect(body.data).to.have.lengthOf(0)
200 }
201
202 {
203 const body = await servers[2].follows.getFollowers({
204 start,
205 count,
206 sort,
207 search,
208 state: 'accepted',
209 actorType: 'Application'
210 })
211 expect(body.total).to.equal(1)
212 expect(body.data).to.have.lengthOf(1)
213 }
214
215 {
216 const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' })
217 expect(body.total).to.equal(0)
218 expect(body.data).to.have.lengthOf(0)
219 }
220 }
221
222 {
223 const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' })
224 expect(body.total).to.equal(0)
225
226 const follows = body.data
227 expect(follows).to.have.lengthOf(0)
228 }
229 })
230
231 it('Should have the correct follows counts', async function () {
232 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
233 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
234 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
235
236 // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
237 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
238 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
239 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
240
241 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
242 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
243 })
244
245 it('Should unfollow server 3 on server 1', async function () {
246 this.timeout(15000)
247
248 await servers[0].follows.unfollow({ target: servers[2] })
249
250 await waitJobs(servers)
251 })
252
253 it('Should not follow server 3 on server 1 anymore', async function () {
254 const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' })
255 expect(body.total).to.equal(1)
256
257 const follows = body.data
258 expect(follows).to.be.an('array')
259 expect(follows).to.have.lengthOf(1)
260
261 expect(follows[0].following.host).to.equal(servers[1].host)
262 })
263
264 it('Should not have server 1 as follower on server 3 anymore', async function () {
265 const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' })
266 expect(body.total).to.equal(0)
267
268 const follows = body.data
269 expect(follows).to.be.an('array')
270 expect(follows).to.have.lengthOf(0)
271 })
272
273 it('Should have the correct follows counts after the unfollow', async function () {
274 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
275 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
276 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
277
278 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
279 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 })
280 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
281
282 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 })
283 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 })
284 })
285
286 it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
287 this.timeout(160000)
288
289 await servers[1].videos.upload({ attributes: { name: 'server2' } })
290 await servers[2].videos.upload({ attributes: { name: 'server3' } })
291
292 await waitJobs(servers)
293
294 {
295 const { total, data } = await servers[0].videos.list()
296 expect(total).to.equal(1)
297 expect(data[0].name).to.equal('server2')
298 }
299
300 {
301 const { total, data } = await servers[1].videos.list()
302 expect(total).to.equal(1)
303 expect(data[0].name).to.equal('server2')
304 }
305
306 {
307 const { total, data } = await servers[2].videos.list()
308 expect(total).to.equal(1)
309 expect(data[0].name).to.equal('server3')
310 }
311 })
312
313 it('Should remove account follow', async function () {
314 this.timeout(15000)
315
316 await servers[0].follows.unfollow({ target: 'root@' + servers[1].host })
317
318 await waitJobs(servers)
319 })
320
321 it('Should have removed the account follow', async function () {
322 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
323 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
324
325 {
326 const { total, data } = await servers[0].follows.getFollowings()
327 expect(total).to.equal(0)
328 expect(data).to.have.lengthOf(0)
329 }
330
331 {
332 const { total, data } = await servers[0].videos.list()
333 expect(total).to.equal(0)
334 expect(data).to.have.lengthOf(0)
335 }
336 })
337
338 it('Should follow a channel', async function () {
339 this.timeout(15000)
340
341 await servers[0].follows.follow({
342 handles: [ 'root_channel@' + servers[1].host ]
343 })
344
345 await waitJobs(servers)
346
347 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
348 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
349
350 {
351 const { total, data } = await servers[0].follows.getFollowings()
352 expect(total).to.equal(1)
353 expect(data).to.have.lengthOf(1)
354 }
355
356 {
357 const { total, data } = await servers[0].videos.list()
358 expect(total).to.equal(1)
359 expect(data).to.have.lengthOf(1)
360 }
361 })
362 })
363
364 describe('Should propagate data on a new server follow', function () {
365 let video4: Video
366
367 before(async function () {
368 this.timeout(240000)
369
370 const video4Attributes = {
371 name: 'server3-4',
372 category: 2,
373 nsfw: true,
374 licence: 6,
375 tags: [ 'tag1', 'tag2', 'tag3' ]
376 }
377
378 await servers[2].videos.upload({ attributes: { name: 'server3-2' } })
379 await servers[2].videos.upload({ attributes: { name: 'server3-3' } })
380
381 const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes })
382
383 await servers[2].videos.upload({ attributes: { name: 'server3-5' } })
384 await servers[2].videos.upload({ attributes: { name: 'server3-6' } })
385
386 {
387 const userAccessToken = await servers[2].users.generateUserAndToken('captain')
388
389 await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' })
390 await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' })
391 }
392
393 {
394 await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' })
395
396 await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' })
397 await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' })
398 await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' })
399 }
400
401 {
402 const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' })
403 await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' })
404
405 const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' })
406
407 await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' })
408
409 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId })
410 await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId })
411 }
412
413 await servers[2].captions.add({
414 language: 'ar',
415 videoId: video4CreateResult.id,
416 fixture: 'subtitle-good2.vtt'
417 })
418
419 await waitJobs(servers)
420
421 // Server 1 follows server 3
422 await servers[0].follows.follow({ hosts: [ servers[2].url ] })
423
424 await waitJobs(servers)
425 })
426
427 it('Should have the correct follows counts', async function () {
428 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 })
429 await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
430 await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
431 await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
432
433 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
434 await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 })
435 await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 })
436 await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 })
437
438 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 })
439 await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 })
440 })
441
442 it('Should have propagated videos', async function () {
443 const { total, data } = await servers[0].videos.list()
444 expect(total).to.equal(7)
445
446 const video2 = data.find(v => v.name === 'server3-2')
447 video4 = data.find(v => v.name === 'server3-4')
448 const video6 = data.find(v => v.name === 'server3-6')
449
450 expect(video2).to.not.be.undefined
451 expect(video4).to.not.be.undefined
452 expect(video6).to.not.be.undefined
453
454 const isLocal = false
455 const checkAttributes = {
456 name: 'server3-4',
457 category: 2,
458 licence: 6,
459 language: 'zh',
460 nsfw: true,
461 description: 'my super description',
462 support: 'my super support text',
463 account: {
464 name: 'root',
465 host: servers[2].host
466 },
467 isLocal,
468 commentsEnabled: true,
469 downloadEnabled: true,
470 duration: 5,
471 tags: [ 'tag1', 'tag2', 'tag3' ],
472 privacy: VideoPrivacy.PUBLIC,
473 likes: 1,
474 dislikes: 1,
475 channel: {
476 displayName: 'Main root channel',
477 name: 'root_channel',
478 description: '',
479 isLocal
480 },
481 fixture: 'video_short.webm',
482 files: [
483 {
484 resolution: 720,
485 size: 218910
486 }
487 ]
488 }
489 await completeVideoCheck({
490 server: servers[0],
491 originServer: servers[2],
492 videoUUID: video4.uuid,
493 attributes: checkAttributes
494 })
495 })
496
497 it('Should have propagated comments', async function () {
498 const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' })
499
500 expect(total).to.equal(2)
501 expect(data).to.be.an('array')
502 expect(data).to.have.lengthOf(2)
503
504 {
505 const comment = data[0]
506 expect(comment.inReplyToCommentId).to.be.null
507 expect(comment.text).equal('my super first comment')
508 expect(comment.videoId).to.equal(video4.id)
509 expect(comment.id).to.equal(comment.threadId)
510 expect(comment.account.name).to.equal('root')
511 expect(comment.account.host).to.equal(servers[2].host)
512 expect(comment.totalReplies).to.equal(3)
513 expect(dateIsValid(comment.createdAt as string)).to.be.true
514 expect(dateIsValid(comment.updatedAt as string)).to.be.true
515
516 const threadId = comment.threadId
517
518 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId })
519 expect(tree.comment.text).equal('my super first comment')
520 expect(tree.children).to.have.lengthOf(2)
521
522 const firstChild = tree.children[0]
523 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
524 expect(firstChild.children).to.have.lengthOf(1)
525
526 const childOfFirstChild = firstChild.children[0]
527 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
528 expect(childOfFirstChild.children).to.have.lengthOf(0)
529
530 const secondChild = tree.children[1]
531 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
532 expect(secondChild.children).to.have.lengthOf(0)
533 }
534
535 {
536 const deletedComment = data[1]
537 expect(deletedComment).to.not.be.undefined
538 expect(deletedComment.isDeleted).to.be.true
539 expect(deletedComment.deletedAt).to.not.be.null
540 expect(deletedComment.text).to.equal('')
541 expect(deletedComment.inReplyToCommentId).to.be.null
542 expect(deletedComment.account).to.be.null
543 expect(deletedComment.totalReplies).to.equal(2)
544 expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
545
546 const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId })
547 const [ commentRoot, deletedChildRoot ] = tree.children
548
549 expect(deletedChildRoot).to.not.be.undefined
550 expect(deletedChildRoot.comment.isDeleted).to.be.true
551 expect(deletedChildRoot.comment.deletedAt).to.not.be.null
552 expect(deletedChildRoot.comment.text).to.equal('')
553 expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
554 expect(deletedChildRoot.comment.account).to.be.null
555 expect(deletedChildRoot.children).to.have.lengthOf(1)
556
557 const answerToDeletedChild = deletedChildRoot.children[0]
558 expect(answerToDeletedChild.comment).to.not.be.undefined
559 expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id)
560 expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted')
561 expect(answerToDeletedChild.comment.account.name).to.equal('root')
562
563 expect(commentRoot.comment).to.not.be.undefined
564 expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
565 expect(commentRoot.comment.text).to.equal('answer to deleted')
566 expect(commentRoot.comment.account.name).to.equal('root')
567 }
568 })
569
570 it('Should have propagated captions', async function () {
571 const body = await servers[0].captions.list({ videoId: video4.id })
572 expect(body.total).to.equal(1)
573 expect(body.data).to.have.lengthOf(1)
574
575 const caption1 = body.data[0]
576 expect(caption1.language.id).to.equal('ar')
577 expect(caption1.language.label).to.equal('Arabic')
578 expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$'))
579 await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.')
580 })
581
582 it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () {
583 this.timeout(5000)
584
585 await servers[0].follows.unfollow({ target: servers[2] })
586
587 await waitJobs(servers)
588
589 const { total } = await servers[0].videos.list()
590 expect(total).to.equal(1)
591 })
592 })
593
594 after(async function () {
595 await cleanupTests(servers)
596 })
597 })
598
599 describe('Simple data propagation propagate data on a new channel follow', function () {
600 let servers: PeerTubeServer[] = []
601
602 before(async function () {
603 this.timeout(120000)
604
605 servers = await createMultipleServers(3)
606 await setAccessTokensToServers(servers)
607
608 await servers[0].videos.upload({ attributes: { name: 'video to add' } })
609
610 await waitJobs(servers)
611
612 for (const server of [ servers[1], servers[2] ]) {
613 const video = await server.videos.find({ name: 'video to add' })
614 expect(video).to.not.exist
615 }
616 })
617
618 it('Should have propagated video after new channel follow', async function () {
619 this.timeout(60000)
620
621 await servers[1].follows.follow({ handles: [ 'root_channel@' + servers[0].host ] })
622
623 await waitJobs(servers)
624
625 const video = await servers[1].videos.find({ name: 'video to add' })
626 expect(video).to.exist
627 })
628
629 it('Should have propagated video after new account follow', async function () {
630 this.timeout(60000)
631
632 await servers[2].follows.follow({ handles: [ 'root@' + servers[0].host ] })
633
634 await waitJobs(servers)
635
636 const video = await servers[2].videos.find({ name: 'video to add' })
637 expect(video).to.exist
638 })
639
640 after(async function () {
641 await cleanupTests(servers)
642 })
643 })
644})
diff --git a/packages/tests/src/api/server/handle-down.ts b/packages/tests/src/api/server/handle-down.ts
new file mode 100644
index 000000000..604df129f
--- /dev/null
+++ b/packages/tests/src/api/server/handle-down.ts
@@ -0,0 +1,339 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode, JobState, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 CommentsCommand,
9 createMultipleServers,
10 killallServers,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 waitJobs
14} from '@peertube/peertube-server-commands'
15import { SQLCommand } from '@tests/shared/sql-command.js'
16import { completeVideoCheck } from '@tests/shared/videos.js'
17
18describe('Test handle downs', function () {
19 let servers: PeerTubeServer[] = []
20 let sqlCommands: SQLCommand[] = []
21
22 let threadIdServer1: number
23 let threadIdServer2: number
24 let commentIdServer1: number
25 let commentIdServer2: number
26 let missedVideo1: VideoCreateResult
27 let missedVideo2: VideoCreateResult
28 let unlistedVideo: VideoCreateResult
29
30 const videoIdsServer1: string[] = []
31
32 const videoAttributes = {
33 name: 'my super name for server 1',
34 category: 5,
35 licence: 4,
36 language: 'ja',
37 nsfw: true,
38 privacy: VideoPrivacy.PUBLIC,
39 description: 'my super description for server 1',
40 support: 'my super support text for server 1',
41 tags: [ 'tag1p1', 'tag2p1' ],
42 fixture: 'video_short1.webm'
43 }
44
45 const unlistedVideoAttributes = { ...videoAttributes, privacy: VideoPrivacy.UNLISTED }
46
47 let checkAttributes: any
48 let unlistedCheckAttributes: any
49
50 let commentCommands: CommentsCommand[]
51
52 before(async function () {
53 this.timeout(120000)
54
55 servers = await createMultipleServers(3)
56 commentCommands = servers.map(s => s.comments)
57
58 checkAttributes = {
59 name: 'my super name for server 1',
60 category: 5,
61 licence: 4,
62 language: 'ja',
63 nsfw: true,
64 description: 'my super description for server 1',
65 support: 'my super support text for server 1',
66 account: {
67 name: 'root',
68 host: servers[0].host
69 },
70 isLocal: false,
71 duration: 10,
72 tags: [ 'tag1p1', 'tag2p1' ],
73 privacy: VideoPrivacy.PUBLIC,
74 commentsEnabled: true,
75 downloadEnabled: true,
76 channel: {
77 name: 'root_channel',
78 displayName: 'Main root channel',
79 description: '',
80 isLocal: false
81 },
82 fixture: 'video_short1.webm',
83 files: [
84 {
85 resolution: 720,
86 size: 572456
87 }
88 ]
89 }
90 unlistedCheckAttributes = { ...checkAttributes, privacy: VideoPrivacy.UNLISTED }
91
92 // Get the access tokens
93 await setAccessTokensToServers(servers)
94
95 sqlCommands = servers.map(s => new SQLCommand(s))
96 })
97
98 it('Should remove followers that are often down', async function () {
99 this.timeout(240000)
100
101 // Server 2 and 3 follow server 1
102 await servers[1].follows.follow({ hosts: [ servers[0].url ] })
103 await servers[2].follows.follow({ hosts: [ servers[0].url ] })
104
105 await waitJobs(servers)
106
107 // Upload a video to server 1
108 await servers[0].videos.upload({ attributes: videoAttributes })
109
110 await waitJobs(servers)
111
112 // And check all servers have this video
113 for (const server of servers) {
114 const { data } = await server.videos.list()
115 expect(data).to.be.an('array')
116 expect(data).to.have.lengthOf(1)
117 }
118
119 // Kill server 2
120 await killallServers([ servers[1] ])
121
122 // Remove server 2 follower
123 for (let i = 0; i < 10; i++) {
124 await servers[0].videos.upload({ attributes: videoAttributes })
125 }
126
127 await waitJobs([ servers[0], servers[2] ])
128
129 // Kill server 3
130 await killallServers([ servers[2] ])
131
132 missedVideo1 = await servers[0].videos.upload({ attributes: videoAttributes })
133
134 missedVideo2 = await servers[0].videos.upload({ attributes: videoAttributes })
135
136 // Unlisted video
137 unlistedVideo = await servers[0].videos.upload({ attributes: unlistedVideoAttributes })
138
139 // Add comments to video 2
140 {
141 const text = 'thread 1'
142 let comment = await commentCommands[0].createThread({ videoId: missedVideo2.uuid, text })
143 threadIdServer1 = comment.id
144
145 comment = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-1' })
146
147 const created = await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: comment.id, text: 'comment 1-2' })
148 commentIdServer1 = created.id
149 }
150
151 await waitJobs(servers[0])
152 // Wait scheduler
153 await wait(11000)
154
155 // Only server 3 is still a follower of server 1
156 const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' })
157 expect(body.data).to.be.an('array')
158 expect(body.data).to.have.lengthOf(1)
159 expect(body.data[0].follower.host).to.equal(servers[2].host)
160 })
161
162 it('Should not have pending/processing jobs anymore', async function () {
163 const states: JobState[] = [ 'waiting', 'active' ]
164
165 for (const state of states) {
166 const body = await servers[0].jobs.list({
167 state,
168 start: 0,
169 count: 50,
170 sort: '-createdAt'
171 })
172 expect(body.data).to.have.length(0)
173 }
174 })
175
176 it('Should re-follow server 1', async function () {
177 this.timeout(70000)
178
179 await servers[1].run()
180 await servers[2].run()
181
182 await servers[1].follows.unfollow({ target: servers[0] })
183 await waitJobs(servers)
184
185 await servers[1].follows.follow({ hosts: [ servers[0].url ] })
186
187 await waitJobs(servers)
188
189 const body = await servers[0].follows.getFollowers({ start: 0, count: 2, sort: 'createdAt' })
190 expect(body.data).to.be.an('array')
191 expect(body.data).to.have.lengthOf(2)
192 })
193
194 it('Should send an update to server 3, and automatically fetch the video', async function () {
195 this.timeout(15000)
196
197 {
198 const { data } = await servers[2].videos.list()
199 expect(data).to.be.an('array')
200 expect(data).to.have.lengthOf(11)
201 }
202
203 await servers[0].videos.update({ id: missedVideo1.uuid })
204 await servers[0].videos.update({ id: unlistedVideo.uuid })
205
206 await waitJobs(servers)
207
208 {
209 const { data } = await servers[2].videos.list()
210 expect(data).to.be.an('array')
211 // 1 video is unlisted
212 expect(data).to.have.lengthOf(12)
213 }
214
215 // Check unlisted video
216 const video = await servers[2].videos.get({ id: unlistedVideo.uuid })
217 await completeVideoCheck({ server: servers[2], originServer: servers[0], videoUUID: video.uuid, attributes: unlistedCheckAttributes })
218 })
219
220 it('Should send comments on a video to server 3, and automatically fetch the video', async function () {
221 this.timeout(25000)
222
223 await commentCommands[0].addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer1, text: 'comment 1-3' })
224
225 await waitJobs(servers)
226
227 await servers[2].videos.get({ id: missedVideo2.uuid })
228
229 {
230 const { data } = await servers[2].comments.listThreads({ videoId: missedVideo2.uuid })
231 expect(data).to.be.an('array')
232 expect(data).to.have.lengthOf(1)
233
234 threadIdServer2 = data[0].id
235
236 const tree = await servers[2].comments.getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer2 })
237 expect(tree.comment.text).equal('thread 1')
238 expect(tree.children).to.have.lengthOf(1)
239
240 const firstChild = tree.children[0]
241 expect(firstChild.comment.text).to.equal('comment 1-1')
242 expect(firstChild.children).to.have.lengthOf(1)
243
244 const childOfFirstChild = firstChild.children[0]
245 expect(childOfFirstChild.comment.text).to.equal('comment 1-2')
246 expect(childOfFirstChild.children).to.have.lengthOf(1)
247
248 const childOfChildFirstChild = childOfFirstChild.children[0]
249 expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3')
250 expect(childOfChildFirstChild.children).to.have.lengthOf(0)
251
252 commentIdServer2 = childOfChildFirstChild.comment.id
253 }
254 })
255
256 it('Should correctly reply to the comment', async function () {
257 this.timeout(15000)
258
259 await servers[2].comments.addReply({ videoId: missedVideo2.uuid, toCommentId: commentIdServer2, text: 'comment 1-4' })
260
261 await waitJobs(servers)
262
263 const tree = await commentCommands[0].getThread({ videoId: missedVideo2.uuid, threadId: threadIdServer1 })
264
265 expect(tree.comment.text).equal('thread 1')
266 expect(tree.children).to.have.lengthOf(1)
267
268 const firstChild = tree.children[0]
269 expect(firstChild.comment.text).to.equal('comment 1-1')
270 expect(firstChild.children).to.have.lengthOf(1)
271
272 const childOfFirstChild = firstChild.children[0]
273 expect(childOfFirstChild.comment.text).to.equal('comment 1-2')
274 expect(childOfFirstChild.children).to.have.lengthOf(1)
275
276 const childOfChildFirstChild = childOfFirstChild.children[0]
277 expect(childOfChildFirstChild.comment.text).to.equal('comment 1-3')
278 expect(childOfChildFirstChild.children).to.have.lengthOf(1)
279
280 const childOfChildOfChildOfFirstChild = childOfChildFirstChild.children[0]
281 expect(childOfChildOfChildOfFirstChild.comment.text).to.equal('comment 1-4')
282 expect(childOfChildOfChildOfFirstChild.children).to.have.lengthOf(0)
283 })
284
285 it('Should upload many videos on server 1', async function () {
286 this.timeout(240000)
287
288 for (let i = 0; i < 10; i++) {
289 const uuid = (await servers[0].videos.quickUpload({ name: 'video ' + i })).uuid
290 videoIdsServer1.push(uuid)
291 }
292
293 await waitJobs(servers)
294
295 for (const id of videoIdsServer1) {
296 await servers[1].videos.get({ id })
297 }
298
299 await waitJobs(servers)
300 await sqlCommands[1].setActorFollowScores(20)
301
302 // Wait video expiration
303 await wait(11000)
304
305 // Refresh video -> score + 10 = 30
306 await servers[1].videos.get({ id: videoIdsServer1[0] })
307
308 await waitJobs(servers)
309 })
310
311 it('Should remove followings that are down', async function () {
312 this.timeout(120000)
313
314 await killallServers([ servers[0] ])
315
316 // Wait video expiration
317 await wait(11000)
318
319 for (let i = 0; i < 5; i++) {
320 try {
321 await servers[1].videos.get({ id: videoIdsServer1[i] })
322 await waitJobs([ servers[1] ])
323 await wait(1500)
324 } catch {}
325 }
326
327 for (const id of videoIdsServer1) {
328 await servers[1].videos.get({ id, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
329 }
330 })
331
332 after(async function () {
333 for (const sqlCommand of sqlCommands) {
334 await sqlCommand.cleanup()
335 }
336
337 await cleanupTests(servers)
338 })
339})
diff --git a/packages/tests/src/api/server/homepage.ts b/packages/tests/src/api/server/homepage.ts
new file mode 100644
index 000000000..082a2fb91
--- /dev/null
+++ b/packages/tests/src/api/server/homepage.ts
@@ -0,0 +1,81 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 CustomPagesCommand,
9 killallServers,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar
14} from '@peertube/peertube-server-commands'
15
16async function getHomepageState (server: PeerTubeServer) {
17 const config = await server.config.getConfig()
18
19 return config.homepage.enabled
20}
21
22describe('Test instance homepage actions', function () {
23 let server: PeerTubeServer
24 let command: CustomPagesCommand
25
26 before(async function () {
27 this.timeout(30000)
28
29 server = await createSingleServer(1)
30 await setAccessTokensToServers([ server ])
31 await setDefaultChannelAvatar(server)
32 await setDefaultAccountAvatar(server)
33
34 command = server.customPage
35 })
36
37 it('Should not have a homepage', async function () {
38 const state = await getHomepageState(server)
39 expect(state).to.be.false
40
41 await command.getInstanceHomepage({ expectedStatus: HttpStatusCode.NOT_FOUND_404 })
42 })
43
44 it('Should set a homepage', async function () {
45 await command.updateInstanceHomepage({ content: '<picsou-magazine></picsou-magazine>' })
46
47 const page = await command.getInstanceHomepage()
48 expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
49
50 const state = await getHomepageState(server)
51 expect(state).to.be.true
52 })
53
54 it('Should have the same homepage after a restart', async function () {
55 this.timeout(30000)
56
57 await killallServers([ server ])
58
59 await server.run()
60
61 const page = await command.getInstanceHomepage()
62 expect(page.content).to.equal('<picsou-magazine></picsou-magazine>')
63
64 const state = await getHomepageState(server)
65 expect(state).to.be.true
66 })
67
68 it('Should empty the homepage', async function () {
69 await command.updateInstanceHomepage({ content: '' })
70
71 const page = await command.getInstanceHomepage()
72 expect(page.content).to.be.empty
73
74 const state = await getHomepageState(server)
75 expect(state).to.be.false
76 })
77
78 after(async function () {
79 await cleanupTests([ server ])
80 })
81})
diff --git a/packages/tests/src/api/server/index.ts b/packages/tests/src/api/server/index.ts
new file mode 100644
index 000000000..5c80a5a37
--- /dev/null
+++ b/packages/tests/src/api/server/index.ts
@@ -0,0 +1,22 @@
1import './auto-follows.js'
2import './bulk.js'
3import './config-defaults.js'
4import './config.js'
5import './contact-form.js'
6import './email.js'
7import './follow-constraints.js'
8import './follows.js'
9import './follows-moderation.js'
10import './homepage.js'
11import './handle-down.js'
12import './jobs.js'
13import './logs.js'
14import './reverse-proxy.js'
15import './services.js'
16import './slow-follows.js'
17import './stats.js'
18import './tracker.js'
19import './no-client.js'
20import './open-telemetry.js'
21import './plugins.js'
22import './proxy.js'
diff --git a/packages/tests/src/api/server/jobs.ts b/packages/tests/src/api/server/jobs.ts
new file mode 100644
index 000000000..3d60b1431
--- /dev/null
+++ b/packages/tests/src/api/server/jobs.ts
@@ -0,0 +1,128 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { dateIsValid } from '@tests/shared/checks.js'
5import { wait } from '@peertube/peertube-core-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test jobs', function () {
16 let servers: PeerTubeServer[]
17
18 before(async function () {
19 this.timeout(240000)
20
21 servers = await createMultipleServers(2)
22
23 await setAccessTokensToServers(servers)
24
25 // Server 1 and server 2 follow each other
26 await doubleFollow(servers[0], servers[1])
27 })
28
29 it('Should create some jobs', async function () {
30 this.timeout(240000)
31
32 await servers[1].videos.upload({ attributes: { name: 'video1' } })
33 await servers[1].videos.upload({ attributes: { name: 'video2' } })
34
35 await waitJobs(servers)
36 })
37
38 it('Should list jobs', async function () {
39 const body = await servers[1].jobs.list({ state: 'completed' })
40 expect(body.total).to.be.above(2)
41 expect(body.data).to.have.length.above(2)
42 })
43
44 it('Should list jobs with sort, pagination and job type', async function () {
45 {
46 const body = await servers[1].jobs.list({
47 state: 'completed',
48 start: 1,
49 count: 2,
50 sort: 'createdAt'
51 })
52 expect(body.total).to.be.above(2)
53 expect(body.data).to.have.lengthOf(2)
54
55 let job = body.data[0]
56 // Skip repeat jobs
57 if (job.type === 'videos-views-stats') job = body.data[1]
58
59 expect(job.state).to.equal('completed')
60 expect(dateIsValid(job.createdAt as string)).to.be.true
61 expect(dateIsValid(job.processedOn as string)).to.be.true
62 expect(dateIsValid(job.finishedOn as string)).to.be.true
63 }
64
65 {
66 const body = await servers[1].jobs.list({
67 state: 'completed',
68 start: 0,
69 count: 100,
70 sort: 'createdAt',
71 jobType: 'activitypub-http-broadcast'
72 })
73 expect(body.total).to.be.above(2)
74
75 for (const j of body.data) {
76 expect(j.type).to.equal('activitypub-http-broadcast')
77 }
78 }
79 })
80
81 it('Should list all jobs', async function () {
82 const body = await servers[1].jobs.list()
83 expect(body.total).to.be.above(2)
84
85 const jobs = body.data
86 expect(jobs).to.have.length.above(2)
87
88 expect(jobs.find(j => j.state === 'completed')).to.not.be.undefined
89 })
90
91 it('Should pause the job queue', async function () {
92 this.timeout(120000)
93
94 const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video2' } })
95 await waitJobs(servers)
96
97 await servers[1].jobs.pauseJobQueue()
98 await servers[1].videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' })
99
100 await wait(5000)
101
102 {
103 const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
104 // waiting includes waiting-children
105 expect(body.data).to.have.lengthOf(4)
106 }
107
108 {
109 const body = await servers[1].jobs.list({ state: 'waiting-children', jobType: 'video-transcoding' })
110 expect(body.data).to.have.lengthOf(1)
111 }
112 })
113
114 it('Should resume the job queue', async function () {
115 this.timeout(120000)
116
117 await servers[1].jobs.resumeJobQueue()
118
119 await waitJobs(servers)
120
121 const body = await servers[1].jobs.list({ state: 'waiting', jobType: 'video-transcoding' })
122 expect(body.data).to.have.lengthOf(0)
123 })
124
125 after(async function () {
126 await cleanupTests(servers)
127 })
128})
diff --git a/packages/tests/src/api/server/logs.ts b/packages/tests/src/api/server/logs.ts
new file mode 100644
index 000000000..11c86d694
--- /dev/null
+++ b/packages/tests/src/api/server/logs.ts
@@ -0,0 +1,265 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 killallServers,
9 LogsCommand,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 waitJobs
13} from '@peertube/peertube-server-commands'
14
15describe('Test logs', function () {
16 let server: PeerTubeServer
17 let logsCommand: LogsCommand
18
19 before(async function () {
20 this.timeout(30000)
21
22 server = await createSingleServer(1)
23 await setAccessTokensToServers([ server ])
24
25 logsCommand = server.logs
26 })
27
28 describe('With the standard log file', function () {
29
30 it('Should get logs with a start date', async function () {
31 this.timeout(60000)
32
33 await server.videos.upload({ attributes: { name: 'video 1' } })
34 await waitJobs([ server ])
35
36 const now = new Date()
37
38 await server.videos.upload({ attributes: { name: 'video 2' } })
39 await waitJobs([ server ])
40
41 const body = await logsCommand.getLogs({ startDate: now })
42 const logsString = JSON.stringify(body)
43
44 expect(logsString.includes('Video with name video 1')).to.be.false
45 expect(logsString.includes('Video with name video 2')).to.be.true
46 })
47
48 it('Should get logs with an end date', async function () {
49 this.timeout(60000)
50
51 await server.videos.upload({ attributes: { name: 'video 3' } })
52 await waitJobs([ server ])
53
54 const now1 = new Date()
55
56 await server.videos.upload({ attributes: { name: 'video 4' } })
57 await waitJobs([ server ])
58
59 const now2 = new Date()
60
61 await server.videos.upload({ attributes: { name: 'video 5' } })
62 await waitJobs([ server ])
63
64 const body = await logsCommand.getLogs({ startDate: now1, endDate: now2 })
65 const logsString = JSON.stringify(body)
66
67 expect(logsString.includes('Video with name video 3')).to.be.false
68 expect(logsString.includes('Video with name video 4')).to.be.true
69 expect(logsString.includes('Video with name video 5')).to.be.false
70 })
71
72 it('Should filter by level', async function () {
73 this.timeout(60000)
74
75 const now = new Date()
76
77 await server.videos.upload({ attributes: { name: 'video 6' } })
78 await waitJobs([ server ])
79
80 {
81 const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
82 const logsString = JSON.stringify(body)
83
84 expect(logsString.includes('Video with name video 6')).to.be.true
85 }
86
87 {
88 const body = await logsCommand.getLogs({ startDate: now, level: 'warn' })
89 const logsString = JSON.stringify(body)
90
91 expect(logsString.includes('Video with name video 6')).to.be.false
92 }
93 })
94
95 it('Should filter by tag', async function () {
96 const now = new Date()
97
98 const { uuid } = await server.videos.upload({ attributes: { name: 'video 6' } })
99 await waitJobs([ server ])
100
101 {
102 const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ 'toto' ] })
103 expect(body).to.have.lengthOf(0)
104 }
105
106 {
107 const body = await logsCommand.getLogs({ startDate: now, level: 'debug', tagsOneOf: [ uuid ] })
108 expect(body).to.not.have.lengthOf(0)
109
110 for (const line of body) {
111 expect(line.tags).to.contain(uuid)
112 }
113 }
114 })
115
116 it('Should log ping requests', async function () {
117 const now = new Date()
118
119 await server.servers.ping()
120
121 const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
122 const logsString = JSON.stringify(body)
123
124 expect(logsString.includes('/api/v1/ping')).to.be.true
125 })
126
127 it('Should not log ping requests', async function () {
128 this.timeout(60000)
129
130 await killallServers([ server ])
131
132 await server.run({ log: { log_ping_requests: false } })
133
134 const now = new Date()
135
136 await server.servers.ping()
137
138 const body = await logsCommand.getLogs({ startDate: now, level: 'info' })
139 const logsString = JSON.stringify(body)
140
141 expect(logsString.includes('/api/v1/ping')).to.be.false
142 })
143 })
144
145 describe('With the audit log', function () {
146
147 it('Should get logs with a start date', async function () {
148 this.timeout(60000)
149
150 await server.videos.upload({ attributes: { name: 'video 7' } })
151 await waitJobs([ server ])
152
153 const now = new Date()
154
155 await server.videos.upload({ attributes: { name: 'video 8' } })
156 await waitJobs([ server ])
157
158 const body = await logsCommand.getAuditLogs({ startDate: now })
159 const logsString = JSON.stringify(body)
160
161 expect(logsString.includes('video 7')).to.be.false
162 expect(logsString.includes('video 8')).to.be.true
163
164 expect(body).to.have.lengthOf(1)
165
166 const item = body[0]
167
168 const message = JSON.parse(item.message)
169 expect(message.domain).to.equal('videos')
170 expect(message.action).to.equal('create')
171 })
172
173 it('Should get logs with an end date', async function () {
174 this.timeout(60000)
175
176 await server.videos.upload({ attributes: { name: 'video 9' } })
177 await waitJobs([ server ])
178
179 const now1 = new Date()
180
181 await server.videos.upload({ attributes: { name: 'video 10' } })
182 await waitJobs([ server ])
183
184 const now2 = new Date()
185
186 await server.videos.upload({ attributes: { name: 'video 11' } })
187 await waitJobs([ server ])
188
189 const body = await logsCommand.getAuditLogs({ startDate: now1, endDate: now2 })
190 const logsString = JSON.stringify(body)
191
192 expect(logsString.includes('video 9')).to.be.false
193 expect(logsString.includes('video 10')).to.be.true
194 expect(logsString.includes('video 11')).to.be.false
195 })
196 })
197
198 describe('When creating log from the client', function () {
199
200 it('Should create a warn client log', async function () {
201 const now = new Date()
202
203 await server.logs.createLogClient({
204 payload: {
205 level: 'warn',
206 url: 'http://example.com',
207 message: 'my super client message'
208 },
209 token: null
210 })
211
212 const body = await logsCommand.getLogs({ startDate: now })
213 const logsString = JSON.stringify(body)
214
215 expect(logsString.includes('my super client message')).to.be.true
216 })
217
218 it('Should create an error authenticated client log', async function () {
219 const now = new Date()
220
221 await server.logs.createLogClient({
222 payload: {
223 url: 'https://example.com/page1',
224 level: 'error',
225 message: 'my super client message 2',
226 userAgent: 'super user agent',
227 meta: '{hello}',
228 stackTrace: 'super stack trace'
229 }
230 })
231
232 const body = await logsCommand.getLogs({ startDate: now })
233 const logsString = JSON.stringify(body)
234
235 expect(logsString.includes('my super client message 2')).to.be.true
236 expect(logsString.includes('super user agent')).to.be.true
237 expect(logsString.includes('super stack trace')).to.be.true
238 expect(logsString.includes('{hello}')).to.be.true
239 expect(logsString.includes('https://example.com/page1')).to.be.true
240 })
241
242 it('Should refuse to create client logs', async function () {
243 await server.kill()
244
245 await server.run({
246 log: {
247 accept_client_log: false
248 }
249 })
250
251 await server.logs.createLogClient({
252 payload: {
253 level: 'warn',
254 url: 'http://example.com',
255 message: 'my super client message'
256 },
257 expectedStatus: HttpStatusCode.FORBIDDEN_403
258 })
259 })
260 })
261
262 after(async function () {
263 await cleanupTests([ server ])
264 })
265})
diff --git a/packages/tests/src/api/server/no-client.ts b/packages/tests/src/api/server/no-client.ts
new file mode 100644
index 000000000..0f097d50b
--- /dev/null
+++ b/packages/tests/src/api/server/no-client.ts
@@ -0,0 +1,24 @@
1import request from 'supertest'
2import { HttpStatusCode } from '@peertube/peertube-models'
3import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands'
4
5describe('Start and stop server without web client routes', function () {
6 let server: PeerTubeServer
7
8 before(async function () {
9 this.timeout(30000)
10
11 server = await createSingleServer(1, {}, { peertubeArgs: [ '--no-client' ] })
12 })
13
14 it('Should fail getting the client', function () {
15 const req = request(server.url)
16 .get('/')
17
18 return req.expect(HttpStatusCode.NOT_FOUND_404)
19 })
20
21 after(async function () {
22 await cleanupTests([ server ])
23 })
24})
diff --git a/packages/tests/src/api/server/open-telemetry.ts b/packages/tests/src/api/server/open-telemetry.ts
new file mode 100644
index 000000000..8ed3801db
--- /dev/null
+++ b/packages/tests/src/api/server/open-telemetry.ts
@@ -0,0 +1,193 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode, PlaybackMetricCreate, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 makeRawRequest,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12import { expectLogDoesNotContain, expectLogContain } from '@tests/shared/checks.js'
13import { MockHTTP } from '@tests/shared/mock-servers/mock-http.js'
14
15describe('Open Telemetry', function () {
16 let server: PeerTubeServer
17
18 describe('Metrics', function () {
19 const metricsUrl = 'http://127.0.0.1:9092/metrics'
20
21 it('Should not enable open telemetry metrics', async function () {
22 this.timeout(60000)
23
24 server = await createSingleServer(1)
25
26 let hasError = false
27 try {
28 await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
29 } catch (err) {
30 hasError = err.message.includes('ECONNREFUSED')
31 }
32
33 expect(hasError).to.be.true
34
35 await server.kill()
36 })
37
38 it('Should enable open telemetry metrics', async function () {
39 this.timeout(120000)
40
41 await server.run({
42 open_telemetry: {
43 metrics: {
44 enabled: true
45 }
46 }
47 })
48
49 // Simulate a HTTP request
50 await server.videos.list()
51
52 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
53 expect(res.text).to.contain('peertube_job_queue_total{')
54 expect(res.text).to.contain('http_request_duration_ms_bucket{')
55 })
56
57 it('Should have playback metrics', async function () {
58 await setAccessTokensToServers([ server ])
59
60 const video = await server.videos.quickUpload({ name: 'video' })
61
62 await server.metrics.addPlaybackMetric({
63 metrics: {
64 playerMode: 'p2p-media-loader',
65 resolution: VideoResolution.H_1080P,
66 fps: 30,
67 resolutionChanges: 1,
68 errors: 2,
69 downloadedBytesP2P: 0,
70 downloadedBytesHTTP: 0,
71 uploadedBytesP2P: 5,
72 p2pPeers: 1,
73 p2pEnabled: false,
74 videoId: video.uuid
75 }
76 })
77
78 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
79
80 expect(res.text).to.contain('peertube_playback_http_downloaded_bytes_total{')
81 expect(res.text).to.contain('peertube_playback_p2p_peers{')
82 expect(res.text).to.contain('p2pEnabled="false"')
83 })
84
85 it('Should take the last playback metric', async function () {
86 await setAccessTokensToServers([ server ])
87
88 const video = await server.videos.quickUpload({ name: 'video' })
89
90 const metrics = {
91 playerMode: 'p2p-media-loader',
92 resolution: VideoResolution.H_1080P,
93 fps: 30,
94 resolutionChanges: 1,
95 errors: 2,
96 downloadedBytesP2P: 0,
97 downloadedBytesHTTP: 0,
98 uploadedBytesP2P: 5,
99 p2pPeers: 7,
100 p2pEnabled: false,
101 videoId: video.uuid
102 } as PlaybackMetricCreate
103
104 await server.metrics.addPlaybackMetric({ metrics })
105
106 metrics.p2pPeers = 42
107 await server.metrics.addPlaybackMetric({ metrics })
108
109 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
110
111 // eslint-disable-next-line max-len
112 const label = `{videoOrigin="local",playerMode="p2p-media-loader",resolution="1080",fps="30",p2pEnabled="false",videoUUID="${video.uuid}"}`
113 expect(res.text).to.contain(`peertube_playback_p2p_peers${label} 42`)
114 expect(res.text).to.not.contain(`peertube_playback_p2p_peers${label} 7`)
115 })
116
117 it('Should disable http request duration metrics', async function () {
118 await server.kill()
119
120 await server.run({
121 open_telemetry: {
122 metrics: {
123 enabled: true,
124 http_request_duration: {
125 enabled: false
126 }
127 }
128 }
129 })
130
131 // Simulate a HTTP request
132 await server.videos.list()
133
134 const res = await makeRawRequest({ url: metricsUrl, expectedStatus: HttpStatusCode.OK_200 })
135 expect(res.text).to.not.contain('http_request_duration_ms_bucket{')
136 })
137
138 after(async function () {
139 await server.kill()
140 })
141 })
142
143 describe('Tracing', function () {
144 let mockHTTP: MockHTTP
145 let mockPort: number
146
147 before(async function () {
148 mockHTTP = new MockHTTP()
149 mockPort = await mockHTTP.initialize()
150 })
151
152 it('Should enable open telemetry tracing', async function () {
153 server = await createSingleServer(1)
154
155 await expectLogDoesNotContain(server, 'Registering Open Telemetry tracing')
156
157 await server.kill()
158 })
159
160 it('Should enable open telemetry metrics', async function () {
161 await server.run({
162 open_telemetry: {
163 tracing: {
164 enabled: true,
165 jaeger_exporter: {
166 endpoint: 'http://127.0.0.1:' + mockPort
167 }
168 }
169 }
170 })
171
172 await expectLogContain(server, 'Registering Open Telemetry tracing')
173 })
174
175 it('Should upload a video and correctly works', async function () {
176 await setAccessTokensToServers([ server ])
177
178 const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PUBLIC })
179
180 const video = await server.videos.get({ id: uuid })
181
182 expect(video.name).to.equal('video')
183 })
184
185 after(async function () {
186 await mockHTTP.terminate()
187 })
188 })
189
190 after(async function () {
191 await cleanupTests([ server ])
192 })
193})
diff --git a/packages/tests/src/api/server/plugins.ts b/packages/tests/src/api/server/plugins.ts
new file mode 100644
index 000000000..a78cea025
--- /dev/null
+++ b/packages/tests/src/api/server/plugins.ts
@@ -0,0 +1,410 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, remove } from 'fs-extra/esm'
5import { join } from 'path'
6import { wait } from '@peertube/peertube-core-utils'
7import { HttpStatusCode, PluginType } from '@peertube/peertube-models'
8import {
9 cleanupTests,
10 createSingleServer,
11 killallServers,
12 makeGetRequest,
13 PeerTubeServer,
14 PluginsCommand,
15 setAccessTokensToServers
16} from '@peertube/peertube-server-commands'
17import { SQLCommand } from '@tests/shared/sql-command.js'
18import { testHelloWorldRegisteredSettings } from '@tests/shared/plugins.js'
19
20describe('Test plugins', function () {
21 let server: PeerTubeServer
22 let sqlCommand: SQLCommand
23 let command: PluginsCommand
24
25 before(async function () {
26 this.timeout(30000)
27
28 const configOverride = {
29 plugins: {
30 index: { check_latest_versions_interval: '5 seconds' }
31 }
32 }
33 server = await createSingleServer(1, configOverride)
34 await setAccessTokensToServers([ server ])
35
36 command = server.plugins
37
38 sqlCommand = new SQLCommand(server)
39 })
40
41 it('Should list and search available plugins and themes', async function () {
42 this.timeout(30000)
43
44 {
45 const body = await command.listAvailable({
46 count: 1,
47 start: 0,
48 pluginType: PluginType.THEME,
49 search: 'background-red'
50 })
51
52 expect(body.total).to.be.at.least(1)
53 expect(body.data).to.have.lengthOf(1)
54 }
55
56 {
57 const body1 = await command.listAvailable({
58 count: 2,
59 start: 0,
60 sort: 'npmName'
61 })
62 expect(body1.total).to.be.at.least(2)
63
64 const data1 = body1.data
65 expect(data1).to.have.lengthOf(2)
66
67 const body2 = await command.listAvailable({
68 count: 2,
69 start: 0,
70 sort: '-npmName'
71 })
72 expect(body2.total).to.be.at.least(2)
73
74 const data2 = body2.data
75 expect(data2).to.have.lengthOf(2)
76
77 expect(data1[0].npmName).to.not.equal(data2[0].npmName)
78 }
79
80 {
81 const body = await command.listAvailable({
82 count: 10,
83 start: 0,
84 pluginType: PluginType.THEME,
85 search: 'background-red',
86 currentPeerTubeEngine: '1.0.0'
87 })
88
89 const p = body.data.find(p => p.npmName === 'peertube-theme-background-red')
90 expect(p).to.be.undefined
91 }
92 })
93
94 it('Should install a plugin and a theme', async function () {
95 this.timeout(30000)
96
97 await command.install({ npmName: 'peertube-plugin-hello-world' })
98 await command.install({ npmName: 'peertube-theme-background-red' })
99 })
100
101 it('Should have the plugin loaded in the configuration', async function () {
102 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
103 const theme = config.theme.registered.find(r => r.name === 'background-red')
104 expect(theme).to.not.be.undefined
105 expect(theme.npmName).to.equal('peertube-theme-background-red')
106
107 const plugin = config.plugin.registered.find(r => r.name === 'hello-world')
108 expect(plugin).to.not.be.undefined
109 expect(plugin.npmName).to.equal('peertube-plugin-hello-world')
110 }
111 })
112
113 it('Should update the default theme in the configuration', async function () {
114 await server.config.updateCustomSubConfig({
115 newConfig: {
116 theme: { default: 'background-red' }
117 }
118 })
119
120 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
121 expect(config.theme.default).to.equal('background-red')
122 }
123 })
124
125 it('Should update my default theme', async function () {
126 await server.users.updateMe({ theme: 'background-red' })
127
128 const user = await server.users.getMyInfo()
129 expect(user.theme).to.equal('background-red')
130 })
131
132 it('Should list plugins and themes', async function () {
133 {
134 const body = await command.list({
135 count: 1,
136 start: 0,
137 pluginType: PluginType.THEME
138 })
139 expect(body.total).to.be.at.least(1)
140
141 const data = body.data
142 expect(data).to.have.lengthOf(1)
143 expect(data[0].name).to.equal('background-red')
144 }
145
146 {
147 const { data } = await command.list({
148 count: 2,
149 start: 0,
150 sort: 'name'
151 })
152
153 expect(data[0].name).to.equal('background-red')
154 expect(data[1].name).to.equal('hello-world')
155 }
156
157 {
158 const body = await command.list({
159 count: 2,
160 start: 1,
161 sort: 'name'
162 })
163
164 expect(body.data[0].name).to.equal('hello-world')
165 }
166 })
167
168 it('Should get registered settings', async function () {
169 await testHelloWorldRegisteredSettings(server)
170 })
171
172 it('Should get public settings', async function () {
173 const body = await command.getPublicSettings({ npmName: 'peertube-plugin-hello-world' })
174 const publicSettings = body.publicSettings
175
176 expect(Object.keys(publicSettings)).to.have.lengthOf(1)
177 expect(Object.keys(publicSettings)).to.deep.equal([ 'user-name' ])
178 expect(publicSettings['user-name']).to.be.null
179 })
180
181 it('Should update the settings', async function () {
182 const settings = {
183 'admin-name': 'Cid'
184 }
185
186 await command.updateSettings({
187 npmName: 'peertube-plugin-hello-world',
188 settings
189 })
190 })
191
192 it('Should have watched settings changes', async function () {
193 await server.servers.waitUntilLog('Settings changed!')
194 })
195
196 it('Should get a plugin and a theme', async function () {
197 {
198 const plugin = await command.get({ npmName: 'peertube-plugin-hello-world' })
199
200 expect(plugin.type).to.equal(PluginType.PLUGIN)
201 expect(plugin.name).to.equal('hello-world')
202 expect(plugin.description).to.exist
203 expect(plugin.homepage).to.exist
204 expect(plugin.uninstalled).to.be.false
205 expect(plugin.enabled).to.be.true
206 expect(plugin.description).to.exist
207 expect(plugin.version).to.exist
208 expect(plugin.peertubeEngine).to.exist
209 expect(plugin.createdAt).to.exist
210
211 expect(plugin.settings).to.not.be.undefined
212 expect(plugin.settings['admin-name']).to.equal('Cid')
213 }
214
215 {
216 const plugin = await command.get({ npmName: 'peertube-theme-background-red' })
217
218 expect(plugin.type).to.equal(PluginType.THEME)
219 expect(plugin.name).to.equal('background-red')
220 expect(plugin.description).to.exist
221 expect(plugin.homepage).to.exist
222 expect(plugin.uninstalled).to.be.false
223 expect(plugin.enabled).to.be.true
224 expect(plugin.description).to.exist
225 expect(plugin.version).to.exist
226 expect(plugin.peertubeEngine).to.exist
227 expect(plugin.createdAt).to.exist
228
229 expect(plugin.settings).to.be.null
230 }
231 })
232
233 it('Should update the plugin and the theme', async function () {
234 this.timeout(180000)
235
236 // Wait the scheduler that get the latest plugins versions
237 await wait(6000)
238
239 async function testUpdate (type: 'plugin' | 'theme', name: string) {
240 // Fake update our plugin version
241 await sqlCommand.setPluginVersion(name, '0.0.1')
242
243 // Fake update package.json
244 const packageJSON = await command.getPackageJSON(`peertube-${type}-${name}`)
245 const oldVersion = packageJSON.version
246
247 packageJSON.version = '0.0.1'
248 await command.updatePackageJSON(`peertube-${type}-${name}`, packageJSON)
249
250 // Restart the server to take into account this change
251 await killallServers([ server ])
252 await server.run()
253
254 const checkConfig = async (version: string) => {
255 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
256 expect(config[type].registered.find(r => r.name === name).version).to.equal(version)
257 }
258 }
259
260 const getPluginFromAPI = async () => {
261 const body = await command.list({ pluginType: type === 'plugin' ? PluginType.PLUGIN : PluginType.THEME })
262
263 return body.data.find(p => p.name === name)
264 }
265
266 {
267 const plugin = await getPluginFromAPI()
268 expect(plugin.version).to.equal('0.0.1')
269 expect(plugin.latestVersion).to.exist
270 expect(plugin.latestVersion).to.not.equal('0.0.1')
271
272 await checkConfig('0.0.1')
273 }
274
275 {
276 await command.update({ npmName: `peertube-${type}-${name}` })
277
278 const plugin = await getPluginFromAPI()
279 expect(plugin.version).to.equal(oldVersion)
280
281 const updatedPackageJSON = await command.getPackageJSON(`peertube-${type}-${name}`)
282 expect(updatedPackageJSON.version).to.equal(oldVersion)
283
284 await checkConfig(oldVersion)
285 }
286 }
287
288 await testUpdate('theme', 'background-red')
289 await testUpdate('plugin', 'hello-world')
290 })
291
292 it('Should uninstall the plugin', async function () {
293 await command.uninstall({ npmName: 'peertube-plugin-hello-world' })
294
295 const body = await command.list({ pluginType: PluginType.PLUGIN })
296 expect(body.total).to.equal(0)
297 expect(body.data).to.have.lengthOf(0)
298 })
299
300 it('Should list uninstalled plugins', async function () {
301 const body = await command.list({ pluginType: PluginType.PLUGIN, uninstalled: true })
302 expect(body.total).to.equal(1)
303 expect(body.data).to.have.lengthOf(1)
304
305 const plugin = body.data[0]
306 expect(plugin.name).to.equal('hello-world')
307 expect(plugin.enabled).to.be.false
308 expect(plugin.uninstalled).to.be.true
309 })
310
311 it('Should uninstall the theme', async function () {
312 await command.uninstall({ npmName: 'peertube-theme-background-red' })
313 })
314
315 it('Should have updated the configuration', async function () {
316 for (const config of [ await server.config.getConfig(), await server.config.getIndexHTMLConfig() ]) {
317 expect(config.theme.default).to.equal('default')
318
319 const theme = config.theme.registered.find(r => r.name === 'background-red')
320 expect(theme).to.be.undefined
321
322 const plugin = config.plugin.registered.find(r => r.name === 'hello-world')
323 expect(plugin).to.be.undefined
324 }
325 })
326
327 it('Should have updated the user theme', async function () {
328 const user = await server.users.getMyInfo()
329 expect(user.theme).to.equal('instance-default')
330 })
331
332 it('Should not install a broken plugin', async function () {
333 this.timeout(60000)
334
335 async function check () {
336 const body = await command.list({ pluginType: PluginType.PLUGIN })
337 const plugins = body.data
338 expect(plugins.find(p => p.name === 'test-broken')).to.not.exist
339 }
340
341 await command.install({
342 path: PluginsCommand.getPluginTestPath('-broken'),
343 expectedStatus: HttpStatusCode.BAD_REQUEST_400
344 })
345
346 await check()
347
348 await killallServers([ server ])
349 await server.run()
350
351 await check()
352 })
353
354 it('Should rebuild native modules on Node ABI change', async function () {
355 this.timeout(60000)
356
357 const removeNativeModule = async () => {
358 await remove(join(baseNativeModule, 'build'))
359 await remove(join(baseNativeModule, 'prebuilds'))
360 }
361
362 await command.install({ path: PluginsCommand.getPluginTestPath('-native') })
363
364 await makeGetRequest({
365 url: server.url,
366 path: '/plugins/test-native/router',
367 expectedStatus: HttpStatusCode.NO_CONTENT_204
368 })
369
370 const query = `UPDATE "application" SET "nodeABIVersion" = 1`
371 await sqlCommand.updateQuery(query)
372
373 const baseNativeModule = server.servers.buildDirectory(join('plugins', 'node_modules', 'a-native-example'))
374
375 await removeNativeModule()
376 await server.kill()
377 await server.run()
378
379 await wait(3000)
380
381 expect(await pathExists(join(baseNativeModule, 'build'))).to.be.true
382 expect(await pathExists(join(baseNativeModule, 'prebuilds'))).to.be.true
383
384 await makeGetRequest({
385 url: server.url,
386 path: '/plugins/test-native/router',
387 expectedStatus: HttpStatusCode.NO_CONTENT_204
388 })
389
390 await removeNativeModule()
391
392 await server.kill()
393 await server.run()
394
395 expect(await pathExists(join(baseNativeModule, 'build'))).to.be.false
396 expect(await pathExists(join(baseNativeModule, 'prebuilds'))).to.be.false
397
398 await makeGetRequest({
399 url: server.url,
400 path: '/plugins/test-native/router',
401 expectedStatus: HttpStatusCode.NOT_FOUND_404
402 })
403 })
404
405 after(async function () {
406 await sqlCommand.cleanup()
407
408 await cleanupTests([ server ])
409 })
410})
diff --git a/packages/tests/src/api/server/proxy.ts b/packages/tests/src/api/server/proxy.ts
new file mode 100644
index 000000000..c7d13f4ab
--- /dev/null
+++ b/packages/tests/src/api/server/proxy.ts
@@ -0,0 +1,173 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { HttpStatusCode, HttpStatusCodeType, VideoPrivacy } from '@peertube/peertube-models'
5import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 ObjectStorageCommand,
11 PeerTubeServer,
12 setAccessTokensToServers,
13 setDefaultVideoChannel,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16import { FIXTURE_URLS } from '@tests/shared/tests.js'
17import { expectStartWith, expectNotStartWith } from '@tests/shared/checks.js'
18import { MockProxy } from '@tests/shared/mock-servers/mock-proxy.js'
19
20describe('Test proxy', function () {
21 let servers: PeerTubeServer[] = []
22 let proxy: MockProxy
23
24 const goodEnv = { HTTP_PROXY: '' }
25 const badEnv = { HTTP_PROXY: 'http://127.0.0.1:9000' }
26
27 before(async function () {
28 this.timeout(120000)
29
30 proxy = new MockProxy()
31
32 const proxyPort = await proxy.initialize()
33 servers = await createMultipleServers(2)
34
35 goodEnv.HTTP_PROXY = 'http://127.0.0.1:' + proxyPort
36
37 await setAccessTokensToServers(servers)
38 await setDefaultVideoChannel(servers)
39 await doubleFollow(servers[0], servers[1])
40 })
41
42 describe('Federation', function () {
43
44 it('Should succeed federation with the appropriate proxy config', async function () {
45 this.timeout(40000)
46
47 await servers[0].kill()
48 await servers[0].run({}, { env: goodEnv })
49
50 await servers[0].videos.quickUpload({ name: 'video 1' })
51
52 await waitJobs(servers)
53
54 for (const server of servers) {
55 const { total, data } = await server.videos.list()
56 expect(total).to.equal(1)
57 expect(data).to.have.lengthOf(1)
58 }
59 })
60
61 it('Should fail federation with a wrong proxy config', async function () {
62 this.timeout(40000)
63
64 await servers[0].kill()
65 await servers[0].run({}, { env: badEnv })
66
67 await servers[0].videos.quickUpload({ name: 'video 2' })
68
69 await waitJobs(servers)
70
71 {
72 const { total, data } = await servers[0].videos.list()
73 expect(total).to.equal(2)
74 expect(data).to.have.lengthOf(2)
75 }
76
77 {
78 const { total, data } = await servers[1].videos.list()
79 expect(total).to.equal(1)
80 expect(data).to.have.lengthOf(1)
81 }
82 })
83 })
84
85 describe('Videos import', async function () {
86
87 function quickImport (expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) {
88 return servers[0].imports.importVideo({
89 attributes: {
90 name: 'video import',
91 channelId: servers[0].store.channel.id,
92 privacy: VideoPrivacy.PUBLIC,
93 targetUrl: FIXTURE_URLS.peertube_long
94 },
95 expectedStatus
96 })
97 }
98
99 it('Should succeed import with the appropriate proxy config', async function () {
100 this.timeout(240000)
101
102 await servers[0].kill()
103 await servers[0].run({}, { env: goodEnv })
104
105 await quickImport()
106
107 await waitJobs(servers)
108
109 const { total, data } = await servers[0].videos.list()
110 expect(total).to.equal(3)
111 expect(data).to.have.lengthOf(3)
112 })
113
114 it('Should fail import with a wrong proxy config', async function () {
115 this.timeout(120000)
116
117 await servers[0].kill()
118 await servers[0].run({}, { env: badEnv })
119
120 await quickImport(HttpStatusCode.BAD_REQUEST_400)
121 })
122 })
123
124 describe('Object storage', function () {
125 if (areMockObjectStorageTestsDisabled()) return
126
127 const objectStorage = new ObjectStorageCommand()
128
129 before(async function () {
130 this.timeout(30000)
131
132 await objectStorage.prepareDefaultMockBuckets()
133 })
134
135 it('Should succeed to upload to object storage with the appropriate proxy config', async function () {
136 this.timeout(120000)
137
138 await servers[0].kill()
139 await servers[0].run(objectStorage.getDefaultMockConfig(), { env: goodEnv })
140
141 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
142 await waitJobs(servers)
143
144 const video = await servers[0].videos.get({ id: uuid })
145
146 expectStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
147 })
148
149 it('Should fail to upload to object storage with a wrong proxy config', async function () {
150 this.timeout(120000)
151
152 await servers[0].kill()
153 await servers[0].run(objectStorage.getDefaultMockConfig(), { env: badEnv })
154
155 const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
156 await waitJobs(servers, { skipDelayed: true })
157
158 const video = await servers[0].videos.get({ id: uuid })
159
160 expectNotStartWith(video.files[0].fileUrl, objectStorage.getMockWebVideosBaseUrl())
161 })
162
163 after(async function () {
164 await objectStorage.cleanupMock()
165 })
166 })
167
168 after(async function () {
169 await proxy.terminate()
170
171 await cleanupTests(servers)
172 })
173})
diff --git a/packages/tests/src/api/server/reverse-proxy.ts b/packages/tests/src/api/server/reverse-proxy.ts
new file mode 100644
index 000000000..7e334cc3e
--- /dev/null
+++ b/packages/tests/src/api/server/reverse-proxy.ts
@@ -0,0 +1,156 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { HttpStatusCode } from '@peertube/peertube-models'
6import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands'
7
8describe('Test application behind a reverse proxy', function () {
9 let server: PeerTubeServer
10 let userAccessToken: string
11 let videoId: string
12
13 before(async function () {
14 this.timeout(60000)
15
16 const config = {
17 rates_limit: {
18 api: {
19 max: 50,
20 window: 5000
21 },
22 signup: {
23 max: 3,
24 window: 5000
25 },
26 login: {
27 max: 20
28 }
29 },
30 signup: {
31 limit: 20
32 }
33 }
34
35 server = await createSingleServer(1, config)
36 await setAccessTokensToServers([ server ])
37
38 userAccessToken = await server.users.generateUserAndToken('user')
39
40 const { uuid } = await server.videos.upload()
41 videoId = uuid
42 })
43
44 it('Should view a video only once with the same IP by default', async function () {
45 this.timeout(40000)
46
47 await server.views.simulateView({ id: videoId })
48 await server.views.simulateView({ id: videoId })
49
50 // Wait the repeatable job
51 await wait(8000)
52
53 const video = await server.videos.get({ id: videoId })
54 expect(video.views).to.equal(1)
55 })
56
57 it('Should view a video 2 times with the X-Forwarded-For header set', async function () {
58 this.timeout(20000)
59
60 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.1,127.0.0.1' })
61 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.2,127.0.0.1' })
62
63 // Wait the repeatable job
64 await wait(8000)
65
66 const video = await server.videos.get({ id: videoId })
67 expect(video.views).to.equal(3)
68 })
69
70 it('Should view a video only once with the same client IP in the X-Forwarded-For header', async function () {
71 this.timeout(20000)
72
73 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.4,0.0.0.3,::ffff:127.0.0.1' })
74 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.5,0.0.0.3,127.0.0.1' })
75
76 // Wait the repeatable job
77 await wait(8000)
78
79 const video = await server.videos.get({ id: videoId })
80 expect(video.views).to.equal(4)
81 })
82
83 it('Should view a video two times with a different client IP in the X-Forwarded-For header', async function () {
84 this.timeout(20000)
85
86 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.6,127.0.0.1' })
87 await server.views.simulateView({ id: videoId, xForwardedFor: '0.0.0.8,0.0.0.7,127.0.0.1' })
88
89 // Wait the repeatable job
90 await wait(8000)
91
92 const video = await server.videos.get({ id: videoId })
93 expect(video.views).to.equal(6)
94 })
95
96 it('Should rate limit logins', async function () {
97 const user = { username: 'root', password: 'fail' }
98
99 for (let i = 0; i < 18; i++) {
100 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
101 }
102
103 await server.login.login({ user, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
104 })
105
106 it('Should rate limit signup', async function () {
107 for (let i = 0; i < 10; i++) {
108 try {
109 await server.registrations.register({ username: 'test' + i })
110 } catch {
111 // empty
112 }
113 }
114
115 await server.registrations.register({ username: 'test42', expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
116 })
117
118 it('Should not rate limit failed signup', async function () {
119 this.timeout(30000)
120
121 await wait(7000)
122
123 for (let i = 0; i < 3; i++) {
124 await server.registrations.register({ username: 'test' + i, expectedStatus: HttpStatusCode.CONFLICT_409 })
125 }
126
127 await server.registrations.register({ username: 'test43', expectedStatus: HttpStatusCode.NO_CONTENT_204 })
128
129 })
130
131 it('Should rate limit API calls', async function () {
132 this.timeout(30000)
133
134 await wait(7000)
135
136 for (let i = 0; i < 100; i++) {
137 try {
138 await server.videos.get({ id: videoId })
139 } catch {
140 // don't care if it fails
141 }
142 }
143
144 await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
145 })
146
147 it('Should rate limit API calls with a user but not with an admin', async function () {
148 await server.videos.get({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
149
150 await server.videos.get({ id: videoId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
151 })
152
153 after(async function () {
154 await cleanupTests([ server ])
155 })
156})
diff --git a/packages/tests/src/api/server/services.ts b/packages/tests/src/api/server/services.ts
new file mode 100644
index 000000000..349d29a58
--- /dev/null
+++ b/packages/tests/src/api/server/services.ts
@@ -0,0 +1,143 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { Video, VideoPlaylistPrivacy } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createSingleServer,
8 PeerTubeServer,
9 setAccessTokensToServers,
10 setDefaultVideoChannel
11} from '@peertube/peertube-server-commands'
12
13describe('Test services', function () {
14 let server: PeerTubeServer = null
15 let playlistUUID: string
16 let playlistDisplayName: string
17 let video: Video
18
19 const urlSuffixes = [
20 {
21 input: '',
22 output: ''
23 },
24 {
25 input: '?param=1',
26 output: ''
27 },
28 {
29 input: '?muted=1&warningTitle=0&toto=1',
30 output: '?muted=1&warningTitle=0'
31 }
32 ]
33
34 before(async function () {
35 this.timeout(120000)
36
37 server = await createSingleServer(1)
38
39 await setAccessTokensToServers([ server ])
40 await setDefaultVideoChannel([ server ])
41
42 {
43 const attributes = { name: 'my super name' }
44 await server.videos.upload({ attributes })
45
46 const { data } = await server.videos.list()
47 video = data[0]
48 }
49
50 {
51 const created = await server.playlists.create({
52 attributes: {
53 displayName: 'The Life and Times of Scrooge McDuck',
54 privacy: VideoPlaylistPrivacy.PUBLIC,
55 videoChannelId: server.store.channel.id
56 }
57 })
58
59 playlistUUID = created.uuid
60 playlistDisplayName = 'The Life and Times of Scrooge McDuck'
61
62 await server.playlists.addElement({
63 playlistId: created.id,
64 attributes: {
65 videoId: video.id
66 }
67 })
68 }
69 })
70
71 it('Should have a valid oEmbed video response', async function () {
72 for (const basePath of [ '/videos/watch/', '/w/' ]) {
73 for (const suffix of urlSuffixes) {
74 const oembedUrl = server.url + basePath + video.uuid + suffix.input
75
76 const res = await server.services.getOEmbed({ oembedUrl })
77 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
78 `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}${suffix.output}" ` +
79 'frameborder="0" allowfullscreen></iframe>'
80
81 const expectedThumbnailUrl = 'http://' + server.host + video.previewPath
82
83 expect(res.body.html).to.equal(expectedHtml)
84 expect(res.body.title).to.equal(video.name)
85 expect(res.body.author_name).to.equal(server.store.channel.displayName)
86 expect(res.body.width).to.equal(560)
87 expect(res.body.height).to.equal(315)
88 expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
89 expect(res.body.thumbnail_width).to.equal(850)
90 expect(res.body.thumbnail_height).to.equal(480)
91 }
92 }
93 })
94
95 it('Should have a valid playlist oEmbed response', async function () {
96 for (const basePath of [ '/videos/watch/playlist/', '/w/p/' ]) {
97 for (const suffix of urlSuffixes) {
98 const oembedUrl = server.url + basePath + playlistUUID + suffix.input
99
100 const res = await server.services.getOEmbed({ oembedUrl })
101 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" ' +
102 `title="${playlistDisplayName}" src="http://${server.host}/video-playlists/embed/${playlistUUID}${suffix.output}" ` +
103 'frameborder="0" allowfullscreen></iframe>'
104
105 expect(res.body.html).to.equal(expectedHtml)
106 expect(res.body.title).to.equal('The Life and Times of Scrooge McDuck')
107 expect(res.body.author_name).to.equal(server.store.channel.displayName)
108 expect(res.body.width).to.equal(560)
109 expect(res.body.height).to.equal(315)
110 expect(res.body.thumbnail_url).exist
111 expect(res.body.thumbnail_width).to.equal(280)
112 expect(res.body.thumbnail_height).to.equal(157)
113 }
114 }
115 })
116
117 it('Should have a valid oEmbed response with small max height query', async function () {
118 for (const basePath of [ '/videos/watch/', '/w/' ]) {
119 const oembedUrl = 'http://' + server.host + basePath + video.uuid
120 const format = 'json'
121 const maxHeight = 50
122 const maxWidth = 50
123
124 const res = await server.services.getOEmbed({ oembedUrl, format, maxHeight, maxWidth })
125 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts allow-popups" ' +
126 `title="${video.name}" src="http://${server.host}/videos/embed/${video.uuid}" ` +
127 'frameborder="0" allowfullscreen></iframe>'
128
129 expect(res.body.html).to.equal(expectedHtml)
130 expect(res.body.title).to.equal(video.name)
131 expect(res.body.author_name).to.equal(server.store.channel.displayName)
132 expect(res.body.height).to.equal(50)
133 expect(res.body.width).to.equal(50)
134 expect(res.body).to.not.have.property('thumbnail_url')
135 expect(res.body).to.not.have.property('thumbnail_width')
136 expect(res.body).to.not.have.property('thumbnail_height')
137 }
138 })
139
140 after(async function () {
141 await cleanupTests([ server ])
142 })
143})
diff --git a/packages/tests/src/api/server/slow-follows.ts b/packages/tests/src/api/server/slow-follows.ts
new file mode 100644
index 000000000..d03109001
--- /dev/null
+++ b/packages/tests/src/api/server/slow-follows.ts
@@ -0,0 +1,85 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { Job } from '@peertube/peertube-models'
5import {
6 cleanupTests,
7 createMultipleServers,
8 doubleFollow,
9 PeerTubeServer,
10 setAccessTokensToServers,
11 waitJobs
12} from '@peertube/peertube-server-commands'
13
14describe('Test slow follows', function () {
15 let servers: PeerTubeServer[] = []
16
17 let afterFollows: Date
18
19 before(async function () {
20 this.timeout(120000)
21
22 servers = await createMultipleServers(3)
23
24 // Get the access tokens
25 await setAccessTokensToServers(servers)
26
27 await doubleFollow(servers[0], servers[1])
28 await doubleFollow(servers[0], servers[2])
29
30 afterFollows = new Date()
31
32 for (let i = 0; i < 5; i++) {
33 await servers[0].videos.quickUpload({ name: 'video ' + i })
34 }
35
36 await waitJobs(servers)
37 })
38
39 it('Should only have broadcast jobs', async function () {
40 const { data } = await servers[0].jobs.list({ jobType: 'activitypub-http-unicast', sort: '-createdAt' })
41
42 for (const job of data) {
43 expect(new Date(job.createdAt)).below(afterFollows)
44 }
45 })
46
47 it('Should process bad follower', async function () {
48 this.timeout(30000)
49
50 await servers[1].kill()
51
52 // Set server 2 as bad follower
53 await servers[0].videos.quickUpload({ name: 'video 6' })
54 await waitJobs(servers[0])
55
56 afterFollows = new Date()
57 const filter = (job: Job) => new Date(job.createdAt) > afterFollows
58
59 // Resend another broadcast job
60 await servers[0].videos.quickUpload({ name: 'video 7' })
61 await waitJobs(servers[0])
62
63 const resBroadcast = await servers[0].jobs.list({ jobType: 'activitypub-http-broadcast', sort: '-createdAt' })
64 const resUnicast = await servers[0].jobs.list({ jobType: 'activitypub-http-unicast', sort: '-createdAt' })
65
66 const broadcast = resBroadcast.data.filter(filter)
67 const unicast = resUnicast.data.filter(filter)
68
69 expect(unicast).to.have.lengthOf(2)
70 expect(broadcast).to.have.lengthOf(2)
71
72 for (const u of unicast) {
73 expect(u.data.uri).to.equal(servers[1].url + '/inbox')
74 }
75
76 for (const b of broadcast) {
77 expect(b.data.uris).to.have.lengthOf(1)
78 expect(b.data.uris[0]).to.equal(servers[2].url + '/inbox')
79 }
80 })
81
82 after(async function () {
83 await cleanupTests(servers)
84 })
85})
diff --git a/packages/tests/src/api/server/stats.ts b/packages/tests/src/api/server/stats.ts
new file mode 100644
index 000000000..32ab323ce
--- /dev/null
+++ b/packages/tests/src/api/server/stats.ts
@@ -0,0 +1,279 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { wait } from '@peertube/peertube-core-utils'
5import { ActivityType, VideoPlaylistPrivacy } from '@peertube/peertube-models'
6import {
7 cleanupTests,
8 createMultipleServers,
9 doubleFollow,
10 PeerTubeServer,
11 setAccessTokensToServers,
12 setDefaultAccountAvatar,
13 setDefaultChannelAvatar,
14 waitJobs
15} from '@peertube/peertube-server-commands'
16
17describe('Test stats (excluding redundancy)', function () {
18 let servers: PeerTubeServer[] = []
19 let channelId
20 const user = {
21 username: 'user1',
22 password: 'super_password'
23 }
24
25 before(async function () {
26 this.timeout(120000)
27
28 servers = await createMultipleServers(3)
29
30 await setAccessTokensToServers(servers)
31 await setDefaultChannelAvatar(servers)
32 await setDefaultAccountAvatar(servers)
33
34 await doubleFollow(servers[0], servers[1])
35
36 await servers[0].users.create({ username: user.username, password: user.password })
37
38 const { uuid } = await servers[0].videos.upload({ attributes: { fixture: 'video_short.webm' } })
39
40 await servers[0].comments.createThread({ videoId: uuid, text: 'comment' })
41
42 await servers[0].views.simulateView({ id: uuid })
43
44 // Wait the video views repeatable job
45 await wait(8000)
46
47 await servers[2].follows.follow({ hosts: [ servers[0].url ] })
48 await waitJobs(servers)
49 })
50
51 it('Should have the correct stats on instance 1', async function () {
52 const data = await servers[0].stats.get()
53
54 expect(data.totalLocalVideoComments).to.equal(1)
55 expect(data.totalLocalVideos).to.equal(1)
56 expect(data.totalLocalVideoViews).to.equal(1)
57 expect(data.totalLocalVideoFilesSize).to.equal(218910)
58 expect(data.totalUsers).to.equal(2)
59 expect(data.totalVideoComments).to.equal(1)
60 expect(data.totalVideos).to.equal(1)
61 expect(data.totalInstanceFollowers).to.equal(2)
62 expect(data.totalInstanceFollowing).to.equal(1)
63 expect(data.totalLocalPlaylists).to.equal(0)
64 })
65
66 it('Should have the correct stats on instance 2', async function () {
67 const data = await servers[1].stats.get()
68
69 expect(data.totalLocalVideoComments).to.equal(0)
70 expect(data.totalLocalVideos).to.equal(0)
71 expect(data.totalLocalVideoViews).to.equal(0)
72 expect(data.totalLocalVideoFilesSize).to.equal(0)
73 expect(data.totalUsers).to.equal(1)
74 expect(data.totalVideoComments).to.equal(1)
75 expect(data.totalVideos).to.equal(1)
76 expect(data.totalInstanceFollowers).to.equal(1)
77 expect(data.totalInstanceFollowing).to.equal(1)
78 expect(data.totalLocalPlaylists).to.equal(0)
79 })
80
81 it('Should have the correct stats on instance 3', async function () {
82 const data = await servers[2].stats.get()
83
84 expect(data.totalLocalVideoComments).to.equal(0)
85 expect(data.totalLocalVideos).to.equal(0)
86 expect(data.totalLocalVideoViews).to.equal(0)
87 expect(data.totalUsers).to.equal(1)
88 expect(data.totalVideoComments).to.equal(1)
89 expect(data.totalVideos).to.equal(1)
90 expect(data.totalInstanceFollowing).to.equal(1)
91 expect(data.totalInstanceFollowers).to.equal(0)
92 expect(data.totalLocalPlaylists).to.equal(0)
93 })
94
95 it('Should have the correct total videos stats after an unfollow', async function () {
96 this.timeout(15000)
97
98 await servers[2].follows.unfollow({ target: servers[0] })
99 await waitJobs(servers)
100
101 const data = await servers[2].stats.get()
102
103 expect(data.totalVideos).to.equal(0)
104 })
105
106 it('Should have the correct active user stats', async function () {
107 const server = servers[0]
108
109 {
110 const data = await server.stats.get()
111
112 expect(data.totalDailyActiveUsers).to.equal(1)
113 expect(data.totalWeeklyActiveUsers).to.equal(1)
114 expect(data.totalMonthlyActiveUsers).to.equal(1)
115 }
116
117 {
118 await server.login.getAccessToken(user)
119
120 const data = await server.stats.get()
121
122 expect(data.totalDailyActiveUsers).to.equal(2)
123 expect(data.totalWeeklyActiveUsers).to.equal(2)
124 expect(data.totalMonthlyActiveUsers).to.equal(2)
125 }
126 })
127
128 it('Should have the correct active channel stats', async function () {
129 const server = servers[0]
130
131 {
132 const data = await server.stats.get()
133
134 expect(data.totalLocalVideoChannels).to.equal(2)
135 expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
136 expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
137 expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
138 }
139
140 {
141 const attributes = {
142 name: 'stats_channel',
143 displayName: 'My stats channel'
144 }
145 const created = await server.channels.create({ attributes })
146 channelId = created.id
147
148 const data = await server.stats.get()
149
150 expect(data.totalLocalVideoChannels).to.equal(3)
151 expect(data.totalLocalDailyActiveVideoChannels).to.equal(1)
152 expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1)
153 expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1)
154 }
155
156 {
157 await server.videos.upload({ attributes: { fixture: 'video_short.webm', channelId } })
158
159 const data = await server.stats.get()
160
161 expect(data.totalLocalVideoChannels).to.equal(3)
162 expect(data.totalLocalDailyActiveVideoChannels).to.equal(2)
163 expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2)
164 expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2)
165 }
166 })
167
168 it('Should have the correct playlist stats', async function () {
169 const server = servers[0]
170
171 {
172 const data = await server.stats.get()
173 expect(data.totalLocalPlaylists).to.equal(0)
174 }
175
176 {
177 await server.playlists.create({
178 attributes: {
179 displayName: 'playlist for count',
180 privacy: VideoPlaylistPrivacy.PUBLIC,
181 videoChannelId: channelId
182 }
183 })
184
185 const data = await server.stats.get()
186 expect(data.totalLocalPlaylists).to.equal(1)
187 }
188 })
189
190 it('Should correctly count video file sizes if transcoding is enabled', async function () {
191 this.timeout(120000)
192
193 await servers[0].config.updateCustomSubConfig({
194 newConfig: {
195 transcoding: {
196 enabled: true,
197 webVideos: {
198 enabled: true
199 },
200 hls: {
201 enabled: true
202 },
203 resolutions: {
204 '0p': false,
205 '144p': false,
206 '240p': false,
207 '360p': false,
208 '480p': false,
209 '720p': false,
210 '1080p': false,
211 '1440p': false,
212 '2160p': false
213 }
214 }
215 }
216 })
217
218 await servers[0].videos.upload({ attributes: { name: 'video', fixture: 'video_short.webm' } })
219
220 await waitJobs(servers)
221
222 {
223 const data = await servers[1].stats.get()
224 expect(data.totalLocalVideoFilesSize).to.equal(0)
225 }
226
227 {
228 const data = await servers[0].stats.get()
229 expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000)
230 expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000)
231 }
232 })
233
234 it('Should have the correct AP stats', async function () {
235 this.timeout(120000)
236
237 await servers[0].config.disableTranscoding()
238
239 const first = await servers[1].stats.get()
240
241 for (let i = 0; i < 10; i++) {
242 await servers[0].videos.upload({ attributes: { name: 'video' } })
243 }
244
245 await waitJobs(servers)
246
247 await wait(6000)
248
249 const second = await servers[1].stats.get()
250 expect(second.totalActivityPubMessagesProcessed).to.be.greaterThan(first.totalActivityPubMessagesProcessed)
251
252 const apTypes: ActivityType[] = [
253 'Create', 'Update', 'Delete', 'Follow', 'Accept', 'Announce', 'Undo', 'Like', 'Reject', 'View', 'Dislike', 'Flag'
254 ]
255
256 const processed = apTypes.reduce(
257 (previous, type) => previous + second['totalActivityPub' + type + 'MessagesSuccesses'],
258 0
259 )
260 expect(second.totalActivityPubMessagesProcessed).to.equal(processed)
261 expect(second.totalActivityPubMessagesSuccesses).to.equal(processed)
262
263 expect(second.totalActivityPubMessagesErrors).to.equal(0)
264
265 for (const apType of apTypes) {
266 expect(second['totalActivityPub' + apType + 'MessagesErrors']).to.equal(0)
267 }
268
269 await wait(6000)
270
271 const third = await servers[1].stats.get()
272 expect(third.totalActivityPubMessagesWaiting).to.equal(0)
273 expect(third.activityPubMessagesProcessedPerSecond).to.be.lessThan(second.activityPubMessagesProcessedPerSecond)
274 })
275
276 after(async function () {
277 await cleanupTests(servers)
278 })
279})
diff --git a/packages/tests/src/api/server/tracker.ts b/packages/tests/src/api/server/tracker.ts
new file mode 100644
index 000000000..4df4e4613
--- /dev/null
+++ b/packages/tests/src/api/server/tracker.ts
@@ -0,0 +1,110 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */
2
3import { decode as magnetUriDecode, encode as magnetUriEncode } from 'magnet-uri'
4import WebTorrent from 'webtorrent'
5import {
6 cleanupTests,
7 createSingleServer,
8 killallServers,
9 PeerTubeServer,
10 setAccessTokensToServers
11} from '@peertube/peertube-server-commands'
12
13describe('Test tracker', function () {
14 let server: PeerTubeServer
15 let badMagnet: string
16 let goodMagnet: string
17
18 before(async function () {
19 this.timeout(60000)
20 server = await createSingleServer(1)
21 await setAccessTokensToServers([ server ])
22
23 {
24 const { uuid } = await server.videos.upload()
25 const video = await server.videos.get({ id: uuid })
26 goodMagnet = video.files[0].magnetUri
27
28 const parsed = magnetUriDecode(goodMagnet)
29 parsed.infoHash = '010597bb88b1968a5693a4fa8267c592ca65f2e9'
30
31 badMagnet = magnetUriEncode(parsed)
32 }
33 })
34
35 it('Should succeed with the correct infohash', function (done) {
36 const webtorrent = new WebTorrent()
37
38 const torrent = webtorrent.add(goodMagnet)
39
40 torrent.on('error', done)
41 torrent.on('warning', warn => {
42 const message = typeof warn === 'string' ? warn : warn.message
43 if (message.includes('Unknown infoHash ')) return done(new Error('Error on infohash'))
44 })
45
46 torrent.on('done', done)
47 })
48
49 it('Should disable the tracker', function (done) {
50 this.timeout(20000)
51
52 const errCb = () => done(new Error('Tracker is enabled'))
53
54 killallServers([ server ])
55 .then(() => server.run({ tracker: { enabled: false } }))
56 .then(() => {
57 const webtorrent = new WebTorrent()
58
59 const torrent = webtorrent.add(goodMagnet)
60
61 torrent.on('error', done)
62 torrent.on('warning', warn => {
63 const message = typeof warn === 'string' ? warn : warn.message
64 if (message.includes('disabled ')) {
65 torrent.off('done', errCb)
66
67 return done()
68 }
69 })
70
71 torrent.on('done', errCb)
72 })
73 })
74
75 it('Should return an error when adding an incorrect infohash', function (done) {
76 this.timeout(20000)
77
78 killallServers([ server ])
79 .then(() => server.run())
80 .then(() => {
81 const webtorrent = new WebTorrent()
82
83 const torrent = webtorrent.add(badMagnet)
84
85 torrent.on('error', done)
86 torrent.on('warning', warn => {
87 const message = typeof warn === 'string' ? warn : warn.message
88 if (message.includes('Unknown infoHash ')) return done()
89 })
90
91 torrent.on('done', () => done(new Error('No error on infohash')))
92 })
93 })
94
95 it('Should block the IP after the failed infohash', function (done) {
96 const webtorrent = new WebTorrent()
97
98 const torrent = webtorrent.add(goodMagnet)
99
100 torrent.on('error', done)
101 torrent.on('warning', warn => {
102 const message = typeof warn === 'string' ? warn : warn.message
103 if (message.includes('Unsupported tracker protocol')) return done()
104 })
105 })
106
107 after(async function () {
108 await cleanupTests([ server ])
109 })
110})