]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to list jobs
authorChocobozzz <florian.bigard@gmail.com>
Thu, 30 Nov 2017 09:51:13 +0000 (10:51 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Thu, 30 Nov 2017 09:55:06 +0000 (10:55 +0100)
29 files changed:
client/src/app/+admin/admin-routing.module.ts
client/src/app/+admin/admin.module.ts
client/src/app/+admin/jobs/index.ts [new file with mode: 0644]
client/src/app/+admin/jobs/job.component.ts [new file with mode: 0644]
client/src/app/+admin/jobs/job.routes.ts [new file with mode: 0644]
client/src/app/+admin/jobs/jobs-list/index.ts [new file with mode: 0644]
client/src/app/+admin/jobs/jobs-list/jobs-list.component.html [new file with mode: 0644]
client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts [new file with mode: 0644]
client/src/app/+admin/jobs/shared/index.ts [new file with mode: 0644]
client/src/app/+admin/jobs/shared/job.service.ts [new file with mode: 0644]
client/src/app/core/menu/menu-admin.component.html
client/src/app/core/menu/menu-admin.component.ts
server/controllers/api/index.ts
server/controllers/api/jobs.ts [new file with mode: 0644]
server/initializers/constants.ts
server/middlewares/sort.ts
server/middlewares/validators/sort.ts
server/models/account/user-interface.ts
server/models/job/job-interface.ts
server/models/job/job.ts
server/tests/api/check-params/index.ts
server/tests/api/check-params/jobs.ts [new file with mode: 0644]
server/tests/api/index-slow.ts
server/tests/api/jobs.ts [new file with mode: 0644]
server/tests/utils/follows.ts
server/tests/utils/jobs.ts [new file with mode: 0644]
shared/models/job.model.ts
shared/models/users/user-right.enum.ts
shared/models/users/user-role.ts

index cd8b9bdef9a084690cf066b78559f765756ebf3e..7ef5c61050f4261ea722c100a84b9ad1ed8588b2 100644 (file)
@@ -8,6 +8,7 @@ import { FollowsRoutes } from './follows'
 import { UsersRoutes } from './users'
 import { VideoAbusesRoutes } from './video-abuses'
 import { VideoBlacklistRoutes } from './video-blacklist'
+import { JobsRoutes } from './jobs/job.routes'
 
 const adminRoutes: Routes = [
   {
@@ -24,7 +25,8 @@ const adminRoutes: Routes = [
       ...FollowsRoutes,
       ...UsersRoutes,
       ...VideoAbusesRoutes,
-      ...VideoBlacklistRoutes
+      ...VideoBlacklistRoutes,
+      ...JobsRoutes
     ]
   }
 ]
index 3c6b7a79384335aeafbd479f79e1975167ed1cb8..c0b006e73bca6525da8ced36f8834ebf135367bd 100644 (file)
@@ -5,6 +5,9 @@ import { AdminRoutingModule } from './admin-routing.module'
 import { AdminComponent } from './admin.component'
 import { FollowersListComponent, FollowingAddComponent, FollowsComponent, FollowService } from './follows'
 import { FollowingListComponent } from './follows/following-list/following-list.component'
+import { JobsComponent } from './jobs/job.component'
+import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
+import { JobService } from './jobs/shared/job.service'
 import { UserAddComponent, UserListComponent, UsersComponent, UserService, UserUpdateComponent } from './users'
 import { VideoAbuseListComponent, VideoAbusesComponent } from './video-abuses'
 import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist'
@@ -33,7 +36,10 @@ import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-bl
     VideoBlacklistListComponent,
 
     VideoAbusesComponent,
-    VideoAbuseListComponent
+    VideoAbuseListComponent,
+
+    JobsComponent,
+    JobsListComponent
   ],
 
   exports: [
@@ -42,7 +48,8 @@ import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-bl
 
   providers: [
     FollowService,
-    UserService
+    UserService,
+    JobService
   ]
 })
 export class AdminModule { }
