Add hook filters tests
authorChocobozzz <me@florianbigard.com>
Fri, 19 Jul 2019 15:30:41 +0000 (17:30 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Wed, 24 Jul 2019 08:58:16 +0000 (10:58 +0200)
18 files changed:
server.ts
server/controllers/activitypub/client.ts
server/controllers/api/videos/comment.ts
server/controllers/api/videos/index.ts
server/lib/plugins/hooks.ts
server/lib/plugins/plugin-manager.ts
server/lib/video-blacklist.ts
server/middlewares/validators/videos/videos.ts
server/models/video/video.ts
server/tests/cli/plugins.ts
server/tests/fixtures/peertube-plugin-test-two/main.js [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test-two/package.json [new file with mode: 0644]
server/tests/fixtures/peertube-plugin-test/main.js
server/tests/plugins/action-hooks.ts
server/tests/plugins/filter-hooks.ts
shared/core-utils/plugins/hooks.ts
shared/extra-utils/server/plugins.ts
shared/extra-utils/server/servers.ts

index b75c78b0764b506038a5abce15c18a25154c2062..abfeeed2e02ad5a2f17b2f56a347bdebbdc88570 100644 (file)
--- a/server.ts
+++ b/server.ts
@@ -114,6 +114,7 @@ import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
 import { PeerTubeSocket } from './server/lib/peertube-socket'
 import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
 import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler'
+import { Hooks } from './server/lib/plugins/hooks'
 
 // ----------- Command line -----------
 
@@ -269,7 +270,7 @@ async function startApplication () {
     logger.info('Server listening on %s:%d', hostname, port)
     logger.info('Web server: %s', WEBSERVER.URL)
 
-    PluginManager.Instance.runHook('action:application.listening')
+    Hooks.runAction('action:application.listening')
   })
 
   process.on('exit', () => {
index d36d10de1b3d33bc63bec0bc25e12bf2f57be458..11504b35427b0883776e750f71f311d5d7a215e2 100644 (file)
@@ -208,7 +208,7 @@ function getAccountVideoRate (rateType: VideoRateType) {
 
 async function videoController (req: express.Request, res: express.Response) {
   // We need more attributes
-  const video = await VideoModel.loadForGetAPI(res.locals.video.id)
+  const video = await VideoModel.loadForGetAPI({ id: res.locals.video.id })
 
   if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url)
 
index a95392543e1bce89b8b7a608b0460cca57e24e11..feda71bdd769caf8529b76adec385f5e5e15be0a 100644 (file)
@@ -85,8 +85,9 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
       user: user
     }, 'filter:api.video-threads.list.params')
 
-    resultList = await Hooks.wrapPromise(
-      VideoCommentModel.listThreadsForApi(apiOptions),
+    resultList = await Hooks.wrapPromiseFun(
+      VideoCommentModel.listThreadsForApi,
+      apiOptions,
       'filter:api.video-threads.list.result'
     )
   } else {
@@ -112,8 +113,9 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
       user: user
     }, 'filter:api.video-thread-comments.list.params')
 
-    resultList = await Hooks.wrapPromise(
-      VideoCommentModel.listThreadCommentsForApi(apiOptions),
+    resultList = await Hooks.wrapPromiseFun(
+      VideoCommentModel.listThreadCommentsForApi,
+      apiOptions,
       'filter:api.video-thread-comments.list.result'
     )
   } else {
index a3b1dde29bda01cbaf924c5b47a1d100e162bc97..11e468df283476a0d25074bbba021532bcc268ec 100644 (file)
@@ -436,8 +436,9 @@ async function getVideo (req: express.Request, res: express.Response) {
   // We need more attributes
   const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
 
-  const video = await Hooks.wrapPromise(
-    VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId),
+  const video = await Hooks.wrapPromiseFun(
+    VideoModel.loadForGetAPI,
+    { id: res.locals.video.id, userId },
     'filter:api.video.get.result'
   )
 
@@ -502,8 +503,9 @@ async function listVideos (req: express.Request, res: express.Response) {
     user: res.locals.oauth ? res.locals.oauth.token.User : undefined
   }, 'filter:api.videos.list.params')
 
-  const resultList = await Hooks.wrapPromise(
-    VideoModel.listForApi(apiOptions),
+  const resultList = await Hooks.wrapPromiseFun(
+    VideoModel.listForApi,
+    apiOptions,
     'filter:api.videos.list.result'
   )
 
index 7bb907e6a79d1cd96dd2ea6e6a0513f193a9e883..b694d4118f9a991695dfcb6ec602bcbfd9e1941f 100644 (file)
@@ -3,16 +3,25 @@ import { PluginManager } from './plugin-manager'
 import { logger } from '../../helpers/logger'
 import * as Bluebird from 'bluebird'
 
+type PromiseFunction <U, T> = (params: U) => Promise<T> | Bluebird<T>
+type RawFunction <U, T> = (params: U) => T
+
 // Helpers to run hooks
 const Hooks = {
-  wrapObject: <T, U extends ServerFilterHookName>(obj: T, hookName: U) => {
-    return PluginManager.Instance.runHook(hookName, obj) as Promise<T>
+  wrapObject: <T, U extends ServerFilterHookName>(result: T, hookName: U) => {
+    return PluginManager.Instance.runHook(hookName, result) as Promise<T>
+  },
+
+  wrapPromiseFun: async <U, T, V extends ServerFilterHookName>(fun: PromiseFunction<U, T>, params: U, hookName: V) => {
+    const result = await fun(params)
+
+    return PluginManager.Instance.runHook(hookName, result, params)
   },
 
-  wrapPromise: async <T, U extends ServerFilterHookName>(fun: Promise<T> | Bluebird<T>, hookName: U) => {
-    const result = await fun
+  wrapFun: async <U, T, V extends ServerFilterHookName>(fun: RawFunction<U, T>, params: U, hookName: V) => {
+    const result = fun(params)
 
-    return PluginManager.Instance.runHook(hookName, result)
+    return PluginManager.Instance.runHook(hookName, result, params)
   },
 
   runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => {
index 9afda97ead83cbf225a5aea94bd02649f2ea667f..6485a47c515a8304d00fec02c2366bc59f8ed6be 100644 (file)
@@ -98,15 +98,15 @@ export class PluginManager implements ServerHook {
 
   // ###################### Hooks ######################
 
-  async runHook (hookName: ServerHookName, param?: any) {
-    let result = param
-
-    if (!this.hooks[hookName]) return result
+  async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
+    if (!this.hooks[hookName]) return Promise.resolve(result)
 
     const hookType = getHookType(hookName)
 
     for (const hook of this.hooks[hookName]) {
-      result = await internalRunHook(hook.handler, hookType, param, err => {
+      logger.debug('Running hook %s of plugin %s.', hookName, hook.npmName)
+
+      result = await internalRunHook(hook.handler, hookType, result, params, err => {
         logger.error('Cannot run hook %s of plugin %s.', hookName, hook.pluginName, { err })
       })
     }
index 32b1a28fad3a894974780d6904d8af6a759ee833..9bc996f5a84d0f15cf2a29a9a257ba39f551e161 100644 (file)
@@ -9,8 +9,9 @@ import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
 import { Hooks } from './plugins/hooks'
 
 async function autoBlacklistVideoIfNeeded (video: VideoModel, user?: UserModel, transaction?: Transaction) {
-  const doAutoBlacklist = await Hooks.wrapPromise(
-    autoBlacklistNeeded({ video, user }),
+  const doAutoBlacklist = await Hooks.wrapPromiseFun(
+    autoBlacklistNeeded,
+    { video, user },
     'filter:video.auto-blacklist.result'
   )
 
index cb2c071bac3deed9f4599299a052972af0de01ff..5593ede647213196d6ff9a152aab129ee765dda5 100644 (file)
@@ -444,8 +444,9 @@ async function isVideoAccepted (req: express.Request, res: express.Response, vid
     videoFile,
     user: res.locals.oauth.token.User
   }
-  const acceptedResult = await Hooks.wrapObject(
-    isLocalVideoAccepted(acceptParameters),
+  const acceptedResult = await Hooks.wrapFun(
+    isLocalVideoAccepted,
+    acceptParameters,
     'filter:api.video.upload.accept.result'
   )
 
index ec3d5ddb06cb22fb772fd73092ee0e85f50240e2..443aec9c29655508da22c99252e300b3bee5f3d4 100644 (file)
@@ -1472,7 +1472,12 @@ export class VideoModel extends Model<VideoModel> {
       .findOne(options)
   }
 
-  static loadForGetAPI (id: number | string, t?: Transaction, userId?: number) {
+  static loadForGetAPI (parameters: {
+    id: number | string,
+    t?: Transaction,
+    userId?: number
+  }) {
+    const { id, t, userId } = parameters
     const where = buildWhereIdOrUUID(id)
 
     const options = {
index d7bf8a69046d057a4c81109a2c1afa311e9509b3..a5257d671bce4e9b3f0f171639c2d6ce6628f70f 100644 (file)
@@ -6,13 +6,13 @@ import {
   execCLI,
   flushAndRunServer,
   getConfig,
-  getEnvCli, killallServers,
+  getEnvCli,
+  getPluginTestPath,
+  killallServers,
   reRunServer,
-  root,
   ServerInfo,
   setAccessTokensToServers
 } from '../../../shared/extra-utils'
-import { join } from 'path'
 import { ServerConfig } from '../../../shared/models/server'
 import { expect } from 'chai'
 
@@ -29,7 +29,7 @@ describe('Test plugin scripts', function () {
   it('Should install a plugin from stateless CLI', async function () {
     this.timeout(60000)
 
-    const packagePath = join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test')
+    const packagePath = getPluginTestPath()
 
     const env = getEnvCli(server)
     await execCLI(`${env} npm run plugin:install -- --plugin-path ${packagePath}`)
diff --git a/server/tests/fixtures/peertube-plugin-test-two/main.js b/server/tests/fixtures/peertube-plugin-test-two/main.js
new file mode 100644 (file)
index 0000000..71c11b2
--- /dev/null
@@ -0,0 +1,21 @@
+async function register ({ registerHook, registerSetting, settingsManager, storageManager, peertubeHelpers }) {
+  registerHook({
+    target: 'filter:api.videos.list.params',
+    handler: obj => addToCount(obj)
+  })
+}
+
+async function unregister () {
+  return
+}
+
+module.exports = {
+  register,
+  unregister
+}
+
+// ############################################################################
+
+function addToCount (obj) {
+  return Object.assign({}, obj, { count: obj.count + 1 })
+}
diff --git a/server/tests/fixtures/peertube-plugin-test-two/package.json b/server/tests/fixtures/peertube-plugin-test-two/package.json
new file mode 100644 (file)
index 0000000..52ebb5a
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "name": "peertube-plugin-test-two",
+  "version": "0.0.1",
+  "description": "Plugin test 2",
+  "engine": {
+    "peertube": ">=1.3.0"
+  },
+  "keywords": [
+    "peertube",
+    "plugin"
+  ],
+  "homepage": "https://github.com/Chocobozzz/PeerTube",
+  "author": "Chocobozzz",
+  "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
+  "library": "./main.js",
+  "staticDirs": {},
+  "css": [],
+  "clientScripts": []
+}
index fae0ef9488c91aba916765201db6ac7ed731bec5..c5317ab41aacadbad39f37f99c1dd94e27231b3d 100644 (file)
@@ -1,23 +1,52 @@
-async function register ({ registerHook, registerSetting, settingsManager, storageManager }) {
-  const defaultAdmin = 'PeerTube admin'
+async function register ({ registerHook, registerSetting, settingsManager, storageManager, peertubeHelpers }) {
+  const actionHooks = [
+    'action:application.listening',
+
+    'action:api.video.updated',
+    'action:api.video.deleted',
+    'action:api.video.uploaded',
+    'action:api.video.viewed',
+
+    'action:api.video-thread.created',
+    'action:api.video-comment-reply.created',
+    'action:api.video-comment.deleted'
+  ]
+
+  for (const h of actionHooks) {
+    registerHook({
+      target: h,
+      handler: () => peertubeHelpers.logger.debug('Run hook %s.', h)
+    })
+  }
 
   registerHook({
-    target: 'action:application.listening',
-    handler: () => displayHelloWorld(settingsManager, defaultAdmin)
+    target: 'filter:api.videos.list.params',
+    handler: obj => addToCount(obj)
   })
 
-  registerSetting({
-    name: 'admin-name',
-    label: 'Admin name',
-    type: 'input',
-    default: defaultAdmin
+  registerHook({
+    target: 'filter:api.videos.list.result',
+    handler: obj => ({ data: obj.data, total: obj.total + 1 })
   })
 
-  const value = await storageManager.getData('toto')
-  console.log(value)
-  console.log(value.coucou)
+  registerHook({
+    target: 'filter:api.video.get.result',
+    handler: video => {
+      video.name += ' <3'
 
-  await storageManager.storeData('toto', { coucou: 'hello' + new Date() })
+      return video
+    }
+  })
+
+  registerHook({
+    target: 'filter:api.video.upload.accept.result',
+    handler: ({ accepted }, { videoBody }) => {
+      if (accepted !== false) return { accepted: true }
+      if (videoBody.name.indexOf('bad word') !== -1) return { accepted: false, errorMessage: 'bad word '}
+
+      return { accepted: true }
+    }
+  })
 }
 
 async function unregister () {
@@ -31,9 +60,6 @@ module.exports = {
 
 // ############################################################################
 
-async function displayHelloWorld (settingsManager, defaultAdmin) {
-  let value = await settingsManager.getSetting('admin-name')
-  if (!value) value = defaultAdmin
-
-  console.log('hello world ' + value)
+function addToCount (obj) {
+  return Object.assign({}, obj, { count: obj.count + 1 })
 }
index 93dc57d09742a78436d3125b4f05101520eb7978..2a941148a42ce102409fa99dea72b20df914dcbc 100644 (file)
 
 import * as chai from 'chai'
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
-import { setAccessTokensToServers } from '../../../shared/extra-utils'
+import {
+  cleanupTests,
+  flushAndRunMultipleServers,
+  flushAndRunServer, killallServers, reRunServer,
+  ServerInfo,
+  waitUntilLog
+} from '../../../shared/extra-utils/server/servers'
+import {
+  addVideoCommentReply,
+  addVideoCommentThread, deleteVideoComment,
+  getPluginTestPath,
+  installPlugin, removeVideo,
+  setAccessTokensToServers,
+  updateVideo,
+  uploadVideo,
+  viewVideo
+} from '../../../shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test plugin action hooks', function () {
-  let server: ServerInfo
+  let servers: ServerInfo[]
+  let videoUUID: string
+  let threadId: number
+
+  function checkHook (hook: string) {
+    return waitUntilLog(servers[0], 'Run hook ' + hook)
+  }
 
   before(async function () {
     this.timeout(30000)
-    server = await flushAndRunServer(1)
 
-    await setAccessTokensToServers([ server ])
+    servers = await flushAndRunMultipleServers(2)
+    await setAccessTokensToServers(servers)
+
+    await installPlugin({
+      url: servers[0].url,
+      accessToken: servers[0].accessToken,
+      path: getPluginTestPath()
+    })
+
+    await killallServers([ servers[0] ])
+
+    await reRunServer(servers[0])
   })
 
-  it('Should execute ', async function () {
-    // empty
+  it('Should run action:application.listening', async function () {
+    await checkHook('action:application.listening')
+  })
+
+  it('Should run action:api.video.uploaded', async function () {
+    const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' })
+    videoUUID = res.body.video.uuid
+
+    await checkHook('action:api.video.uploaded')
+  })
+
+  it('Should run action:api.video.updated', async function () {
+    await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video updated' })
+
+    await checkHook('action:api.video.updated')
+  })
+
+  it('Should run action:api.video.viewed', async function () {
+    await viewVideo(servers[0].url, videoUUID)
+
+    await checkHook('action:api.video.viewed')
+  })
+
+  it('Should run action:api.video-thread.created', async function () {
+    const res = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'thread')
+    threadId = res.body.comment.id
+
+    await checkHook('action:api.video-thread.created')
+  })
+
+  it('Should run action:api.video-comment-reply.created', async function () {
+    await addVideoCommentReply(servers[0].url, servers[0].accessToken, videoUUID, threadId, 'reply')
+
+    await checkHook('action:api.video-comment-reply.created')
+  })
+
+  it('Should run action:api.video-comment.deleted', async function () {
+    await deleteVideoComment(servers[0].url, servers[0].accessToken, videoUUID, threadId)
+
+    await checkHook('action:api.video-comment.deleted')
+  })
+
+  it('Should run action:api.video.deleted', async function () {
+    await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
+
+    await checkHook('action:api.video.deleted')
   })
 
   after(async function () {
-    await cleanupTests([ server ])
+    await cleanupTests(servers)
   })
 })
index 500728712026cfd059db9e8adebfcb375f6c3719..4fc2c437b9153d7d8b24095e13cf1a4a584c32e9 100644 (file)
@@ -2,26 +2,79 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
-import { setAccessTokensToServers } from '../../../shared/extra-utils'
+import {
+  cleanupTests,
+  flushAndRunMultipleServers,
+  flushAndRunServer, killallServers, reRunServer,
+  ServerInfo,
+  waitUntilLog
+} from '../../../shared/extra-utils/server/servers'
+import {
+  addVideoCommentReply,
+  addVideoCommentThread, deleteVideoComment,
+  getPluginTestPath, getVideosList,
+  installPlugin, removeVideo,
+  setAccessTokensToServers,
+  updateVideo,
+  uploadVideo,
+  viewVideo,
+  getVideosListPagination, getVideo
+} from '../../../shared/extra-utils'
 
 const expect = chai.expect
 
 describe('Test plugin filter hooks', function () {
-  let server: ServerInfo
+  let servers: ServerInfo[]
+  let videoUUID: string
+  let threadId: number
 
   before(async function () {
     this.timeout(30000)
-    server = await flushAndRunServer(1)
 
-    await setAccessTokensToServers([ server ])
+    servers = await flushAndRunMultipleServers(2)
+    await setAccessTokensToServers(servers)
+
+    await installPlugin({
+      url: servers[0].url,
+      accessToken: servers[0].accessToken,
+      path: getPluginTestPath()
+    })
+
+    await installPlugin({
+      url: servers[0].url,
+      accessToken: servers[0].accessToken,
+      path: getPluginTestPath('-two')
+    })
+
+    for (let i = 0; i < 10; i++) {
+      await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'default video ' + i })
+    }
+
+    const res = await getVideosList(servers[0].url)
+    videoUUID = res.body.data[0].uuid
   })
 
-  it('Should execute ', async function () {
-    // empty
+  it('Should run filter:api.videos.list.params hook', async function () {
+    const res = await getVideosListPagination(servers[0].url, 0, 2)
+
+    // 2 plugins do +1 to the count parameter
+    expect(res.body.data).to.have.lengthOf(4)
+  })
+
+  it('Should run filter:api.videos.list.result', async function () {
+    const res = await getVideosListPagination(servers[0].url, 0, 0)
+
+    // Plugin do +1 to the total result
+    expect(res.body.total).to.equal(11)
+  })
+
+  it('Should run filter:api.video.get.result', async function () {
+    const res = await getVideo(servers[0].url, videoUUID)
+
+    expect(res.body.name).to.contain('<3')
   })
 
   after(async function () {
-    await cleanupTests([ server ])
+    await cleanupTests(servers)
   })
 })
index 047c04f7b63a426b11c767c314399ad9c63cf683..60f4130f5b05adac67a858afe76d9fe3d4993376 100644 (file)
@@ -8,25 +8,30 @@ function getHookType (hookName: string) {
   return HookType.STATIC
 }
 
-async function internalRunHook (handler: Function, hookType: HookType, param: any, onError: (err: Error) => void) {
-  let result = param
-
+async function internalRunHook <T>(handler: Function, hookType: HookType, result: T, params: any, onError: (err: Error) => void) {
   try {
-    const p = handler(result)
+    if (hookType === HookType.FILTER) {
+      const p = handler(result, params)
+
+      if (isPromise(p)) result = await p
+      else result = p
+
+      return result
+    }
 
-    switch (hookType) {
-      case HookType.FILTER:
-        if (isPromise(p)) result = await p
-        else result = p
-        break
+    // Action/static hooks do not have result value
+    const p = handler(params)
+
+    if (hookType === HookType.STATIC) {
+      if (isPromise(p)) await p
+
+      return undefined
+    }
 
-      case HookType.STATIC:
-        if (isPromise(p)) await p
-        break
+    if (hookType === HookType.ACTION) {
+      if (isCatchable(p)) p.catch(err => onError(err))
 
-      case HookType.ACTION:
-        if (isCatchable(p)) p.catch(err => onError(err))
-        break
+      return undefined
     }
   } catch (err) {
     onError(err)
index 7a5c5344b7b70cbf72d07f4649146209e816c1e6..2302208a8088e90c86cff0d9d7dae3d9244db9ef 100644 (file)
@@ -201,6 +201,10 @@ function getPluginPackageJSON (server: ServerInfo, npmName: string) {
   return readJSON(path)
 }
 
+function getPluginTestPath (suffix = '') {
+  return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
+}
+
 export {
   listPlugins,
   listAvailablePlugins,
@@ -213,5 +217,6 @@ export {
   getPluginRegisteredSettings,
   getPackageJSONPath,
   updatePluginPackageJSON,
-  getPluginPackageJSON
+  getPluginPackageJSON,
+  getPluginTestPath
 }
index 9167ebe5b43f5cc1aba33c59528c52b3e0267b1e..40cf7f0f38c8e3ade6606ad264549a12c1957532 100644 (file)
@@ -171,7 +171,8 @@ async function runServer (server: ServerInfo, configOverrideArg?: any, args = []
         thumbnails: `test${server.internalServerNumber}/thumbnails/`,
         torrents: `test${server.internalServerNumber}/torrents/`,
         captions: `test${server.internalServerNumber}/captions/`,
-        cache: `test${server.internalServerNumber}/cache/`
+        cache: `test${server.internalServerNumber}/cache/`,
+        plugins: `test${server.internalServerNumber}/plugins/`
       },
       admin: {
         email: `admin${server.internalServerNumber}@example.com`