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