aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils/server
diff options
context:
space:
mode:
Diffstat (limited to 'shared/extra-utils/server')
-rw-r--r--shared/extra-utils/server/activitypub.ts15
-rw-r--r--shared/extra-utils/server/clients.ts20
-rw-r--r--shared/extra-utils/server/config-command.ts263
-rw-r--r--shared/extra-utils/server/config.ts260
-rw-r--r--shared/extra-utils/server/contact-form-command.ts31
-rw-r--r--shared/extra-utils/server/contact-form.ts31
-rw-r--r--shared/extra-utils/server/debug-command.ts33
-rw-r--r--shared/extra-utils/server/debug.ts33
-rw-r--r--shared/extra-utils/server/directories.ts34
-rw-r--r--shared/extra-utils/server/follows-command.ts141
-rw-r--r--shared/extra-utils/server/follows.ts133
-rw-r--r--shared/extra-utils/server/index.ts15
-rw-r--r--shared/extra-utils/server/jobs-command.ts36
-rw-r--r--shared/extra-utils/server/jobs.ts82
-rw-r--r--shared/extra-utils/server/plugins-command.ts256
-rw-r--r--shared/extra-utils/server/plugins.ts299
-rw-r--r--shared/extra-utils/server/redundancy-command.ts80
-rw-r--r--shared/extra-utils/server/redundancy.ts88
-rw-r--r--shared/extra-utils/server/server.ts369
-rw-r--r--shared/extra-utils/server/servers-command.ts88
-rw-r--r--shared/extra-utils/server/servers.ts375
-rw-r--r--shared/extra-utils/server/stats-command.ts25
-rw-r--r--shared/extra-utils/server/stats.ts23
23 files changed, 1415 insertions, 1315 deletions
diff --git a/shared/extra-utils/server/activitypub.ts b/shared/extra-utils/server/activitypub.ts
deleted file mode 100644
index cf967ed7d..000000000
--- a/shared/extra-utils/server/activitypub.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
5 return request(url)
6 .get(path)
7 .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8')
8 .expect(expectedStatus)
9}
10
11// ---------------------------------------------------------------------------
12
13export {
14 makeActivityPubGetRequest
15}
diff --git a/shared/extra-utils/server/clients.ts b/shared/extra-utils/server/clients.ts
deleted file mode 100644
index 894fe4911..000000000
--- a/shared/extra-utils/server/clients.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import * as request from 'supertest'
2import { URL } from 'url'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function getClient (url: string) {
6 const path = '/api/v1/oauth-clients/local'
7
8 return request(url)
9 .get(path)
10 .set('Host', new URL(url).host)
11 .set('Accept', 'application/json')
12 .expect(HttpStatusCode.OK_200)
13 .expect('Content-Type', /json/)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 getClient
20}
diff --git a/shared/extra-utils/server/config-command.ts b/shared/extra-utils/server/config-command.ts
new file mode 100644
index 000000000..11148aa46
--- /dev/null
+++ b/shared/extra-utils/server/config-command.ts
@@ -0,0 +1,263 @@
1import { merge } from 'lodash'
2import { DeepPartial } from '@shared/core-utils'
3import { About, HttpStatusCode, ServerConfig } from '@shared/models'
4import { CustomConfig } from '../../models/server/custom-config.model'
5import { AbstractCommand, OverrideCommandOptions } from '../shared'
6
7export class ConfigCommand extends AbstractCommand {
8
9 static getCustomConfigResolutions (enabled: boolean) {
10 return {
11 '240p': enabled,
12 '360p': enabled,
13 '480p': enabled,
14 '720p': enabled,
15 '1080p': enabled,
16 '1440p': enabled,
17 '2160p': enabled
18 }
19 }
20
21 getConfig (options: OverrideCommandOptions = {}) {
22 const path = '/api/v1/config'
23
24 return this.getRequestBody<ServerConfig>({
25 ...options,
26
27 path,
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 })
31 }
32
33 getAbout (options: OverrideCommandOptions = {}) {
34 const path = '/api/v1/config/about'
35
36 return this.getRequestBody<About>({
37 ...options,
38
39 path,
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 })
43 }
44
45 getCustomConfig (options: OverrideCommandOptions = {}) {
46 const path = '/api/v1/config/custom'
47
48 return this.getRequestBody<CustomConfig>({
49 ...options,
50
51 path,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 updateCustomConfig (options: OverrideCommandOptions & {
58 newCustomConfig: CustomConfig
59 }) {
60 const path = '/api/v1/config/custom'
61
62 return this.putBodyRequest({
63 ...options,
64
65 path,
66 fields: options.newCustomConfig,
67 implicitToken: true,
68 defaultExpectedStatus: HttpStatusCode.OK_200
69 })
70 }
71
72 deleteCustomConfig (options: OverrideCommandOptions = {}) {
73 const path = '/api/v1/config/custom'
74
75 return this.deleteRequest({
76 ...options,
77
78 path,
79 implicitToken: true,
80 defaultExpectedStatus: HttpStatusCode.OK_200
81 })
82 }
83
84 updateCustomSubConfig (options: OverrideCommandOptions & {
85 newConfig: DeepPartial<CustomConfig>
86 }) {
87 const newCustomConfig: CustomConfig = {
88 instance: {
89 name: 'PeerTube updated',
90 shortDescription: 'my short description',
91 description: 'my super description',
92 terms: 'my super terms',
93 codeOfConduct: 'my super coc',
94
95 creationReason: 'my super creation reason',
96 moderationInformation: 'my super moderation information',
97 administrator: 'Kuja',
98 maintenanceLifetime: 'forever',
99 businessModel: 'my super business model',
100 hardwareInformation: '2vCore 3GB RAM',
101
102 languages: [ 'en', 'es' ],
103 categories: [ 1, 2 ],
104
105 isNSFW: true,
106 defaultNSFWPolicy: 'blur',
107
108 defaultClientRoute: '/videos/recently-added',
109
110 customizations: {
111 javascript: 'alert("coucou")',
112 css: 'body { background-color: red; }'
113 }
114 },
115 theme: {
116 default: 'default'
117 },
118 services: {
119 twitter: {
120 username: '@MySuperUsername',
121 whitelisted: true
122 }
123 },
124 cache: {
125 previews: {
126 size: 2
127 },
128 captions: {
129 size: 3
130 },
131 torrents: {
132 size: 4
133 }
134 },
135 signup: {
136 enabled: false,
137 limit: 5,
138 requiresEmailVerification: false,
139 minimumAge: 16
140 },
141 admin: {
142 email: 'superadmin1@example.com'
143 },
144 contactForm: {
145 enabled: true
146 },
147 user: {
148 videoQuota: 5242881,
149 videoQuotaDaily: 318742
150 },
151 transcoding: {
152 enabled: true,
153 allowAdditionalExtensions: true,
154 allowAudioFiles: true,
155 threads: 1,
156 concurrency: 3,
157 profile: 'default',
158 resolutions: {
159 '0p': false,
160 '240p': false,
161 '360p': true,
162 '480p': true,
163 '720p': false,
164 '1080p': false,
165 '1440p': false,
166 '2160p': false
167 },
168 webtorrent: {
169 enabled: true
170 },
171 hls: {
172 enabled: false
173 }
174 },
175 live: {
176 enabled: true,
177 allowReplay: false,
178 maxDuration: -1,
179 maxInstanceLives: -1,
180 maxUserLives: 50,
181 transcoding: {
182 enabled: true,
183 threads: 4,
184 profile: 'default',
185 resolutions: {
186 '240p': true,
187 '360p': true,
188 '480p': true,
189 '720p': true,
190 '1080p': true,
191 '1440p': true,
192 '2160p': true
193 }
194 }
195 },
196 import: {
197 videos: {
198 concurrency: 3,
199 http: {
200 enabled: false
201 },
202 torrent: {
203 enabled: false
204 }
205 }
206 },
207 trending: {
208 videos: {
209 algorithms: {
210 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
211 default: 'hot'
212 }
213 }
214 },
215 autoBlacklist: {
216 videos: {
217 ofUsers: {
218 enabled: false
219 }
220 }
221 },
222 followers: {
223 instance: {
224 enabled: true,
225 manualApproval: false
226 }
227 },
228 followings: {
229 instance: {
230 autoFollowBack: {
231 enabled: false
232 },
233 autoFollowIndex: {
234 indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
235 enabled: false
236 }
237 }
238 },
239 broadcastMessage: {
240 enabled: true,
241 level: 'warning',
242 message: 'hello',
243 dismissable: true
244 },
245 search: {
246 remoteUri: {
247 users: true,
248 anonymous: true
249 },
250 searchIndex: {
251 enabled: true,
252 url: 'https://search.joinpeertube.org',
253 disableLocalSearch: true,
254 isDefaultSearch: true
255 }
256 }
257 }
258
259 merge(newCustomConfig, options.newConfig)
260
261 return this.updateCustomConfig({ ...options, newCustomConfig })
262 }
263}
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
deleted file mode 100644
index 9fcfb31fd..000000000
--- a/shared/extra-utils/server/config.ts
+++ /dev/null
@@ -1,260 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../models/server/custom-config.model'
3import { DeepPartial, HttpStatusCode } from '@shared/core-utils'
4import { merge } from 'lodash'
5
6function getConfig (url: string) {
7 const path = '/api/v1/config'
8
9 return makeGetRequest({
10 url,
11 path,
12 statusCodeExpected: HttpStatusCode.OK_200
13 })
14}
15
16function getAbout (url: string) {
17 const path = '/api/v1/config/about'
18
19 return makeGetRequest({
20 url,
21 path,
22 statusCodeExpected: HttpStatusCode.OK_200
23 })
24}
25
26function getCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
27 const path = '/api/v1/config/custom'
28
29 return makeGetRequest({
30 url,
31 token,
32 path,
33 statusCodeExpected
34 })
35}
36
37function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = HttpStatusCode.OK_200) {
38 const path = '/api/v1/config/custom'
39
40 return makePutBodyRequest({
41 url,
42 token,
43 path,
44 fields: newCustomConfig,
45 statusCodeExpected
46 })
47}
48
49function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
50 const updateParams: CustomConfig = {
51 instance: {
52 name: 'PeerTube updated',
53 shortDescription: 'my short description',
54 description: 'my super description',
55 terms: 'my super terms',
56 codeOfConduct: 'my super coc',
57
58 creationReason: 'my super creation reason',
59 moderationInformation: 'my super moderation information',
60 administrator: 'Kuja',
61 maintenanceLifetime: 'forever',
62 businessModel: 'my super business model',
63 hardwareInformation: '2vCore 3GB RAM',
64
65 languages: [ 'en', 'es' ],
66 categories: [ 1, 2 ],
67
68 isNSFW: true,
69 defaultNSFWPolicy: 'blur',
70
71 defaultClientRoute: '/videos/recently-added',
72
73 customizations: {
74 javascript: 'alert("coucou")',
75 css: 'body { background-color: red; }'
76 }
77 },
78 theme: {
79 default: 'default'
80 },
81 services: {
82 twitter: {
83 username: '@MySuperUsername',
84 whitelisted: true
85 }
86 },
87 cache: {
88 previews: {
89 size: 2
90 },
91 captions: {
92 size: 3
93 },
94 torrents: {
95 size: 4
96 }
97 },
98 signup: {
99 enabled: false,
100 limit: 5,
101 requiresEmailVerification: false,
102 minimumAge: 16
103 },
104 admin: {
105 email: 'superadmin1@example.com'
106 },
107 contactForm: {
108 enabled: true
109 },
110 user: {
111 videoQuota: 5242881,
112 videoQuotaDaily: 318742
113 },
114 transcoding: {
115 enabled: true,
116 allowAdditionalExtensions: true,
117 allowAudioFiles: true,
118 threads: 1,
119 concurrency: 3,
120 profile: 'default',
121 resolutions: {
122 '0p': false,
123 '240p': false,
124 '360p': true,
125 '480p': true,
126 '720p': false,
127 '1080p': false,
128 '1440p': false,
129 '2160p': false
130 },
131 webtorrent: {
132 enabled: true
133 },
134 hls: {
135 enabled: false
136 }
137 },
138 live: {
139 enabled: true,
140 allowReplay: false,
141 maxDuration: -1,
142 maxInstanceLives: -1,
143 maxUserLives: 50,
144 transcoding: {
145 enabled: true,
146 threads: 4,
147 profile: 'default',
148 resolutions: {
149 '240p': true,
150 '360p': true,
151 '480p': true,
152 '720p': true,
153 '1080p': true,
154 '1440p': true,
155 '2160p': true
156 }
157 }
158 },
159 import: {
160 videos: {
161 concurrency: 3,
162 http: {
163 enabled: false
164 },
165 torrent: {
166 enabled: false
167 }
168 }
169 },
170 trending: {
171 videos: {
172 algorithms: {
173 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
174 default: 'hot'
175 }
176 }
177 },
178 autoBlacklist: {
179 videos: {
180 ofUsers: {
181 enabled: false
182 }
183 }
184 },
185 followers: {
186 instance: {
187 enabled: true,
188 manualApproval: false
189 }
190 },
191 followings: {
192 instance: {
193 autoFollowBack: {
194 enabled: false
195 },
196 autoFollowIndex: {
197 indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
198 enabled: false
199 }
200 }
201 },
202 broadcastMessage: {
203 enabled: true,
204 level: 'warning',
205 message: 'hello',
206 dismissable: true
207 },
208 search: {
209 remoteUri: {
210 users: true,
211 anonymous: true
212 },
213 searchIndex: {
214 enabled: true,
215 url: 'https://search.joinpeertube.org',
216 disableLocalSearch: true,
217 isDefaultSearch: true
218 }
219 }
220 }
221
222 merge(updateParams, newConfig)
223
224 return updateCustomConfig(url, token, updateParams)
225}
226
227function getCustomConfigResolutions (enabled: boolean) {
228 return {
229 '240p': enabled,
230 '360p': enabled,
231 '480p': enabled,
232 '720p': enabled,
233 '1080p': enabled,
234 '1440p': enabled,
235 '2160p': enabled
236 }
237}
238
239function deleteCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
240 const path = '/api/v1/config/custom'
241
242 return makeDeleteRequest({
243 url,
244 token,
245 path,
246 statusCodeExpected
247 })
248}
249
250// ---------------------------------------------------------------------------
251
252export {
253 getConfig,
254 getCustomConfig,
255 updateCustomConfig,
256 getAbout,
257 deleteCustomConfig,
258 updateCustomSubConfig,
259 getCustomConfigResolutions
260}
diff --git a/shared/extra-utils/server/contact-form-command.ts b/shared/extra-utils/server/contact-form-command.ts
new file mode 100644
index 000000000..0e8fd6d84
--- /dev/null
+++ b/shared/extra-utils/server/contact-form-command.ts
@@ -0,0 +1,31 @@
1import { HttpStatusCode } from '@shared/models'
2import { ContactForm } from '../../models/server'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class ContactFormCommand extends AbstractCommand {
6
7 send (options: OverrideCommandOptions & {
8 fromEmail: string
9 fromName: string
10 subject: string
11 body: string
12 }) {
13 const path = '/api/v1/server/contact'
14
15 const body: ContactForm = {
16 fromEmail: options.fromEmail,
17 fromName: options.fromName,
18 subject: options.subject,
19 body: options.body
20 }
21
22 return this.postBodyRequest({
23 ...options,
24
25 path,
26 fields: body,
27 implicitToken: false,
28 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
29 })
30 }
31}
diff --git a/shared/extra-utils/server/contact-form.ts b/shared/extra-utils/server/contact-form.ts
deleted file mode 100644
index 6c9232cc6..000000000
--- a/shared/extra-utils/server/contact-form.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as request from 'supertest'
2import { ContactForm } from '../../models/server'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function sendContactForm (options: {
6 url: string
7 fromEmail: string
8 fromName: string
9 subject: string
10 body: string
11 expectedStatus?: number
12}) {
13 const path = '/api/v1/server/contact'
14
15 const body: ContactForm = {
16 fromEmail: options.fromEmail,
17 fromName: options.fromName,
18 subject: options.subject,
19 body: options.body
20 }
21 return request(options.url)
22 .post(path)
23 .send(body)
24 .expect(options.expectedStatus || HttpStatusCode.NO_CONTENT_204)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 sendContactForm
31}
diff --git a/shared/extra-utils/server/debug-command.ts b/shared/extra-utils/server/debug-command.ts
new file mode 100644
index 000000000..3c5a785bb
--- /dev/null
+++ b/shared/extra-utils/server/debug-command.ts
@@ -0,0 +1,33 @@
1import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class DebugCommand extends AbstractCommand {
5
6 getDebug (options: OverrideCommandOptions = {}) {
7 const path = '/api/v1/server/debug'
8
9 return this.getRequestBody<Debug>({
10 ...options,
11
12 path,
13 implicitToken: true,
14 defaultExpectedStatus: HttpStatusCode.OK_200
15 })
16 }
17
18 sendCommand (options: OverrideCommandOptions & {
19 body: SendDebugCommand
20 }) {
21 const { body } = options
22 const path = '/api/v1/server/debug/run-command'
23
24 return this.postBodyRequest({
25 ...options,
26
27 path,
28 fields: body,
29 implicitToken: true,
30 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
31 })
32 }
33}
diff --git a/shared/extra-utils/server/debug.ts b/shared/extra-utils/server/debug.ts
deleted file mode 100644
index f196812b7..000000000
--- a/shared/extra-utils/server/debug.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import { makeGetRequest, makePostBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
3import { SendDebugCommand } from '@shared/models'
4
5function getDebug (url: string, token: string) {
6 const path = '/api/v1/server/debug'
7
8 return makeGetRequest({
9 url,
10 path,
11 token,
12 statusCodeExpected: HttpStatusCode.OK_200
13 })
14}
15
16function sendDebugCommand (url: string, token: string, body: SendDebugCommand) {
17 const path = '/api/v1/server/debug/run-command'
18
19 return makePostBodyRequest({
20 url,
21 path,
22 token,
23 fields: body,
24 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
25 })
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 getDebug,
32 sendDebugCommand
33}
diff --git a/shared/extra-utils/server/directories.ts b/shared/extra-utils/server/directories.ts
new file mode 100644
index 000000000..b6465cbf4
--- /dev/null
+++ b/shared/extra-utils/server/directories.ts
@@ -0,0 +1,34 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@server/helpers/core-utils'
7import { PeerTubeServer } from './server'
8
9async function checkTmpIsEmpty (server: PeerTubeServer) {
10 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
11
12 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
13 await checkDirectoryIsEmpty(server, 'tmp/hls')
14 }
15}
16
17async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
18 const testDirectory = 'test' + server.internalServerNumber
19
20 const directoryPath = join(root(), testDirectory, directory)
21
22 const directoryExists = await pathExists(directoryPath)
23 expect(directoryExists).to.be.true
24
25 const files = await readdir(directoryPath)
26 const filtered = files.filter(f => exceptions.includes(f) === false)
27
28 expect(filtered).to.have.lengthOf(0)
29}
30
31export {
32 checkTmpIsEmpty,
33 checkDirectoryIsEmpty
34}
diff --git a/shared/extra-utils/server/follows-command.ts b/shared/extra-utils/server/follows-command.ts
new file mode 100644
index 000000000..2b889cf66
--- /dev/null
+++ b/shared/extra-utils/server/follows-command.ts
@@ -0,0 +1,141 @@
1import { pick } from 'lodash'
2import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4import { PeerTubeServer } from './server'
5
6export class FollowsCommand extends AbstractCommand {
7
8 getFollowers (options: OverrideCommandOptions & {
9 start: number
10 count: number
11 sort: string
12 search?: string
13 actorType?: ActivityPubActorType
14 state?: FollowState
15 }) {
16 const path = '/api/v1/server/followers'
17
18 const toPick = [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]
19 const query = pick(options, toPick)
20
21 return this.getRequestBody<ResultList<ActorFollow>>({
22 ...options,
23
24 path,
25 query,
26 implicitToken: false,
27 defaultExpectedStatus: HttpStatusCode.OK_200
28 })
29 }
30
31 getFollowings (options: OverrideCommandOptions & {
32 start?: number
33 count?: number
34 sort?: string
35 search?: string
36 actorType?: ActivityPubActorType
37 state?: FollowState
38 } = {}) {
39 const path = '/api/v1/server/following'
40
41 const toPick = [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]
42 const query = pick(options, toPick)
43
44 return this.getRequestBody<ResultList<ActorFollow>>({
45 ...options,
46
47 path,
48 query,
49 implicitToken: false,
50 defaultExpectedStatus: HttpStatusCode.OK_200
51 })
52 }
53
54 follow (options: OverrideCommandOptions & {
55 hosts?: string[]
56 handles?: string[]
57 }) {
58 const path = '/api/v1/server/following'
59
60 const fields: ServerFollowCreate = {}
61
62 if (options.hosts) {
63 fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
64 }
65
66 if (options.handles) {
67 fields.handles = options.handles
68 }
69
70 return this.postBodyRequest({
71 ...options,
72
73 path,
74 fields,
75 implicitToken: true,
76 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
77 })
78 }
79
80 async unfollow (options: OverrideCommandOptions & {
81 target: PeerTubeServer | string
82 }) {
83 const { target } = options
84
85 const handle = typeof target === 'string'
86 ? target
87 : target.host
88
89 const path = '/api/v1/server/following/' + handle
90
91 return this.deleteRequest({
92 ...options,
93
94 path,
95 implicitToken: true,
96 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
97 })
98 }
99
100 acceptFollower (options: OverrideCommandOptions & {
101 follower: string
102 }) {
103 const path = '/api/v1/server/followers/' + options.follower + '/accept'
104
105 return this.postBodyRequest({
106 ...options,
107
108 path,
109 implicitToken: true,
110 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
111 })
112 }
113
114 rejectFollower (options: OverrideCommandOptions & {
115 follower: string
116 }) {
117 const path = '/api/v1/server/followers/' + options.follower + '/reject'
118
119 return this.postBodyRequest({
120 ...options,
121
122 path,
123 implicitToken: true,
124 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
125 })
126 }
127
128 removeFollower (options: OverrideCommandOptions & {
129 follower: PeerTubeServer
130 }) {
131 const path = '/api/v1/server/followers/peertube@' + options.follower.host
132
133 return this.deleteRequest({
134 ...options,
135
136 path,
137 implicitToken: true,
138 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
139 })
140 }
141}
diff --git a/shared/extra-utils/server/follows.ts b/shared/extra-utils/server/follows.ts
index 6aae4a31d..698238f29 100644
--- a/shared/extra-utils/server/follows.ts
+++ b/shared/extra-utils/server/follows.ts
@@ -1,126 +1,10 @@
1import * as request from 'supertest'
2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs' 1import { waitJobs } from './jobs'
4import { makePostBodyRequest } from '../requests/requests' 2import { PeerTubeServer } from './server'
5import { ActivityPubActorType, FollowState } from '@shared/models'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7 3
8function getFollowersListPaginationAndSort (options: { 4async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
9 url: string
10 start: number
11 count: number
12 sort: string
13 search?: string
14 actorType?: ActivityPubActorType
15 state?: FollowState
16}) {
17 const { url, start, count, sort, search, state, actorType } = options
18 const path = '/api/v1/server/followers'
19
20 const query = {
21 start,
22 count,
23 sort,
24 search,
25 state,
26 actorType
27 }
28
29 return request(url)
30 .get(path)
31 .query(query)
32 .set('Accept', 'application/json')
33 .expect(HttpStatusCode.OK_200)
34 .expect('Content-Type', /json/)
35}
36
37function acceptFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
38 const path = '/api/v1/server/followers/' + follower + '/accept'
39
40 return makePostBodyRequest({
41 url,
42 token,
43 path,
44 statusCodeExpected
45 })
46}
47
48function rejectFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
49 const path = '/api/v1/server/followers/' + follower + '/reject'
50
51 return makePostBodyRequest({
52 url,
53 token,
54 path,
55 statusCodeExpected
56 })
57}
58
59function getFollowingListPaginationAndSort (options: {
60 url: string
61 start: number
62 count: number
63 sort: string
64 search?: string
65 actorType?: ActivityPubActorType
66 state?: FollowState
67}) {
68 const { url, start, count, sort, search, state, actorType } = options
69 const path = '/api/v1/server/following'
70
71 const query = {
72 start,
73 count,
74 sort,
75 search,
76 state,
77 actorType
78 }
79
80 return request(url)
81 .get(path)
82 .query(query)
83 .set('Accept', 'application/json')
84 .expect(HttpStatusCode.OK_200)
85 .expect('Content-Type', /json/)
86}
87
88function follow (follower: string, following: string[], accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
89 const path = '/api/v1/server/following'
90
91 const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
92 return request(follower)
93 .post(path)
94 .set('Accept', 'application/json')
95 .set('Authorization', 'Bearer ' + accessToken)
96 .send({ hosts: followingHosts })
97 .expect(expectedStatus)
98}
99
100async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
101 const path = '/api/v1/server/following/' + target.host
102
103 return request(url)
104 .delete(path)
105 .set('Accept', 'application/json')
106 .set('Authorization', 'Bearer ' + accessToken)
107 .expect(expectedStatus)
108}
109
110function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
111 const path = '/api/v1/server/followers/peertube@' + follower.host
112
113 return request(url)
114 .delete(path)
115 .set('Accept', 'application/json')
116 .set('Authorization', 'Bearer ' + accessToken)
117 .expect(expectedStatus)
118}
119
120async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
121 await Promise.all([ 5 await Promise.all([
122 follow(server1.url, [ server2.url ], server1.accessToken), 6 server1.follows.follow({ hosts: [ server2.url ] }),
123 follow(server2.url, [ server1.url ], server2.accessToken) 7 server2.follows.follow({ hosts: [ server1.url ] })
124 ]) 8 ])
125 9
126 // Wait request propagation 10 // Wait request propagation
@@ -132,12 +16,5 @@ async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
132// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
133 17
134export { 18export {
135 getFollowersListPaginationAndSort, 19 doubleFollow
136 getFollowingListPaginationAndSort,
137 unfollow,
138 removeFollower,
139 follow,
140 doubleFollow,
141 acceptFollower,
142 rejectFollower
143} 20}
diff --git a/shared/extra-utils/server/index.ts b/shared/extra-utils/server/index.ts
new file mode 100644
index 000000000..9055dfc57
--- /dev/null
+++ b/shared/extra-utils/server/index.ts
@@ -0,0 +1,15 @@
1export * from './config-command'
2export * from './contact-form-command'
3export * from './debug-command'
4export * from './directories'
5export * from './follows-command'
6export * from './follows'
7export * from './jobs'
8export * from './jobs-command'
9export * from './plugins-command'
10export * from './plugins'
11export * from './redundancy-command'
12export * from './server'
13export * from './servers-command'
14export * from './servers'
15export * from './stats-command'
diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts
new file mode 100644
index 000000000..09a299e5b
--- /dev/null
+++ b/shared/extra-utils/server/jobs-command.ts
@@ -0,0 +1,36 @@
1import { pick } from 'lodash'
2import { HttpStatusCode } from '@shared/models'
3import { Job, JobState, JobType, ResultList } from '../../models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class JobsCommand extends AbstractCommand {
7
8 getJobsList (options: OverrideCommandOptions & {
9 state?: JobState
10 jobType?: JobType
11 start?: number
12 count?: number
13 sort?: string
14 } = {}) {
15 const path = this.buildJobsUrl(options.state)
16
17 const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
18
19 return this.getRequestBody<ResultList<Job>>({
20 ...options,
21
22 path,
23 query,
24 implicitToken: true,
25 defaultExpectedStatus: HttpStatusCode.OK_200
26 })
27 }
28
29 private buildJobsUrl (state?: JobState) {
30 let path = '/api/v1/jobs'
31
32 if (state) path += '/' + state
33
34 return path
35 }
36}
diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts
index 763374e03..64a0353eb 100644
--- a/shared/extra-utils/server/jobs.ts
+++ b/shared/extra-utils/server/jobs.ts
@@ -1,66 +1,17 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { getDebug, makeGetRequest } from '../../../shared/extra-utils'
4import { Job, JobState, JobType, ServerDebug } from '../../models'
5import { wait } from '../miscs/miscs'
6import { ServerInfo } from './servers'
7 1
8function buildJobsUrl (state?: JobState) { 2import { JobState } from '../../models'
9 let path = '/api/v1/jobs' 3import { wait } from '../miscs'
4import { PeerTubeServer } from './server'
10 5
11 if (state) path += '/' + state 6async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer) {
12
13 return path
14}
15
16function getJobsList (url: string, accessToken: string, state?: JobState) {
17 const path = buildJobsUrl(state)
18
19 return request(url)
20 .get(path)
21 .set('Accept', 'application/json')
22 .set('Authorization', 'Bearer ' + accessToken)
23 .expect(HttpStatusCode.OK_200)
24 .expect('Content-Type', /json/)
25}
26
27function getJobsListPaginationAndSort (options: {
28 url: string
29 accessToken: string
30 start: number
31 count: number
32 sort: string
33 state?: JobState
34 jobType?: JobType
35}) {
36 const { url, accessToken, state, start, count, sort, jobType } = options
37 const path = buildJobsUrl(state)
38
39 const query = {
40 start,
41 count,
42 sort,
43 jobType
44 }
45
46 return makeGetRequest({
47 url,
48 path,
49 token: accessToken,
50 statusCodeExpected: HttpStatusCode.OK_200,
51 query
52 })
53}
54
55async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
56 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT 7 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
57 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) 8 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
58 : 250 9 : 250
59 10
60 let servers: ServerInfo[] 11 let servers: PeerTubeServer[]
61 12
62 if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ] 13 if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
63 else servers = serversArg as ServerInfo[] 14 else servers = serversArg as PeerTubeServer[]
64 15
65 const states: JobState[] = [ 'waiting', 'active', 'delayed' ] 16 const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
66 const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ] 17 const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ]
@@ -72,15 +23,13 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
72 // Check if each server has pending request 23 // Check if each server has pending request
73 for (const server of servers) { 24 for (const server of servers) {
74 for (const state of states) { 25 for (const state of states) {
75 const p = getJobsListPaginationAndSort({ 26 const p = server.jobs.getJobsList({
76 url: server.url, 27 state,
77 accessToken: server.accessToken,
78 state: state,
79 start: 0, 28 start: 0,
80 count: 10, 29 count: 10,
81 sort: '-createdAt' 30 sort: '-createdAt'
82 }).then(res => res.body.data) 31 }).then(body => body.data)
83 .then((jobs: Job[]) => jobs.filter(j => !repeatableJobs.includes(j.type))) 32 .then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
84 .then(jobs => { 33 .then(jobs => {
85 if (jobs.length !== 0) { 34 if (jobs.length !== 0) {
86 pendingRequests = true 35 pendingRequests = true
@@ -90,9 +39,8 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
90 tasks.push(p) 39 tasks.push(p)
91 } 40 }
92 41
93 const p = getDebug(server.url, server.accessToken) 42 const p = server.debug.getDebug()
94 .then(res => res.body) 43 .then(obj => {
95 .then((obj: ServerDebug) => {
96 if (obj.activityPubMessagesWaiting !== 0) { 44 if (obj.activityPubMessagesWaiting !== 0) {
97 pendingRequests = true 45 pendingRequests = true
98 } 46 }
@@ -123,7 +71,5 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
123// --------------------------------------------------------------------------- 71// ---------------------------------------------------------------------------
124 72
125export { 73export {
126 getJobsList, 74 waitJobs
127 waitJobs,
128 getJobsListPaginationAndSort
129} 75}
diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts
new file mode 100644
index 000000000..b944475a2
--- /dev/null
+++ b/shared/extra-utils/server/plugins-command.ts
@@ -0,0 +1,256 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readJSON, writeJSON } from 'fs-extra'
4import { join } from 'path'
5import { root } from '@server/helpers/core-utils'
6import {
7 HttpStatusCode,
8 PeerTubePlugin,
9 PeerTubePluginIndex,
10 PeertubePluginIndexList,
11 PluginPackageJson,
12 PluginTranslation,
13 PluginType,
14 PublicServerSetting,
15 RegisteredServerSettings,
16 ResultList
17} from '@shared/models'
18import { AbstractCommand, OverrideCommandOptions } from '../shared'
19
20export class PluginsCommand extends AbstractCommand {
21
22 static getPluginTestPath (suffix = '') {
23 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
24 }
25
26 list (options: OverrideCommandOptions & {
27 start?: number
28 count?: number
29 sort?: string
30 pluginType?: PluginType
31 uninstalled?: boolean
32 }) {
33 const { start, count, sort, pluginType, uninstalled } = options
34 const path = '/api/v1/plugins'
35
36 return this.getRequestBody<ResultList<PeerTubePlugin>>({
37 ...options,
38
39 path,
40 query: {
41 start,
42 count,
43 sort,
44 pluginType,
45 uninstalled
46 },
47 implicitToken: true,
48 defaultExpectedStatus: HttpStatusCode.OK_200
49 })
50 }
51
52 listAvailable (options: OverrideCommandOptions & {
53 start?: number
54 count?: number
55 sort?: string
56 pluginType?: PluginType
57 currentPeerTubeEngine?: string
58 search?: string
59 expectedStatus?: HttpStatusCode
60 }) {
61 const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options
62 const path = '/api/v1/plugins/available'
63
64 const query: PeertubePluginIndexList = {
65 start,
66 count,
67 sort,
68 pluginType,
69 currentPeerTubeEngine,
70 search
71 }
72
73 return this.getRequestBody<ResultList<PeerTubePluginIndex>>({
74 ...options,
75
76 path,
77 query,
78 implicitToken: true,
79 defaultExpectedStatus: HttpStatusCode.OK_200
80 })
81 }
82
83 get (options: OverrideCommandOptions & {
84 npmName: string
85 }) {
86 const path = '/api/v1/plugins/' + options.npmName
87
88 return this.getRequestBody<PeerTubePlugin>({
89 ...options,
90
91 path,
92 implicitToken: true,
93 defaultExpectedStatus: HttpStatusCode.OK_200
94 })
95 }
96
97 updateSettings (options: OverrideCommandOptions & {
98 npmName: string
99 settings: any
100 }) {
101 const { npmName, settings } = options
102 const path = '/api/v1/plugins/' + npmName + '/settings'
103
104 return this.putBodyRequest({
105 ...options,
106
107 path,
108 fields: { settings },
109 implicitToken: true,
110 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
111 })
112 }
113
114 getRegisteredSettings (options: OverrideCommandOptions & {
115 npmName: string
116 }) {
117 const path = '/api/v1/plugins/' + options.npmName + '/registered-settings'
118
119 return this.getRequestBody<RegisteredServerSettings>({
120 ...options,
121
122 path,
123 implicitToken: true,
124 defaultExpectedStatus: HttpStatusCode.OK_200
125 })
126 }
127
128 getPublicSettings (options: OverrideCommandOptions & {
129 npmName: string
130 }) {
131 const { npmName } = options
132 const path = '/api/v1/plugins/' + npmName + '/public-settings'
133
134 return this.getRequestBody<PublicServerSetting>({
135 ...options,
136
137 path,
138 implicitToken: false,
139 defaultExpectedStatus: HttpStatusCode.OK_200
140 })
141 }
142
143 getTranslations (options: OverrideCommandOptions & {
144 locale: string
145 }) {
146 const { locale } = options
147 const path = '/plugins/translations/' + locale + '.json'
148
149 return this.getRequestBody<PluginTranslation>({
150 ...options,
151
152 path,
153 implicitToken: false,
154 defaultExpectedStatus: HttpStatusCode.OK_200
155 })
156 }
157
158 install (options: OverrideCommandOptions & {
159 path?: string
160 npmName?: string
161 }) {
162 const { npmName, path } = options
163 const apiPath = '/api/v1/plugins/install'
164
165 return this.postBodyRequest({
166 ...options,
167
168 path: apiPath,
169 fields: { npmName, path },
170 implicitToken: true,
171 defaultExpectedStatus: HttpStatusCode.OK_200
172 })
173 }
174
175 update (options: OverrideCommandOptions & {
176 path?: string
177 npmName?: string
178 }) {
179 const { npmName, path } = options
180 const apiPath = '/api/v1/plugins/update'
181
182 return this.postBodyRequest({
183 ...options,
184
185 path: apiPath,
186 fields: { npmName, path },
187 implicitToken: true,
188 defaultExpectedStatus: HttpStatusCode.OK_200
189 })
190 }
191
192 uninstall (options: OverrideCommandOptions & {
193 npmName: string
194 }) {
195 const { npmName } = options
196 const apiPath = '/api/v1/plugins/uninstall'
197
198 return this.postBodyRequest({
199 ...options,
200
201 path: apiPath,
202 fields: { npmName },
203 implicitToken: true,
204 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
205 })
206 }
207
208 getCSS (options: OverrideCommandOptions = {}) {
209 const path = '/plugins/global.css'
210
211 return this.getRequestText({
212 ...options,
213
214 path,
215 implicitToken: false,
216 defaultExpectedStatus: HttpStatusCode.OK_200
217 })
218 }
219
220 getExternalAuth (options: OverrideCommandOptions & {
221 npmName: string
222 npmVersion: string
223 authName: string
224 query?: any
225 }) {
226 const { npmName, npmVersion, authName, query } = options
227
228 const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
229
230 return this.getRequest({
231 ...options,
232
233 path,
234 query,
235 implicitToken: false,
236 defaultExpectedStatus: HttpStatusCode.OK_200,
237 redirects: 0
238 })
239 }
240
241 updatePackageJSON (npmName: string, json: any) {
242 const path = this.getPackageJSONPath(npmName)
243
244 return writeJSON(path, json)
245 }
246
247 getPackageJSON (npmName: string): Promise<PluginPackageJson> {
248 const path = this.getPackageJSONPath(npmName)
249
250 return readJSON(path)
251 }
252
253 private getPackageJSONPath (npmName: string) {
254 return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json'))
255 }
256}
diff --git a/shared/extra-utils/server/plugins.ts b/shared/extra-utils/server/plugins.ts
index d53e5b382..0f5fabd5a 100644
--- a/shared/extra-utils/server/plugins.ts
+++ b/shared/extra-utils/server/plugins.ts
@@ -1,307 +1,18 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { readJSON, writeJSON } from 'fs-extra' 4import { PeerTubeServer } from '../server/server'
5import { join } from 'path'
6import { RegisteredServerSettings } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
8import { PeertubePluginIndexList } from '../../models/plugins/plugin-index/peertube-plugin-index-list.model'
9import { PluginType } from '../../models/plugins/plugin.type'
10import { buildServerDirectory, root } from '../miscs/miscs'
11import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
12import { ServerInfo } from './servers'
13 5
14function listPlugins (parameters: { 6async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
15 url: string 7 const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
16 accessToken: string
17 start?: number
18 count?: number
19 sort?: string
20 pluginType?: PluginType
21 uninstalled?: boolean
22 expectedStatus?: HttpStatusCode
23}) {
24 const { url, accessToken, start, count, sort, pluginType, uninstalled, expectedStatus = HttpStatusCode.OK_200 } = parameters
25 const path = '/api/v1/plugins'
26
27 return makeGetRequest({
28 url,
29 path,
30 token: accessToken,
31 query: {
32 start,
33 count,
34 sort,
35 pluginType,
36 uninstalled
37 },
38 statusCodeExpected: expectedStatus
39 })
40}
41
42function listAvailablePlugins (parameters: {
43 url: string
44 accessToken: string
45 start?: number
46 count?: number
47 sort?: string
48 pluginType?: PluginType
49 currentPeerTubeEngine?: string
50 search?: string
51 expectedStatus?: HttpStatusCode
52}) {
53 const {
54 url,
55 accessToken,
56 start,
57 count,
58 sort,
59 pluginType,
60 search,
61 currentPeerTubeEngine,
62 expectedStatus = HttpStatusCode.OK_200
63 } = parameters
64 const path = '/api/v1/plugins/available'
65
66 const query: PeertubePluginIndexList = {
67 start,
68 count,
69 sort,
70 pluginType,
71 currentPeerTubeEngine,
72 search
73 }
74
75 return makeGetRequest({
76 url,
77 path,
78 token: accessToken,
79 query,
80 statusCodeExpected: expectedStatus
81 })
82}
83
84function getPlugin (parameters: {
85 url: string
86 accessToken: string
87 npmName: string
88 expectedStatus?: HttpStatusCode
89}) {
90 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
91 const path = '/api/v1/plugins/' + npmName
92
93 return makeGetRequest({
94 url,
95 path,
96 token: accessToken,
97 statusCodeExpected: expectedStatus
98 })
99}
100
101function updatePluginSettings (parameters: {
102 url: string
103 accessToken: string
104 npmName: string
105 settings: any
106 expectedStatus?: HttpStatusCode
107}) {
108 const { url, accessToken, npmName, settings, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
109 const path = '/api/v1/plugins/' + npmName + '/settings'
110
111 return makePutBodyRequest({
112 url,
113 path,
114 token: accessToken,
115 fields: { settings },
116 statusCodeExpected: expectedStatus
117 })
118}
119
120function getPluginRegisteredSettings (parameters: {
121 url: string
122 accessToken: string
123 npmName: string
124 expectedStatus?: HttpStatusCode
125}) {
126 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
127 const path = '/api/v1/plugins/' + npmName + '/registered-settings'
128
129 return makeGetRequest({
130 url,
131 path,
132 token: accessToken,
133 statusCodeExpected: expectedStatus
134 })
135}
136
137async function testHelloWorldRegisteredSettings (server: ServerInfo) {
138 const res = await getPluginRegisteredSettings({
139 url: server.url,
140 accessToken: server.accessToken,
141 npmName: 'peertube-plugin-hello-world'
142 })
143
144 const registeredSettings = (res.body as RegisteredServerSettings).registeredSettings
145 8
9 const registeredSettings = body.registeredSettings
146 expect(registeredSettings).to.have.length.at.least(1) 10 expect(registeredSettings).to.have.length.at.least(1)
147 11
148 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name') 12 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
149 expect(adminNameSettings).to.not.be.undefined 13 expect(adminNameSettings).to.not.be.undefined
150} 14}
151 15
152function getPublicSettings (parameters: {
153 url: string
154 npmName: string
155 expectedStatus?: HttpStatusCode
156}) {
157 const { url, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
158 const path = '/api/v1/plugins/' + npmName + '/public-settings'
159
160 return makeGetRequest({
161 url,
162 path,
163 statusCodeExpected: expectedStatus
164 })
165}
166
167function getPluginTranslations (parameters: {
168 url: string
169 locale: string
170 expectedStatus?: HttpStatusCode
171}) {
172 const { url, locale, expectedStatus = HttpStatusCode.OK_200 } = parameters
173 const path = '/plugins/translations/' + locale + '.json'
174
175 return makeGetRequest({
176 url,
177 path,
178 statusCodeExpected: expectedStatus
179 })
180}
181
182function installPlugin (parameters: {
183 url: string
184 accessToken: string
185 path?: string
186 npmName?: string
187 expectedStatus?: HttpStatusCode
188}) {
189 const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
190 const apiPath = '/api/v1/plugins/install'
191
192 return makePostBodyRequest({
193 url,
194 path: apiPath,
195 token: accessToken,
196 fields: { npmName, path },
197 statusCodeExpected: expectedStatus
198 })
199}
200
201function updatePlugin (parameters: {
202 url: string
203 accessToken: string
204 path?: string
205 npmName?: string
206 expectedStatus?: HttpStatusCode
207}) {
208 const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
209 const apiPath = '/api/v1/plugins/update'
210
211 return makePostBodyRequest({
212 url,
213 path: apiPath,
214 token: accessToken,
215 fields: { npmName, path },
216 statusCodeExpected: expectedStatus
217 })
218}
219
220function uninstallPlugin (parameters: {
221 url: string
222 accessToken: string
223 npmName: string
224 expectedStatus?: HttpStatusCode
225}) {
226 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
227 const apiPath = '/api/v1/plugins/uninstall'
228
229 return makePostBodyRequest({
230 url,
231 path: apiPath,
232 token: accessToken,
233 fields: { npmName },
234 statusCodeExpected: expectedStatus
235 })
236}
237
238function getPluginsCSS (url: string) {
239 const path = '/plugins/global.css'
240
241 return makeGetRequest({
242 url,
243 path,
244 statusCodeExpected: HttpStatusCode.OK_200
245 })
246}
247
248function getPackageJSONPath (server: ServerInfo, npmName: string) {
249 return buildServerDirectory(server, join('plugins', 'node_modules', npmName, 'package.json'))
250}
251
252function updatePluginPackageJSON (server: ServerInfo, npmName: string, json: any) {
253 const path = getPackageJSONPath(server, npmName)
254
255 return writeJSON(path, json)
256}
257
258function getPluginPackageJSON (server: ServerInfo, npmName: string) {
259 const path = getPackageJSONPath(server, npmName)
260
261 return readJSON(path)
262}
263
264function getPluginTestPath (suffix = '') {
265 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
266}
267
268function getExternalAuth (options: {
269 url: string
270 npmName: string
271 npmVersion: string
272 authName: string
273 query?: any
274 statusCodeExpected?: HttpStatusCode
275}) {
276 const { url, npmName, npmVersion, authName, statusCodeExpected, query } = options
277
278 const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
279
280 return makeGetRequest({
281 url,
282 path,
283 query,
284 statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200,
285 redirects: 0
286 })
287}
288
289export { 16export {
290 listPlugins, 17 testHelloWorldRegisteredSettings
291 listAvailablePlugins,
292 installPlugin,
293 getPluginTranslations,
294 getPluginsCSS,
295 updatePlugin,
296 getPlugin,
297 uninstallPlugin,
298 testHelloWorldRegisteredSettings,
299 updatePluginSettings,
300 getPluginRegisteredSettings,
301 getPackageJSONPath,
302 updatePluginPackageJSON,
303 getPluginPackageJSON,
304 getPluginTestPath,
305 getPublicSettings,
306 getExternalAuth
307} 18}
diff --git a/shared/extra-utils/server/redundancy-command.ts b/shared/extra-utils/server/redundancy-command.ts
new file mode 100644
index 000000000..e7a8b3c29
--- /dev/null
+++ b/shared/extra-utils/server/redundancy-command.ts
@@ -0,0 +1,80 @@
1import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class RedundancyCommand extends AbstractCommand {
5
6 updateRedundancy (options: OverrideCommandOptions & {
7 host: string
8 redundancyAllowed: boolean
9 }) {
10 const { host, redundancyAllowed } = options
11 const path = '/api/v1/server/redundancy/' + host
12
13 return this.putBodyRequest({
14 ...options,
15
16 path,
17 fields: { redundancyAllowed },
18 implicitToken: true,
19 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
20 })
21 }
22
23 listVideos (options: OverrideCommandOptions & {
24 target: VideoRedundanciesTarget
25 start?: number
26 count?: number
27 sort?: string
28 }) {
29 const path = '/api/v1/server/redundancy/videos'
30
31 const { target, start, count, sort } = options
32
33 return this.getRequestBody<ResultList<VideoRedundancy>>({
34 ...options,
35
36 path,
37
38 query: {
39 start: start ?? 0,
40 count: count ?? 5,
41 sort: sort ?? 'name',
42 target
43 },
44
45 implicitToken: true,
46 defaultExpectedStatus: HttpStatusCode.OK_200
47 })
48 }
49
50 addVideo (options: OverrideCommandOptions & {
51 videoId: number
52 }) {
53 const path = '/api/v1/server/redundancy/videos'
54 const { videoId } = options
55
56 return this.postBodyRequest({
57 ...options,
58
59 path,
60 fields: { videoId },
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
63 })
64 }
65
66 removeVideo (options: OverrideCommandOptions & {
67 redundancyId: number
68 }) {
69 const { redundancyId } = options
70 const path = '/api/v1/server/redundancy/videos/' + redundancyId
71
72 return this.deleteRequest({
73 ...options,
74
75 path,
76 implicitToken: true,
77 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
78 })
79 }
80}
diff --git a/shared/extra-utils/server/redundancy.ts b/shared/extra-utils/server/redundancy.ts
deleted file mode 100644
index b83815a37..000000000
--- a/shared/extra-utils/server/redundancy.ts
+++ /dev/null
@@ -1,88 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2import { VideoRedundanciesTarget } from '@shared/models'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function updateRedundancy (
6 url: string,
7 accessToken: string,
8 host: string,
9 redundancyAllowed: boolean,
10 expectedStatus = HttpStatusCode.NO_CONTENT_204
11) {
12 const path = '/api/v1/server/redundancy/' + host
13
14 return makePutBodyRequest({
15 url,
16 path,
17 token: accessToken,
18 fields: { redundancyAllowed },
19 statusCodeExpected: expectedStatus
20 })
21}
22
23function listVideoRedundancies (options: {
24 url: string
25 accessToken: string
26 target: VideoRedundanciesTarget
27 start?: number
28 count?: number
29 sort?: string
30 statusCodeExpected?: HttpStatusCode
31}) {
32 const path = '/api/v1/server/redundancy/videos'
33
34 const { url, accessToken, target, statusCodeExpected, start, count, sort } = options
35
36 return makeGetRequest({
37 url,
38 token: accessToken,
39 path,
40 query: {
41 start: start ?? 0,
42 count: count ?? 5,
43 sort: sort ?? 'name',
44 target
45 },
46 statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200
47 })
48}
49
50function addVideoRedundancy (options: {
51 url: string
52 accessToken: string
53 videoId: number
54}) {
55 const path = '/api/v1/server/redundancy/videos'
56 const { url, accessToken, videoId } = options
57
58 return makePostBodyRequest({
59 url,
60 token: accessToken,
61 path,
62 fields: { videoId },
63 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
64 })
65}
66
67function removeVideoRedundancy (options: {
68 url: string
69 accessToken: string
70 redundancyId: number
71}) {
72 const { url, accessToken, redundancyId } = options
73 const path = '/api/v1/server/redundancy/videos/' + redundancyId
74
75 return makeDeleteRequest({
76 url,
77 token: accessToken,
78 path,
79 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
80 })
81}
82
83export {
84 updateRedundancy,
85 listVideoRedundancies,
86 addVideoRedundancy,
87 removeVideoRedundancy
88}
diff --git a/shared/extra-utils/server/server.ts b/shared/extra-utils/server/server.ts
new file mode 100644
index 000000000..3c335b8e4
--- /dev/null
+++ b/shared/extra-utils/server/server.ts
@@ -0,0 +1,369 @@
1import { ChildProcess, fork } from 'child_process'
2import { copy } from 'fs-extra'
3import { join } from 'path'
4import { root } from '@server/helpers/core-utils'
5import { randomInt } from '@shared/core-utils'
6import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '../../models/videos'
7import { BulkCommand } from '../bulk'
8import { CLICommand } from '../cli'
9import { CustomPagesCommand } from '../custom-pages'
10import { FeedCommand } from '../feeds'
11import { LogsCommand } from '../logs'
12import { parallelTests, SQLCommand } from '../miscs'
13import { AbusesCommand } from '../moderation'
14import { OverviewsCommand } from '../overviews'
15import { SearchCommand } from '../search'
16import { SocketIOCommand } from '../socket'
17import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users'
18import {
19 BlacklistCommand,
20 CaptionsCommand,
21 ChangeOwnershipCommand,
22 ChannelsCommand,
23 HistoryCommand,
24 ImportsCommand,
25 LiveCommand,
26 PlaylistsCommand,
27 ServicesCommand,
28 StreamingPlaylistsCommand,
29 VideosCommand
30} from '../videos'
31import { CommentsCommand } from '../videos/comments-command'
32import { ConfigCommand } from './config-command'
33import { ContactFormCommand } from './contact-form-command'
34import { DebugCommand } from './debug-command'
35import { FollowsCommand } from './follows-command'
36import { JobsCommand } from './jobs-command'
37import { PluginsCommand } from './plugins-command'
38import { RedundancyCommand } from './redundancy-command'
39import { ServersCommand } from './servers-command'
40import { StatsCommand } from './stats-command'
41
42export type RunServerOptions = {
43 hideLogs?: boolean
44 nodeArgs?: string[]
45 peertubeArgs?: string[]
46}
47
48export class PeerTubeServer {
49 app?: ChildProcess
50
51 url: string
52 host?: string
53 hostname?: string
54 port?: number
55
56 rtmpPort?: number
57
58 parallel?: boolean
59 internalServerNumber: number
60
61 serverNumber?: number
62 customConfigFile?: string
63
64 store?: {
65 client?: {
66 id?: string
67 secret?: string
68 }
69
70 user?: {
71 username: string
72 password: string
73 email?: string
74 }
75
76 channel?: VideoChannel
77
78 video?: Video
79 videoCreated?: VideoCreateResult
80 videoDetails?: VideoDetails
81
82 videos?: { id: number, uuid: string }[]
83 }
84
85 accessToken?: string
86 refreshToken?: string
87
88 bulk?: BulkCommand
89 cli?: CLICommand
90 customPage?: CustomPagesCommand
91 feed?: FeedCommand
92 logs?: LogsCommand
93 abuses?: AbusesCommand
94 overviews?: OverviewsCommand
95 search?: SearchCommand
96 contactForm?: ContactFormCommand
97 debug?: DebugCommand
98 follows?: FollowsCommand
99 jobs?: JobsCommand
100 plugins?: PluginsCommand
101 redundancy?: RedundancyCommand
102 stats?: StatsCommand
103 config?: ConfigCommand
104 socketIO?: SocketIOCommand
105 accounts?: AccountsCommand
106 blocklist?: BlocklistCommand
107 subscriptions?: SubscriptionsCommand
108 live?: LiveCommand
109 services?: ServicesCommand
110 blacklist?: BlacklistCommand
111 captions?: CaptionsCommand
112 changeOwnership?: ChangeOwnershipCommand
113 playlists?: PlaylistsCommand
114 history?: HistoryCommand
115 imports?: ImportsCommand
116 streamingPlaylists?: StreamingPlaylistsCommand
117 channels?: ChannelsCommand
118 comments?: CommentsCommand
119 sql?: SQLCommand
120 notifications?: NotificationsCommand
121 servers?: ServersCommand
122 login?: LoginCommand
123 users?: UsersCommand
124 videos?: VideosCommand
125
126 constructor (options: { serverNumber: number } | { url: string }) {
127 if ((options as any).url) {
128 this.setUrl((options as any).url)
129 } else {
130 this.setServerNumber((options as any).serverNumber)
131 }
132
133 this.store = {
134 client: {
135 id: null,
136 secret: null
137 },
138 user: {
139 username: null,
140 password: null
141 }
142 }
143
144 this.assignCommands()
145 }
146
147 setServerNumber (serverNumber: number) {
148 this.serverNumber = serverNumber
149
150 this.parallel = parallelTests()
151
152 this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
153 this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
154 this.port = 9000 + this.internalServerNumber
155
156 this.url = `http://localhost:${this.port}`
157 this.host = `localhost:${this.port}`
158 this.hostname = 'localhost'
159 }
160
161 setUrl (url: string) {
162 const parsed = new URL(url)
163
164 this.url = url
165 this.host = parsed.host
166 this.hostname = parsed.hostname
167 this.port = parseInt(parsed.port)
168 }
169
170 async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
171 await ServersCommand.flushTests(this.internalServerNumber)
172
173 return this.run(configOverride, options)
174 }
175
176 async run (configOverrideArg?: any, options: RunServerOptions = {}) {
177 // These actions are async so we need to be sure that they have both been done
178 const serverRunString = {
179 'HTTP server listening': false
180 }
181 const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
182 serverRunString[key] = false
183
184 const regexps = {
185 client_id: 'Client id: (.+)',
186 client_secret: 'Client secret: (.+)',
187 user_username: 'Username: (.+)',
188 user_password: 'User password: (.+)'
189 }
190
191 await this.assignCustomConfigFile()
192
193 const configOverride = this.buildConfigOverride()
194
195 if (configOverrideArg !== undefined) {
196 Object.assign(configOverride, configOverrideArg)
197 }
198
199 // Share the environment
200 const env = Object.create(process.env)
201 env['NODE_ENV'] = 'test'
202 env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
203 env['NODE_CONFIG'] = JSON.stringify(configOverride)
204
205 const forkOptions = {
206 silent: true,
207 env,
208 detached: true,
209 execArgv: options.nodeArgs || []
210 }
211
212 return new Promise<void>(res => {
213 const self = this
214
215 this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
216 this.app.stdout.on('data', function onStdout (data) {
217 let dontContinue = false
218
219 // Capture things if we want to
220 for (const key of Object.keys(regexps)) {
221 const regexp = regexps[key]
222 const matches = data.toString().match(regexp)
223 if (matches !== null) {
224 if (key === 'client_id') self.store.client.id = matches[1]
225 else if (key === 'client_secret') self.store.client.secret = matches[1]
226 else if (key === 'user_username') self.store.user.username = matches[1]
227 else if (key === 'user_password') self.store.user.password = matches[1]
228 }
229 }
230
231 // Check if all required sentences are here
232 for (const key of Object.keys(serverRunString)) {
233 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
234 if (serverRunString[key] === false) dontContinue = true
235 }
236
237 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
238 if (dontContinue === true) return
239
240 if (options.hideLogs === false) {
241 console.log(data.toString())
242 } else {
243 self.app.stdout.removeListener('data', onStdout)
244 }
245
246 process.on('exit', () => {
247 try {
248 process.kill(self.app.pid)
249 } catch { /* empty */ }
250 })
251
252 res()
253 })
254 })
255 }
256
257 async kill () {
258 if (!this.app) return
259
260 await this.sql.cleanup()
261
262 process.kill(-this.app.pid)
263
264 this.app = null
265 }
266
267 private randomServer () {
268 const low = 10
269 const high = 10000
270
271 return randomInt(low, high)
272 }
273
274 private randomRTMP () {
275 const low = 1900
276 const high = 2100
277
278 return randomInt(low, high)
279 }
280
281 private async assignCustomConfigFile () {
282 if (this.internalServerNumber === this.serverNumber) return
283
284 const basePath = join(root(), 'config')
285
286 const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
287 await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
288
289 this.customConfigFile = tmpConfigFile
290 }
291
292 private buildConfigOverride () {
293 if (!this.parallel) return {}
294
295 return {
296 listen: {
297 port: this.port
298 },
299 webserver: {
300 port: this.port
301 },
302 database: {
303 suffix: '_test' + this.internalServerNumber
304 },
305 storage: {
306 tmp: `test${this.internalServerNumber}/tmp/`,
307 avatars: `test${this.internalServerNumber}/avatars/`,
308 videos: `test${this.internalServerNumber}/videos/`,
309 streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
310 redundancy: `test${this.internalServerNumber}/redundancy/`,
311 logs: `test${this.internalServerNumber}/logs/`,
312 previews: `test${this.internalServerNumber}/previews/`,
313 thumbnails: `test${this.internalServerNumber}/thumbnails/`,
314 torrents: `test${this.internalServerNumber}/torrents/`,
315 captions: `test${this.internalServerNumber}/captions/`,
316 cache: `test${this.internalServerNumber}/cache/`,
317 plugins: `test${this.internalServerNumber}/plugins/`
318 },
319 admin: {
320 email: `admin${this.internalServerNumber}@example.com`
321 },
322 live: {
323 rtmp: {
324 port: this.rtmpPort
325 }
326 }
327 }
328 }
329
330 private assignCommands () {
331 this.bulk = new BulkCommand(this)
332 this.cli = new CLICommand(this)
333 this.customPage = new CustomPagesCommand(this)
334 this.feed = new FeedCommand(this)
335 this.logs = new LogsCommand(this)
336 this.abuses = new AbusesCommand(this)
337 this.overviews = new OverviewsCommand(this)
338 this.search = new SearchCommand(this)
339 this.contactForm = new ContactFormCommand(this)
340 this.debug = new DebugCommand(this)
341 this.follows = new FollowsCommand(this)
342 this.jobs = new JobsCommand(this)
343 this.plugins = new PluginsCommand(this)
344 this.redundancy = new RedundancyCommand(this)
345 this.stats = new StatsCommand(this)
346 this.config = new ConfigCommand(this)
347 this.socketIO = new SocketIOCommand(this)
348 this.accounts = new AccountsCommand(this)
349 this.blocklist = new BlocklistCommand(this)
350 this.subscriptions = new SubscriptionsCommand(this)
351 this.live = new LiveCommand(this)
352 this.services = new ServicesCommand(this)
353 this.blacklist = new BlacklistCommand(this)
354 this.captions = new CaptionsCommand(this)
355 this.changeOwnership = new ChangeOwnershipCommand(this)
356 this.playlists = new PlaylistsCommand(this)
357 this.history = new HistoryCommand(this)
358 this.imports = new ImportsCommand(this)
359 this.streamingPlaylists = new StreamingPlaylistsCommand(this)
360 this.channels = new ChannelsCommand(this)
361 this.comments = new CommentsCommand(this)
362 this.sql = new SQLCommand(this)
363 this.notifications = new NotificationsCommand(this)
364 this.servers = new ServersCommand(this)
365 this.login = new LoginCommand(this)
366 this.users = new UsersCommand(this)
367 this.videos = new VideosCommand(this)
368 }
369}
diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts
new file mode 100644
index 000000000..40a11e8d7
--- /dev/null
+++ b/shared/extra-utils/server/servers-command.ts
@@ -0,0 +1,88 @@
1import { exec } from 'child_process'
2import { copy, ensureDir, readFile, remove } from 'fs-extra'
3import { basename, join } from 'path'
4import { root } from '@server/helpers/core-utils'
5import { HttpStatusCode } from '@shared/models'
6import { getFileSize, isGithubCI, wait } from '../miscs'
7import { AbstractCommand, OverrideCommandOptions } from '../shared'
8
9export class ServersCommand extends AbstractCommand {
10
11 static flushTests (internalServerNumber: number) {
12 return new Promise<void>((res, rej) => {
13 const suffix = ` -- ${internalServerNumber}`
14
15 return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
16 if (err || stderr) return rej(err || new Error(stderr))
17
18 return res()
19 })
20 })
21 }
22
23 ping (options: OverrideCommandOptions = {}) {
24 return this.getRequestBody({
25 ...options,
26
27 path: '/api/v1/ping',
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 })
31 }
32
33 async cleanupTests () {
34 const p: Promise<any>[] = []
35
36 if (isGithubCI()) {
37 await ensureDir('artifacts')
38
39 const origin = this.buildDirectory('logs/peertube.log')
40 const destname = `peertube-${this.server.internalServerNumber}.log`
41 console.log('Saving logs %s.', destname)
42
43 await copy(origin, join('artifacts', destname))
44 }
45
46 if (this.server.parallel) {
47 p.push(ServersCommand.flushTests(this.server.internalServerNumber))
48 }
49
50 if (this.server.customConfigFile) {
51 p.push(remove(this.server.customConfigFile))
52 }
53
54 return p
55 }
56
57 async waitUntilLog (str: string, count = 1, strictCount = true) {
58 const logfile = this.server.servers.buildDirectory('logs/peertube.log')
59
60 while (true) {
61 const buf = await readFile(logfile)
62
63 const matches = buf.toString().match(new RegExp(str, 'g'))
64 if (matches && matches.length === count) return
65 if (matches && strictCount === false && matches.length >= count) return
66
67 await wait(1000)
68 }
69 }
70
71 buildDirectory (directory: string) {
72 return join(root(), 'test' + this.server.internalServerNumber, directory)
73 }
74
75 buildWebTorrentFilePath (fileUrl: string) {
76 return this.buildDirectory(join('videos', basename(fileUrl)))
77 }
78
79 buildFragmentedFilePath (videoUUID: string, fileUrl: string) {
80 return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
81 }
82
83 async getServerFileSize (subPath: string) {
84 const path = this.server.servers.buildDirectory(subPath)
85
86 return getFileSize(path)
87 }
88}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index 28e431e94..f0622feb0 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -1,384 +1,49 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1import { ensureDir } from 'fs-extra'
2import { isGithubCI } from '../miscs'
3import { PeerTubeServer, RunServerOptions } from './server'
2 4
3import { expect } from 'chai' 5async function createSingleServer (serverNumber: number, configOverride?: Object, options: RunServerOptions = {}) {
4import { ChildProcess, exec, fork } from 'child_process' 6 const server = new PeerTubeServer({ serverNumber })
5import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
6import { join } from 'path'
7import { randomInt } from '../../core-utils/miscs/miscs'
8import { VideoChannel } from '../../models/videos'
9import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
10import { makeGetRequest } from '../requests/requests'
11 7
12interface ServerInfo { 8 await server.flushAndRun(configOverride, options)
13 app: ChildProcess
14
15 url: string
16 host: string
17 hostname: string
18 port: number
19
20 rtmpPort: number
21
22 parallel: boolean
23 internalServerNumber: number
24 serverNumber: number
25
26 client: {
27 id: string
28 secret: string
29 }
30
31 user: {
32 username: string
33 password: string
34 email?: string
35 }
36
37 customConfigFile?: string
38
39 accessToken?: string
40 refreshToken?: string
41 videoChannel?: VideoChannel
42
43 video?: {
44 id: number
45 uuid: string
46 shortUUID: string
47 name?: string
48 url?: string
49
50 account?: {
51 name: string
52 }
53
54 embedPath?: string
55 }
56
57 remoteVideo?: {
58 id: number
59 uuid: string
60 }
61
62 videos?: { id: number, uuid: string }[]
63}
64
65function parallelTests () {
66 return process.env.MOCHA_PARALLEL === 'true'
67}
68
69function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
70 const apps = []
71 let i = 0
72
73 return new Promise<ServerInfo[]>(res => {
74 function anotherServerDone (serverNumber, app) {
75 apps[serverNumber - 1] = app
76 i++
77 if (i === totalServers) {
78 return res(apps)
79 }
80 }
81
82 for (let j = 1; j <= totalServers; j++) {
83 flushAndRunServer(j, configOverride).then(app => anotherServerDone(j, app))
84 }
85 })
86}
87
88function flushTests (serverNumber?: number) {
89 return new Promise<void>((res, rej) => {
90 const suffix = serverNumber ? ` -- ${serverNumber}` : ''
91
92 return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
93 if (err || stderr) return rej(err || new Error(stderr))
94
95 return res()
96 })
97 })
98}
99
100function randomServer () {
101 const low = 10
102 const high = 10000
103
104 return randomInt(low, high)
105}
106
107function randomRTMP () {
108 const low = 1900
109 const high = 2100
110
111 return randomInt(low, high)
112}
113
114type RunServerOptions = {
115 hideLogs?: boolean
116 execArgv?: string[]
117}
118
119async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = [], options: RunServerOptions = {}) {
120 const parallel = parallelTests()
121
122 const internalServerNumber = parallel ? randomServer() : serverNumber
123 const rtmpPort = parallel ? randomRTMP() : 1936
124 const port = 9000 + internalServerNumber
125
126 await flushTests(internalServerNumber)
127
128 const server: ServerInfo = {
129 app: null,
130 port,
131 internalServerNumber,
132 rtmpPort,
133 parallel,
134 serverNumber,
135 url: `http://localhost:${port}`,
136 host: `localhost:${port}`,
137 hostname: 'localhost',
138 client: {
139 id: null,
140 secret: null
141 },
142 user: {
143 username: null,
144 password: null
145 }
146 }
147
148 return runServer(server, configOverride, args, options)
149}
150
151async function runServer (server: ServerInfo, configOverrideArg?: any, args = [], options: RunServerOptions = {}) {
152 // These actions are async so we need to be sure that they have both been done
153 const serverRunString = {
154 'HTTP server listening': false
155 }
156 const key = 'Database peertube_test' + server.internalServerNumber + ' is ready'
157 serverRunString[key] = false
158
159 const regexps = {
160 client_id: 'Client id: (.+)',
161 client_secret: 'Client secret: (.+)',
162 user_username: 'Username: (.+)',
163 user_password: 'User password: (.+)'
164 }
165
166 if (server.internalServerNumber !== server.serverNumber) {
167 const basePath = join(root(), 'config')
168
169 const tmpConfigFile = join(basePath, `test-${server.internalServerNumber}.yaml`)
170 await copy(join(basePath, `test-${server.serverNumber}.yaml`), tmpConfigFile)
171
172 server.customConfigFile = tmpConfigFile
173 }
174
175 const configOverride: any = {}
176
177 if (server.parallel) {
178 Object.assign(configOverride, {
179 listen: {
180 port: server.port
181 },
182 webserver: {
183 port: server.port
184 },
185 database: {
186 suffix: '_test' + server.internalServerNumber
187 },
188 storage: {
189 tmp: `test${server.internalServerNumber}/tmp/`,
190 avatars: `test${server.internalServerNumber}/avatars/`,
191 videos: `test${server.internalServerNumber}/videos/`,
192 streaming_playlists: `test${server.internalServerNumber}/streaming-playlists/`,
193 redundancy: `test${server.internalServerNumber}/redundancy/`,
194 logs: `test${server.internalServerNumber}/logs/`,
195 previews: `test${server.internalServerNumber}/previews/`,
196 thumbnails: `test${server.internalServerNumber}/thumbnails/`,
197 torrents: `test${server.internalServerNumber}/torrents/`,
198 captions: `test${server.internalServerNumber}/captions/`,
199 cache: `test${server.internalServerNumber}/cache/`,
200 plugins: `test${server.internalServerNumber}/plugins/`
201 },
202 admin: {
203 email: `admin${server.internalServerNumber}@example.com`
204 },
205 live: {
206 rtmp: {
207 port: server.rtmpPort
208 }
209 }
210 })
211 }
212
213 if (configOverrideArg !== undefined) {
214 Object.assign(configOverride, configOverrideArg)
215 }
216
217 // Share the environment
218 const env = Object.create(process.env)
219 env['NODE_ENV'] = 'test'
220 env['NODE_APP_INSTANCE'] = server.internalServerNumber.toString()
221 env['NODE_CONFIG'] = JSON.stringify(configOverride)
222
223 const forkOptions = {
224 silent: true,
225 env,
226 detached: true,
227 execArgv: options.execArgv || []
228 }
229
230 return new Promise<ServerInfo>(res => {
231 server.app = fork(join(root(), 'dist', 'server.js'), args, forkOptions)
232 server.app.stdout.on('data', function onStdout (data) {
233 let dontContinue = false
234
235 // Capture things if we want to
236 for (const key of Object.keys(regexps)) {
237 const regexp = regexps[key]
238 const matches = data.toString().match(regexp)
239 if (matches !== null) {
240 if (key === 'client_id') server.client.id = matches[1]
241 else if (key === 'client_secret') server.client.secret = matches[1]
242 else if (key === 'user_username') server.user.username = matches[1]
243 else if (key === 'user_password') server.user.password = matches[1]
244 }
245 }
246
247 // Check if all required sentences are here
248 for (const key of Object.keys(serverRunString)) {
249 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
250 if (serverRunString[key] === false) dontContinue = true
251 }
252
253 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
254 if (dontContinue === true) return
255
256 if (options.hideLogs === false) {
257 console.log(data.toString())
258 } else {
259 server.app.stdout.removeListener('data', onStdout)
260 }
261
262 process.on('exit', () => {
263 try {
264 process.kill(server.app.pid)
265 } catch { /* empty */ }
266 })
267
268 res(server)
269 })
270 })
271}
272
273async function reRunServer (server: ServerInfo, configOverride?: any) {
274 const newServer = await runServer(server, configOverride)
275 server.app = newServer.app
276 9
277 return server 10 return server
278} 11}
279 12
280async function checkTmpIsEmpty (server: ServerInfo) { 13function createMultipleServers (totalServers: number, configOverride?: Object) {
281 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ]) 14 const serverPromises: Promise<PeerTubeServer>[] = []
282 15
283 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) { 16 for (let i = 1; i <= totalServers; i++) {
284 await checkDirectoryIsEmpty(server, 'tmp/hls') 17 serverPromises.push(createSingleServer(i, configOverride))
285 } 18 }
286}
287
288async function checkDirectoryIsEmpty (server: ServerInfo, directory: string, exceptions: string[] = []) {
289 const testDirectory = 'test' + server.internalServerNumber
290
291 const directoryPath = join(root(), testDirectory, directory)
292 19
293 const directoryExists = await pathExists(directoryPath) 20 return Promise.all(serverPromises)
294 expect(directoryExists).to.be.true
295
296 const files = await readdir(directoryPath)
297 const filtered = files.filter(f => exceptions.includes(f) === false)
298
299 expect(filtered).to.have.lengthOf(0)
300} 21}
301 22
302function killallServers (servers: ServerInfo[]) { 23async function killallServers (servers: PeerTubeServer[]) {
303 for (const server of servers) { 24 return Promise.all(servers.map(s => s.kill()))
304 if (!server.app) continue
305
306 process.kill(-server.app.pid)
307 server.app = null
308 }
309} 25}
310 26
311async function cleanupTests (servers: ServerInfo[]) { 27async function cleanupTests (servers: PeerTubeServer[]) {
312 killallServers(servers) 28 await killallServers(servers)
313 29
314 if (isGithubCI()) { 30 if (isGithubCI()) {
315 await ensureDir('artifacts') 31 await ensureDir('artifacts')
316 } 32 }
317 33
318 const p: Promise<any>[] = [] 34 let p: Promise<any>[] = []
319 for (const server of servers) { 35 for (const server of servers) {
320 if (isGithubCI()) { 36 p = p.concat(server.servers.cleanupTests())
321 const origin = await buildServerDirectory(server, 'logs/peertube.log')
322 const destname = `peertube-${server.internalServerNumber}.log`
323 console.log('Saving logs %s.', destname)
324
325 await copy(origin, join('artifacts', destname))
326 }
327
328 if (server.parallel) {
329 p.push(flushTests(server.internalServerNumber))
330 }
331
332 if (server.customConfigFile) {
333 p.push(remove(server.customConfigFile))
334 }
335 } 37 }
336 38
337 return Promise.all(p) 39 return Promise.all(p)
338} 40}
339 41
340async function waitUntilLog (server: ServerInfo, str: string, count = 1, strictCount = true) {
341 const logfile = buildServerDirectory(server, 'logs/peertube.log')
342
343 while (true) {
344 const buf = await readFile(logfile)
345
346 const matches = buf.toString().match(new RegExp(str, 'g'))
347 if (matches && matches.length === count) return
348 if (matches && strictCount === false && matches.length >= count) return
349
350 await wait(1000)
351 }
352}
353
354async function getServerFileSize (server: ServerInfo, subPath: string) {
355 const path = buildServerDirectory(server, subPath)
356
357 return getFileSize(path)
358}
359
360function makePingRequest (server: ServerInfo) {
361 return makeGetRequest({
362 url: server.url,
363 path: '/api/v1/ping',
364 statusCodeExpected: 200
365 })
366}
367
368// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
369 43
370export { 44export {
371 checkDirectoryIsEmpty, 45 createSingleServer,
372 checkTmpIsEmpty, 46 createMultipleServers,
373 getServerFileSize,
374 ServerInfo,
375 parallelTests,
376 cleanupTests, 47 cleanupTests,
377 flushAndRunMultipleServers, 48 killallServers
378 flushTests,
379 makePingRequest,
380 flushAndRunServer,
381 killallServers,
382 reRunServer,
383 waitUntilLog
384} 49}
diff --git a/shared/extra-utils/server/stats-command.ts b/shared/extra-utils/server/stats-command.ts
new file mode 100644
index 000000000..64a452306
--- /dev/null
+++ b/shared/extra-utils/server/stats-command.ts
@@ -0,0 +1,25 @@
1import { HttpStatusCode, ServerStats } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class StatsCommand extends AbstractCommand {
5
6 get (options: OverrideCommandOptions & {
7 useCache?: boolean // default false
8 } = {}) {
9 const { useCache = false } = options
10 const path = '/api/v1/server/stats'
11
12 const query = {
13 t: useCache ? undefined : new Date().getTime()
14 }
15
16 return this.getRequestBody<ServerStats>({
17 ...options,
18
19 path,
20 query,
21 implicitToken: false,
22 defaultExpectedStatus: HttpStatusCode.OK_200
23 })
24 }
25}
diff --git a/shared/extra-utils/server/stats.ts b/shared/extra-utils/server/stats.ts
deleted file mode 100644
index b9dae24e2..000000000
--- a/shared/extra-utils/server/stats.ts
+++ /dev/null
@@ -1,23 +0,0 @@
1import { makeGetRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getStats (url: string, useCache = false) {
5 const path = '/api/v1/server/stats'
6
7 const query = {
8 t: useCache ? undefined : new Date().getTime()
9 }
10
11 return makeGetRequest({
12 url,
13 path,
14 query,
15 statusCodeExpected: HttpStatusCode.OK_200
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 getStats
23}