diff --git a/client/src/app/+admin/jobs/index.ts b/client/src/app/+admin/jobs/index.ts
new file mode 100644 (file)
index 0000000..7b52719
--- /dev/null
@@ -0,0 +1 @@
+export * from './'
diff --git a/client/src/app/+admin/jobs/job.component.ts b/client/src/app/+admin/jobs/job.component.ts
new file mode 100644 (file)
index 0000000..bc80c9a
--- /dev/null
@@ -0,0 +1,6 @@
+import { Component } from '@angular/core'
+
+@Component({
+  template: '<router-outlet></router-outlet>'
+})
+export class JobsComponent {}
diff --git a/client/src/app/+admin/jobs/job.routes.ts b/client/src/app/+admin/jobs/job.routes.ts
new file mode 100644 (file)
index 0000000..a7bf2b2
--- /dev/null
@@ -0,0 +1,35 @@
+import { Routes } from '@angular/router'
+
+import { UserRightGuard } from '../../core'
+import { FollowingAddComponent } from './following-add'
+import { UserRight } from '../../../../../shared'
+import { FollowingListComponent } from './following-list/following-list.component'
+import { JobsComponent } from './job.component'
+import { JobsListComponent } from './jobs-list/jobs-list.component'
+
+export const JobsRoutes: Routes = [
+  {
+    path: 'jobs',
+    component: JobsComponent,
+    canActivate: [ UserRightGuard ],
+    data: {
+      userRight: UserRight.MANAGE_JOBS
+    },
+    children: [
+      {
+        path: '',
+        redirectTo: 'list',
+        pathMatch: 'full'
+      },
+      {
+        path: 'list',
+        component: JobsListComponent,
+        data: {
+          meta: {
+            title: 'Jobs list'
+          }
+        }
+      }
+    ]
+  }
+]
diff --git a/client/src/app/+admin/jobs/jobs-list/index.ts b/client/src/app/+admin/jobs/jobs-list/index.ts
new file mode 100644 (file)
index 0000000..cf590a6
--- /dev/null
@@ -0,0 +1 @@
+export * from './jobs-list.component'
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.html
new file mode 100644 (file)
index 0000000..a902671
--- /dev/null
@@ -0,0 +1,18 @@
+<div class="row">
+  <div class="content-padding">
+    <h3>Jobs list</h3>
+
+    <p-dataTable
+        [value]="jobs" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
+        sortField="createdAt" (onLazyLoad)="loadLazy($event)"
+    >
+      <p-column field="id" header="ID"></p-column>
+      <p-column field="category" header="Category"></p-column>
+      <p-column field="handlerName" header="Handler name"></p-column>
+      <p-column field="handlerInputData" header="Input data"></p-column>
+      <p-column field="state" header="State"></p-column>
+      <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
+      <p-column field="updatedAt" header="Updated date"></p-column>
+    </p-dataTable>
+  </div>
+</div>
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts
new file mode 100644 (file)
index 0000000..88fe259
--- /dev/null
@@ -0,0 +1,50 @@
+import { Component } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { SortMeta } from 'primeng/primeng'
+import { Job } from '../../../../../../shared/index'
+import { RestPagination, RestTable } from '../../../shared'
+import { JobService } from '../shared'
+import { RestExtractor } from '../../../shared/rest/rest-extractor.service'
+
+@Component({
+  selector: 'my-jobs-list',
+  templateUrl: './jobs-list.component.html',
+  styleUrls: [ ]
+})
+export class JobsListComponent extends RestTable {
+  jobs: Job[] = []
+  totalRecords = 0
+  rowsPerPage = 10
+  sort: SortMeta = { field: 'createdAt', order: 1 }
+  pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
+
+  constructor (
+    private notificationsService: NotificationsService,
+    private restExtractor: RestExtractor,
+    private jobsService: JobService
+  ) {
+    super()
+  }
+
+  protected loadData () {
+    this.jobsService
+      .getJobs(this.pagination, this.sort)
+      .map(res => this.restExtractor.applyToResultListData(res, this.formatJob.bind(this)))
+      .subscribe(
+        resultList => {
+          this.jobs = resultList.data
+          this.totalRecords = resultList.total
+        },
+
+        err => this.notificationsService.error('Error', err.message)
+      )
+  }
+
+  private formatJob (job: Job) {
+    const handlerInputData = JSON.stringify(job.handlerInputData)
+
+    return Object.assign(job, {
+      handlerInputData
+    })
+  }
+}
diff --git a/client/src/app/+admin/jobs/shared/index.ts b/client/src/app/+admin/jobs/shared/index.ts
new file mode 100644 (file)
index 0000000..609439e
--- /dev/null
@@ -0,0 +1 @@
+export * from './job.service'
diff --git a/client/src/app/+admin/jobs/shared/job.service.ts b/client/src/app/+admin/jobs/shared/job.service.ts
new file mode 100644 (file)
index 0000000..49f1ab6
--- /dev/null
@@ -0,0 +1,30 @@
+import { HttpClient, HttpParams } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { SortMeta } from 'primeng/primeng'
+import 'rxjs/add/operator/catch'
+import 'rxjs/add/operator/map'
+import { Observable } from 'rxjs/Observable'
+import { ResultList } from '../../../../../../shared'
+import { Job } from '../../../../../../shared/models/job.model'
+
+import { RestExtractor, RestPagination, RestService } from '../../../shared'
+
+@Injectable()
+export class JobService {
+  private static BASE_JOB_URL = API_URL + '/api/v1/jobs'
+
+  constructor (
+    private authHttp: HttpClient,
+    private restService: RestService,
+    private restExtractor: RestExtractor
+  ) {}
+
+  getJobs (pagination: RestPagination, sort: SortMeta): Observable<ResultList<Job>> {
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp.get<ResultList<Job>>(JobService.BASE_JOB_URL, { params })
+      .map(res => this.restExtractor.convertResultListDateToHuman(res))
+      .catch(err => this.restExtractor.handleError(err))
+  }
+}
index eb2d0d69c039d5574971ecbffe41c0541d7c40e5..9857b2e3ed783ffc9a8a919067eb24449adc495d 100644 (file)
       <span class="hidden-xs glyphicon glyphicon-eye-close"></span>
       Video blacklist
     </a>
