From 07b1a18aa678d260009a93e36606c5c5f585723d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 28 Feb 2019 11:14:26 +0100 Subject: [PATCH] Add playlist check param tests --- server/controllers/api/video-playlist.ts | 16 +- .../custom-validators/video-channels.ts | 6 +- server/lib/activitypub/index.ts | 1 + server/lib/activitypub/url.ts | 2 - .../validators/videos/video-playlists.ts | 44 +- server/models/video/video-channel.ts | 2 +- server/models/video/video-playlist-element.ts | 3 +- server/models/video/video-playlist.ts | 4 +- .../tests/api/check-params/video-playlists.ts | 780 +++++++++++++++++- .../tests/api/check-params/videos-filter.ts | 33 +- server/tests/api/redundancy/redundancy.ts | 14 +- .../playlist/video-playlist-create.model.ts | 4 +- .../video-playlist-element-create.model.ts | 2 + .../playlist/video-playlist-update.model.ts | 4 +- shared/utils/index.ts | 3 +- shared/utils/videos/video-playlists.ts | 30 +- 16 files changed, 883 insertions(+), 65 deletions(-) diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index 709c58beb..e026b4d16 100644 --- a/server/controllers/api/video-playlist.ts +++ b/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) { diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts index cbf150e53..3792bbdcc 100644 --- a/server/helpers/custom-validators/video-channels.ts +++ b/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) diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts index 6906bf9d3..d8c7d83b7 100644 --- a/server/lib/activitypub/index.ts +++ b/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' diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 00bbbba2d..c80b09436 100644 --- a/server/lib/activitypub/url.ts +++ b/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 diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index ef8d0b851..0e97c8dc0 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/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() } ] diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 112abf8cf..c077fb518 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -223,7 +223,7 @@ export class VideoChannelModel extends Model { @HasMany(() => VideoPlaylistModel, { foreignKey: { - allowNull: false + allowNull: true }, onDelete: 'cascade', hooks: true diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index d76149d12..5530e0492 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts @@ -188,7 +188,8 @@ export class VideoPlaylistElementModel extends Model [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) diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 93b8c2f58..397887ebf 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -197,7 +197,7 @@ export class VideoPlaylistModel extends Model { @BelongsTo(() => VideoChannelModel, { foreignKey: { - allowNull: false + allowNull: true }, onDelete: 'CASCADE' }) @@ -351,7 +351,7 @@ export class VideoPlaylistModel extends Model { updatedAt: this.updatedAt, ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(), - videoChannel: this.VideoChannel.toFormattedSummaryJSON() + videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null } } diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts index 7004badac..68fe362e9 100644 --- a/server/tests/api/check-params/video-playlists.ts +++ b/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 ]) diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts index e998c8a3d..cc2f35069 100644 --- a/server/tests/api/check-params/videos-filter.ts +++ b/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', diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index 778611fff..fc5ffbad7 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/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' diff --git a/shared/models/videos/playlist/video-playlist-create.model.ts b/shared/models/videos/playlist/video-playlist-create.model.ts index 386acbb96..67a33fa35 100644 --- a/shared/models/videos/playlist/video-playlist-create.model.ts +++ b/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 } diff --git a/shared/models/videos/playlist/video-playlist-element-create.model.ts b/shared/models/videos/playlist/video-playlist-element-create.model.ts index 9bd56a8ca..c31702892 100644 --- a/shared/models/videos/playlist/video-playlist-element-create.model.ts +++ b/shared/models/videos/playlist/video-playlist-element-create.model.ts @@ -1,4 +1,6 @@ export interface VideoPlaylistElementCreate { + videoId: number + startTimestamp?: number stopTimestamp?: number } diff --git a/shared/models/videos/playlist/video-playlist-update.model.ts b/shared/models/videos/playlist/video-playlist-update.model.ts index c7a15c550..0ff5bcb0f 100644 --- a/shared/models/videos/playlist/video-playlist-update.model.ts +++ b/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 } diff --git a/shared/utils/index.ts b/shared/utils/index.ts index 156901372..c09565d95 100644 --- a/shared/utils/index.ts +++ b/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' diff --git a/shared/utils/videos/video-playlists.ts b/shared/utils/videos/video-playlists.ts index 5186d9c4f..21285688a 100644 --- a/shared/utils/videos/video-playlists.ts +++ b/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 -- 2.41.0