diff options
-rw-r--r-- | server/lib/plugins/plugin-manager.ts | 4 | ||||
-rw-r--r-- | server/middlewares/validators/plugins.ts | 5 | ||||
-rw-r--r-- | server/tests/api/check-params/index.ts | 1 | ||||
-rw-r--r-- | server/tests/api/check-params/plugins.ts | 474 |
4 files changed, 478 insertions, 6 deletions
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 85ee3decb..e76dbb53e 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts | |||
@@ -143,6 +143,8 @@ export class PluginManager implements ServerHook { | |||
143 | throw new Error(`Unknown plugin ${npmName} to unregister`) | 143 | throw new Error(`Unknown plugin ${npmName} to unregister`) |
144 | } | 144 | } |
145 | 145 | ||
146 | delete this.registeredPlugins[plugin.npmName] | ||
147 | |||
146 | if (plugin.type === PluginType.PLUGIN) { | 148 | if (plugin.type === PluginType.PLUGIN) { |
147 | await plugin.unregister() | 149 | await plugin.unregister() |
148 | 150 | ||
@@ -154,8 +156,6 @@ export class PluginManager implements ServerHook { | |||
154 | logger.info('Regenerating registered plugin CSS to global file.') | 156 | logger.info('Regenerating registered plugin CSS to global file.') |
155 | await this.regeneratePluginGlobalCSS() | 157 | await this.regeneratePluginGlobalCSS() |
156 | } | 158 | } |
157 | |||
158 | delete this.registeredPlugins[plugin.npmName] | ||
159 | } | 159 | } |
160 | 160 | ||
161 | // ###################### Installation ###################### | 161 | // ###################### Installation ###################### |
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index 8cb3153aa..68704bf56 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts | |||
@@ -88,7 +88,7 @@ const uninstallPluginValidator = [ | |||
88 | ] | 88 | ] |
89 | 89 | ||
90 | const existingPluginValidator = [ | 90 | const existingPluginValidator = [ |
91 | param('npmName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), | 91 | param('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid plugin name'), |
92 | 92 | ||
93 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 93 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
94 | logger.debug('Checking enabledPluginValidator parameters', { parameters: req.params }) | 94 | logger.debug('Checking enabledPluginValidator parameters', { parameters: req.params }) |
@@ -121,9 +121,6 @@ const updatePluginSettingsValidator = [ | |||
121 | ] | 121 | ] |
122 | 122 | ||
123 | const listAvailablePluginsValidator = [ | 123 | const listAvailablePluginsValidator = [ |
124 | query('sort') | ||
125 | .optional() | ||
126 | .exists().withMessage('Should have a valid sort'), | ||
127 | query('search') | 124 | query('search') |
128 | .optional() | 125 | .optional() |
129 | .exists().withMessage('Should have a valid search'), | 126 | .exists().withMessage('Should have a valid search'), |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 844fa31c5..924c0df76 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -6,6 +6,7 @@ import './debug' | |||
6 | import './follows' | 6 | import './follows' |
7 | import './jobs' | 7 | import './jobs' |
8 | import './logs' | 8 | import './logs' |
9 | import './plugins' | ||
9 | import './redundancy' | 10 | import './redundancy' |
10 | import './search' | 11 | import './search' |
11 | import './services' | 12 | import './services' |
diff --git a/server/tests/api/check-params/plugins.ts b/server/tests/api/check-params/plugins.ts new file mode 100644 index 000000000..dd03766c9 --- /dev/null +++ b/server/tests/api/check-params/plugins.ts | |||
@@ -0,0 +1,474 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { | ||
6 | checkBadCountPagination, | ||
7 | checkBadSortPagination, | ||
8 | checkBadStartPagination, | ||
9 | cleanupTests, | ||
10 | createUser, | ||
11 | flushAndRunServer, | ||
12 | immutableAssign, | ||
13 | installPlugin, | ||
14 | makeGetRequest, makePostBodyRequest, makePutBodyRequest, | ||
15 | ServerInfo, | ||
16 | setAccessTokensToServers, | ||
17 | userLogin | ||
18 | } from '../../../../shared/extra-utils' | ||
19 | import { PluginType } from '../../../../shared/models/plugins/plugin.type' | ||
20 | import { PeerTubePlugin } from '../../../../shared/models/plugins/peertube-plugin.model' | ||
21 | |||
22 | describe('Test server plugins API validators', function () { | ||
23 | let server: ServerInfo | ||
24 | let userAccessToken = null | ||
25 | |||
26 | const npmPlugin = 'peertube-plugin-hello-world' | ||
27 | const pluginName = 'hello-world' | ||
28 | let npmVersion: string | ||
29 | |||
30 | const themePlugin = 'peertube-theme-background-red' | ||
31 | const themeName = 'background-red' | ||
32 | let themeVersion: string | ||
33 | |||
34 | // --------------------------------------------------------------- | ||
35 | |||
36 | before(async function () { | ||
37 | this.timeout(30000) | ||
38 | |||
39 | server = await flushAndRunServer(1) | ||
40 | |||
41 | await setAccessTokensToServers([ server ]) | ||
42 | |||
43 | const user = { | ||
44 | username: 'user1', | ||
45 | password: 'password' | ||
46 | } | ||
47 | |||
48 | await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) | ||
49 | userAccessToken = await userLogin(server, user) | ||
50 | |||
51 | { | ||
52 | const res = await installPlugin({ url: server.url, accessToken: server.accessToken, npmName: npmPlugin }) | ||
53 | const plugin = res.body as PeerTubePlugin | ||
54 | npmVersion = plugin.version | ||
55 | } | ||
56 | |||
57 | { | ||
58 | const res = await installPlugin({ url: server.url, accessToken: server.accessToken, npmName: themePlugin }) | ||
59 | const plugin = res.body as PeerTubePlugin | ||
60 | themeVersion = plugin.version | ||
61 | } | ||
62 | }) | ||
63 | |||
64 | describe('With static plugin routes', function () { | ||
65 | it('Should fail with an unknown plugin name/plugin version', async function () { | ||
66 | const paths = [ | ||
67 | '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png', | ||
68 | '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js', | ||
69 | '/themes/' + themeName + '/0.0.1/static/images/chocobo.png', | ||
70 | '/themes/' + themeName + '/0.0.1/client-scripts/client/video-watch-client-plugin.js', | ||
71 | '/themes/' + themeName + '/0.0.1/css/assets/style1.css' | ||
72 | ] | ||
73 | |||
74 | for (const p of paths) { | ||
75 | await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 404 }) | ||
76 | } | ||
77 | }) | ||
78 | |||
79 | it('Should fail when requesting a plugin in the theme path', async function () { | ||
80 | await makeGetRequest({ | ||
81 | url: server.url, | ||
82 | path: '/themes/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png', | ||
83 | statusCodeExpected: 404 | ||
84 | }) | ||
85 | }) | ||
86 | |||
87 | it('Should fail with invalid versions', async function () { | ||
88 | const paths = [ | ||
89 | '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png', | ||
90 | '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js', | ||
91 | '/themes/' + themeName + '/1/static/images/chocobo.png', | ||
92 | '/themes/' + themeName + '/0.0.1000a/client-scripts/client/video-watch-client-plugin.js', | ||
93 | '/themes/' + themeName + '/0.a.1/css/assets/style1.css' | ||
94 | ] | ||
95 | |||
96 | for (const p of paths) { | ||
97 | await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 400 }) | ||
98 | } | ||
99 | }) | ||
100 | |||
101 | it('Should fail with invalid paths', async function () { | ||
102 | const paths = [ | ||
103 | '/plugins/' + pluginName + '/' + npmVersion + '/static/images/../chocobo.png', | ||
104 | '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/../client/common-client-plugin.js', | ||
105 | '/themes/' + themeName + '/' + themeVersion + '/static/../images/chocobo.png', | ||
106 | '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js/..', | ||
107 | '/themes/' + themeName + '/' + themeVersion + '/css/../assets/style1.css' | ||
108 | ] | ||
109 | |||
110 | for (const p of paths) { | ||
111 | await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 400 }) | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | it('Should fail with an unknown static file', async function () { | ||
116 | const paths = [ | ||
117 | '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png', | ||
118 | '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/fake.js', | ||
119 | '/themes/' + themeName + '/' + themeVersion + '/static/fake/chocobo.png', | ||
120 | '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/fake.js' | ||
121 | ] | ||
122 | |||
123 | for (const p of paths) { | ||
124 | await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 404 }) | ||
125 | } | ||
126 | }) | ||
127 | |||
128 | it('Should fail with an unknown CSS file', async function () { | ||
129 | await makeGetRequest({ | ||
130 | url: server.url, | ||
131 | path: '/themes/' + themeName + '/' + themeVersion + '/css/assets/fake.css', | ||
132 | statusCodeExpected: 404 | ||
133 | }) | ||
134 | }) | ||
135 | |||
136 | it('Should succeed with the correct parameters', async function () { | ||
137 | const paths = [ | ||
138 | '/plugins/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png', | ||
139 | '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/common-client-plugin.js', | ||
140 | '/themes/' + themeName + '/' + themeVersion + '/static/images/chocobo.png', | ||
141 | '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js', | ||
142 | '/themes/' + themeName + '/' + themeVersion + '/css/assets/style1.css' | ||
143 | ] | ||
144 | |||
145 | for (const p of paths) { | ||
146 | await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 }) | ||
147 | } | ||
148 | }) | ||
149 | }) | ||
150 | |||
151 | describe('When listing available plugins/themes', function () { | ||
152 | const path = '/api/v1/plugins/available' | ||
153 | const baseQuery = { | ||
154 | search: 'super search', | ||
155 | pluginType: PluginType.PLUGIN | ||
156 | } | ||
157 | |||
158 | it('Should fail with an invalid token', async function () { | ||
159 | await makeGetRequest({ | ||
160 | url: server.url, | ||
161 | path, | ||
162 | token: 'fake_token', | ||
163 | query: baseQuery, | ||
164 | statusCodeExpected: 401 | ||
165 | }) | ||
166 | }) | ||
167 | |||
168 | it('Should fail if the user is not an administrator', async function () { | ||
169 | await makeGetRequest({ | ||
170 | url: server.url, | ||
171 | path, | ||
172 | token: userAccessToken, | ||
173 | query: baseQuery, | ||
174 | statusCodeExpected: 403 | ||
175 | }) | ||
176 | }) | ||
177 | |||
178 | it('Should fail with a bad start pagination', async function () { | ||
179 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
180 | }) | ||
181 | |||
182 | it('Should fail with a bad count pagination', async function () { | ||
183 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
184 | }) | ||
185 | |||
186 | it('Should fail with an incorrect sort', async function () { | ||
187 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
188 | }) | ||
189 | |||
190 | it('Should fail with an invalid plugin type', async function () { | ||
191 | const query = immutableAssign(baseQuery, { pluginType: 5 }) | ||
192 | |||
193 | await makeGetRequest({ | ||
194 | url: server.url, | ||
195 | path, | ||
196 | token: server.accessToken, | ||
197 | query | ||
198 | }) | ||
199 | }) | ||
200 | |||
201 | it('Should success with the correct parameters', async function () { | ||
202 | await makeGetRequest({ | ||
203 | url: server.url, | ||
204 | path, | ||
205 | token: server.accessToken, | ||
206 | query: baseQuery, | ||
207 | statusCodeExpected: 200 | ||
208 | }) | ||
209 | }) | ||
210 | }) | ||
211 | |||
212 | describe('When listing local plugins/themes', function () { | ||
213 | const path = '/api/v1/plugins' | ||
214 | const baseQuery = { | ||
215 | pluginType: PluginType.THEME | ||
216 | } | ||
217 | |||
218 | it('Should fail with an invalid token', async function () { | ||
219 | await makeGetRequest({ | ||
220 | url: server.url, | ||
221 | path, | ||
222 | token: 'fake_token', | ||
223 | query: baseQuery, | ||
224 | statusCodeExpected: 401 | ||
225 | }) | ||
226 | }) | ||
227 | |||
228 | it('Should fail if the user is not an administrator', async function () { | ||
229 | await makeGetRequest({ | ||
230 | url: server.url, | ||
231 | path, | ||
232 | token: userAccessToken, | ||
233 | query: baseQuery, | ||
234 | statusCodeExpected: 403 | ||
235 | }) | ||
236 | }) | ||
237 | |||
238 | it('Should fail with a bad start pagination', async function () { | ||
239 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
240 | }) | ||
241 | |||
242 | it('Should fail with a bad count pagination', async function () { | ||
243 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
244 | }) | ||
245 | |||
246 | it('Should fail with an incorrect sort', async function () { | ||
247 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
248 | }) | ||
249 | |||
250 | it('Should fail with an invalid plugin type', async function () { | ||
251 | const query = immutableAssign(baseQuery, { pluginType: 5 }) | ||
252 | |||
253 | await makeGetRequest({ | ||
254 | url: server.url, | ||
255 | path, | ||
256 | token: server.accessToken, | ||
257 | query | ||
258 | }) | ||
259 | }) | ||
260 | |||
261 | it('Should success with the correct parameters', async function () { | ||
262 | await makeGetRequest({ | ||
263 | url: server.url, | ||
264 | path, | ||
265 | token: server.accessToken, | ||
266 | query: baseQuery, | ||
267 | statusCodeExpected: 200 | ||
268 | }) | ||
269 | }) | ||
270 | }) | ||
271 | |||
272 | describe('When getting a plugin or the registered settings', function () { | ||
273 | const path = '/api/v1/plugins/' | ||
274 | |||
275 | it('Should fail with an invalid token', async function () { | ||
276 | for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) { | ||
277 | await makeGetRequest({ | ||
278 | url: server.url, | ||
279 | path: path + suffix, | ||
280 | token: 'fake_token', | ||
281 | statusCodeExpected: 401 | ||
282 | }) | ||
283 | } | ||
284 | }) | ||
285 | |||
286 | it('Should fail if the user is not an administrator', async function () { | ||
287 | for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) { | ||
288 | await makeGetRequest({ | ||
289 | url: server.url, | ||
290 | path: path + suffix, | ||
291 | token: userAccessToken, | ||
292 | statusCodeExpected: 403 | ||
293 | }) | ||
294 | } | ||
295 | }) | ||
296 | |||
297 | it('Should fail with an invalid npm name', async function () { | ||
298 | for (const suffix of [ 'toto', 'toto/registered-settings' ]) { | ||
299 | await makeGetRequest({ | ||
300 | url: server.url, | ||
301 | path: path + suffix, | ||
302 | token: server.accessToken, | ||
303 | statusCodeExpected: 400 | ||
304 | }) | ||
305 | } | ||
306 | |||
307 | for (const suffix of [ 'peertube-plugin-TOTO', 'peertube-plugin-TOTO/registered-settings' ]) { | ||
308 | await makeGetRequest({ | ||
309 | url: server.url, | ||
310 | path: path + suffix, | ||
311 | token: server.accessToken, | ||
312 | statusCodeExpected: 400 | ||
313 | }) | ||
314 | } | ||
315 | }) | ||
316 | |||
317 | it('Should fail with an unknown plugin', async function () { | ||
318 | for (const suffix of [ 'peertube-plugin-toto', 'peertube-plugin-toto/registered-settings' ]) { | ||
319 | await makeGetRequest({ | ||
320 | url: server.url, | ||
321 | path: path + suffix, | ||
322 | token: server.accessToken, | ||
323 | statusCodeExpected: 404 | ||
324 | }) | ||
325 | } | ||
326 | }) | ||
327 | |||
328 | it('Should succeed with the correct parameters', async function () { | ||
329 | for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) { | ||
330 | await makeGetRequest({ | ||
331 | url: server.url, | ||
332 | path: path + suffix, | ||
333 | token: server.accessToken, | ||
334 | statusCodeExpected: 200 | ||
335 | }) | ||
336 | } | ||
337 | }) | ||
338 | }) | ||
339 | |||
340 | describe('When updating plugin settings', function () { | ||
341 | const path = '/api/v1/plugins/' | ||
342 | const settings = { setting1: 'value1' } | ||
343 | |||
344 | it('Should fail with an invalid token', async function () { | ||
345 | await makePutBodyRequest({ | ||
346 | url: server.url, | ||
347 | path: path + npmPlugin + '/settings', | ||
348 | fields: { settings }, | ||
349 | token: 'fake_token', | ||
350 | statusCodeExpected: 401 | ||
351 | }) | ||
352 | }) | ||
353 | |||
354 | it('Should fail if the user is not an administrator', async function () { | ||
355 | await makePutBodyRequest({ | ||
356 | url: server.url, | ||
357 | path: path + npmPlugin + '/settings', | ||
358 | fields: { settings }, | ||
359 | token: userAccessToken, | ||
360 | statusCodeExpected: 403 | ||
361 | }) | ||
362 | }) | ||
363 | |||
364 | it('Should fail with an invalid npm name', async function () { | ||
365 | await makePutBodyRequest({ | ||
366 | url: server.url, | ||
367 | path: path + 'toto/settings', | ||
368 | fields: { settings }, | ||
369 | token: server.accessToken, | ||
370 | statusCodeExpected: 400 | ||
371 | }) | ||
372 | |||
373 | await makePutBodyRequest({ | ||
374 | url: server.url, | ||
375 | path: path + 'peertube-plugin-TOTO/settings', | ||
376 | fields: { settings }, | ||
377 | token: server.accessToken, | ||
378 | statusCodeExpected: 400 | ||
379 | }) | ||
380 | }) | ||
381 | |||
382 | it('Should fail with an unknown plugin', async function () { | ||
383 | await makePutBodyRequest({ | ||
384 | url: server.url, | ||
385 | path: path + 'peertube-plugin-toto/settings', | ||
386 | fields: { settings }, | ||
387 | token: server.accessToken, | ||
388 | statusCodeExpected: 404 | ||
389 | }) | ||
390 | }) | ||
391 | |||
392 | it('Should succeed with the correct parameters', async function () { | ||
393 | await makePutBodyRequest({ | ||
394 | url: server.url, | ||
395 | path: path + npmPlugin + '/settings', | ||
396 | fields: { settings }, | ||
397 | token: server.accessToken, | ||
398 | statusCodeExpected: 204 | ||
399 | }) | ||
400 | }) | ||
401 | }) | ||
402 | |||
403 | describe('When installing/updating/uninstalling a plugin', function () { | ||
404 | const path = '/api/v1/plugins/' | ||
405 | |||
406 | it('Should fail with an invalid token', async function () { | ||
407 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
408 | await makePostBodyRequest({ | ||
409 | url: server.url, | ||
410 | path: path + suffix, | ||
411 | fields: { npmName: npmPlugin }, | ||
412 | token: 'fake_token', | ||
413 | statusCodeExpected: 401 | ||
414 | }) | ||
415 | } | ||
416 | }) | ||
417 | |||
418 | it('Should fail if the user is not an administrator', async function () { | ||
419 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
420 | await makePostBodyRequest({ | ||
421 | url: server.url, | ||
422 | path: path + suffix, | ||
423 | fields: { npmName: npmPlugin }, | ||
424 | token: userAccessToken, | ||
425 | statusCodeExpected: 403 | ||
426 | }) | ||
427 | } | ||
428 | }) | ||
429 | |||
430 | it('Should fail with an invalid npm name', async function () { | ||
431 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
432 | await makePostBodyRequest({ | ||
433 | url: server.url, | ||
434 | path: path + suffix, | ||
435 | fields: { npmName: 'toto' }, | ||
436 | token: server.accessToken, | ||
437 | statusCodeExpected: 400 | ||
438 | }) | ||
439 | } | ||
440 | |||
441 | for (const suffix of [ 'install', 'update', 'uninstall' ]) { | ||
442 | await makePostBodyRequest({ | ||
443 | url: server.url, | ||
444 | path: path + suffix, | ||
445 | fields: { npmName: 'peertube-plugin-TOTO' }, | ||
446 | token: server.accessToken, | ||
447 | statusCodeExpected: 400 | ||
448 | }) | ||
449 | } | ||
450 | }) | ||
451 | |||
452 | it('Should succeed with the correct parameters', async function () { | ||
453 | const it = [ | ||
454 | { suffix: 'install', status: 200 }, | ||
455 | { suffix: 'update', status: 200 }, | ||
456 | { suffix: 'uninstall', status: 204 } | ||
457 | ] | ||
458 | |||
459 | for (const obj of it) { | ||
460 | await makePostBodyRequest({ | ||
461 | url: server.url, | ||
462 | path: path + obj.suffix, | ||
463 | fields: { npmName: npmPlugin }, | ||
464 | token: server.accessToken, | ||
465 | statusCodeExpected: obj.status | ||
466 | }) | ||
467 | } | ||
468 | }) | ||
469 | }) | ||
470 | |||
471 | after(async function () { | ||
472 | await cleanupTests([ server ]) | ||
473 | }) | ||
474 | }) | ||