+
+    <a *ngIf="hasJobsRight()" routerLink="/admin/jobs" routerLinkActive="active">
+      <span class="hidden-xs glyphicon glyphicon-tasks"></span>
+      Jobs
+    </a>
   </div>
 
   <div class="panel-block">
index 466da1aee148cf26309334dccc0d181d6204cbf0..ea8d5f57cb6c1a10fe981bd4d6d3afe75a3da39f 100644 (file)
@@ -26,4 +26,8 @@ export class MenuAdminComponent {
   hasVideoBlacklistRight () {
     return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
   }
+
+  hasJobsRight () {
+    return this.auth.getUser().hasRight(UserRight.MANAGE_JOBS)
+  }
 }
index b00fb74677bf565e590b25f32f23774ee2a8f6ed..737ea46027b9257fa7779d103d1d2bf0c7345808 100644 (file)
@@ -7,6 +7,7 @@ import { configRouter } from './config'
 import { serverRouter } from './server'
 import { usersRouter } from './users'
 import { videosRouter } from './videos'
+import { jobsRouter } from './jobs'
 
 const apiRouter = express.Router()
 
@@ -15,6 +16,7 @@ apiRouter.use('/oauth-clients', oauthClientsRouter)
 apiRouter.use('/config', configRouter)
 apiRouter.use('/users', usersRouter)
 apiRouter.use('/videos', videosRouter)
