From d525fc399a14a8b16eaad6d4c0bc0a9c4093c3c9 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 20 Jul 2018 14:35:18 +0200 Subject: [PATCH] Add videos list filters --- server/controllers/api/accounts.ts | 12 +- server/controllers/api/search.ts | 16 +- server/controllers/api/video-channel.ts | 12 +- server/controllers/api/videos/index.ts | 12 +- server/controllers/feeds.ts | 5 +- server/helpers/custom-validators/misc.ts | 7 + server/helpers/custom-validators/search.ts | 19 ++ server/helpers/express-utils.ts | 14 +- server/initializers/constants.ts | 2 +- server/middlewares/sort.ts | 2 +- server/middlewares/validators/search.ts | 46 ++- server/models/utils.ts | 2 +- server/models/video/video.ts | 140 ++++++-- server/tests/api/check-params/index.ts | 1 + server/tests/api/check-params/search.ts | 122 +++++++ server/tests/api/index-fast.ts | 1 + server/tests/api/search/search-videos.ts | 299 ++++++++++++++++++ server/tests/api/videos/single-server.ts | 153 +-------- server/tests/api/videos/video-nsfw.ts | 30 +- server/tests/utils/index.ts | 1 + .../tests/utils/requests/check-api-params.ts | 13 +- server/tests/utils/search/videos.ts | 77 +++++ server/tests/utils/videos/videos.ts | 86 ++--- shared/models/index.ts | 1 + shared/models/search/index.ts | 1 + .../search/videos-search-query.model.ts | 24 ++ 26 files changed, 831 insertions(+), 267 deletions(-) create mode 100644 server/helpers/custom-validators/search.ts create mode 100644 server/tests/api/check-params/search.ts create mode 100644 server/tests/api/search/search-videos.ts create mode 100644 server/tests/utils/search/videos.ts create mode 100644 shared/models/search/index.ts create mode 100644 shared/models/search/videos-search-query.model.ts diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 8e937276c..0117fc8c6 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts @@ -1,7 +1,7 @@ import * as express from 'express' import { getFormattedObjects } from '../../helpers/utils' import { - asyncMiddleware, + asyncMiddleware, commonVideosFiltersValidator, listVideoAccountChannelsValidator, optionalAuthenticate, paginationValidator, @@ -11,7 +11,7 @@ import { import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' import { AccountModel } from '../../models/account/account' import { VideoModel } from '../../models/video/video' -import { isNSFWHidden } from '../../helpers/express-utils' +import { buildNSFWFilter } from '../../helpers/express-utils' import { VideoChannelModel } from '../../models/video/video-channel' const accountsRouter = express.Router() @@ -36,6 +36,7 @@ accountsRouter.get('/:accountName/videos', setDefaultSort, setDefaultPagination, optionalAuthenticate, + commonVideosFiltersValidator, asyncMiddleware(listAccountVideos) ) @@ -77,7 +78,12 @@ async function listAccountVideos (req: express.Request, res: express.Response, n start: req.query.start, count: req.query.count, sort: req.query.sort, - hideNSFW: isNSFWHidden(res), + categoryOneOf: req.query.categoryOneOf, + licenceOneOf: req.query.licenceOneOf, + languageOneOf: req.query.languageOneOf, + tagsOneOf: req.query.tagsOneOf, + tagsAllOf: req.query.tagsAllOf, + nsfw: buildNSFWFilter(res, req.query.nsfw), withFiles: false, accountId: account.id }) diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts index 2ff340b59..f810c7452 100644 --- a/server/controllers/api/search.ts +++ b/server/controllers/api/search.ts @@ -1,9 +1,10 @@ import * as express from 'express' -import { isNSFWHidden } from '../../helpers/express-utils' +import { buildNSFWFilter } from '../../helpers/express-utils' import { getFormattedObjects } from '../../helpers/utils' import { VideoModel } from '../../models/video/video' import { asyncMiddleware, + commonVideosFiltersValidator, optionalAuthenticate, paginationValidator, searchValidator, @@ -11,6 +12,7 @@ import { setDefaultSearchSort, videosSearchSortValidator } from '../../middlewares' +import { VideosSearchQuery } from '../../../shared/models/search' const searchRouter = express.Router() @@ -20,6 +22,7 @@ searchRouter.get('/videos', videosSearchSortValidator, setDefaultSearchSort, optionalAuthenticate, + commonVideosFiltersValidator, searchValidator, asyncMiddleware(searchVideos) ) @@ -31,13 +34,10 @@ export { searchRouter } // --------------------------------------------------------------------------- async function searchVideos (req: express.Request, res: express.Response) { - const resultList = await VideoModel.searchAndPopulateAccountAndServer( - req.query.search as string, - req.query.start as number, - req.query.count as number, - req.query.sort as string, - isNSFWHidden(res) - ) + const query: VideosSearchQuery = req.query + + const options = Object.assign(query, { nsfw: buildNSFWFilter(res, query.nsfw) }) + const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 1707732ee..0488ba8f5 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts @@ -3,7 +3,7 @@ import { getFormattedObjects, resetSequelizeInstance } from '../../helpers/utils import { asyncMiddleware, asyncRetryTransactionMiddleware, - authenticate, + authenticate, commonVideosFiltersValidator, optionalAuthenticate, paginationValidator, setDefaultPagination, @@ -19,7 +19,7 @@ import { videosSortValidator } from '../../middlewares/validators' import { sendUpdateActor } from '../../lib/activitypub/send' import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' import { createVideoChannel } from '../../lib/video-channel' -import { createReqFiles, isNSFWHidden } from '../../helpers/express-utils' +import { createReqFiles, buildNSFWFilter } from '../../helpers/express-utils' import { setAsyncActorKeys } from '../../lib/activitypub' import { AccountModel } from '../../models/account/account' import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' @@ -79,6 +79,7 @@ videoChannelRouter.get('/:id/videos', setDefaultSort, setDefaultPagination, optionalAuthenticate, + commonVideosFiltersValidator, asyncMiddleware(listVideoChannelVideos) ) @@ -189,7 +190,12 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon start: req.query.start, count: req.query.count, sort: req.query.sort, - hideNSFW: isNSFWHidden(res), + categoryOneOf: req.query.categoryOneOf, + licenceOneOf: req.query.licenceOneOf, + languageOneOf: req.query.languageOneOf, + tagsOneOf: req.query.tagsOneOf, + tagsAllOf: req.query.tagsAllOf, + nsfw: buildNSFWFilter(res, req.query.nsfw), withFiles: false, videoChannelId: videoChannelInstance.id }) diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 547522123..101183eab 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -31,6 +31,7 @@ import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, + commonVideosFiltersValidator, optionalAuthenticate, paginationValidator, setDefaultPagination, @@ -49,7 +50,7 @@ import { blacklistRouter } from './blacklist' import { videoCommentRouter } from './comment' import { rateVideoRouter } from './rate' import { VideoFilter } from '../../../../shared/models/videos/video-query.type' -import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils' +import { createReqFiles, buildNSFWFilter } from '../../../helpers/express-utils' import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' import { videoCaptionsRouter } from './captions' @@ -90,6 +91,7 @@ videosRouter.get('/', setDefaultSort, setDefaultPagination, optionalAuthenticate, + commonVideosFiltersValidator, asyncMiddleware(listVideos) ) videosRouter.put('/:id', @@ -401,8 +403,12 @@ async function listVideos (req: express.Request, res: express.Response, next: ex start: req.query.start, count: req.query.count, sort: req.query.sort, - category: req.query.category, - hideNSFW: isNSFWHidden(res), + categoryOneOf: req.query.categoryOneOf, + licenceOneOf: req.query.licenceOneOf, + languageOneOf: req.query.languageOneOf, + tagsOneOf: req.query.tagsOneOf, + tagsAllOf: req.query.tagsAllOf, + nsfw: buildNSFWFilter(res, req.query.nsfw), filter: req.query.filter as VideoFilter, withFiles: false }) diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index ff6b423d9..682f4abda 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts @@ -8,6 +8,7 @@ import { AccountModel } from '../models/account/account' import { cacheRoute } from '../middlewares/cache' import { VideoChannelModel } from '../models/video/video-channel' import { VideoCommentModel } from '../models/video/video-comment' +import { buildNSFWFilter } from '../helpers/express-utils' const feedsRouter = express.Router() @@ -73,7 +74,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n const account: AccountModel = res.locals.account const videoChannel: VideoChannelModel = res.locals.videoChannel - const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' + const nsfw = buildNSFWFilter(res, req.query.nsfw) let name: string let description: string @@ -95,7 +96,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n start, count: FEEDS.COUNT, sort: req.query.sort, - hideNSFW, + nsfw, filter: req.query.filter, withFiles: true, accountId: account ? account.id : null, diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 455aae367..151fc852b 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts @@ -41,6 +41,12 @@ function toValueOrNull (value: string) { return value } +function toArray (value: string) { + if (value && isArray(value) === false) return [ value ] + + return value +} + function isFileValid ( files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], mimeTypeRegex: string, @@ -80,5 +86,6 @@ export { toValueOrNull, isBooleanValid, toIntOrNull, + toArray, isFileValid } diff --git a/server/helpers/custom-validators/search.ts b/server/helpers/custom-validators/search.ts new file mode 100644 index 000000000..2fde39160 --- /dev/null +++ b/server/helpers/custom-validators/search.ts @@ -0,0 +1,19 @@ +import * as validator from 'validator' +import 'express-validator' + +import { isArray } from './misc' + +function isNumberArray (value: any) { + return isArray(value) && value.every(v => validator.isInt('' + v)) +} + +function isStringArray (value: any) { + return isArray(value) && value.every(v => typeof v === 'string') +} + +// --------------------------------------------------------------------------- + +export { + isNumberArray, + isStringArray +} diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index d023117a8..5bf1e1a5f 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts @@ -5,13 +5,19 @@ import { logger } from './logger' import { User } from '../../shared/models/users' import { generateRandomString } from './utils' -function isNSFWHidden (res: express.Response) { +function buildNSFWFilter (res: express.Response, paramNSFW?: boolean) { + if (paramNSFW === true || paramNSFW === false) return paramNSFW + if (res.locals.oauth) { const user: User = res.locals.oauth.token.User - if (user) return user.nsfwPolicy === 'do_not_list' + // User does not want NSFW videos + if (user && user.nsfwPolicy === 'do_not_list') return false } - return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' + if (CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list') return false + + // Display all + return null } function getHostWithPort (host: string) { @@ -70,7 +76,7 @@ function createReqFiles ( // --------------------------------------------------------------------------- export { - isNSFWHidden, + buildNSFWFilter, getHostWithPort, badRequest, createReqFiles diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index b966c0acb..9f220aea5 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -37,7 +37,7 @@ const SORTABLE_COLUMNS = { FOLLOWERS: [ 'createdAt' ], FOLLOWING: [ 'createdAt' ], - VIDEOS_SEARCH: [ 'bestmatch', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ] + VIDEOS_SEARCH: [ 'match', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ] } const OAUTH_LIFETIME = { diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index 6307ee154..8a62c8be6 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts @@ -9,7 +9,7 @@ function setDefaultSort (req: express.Request, res: express.Response, next: expr } function setDefaultSearchSort (req: express.Request, res: express.Response, next: express.NextFunction) { - if (!req.query.sort) req.query.sort = '-bestmatch' + if (!req.query.sort) req.query.sort = '-match' return next() } diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index 774845e8a..fb2148eb3 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts @@ -2,12 +2,55 @@ import * as express from 'express' import { areValidationErrors } from './utils' import { logger } from '../../helpers/logger' import { query } from 'express-validator/check' +import { isNumberArray, isStringArray } from '../../helpers/custom-validators/search' +import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc' const searchValidator = [ query('search').not().isEmpty().withMessage('Should have a valid search'), + query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'), + query('endDate').optional().custom(isDateValid).withMessage('Should have a valid end date'), + + query('durationMin').optional().isInt().withMessage('Should have a valid min duration'), + query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking search query', { parameters: req.query }) + + if (areValidationErrors(req, res)) return + + return next() + } +] + +const commonVideosFiltersValidator = [ + query('categoryOneOf') + .optional() + .customSanitizer(toArray) + .custom(isNumberArray).withMessage('Should have a valid one of category array'), + query('licenceOneOf') + .optional() + .customSanitizer(toArray) + .custom(isNumberArray).withMessage('Should have a valid one of licence array'), + query('languageOneOf') + .optional() + .customSanitizer(toArray) + .custom(isStringArray).withMessage('Should have a valid one of language array'), + query('tagsOneOf') + .optional() + .customSanitizer(toArray) + .custom(isStringArray).withMessage('Should have a valid one of tags array'), + query('tagsAllOf') + .optional() + .customSanitizer(toArray) + .custom(isStringArray).withMessage('Should have a valid all of tags array'), + query('nsfw') + .optional() + .toBoolean() + .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), + (req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.debug('Checking search parameters', { parameters: req.params }) + logger.debug('Checking commons video filters query', { parameters: req.query }) if (areValidationErrors(req, res)) return @@ -18,5 +61,6 @@ const searchValidator = [ // --------------------------------------------------------------------------- export { + commonVideosFiltersValidator, searchValidator } diff --git a/server/models/utils.ts b/server/models/utils.ts index 49d32c24f..393f8f036 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts @@ -14,7 +14,7 @@ function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { } // Alias - if (field.toLowerCase() === 'bestmatch') field = Sequelize.col('similarity') + if (field.toLowerCase() === 'match') field = Sequelize.col('similarity') return [ [ field, direction ], lastSort ] } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 15b4dda5b..68116e309 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -93,6 +93,7 @@ import { VideoShareModel } from './video-share' import { VideoTagModel } from './video-tag' import { ScheduleVideoUpdateModel } from './schedule-video-update' import { VideoCaptionModel } from './video-caption' +import { VideosSearchQuery } from '../../../shared/models/search' // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation const indexes: Sequelize.DefineIndexesOptions[] = [ @@ -133,16 +134,22 @@ export enum ScopeNames { WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE' } +type AvailableForListOptions = { + actorId: number, + filter?: VideoFilter, + categoryOneOf?: number[], + nsfw?: boolean, + licenceOneOf?: number[], + languageOneOf?: string[], + tagsOneOf?: string[], + tagsAllOf?: string[], + withFiles?: boolean, + accountId?: number, + videoChannelId?: number +} + @Scopes({ - [ScopeNames.AVAILABLE_FOR_LIST]: (options: { - actorId: number, - hideNSFW: boolean, - filter?: VideoFilter, - category?: number, - withFiles?: boolean, - accountId?: number, - videoChannelId?: number - }) => { + [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { const accountInclude = { attributes: [ 'id', 'name' ], model: AccountModel.unscoped(), @@ -243,13 +250,55 @@ export enum ScopeNames { }) } - // Hide nsfw videos? - if (options.hideNSFW === true) { - query.where['nsfw'] = false + // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN() + if (options.tagsAllOf || options.tagsOneOf) { + const createTagsIn = (tags: string[]) => { + return tags.map(t => VideoModel.sequelize.escape(t)) + .join(', ') + } + + if (options.tagsOneOf) { + query.where['id'][Sequelize.Op.in] = Sequelize.literal( + '(' + + 'SELECT "videoId" FROM "videoTag" ' + + 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + + 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsOneOf) + ')' + + ')' + ) + } + + if (options.tagsAllOf) { + query.where['id'][Sequelize.Op.in] = Sequelize.literal( + '(' + + 'SELECT "videoId" FROM "videoTag" ' + + 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + + 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsAllOf) + ')' + + 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length + + ')' + ) + } + } + + if (options.nsfw === true || options.nsfw === false) { + query.where['nsfw'] = options.nsfw + } + + if (options.categoryOneOf) { + query.where['category'] = { + [Sequelize.Op.or]: options.categoryOneOf + } + } + + if (options.licenceOneOf) { + query.where['licence'] = { + [Sequelize.Op.or]: options.licenceOneOf + } } - if (options.category) { - query.where['category'] = options.category + if (options.languageOneOf) { + query.where['language'] = { + [Sequelize.Op.or]: options.languageOneOf + } } if (options.accountId) { @@ -756,9 +805,13 @@ export class VideoModel extends Model { start: number, count: number, sort: string, - hideNSFW: boolean, + nsfw: boolean, withFiles: boolean, - category?: number, + categoryOneOf?: number[], + licenceOneOf?: number[], + languageOneOf?: string[], + tagsOneOf?: string[], + tagsAllOf?: string[], filter?: VideoFilter, accountId?: number, videoChannelId?: number @@ -774,13 +827,17 @@ export class VideoModel extends Model { method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: serverActor.id, - hideNSFW: options.hideNSFW, - category: options.category, + nsfw: options.nsfw, + categoryOneOf: options.categoryOneOf, + licenceOneOf: options.licenceOneOf, + languageOneOf: options.languageOneOf, + tagsOneOf: options.tagsOneOf, + tagsAllOf: options.tagsAllOf, filter: options.filter, withFiles: options.withFiles, accountId: options.accountId, videoChannelId: options.videoChannelId - } + } as AvailableForListOptions ] } @@ -794,15 +851,39 @@ export class VideoModel extends Model { }) } - static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) { + static async searchAndPopulateAccountAndServer (options: VideosSearchQuery) { + const whereAnd = [ ] + + if (options.startDate || options.endDate) { + const publishedAtRange = { } + + if (options.startDate) publishedAtRange[Sequelize.Op.gte] = options.startDate + if (options.endDate) publishedAtRange[Sequelize.Op.lte] = options.endDate + + whereAnd.push({ publishedAt: publishedAtRange }) + } + + if (options.durationMin || options.durationMax) { + const durationRange = { } + + if (options.durationMin) durationRange[Sequelize.Op.gte] = options.durationMin + if (options.durationMax) durationRange[Sequelize.Op.lte] = options.durationMax + + whereAnd.push({ duration: durationRange }) + } + + whereAnd.push(createSearchTrigramQuery('VideoModel.name', options.search)) + const query: IFindOptions = { attributes: { - include: [ createSimilarityAttribute('VideoModel.name', value) ] + include: [ createSimilarityAttribute('VideoModel.name', options.search) ] }, - offset: start, - limit: count, - order: getSort(sort), - where: createSearchTrigramQuery('VideoModel.name', value) + offset: options.start, + limit: options.count, + order: getSort(options.sort), + where: { + [ Sequelize.Op.and ]: whereAnd + } } const serverActor = await getServerActor() @@ -810,8 +891,13 @@ export class VideoModel extends Model { method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: serverActor.id, - hideNSFW - } + nsfw: options.nsfw, + categoryOneOf: options.categoryOneOf, + licenceOneOf: options.licenceOneOf, + languageOneOf: options.languageOneOf, + tagsOneOf: options.tagsOneOf, + tagsAllOf: options.tagsAllOf + } as AvailableForListOptions ] } diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index c0e0302df..820dde889 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts @@ -10,3 +10,4 @@ import './video-captions' import './video-channels' import './video-comments' import './videos' +import './search' diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts new file mode 100644 index 000000000..d35eac7fe --- /dev/null +++ b/server/tests/api/check-params/search.ts @@ -0,0 +1,122 @@ +/* tslint:disable:no-unused-expression */ + +import 'mocha' + +import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../utils' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' + +describe('Test videos API validator', function () { + const path = '/api/v1/search/videos/' + let server: ServerInfo + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + await flushTests() + + server = await runServer(1) + }) + + describe('When searching videos', function () { + const query = { + search: 'coucou' + } + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, null, query) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, null, query) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, null, query) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, query, statusCodeExpected: 200 }) + }) + + it('Should fail with an invalid category', async function () { + const customQuery1 = immutableAssign(query, { categoryOneOf: [ 'aa', 'b' ] }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 }) + + const customQuery2 = immutableAssign(query, { categoryOneOf: 'a' }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 }) + }) + + it('Should succeed with a valid category', async function () { + const customQuery1 = immutableAssign(query, { categoryOneOf: [ 1, 7 ] }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 }) + + const customQuery2 = immutableAssign(query, { categoryOneOf: 1 }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 }) + }) + + it('Should fail with an invalid licence', async function () { + const customQuery1 = immutableAssign(query, { licenceOneOf: [ 'aa', 'b' ] }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 }) + + const customQuery2 = immutableAssign(query, { licenceOneOf: 'a' }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 }) + }) + + it('Should succeed with a valid licence', async function () { + const customQuery1 = immutableAssign(query, { licenceOneOf: [ 1, 2 ] }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 }) + + const customQuery2 = immutableAssign(query, { licenceOneOf: 1 }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 }) + }) + + it('Should succeed with a valid language', async function () { + const customQuery1 = immutableAssign(query, { languageOneOf: [ 'fr', 'en' ] }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 }) + + const customQuery2 = immutableAssign(query, { languageOneOf: 'fr' }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 }) + }) + + it('Should succeed with valid tags', async function () { + const customQuery1 = immutableAssign(query, { tagsOneOf: [ 'tag1', 'tag2' ] }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 }) + + const customQuery2 = immutableAssign(query, { tagsOneOf: 'tag1' }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 }) + + const customQuery3 = immutableAssign(query, { tagsAllOf: [ 'tag1', 'tag2' ] }) + await makeGetRequest({ url: server.url, path, query: customQuery3, statusCodeExpected: 200 }) + + const customQuery4 = immutableAssign(query, { tagsAllOf: 'tag1' }) + await makeGetRequest({ url: server.url, path, query: customQuery4, statusCodeExpected: 200 }) + }) + + it('Should fail with invalid durations', async function () { + const customQuery1 = immutableAssign(query, { durationMin: 'hello' }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 }) + + const customQuery2 = immutableAssign(query, { durationMax: 'hello' }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 }) + }) + + it('Should fail with invalid dates', async function () { + const customQuery1 = immutableAssign(query, { startDate: 'hello' }) + await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 }) + + const customQuery2 = immutableAssign(query, { endDate: 'hello' }) + await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 }) + }) + }) + + after(async function () { + killallServers([ server ]) + + // Keep the logs if the test failed + if (this['ok']) { + await flushTests() + } + }) +}) diff --git a/server/tests/api/index-fast.ts b/server/tests/api/index-fast.ts index d530dfc06..531a09b82 100644 --- a/server/tests/api/index-fast.ts +++ b/server/tests/api/index-fast.ts @@ -14,3 +14,4 @@ import './videos/services' import './server/email' import './server/config' import './server/reverse-proxy' +import './search/search-videos' diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts new file mode 100644 index 000000000..7fc133b46 --- /dev/null +++ b/server/tests/api/search/search-videos.ts @@ -0,0 +1,299 @@ +/* tslint:disable:no-unused-expression */ + +import * as chai from 'chai' +import 'mocha' +import { + advancedVideosSearch, + flushTests, + killallServers, + runServer, + searchVideo, + ServerInfo, + setAccessTokensToServers, + uploadVideo, + wait, + immutableAssign +} from '../../utils' + +const expect = chai.expect + +describe('Test a videos search', function () { + let server: ServerInfo = null + let startDate: string + + before(async function () { + this.timeout(30000) + + await flushTests() + + server = await runServer(1) + + await setAccessTokensToServers([ server ]) + + { + const attributes1 = { + name: '1111 2222 3333', + fixture: '60fps_720p_small.mp4', // 2 seconds + category: 1, + licence: 1, + nsfw: false, + language: 'fr' + } + await uploadVideo(server.url, server.accessToken, attributes1) + + const attributes2 = immutableAssign(attributes1, { name: attributes1.name + ' - 2', fixture: 'video_short.mp4' }) + await uploadVideo(server.url, server.accessToken, attributes2) + + const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: 'en' }) + await uploadVideo(server.url, server.accessToken, attributes3) + + const attributes4 = immutableAssign(attributes1, { name: attributes1.name + ' - 4', language: 'pl', nsfw: true }) + await uploadVideo(server.url, server.accessToken, attributes4) + + await wait(1000) + + startDate = new Date().toISOString() + + const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2 }) + await uploadVideo(server.url, server.accessToken, attributes5) + + const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] }) + await uploadVideo(server.url, server.accessToken, attributes6) + + const attributes7 = immutableAssign(attributes1, { name: attributes1.name + ' - 7' }) + await uploadVideo(server.url, server.accessToken, attributes7) + + const attributes8 = immutableAssign(attributes1, { name: attributes1.name + ' - 8', licence: 4 }) + await uploadVideo(server.url, server.accessToken, attributes8) + } + + { + const attributes = { + name: '3333 4444 5555', + fixture: 'video_short.mp4', + category: 2, + licence: 2, + language: 'en' + } + await uploadVideo(server.url, server.accessToken, attributes) + + await uploadVideo(server.url, server.accessToken, immutableAssign(attributes, { name: attributes.name + ' duplicate' })) + } + + { + const attributes = { + name: '6666 7777 8888', + fixture: 'video_short.mp4', + category: 3, + licence: 3, + language: 'pl' + } + await uploadVideo(server.url, server.accessToken, attributes) + } + + { + const attributes1 = { + name: '9999', + tags: [ 'aaaa', 'bbbb', 'cccc' ], + category: 1 + } + await uploadVideo(server.url, server.accessToken, attributes1) + await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { category: 2 })) + + await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'cccc', 'dddd' ] })) + await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'eeee', 'ffff' ] })) + } + }) + + it('Should make a simple search and not have results', async function () { + const res = await searchVideo(server.url, 'abc') + + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + }) + + it('Should make a simple search and have results', async function () { + const res = await searchVideo(server.url, '4444 5555 duplicate') + + expect(res.body.total).to.equal(2) + + const videos = res.body.data + expect(videos).to.have.lengthOf(2) + + // bestmatch + expect(videos[0].name).to.equal('3333 4444 5555 duplicate') + expect(videos[1].name).to.equal('3333 4444 5555') + }) + + it('Should search by tags (one of)', async function () { + const query = { + search: '9999', + categoryOneOf: [ 1 ], + tagsOneOf: [ 'aaaa', 'ffff' ] + } + const res1 = await advancedVideosSearch(server.url, query) + expect(res1.body.total).to.equal(2) + + const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsOneOf: [ 'blabla' ] })) + expect(res2.body.total).to.equal(0) + }) + + it('Should search by tags (all of)', async function () { + const query = { + search: '9999', + categoryOneOf: [ 1 ], + tagsAllOf: [ 'cccc' ] + } + const res1 = await advancedVideosSearch(server.url, query) + expect(res1.body.total).to.equal(2) + + const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blabla' ] })) + expect(res2.body.total).to.equal(0) + + const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'cccc' ] })) + expect(res3.body.total).to.equal(1) + }) + + it('Should search by category', async function () { + const query = { + search: '6666', + categoryOneOf: [ 3 ] + } + const res1 = await advancedVideosSearch(server.url, query) + expect(res1.body.total).to.equal(1) + expect(res1.body.data[0].name).to.equal('6666 7777 8888') + + const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { categoryOneOf: [ 2 ] })) + expect(res2.body.total).to.equal(0) + }) + + it('Should search by licence', async function () { + const query = { + search: '4444 5555', + licenceOneOf: [ 2 ] + } + const res1 = await advancedVideosSearch(server.url, query) + expect(res1.body.total).to.equal(2) + expect(res1.body.data[0].name).to.equal('3333 4444 5555') + expect(res1.body.data[1].name).to.equal('3333 4444 5555 duplicate') + + const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { licenceOneOf: [ 3 ] })) + expect(res2.body.total).to.equal(0) + }) + + it('Should search by languages', async function () { + const query = { + search: '1111 2222 3333', + languageOneOf: [ 'pl', 'en' ] + } + const res1 = await advancedVideosSearch(server.url, query) + expect(res1.body.total).to.equal(2) + expect(res1.body.data[0].name).to.equal('1111 2222 3333 - 3') + expect(res1.body.data[1].name).to.equal('1111 2222 3333 - 4') + + const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] })) + expect(res2.body.total).to.equal(0) + }) + + it('Should search by start date', async function () { + const query = { + search: '1111 2222 3333', + startDate + } + + const res = await advancedVideosSearch(server.url, query) + expect(res.body.total).to.equal(4) + + const videos = res.body.data + expect(videos[0].name).to.equal('1111 2222 3333 - 5') + expect(videos[1].name).to.equal('1111 2222 3333 - 6') + expect(videos[2].name).to.equal('1111 2222 3333 - 7') + expect(videos[3].name).to.equal('1111 2222 3333 - 8') + }) + + it('Should make an advanced search', async function () { + const query = { + search: '1111 2222 3333', + languageOneOf: [ 'pl', 'fr' ], + durationMax: 4, + nsfw: false, + licenceOneOf: [ 1, 4 ] + } + + const res = await advancedVideosSearch(server.url, query) + expect(res.body.total).to.equal(4) + + const videos = res.body.data + expect(videos[0].name).to.equal('1111 2222 3333') + expect(videos[1].name).to.equal('1111 2222 3333 - 6') + expect(videos[2].name).to.equal('1111 2222 3333 - 7') + expect(videos[3].name).to.equal('1111 2222 3333 - 8') + }) + + it('Should make an advanced search and sort results', async function () { + const query = { + search: '1111 2222 3333', + languageOneOf: [ 'pl', 'fr' ], + durationMax: 4, + nsfw: false, + licenceOneOf: [ 1, 4 ], + sort: '-name' + } + + const res = await advancedVideosSearch(server.url, query) + expect(res.body.total).to.equal(4) + + const videos = res.body.data + expect(videos[0].name).to.equal('1111 2222 3333 - 8') + expect(videos[1].name).to.equal('1111 2222 3333 - 7') + expect(videos[2].name).to.equal('1111 2222 3333 - 6') + expect(videos[3].name).to.equal('1111 2222 3333') + }) + + it('Should make an advanced search and only show the first result', async function () { + const query = { + search: '1111 2222 3333', + languageOneOf: [ 'pl', 'fr' ], + durationMax: 4, + nsfw: false, + licenceOneOf: [ 1, 4 ], + sort: '-name', + start: 0, + count: 1 + } + + const res = await advancedVideosSearch(server.url, query) + expect(res.body.total).to.equal(4) + + const videos = res.body.data + expect(videos[0].name).to.equal('1111 2222 3333 - 8') + }) + + it('Should make an advanced search and only show the last result', async function () { + const query = { + search: '1111 2222 3333', + languageOneOf: [ 'pl', 'fr' ], + durationMax: 4, + nsfw: false, + licenceOneOf: [ 1, 4 ], + sort: '-name', + start: 3, + count: 1 + } + + const res = await advancedVideosSearch(server.url, query) + expect(res.body.total).to.equal(4) + + const videos = res.body.data + expect(videos[0].name).to.equal('1111 2222 3333') + }) + + after(async function () { + killallServers([ server ]) + + // Keep the logs if the test failed + if (this['ok']) { + await flushTests() + } + }) +}) diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts index d8af94e8f..ba4920d1b 100644 --- a/server/tests/api/videos/single-server.ts +++ b/server/tests/api/videos/single-server.ts @@ -16,13 +16,11 @@ import { getVideosList, getVideosListPagination, getVideosListSort, + getVideosWithFilters, killallServers, rateVideo, removeVideo, runServer, - searchVideo, - searchVideoWithPagination, - searchVideoWithSort, ServerInfo, setAccessTokensToServers, testImage, @@ -218,72 +216,6 @@ describe('Test a single server', function () { expect(video.views).to.equal(3) }) - it('Should search the video by name', async function () { - const res = await searchVideo(server.url, 'my') - - expect(res.body.total).to.equal(1) - expect(res.body.data).to.be.an('array') - expect(res.body.data.length).to.equal(1) - - const video = res.body.data[0] - await completeVideoCheck(server.url, video, getCheckAttributes) - }) - - // Not implemented yet - // it('Should search the video by tag', async function () { - // const res = await searchVideo(server.url, 'tag1') - // - // expect(res.body.total).to.equal(1) - // expect(res.body.data).to.be.an('array') - // expect(res.body.data.length).to.equal(1) - // - // const video = res.body.data[0] - // expect(video.name).to.equal('my super name') - // expect(video.category).to.equal(2) - // expect(video.categoryLabel).to.equal('Films') - // expect(video.licence).to.equal(6) - // expect(video.licenceLabel).to.equal('Attribution - Non Commercial - No Derivatives') - // expect(video.language).to.equal('zh') - // expect(video.languageLabel).to.equal('Chinese') - // expect(video.nsfw).to.be.ok - // expect(video.description).to.equal('my super description') - // expect(video.account.name).to.equal('root') - // expect(video.account.host).to.equal('localhost:9001') - // expect(video.isLocal).to.be.true - // expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ]) - // expect(dateIsValid(video.createdAt)).to.be.true - // expect(dateIsValid(video.updatedAt)).to.be.true - // - // const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath) - // expect(test).to.equal(true) - // }) - - it('Should not find a search by name', async function () { - const res = await searchVideo(server.url, 'hello') - - expect(res.body.total).to.equal(0) - expect(res.body.data).to.be.an('array') - expect(res.body.data.length).to.equal(0) - }) - - // Not implemented yet - // it('Should not find a search by author', async function () { - // const res = await searchVideo(server.url, 'hello') - // - // expect(res.body.total).to.equal(0) - // expect(res.body.data).to.be.an('array') - // expect(res.body.data.length).to.equal(0) - // }) - // - // Not implemented yet - // it('Should not find a search by tag', async function () { - // const res = await searchVideo(server.url, 'hello') - // - // expect(res.body.total).to.equal(0) - // expect(res.body.data).to.be.an('array') - // expect(res.body.data.length).to.equal(0) - // }) - it('Should remove the video', async function () { await removeVideo(server.url, server.accessToken, videoId) @@ -386,65 +318,6 @@ describe('Test a single server', function () { expect(videos[0].name).to.equal(videosListBase[5].name) }) - it('Should search the first video', async function () { - const res = await searchVideoWithPagination(server.url, 'webm', 0, 1, 'name') - - const videos = res.body.data - expect(res.body.total).to.equal(4) - expect(videos.length).to.equal(1) - expect(videos[0].name).to.equal('video_short1.webm name') - }) - - it('Should search the last two videos', async function () { - const res = await searchVideoWithPagination(server.url, 'webm', 2, 2, 'name') - - const videos = res.body.data - expect(res.body.total).to.equal(4) - expect(videos.length).to.equal(2) - expect(videos[0].name).to.equal('video_short3.webm name') - expect(videos[1].name).to.equal('video_short.webm name') - }) - - it('Should search all the webm videos', async function () { - const res = await searchVideoWithPagination(server.url, 'webm', 0, 15) - - const videos = res.body.data - expect(res.body.total).to.equal(4) - expect(videos.length).to.equal(4) - }) - - // Not implemented yet - // it('Should search all the root author videos', async function () { - // const res = await searchVideoWithPagination(server.url, 'root', 0, 15) - // - // const videos = res.body.data - // expect(res.body.total).to.equal(6) - // expect(videos.length).to.equal(6) - // }) - - // Not implemented yet - // it('Should search all the 9001 port videos', async function () { - // const res = await videosUtils.searchVideoWithPagination(server.url, '9001', 'host', 0, 15) - - // const videos = res.body.data - // expect(res.body.total).to.equal(6) - // expect(videos.length).to.equal(6) - - // done() - // }) - // }) - - // it('Should search all the localhost videos', async function () { - // const res = await videosUtils.searchVideoWithPagination(server.url, 'localhost', 'host', 0, 15) - - // const videos = res.body.data - // expect(res.body.total).to.equal(6) - // expect(videos.length).to.equal(6) - - // done() - // }) - // }) - it('Should list and sort by name in descending order', async function () { const res = await getVideosListSort(server.url, '-name') @@ -457,21 +330,8 @@ describe('Test a single server', function () { expect(videos[3].name).to.equal('video_short3.webm name') expect(videos[4].name).to.equal('video_short2.webm name') expect(videos[5].name).to.equal('video_short1.webm name') - }) - - it('Should search and sort by name in ascending order', async function () { - const res = await searchVideoWithSort(server.url, 'webm', 'name') - const videos = res.body.data - expect(res.body.total).to.equal(4) - expect(videos.length).to.equal(4) - - expect(videos[0].name).to.equal('video_short1.webm name') - expect(videos[1].name).to.equal('video_short2.webm name') - expect(videos[2].name).to.equal('video_short3.webm name') - expect(videos[3].name).to.equal('video_short.webm name') - - videoId = videos[2].id + videoId = videos[3].uuid }) it('Should update a video', async function () { @@ -488,6 +348,15 @@ describe('Test a single server', function () { await updateVideo(server.url, server.accessToken, videoId, attributes) }) + it('Should filter by tags and category', async function () { + const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 }) + expect(res1.body.total).to.equal(1) + expect(res1.body.data[0].name).to.equal('my super video updated') + + const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 }) + expect(res2.body.total).to.equal(0) + }) + it('Should have the video updated', async function () { this.timeout(60000) diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts index 6af0ca8af..38bdaa54e 100644 --- a/server/tests/api/videos/video-nsfw.ts +++ b/server/tests/api/videos/video-nsfw.ts @@ -30,7 +30,7 @@ describe('Test video NSFW policy', function () { let userAccessToken: string let customConfig: CustomConfig - function getVideosFunctions (token?: string) { + function getVideosFunctions (token?: string, query = {}) { return getMyUserInformation(server.url, server.accessToken) .then(res => { const user: User = res.body @@ -39,10 +39,10 @@ describe('Test video NSFW policy', function () { if (token) { return Promise.all([ - getVideosListWithToken(server.url, token), - searchVideoWithToken(server.url, 'n', token), - getAccountVideos(server.url, token, accountName, 0, 5), - getVideoChannelVideos(server.url, token, videoChannelUUID, 0, 5) + getVideosListWithToken(server.url, token, query), + searchVideoWithToken(server.url, 'n', token, query), + getAccountVideos(server.url, token, accountName, 0, 5, undefined, query), + getVideoChannelVideos(server.url, token, videoChannelUUID, 0, 5, undefined, query) ]) } @@ -200,6 +200,26 @@ describe('Test video NSFW policy', function () { expect(videos[ 0 ].name).to.equal('normal') expect(videos[ 1 ].name).to.equal('nsfw') }) + + it('Should display NSFW videos when the nsfw param === true', async function () { + for (const res of await getVideosFunctions(server.accessToken, { nsfw: true })) { + expect(res.body.total).to.equal(1) + + const videos = res.body.data + expect(videos).to.have.lengthOf(1) + expect(videos[ 0 ].name).to.equal('nsfw') + } + }) + + it('Should hide NSFW videos when the nsfw param === true', async function () { + for (const res of await getVideosFunctions(server.accessToken, { nsfw: false })) { + expect(res.body.total).to.equal(1) + + const videos = res.body.data + expect(videos).to.have.lengthOf(1) + expect(videos[ 0 ].name).to.equal('normal') + } + }) }) after(async function () { diff --git a/server/tests/utils/index.ts b/server/tests/utils/index.ts index 5b560ca39..391db18cf 100644 --- a/server/tests/utils/index.ts +++ b/server/tests/utils/index.ts @@ -14,3 +14,4 @@ export * from './videos/video-blacklist' export * from './videos/video-channels' export * from './videos/videos' export * from './feeds/feeds' +export * from './search/videos' diff --git a/server/tests/utils/requests/check-api-params.ts b/server/tests/utils/requests/check-api-params.ts index 7550eb3d8..edb47e0e9 100644 --- a/server/tests/utils/requests/check-api-params.ts +++ b/server/tests/utils/requests/check-api-params.ts @@ -1,31 +1,32 @@ import { makeGetRequest } from './requests' +import { immutableAssign } from '..' -function checkBadStartPagination (url: string, path: string, token?: string) { +function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { return makeGetRequest({ url, path, token, - query: { start: 'hello' }, + query: immutableAssign(query, { start: 'hello' }), statusCodeExpected: 400 }) } -function checkBadCountPagination (url: string, path: string, token?: string) { +function checkBadCountPagination (url: string, path: string, token?: string, query = {}) { return makeGetRequest({ url, path, token, - query: { count: 'hello' }, + query: immutableAssign(query, { count: 'hello' }), statusCodeExpected: 400 }) } -function checkBadSortPagination (url: string, path: string, token?: string) { +function checkBadSortPagination (url: string, path: string, token?: string, query = {}) { return makeGetRequest({ url, path, token, - query: { sort: 'hello' }, + query: immutableAssign(query, { sort: 'hello' }), statusCodeExpected: 400 }) } diff --git a/server/tests/utils/search/videos.ts b/server/tests/utils/search/videos.ts new file mode 100644 index 000000000..3a0c10e42 --- /dev/null +++ b/server/tests/utils/search/videos.ts @@ -0,0 +1,77 @@ +/* tslint:disable:no-unused-expression */ + +import * as request from 'supertest' +import { VideosSearchQuery } from '../../../../shared/models/search' +import { immutableAssign } from '..' + +function searchVideo (url: string, search: string) { + const path = '/api/v1/search/videos' + const req = request(url) + .get(path) + .query({ sort: '-publishedAt', search }) + .set('Accept', 'application/json') + + return req.expect(200) + .expect('Content-Type', /json/) +} + +function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) { + const path = '/api/v1/search/videos' + const req = request(url) + .get(path) + .set('Authorization', 'Bearer ' + token) + .query(immutableAssign(query, { sort: '-publishedAt', search })) + .set('Accept', 'application/json') + + return req.expect(200) + .expect('Content-Type', /json/) +} + +function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { + const path = '/api/v1/search/videos' + + const req = request(url) + .get(path) + .query({ start }) + .query({ search }) + .query({ count }) + + if (sort) req.query({ sort }) + + return req.set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) +} + +function searchVideoWithSort (url: string, search: string, sort: string) { + const path = '/api/v1/search/videos' + + return request(url) + .get(path) + .query({ search }) + .query({ sort }) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) +} + +function advancedVideosSearch (url: string, options: VideosSearchQuery) { + const path = '/api/v1/search/videos' + + return request(url) + .get(path) + .query(options) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) +} + +// --------------------------------------------------------------------------- + +export { + searchVideo, + advancedVideosSearch, + searchVideoWithToken, + searchVideoWithPagination, + searchVideoWithSort +} diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index a42d0f043..8c49eb02b 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts @@ -7,7 +7,7 @@ import { extname, join } from 'path' import * as request from 'supertest' import { buildAbsoluteFixturePath, - getMyUserInformation, + getMyUserInformation, immutableAssign, makeGetRequest, makePutBodyRequest, makeUploadRequest, @@ -133,13 +133,13 @@ function getVideosList (url: string) { .expect('Content-Type', /json/) } -function getVideosListWithToken (url: string, token: string) { +function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) { const path = '/api/v1/videos' return request(url) .get(path) .set('Authorization', 'Bearer ' + token) - .query({ sort: 'name' }) + .query(immutableAssign(query, { sort: 'name' })) .set('Accept', 'application/json') .expect(200) .expect('Content-Type', /json/) @@ -172,17 +172,25 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu .expect('Content-Type', /json/) } -function getAccountVideos (url: string, accessToken: string, accountName: string, start: number, count: number, sort?: string) { +function getAccountVideos ( + url: string, + accessToken: string, + accountName: string, + start: number, + count: number, + sort?: string, + query: { nsfw?: boolean } = {} +) { const path = '/api/v1/accounts/' + accountName + '/videos' return makeGetRequest({ url, path, - query: { + query: immutableAssign(query, { start, count, sort - }, + }), token: accessToken, statusCodeExpected: 200 }) @@ -194,18 +202,19 @@ function getVideoChannelVideos ( videoChannelId: number | string, start: number, count: number, - sort?: string + sort?: string, + query: { nsfw?: boolean } = {} ) { const path = '/api/v1/video-channels/' + videoChannelId + '/videos' return makeGetRequest({ url, path, - query: { + query: immutableAssign(query, { start, count, sort - }, + }), token: accessToken, statusCodeExpected: 200 }) @@ -237,65 +246,25 @@ function getVideosListSort (url: string, sort: string) { .expect('Content-Type', /json/) } -function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) { +function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) { const path = '/api/v1/videos' return request(url) - .delete(path + '/' + id) - .set('Accept', 'application/json') - .set('Authorization', 'Bearer ' + token) - .expect(expectedStatus) -} - -function searchVideo (url: string, search: string) { - const path = '/api/v1/search/videos' - const req = request(url) .get(path) - .query({ search }) + .query(query) .set('Accept', 'application/json') - - return req.expect(200) + .expect(200) .expect('Content-Type', /json/) } -function searchVideoWithToken (url: string, search: string, token: string) { +function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) { const path = '/api/v1/videos' - const req = request(url) - .get(path + '/search') - .set('Authorization', 'Bearer ' + token) - .query({ search }) - .set('Accept', 'application/json') - - return req.expect(200) - .expect('Content-Type', /json/) -} - -function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { - const path = '/api/v1/search/videos' - - const req = request(url) - .get(path) - .query({ start }) - .query({ search }) - .query({ count }) - - if (sort) req.query({ sort }) - - return req.set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) -} - -function searchVideoWithSort (url: string, search: string, sort: string) { - const path = '/api/v1/search/videos' return request(url) - .get(path) - .query({ search }) - .query({ sort }) + .delete(path + '/' + id) .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) + .set('Authorization', 'Bearer ' + token) + .expect(expectedStatus) } async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) { @@ -581,18 +550,15 @@ export { getMyVideos, getAccountVideos, getVideoChannelVideos, - searchVideoWithToken, getVideo, getVideoWithToken, getVideosList, getVideosListPagination, getVideosListSort, removeVideo, - searchVideo, - searchVideoWithPagination, - searchVideoWithSort, getVideosListWithToken, uploadVideo, + getVideosWithFilters, updateVideo, rateVideo, viewVideo, diff --git a/shared/models/index.ts b/shared/models/index.ts index c8ce71f17..1db00c295 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -4,6 +4,7 @@ export * from './users' export * from './videos' export * from './feeds' export * from './i18n' +export * from './search' export * from './server/job.model' export * from './oauth-client-local.model' export * from './result-list.model' diff --git a/shared/models/search/index.ts b/shared/models/search/index.ts new file mode 100644 index 000000000..288ee41ef --- /dev/null +++ b/shared/models/search/index.ts @@ -0,0 +1 @@ +export * from './videos-search-query.model' diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts new file mode 100644 index 000000000..bb23bd636 --- /dev/null +++ b/shared/models/search/videos-search-query.model.ts @@ -0,0 +1,24 @@ +export interface VideosSearchQuery { + search: string + + start?: number + count?: number + sort?: string + + startDate?: string // ISO 8601 + endDate?: string // ISO 8601 + + nsfw?: boolean + + categoryOneOf?: number[] + + licenceOneOf?: number[] + + languageOneOf?: string[] + + tagsOneOf?: string[] + tagsAllOf?: string[] + + durationMin?: number // seconds + durationMax?: number // seconds +} -- 2.25.1