Browse Source

Add playlist check param tests

Chocobozzz 5 years ago
parent
commit
07b1a18aa6

+ 10 - 6
server/controllers/api/video-playlist.ts

@@ -4,7 +4,7 @@ import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
   authenticate,
-  commonVideosFiltersValidator,
+  commonVideosFiltersValidator, optionalAuthenticate,
   paginationValidator,
   setDefaultPagination,
   setDefaultSort
@@ -31,12 +31,14 @@ import { processImage } from '../../helpers/image-utils'
 import { join } from 'path'
 import { UserModel } from '../../models/account/user'
 import {
-  getVideoPlaylistActivityPubUrl,
-  getVideoPlaylistElementActivityPubUrl,
   sendCreateVideoPlaylist,
   sendDeleteVideoPlaylist,
   sendUpdateVideoPlaylist
-} from '../../lib/activitypub'
+} from '../../lib/activitypub/send'
+import {
+  getVideoPlaylistActivityPubUrl,
+  getVideoPlaylistElementActivityPubUrl
+} from '../../lib/activitypub/url'
 import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model'
 import { VideoModel } from '../../models/video/video'
 import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
@@ -85,6 +87,7 @@ videoPlaylistRouter.get('/:playlistId/videos',
   asyncMiddleware(videoPlaylistsGetValidator),
   paginationValidator,
   setDefaultPagination,
+  optionalAuthenticate,
   commonVideosFiltersValidator,
   asyncMiddleware(getVideoPlaylistVideos)
 )
@@ -95,7 +98,7 @@ videoPlaylistRouter.post('/:playlistId/videos',
   asyncRetryTransactionMiddleware(addVideoInPlaylist)
 )
 
