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.ts379
-rw-r--r--shared/extra-utils/server/servers-command.ts80
-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, 1417 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..5bdcbac52
--- /dev/null
+++ b/shared/extra-utils/server/server.ts
@@ -0,0 +1,379 @@
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 '../../core-utils/miscs/miscs'
6import { VideoChannel } 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?: {
79 id: number
80 uuid: string
81 shortUUID: string
82 name?: string
83 url?: string
84
85 account?: {
86 name: string
87 }
88
89 embedPath?: string
90 }
91
92 videos?: { id: number, uuid: string }[]
93 }
94
95 accessToken?: string
96 refreshToken?: string
97
98 bulk?: BulkCommand
99 cli?: CLICommand
100 customPage?: CustomPagesCommand
101 feed?: FeedCommand
102 logs?: LogsCommand
103 abuses?: AbusesCommand
104 overviews?: OverviewsCommand
105 search?: SearchCommand
106 contactForm?: ContactFormCommand
107 debug?: DebugCommand
108 follows?: FollowsCommand
109 jobs?: JobsCommand
110 plugins?: PluginsCommand
111 redundancy?: RedundancyCommand
112 stats?: StatsCommand
113 config?: ConfigCommand
114 socketIO?: SocketIOCommand
115 accounts?: AccountsCommand
116 blocklist?: BlocklistCommand
117 subscriptions?: SubscriptionsCommand
118 live?: LiveCommand
119 services?: ServicesCommand
120 blacklist?: BlacklistCommand
121 captions?: CaptionsCommand
122 changeOwnership?: ChangeOwnershipCommand
123 playlists?: PlaylistsCommand
124 history?: HistoryCommand
125 imports?: ImportsCommand
126 streamingPlaylists?: StreamingPlaylistsCommand
127 channels?: ChannelsCommand
128 comments?: CommentsCommand
129 sql?: SQLCommand
130 notifications?: NotificationsCommand
131 servers?: ServersCommand
132 login?: LoginCommand
133 users?: UsersCommand
134 videos?: VideosCommand
135
136 constructor (options: { serverNumber: number } | { url: string }) {
137 if ((options as any).url) {
138 this.setUrl((options as any).url)
139 } else {
140 this.setServerNumber((options as any).serverNumber)
141 }
142
143 this.store = {
144 client: {
145 id: null,
146 secret: null
147 },
148 user: {
149 username: null,
150 password: null
151 }
152 }
153
154 this.assignCommands()
155 }
156
157 setServerNumber (serverNumber: number) {
158 this.serverNumber = serverNumber
159
160 this.parallel = parallelTests()
161
162 this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
163 this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
164 this.port = 9000 + this.internalServerNumber
165
166 this.url = `http://localhost:${this.port}`
167 this.host = `localhost:${this.port}`
168 this.hostname = 'localhost'
169 }
170
171 setUrl (url: string) {
172 const parsed = new URL(url)
173
174 this.url = url
175 this.host = parsed.host
176 this.hostname = parsed.hostname
177 this.port = parseInt(parsed.port)
178 }
179
180 async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
181 await ServersCommand.flushTests(this.internalServerNumber)
182
183 return this.run(configOverride, options)
184 }
185
186 async run (configOverrideArg?: any, options: RunServerOptions = {}) {
187 // These actions are async so we need to be sure that they have both been done
188 const serverRunString = {
189 'HTTP server listening': false
190 }
191 const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
192 serverRunString[key] = false
193
194 const regexps = {
195 client_id: 'Client id: (.+)',
196 client_secret: 'Client secret: (.+)',
197 user_username: 'Username: (.+)',
198 user_password: 'User password: (.+)'
199 }
200
201 await this.assignCustomConfigFile()
202
203 const configOverride = this.buildConfigOverride()
204
205 if (configOverrideArg !== undefined) {
206 Object.assign(configOverride, configOverrideArg)
207 }
208
209 // Share the environment
210 const env = Object.create(process.env)
211 env['NODE_ENV'] = 'test'
212 env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
213 env['NODE_CONFIG'] = JSON.stringify(configOverride)
214
215 const forkOptions = {
216 silent: true,
217 env,
218 detached: true,
219 execArgv: options.nodeArgs || []
220 }
221
222 return new Promise<void>(res => {
223 const self = this
224
225 this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
226 this.app.stdout.on('data', function onStdout (data) {
227 let dontContinue = false
228
229 // Capture things if we want to
230 for (const key of Object.keys(regexps)) {
231 const regexp = regexps[key]
232 const matches = data.toString().match(regexp)
233 if (matches !== null) {
234 if (key === 'client_id') self.store.client.id = matches[1]
235 else if (key === 'client_secret') self.store.client.secret = matches[1]
236 else if (key === 'user_username') self.store.user.username = matches[1]
237 else if (key === 'user_password') self.store.user.password = matches[1]
238 }
239 }
240
241 // Check if all required sentences are here
242 for (const key of Object.keys(serverRunString)) {
243 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
244 if (serverRunString[key] === false) dontContinue = true
245 }
246
247 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
248 if (dontContinue === true) return
249
250 if (options.hideLogs === false) {
251 console.log(data.toString())
252 } else {
253 self.app.stdout.removeListener('data', onStdout)
254 }
255
256 process.on('exit', () => {
257 try {
258 process.kill(self.app.pid)
259 } catch { /* empty */ }
260 })
261
262 res()
263 })
264 })
265 }
266
267 async kill () {
268 if (!this.app) return
269
270 await this.sql.cleanup()
271
272 process.kill(-this.app.pid)
273
274 this.app = null
275 }
276
277 private randomServer () {
278 const low = 10
279 const high = 10000
280
281 return randomInt(low, high)
282 }
283
284 private randomRTMP () {
285 const low = 1900
286 const high = 2100
287
288 return randomInt(low, high)
289 }
290
291 private async assignCustomConfigFile () {
292 if (this.internalServerNumber === this.serverNumber) return
293
294 const basePath = join(root(), 'config')
295
296 const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
297 await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
298
299 this.customConfigFile = tmpConfigFile
300 }
301
302 private buildConfigOverride () {
303 if (!this.parallel) return {}
304
305 return {
306 listen: {
307 port: this.port
308 },
309 webserver: {
310 port: this.port
311 },
312 database: {
313 suffix: '_test' + this.internalServerNumber
314 },
315 storage: {
316 tmp: `test${this.internalServerNumber}/tmp/`,
317 avatars: `test${this.internalServerNumber}/avatars/`,
318 videos: `test${this.internalServerNumber}/videos/`,
319 streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
320 redundancy: `test${this.internalServerNumber}/redundancy/`,
321 logs: `test${this.internalServerNumber}/logs/`,
322 previews: `test${this.internalServerNumber}/previews/`,
323 thumbnails: `test${this.internalServerNumber}/thumbnails/`,
324 torrents: `test${this.internalServerNumber}/torrents/`,
325 captions: `test${this.internalServerNumber}/captions/`,
326 cache: `test${this.internalServerNumber}/cache/`,
327 plugins: `test${this.internalServerNumber}/plugins/`
328 },
329 admin: {
330 email: `admin${this.internalServerNumber}@example.com`
331 },
332 live: {
333 rtmp: {
334 port: this.rtmpPort
335 }
336 }
337 }
338 }
339
340 private assignCommands () {
341 this.bulk = new BulkCommand(this)
342 this.cli = new CLICommand(this)
343 this.customPage = new CustomPagesCommand(this)
344 this.feed = new FeedCommand(this)
345 this.logs = new LogsCommand(this)
346 this.abuses = new AbusesCommand(this)
347 this.overviews = new OverviewsCommand(this)
348 this.search = new SearchCommand(this)
349 this.contactForm = new ContactFormCommand(this)
350 this.debug = new DebugCommand(this)
351 this.follows = new FollowsCommand(this)
352 this.jobs = new JobsCommand(this)
353 this.plugins = new PluginsCommand(this)
354 this.redundancy = new RedundancyCommand(this)
355 this.stats = new StatsCommand(this)
356 this.config = new ConfigCommand(this)
357 this.socketIO = new SocketIOCommand(this)
358 this.accounts = new AccountsCommand(this)
359 this.blocklist = new BlocklistCommand(this)
360 this.subscriptions = new SubscriptionsCommand(this)
361 this.live = new LiveCommand(this)
362 this.services = new ServicesCommand(this)
363 this.blacklist = new BlacklistCommand(this)
364 this.captions = new CaptionsCommand(this)
365 this.changeOwnership = new ChangeOwnershipCommand(this)
366 this.playlists = new PlaylistsCommand(this)
367 this.history = new HistoryCommand(this)
368 this.imports = new ImportsCommand(this)
369 this.streamingPlaylists = new StreamingPlaylistsCommand(this)
370 this.channels = new ChannelsCommand(this)
371 this.comments = new CommentsCommand(this)
372 this.sql = new SQLCommand(this)
373 this.notifications = new NotificationsCommand(this)
374 this.servers = new ServersCommand(this)
375 this.login = new LoginCommand(this)
376 this.users = new UsersCommand(this)
377 this.videos = new VideosCommand(this)
378 }
379}
diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts
new file mode 100644
index 000000000..a78921f2a
--- /dev/null
+++ b/shared/extra-utils/server/servers-command.ts
@@ -0,0 +1,80 @@
1import { exec } from 'child_process'
2import { copy, ensureDir, readFile, remove } from 'fs-extra'
3import { 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 async getServerFileSize (subPath: string) {
76 const path = this.server.servers.buildDirectory(subPath)
77
78 return getFileSize(path)
79 }
80}
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}