+apiRouter.use('/jobs', jobsRouter)
 apiRouter.use('/ping', pong)
 apiRouter.use('/*', badRequest)
 
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
new file mode 100644 (file)
index 0000000..f6fbff3
--- /dev/null
@@ -0,0 +1,34 @@
+import * as express from 'express'
+import { asyncMiddleware, jobsSortValidator, setJobsSort, setPagination } from '../../middlewares'
+import { paginationValidator } from '../../middlewares/validators/pagination'
+import { database as db } from '../../initializers'
+import { getFormattedObjects } from '../../helpers/utils'
+import { authenticate } from '../../middlewares/oauth'
+import { ensureUserHasRight } from '../../middlewares/user-right'
+import { UserRight } from '../../../shared/models/users/user-right.enum'
+
+const jobsRouter = express.Router()
+
+jobsRouter.get('/',
+  authenticate,
+  ensureUserHasRight(UserRight.MANAGE_JOBS),
+  paginationValidator,
+  jobsSortValidator,
+  setJobsSort,
+  setPagination,
+  asyncMiddleware(listJobs)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  jobsRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function listJobs (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const resultList = await db.Job.listForApi(req.query.start, req.query.count, req.query.sort)
+
+  return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
index 6d8aa73328c9fed4ee301c6798ca657bd493e31a..8f278fb0bf6dd5be2cbada9bb503914129c1c4fd 100644 (file)
@@ -32,6 +32,7 @@ const SEARCHABLE_COLUMNS = {
 // Sortable columns per schema
 const SORTABLE_COLUMNS = {
   USERS: [ 'id', 'username', 'createdAt' ],
+  JOBS: [ 'id', 'createdAt' ],
   VIDEO_ABUSES: [ 'id', 'createdAt' ],
   VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
   VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
index 279b29e654c40d4f069f14de7d3c3024149ab6c7..7b60920dea0c707159b16087734a100c0b698676 100644 (file)
@@ -10,6 +10,12 @@ function setUsersSort (req: express.Request, res: express.Response, next: expres
   return next()
 }
 
+function setJobsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
+  if (!req.query.sort) req.query.sort = '-createdAt'
+
+  return next()
+}
+
 function setVideoAbusesSort (req: express.Request, res: express.Response, next: express.NextFunction) {
   if (!req.query.sort) req.query.sort = '-createdAt'
 
@@ -70,5 +76,6 @@ export {
   setVideosSort,
   setBlacklistSort,
   setFollowersSort,
-  setFollowingSort
+  setFollowingSort,
+  setJobsSort
 }
index 636f68885ab106d6b47550fdf734ce307e5d1f87..d5822ac81562227c713c25c82b7826864958c58a 100644 (file)
@@ -7,6 +7,7 @@ import { areValidationErrors } from './utils'
 
 // Initialize constants here for better performances
 const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
+const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
 const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
 const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
 const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
@@ -15,6 +16,7 @@ const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOW
 const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
 
 const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
+const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
 const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
 const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
 const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
@@ -31,7 +33,8 @@ export {
   videosSortValidator,
   blacklistSortValidator,
   followersSortValidator,
-  followingSortValidator
+  followingSortValidator,
+  jobsSortValidator
 }
 
 // ---------------------------------------------------------------------------
index 1a04fb7505ce980deda11856bf35a3f2b34990c4..0f0b7206356339778c785acbc97a9f3561ae9751 100644 (file)
@@ -1,12 +1,10 @@
-import * as Sequelize from 'sequelize'
 import * as Bluebird from 'bluebird'
-
-// Don't use barrel, import just what we need
-import { AccountInstance } from './account-interface'
-import { User as FormattedUser } from '../../../shared/models/users/user.model'
+import * as Sequelize from 'sequelize'
 import { ResultList } from '../../../shared/models/result-list.model'
 import { UserRight } from '../../../shared/models/users/user-right.enum'
 import { UserRole } from '../../../shared/models/users/user-role'
+import { User as FormattedUser } from '../../../shared/models/users/user.model'
+import { AccountInstance } from './account-interface'
 
 export namespace UserMethods {
   export type HasRight = (this: UserInstance, right: UserRight) => boolean
index 411a05029e56fa2fe7e95da66f362177180a7594..3cfc0fbeded232ba92c51376921587399280f22f 100644 (file)
@@ -1,18 +1,23 @@
+import * as Bluebird from 'bluebird'
 import * as Sequelize from 'sequelize'
-import * as Promise from 'bluebird'
-
-import { JobCategory, JobState } from '../../../shared/models/job.model'
+import { Job as FormattedJob, JobCategory, JobState } from '../../../shared/models/job.model'
+import { ResultList } from '../../../shared/models/result-list.model'
 
 export namespace JobMethods {
-  export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise<JobInstance[]>
+  export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Bluebird<JobInstance[]>
+  export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<JobInstance> >
+
+  export type ToFormattedJSON = (this: JobInstance) => FormattedJob
 }
 
 export interface JobClass {
   listWithLimitByCategory: JobMethods.ListWithLimitByCategory
+  listForApi: JobMethods.ListForApi,
 }
 
 export interface JobAttributes {
   state: JobState
+  category: JobCategory
   handlerName: string
   handlerInputData: any
 }
@@ -21,6 +26,8 @@ export interface JobInstance extends JobClass, JobAttributes, Sequelize.Instance
   id: number
   createdAt: Date
   updatedAt: Date
+
+  toFormattedJSON: JobMethods.ToFormattedJSON
 }
 
 export interface JobModel extends JobClass, Sequelize.Model<JobInstance, JobAttributes> {}
index c2d088090accc4068bce6d45694a69b10190a8ff..f428e26dbe40cd15fd1bd506a5945373cf42a5a9 100644 (file)
@@ -1,19 +1,14 @@
 import { values } from 'lodash'
 import * as Sequelize from 'sequelize'
-
-import { JOB_STATES, JOB_CATEGORIES } from '../../initializers'
-
-import { addMethodsToModel } from '../utils'
-import {
-  JobInstance,
-  JobAttributes,
-
-  JobMethods
-} from './job-interface'
 import { JobCategory, JobState } from '../../../shared/models/job.model'
+import { JOB_CATEGORIES, JOB_STATES } from '../../initializers'
+import { addMethodsToModel, getSort } from '../utils'
+import { JobAttributes, JobInstance, JobMethods } from './job-interface'
 
 let Job: Sequelize.Model<JobInstance, JobAttributes>
 let listWithLimitByCategory: JobMethods.ListWithLimitByCategory
+let listForApi: JobMethods.ListForApi
+let toFormattedJSON: JobMethods.ToFormattedJSON
 
 export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   Job = sequelize.define<JobInstance, JobAttributes>('Job',
@@ -44,12 +39,30 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se
     }
   )
 
-  const classMethods = [ listWithLimitByCategory ]
-  addMethodsToModel(Job, classMethods)
+  const classMethods = [
+    listWithLimitByCategory,
+    listForApi
+  ]
+  const instanceMethods = [
+    toFormattedJSON
+  ]
+  addMethodsToModel(Job, classMethods, instanceMethods)
 
   return Job
 }
 
+toFormattedJSON = function (this: JobInstance) {
+  return {
+    id: this.id,
+    state: this.state,
+    category: this.category,
+    handlerName: this.handlerName,
+    handlerInputData: this.handlerInputData,
+    createdAt: this.createdAt,
+    updatedAt: this.updatedAt
+  }
+}
+
 // ---------------------------------------------------------------------------
 
 listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) {
@@ -66,3 +79,18 @@ listWithLimitByCategory = function (limit: number, state: JobState, jobCategory:
 
   return Job.findAll(query)
 }
+
+listForApi = function (start: number, count: number, sort: string) {
+  const query = {
+    offset: start,
+    limit: count,
+    order: [ getSort(sort) ]
+  }
+
+  return Job.findAndCountAll(query).then(({ rows, count }) => {
+    return {
+      data: rows,
+      total: count
+    }
+  })
+}
index 287480808fdfc2e507a5b39d2aebb36fd8212828..b22bf054abc380b6746c91efc13ad9a94af8cdf7 100644 (file)
@@ -1,5 +1,6 @@
 // Order of the tests we want to execute
 import './follows'
+import './jobs'
 import './users'
 import './services'
 import './videos'
diff --git a/server/tests/api/check-params/jobs.ts b/server/tests/api/check-params/jobs.ts
new file mode 100644 (file)
index 0000000..7a0dd6e
--- /dev/null
@@ -0,0 +1,84 @@
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+import * as request from 'supertest'
+
+import { createUser, flushTests, getUserAccessToken, killallServers, runServer, ServerInfo, setAccessTokensToServers } from '../../utils'
+
+describe('Test jobs API validators', function () {
+  const path = '/api/v1/jobs/'
+  let server: ServerInfo
+  let userAccessToken = ''
+
+  // ---------------------------------------------------------------
+
+  before(async function () {
+    this.timeout(120000)
+
+    await flushTests()
+
+    server = await runServer(1)
+
+    await setAccessTokensToServers([ server ])
+
+    const user = {
+      username: 'user1',
+      password: 'my super password'
+    }
+    await createUser(server.url, server.accessToken, user.username, user.password)
+    userAccessToken = await getUserAccessToken(server, user)
+  })
+
+  describe('When listing jobs', function () {
+    it('Should fail with a bad start pagination', async function () {
+      await request(server.url)
+              .get(path)
+              .query({ start: 'hello' })
+              .set('Accept', 'application/json')
+              .set('Authorization', 'Bearer ' + server.accessToken)
+              .expect(400)
+    })
+
+    it('Should fail with a bad count pagination', async function () {
+      await request(server.url)
+              .get(path)
+              .query({ count: 'hello' })
+              .set('Accept', 'application/json')
+              .set('Authorization', 'Bearer ' + server.accessToken)
+              .expect(400)
+    })
+
+    it('Should fail with an incorrect sort', async function () {
+      await request(server.url)
+              .get(path)
+              .query({ sort: 'hello' })
+              .set('Accept', 'application/json')
+              .set('Authorization', 'Bearer ' + server.accessToken)
+              .expect(400)
+    })
+
+    it('Should fail with a non authenticated user', async function () {
+      await request(server.url)
+        .get(path)
+        .set('Accept', 'application/json')
+        .expect(401)
+    })
+
+    it('Should fail with a non admin user', async function () {
+      await request(server.url)
+        .get(path)
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + userAccessToken)
+        .expect(403)
+    })
+  })
+
+  after(async function () {
+    killallServers([ server ])
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
index 2448147d84733ae46d51fac980f26146f0273403..4cd5b09a2bc52d6eadd31cff62818f1ecc9f553e 100644 (file)
@@ -3,3 +3,4 @@
 import './video-transcoder'
 import './multiple-servers'
 import './follows'
+import './jobs'
diff --git a/server/tests/api/jobs.ts b/server/tests/api/jobs.ts
new file mode 100644 (file)
index 0000000..4d9b613
--- /dev/null
@@ -0,0 +1,64 @@
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { flushTests, killallServers, ServerInfo, setAccessTokensToServers, wait } from '../utils'
+import { doubleFollow } from '../utils/follows'
+import { getJobsList, getJobsListPaginationAndSort } from '../utils/jobs'
+import { flushAndRunMultipleServers } from '../utils/servers'
+import { uploadVideo } from '../utils/videos'
+import { dateIsValid } from '../utils/miscs'
+
+const expect = chai.expect
+
+describe('Test jobs', function () {
+  let servers: ServerInfo[]
+
+  before(async function () {
+    this.timeout(30000)
+
+    servers = await flushAndRunMultipleServers(2)
+
+    await setAccessTokensToServers(servers)
+
+    // Server 1 and server 2 follow each other
+    await doubleFollow(servers[0], servers[1])
+  })
+
+  it('Should create some jobs', async function () {
+    this.timeout(30000)
+
+    await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' })
+    await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' })
+
+    await wait(15000)
+  })
+
+  it('Should list jobs', async function () {
+    const res = await getJobsList(servers[1].url, servers[1].accessToken)
+    expect(res.body.total).to.be.above(2)
+    expect(res.body.data).to.have.length.above(2)
+  })
+
+  it('Should list jobs with sort and pagination', async function () {
+    const res = await getJobsListPaginationAndSort(servers[1].url, servers[1].accessToken, 4, 1, 'createdAt')
+    expect(res.body.total).to.be.above(2)
+    expect(res.body.data).to.have.lengthOf(1)
+
+    const job = res.body.data[0]
+    expect(job.state).to.equal('success')
+    expect(job.category).to.equal('transcoding')
+    expect(job.handlerName).to.have.length.above(3)
+    expect(dateIsValid(job.createdAt)).to.be.true
+    expect(dateIsValid(job.updatedAt)).to.be.true
+  })
+
+  after(async function () {
+    killallServers(servers)
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
index b88776011e590f4235782b60a806f101062305b1..033c6a719828f5f026451d1c895ad9eb6456b5f1 100644 (file)
@@ -61,7 +61,7 @@ async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
   ])
 
   // Wait request propagation
-  await wait(20000)
+  await wait(10000)
 
   return true
 }
diff --git a/server/tests/utils/jobs.ts b/server/tests/utils/jobs.ts
new file mode 100644 (file)
index 0000000..0a8c515
--- /dev/null
@@ -0,0 +1,33 @@
+import * as request from 'supertest'
+
+function getJobsList (url: string, accessToken: string) {
+  const path = '/api/v1/jobs'
+
+  return request(url)
+          .get(path)
+          .set('Accept', 'application/json')
+          .set('Authorization', 'Bearer ' + accessToken)
+          .expect(200)
+          .expect('Content-Type', /json/)
+}
+
+function getJobsListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string) {
+  const path = '/api/v1/jobs'
+
+  return request(url)
+          .get(path)
+          .query({ start })
+          .query({ count })
+          .query({ sort })
+          .set('Accept', 'application/json')
+          .set('Authorization', 'Bearer ' + accessToken)
+          .expect(200)
+          .expect('Content-Type', /json/)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  getJobsList,
+  getJobsListPaginationAndSort
+}
index 10696e3f899b92d51d6d68579a4dd14804ccf99a..1c46a79004569207dd8d8bff1b889491985379ca 100644 (file)
@@ -1,2 +1,12 @@
 export type JobState = 'pending' | 'processing' | 'error' | 'success'
 export type JobCategory = 'transcoding' | 'activitypub-http'
+
+export interface Job {
+  id: number
+  state: JobState
+  category: JobCategory
+  handlerName: string
+  handlerInputData: any
+  createdAt: Date
+  updatedAt: Date
+}
index 9460b668ee438e7994a2157ec088d5ff540e078d..238e38a366f00f2b34043e503e8da30a8b493471 100644 (file)
@@ -4,6 +4,7 @@ export enum UserRight {
   MANAGE_SERVER_FOLLOW,
   MANAGE_VIDEO_ABUSES,
   MANAGE_VIDEO_BLACKLIST,
+  MANAGE_JOBS,
   REMOVE_ANY_VIDEO,
   REMOVE_ANY_VIDEO_CHANNEL
 }
index cc32c768d9dace81751877154afdb7fbd0d65535..954fa426ecc213e9107a588747d55b0d862d9c1d 100644 (file)
@@ -1,4 +1,5 @@
 import { UserRight } from './user-right.enum'
+import user from '../../../server/models/account/user'
 
 // Keep the order
 export enum UserRole {