-videoPlaylistRouter.put('/:playlistId/videos',
+videoPlaylistRouter.post('/:playlistId/videos/reorder',
   authenticate,
   asyncMiddleware(videoPlaylistsReorderVideosValidator),
   asyncRetryTransactionMiddleware(reorderVideosPlaylist)
@@ -168,6 +171,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
   const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
     const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
 
+    videoPlaylistCreated.OwnerAccount = user.Account
     await sendCreateVideoPlaylist(videoPlaylistCreated, t)
 
     return videoPlaylistCreated
@@ -349,7 +353,7 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
   const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
 
   const start: number = req.body.startPosition
-  const insertAfter: number = req.body.insertAfter
+  const insertAfter: number = req.body.insertAfterPosition
   const reorderLength: number = req.body.reorderLength || 1
 
   if (start === insertAfter) {

+ 3 - 3
server/helpers/custom-validators/video-channels.ts

@@ -26,12 +26,12 @@ async function isLocalVideoChannelNameExist (name: string, res: express.Response
   return processVideoChannelExist(videoChannel, res)
 }
 
-async function isVideoChannelIdExist (id: string, res: express.Response) {
+async function isVideoChannelIdExist (id: number | string, res: express.Response) {
   let videoChannel: VideoChannelModel
-  if (validator.isInt(id)) {
+  if (validator.isInt('' + id)) {
     videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
   } else { // UUID
-    videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id)
+    videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount('' + id)
   }
 
   return processVideoChannelExist(videoChannel, res)

+ 1 - 0
server/lib/activitypub/index.ts

@@ -2,6 +2,7 @@ export * from './process'
 export * from './send'
 export * from './actor'
 export * from './share'
+export * from './playlist'
 export * from './videos'
 export * from './video-comments'
 export * from './video-rates'

+ 0 - 2
server/lib/activitypub/url.ts

@@ -5,10 +5,8 @@ import { VideoModel } from '../../models/video/video'
 import { VideoAbuseModel } from '../../models/video/video-abuse'
 import { VideoCommentModel } from '../../models/video/video-comment'
 import { VideoFileModel } from '../../models/video/video-file'
-import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
-import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
 
 function getVideoActivityPubUrl (video: VideoModel) {
   return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid

+ 41 - 3
server/middlewares/validators/videos/video-playlists.ts

@@ -6,7 +6,7 @@ import { UserModel } from '../../../models/account/user'
 import { areValidationErrors } from '../utils'
 import { isVideoExist, isVideoImage } from '../../../helpers/custom-validators/videos'
 import { CONSTRAINTS_FIELDS } from '../../../initializers'
-import { isIdOrUUIDValid, toValueOrNull } from '../../../helpers/custom-validators/misc'
+import { isIdOrUUIDValid, isUUIDValid, toValueOrNull } from '../../../helpers/custom-validators/misc'
 import {
   isVideoPlaylistDescriptionValid,
   isVideoPlaylistExist,
@@ -43,10 +43,19 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
     if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
 
     if (!await isVideoPlaylistExist(req.params.playlistId, res)) return cleanUpReqFiles(req)
+
+    const videoPlaylist = res.locals.videoPlaylist
+
     if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
       return cleanUpReqFiles(req)
     }
 
+    if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PRIVATE && req.body.privacy === VideoPlaylistPrivacy.PRIVATE) {
+      cleanUpReqFiles(req)
+      return res.status(409)
+                .json({ error: 'Cannot set "private" a video playlist that was not private.' })
+    }
+
     if (req.body.videoChannelId && !await isVideoChannelIdExist(req.body.videoChannelId, res)) return cleanUpReqFiles(req)
 
     return next()
@@ -83,6 +92,14 @@ const videoPlaylistsGetValidator = [
     if (!await isVideoPlaylistExist(req.params.playlistId, res)) return
 
     const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
+
+    // Video is unlisted, check we used the uuid to fetch it
+    if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
+      if (isUUIDValid(req.params.playlistId)) return next()
+
+      return res.status(404).end()
+    }
+
     if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
       await authenticatePromiseIfNeeded(req, res)
 
@@ -121,7 +138,7 @@ const videoPlaylistsAddVideoValidator = [
     if (areValidationErrors(req, res)) return
 
     if (!await isVideoPlaylistExist(req.params.playlistId, res)) return
-    if (!await isVideoExist(req.body.videoId, res, 'id')) return
+    if (!await isVideoExist(req.body.videoId, res, 'only-video')) return
 
     const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
     const video: VideoModel = res.locals.video
@@ -161,7 +178,7 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
     if (areValidationErrors(req, res)) return
 
     if (!await isVideoPlaylistExist(req.params.playlistId, res)) return
-    if (!await isVideoExist(req.params.playlistId, res, 'id')) return
+    if (!await isVideoExist(req.params.videoId, res, 'id')) return
 
     const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
     const video: VideoModel = res.locals.video
@@ -233,6 +250,27 @@ const videoPlaylistsReorderVideosValidator = [
     const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
     if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
 
+    const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
+    const startPosition: number = req.body.startPosition
+    const insertAfterPosition: number = req.body.insertAfterPosition
+    const reorderLength: number = req.body.reorderLength
+
+    if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
+      res.status(400)
+         .json({ error: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
+         .end()
+
+      return
+    }
+
+    if (reorderLength && reorderLength + startPosition > nextPosition) {
+      res.status(400)
+         .json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
+         .end()
+
+      return
+    }
+
     return next()
   }
 ]

+ 1 - 1
server/models/video/video-channel.ts

@@ -223,7 +223,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
 
   @HasMany(() => VideoPlaylistModel, {
     foreignKey: {
-      allowNull: false
+      allowNull: true
     },
     onDelete: 'cascade',
     hooks: true

+ 2 - 1
server/models/video/video-playlist-element.ts

@@ -188,7 +188,8 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
           [Sequelize.Op.lte]: endPosition
         }
       },
-      transaction
+      transaction,
+      validate: false // We use a literal to update the position
     }
 
     return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query)

+ 2 - 2
server/models/video/video-playlist.ts

@@ -197,7 +197,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
 
   @BelongsTo(() => VideoChannelModel, {
     foreignKey: {
-      allowNull: false
+      allowNull: true
     },
     onDelete: 'CASCADE'
   })
@@ -351,7 +351,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       updatedAt: this.updatedAt,
 
       ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(),
-      videoChannel: this.VideoChannel.toFormattedSummaryJSON()
+      videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null
     }
   }
 

+ 767 - 13
server/tests/api/check-params/video-playlists.ts

@@ -1,35 +1,35 @@
 /* tslint:disable:no-unused-expression */
 
-import { omit } from 'lodash'
 import 'mocha'
-import { join } from 'path'
-import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
 import {
   createUser,
+  createVideoPlaylist,
+  deleteVideoPlaylist,
   flushTests,
-  getMyUserInformation,
+  getVideoPlaylist,
   immutableAssign,
   killallServers,
   makeGetRequest,
-  makePostBodyRequest,
-  makeUploadRequest,
   runServer,
   ServerInfo,
   setAccessTokensToServers,
-  updateCustomSubConfig,
-  userLogin
+  updateVideoPlaylist,
+  userLogin,
+  addVideoInPlaylist, uploadVideo, updateVideoPlaylistElement, removeVideoFromPlaylist, reorderVideosPlaylist
 } from '../../../../shared/utils'
 import {
   checkBadCountPagination,
   checkBadSortPagination,
   checkBadStartPagination
 } from '../../../../shared/utils/requests/check-api-params'
-import { getMagnetURI, getYoutubeVideoUrl } from '../../../../shared/utils/videos/video-imports'
+import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 
 describe('Test video playlists API validator', function () {
-  const path = '/api/v1/videos/video-playlists'
   let server: ServerInfo
   let userAccessToken = ''
+  let playlistUUID: string
+  let videoId: number
+  let videoId2: number
 
   // ---------------------------------------------------------------
 
@@ -46,9 +46,31 @@ describe('Test video playlists API validator', function () {
     const password = 'my super password'
     await createUser(server.url, server.accessToken, username, password)
     userAccessToken = await userLogin(server, { username, password })
+
+    {
+      const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
+      videoId = res.body.video.id
+    }
+
+    {
+      const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
+      videoId2 = res.body.video.id
+    }
+
+    {
+      const res = await createVideoPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'super playlist',
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        }
+      })
+      playlistUUID = res.body.videoPlaylist.uuid
+    }
   })
 
-  describe('When listing video playlists', function () {
+  describe('When listing playlists', function () {
     const globalPath = '/api/v1/video-playlists'
     const accountPath = '/api/v1/accounts/root/video-playlists'
     const videoChannelPath = '/api/v1/video-channels/root_channel/video-playlists'
@@ -90,7 +112,7 @@ describe('Test video playlists API validator', function () {
     })
   })
 
-  describe('When listing videos of a playlist', async function () {
+  describe('When listing videos of a playlist', function () {
     const path = '/api/v1/video-playlists'
 
     it('Should fail with a bad start pagination', async function () {
@@ -101,11 +123,743 @@ describe('Test video playlists API validator', function () {
       await checkBadCountPagination(server.url, path, server.accessToken)
     })
 
-    it('Should fail with an incorrect sort', async function () {
+    it('Should fail with a bad filter', async function () {
       await checkBadSortPagination(server.url, path, server.accessToken)
     })
   })
 
+  describe('When getting a video playlist', function () {
+    it('Should fail with a bad id or uuid', async function () {
+      await getVideoPlaylist(server.url, 'toto', 400)
+    })
+
+    it('Should fail with an unknown playlist', async function () {
+      await getVideoPlaylist(server.url, 42, 404)
+    })
+
+    it('Should fail to get an unlisted playlist with the number id', async function () {
+      const res = await createVideoPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'super playlist',
+          privacy: VideoPlaylistPrivacy.UNLISTED
+        }
+      })
+      const playlist = res.body.videoPlaylist
+
+      await getVideoPlaylist(server.url, playlist.id, 404)
+      await getVideoPlaylist(server.url, playlist.uuid, 200)
+    })
+
+    it('Should succeed with the correct params', async function () {
+      await getVideoPlaylist(server.url, playlistUUID, 200)
+    })
+  })
+
+  describe('When creating/updating a video playlist', function () {
+
+    it('Should fail with an unauthenticated user', async function () {
+      const baseParams = {
+        url: server.url,
+        token: null,
+        playlistAttrs: {
+          displayName: 'super playlist',
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        },
+        expectedStatus: 401
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+
+    it('Should fail without displayName', async function () {
+      const baseParams = {
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        } as any,
+        expectedStatus: 400
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+
+    it('Should fail with an incorrect display name', async function () {
+      const baseParams = {
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 's'.repeat(300),
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        },
+        expectedStatus: 400
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+
+    it('Should fail with an incorrect description', async function () {
+      const baseParams = {
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.PUBLIC,
+          description: 't'
+        },
+        expectedStatus: 400
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+
+    it('Should fail with an incorrect privacy', async function () {
+      const baseParams = {
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: 45
+        } as any,
+        expectedStatus: 400
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+
+    it('Should fail with an unknown video channel id', async function () {
+      const baseParams = {
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.PUBLIC,
+          videoChannelId: 42
+        },
+        expectedStatus: 404
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+
+    it('Should fail with an incorrect thumbnail file', async function () {
+      const baseParams = {
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.PUBLIC,
+          thumbnailfile: 'avatar.png'
+        },
+        expectedStatus: 400
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+
+    it('Should fail with an unknown playlist to update', async function () {
+      await updateVideoPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: 42,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        },
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail to update a playlist of another user', async function () {
+      await updateVideoPlaylist({
+        url: server.url,
+        token: userAccessToken,
+        playlistId: playlistUUID,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        },
+        expectedStatus: 403
+      })
+    })
+
+    it('Should fail to update to private a public/unlisted playlist', async function () {
+      const res = await createVideoPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'super playlist',
+          privacy: VideoPlaylistPrivacy.PUBLIC
+        }
+      })
+      const playlist = res.body.videoPlaylist
+
+      await updateVideoPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlist.id,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.PRIVATE
+        },
+        expectedStatus: 409
+      })
+    })
+
+    it('Should succeed with the correct params', async function () {
+      const baseParams = {
+        url: server.url,
+        token: server.accessToken,
+        playlistAttrs: {
+          displayName: 'display name',
+          privacy: VideoPlaylistPrivacy.UNLISTED,
+          thumbnailfile: 'thumbnail.jpg'
+        }
+      }
+
+      await createVideoPlaylist(baseParams)
+      await updateVideoPlaylist(immutableAssign(baseParams, { playlistId: playlistUUID }))
+    })
+  })
+
+  describe('When adding an element in a playlist', function () {
+    it('Should fail with an unauthenticated user', async function () {
+      await addVideoInPlaylist({
+        url: server.url,
+        token: null,
+        elementAttrs: {
+          videoId: videoId
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 401
+      })
+    })
+
+    it('Should fail with the playlist of another user', async function () {
+      await addVideoInPlaylist({
+        url: server.url,
+        token: userAccessToken,
+        elementAttrs: {
+          videoId: videoId
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 403
+      })
+    })
+
+    it('Should fail with an unknown or incorrect playlist id', async function () {
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: videoId
+        },
+        playlistId: 'toto',
+        expectedStatus: 400
+      })
+
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: videoId
+        },
+        playlistId: 42,
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail with an unknown or incorrect video id', async function () {
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: 'toto' as any
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 400
+      })
+
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: 42
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail with a bad start/stop timestamp', async function () {
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: videoId,
+          startTimestamp: -42
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 400
+      })
+
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: videoId,
+          stopTimestamp: 'toto' as any
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 400
+      })
+    })
+
+    it('Succeed with the correct params', async function () {
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: videoId,
+          stopTimestamp: 3
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 200
+      })
+    })
+
+    it('Should fail if the video was already added in the playlist', async function () {
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          videoId: videoId,
+          stopTimestamp: 3
+        },
+        playlistId: playlistUUID,
+        expectedStatus: 409
+      })
+    })
+  })
+
+  describe('When updating an element in a playlist', function () {
+    it('Should fail with an unauthenticated user', async function () {
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: null,
+        elementAttrs: { },
+        videoId: videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 401
+      })
+    })
+
+    it('Should fail with the playlist of another user', async function () {
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: userAccessToken,
+        elementAttrs: { },
+        videoId: videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 403
+      })
+    })
+
+    it('Should fail with an unknown or incorrect playlist id', async function () {
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: { },
+        videoId: videoId,
+        playlistId: 'toto',
+        expectedStatus: 400
+      })
+
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: { },
+        videoId: videoId,
+        playlistId: 42,
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail with an unknown or incorrect video id', async function () {
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: { },
+        videoId: 'toto',
+        playlistId: playlistUUID,
+        expectedStatus: 400
+      })
+
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: { },
+        videoId: 42,
+        playlistId: playlistUUID,
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail with a bad start/stop timestamp', async function () {
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          startTimestamp: 'toto' as any
+        },
+        videoId: videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 400
+      })
+
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          stopTimestamp: -42
+        },
+        videoId: videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 400
+      })
+    })
+
+    it('Should fail with an unknown element', async function () {
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          stopTimestamp: 2
+        },
+        videoId: videoId2,
+        playlistId: playlistUUID,
+        expectedStatus: 404
+      })
+    })
+
+    it('Succeed with the correct params', async function () {
+      await updateVideoPlaylistElement({
+        url: server.url,
+        token: server.accessToken,
+        elementAttrs: {
+          stopTimestamp: 2
+        },
+        videoId: videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 204
+      })
+    })
+  })
+
+  describe('When reordering elements of a playlist', function () {
+    let videoId3: number
+    let videoId4: number
+
+    before(async function () {
+      {
+        const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
+        videoId3 = res.body.video.id
+      }
+
+      {
+        const res = await uploadVideo(server.url, server.accessToken, { name: 'video 4' })
+        videoId4 = res.body.video.id
+      }
+
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: { videoId: videoId3 }
+      })
+
+      await addVideoInPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: { videoId: videoId4 }
+      })
+    })
+
+    it('Should fail with an unauthenticated user', async function () {
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: null,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2
+        },
+        expectedStatus: 401
+      })
+    })
+
+    it('Should fail with the playlist of another user', async function () {
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: userAccessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2
+        },
+        expectedStatus: 403
+      })
+    })
+
+    it('Should fail with an invalid playlist', async function () {
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: 'toto',
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2
+        },
+        expectedStatus: 400
+      })
+
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: 42,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2
+        },
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail with an invalid start position', async function () {
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: -1,
+          insertAfterPosition: 2
+        },
+        expectedStatus: 400
+      })
+
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 'toto' as any,
+          insertAfterPosition: 2
+        },
+        expectedStatus: 400
+      })
+
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 42,
+          insertAfterPosition: 2
+        },
+        expectedStatus: 400
+      })
+    })
+
+    it('Should fail with an invalid insert after position', async function () {
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 'toto' as any
+        },
+        expectedStatus: 400
+      })
+
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: -2
+        },
+        expectedStatus: 400
+      })
+
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 42
+        },
+        expectedStatus: 400
+      })
+    })
+
+    it('Should fail with an invalid reorder length', async function () {
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2,
+          reorderLength: 'toto' as any
+        },
+        expectedStatus: 400
+      })
+
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2,
+          reorderLength: -1
+        },
+        expectedStatus: 400
+      })
+
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2,
+          reorderLength: 4
+        },
+        expectedStatus: 400
+      })
+    })
+
+    it('Succeed with the correct params', async function () {
+      await reorderVideosPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        playlistId: playlistUUID,
+        elementAttrs: {
+          startPosition: 1,
+          insertAfterPosition: 2,
+          reorderLength: 3
+        },
+        expectedStatus: 204
+      })
+    })
+  })
+
+  describe('When deleting an element in a playlist', function () {
+    it('Should fail with an unauthenticated user', async function () {
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: null,
+        videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 401
+      })
+    })
+
+    it('Should fail with the playlist of another user', async function () {
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: userAccessToken,
+        videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 403
+      })
+    })
+
+    it('Should fail with an unknown or incorrect playlist id', async function () {
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        videoId,
+        playlistId: 'toto',
+        expectedStatus: 400
+      })
+
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        videoId,
+        playlistId: 42,
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail with an unknown or incorrect video id', async function () {
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        videoId: 'toto',
+        playlistId: playlistUUID,
+        expectedStatus: 400
+      })
+
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        videoId: 42,
+        playlistId: playlistUUID,
+        expectedStatus: 404
+      })
+    })
+
+    it('Should fail with an unknown element', async function () {
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        videoId: videoId2,
+        playlistId: playlistUUID,
+        expectedStatus: 404
+      })
+    })
+
+    it('Succeed with the correct params', async function () {
+      await removeVideoFromPlaylist({
+        url: server.url,
+        token: server.accessToken,
+        videoId: videoId,
+        playlistId: playlistUUID,
+        expectedStatus: 204
+      })
+    })
+  })
+
+  describe('When deleting a playlist', function () {
+    it('Should fail with an unknown playlist', async function () {
+      await deleteVideoPlaylist(server.url, server.accessToken, 42, 404)
+    })
+
+    it('Should fail with a playlist of another user', async function () {
+      await deleteVideoPlaylist(server.url, userAccessToken, playlistUUID, 403)
+    })
+
+    it('Should succeed with the correct params', async function () {
+      await deleteVideoPlaylist(server.url, server.accessToken, playlistUUID)
+    })
+  })
+
   after(async function () {
     killallServers([ server ])
 

+ 22 - 11
server/tests/api/check-params/videos-filter.ts

@@ -1,9 +1,9 @@
 /* tslint:disable:no-unused-expression */
 
-import * as chai from 'chai'
 import 'mocha'
 import {
   createUser,
+  createVideoPlaylist,
   flushTests,
   killallServers,
   makeGetRequest,
@@ -13,15 +13,15 @@ import {
   userLogin
 } from '../../../../shared/utils'
 import { UserRole } from '../../../../shared/models/users'
+import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 
-const expect = chai.expect
-
-async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
+async function testEndpoints (server: ServerInfo, token: string, filter: string, playlistUUID: string, statusCodeExpected: number) {
   const paths = [
     '/api/v1/video-channels/root_channel/videos',
     '/api/v1/accounts/root/videos',
     '/api/v1/videos',
-    '/api/v1/search/videos'
+    '/api/v1/search/videos',
+    '/api/v1/video-playlists/' + playlistUUID + '/videos'
   ]
 
   for (const path of paths) {
@@ -41,6 +41,7 @@ describe('Test videos filters', function () {
   let server: ServerInfo
   let userAccessToken: string
   let moderatorAccessToken: string
+  let playlistUUID: string
 
   // ---------------------------------------------------------------
 
@@ -68,28 +69,38 @@ describe('Test videos filters', function () {
       UserRole.MODERATOR
     )
     moderatorAccessToken = await userLogin(server, moderator)
+
+    const res = await createVideoPlaylist({
+      url: server.url,
+      token: server.accessToken,
+      playlistAttrs: {
+        displayName: 'super playlist',
+        privacy: VideoPlaylistPrivacy.PUBLIC
+      }
+    })
+    playlistUUID = res.body.videoPlaylist.uuid
   })
 
   describe('When setting a video filter', function () {
 
     it('Should fail with a bad filter', async function () {
-      await testEndpoints(server, server.accessToken, 'bad-filter', 400)
+      await testEndpoints(server, server.accessToken, 'bad-filter', playlistUUID, 400)
     })
 
     it('Should succeed with a good filter', async function () {
-      await testEndpoints(server, server.accessToken,'local', 200)
+      await testEndpoints(server, server.accessToken,'local', playlistUUID, 200)
     })
 
     it('Should fail to list all-local with a simple user', async function () {
-      await testEndpoints(server, userAccessToken, 'all-local', 401)
+      await testEndpoints(server, userAccessToken, 'all-local', playlistUUID, 401)
     })
 
     it('Should succeed to list all-local with a moderator', async function () {
-      await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
+      await testEndpoints(server, moderatorAccessToken, 'all-local', playlistUUID, 200)
     })
 
     it('Should succeed to list all-local with an admin', async function () {
-      await testEndpoints(server, server.accessToken, 'all-local', 200)
+      await testEndpoints(server, server.accessToken, 'all-local', playlistUUID, 200)
     })
 
     // Because we cannot authenticate the user on the RSS endpoint
@@ -104,7 +115,7 @@ describe('Test videos filters', function () {
       })
     })
 
-    it('Should succed on the feeds endpoint with the local filter', async function () {
+    it('Should succeed on the feeds endpoint with the local filter', async function () {
       await makeGetRequest({
         url: server.url,
         path: '/feeds/videos.json',

+ 10 - 4
server/tests/api/redundancy/redundancy.ts

@@ -4,20 +4,26 @@ import * as chai from 'chai'
 import 'mocha'
 import { VideoDetails } from '../../../../shared/models/videos'
 import {
+  checkSegmentHash,
+  checkVideoFilesWereRemoved,
   doubleFollow,
   flushAndRunMultipleServers,
   getFollowingListPaginationAndSort,
   getVideo,
+  getVideoWithToken,
   immutableAssign,
-  killallServers, makeGetRequest,
+  killallServers,
+  makeGetRequest,
+  removeVideo,
+  reRunServer,
   root,
   ServerInfo,
-  setAccessTokensToServers, unfollow,
+  setAccessTokensToServers,
+  unfollow,
   uploadVideo,
   viewVideo,
   wait,
-  waitUntilLog,
-  checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash
+  waitUntilLog
 } from '../../../../shared/utils'
 import { waitJobs } from '../../../../shared/utils/server/jobs'
 

+ 2 - 2
shared/models/videos/playlist/video-playlist-create.model.ts

@@ -2,10 +2,10 @@ import { VideoPlaylistPrivacy } from './video-playlist-privacy.model'
 
 export interface VideoPlaylistCreate {
   displayName: string
-  description: string
   privacy: VideoPlaylistPrivacy
 
+  description?: string
   videoChannelId?: number
 
-  thumbnailfile?: Blob
+  thumbnailfile?: any
 }

+ 2 - 0
shared/models/videos/playlist/video-playlist-element-create.model.ts

@@ -1,4 +1,6 @@
 export interface VideoPlaylistElementCreate {
+  videoId: number
+
   startTimestamp?: number
   stopTimestamp?: number
 }

+ 2 - 2
shared/models/videos/playlist/video-playlist-update.model.ts

@@ -2,9 +2,9 @@ import { VideoPlaylistPrivacy } from './video-playlist-privacy.model'
 
 export interface VideoPlaylistUpdate {
   displayName: string
-  description: string
   privacy: VideoPlaylistPrivacy
 
+  description?: string
   videoChannelId?: number
-  thumbnailfile?: Blob
+  thumbnailfile?: any
 }

+ 2 - 1
shared/utils/index.ts

@@ -13,12 +13,13 @@ export * from './requests/requests'
 export * from './requests/check-api-params'
 export * from './server/servers'
 export * from './videos/services'
+export * from './videos/video-playlists'
 export * from './users/users'
 export * from './videos/video-abuses'
 export * from './videos/video-blacklist'
 export * from './videos/video-channels'
 export * from './videos/video-comments'
-export * from './videos/video-playlists'
+export * from './videos/video-streaming-playlists'
 export * from './videos/videos'
 export * from './videos/video-change-ownership'
 export * from './feeds/feeds'

+ 16 - 14
shared/utils/videos/video-playlists.ts

@@ -31,7 +31,7 @@ function getVideoPlaylist (url: string, playlistId: number | string, statusCodeE
   })
 }
 
-function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = 200) {
+function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = 204) {
   const path = '/api/v1/video-playlists/' + playlistId
 
   return makeDeleteRequest({
@@ -46,7 +46,7 @@ function createVideoPlaylist (options: {
   url: string,
   token: string,
   playlistAttrs: VideoPlaylistCreate,
-  expectedStatus: number
+  expectedStatus?: number
 }) {
   const path = '/api/v1/video-playlists/'
 
@@ -63,7 +63,7 @@ function createVideoPlaylist (options: {
     token: options.token,
     fields,
     attaches,
-    statusCodeExpected: options.expectedStatus
+    statusCodeExpected: options.expectedStatus || 200
   })
 }
 
@@ -71,9 +71,10 @@ function updateVideoPlaylist (options: {
   url: string,
   token: string,
   playlistAttrs: VideoPlaylistUpdate,
-  expectedStatus: number
+  playlistId: number | string,
+  expectedStatus?: number
 }) {
-  const path = '/api/v1/video-playlists/'
+  const path = '/api/v1/video-playlists/' + options.playlistId
 
   const fields = omit(options.playlistAttrs, 'thumbnailfile')
 
@@ -88,7 +89,7 @@ function updateVideoPlaylist (options: {
     token: options.token,
     fields,
     attaches,
-    statusCodeExpected: options.expectedStatus
+    statusCodeExpected: options.expectedStatus || 204
   })
 }
 
@@ -97,7 +98,7 @@ function addVideoInPlaylist (options: {
   token: string,
   playlistId: number | string,
   elementAttrs: VideoPlaylistElementCreate
-  expectedStatus: number
+  expectedStatus?: number
 }) {
   const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
 
@@ -106,7 +107,7 @@ function addVideoInPlaylist (options: {
     path,
     token: options.token,
     fields: options.elementAttrs,
-    statusCodeExpected: options.expectedStatus
+    statusCodeExpected: options.expectedStatus || 200
   })
 }
 
@@ -116,7 +117,7 @@ function updateVideoPlaylistElement (options: {
   playlistId: number | string,
   videoId: number | string,
   elementAttrs: VideoPlaylistElementUpdate,
-  expectedStatus: number
+  expectedStatus?: number
 }) {
   const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
 
@@ -125,7 +126,7 @@ function updateVideoPlaylistElement (options: {
     path,
     token: options.token,
     fields: options.elementAttrs,
-    statusCodeExpected: options.expectedStatus
+    statusCodeExpected: options.expectedStatus || 204
   })
 }
 
@@ -142,7 +143,7 @@ function removeVideoFromPlaylist (options: {
     url: options.url,
     path,
     token: options.token,
-    statusCodeExpected: options.expectedStatus
+    statusCodeExpected: options.expectedStatus || 204
   })
 }
 
@@ -152,14 +153,14 @@ function reorderVideosPlaylist (options: {
   playlistId: number | string,
   elementAttrs: {
     startPosition: number,
-    insertAfter: number,
+    insertAfterPosition: number,
     reorderLength?: number
   },
   expectedStatus: number
 }) {
-  const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
+  const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
 
-  return makePutBodyRequest({
+  return makePostBodyRequest({
     url: options.url,
     path,
     token: options.token,
@@ -179,6 +180,7 @@ export {
   deleteVideoPlaylist,
 
   addVideoInPlaylist,
+  updateVideoPlaylistElement,
   removeVideoFromPlaylist,
 
   reorderVideosPlaylist