From 3a4992633ee62d5edfbb484d9c6bcb3cf158489d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 31 Jul 2023 14:34:36 +0200 Subject: Migrate server to ESM Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports) --- packages/tests/src/api/check-params/abuses.ts | 438 ++++++++++ packages/tests/src/api/check-params/accounts.ts | 43 + packages/tests/src/api/check-params/blocklist.ts | 556 +++++++++++++ packages/tests/src/api/check-params/bulk.ts | 86 ++ .../src/api/check-params/channel-import-videos.ts | 209 +++++ packages/tests/src/api/check-params/config.ts | 428 ++++++++++ .../tests/src/api/check-params/contact-form.ts | 86 ++ .../tests/src/api/check-params/custom-pages.ts | 79 ++ packages/tests/src/api/check-params/debug.ts | 67 ++ packages/tests/src/api/check-params/follows.ts | 369 +++++++++ packages/tests/src/api/check-params/index.ts | 45 + packages/tests/src/api/check-params/jobs.ts | 125 +++ packages/tests/src/api/check-params/live.ts | 590 +++++++++++++ packages/tests/src/api/check-params/logs.ts | 163 ++++ packages/tests/src/api/check-params/metrics.ts | 214 +++++ packages/tests/src/api/check-params/my-user.ts | 492 +++++++++++ packages/tests/src/api/check-params/plugins.ts | 490 +++++++++++ packages/tests/src/api/check-params/redundancy.ts | 240 ++++++ .../tests/src/api/check-params/registrations.ts | 446 ++++++++++ packages/tests/src/api/check-params/runners.ts | 911 +++++++++++++++++++++ packages/tests/src/api/check-params/search.ts | 278 +++++++ packages/tests/src/api/check-params/services.ts | 207 +++++ packages/tests/src/api/check-params/transcoding.ts | 112 +++ packages/tests/src/api/check-params/two-factor.ts | 294 +++++++ .../tests/src/api/check-params/upload-quota.ts | 134 +++ .../src/api/check-params/user-notifications.ts | 290 +++++++ .../src/api/check-params/user-subscriptions.ts | 298 +++++++ packages/tests/src/api/check-params/users-admin.ts | 457 +++++++++++ .../tests/src/api/check-params/users-emails.ts | 122 +++ .../tests/src/api/check-params/video-blacklist.ts | 292 +++++++ .../tests/src/api/check-params/video-captions.ts | 307 +++++++ .../src/api/check-params/video-channel-syncs.ts | 319 ++++++++ .../tests/src/api/check-params/video-channels.ts | 379 +++++++++ .../tests/src/api/check-params/video-comments.ts | 484 +++++++++++ packages/tests/src/api/check-params/video-files.ts | 195 +++++ .../tests/src/api/check-params/video-imports.ts | 433 ++++++++++ .../tests/src/api/check-params/video-passwords.ts | 604 ++++++++++++++ .../tests/src/api/check-params/video-playlists.ts | 695 ++++++++++++++++ .../tests/src/api/check-params/video-source.ts | 154 ++++ .../src/api/check-params/video-storyboards.ts | 45 + .../tests/src/api/check-params/video-studio.ts | 392 +++++++++ packages/tests/src/api/check-params/video-token.ts | 70 ++ .../src/api/check-params/videos-common-filters.ts | 171 ++++ .../tests/src/api/check-params/videos-history.ts | 145 ++++ .../tests/src/api/check-params/videos-overviews.ts | 31 + packages/tests/src/api/check-params/videos.ts | 883 ++++++++++++++++++++ packages/tests/src/api/check-params/views.ts | 227 +++++ 47 files changed, 14095 insertions(+) create mode 100644 packages/tests/src/api/check-params/abuses.ts create mode 100644 packages/tests/src/api/check-params/accounts.ts create mode 100644 packages/tests/src/api/check-params/blocklist.ts create mode 100644 packages/tests/src/api/check-params/bulk.ts create mode 100644 packages/tests/src/api/check-params/channel-import-videos.ts create mode 100644 packages/tests/src/api/check-params/config.ts create mode 100644 packages/tests/src/api/check-params/contact-form.ts create mode 100644 packages/tests/src/api/check-params/custom-pages.ts create mode 100644 packages/tests/src/api/check-params/debug.ts create mode 100644 packages/tests/src/api/check-params/follows.ts create mode 100644 packages/tests/src/api/check-params/index.ts create mode 100644 packages/tests/src/api/check-params/jobs.ts create mode 100644 packages/tests/src/api/check-params/live.ts create mode 100644 packages/tests/src/api/check-params/logs.ts create mode 100644 packages/tests/src/api/check-params/metrics.ts create mode 100644 packages/tests/src/api/check-params/my-user.ts create mode 100644 packages/tests/src/api/check-params/plugins.ts create mode 100644 packages/tests/src/api/check-params/redundancy.ts create mode 100644 packages/tests/src/api/check-params/registrations.ts create mode 100644 packages/tests/src/api/check-params/runners.ts create mode 100644 packages/tests/src/api/check-params/search.ts create mode 100644 packages/tests/src/api/check-params/services.ts create mode 100644 packages/tests/src/api/check-params/transcoding.ts create mode 100644 packages/tests/src/api/check-params/two-factor.ts create mode 100644 packages/tests/src/api/check-params/upload-quota.ts create mode 100644 packages/tests/src/api/check-params/user-notifications.ts create mode 100644 packages/tests/src/api/check-params/user-subscriptions.ts create mode 100644 packages/tests/src/api/check-params/users-admin.ts create mode 100644 packages/tests/src/api/check-params/users-emails.ts create mode 100644 packages/tests/src/api/check-params/video-blacklist.ts create mode 100644 packages/tests/src/api/check-params/video-captions.ts create mode 100644 packages/tests/src/api/check-params/video-channel-syncs.ts create mode 100644 packages/tests/src/api/check-params/video-channels.ts create mode 100644 packages/tests/src/api/check-params/video-comments.ts create mode 100644 packages/tests/src/api/check-params/video-files.ts create mode 100644 packages/tests/src/api/check-params/video-imports.ts create mode 100644 packages/tests/src/api/check-params/video-passwords.ts create mode 100644 packages/tests/src/api/check-params/video-playlists.ts create mode 100644 packages/tests/src/api/check-params/video-source.ts create mode 100644 packages/tests/src/api/check-params/video-storyboards.ts create mode 100644 packages/tests/src/api/check-params/video-studio.ts create mode 100644 packages/tests/src/api/check-params/video-token.ts create mode 100644 packages/tests/src/api/check-params/videos-common-filters.ts create mode 100644 packages/tests/src/api/check-params/videos-history.ts create mode 100644 packages/tests/src/api/check-params/videos-overviews.ts create mode 100644 packages/tests/src/api/check-params/videos.ts create mode 100644 packages/tests/src/api/check-params/views.ts (limited to 'packages/tests/src/api/check-params') diff --git a/packages/tests/src/api/check-params/abuses.ts b/packages/tests/src/api/check-params/abuses.ts new file mode 100644 index 000000000..1effc82b1 --- /dev/null +++ b/packages/tests/src/api/check-params/abuses.ts @@ -0,0 +1,438 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { AbuseCreate, AbuseState, HttpStatusCode } from '@peertube/peertube-models' +import { + AbusesCommand, + cleanupTests, + createSingleServer, + doubleFollow, + makeGetRequest, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test abuses API validators', function () { + const basePath = '/api/v1/abuses/' + + let server: PeerTubeServer + + let userToken = '' + let userToken2 = '' + let abuseId: number + let messageId: number + + let command: AbusesCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + userToken = await server.users.generateUserAndToken('user_1') + userToken2 = await server.users.generateUserAndToken('user_2') + + server.store.videoCreated = await server.videos.upload() + + command = server.abuses + }) + + describe('When listing abuses for admins', function () { + const path = basePath + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a bad id filter', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } }) + }) + + it('Should fail with a bad filter', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'toto' } }) + await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { filter: 'videos' } }) + }) + + it('Should fail with bad predefined reason', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { predefinedReason: 'violentOrRepulsives' } }) + }) + + it('Should fail with a bad state filter', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } }) + await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 0 } }) + }) + + it('Should fail with a bad videoIs filter', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } }) + }) + + it('Should succeed with the correct params', async function () { + const query = { + id: 13, + predefinedReason: 'violentOrRepulsive', + filter: 'comment', + state: 2, + videoIs: 'deleted' + } + + await makeGetRequest({ url: server.url, path, token: server.accessToken, query, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When listing abuses for users', function () { + const path = '/api/v1/users/me/abuses' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, userToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, userToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, userToken) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a bad id filter', async function () { + await makeGetRequest({ url: server.url, path, token: userToken, query: { id: 'toto' } }) + }) + + it('Should fail with a bad state filter', async function () { + await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 'toto' } }) + await makeGetRequest({ url: server.url, path, token: userToken, query: { state: 0 } }) + }) + + it('Should succeed with the correct params', async function () { + const query = { + id: 13, + state: 2 + } + + await makeGetRequest({ url: server.url, path, token: userToken, query, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When reporting an abuse', function () { + const path = basePath + + it('Should fail with nothing', async function () { + const fields = {} + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should fail with a wrong video', async function () { + const fields = { video: { id: 'blabla' }, reason: 'my super reason' } + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should fail with an unknown video', async function () { + const fields = { video: { id: 42 }, reason: 'my super reason' } + await makePostBodyRequest({ + url: server.url, + path, + token: userToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a wrong comment', async function () { + const fields = { comment: { id: 'blabla' }, reason: 'my super reason' } + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should fail with an unknown comment', async function () { + const fields = { comment: { id: 42 }, reason: 'my super reason' } + await makePostBodyRequest({ + url: server.url, + path, + token: userToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a wrong account', async function () { + const fields = { account: { id: 'blabla' }, reason: 'my super reason' } + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should fail with an unknown account', async function () { + const fields = { account: { id: 42 }, reason: 'my super reason' } + await makePostBodyRequest({ + url: server.url, + path, + token: userToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with not account, comment or video', async function () { + const fields = { reason: 'my super reason' } + await makePostBodyRequest({ + url: server.url, + path, + token: userToken, + fields, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a non authenticated user', async function () { + const fields = { video: { id: server.store.videoCreated.id }, reason: 'my super reason' } + + await makePostBodyRequest({ url: server.url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a reason too short', async function () { + const fields = { video: { id: server.store.videoCreated.id }, reason: 'h' } + + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should fail with a too big reason', async function () { + const fields = { video: { id: server.store.videoCreated.id }, reason: 'super'.repeat(605) } + + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should succeed with the correct parameters (basic)', async function () { + const fields: AbuseCreate = { video: { id: server.store.videoCreated.shortUUID }, reason: 'my super reason' } + + const res = await makePostBodyRequest({ + url: server.url, + path, + token: userToken, + fields, + expectedStatus: HttpStatusCode.OK_200 + }) + abuseId = res.body.abuse.id + }) + + it('Should fail with a wrong predefined reason', async function () { + const fields = { video: server.store.videoCreated, reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] } + + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should fail with negative timestamps', async function () { + const fields = { video: { id: server.store.videoCreated.id, startAt: -1 }, reason: 'my super reason' } + + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should fail mith misordered startAt/endAt', async function () { + const fields = { video: { id: server.store.videoCreated.id, startAt: 5, endAt: 1 }, reason: 'my super reason' } + + await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) + }) + + it('Should succeed with the correct parameters (advanced)', async function () { + const fields: AbuseCreate = { + video: { + id: server.store.videoCreated.id, + startAt: 1, + endAt: 5 + }, + reason: 'my super reason', + predefinedReasons: [ 'serverRules' ] + } + + await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When updating an abuse', function () { + + it('Should fail with a non authenticated user', async function () { + await command.update({ token: 'blabla', abuseId, body: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a non admin user', async function () { + await command.update({ token: userToken, abuseId, body: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad abuse id', async function () { + await command.update({ abuseId: 45, body: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a bad state', async function () { + const body = { state: 5 as any } + await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a bad moderation comment', async function () { + const body = { moderationComment: 'b'.repeat(3001) } + await command.update({ abuseId, body, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct params', async function () { + const body = { state: AbuseState.ACCEPTED } + await command.update({ abuseId, body }) + }) + }) + + describe('When creating an abuse message', function () { + const message = 'my super message' + + it('Should fail with an invalid abuse id', async function () { + await command.addMessage({ token: userToken2, abuseId: 888, message, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a non authenticated user', async function () { + await command.addMessage({ token: 'fake_token', abuseId, message, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with an invalid logged in user', async function () { + await command.addMessage({ token: userToken2, abuseId, message, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with an invalid message', async function () { + await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct params', async function () { + const res = await command.addMessage({ token: userToken, abuseId, message }) + messageId = res.body.abuseMessage.id + }) + }) + + describe('When listing abuse messages', function () { + + it('Should fail with an invalid abuse id', async function () { + await command.listMessages({ token: userToken, abuseId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a non authenticated user', async function () { + await command.listMessages({ token: 'fake_token', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with an invalid logged in user', async function () { + await command.listMessages({ token: userToken2, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct params', async function () { + await command.listMessages({ token: userToken, abuseId }) + }) + }) + + describe('When deleting an abuse message', function () { + it('Should fail with an invalid abuse id', async function () { + await command.deleteMessage({ token: userToken, abuseId: 888, messageId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with an invalid message id', async function () { + await command.deleteMessage({ token: userToken, abuseId, messageId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a non authenticated user', async function () { + await command.deleteMessage({ token: 'fake_token', abuseId, messageId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with an invalid logged in user', async function () { + await command.deleteMessage({ token: userToken2, abuseId, messageId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct params', async function () { + await command.deleteMessage({ token: userToken, abuseId, messageId }) + }) + }) + + describe('When deleting a video abuse', function () { + + it('Should fail with a non authenticated user', async function () { + await command.delete({ token: 'blabla', abuseId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a non admin user', async function () { + await command.delete({ token: userToken, abuseId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad abuse id', async function () { + await command.delete({ abuseId: 45, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct params', async function () { + await command.delete({ abuseId }) + }) + }) + + describe('When trying to manage messages of a remote abuse', function () { + let remoteAbuseId: number + let anotherServer: PeerTubeServer + + before(async function () { + this.timeout(50000) + + anotherServer = await createSingleServer(2) + await setAccessTokensToServers([ anotherServer ]) + + await doubleFollow(anotherServer, server) + + const server2VideoId = await anotherServer.videos.getId({ uuid: server.store.videoCreated.uuid }) + await anotherServer.abuses.report({ reason: 'remote server', videoId: server2VideoId }) + + await waitJobs([ server, anotherServer ]) + + const body = await command.getAdminList({ sort: '-createdAt' }) + remoteAbuseId = body.data[0].id + }) + + it('Should fail when listing abuse messages of a remote abuse', async function () { + await command.listMessages({ abuseId: remoteAbuseId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail when creating abuse message of a remote abuse', async function () { + await command.addMessage({ abuseId: remoteAbuseId, message: 'message', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + after(async function () { + await cleanupTests([ anotherServer ]) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/accounts.ts b/packages/tests/src/api/check-params/accounts.ts new file mode 100644 index 000000000..87810bbd3 --- /dev/null +++ b/packages/tests/src/api/check-params/accounts.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode } from '@peertube/peertube-models' +import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' + +describe('Test accounts API validators', function () { + const path = '/api/v1/accounts/' + let server: PeerTubeServer + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + }) + + describe('When listing accounts', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + }) + + describe('When getting an account', function () { + + it('Should return 404 with a non existing name', async function () { + await server.accounts.get({ accountName: 'arfaze', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/blocklist.ts b/packages/tests/src/api/check-params/blocklist.ts new file mode 100644 index 000000000..fcd6d08f8 --- /dev/null +++ b/packages/tests/src/api/check-params/blocklist.ts @@ -0,0 +1,556 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + makeDeleteRequest, + makeGetRequest, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test blocklist API validators', function () { + let servers: PeerTubeServer[] + let server: PeerTubeServer + let userAccessToken: string + + before(async function () { + this.timeout(60000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + + server = servers[0] + + const user = { username: 'user1', password: 'password' } + await server.users.create({ username: user.username, password: user.password }) + + userAccessToken = await server.login.getAccessToken(user) + + await doubleFollow(servers[0], servers[1]) + }) + + // --------------------------------------------------------------- + + describe('When managing user blocklist', function () { + + describe('When managing user accounts blocklist', function () { + const path = '/api/v1/users/me/blocklist/accounts' + + describe('When listing blocked accounts', function () { + it('Should fail with an unauthenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + }) + + describe('When blocking an account', function () { + it('Should fail with an unauthenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { accountName: 'user1' }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with an unknown account', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'user2' }, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail to block ourselves', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'root' }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'user1' }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When unblocking an account', function () { + it('Should fail with an unauthenticated user', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user1', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with an unknown account block', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user2', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user1', + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + }) + + describe('When managing user servers blocklist', function () { + const path = '/api/v1/users/me/blocklist/servers' + + describe('When listing blocked servers', function () { + it('Should fail with an unauthenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + }) + + describe('When blocking a server', function () { + it('Should fail with an unauthenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { host: '127.0.0.1:9002' }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with an unknown server', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { host: '127.0.0.1:9003' }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should fail with our own server', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { host: server.host }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { host: servers[1].host }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When unblocking a server', function () { + it('Should fail with an unauthenticated user', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/' + servers[1].host, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with an unknown server block', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/127.0.0.1:9004', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/' + servers[1].host, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + }) + }) + + describe('When managing server blocklist', function () { + + describe('When managing server accounts blocklist', function () { + const path = '/api/v1/server/blocklist/accounts' + + describe('When listing blocked accounts', function () { + it('Should fail with an unauthenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a user without the appropriate rights', async function () { + await makeGetRequest({ + url: server.url, + token: userAccessToken, + path, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + }) + + describe('When blocking an account', function () { + it('Should fail with an unauthenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { accountName: 'user1' }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a user without the appropriate rights', async function () { + await makePostBodyRequest({ + url: server.url, + token: userAccessToken, + path, + fields: { accountName: 'user1' }, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an unknown account', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'user2' }, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail to block ourselves', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'root' }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'user1' }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When unblocking an account', function () { + it('Should fail with an unauthenticated user', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user1', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a user without the appropriate rights', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user1', + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an unknown account block', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user2', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user1', + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + }) + + describe('When managing server servers blocklist', function () { + const path = '/api/v1/server/blocklist/servers' + + describe('When listing blocked servers', function () { + it('Should fail with an unauthenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a user without the appropriate rights', async function () { + await makeGetRequest({ + url: server.url, + token: userAccessToken, + path, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + }) + + describe('When blocking a server', function () { + it('Should fail with an unauthenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { host: servers[1].host }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a user without the appropriate rights', async function () { + await makePostBodyRequest({ + url: server.url, + token: userAccessToken, + path, + fields: { host: servers[1].host }, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with an unknown server', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { host: '127.0.0.1:9003' }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should fail with our own server', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { host: server.host }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { host: servers[1].host }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When unblocking a server', function () { + it('Should fail with an unauthenticated user', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/' + servers[1].host, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a user without the appropriate rights', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/' + servers[1].host, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an unknown server block', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/127.0.0.1:9004', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/' + servers[1].host, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + }) + }) + + describe('When getting blocklist status', function () { + const path = '/api/v1/blocklist/status' + + it('Should fail with a bad token', async function () { + await makeGetRequest({ + url: server.url, + path, + token: 'false', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a bad accounts field', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + accounts: 1 + }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeGetRequest({ + url: server.url, + path, + query: { + accounts: [ 1 ] + }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad hosts field', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + hosts: 1 + }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeGetRequest({ + url: server.url, + path, + query: { + hosts: [ 1 ] + }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path, + query: {}, + expectedStatus: HttpStatusCode.OK_200 + }) + + await makeGetRequest({ + url: server.url, + path, + query: { + hosts: [ 'example.com' ], + accounts: [ 'john@example.com' ] + }, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/api/check-params/bulk.ts b/packages/tests/src/api/check-params/bulk.ts new file mode 100644 index 000000000..def0c38eb --- /dev/null +++ b/packages/tests/src/api/check-params/bulk.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test bulk API validators', function () { + let server: PeerTubeServer + let userAccessToken: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + const user = { username: 'user1', password: 'password' } + await server.users.create({ username: user.username, password: user.password }) + + userAccessToken = await server.login.getAccessToken(user) + }) + + describe('When removing comments of', function () { + const path = '/api/v1/bulk/remove-comments-of' + + it('Should fail with an unauthenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { accountName: 'user1', scope: 'my-videos' }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with an unknown account', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'user2', scope: 'my-videos' }, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with an invalid scope', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'user1', scope: 'my-videoss' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail to delete comments of the instance without the appropriate rights', async function () { + await makePostBodyRequest({ + url: server.url, + token: userAccessToken, + path, + fields: { accountName: 'user1', scope: 'instance' }, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path, + fields: { accountName: 'user1', scope: 'instance' }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/channel-import-videos.ts b/packages/tests/src/api/check-params/channel-import-videos.ts new file mode 100644 index 000000000..0e897dad7 --- /dev/null +++ b/packages/tests/src/api/check-params/channel-import-videos.ts @@ -0,0 +1,209 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { FIXTURE_URLS } from '@tests/shared/tests.js' +import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + ChannelsCommand, + cleanupTests, + createSingleServer, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel +} from '@peertube/peertube-server-commands' + +describe('Test videos import in a channel API validator', function () { + let server: PeerTubeServer + const userInfo = { + accessToken: '', + channelName: 'fake_channel', + channelId: -1, + id: -1, + videoQuota: -1, + videoQuotaDaily: -1, + channelSyncId: -1 + } + let command: ChannelsCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + await server.config.enableImports() + await server.config.enableChannelSync() + + const userCreds = { + username: 'fake', + password: 'fake_password' + } + + { + const user = await server.users.create({ username: userCreds.username, password: userCreds.password }) + userInfo.id = user.id + userInfo.accessToken = await server.login.getAccessToken(userCreds) + + const info = await server.users.getMyInfo({ token: userInfo.accessToken }) + userInfo.channelId = info.videoChannels[0].id + } + + { + const { videoChannelSync } = await server.channelSyncs.create({ + token: userInfo.accessToken, + attributes: { + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + videoChannelId: userInfo.channelId + } + }) + userInfo.channelSyncId = videoChannelSync.id + } + + command = server.channels + }) + + it('Should fail when HTTP upload is disabled', async function () { + await server.config.disableChannelSync() + await server.config.disableImports() + + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + token: server.accessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + + await server.config.enableImports() + }) + + it('Should fail when externalChannelUrl is not provided', async function () { + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: null, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail when externalChannelUrl is malformed', async function () { + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: 'not-a-url', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad sync id', async function () { + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + videoChannelSyncId: 'toto' as any, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a unknown sync id', async function () { + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + videoChannelSyncId: 42, + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a sync id of another channel', async function () { + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + videoChannelSyncId: userInfo.channelSyncId, + token: server.accessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with no authentication', async function () { + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail when sync is not owned by the user', async function () { + await command.importVideos({ + channelName: server.store.channel.name, + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + token: userInfo.accessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail when the user has no quota', async function () { + await server.users.update({ + userId: userInfo.id, + videoQuota: 0 + }) + + await command.importVideos({ + channelName: 'fake_channel', + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + token: userInfo.accessToken, + expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413 + }) + + await server.users.update({ + userId: userInfo.id, + videoQuota: userInfo.videoQuota + }) + }) + + it('Should fail when the user has no daily quota', async function () { + await server.users.update({ + userId: userInfo.id, + videoQuotaDaily: 0 + }) + + await command.importVideos({ + channelName: 'fake_channel', + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + token: userInfo.accessToken, + expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413 + }) + + await server.users.update({ + userId: userInfo.id, + videoQuotaDaily: userInfo.videoQuotaDaily + }) + }) + + it('Should succeed when sync is run by its owner', async function () { + if (!areHttpImportTestsDisabled()) return + + await command.importVideos({ + channelName: 'fake_channel', + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + token: userInfo.accessToken + }) + }) + + it('Should succeed when sync is run with root and for another user\'s channel', async function () { + if (!areHttpImportTestsDisabled()) return + + await command.importVideos({ + channelName: 'fake_channel', + externalChannelUrl: FIXTURE_URLS.youtubeChannel + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/config.ts b/packages/tests/src/api/check-params/config.ts new file mode 100644 index 000000000..8179a8815 --- /dev/null +++ b/packages/tests/src/api/check-params/config.ts @@ -0,0 +1,428 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ +import merge from 'lodash-es/merge.js' +import { omit } from '@peertube/peertube-core-utils' +import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeDeleteRequest, + makeGetRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test config API validators', function () { + const path = '/api/v1/config/custom' + let server: PeerTubeServer + let userAccessToken: string + const updateParams: CustomConfig = { + instance: { + name: 'PeerTube updated', + shortDescription: 'my short description', + description: 'my super description', + terms: 'my super terms', + codeOfConduct: 'my super coc', + + creationReason: 'my super reason', + moderationInformation: 'my super moderation information', + administrator: 'Kuja', + maintenanceLifetime: 'forever', + businessModel: 'my super business model', + hardwareInformation: '2vCore 3GB RAM', + + languages: [ 'en', 'es' ], + categories: [ 1, 2 ], + + isNSFW: true, + defaultNSFWPolicy: 'blur', + + defaultClientRoute: '/videos/recently-added', + + customizations: { + javascript: 'alert("coucou")', + css: 'body { background-color: red; }' + } + }, + theme: { + default: 'default' + }, + services: { + twitter: { + username: '@MySuperUsername', + whitelisted: true + } + }, + client: { + videos: { + miniature: { + preferAuthorDisplayName: false + } + }, + menu: { + login: { + redirectOnSingleExternalAuth: false + } + } + }, + cache: { + previews: { + size: 2 + }, + captions: { + size: 3 + }, + torrents: { + size: 4 + }, + storyboards: { + size: 5 + } + }, + signup: { + enabled: false, + limit: 5, + requiresApproval: false, + requiresEmailVerification: false, + minimumAge: 16 + }, + admin: { + email: 'superadmin1@example.com' + }, + contactForm: { + enabled: false + }, + user: { + history: { + videos: { + enabled: true + } + }, + videoQuota: 5242881, + videoQuotaDaily: 318742 + }, + videoChannels: { + maxPerUser: 20 + }, + transcoding: { + enabled: true, + remoteRunners: { + enabled: true + }, + allowAdditionalExtensions: true, + allowAudioFiles: true, + concurrency: 1, + threads: 1, + profile: 'vod_profile', + resolutions: { + '0p': false, + '144p': false, + '240p': false, + '360p': true, + '480p': true, + '720p': false, + '1080p': false, + '1440p': false, + '2160p': false + }, + alwaysTranscodeOriginalResolution: false, + webVideos: { + enabled: true + }, + hls: { + enabled: false + } + }, + live: { + enabled: true, + + allowReplay: false, + latencySetting: { + enabled: false + }, + maxDuration: 30, + maxInstanceLives: -1, + maxUserLives: 50, + + transcoding: { + enabled: true, + remoteRunners: { + enabled: true + }, + threads: 4, + profile: 'live_profile', + resolutions: { + '144p': true, + '240p': true, + '360p': true, + '480p': true, + '720p': true, + '1080p': true, + '1440p': true, + '2160p': true + }, + alwaysTranscodeOriginalResolution: false + } + }, + videoStudio: { + enabled: true, + remoteRunners: { + enabled: true + } + }, + videoFile: { + update: { + enabled: true + } + }, + import: { + videos: { + concurrency: 1, + http: { + enabled: false + }, + torrent: { + enabled: false + } + }, + videoChannelSynchronization: { + enabled: false, + maxPerUser: 10 + } + }, + trending: { + videos: { + algorithms: { + enabled: [ 'hot', 'most-viewed', 'most-liked' ], + default: 'most-viewed' + } + } + }, + autoBlacklist: { + videos: { + ofUsers: { + enabled: false + } + } + }, + followers: { + instance: { + enabled: false, + manualApproval: true + } + }, + followings: { + instance: { + autoFollowBack: { + enabled: true + }, + autoFollowIndex: { + enabled: true, + indexUrl: 'https://index.example.com' + } + } + }, + broadcastMessage: { + enabled: true, + dismissable: true, + message: 'super message', + level: 'warning' + }, + search: { + remoteUri: { + users: true, + anonymous: true + }, + searchIndex: { + enabled: true, + url: 'https://search.joinpeertube.org', + disableLocalSearch: true, + isDefaultSearch: true + } + } + } + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const user = { + username: 'user1', + password: 'password' + } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + }) + + describe('When getting the configuration', function () { + it('Should fail without token', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + describe('When updating the configuration', function () { + it('Should fail without token', async function () { + await makePutBodyRequest({ + url: server.url, + path, + fields: updateParams, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makePutBodyRequest({ + url: server.url, + path, + fields: updateParams, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail if it misses a key', async function () { + const newUpdateParams = { ...updateParams, admin: omit(updateParams.admin, [ 'email' ]) } + + await makePutBodyRequest({ + url: server.url, + path, + fields: newUpdateParams, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad default NSFW policy', async function () { + const newUpdateParams = { + ...updateParams, + + instance: { + defaultNSFWPolicy: 'hello' + } + } + + await makePutBodyRequest({ + url: server.url, + path, + fields: newUpdateParams, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if email disabled and signup requires email verification', async function () { + // opposite scenario - success when enable enabled - covered via tests/api/users/user-verification.ts + const newUpdateParams = { + ...updateParams, + + signup: { + enabled: true, + limit: 5, + requiresApproval: true, + requiresEmailVerification: true + } + } + + await makePutBodyRequest({ + url: server.url, + path, + fields: newUpdateParams, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a disabled web videos & hls transcoding', async function () { + const newUpdateParams = { + ...updateParams, + + transcoding: { + hls: { + enabled: false + }, + web_videos: { + enabled: false + } + } + } + + await makePutBodyRequest({ + url: server.url, + path, + fields: newUpdateParams, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a disabled http upload & enabled sync', async function () { + const newUpdateParams: CustomConfig = merge({}, updateParams, { + import: { + videos: { + http: { enabled: false } + }, + videoChannelSynchronization: { enabled: true } + } + }) + + await makePutBodyRequest({ + url: server.url, + path, + fields: newUpdateParams, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePutBodyRequest({ + url: server.url, + path, + fields: updateParams, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When deleting the configuration', function () { + it('Should fail without token', async function () { + await makeDeleteRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeDeleteRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/contact-form.ts b/packages/tests/src/api/check-params/contact-form.ts new file mode 100644 index 000000000..009cb2ad9 --- /dev/null +++ b/packages/tests/src/api/check-params/contact-form.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + ConfigCommand, + ContactFormCommand, + createSingleServer, + killallServers, + PeerTubeServer +} from '@peertube/peertube-server-commands' + +describe('Test contact form API validators', function () { + let server: PeerTubeServer + const emails: object[] = [] + const defaultBody = { + fromName: 'super name', + fromEmail: 'toto@example.com', + subject: 'my subject', + body: 'Hello, how are you?' + } + let emailPort: number + let command: ContactFormCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(60000) + + emailPort = await MockSmtpServer.Instance.collectEmails(emails) + + // Email is disabled + server = await createSingleServer(1) + command = server.contactForm + }) + + it('Should not accept a contact form if emails are disabled', async function () { + await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 }) + }) + + it('Should not accept a contact form if it is disabled in the configuration', async function () { + this.timeout(25000) + + await killallServers([ server ]) + + // Contact form is disabled + await server.run({ ...ConfigCommand.getEmailOverrideConfig(emailPort), contact_form: { enabled: false } }) + await command.send({ ...defaultBody, expectedStatus: HttpStatusCode.CONFLICT_409 }) + }) + + it('Should not accept a contact form if from email is invalid', async function () { + this.timeout(25000) + + await killallServers([ server ]) + + // Email & contact form enabled + await server.run(ConfigCommand.getEmailOverrideConfig(emailPort)) + + await command.send({ ...defaultBody, fromEmail: 'badEmail', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.send({ ...defaultBody, fromEmail: 'badEmail@', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.send({ ...defaultBody, fromEmail: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should not accept a contact form if from name is invalid', async function () { + await command.send({ ...defaultBody, fromName: 'name'.repeat(100), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.send({ ...defaultBody, fromName: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.send({ ...defaultBody, fromName: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should not accept a contact form if body is invalid', async function () { + await command.send({ ...defaultBody, body: 'body'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.send({ ...defaultBody, body: 'a', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.send({ ...defaultBody, body: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should accept a contact form with the correct parameters', async function () { + await command.send(defaultBody) + }) + + after(async function () { + MockSmtpServer.Instance.kill() + + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/custom-pages.ts b/packages/tests/src/api/check-params/custom-pages.ts new file mode 100644 index 000000000..180a5e406 --- /dev/null +++ b/packages/tests/src/api/check-params/custom-pages.ts @@ -0,0 +1,79 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test custom pages validators', function () { + const path = '/api/v1/custom-pages/homepage/instance' + + let server: PeerTubeServer + let userAccessToken: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + const user = { username: 'user1', password: 'password' } + await server.users.create({ username: user.username, password: user.password }) + + userAccessToken = await server.login.getAccessToken(user) + }) + + describe('When updating instance homepage', function () { + + it('Should fail with an unauthenticated user', async function () { + await makePutBodyRequest({ + url: server.url, + path, + fields: { content: 'super content' }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makePutBodyRequest({ + url: server.url, + path, + token: userAccessToken, + fields: { content: 'super content' }, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePutBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: { content: 'super content' }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When getting instance homapage', function () { + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/debug.ts b/packages/tests/src/api/check-params/debug.ts new file mode 100644 index 000000000..4a7c18a62 --- /dev/null +++ b/packages/tests/src/api/check-params/debug.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test debug API validators', function () { + const path = '/api/v1/server/debug' + let server: PeerTubeServer + let userAccessToken = '' + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const user = { + username: 'user1', + password: 'my super password' + } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + }) + + describe('When getting debug endpoint', function () { + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: { startDate: new Date().toISOString() }, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/follows.ts b/packages/tests/src/api/check-params/follows.ts new file mode 100644 index 000000000..e92a3acd6 --- /dev/null +++ b/packages/tests/src/api/check-params/follows.ts @@ -0,0 +1,369 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeDeleteRequest, + makeGetRequest, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test server follows API validators', function () { + let server: PeerTubeServer + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + }) + + describe('When managing following', function () { + let userAccessToken = null + + before(async function () { + userAccessToken = await server.users.generateUserAndToken('user1') + }) + + describe('When adding follows', function () { + const path = '/api/v1/server/following' + + it('Should fail with nothing', async function () { + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if hosts is not composed by hosts', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { hosts: [ '127.0.0.1:9002', '127.0.0.1:coucou' ] }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if hosts is composed with http schemes', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { hosts: [ '127.0.0.1:9002', 'http://127.0.0.1:9003' ] }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if hosts are not unique', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { urls: [ '127.0.0.1:9002', '127.0.0.1:9002' ] }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if handles is not composed by handles', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { handles: [ 'hello@example.com', '127.0.0.1:9001' ] }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if handles are not unique', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { urls: [ 'hello@example.com', 'hello@example.com' ] }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an invalid token', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { hosts: [ '127.0.0.1:9002' ] }, + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { hosts: [ '127.0.0.1:9002' ] }, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + describe('When listing followings', function () { + const path = '/api/v1/server/following' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path) + }) + + it('Should fail with an incorrect state', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + state: 'blabla' + } + }) + }) + + it('Should fail with an incorrect actor type', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + actorType: 'blabla' + } + }) + }) + + it('Should fail succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.OK_200, + query: { + state: 'accepted', + actorType: 'Application' + } + }) + }) + }) + + describe('When listing followers', function () { + const path = '/api/v1/server/followers' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path) + }) + + it('Should fail with an incorrect actor type', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + actorType: 'blabla' + } + }) + }) + + it('Should fail with an incorrect state', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + state: 'blabla', + actorType: 'Application' + } + }) + }) + + it('Should fail succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.OK_200, + query: { + state: 'accepted' + } + }) + }) + }) + + describe('When removing a follower', function () { + const path = '/api/v1/server/followers' + + it('Should fail with an invalid token', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9002', + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9002', + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid follower', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/toto', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown follower', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9003', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + }) + + describe('When accepting a follower', function () { + const path = '/api/v1/server/followers' + + it('Should fail with an invalid token', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9002/accept', + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9002/accept', + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid follower', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto/accept', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown follower', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9003/accept', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + }) + + describe('When rejecting a follower', function () { + const path = '/api/v1/server/followers' + + it('Should fail with an invalid token', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9002/reject', + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9002/reject', + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid follower', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto/reject', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown follower', async function () { + await makePostBodyRequest({ + url: server.url, + path: path + '/toto@127.0.0.1:9003/reject', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + }) + + describe('When removing following', function () { + const path = '/api/v1/server/following' + + it('Should fail with an invalid token', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/127.0.0.1:9002', + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/127.0.0.1:9002', + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail if we do not follow this server', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/example.com', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/index.ts b/packages/tests/src/api/check-params/index.ts new file mode 100644 index 000000000..ed5fe6b06 --- /dev/null +++ b/packages/tests/src/api/check-params/index.ts @@ -0,0 +1,45 @@ +import './abuses.js' +import './accounts.js' +import './blocklist.js' +import './bulk.js' +import './channel-import-videos.js' +import './config.js' +import './contact-form.js' +import './custom-pages.js' +import './debug.js' +import './follows.js' +import './jobs.js' +import './live.js' +import './logs.js' +import './metrics.js' +import './my-user.js' +import './plugins.js' +import './redundancy.js' +import './registrations.js' +import './runners.js' +import './search.js' +import './services.js' +import './transcoding.js' +import './two-factor.js' +import './upload-quota.js' +import './user-notifications.js' +import './user-subscriptions.js' +import './users-admin.js' +import './users-emails.js' +import './video-blacklist.js' +import './video-captions.js' +import './video-channel-syncs.js' +import './video-channels.js' +import './video-comments.js' +import './video-files.js' +import './video-imports.js' +import './video-playlists.js' +import './video-storyboards.js' +import './video-source.js' +import './video-studio.js' +import './video-token.js' +import './videos-common-filters.js' +import './videos-history.js' +import './videos-overviews.js' +import './videos.js' +import './views.js' diff --git a/packages/tests/src/api/check-params/jobs.ts b/packages/tests/src/api/check-params/jobs.ts new file mode 100644 index 000000000..331d58c6a --- /dev/null +++ b/packages/tests/src/api/check-params/jobs.ts @@ -0,0 +1,125 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test jobs API validators', function () { + const path = '/api/v1/jobs/failed' + let server: PeerTubeServer + let userAccessToken = '' + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const user = { + username: 'user1', + password: 'my super password' + } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + }) + + describe('When listing jobs', function () { + + it('Should fail with a bad state', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path: path + 'ade' + }) + }) + + it('Should fail with an incorrect job type', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path, + query: { + jobType: 'toto' + } + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + describe('When pausing/resuming the job queue', async function () { + const commands = [ 'pause', 'resume' ] + + it('Should fail with a non authenticated user', async function () { + for (const command of commands) { + await makePostBodyRequest({ + url: server.url, + path: '/api/v1/jobs/' + command, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + } + }) + + it('Should fail with a non admin user', async function () { + for (const command of commands) { + await makePostBodyRequest({ + url: server.url, + path: '/api/v1/jobs/' + command, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + } + }) + + it('Should succeed with the correct params', async function () { + for (const command of commands) { + await makePostBodyRequest({ + url: server.url, + path: '/api/v1/jobs/' + command, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/live.ts b/packages/tests/src/api/check-params/live.ts new file mode 100644 index 000000000..5900823ea --- /dev/null +++ b/packages/tests/src/api/check-params/live.ts @@ -0,0 +1,590 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { omit } from '@peertube/peertube-core-utils' +import { HttpStatusCode, LiveVideoLatencyMode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' +import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { + cleanupTests, + createSingleServer, + LiveCommand, + makePostBodyRequest, + makeUploadRequest, + PeerTubeServer, + sendRTMPStream, + setAccessTokensToServers, + stopFfmpeg +} from '@peertube/peertube-server-commands' + +describe('Test video lives API validator', function () { + const path = '/api/v1/videos/live' + let server: PeerTubeServer + let userAccessToken = '' + let channelId: number + let video: VideoCreateResult + let videoIdNotLive: number + let command: LiveCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + latencySetting: { + enabled: false + }, + maxInstanceLives: 20, + maxUserLives: 20, + allowReplay: true + } + } + }) + + const username = 'user1' + const password = 'my super password' + await server.users.create({ username, password }) + userAccessToken = await server.login.getAccessToken({ username, password }) + + { + const { videoChannels } = await server.users.getMyInfo() + channelId = videoChannels[0].id + } + + { + videoIdNotLive = (await server.videos.quickUpload({ name: 'not live' })).id + } + + command = server.live + }) + + describe('When creating a live', function () { + let baseCorrectParams + + before(function () { + baseCorrectParams = { + name: 'my super name', + category: 5, + licence: 1, + language: 'pt', + nsfw: false, + commentsEnabled: true, + downloadEnabled: true, + waitTranscoding: true, + description: 'my super description', + support: 'my super support text', + tags: [ 'tag1', 'tag2' ], + privacy: VideoPrivacy.PUBLIC, + channelId, + saveReplay: false, + replaySettings: undefined, + permanentLive: false, + latencyMode: LiveVideoLatencyMode.DEFAULT + } + }) + + it('Should fail with nothing', async function () { + const fields = {} + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a long name', async function () { + const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad category', async function () { + const fields = { ...baseCorrectParams, category: 125 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad licence', async function () { + const fields = { ...baseCorrectParams, licence: 125 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad language', async function () { + const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a long description', async function () { + const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a long support text', async function () { + const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail without a channel', async function () { + const fields = omit(baseCorrectParams, [ 'channelId' ]) + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad channel', async function () { + const fields = { ...baseCorrectParams, channelId: 545454 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad privacy for replay settings', async function () { + const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: 999 } } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with another user channel', async function () { + const user = { + username: 'fake', + password: 'fake_password' + } + await server.users.create({ username: user.username, password: user.password }) + + const accessTokenUser = await server.login.getAccessToken(user) + const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser }) + const customChannelId = videoChannels[0].id + + const fields = { ...baseCorrectParams, channelId: customChannelId } + + await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields }) + }) + + it('Should fail with too many tags', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a tag length too low', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a tag length too big', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with an incorrect thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a big thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with an incorrect preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('video_short.mp4') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a big preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('custom-preview-big.png') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with bad latency setting', async function () { + const fields = { ...baseCorrectParams, latencyMode: 42 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail to set latency if the server does not allow it', async function () { + const fields = { ...baseCorrectParams, latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct parameters', async function () { + this.timeout(30000) + + const res = await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.OK_200 + }) + + video = res.body.video + }) + + it('Should forbid if live is disabled', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: false + } + } + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should forbid to save replay if not enabled by the admin', async function () { + const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } + + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + allowReplay: false + } + } + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should allow to save replay if enabled by the admin', async function () { + const fields = { ...baseCorrectParams, saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } + + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + allowReplay: true + } + } + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should not allow live if max instance lives is reached', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + maxInstanceLives: 1 + } + } + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should not allow live if max user lives is reached', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + maxInstanceLives: 20, + maxUserLives: 1 + } + } + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + describe('When getting live information', function () { + + it('Should fail with a bad access token', async function () { + await command.get({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should not display private information without access token', async function () { + const live = await command.get({ token: '', videoId: video.id }) + + expect(live.rtmpUrl).to.not.exist + expect(live.streamKey).to.not.exist + expect(live.latencyMode).to.exist + }) + + it('Should not display private information with token of another user', async function () { + const live = await command.get({ token: userAccessToken, videoId: video.id }) + + expect(live.rtmpUrl).to.not.exist + expect(live.streamKey).to.not.exist + expect(live.latencyMode).to.exist + }) + + it('Should display private information with appropriate token', async function () { + const live = await command.get({ videoId: video.id }) + + expect(live.rtmpUrl).to.exist + expect(live.streamKey).to.exist + expect(live.latencyMode).to.exist + }) + + it('Should fail with a bad video id', async function () { + await command.get({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown video id', async function () { + await command.get({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a non live video', async function () { + await command.get({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct params', async function () { + await command.get({ videoId: video.id }) + await command.get({ videoId: video.uuid }) + await command.get({ videoId: video.shortUUID }) + }) + }) + + describe('When getting live sessions', function () { + + it('Should fail with a bad access token', async function () { + await command.listSessions({ token: 'toto', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail without token', async function () { + await command.listSessions({ token: null, videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with the token of another user', async function () { + await command.listSessions({ token: userAccessToken, videoId: video.id, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad video id', async function () { + await command.listSessions({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown video id', async function () { + await command.listSessions({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a non live video', async function () { + await command.listSessions({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct params', async function () { + await command.listSessions({ videoId: video.id }) + }) + }) + + describe('When getting live session of a replay', function () { + + it('Should fail with a bad video id', async function () { + await command.getReplaySession({ videoId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown video id', async function () { + await command.getReplaySession({ videoId: 454555, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a non replay video', async function () { + await command.getReplaySession({ videoId: videoIdNotLive, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + }) + + describe('When updating live information', async function () { + + it('Should fail without access token', async function () { + await command.update({ token: '', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a bad access token', async function () { + await command.update({ token: 'toto', videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with access token of another user', async function () { + await command.update({ token: userAccessToken, videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad video id', async function () { + await command.update({ videoId: 'toto', fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown video id', async function () { + await command.update({ videoId: 454555, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a non live video', async function () { + await command.update({ videoId: videoIdNotLive, fields: {}, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with bad latency setting', async function () { + const fields = { latencyMode: 42 as any } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a bad privacy for replay settings', async function () { + const fields = { saveReplay: true, replaySettings: { privacy: 999 as any } } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with save replay enabled but without replay settings', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + allowReplay: true + } + } + }) + + const fields = { saveReplay: true } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with save replay disabled and replay settings', async function () { + const fields = { saveReplay: false, replaySettings: { privacy: VideoPrivacy.INTERNAL } } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with only replay settings when save replay is disabled', async function () { + const fields = { replaySettings: { privacy: VideoPrivacy.INTERNAL } } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail to set latency if the server does not allow it', async function () { + const fields = { latencyMode: LiveVideoLatencyMode.HIGH_LATENCY } + + await command.update({ videoId: video.id, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct params', async function () { + await command.update({ videoId: video.id, fields: { saveReplay: false } }) + await command.update({ videoId: video.uuid, fields: { saveReplay: false } }) + await command.update({ videoId: video.shortUUID, fields: { saveReplay: false } }) + + await command.update({ videoId: video.id, fields: { saveReplay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } } }) + + }) + + it('Should fail to update replay status if replay is not allowed on the instance', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + allowReplay: false + } + } + }) + + await command.update({ videoId: video.id, fields: { saveReplay: true }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail to update a live if it has already started', async function () { + this.timeout(40000) + + const live = await command.get({ videoId: video.id }) + + const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) + + await command.waitUntilPublished({ videoId: video.id }) + await command.update({ videoId: video.id, fields: {}, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + await stopFfmpeg(ffmpegCommand) + }) + + it('Should fail to change live privacy if it has already started', async function () { + this.timeout(40000) + + const live = await command.get({ videoId: video.id }) + + const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) + + await command.waitUntilPublished({ videoId: video.id }) + + await server.videos.update({ + id: video.id, + attributes: { privacy: VideoPrivacy.PUBLIC } // Same privacy, it's fine + }) + + await server.videos.update({ + id: video.id, + attributes: { privacy: VideoPrivacy.UNLISTED }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await stopFfmpeg(ffmpegCommand) + }) + + it('Should fail to stream twice in the save live', async function () { + this.timeout(40000) + + const live = await command.get({ videoId: video.id }) + + const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) + + await command.waitUntilPublished({ videoId: video.id }) + + await command.runAndTestStreamError({ videoId: video.id, shouldHaveError: true }) + + await stopFfmpeg(ffmpegCommand) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/logs.ts b/packages/tests/src/api/check-params/logs.ts new file mode 100644 index 000000000..629530e30 --- /dev/null +++ b/packages/tests/src/api/check-params/logs.ts @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test logs API validators', function () { + const path = '/api/v1/server/logs' + let server: PeerTubeServer + let userAccessToken = '' + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const user = { + username: 'user1', + password: 'my super password' + } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + }) + + describe('When getting logs', function () { + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a missing startDate query', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad startDate query', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: { startDate: 'toto' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad endDate query', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: { startDate: new Date().toISOString(), endDate: 'toto' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad level parameter', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: { startDate: new Date().toISOString(), level: 'toto' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: { startDate: new Date().toISOString() }, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When creating client logs', function () { + const base = { + level: 'warn' as 'warn', + message: 'my super message', + url: 'https://example.com/toto' + } + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + it('Should fail with an invalid level', async function () { + await server.logs.createLogClient({ payload: { ...base, level: '' as any }, expectedStatus }) + await server.logs.createLogClient({ payload: { ...base, level: undefined }, expectedStatus }) + await server.logs.createLogClient({ payload: { ...base, level: 'toto' as any }, expectedStatus }) + }) + + it('Should fail with an invalid message', async function () { + await server.logs.createLogClient({ payload: { ...base, message: undefined }, expectedStatus }) + await server.logs.createLogClient({ payload: { ...base, message: '' }, expectedStatus }) + await server.logs.createLogClient({ payload: { ...base, message: 'm'.repeat(2500) }, expectedStatus }) + }) + + it('Should fail with an invalid url', async function () { + await server.logs.createLogClient({ payload: { ...base, url: undefined }, expectedStatus }) + await server.logs.createLogClient({ payload: { ...base, url: 'toto' }, expectedStatus }) + }) + + it('Should fail with an invalid stackTrace', async function () { + await server.logs.createLogClient({ payload: { ...base, stackTrace: 's'.repeat(20000) }, expectedStatus }) + }) + + it('Should fail with an invalid userAgent', async function () { + await server.logs.createLogClient({ payload: { ...base, userAgent: 's'.repeat(500) }, expectedStatus }) + }) + + it('Should fail with an invalid meta', async function () { + await server.logs.createLogClient({ payload: { ...base, meta: 's'.repeat(10000) }, expectedStatus }) + }) + + it('Should succeed with the correct params', async function () { + await server.logs.createLogClient({ payload: { ...base, stackTrace: 'stackTrace', meta: '{toto}', userAgent: 'userAgent' } }) + }) + + it('Should rate limit log creation', async function () { + let fail = false + + for (let i = 0; i < 10; i++) { + try { + await server.logs.createLogClient({ token: null, payload: base }) + } catch { + fail = true + } + } + + expect(fail).to.be.true + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/metrics.ts b/packages/tests/src/api/check-params/metrics.ts new file mode 100644 index 000000000..cda854554 --- /dev/null +++ b/packages/tests/src/api/check-params/metrics.ts @@ -0,0 +1,214 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { omit } from '@peertube/peertube-core-utils' +import { HttpStatusCode, PlaybackMetricCreate, VideoResolution } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test metrics API validators', function () { + let server: PeerTubeServer + let videoUUID: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1, { + open_telemetry: { + metrics: { + enabled: true + } + } + }) + + await setAccessTokensToServers([ server ]) + + const { uuid } = await server.videos.quickUpload({ name: 'video' }) + videoUUID = uuid + }) + + describe('When adding playback metrics', function () { + const path = '/api/v1/metrics/playback' + let baseParams: PlaybackMetricCreate + + before(function () { + baseParams = { + playerMode: 'p2p-media-loader', + resolution: VideoResolution.H_1080P, + fps: 30, + resolutionChanges: 1, + errors: 2, + p2pEnabled: true, + downloadedBytesP2P: 0, + downloadedBytesHTTP: 0, + uploadedBytesP2P: 0, + videoId: videoUUID + } + }) + + it('Should fail with an invalid resolution', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, resolution: 'toto' } + }) + }) + + it('Should fail with an invalid fps', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, fps: 'toto' } + }) + }) + + it('Should fail with a missing/invalid player mode', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: omit(baseParams, [ 'playerMode' ]) + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, playerMode: 'toto' } + }) + }) + + it('Should fail with an missing/invalid resolution changes', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: omit(baseParams, [ 'resolutionChanges' ]) + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, resolutionChanges: 'toto' } + }) + }) + + it('Should fail with an missing/invalid errors', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: omit(baseParams, [ 'errors' ]) + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, errors: 'toto' } + }) + }) + + it('Should fail with an missing/invalid downloadedBytesP2P', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: omit(baseParams, [ 'downloadedBytesP2P' ]) + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, downloadedBytesP2P: 'toto' } + }) + }) + + it('Should fail with an missing/invalid downloadedBytesHTTP', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: omit(baseParams, [ 'downloadedBytesHTTP' ]) + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, downloadedBytesHTTP: 'toto' } + }) + }) + + it('Should fail with an missing/invalid uploadedBytesP2P', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: omit(baseParams, [ 'uploadedBytesP2P' ]) + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, uploadedBytesP2P: 'toto' } + }) + }) + + it('Should fail with a missing/invalid p2pEnabled', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: omit(baseParams, [ 'p2pEnabled' ]) + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, p2pEnabled: 'toto' } + }) + }) + + it('Should fail with an invalid totalPeers', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, p2pPeers: 'toto' } + }) + }) + + it('Should fail with a bad video id', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, videoId: 'toto' } + }) + }) + + it('Should fail with an unknown video', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, videoId: 42 }, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: baseParams, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { ...baseParams, p2pEnabled: false, totalPeers: 32 }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/my-user.ts b/packages/tests/src/api/check-params/my-user.ts new file mode 100644 index 000000000..2ef2e242a --- /dev/null +++ b/packages/tests/src/api/check-params/my-user.ts @@ -0,0 +1,492 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' +import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { HttpStatusCode, UserRole, VideoCreateResult } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makePutBodyRequest, + makeUploadRequest, + PeerTubeServer, + setAccessTokensToServers, + UsersCommand +} from '@peertube/peertube-server-commands' + +describe('Test my user API validators', function () { + const path = '/api/v1/users/' + let userId: number + let rootId: number + let moderatorId: number + let video: VideoCreateResult + let server: PeerTubeServer + let userToken = '' + let moderatorToken = '' + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + { + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + } + + { + const result = await server.users.generate('user1') + userToken = result.token + userId = result.userId + } + + { + const result = await server.users.generate('moderator1', UserRole.MODERATOR) + moderatorToken = result.token + } + + { + const result = await server.users.generate('moderator2', UserRole.MODERATOR) + moderatorId = result.userId + } + + { + video = await server.videos.upload() + } + }) + + describe('When updating my account', function () { + + it('Should fail with an invalid email attribute', async function () { + const fields = { + email: 'blabla' + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: server.accessToken, fields }) + }) + + it('Should fail with a too small password', async function () { + const fields = { + currentPassword: 'password', + password: 'bla' + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with a too long password', async function () { + const fields = { + currentPassword: 'password', + password: 'super'.repeat(61) + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail without the current password', async function () { + const fields = { + currentPassword: 'password', + password: 'super'.repeat(61) + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with an invalid current password', async function () { + const fields = { + currentPassword: 'my super password fail', + password: 'super'.repeat(61) + } + + await makePutBodyRequest({ + url: server.url, + path: path + 'me', + token: userToken, + fields, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with an invalid NSFW policy attribute', async function () { + const fields = { + nsfwPolicy: 'hello' + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with an invalid autoPlayVideo attribute', async function () { + const fields = { + autoPlayVideo: -1 + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with an invalid autoPlayNextVideo attribute', async function () { + const fields = { + autoPlayNextVideo: -1 + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with an invalid videosHistoryEnabled attribute', async function () { + const fields = { + videosHistoryEnabled: -1 + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with an non authenticated user', async function () { + const fields = { + currentPassword: 'password', + password: 'my super password' + } + + await makePutBodyRequest({ + url: server.url, + path: path + 'me', + token: 'super token', + fields, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a too long description', async function () { + const fields = { + description: 'super'.repeat(201) + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with an invalid videoLanguages attribute', async function () { + { + const fields = { + videoLanguages: 'toto' + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + } + + { + const languages = [] + for (let i = 0; i < 1000; i++) { + languages.push('fr') + } + + const fields = { + videoLanguages: languages + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + } + }) + + it('Should fail with an invalid theme', async function () { + const fields = { theme: 'invalid' } + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with an unknown theme', async function () { + const fields = { theme: 'peertube-theme-unknown' } + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + }) + + it('Should fail with invalid no modal attributes', async function () { + const keys = [ + 'noInstanceConfigWarningModal', + 'noAccountSetupWarningModal', + 'noWelcomeModal' + ] + + for (const key of keys) { + const fields = { + [key]: -1 + } + + await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields }) + } + }) + + it('Should succeed to change password with the correct params', async function () { + const fields = { + currentPassword: 'password', + password: 'my super password', + nsfwPolicy: 'blur', + autoPlayVideo: false, + email: 'super_email@example.com', + theme: 'default', + noInstanceConfigWarningModal: true, + noWelcomeModal: true, + noAccountSetupWarningModal: true + } + + await makePutBodyRequest({ + url: server.url, + path: path + 'me', + token: userToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should succeed without password change with the correct params', async function () { + const fields = { + nsfwPolicy: 'blur', + autoPlayVideo: false + } + + await makePutBodyRequest({ + url: server.url, + path: path + 'me', + token: userToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When updating my avatar', function () { + it('Should fail without an incorrect input file', async function () { + const fields = {} + const attaches = { + avatarfile: buildAbsoluteFixturePath('video_short.mp4') + } + await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a big file', async function () { + const fields = {} + const attaches = { + avatarfile: buildAbsoluteFixturePath('avatar-big.png') + } + await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) + }) + + it('Should fail with an unauthenticated user', async function () { + const fields = {} + const attaches = { + avatarfile: buildAbsoluteFixturePath('avatar.png') + } + await makeUploadRequest({ + url: server.url, + path: path + '/me/avatar/pick', + fields, + attaches, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with the correct params', async function () { + const fields = {} + const attaches = { + avatarfile: buildAbsoluteFixturePath('avatar.png') + } + await makeUploadRequest({ + url: server.url, + path: path + '/me/avatar/pick', + token: server.accessToken, + fields, + attaches, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When managing my scoped tokens', function () { + + it('Should fail to get my scoped tokens with an non authenticated user', async function () { + await server.users.getMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail to get my scoped tokens with a bad token', async function () { + await server.users.getMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + + }) + + it('Should succeed to get my scoped tokens', async function () { + await server.users.getMyScopedTokens() + }) + + it('Should fail to renew my scoped tokens with an non authenticated user', async function () { + await server.users.renewMyScopedTokens({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail to renew my scoped tokens with a bad token', async function () { + await server.users.renewMyScopedTokens({ token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should succeed to renew my scoped tokens', async function () { + await server.users.renewMyScopedTokens() + }) + }) + + describe('When getting my information', function () { + it('Should fail with a non authenticated user', async function () { + await server.users.getMyInfo({ token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should success with the correct parameters', async function () { + await server.users.getMyInfo({ token: userToken }) + }) + }) + + describe('When getting my video rating', function () { + let command: UsersCommand + + before(function () { + command = server.users + }) + + it('Should fail with a non authenticated user', async function () { + await command.getMyRating({ token: 'fake_token', videoId: video.id, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with an incorrect video uuid', async function () { + await command.getMyRating({ videoId: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown video', async function () { + await command.getMyRating({ videoId: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct parameters', async function () { + await command.getMyRating({ videoId: video.id }) + await command.getMyRating({ videoId: video.uuid }) + await command.getMyRating({ videoId: video.shortUUID }) + }) + }) + + describe('When retrieving my global ratings', function () { + const path = '/api/v1/accounts/user1/ratings' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, userToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, userToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, userToken) + }) + + it('Should fail with a unauthenticated user', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a another user', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad type', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userToken, + query: { rating: 'toto ' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When getting my global followers', function () { + const path = '/api/v1/accounts/user1/followers' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, userToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, userToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, userToken) + }) + + it('Should fail with a unauthenticated user', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a another user', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When blocking/unblocking/removing user', function () { + + it('Should fail with an incorrect id', async function () { + const options = { userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } + + await server.users.remove(options) + await server.users.banUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.users.unbanUser({ userId: 'blabla' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with the root user', async function () { + const options = { userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } + + await server.users.remove(options) + await server.users.banUser(options) + await server.users.unbanUser(options) + }) + + it('Should return 404 with a non existing id', async function () { + const options = { userId: 4545454, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + + await server.users.remove(options) + await server.users.banUser(options) + await server.users.unbanUser(options) + }) + + it('Should fail with a non admin user', async function () { + const options = { userId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 } + + await server.users.remove(options) + await server.users.banUser(options) + await server.users.unbanUser(options) + }) + + it('Should fail on a moderator with a moderator', async function () { + const options = { userId: moderatorId, token: moderatorToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 } + + await server.users.remove(options) + await server.users.banUser(options) + await server.users.unbanUser(options) + }) + + it('Should succeed on a user with a moderator', async function () { + const options = { userId, token: moderatorToken } + + await server.users.banUser(options) + await server.users.unbanUser(options) + }) + }) + + describe('When deleting our account', function () { + + it('Should fail with with the root account', async function () { + await server.users.deleteMe({ expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + }) + + after(async function () { + MockSmtpServer.Instance.kill() + + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/plugins.ts b/packages/tests/src/api/check-params/plugins.ts new file mode 100644 index 000000000..ab2a426fe --- /dev/null +++ b/packages/tests/src/api/check-params/plugins.ts @@ -0,0 +1,490 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode, PeerTubePlugin, PluginType } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test server plugins API validators', function () { + let server: PeerTubeServer + let userAccessToken = null + + const npmPlugin = 'peertube-plugin-hello-world' + const pluginName = 'hello-world' + let npmVersion: string + + const themePlugin = 'peertube-theme-background-red' + const themeName = 'background-red' + let themeVersion: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(60000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const user = { + username: 'user1', + password: 'password' + } + + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + + { + const res = await server.plugins.install({ npmName: npmPlugin }) + const plugin = res.body as PeerTubePlugin + npmVersion = plugin.version + } + + { + const res = await server.plugins.install({ npmName: themePlugin }) + const plugin = res.body as PeerTubePlugin + themeVersion = plugin.version + } + }) + + describe('With static plugin routes', function () { + it('Should fail with an unknown plugin name/plugin version', async function () { + const paths = [ + '/plugins/' + pluginName + '/0.0.1/auth/fake-auth', + '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png', + '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js', + '/themes/' + themeName + '/0.0.1/static/images/chocobo.png', + '/themes/' + themeName + '/0.0.1/client-scripts/client/video-watch-client-plugin.js', + '/themes/' + themeName + '/0.0.1/css/assets/style1.css' + ] + + for (const p of paths) { + await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } + }) + + it('Should fail when requesting a plugin in the theme path', async function () { + await makeGetRequest({ + url: server.url, + path: '/themes/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with invalid versions', async function () { + const paths = [ + '/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth', + '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png', + '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js', + '/themes/' + themeName + '/1/static/images/chocobo.png', + '/themes/' + themeName + '/0.0.1000a/client-scripts/client/video-watch-client-plugin.js', + '/themes/' + themeName + '/0.a.1/css/assets/style1.css' + ] + + for (const p of paths) { + await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + } + }) + + it('Should fail with invalid paths', async function () { + const paths = [ + '/plugins/' + pluginName + '/' + npmVersion + '/static/images/../chocobo.png', + '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/../client/common-client-plugin.js', + '/themes/' + themeName + '/' + themeVersion + '/static/../images/chocobo.png', + '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js/..', + '/themes/' + themeName + '/' + themeVersion + '/css/../assets/style1.css' + ] + + for (const p of paths) { + await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + } + }) + + it('Should fail with an unknown auth name', async function () { + const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth' + + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with an unknown static file', async function () { + const paths = [ + '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png', + '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/fake.js', + '/themes/' + themeName + '/' + themeVersion + '/static/fake/chocobo.png', + '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/fake.js' + ] + + for (const p of paths) { + await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } + }) + + it('Should fail with an unknown CSS file', async function () { + await makeGetRequest({ + url: server.url, + path: '/themes/' + themeName + '/' + themeVersion + '/css/assets/fake.css', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct parameters', async function () { + const paths = [ + '/plugins/' + pluginName + '/' + npmVersion + '/static/images/chocobo.png', + '/plugins/' + pluginName + '/' + npmVersion + '/client-scripts/client/common-client-plugin.js', + '/themes/' + themeName + '/' + themeVersion + '/static/images/chocobo.png', + '/themes/' + themeName + '/' + themeVersion + '/client-scripts/client/video-watch-client-plugin.js', + '/themes/' + themeName + '/' + themeVersion + '/css/assets/style1.css' + ] + + for (const p of paths) { + await makeGetRequest({ url: server.url, path: p, expectedStatus: HttpStatusCode.OK_200 }) + } + + const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth' + await makeGetRequest({ url: server.url, path: authPath, expectedStatus: HttpStatusCode.FOUND_302 }) + }) + }) + + describe('When listing available plugins/themes', function () { + const path = '/api/v1/plugins/available' + const baseQuery = { + search: 'super search', + pluginType: PluginType.PLUGIN, + currentPeerTubeEngine: '1.2.3' + } + + it('Should fail with an invalid token', async function () { + await makeGetRequest({ + url: server.url, + path, + token: 'fake_token', + query: baseQuery, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + query: baseQuery, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an invalid plugin type', async function () { + const query = { ...baseQuery, pluginType: 5 } + + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query + }) + }) + + it('Should fail with an invalid current peertube engine', async function () { + const query = { ...baseQuery, currentPeerTubeEngine: '1.0' } + + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query + }) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: baseQuery, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When listing local plugins/themes', function () { + const path = '/api/v1/plugins' + const baseQuery = { + pluginType: PluginType.THEME + } + + it('Should fail with an invalid token', async function () { + await makeGetRequest({ + url: server.url, + path, + token: 'fake_token', + query: baseQuery, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + query: baseQuery, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an invalid plugin type', async function () { + const query = { ...baseQuery, pluginType: 5 } + + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query + }) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: baseQuery, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When getting a plugin or the registered settings or public settings', function () { + const path = '/api/v1/plugins/' + + it('Should fail with an invalid token', async function () { + for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) { + await makeGetRequest({ + url: server.url, + path: path + suffix, + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + } + }) + + it('Should fail if the user is not an administrator', async function () { + for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings` ]) { + await makeGetRequest({ + url: server.url, + path: path + suffix, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + } + }) + + it('Should fail with an invalid npm name', async function () { + for (const suffix of [ 'toto', 'toto/registered-settings', 'toto/public-settings' ]) { + await makeGetRequest({ + url: server.url, + path: path + suffix, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + + for (const suffix of [ 'peertube-plugin-TOTO', 'peertube-plugin-TOTO/registered-settings', 'peertube-plugin-TOTO/public-settings' ]) { + await makeGetRequest({ + url: server.url, + path: path + suffix, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + }) + + it('Should fail with an unknown plugin', async function () { + for (const suffix of [ 'peertube-plugin-toto', 'peertube-plugin-toto/registered-settings', 'peertube-plugin-toto/public-settings' ]) { + await makeGetRequest({ + url: server.url, + path: path + suffix, + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + } + }) + + it('Should succeed with the correct parameters', async function () { + for (const suffix of [ npmPlugin, `${npmPlugin}/registered-settings`, `${npmPlugin}/public-settings` ]) { + await makeGetRequest({ + url: server.url, + path: path + suffix, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + } + }) + }) + + describe('When updating plugin settings', function () { + const path = '/api/v1/plugins/' + const settings = { setting1: 'value1' } + + it('Should fail with an invalid token', async function () { + await makePutBodyRequest({ + url: server.url, + path: path + npmPlugin + '/settings', + fields: { settings }, + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makePutBodyRequest({ + url: server.url, + path: path + npmPlugin + '/settings', + fields: { settings }, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid npm name', async function () { + await makePutBodyRequest({ + url: server.url, + path: path + 'toto/settings', + fields: { settings }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makePutBodyRequest({ + url: server.url, + path: path + 'peertube-plugin-TOTO/settings', + fields: { settings }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown plugin', async function () { + await makePutBodyRequest({ + url: server.url, + path: path + 'peertube-plugin-toto/settings', + fields: { settings }, + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePutBodyRequest({ + url: server.url, + path: path + npmPlugin + '/settings', + fields: { settings }, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When installing/updating/uninstalling a plugin', function () { + const path = '/api/v1/plugins/' + + it('Should fail with an invalid token', async function () { + for (const suffix of [ 'install', 'update', 'uninstall' ]) { + await makePostBodyRequest({ + url: server.url, + path: path + suffix, + fields: { npmName: npmPlugin }, + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + } + }) + + it('Should fail if the user is not an administrator', async function () { + for (const suffix of [ 'install', 'update', 'uninstall' ]) { + await makePostBodyRequest({ + url: server.url, + path: path + suffix, + fields: { npmName: npmPlugin }, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + } + }) + + it('Should fail with an invalid npm name', async function () { + for (const suffix of [ 'install', 'update', 'uninstall' ]) { + await makePostBodyRequest({ + url: server.url, + path: path + suffix, + fields: { npmName: 'toto' }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + + for (const suffix of [ 'install', 'update', 'uninstall' ]) { + await makePostBodyRequest({ + url: server.url, + path: path + suffix, + fields: { npmName: 'peertube-plugin-TOTO' }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + }) + + it('Should succeed with the correct parameters', async function () { + const it = [ + { suffix: 'install', status: HttpStatusCode.OK_200 }, + { suffix: 'update', status: HttpStatusCode.OK_200 }, + { suffix: 'uninstall', status: HttpStatusCode.NO_CONTENT_204 } + ] + + for (const obj of it) { + await makePostBodyRequest({ + url: server.url, + path: path + obj.suffix, + fields: { npmName: npmPlugin }, + token: server.accessToken, + expectedStatus: obj.status + }) + } + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/redundancy.ts b/packages/tests/src/api/check-params/redundancy.ts new file mode 100644 index 000000000..16a5d0a3d --- /dev/null +++ b/packages/tests/src/api/check-params/redundancy.ts @@ -0,0 +1,240 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode, VideoCreateResult } from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + makeDeleteRequest, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test server redundancy API validators', function () { + let servers: PeerTubeServer[] + let userAccessToken = null + let videoIdLocal: number + let videoRemote: VideoCreateResult + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(160000) + + servers = await createMultipleServers(2) + + await setAccessTokensToServers(servers) + await doubleFollow(servers[0], servers[1]) + + const user = { + username: 'user1', + password: 'password' + } + + await servers[0].users.create({ username: user.username, password: user.password }) + userAccessToken = await servers[0].login.getAccessToken(user) + + videoIdLocal = (await servers[0].videos.quickUpload({ name: 'video' })).id + + const remoteUUID = (await servers[1].videos.quickUpload({ name: 'video' })).uuid + + await waitJobs(servers) + + videoRemote = await servers[0].videos.get({ id: remoteUUID }) + }) + + describe('When listing redundancies', function () { + const path = '/api/v1/server/redundancy/videos' + + let url: string + let token: string + + before(function () { + url = servers[0].url + token = servers[0].accessToken + }) + + it('Should fail with an invalid token', async function () { + await makeGetRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeGetRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(url, path, servers[0].accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(url, path, servers[0].accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(url, path, servers[0].accessToken) + }) + + it('Should fail with a bad target', async function () { + await makeGetRequest({ url, path, token, query: { target: 'bad target' } }) + }) + + it('Should fail without target', async function () { + await makeGetRequest({ url, path, token }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When manually adding a redundancy', function () { + const path = '/api/v1/server/redundancy/videos' + + let url: string + let token: string + + before(function () { + url = servers[0].url + token = servers[0].accessToken + }) + + it('Should fail with an invalid token', async function () { + await makePostBodyRequest({ url, path, token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makePostBodyRequest({ url, path, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail without a video id', async function () { + await makePostBodyRequest({ url, path, token }) + }) + + it('Should fail with an incorrect video id', async function () { + await makePostBodyRequest({ url, path, token, fields: { videoId: 'peertube' } }) + }) + + it('Should fail with a not found video id', async function () { + await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a local a video id', async function () { + await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdLocal } }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url, + path, + token, + fields: { videoId: videoRemote.shortUUID }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should fail if the video is already duplicated', async function () { + this.timeout(30000) + + await waitJobs(servers) + + await makePostBodyRequest({ + url, + path, + token, + fields: { videoId: videoRemote.uuid }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + }) + + describe('When manually removing a redundancy', function () { + const path = '/api/v1/server/redundancy/videos/' + + let url: string + let token: string + + before(function () { + url = servers[0].url + token = servers[0].accessToken + }) + + it('Should fail with an invalid token', async function () { + await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with an incorrect video id', async function () { + await makeDeleteRequest({ url, path: path + 'toto', token }) + }) + + it('Should fail with a not found video redundancy', async function () { + await makeDeleteRequest({ url, path: path + '454545', token, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + }) + + describe('When updating server redundancy', function () { + const path = '/api/v1/server/redundancy' + + it('Should fail with an invalid token', async function () { + await makePutBodyRequest({ + url: servers[0].url, + path: path + '/' + servers[1].host, + fields: { redundancyAllowed: true }, + token: 'fake_token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if the user is not an administrator', async function () { + await makePutBodyRequest({ + url: servers[0].url, + path: path + '/' + servers[1].host, + fields: { redundancyAllowed: true }, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail if we do not follow this server', async function () { + await makePutBodyRequest({ + url: servers[0].url, + path: path + '/example.com', + fields: { redundancyAllowed: true }, + token: servers[0].accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail without de redundancyAllowed param', async function () { + await makePutBodyRequest({ + url: servers[0].url, + path: path + '/' + servers[1].host, + fields: { blabla: true }, + token: servers[0].accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePutBodyRequest({ + url: servers[0].url, + path: path + '/' + servers[1].host, + fields: { redundancyAllowed: true }, + token: servers[0].accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/api/check-params/registrations.ts b/packages/tests/src/api/check-params/registrations.ts new file mode 100644 index 000000000..e4e46da2a --- /dev/null +++ b/packages/tests/src/api/check-params/registrations.ts @@ -0,0 +1,446 @@ +import { omit } from '@peertube/peertube-core-utils' +import { HttpStatusCode, HttpStatusCodeType, UserRole } from '@peertube/peertube-models' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { + cleanupTests, + createSingleServer, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers, + setDefaultAccountAvatar, + setDefaultChannelAvatar +} from '@peertube/peertube-server-commands' + +describe('Test registrations API validators', function () { + let server: PeerTubeServer + let userToken: string + let moderatorToken: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + await setDefaultAccountAvatar([ server ]) + await setDefaultChannelAvatar([ server ]) + + await server.config.enableSignup(false); + + ({ token: moderatorToken } = await server.users.generate('moderator', UserRole.MODERATOR)); + ({ token: userToken } = await server.users.generate('user', UserRole.USER)) + }) + + describe('Register', function () { + const registrationPath = '/api/v1/users/register' + const registrationRequestPath = '/api/v1/users/registrations/request' + + const baseCorrectParams = { + username: 'user3', + displayName: 'super user', + email: 'test3@example.com', + password: 'my super password', + registrationReason: 'my super registration reason' + } + + describe('When registering a new user or requesting user registration', function () { + + async function check (fields: any, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { + await server.config.enableSignup(false) + await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus }) + + await server.config.enableSignup(true) + await makePostBodyRequest({ url: server.url, path: registrationRequestPath, fields, expectedStatus }) + } + + it('Should fail with a too small username', async function () { + const fields = { ...baseCorrectParams, username: '' } + + await check(fields) + }) + + it('Should fail with a too long username', async function () { + const fields = { ...baseCorrectParams, username: 'super'.repeat(50) } + + await check(fields) + }) + + it('Should fail with an incorrect username', async function () { + const fields = { ...baseCorrectParams, username: 'my username' } + + await check(fields) + }) + + it('Should fail with a missing email', async function () { + const fields = omit(baseCorrectParams, [ 'email' ]) + + await check(fields) + }) + + it('Should fail with an invalid email', async function () { + const fields = { ...baseCorrectParams, email: 'test_example.com' } + + await check(fields) + }) + + it('Should fail with a too small password', async function () { + const fields = { ...baseCorrectParams, password: 'bla' } + + await check(fields) + }) + + it('Should fail with a too long password', async function () { + const fields = { ...baseCorrectParams, password: 'super'.repeat(61) } + + await check(fields) + }) + + it('Should fail if we register a user with the same username', async function () { + const fields = { ...baseCorrectParams, username: 'root' } + + await check(fields, HttpStatusCode.CONFLICT_409) + }) + + it('Should fail with a "peertube" username', async function () { + const fields = { ...baseCorrectParams, username: 'peertube' } + + await check(fields, HttpStatusCode.CONFLICT_409) + }) + + it('Should fail if we register a user with the same email', async function () { + const fields = { ...baseCorrectParams, email: 'admin' + server.internalServerNumber + '@example.com' } + + await check(fields, HttpStatusCode.CONFLICT_409) + }) + + it('Should fail with a bad display name', async function () { + const fields = { ...baseCorrectParams, displayName: 'a'.repeat(150) } + + await check(fields) + }) + + it('Should fail with a bad channel name', async function () { + const fields = { ...baseCorrectParams, channel: { name: '[]azf', displayName: 'toto' } } + + await check(fields) + }) + + it('Should fail with a bad channel display name', async function () { + const fields = { ...baseCorrectParams, channel: { name: 'toto', displayName: '' } } + + await check(fields) + }) + + it('Should fail with a channel name that is the same as username', async function () { + const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } } + const fields = { ...baseCorrectParams, ...source } + + await check(fields) + }) + + it('Should fail with an existing channel', async function () { + const attributes = { name: 'existing_channel', displayName: 'hello', description: 'super description' } + await server.channels.create({ attributes }) + + const fields = { ...baseCorrectParams, channel: { name: 'existing_channel', displayName: 'toto' } } + + await check(fields, HttpStatusCode.CONFLICT_409) + }) + + it('Should fail on a server with registration disabled', async function () { + this.timeout(60000) + + await server.config.updateExistingSubConfig({ + newConfig: { + signup: { + enabled: false + } + } + }) + + await server.registrations.register({ username: 'user4', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await server.registrations.requestRegistration({ + username: 'user4', + registrationReason: 'reason', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail if the user limit is reached', async function () { + this.timeout(60000) + + const { total } = await server.users.list() + + await server.config.enableSignup(false, total) + await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + + await server.config.enableSignup(true, total) + await server.registrations.requestRegistration({ + username: 'user42', + registrationReason: 'reason', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed if the user limit is not reached', async function () { + this.timeout(60000) + + const { total } = await server.users.list() + + await server.config.enableSignup(false, total + 1) + await server.registrations.register({ username: 'user43', expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + + await server.config.enableSignup(true, total + 2) + await server.registrations.requestRegistration({ + username: 'user44', + registrationReason: 'reason', + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('On direct registration', function () { + + it('Should succeed with the correct params', async function () { + await server.config.enableSignup(false) + + const fields = { + username: 'user_direct_1', + displayName: 'super user direct 1', + email: 'user_direct_1@example.com', + password: 'my super password', + channel: { name: 'super_user_direct_1_channel', displayName: 'super user direct 1 channel' } + } + + await makePostBodyRequest({ url: server.url, path: registrationPath, fields, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + }) + + it('Should fail if the instance requires approval', async function () { + this.timeout(60000) + + await server.config.enableSignup(true) + await server.registrations.register({ username: 'user42', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + }) + + describe('On registration request', function () { + + before(async function () { + this.timeout(60000) + + await server.config.enableSignup(true) + }) + + it('Should fail with an invalid registration reason', async function () { + for (const registrationReason of [ '', 't', 't'.repeat(5000) ]) { + await server.registrations.requestRegistration({ + username: 'user_request_1', + registrationReason, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + }) + + it('Should succeed with the correct params', async function () { + await server.registrations.requestRegistration({ + username: 'user_request_2', + registrationReason: 'tt', + channel: { + displayName: 'my user request 2 channel', + name: 'user_request_2_channel' + } + }) + }) + + it('Should fail if the username is already awaiting registration approval', async function () { + await server.registrations.requestRegistration({ + username: 'user_request_2', + registrationReason: 'tt', + channel: { + displayName: 'my user request 42 channel', + name: 'user_request_42_channel' + }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should fail if the email is already awaiting registration approval', async function () { + await server.registrations.requestRegistration({ + username: 'user42', + email: 'user_request_2@example.com', + registrationReason: 'tt', + channel: { + displayName: 'my user request 42 channel', + name: 'user_request_42_channel' + }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should fail if the channel is already awaiting registration approval', async function () { + await server.registrations.requestRegistration({ + username: 'user42', + registrationReason: 'tt', + channel: { + displayName: 'my user request 2 channel', + name: 'user_request_2_channel' + }, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should fail if the instance does not require approval', async function () { + this.timeout(60000) + + await server.config.enableSignup(false) + + await server.registrations.requestRegistration({ + username: 'user42', + registrationReason: 'toto', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + }) + + describe('Registrations accept/reject', function () { + let id1: number + let id2: number + + before(async function () { + this.timeout(60000) + + await server.config.enableSignup(true); + + ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_2', registrationReason: 'toto' })); + ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_3', registrationReason: 'toto' })) + }) + + it('Should fail to accept/reject registration without token', async function () { + const options = { id: id1, moderationResponse: 'tt', token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 } + await server.registrations.accept(options) + await server.registrations.reject(options) + }) + + it('Should fail to accept/reject registration with a non moderator user', async function () { + const options = { id: id1, moderationResponse: 'tt', token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 } + await server.registrations.accept(options) + await server.registrations.reject(options) + }) + + it('Should fail to accept/reject registration with a bad registration id', async function () { + { + const options = { id: 't' as any, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } + await server.registrations.accept(options) + await server.registrations.reject(options) + } + + { + const options = { id: 42, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + await server.registrations.accept(options) + await server.registrations.reject(options) + } + }) + + it('Should fail to accept/reject registration with a bad moderation resposne', async function () { + for (const moderationResponse of [ '', 't', 't'.repeat(5000) ]) { + const options = { id: id1, moderationResponse, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } + await server.registrations.accept(options) + await server.registrations.reject(options) + } + }) + + it('Should succeed to accept a registration', async function () { + await server.registrations.accept({ id: id1, moderationResponse: 'tt', token: moderatorToken }) + }) + + it('Should succeed to reject a registration', async function () { + await server.registrations.reject({ id: id2, moderationResponse: 'tt', token: moderatorToken }) + }) + + it('Should fail to accept/reject a registration that was already accepted/rejected', async function () { + for (const id of [ id1, id2 ]) { + const options = { id, moderationResponse: 'tt', token: moderatorToken, expectedStatus: HttpStatusCode.CONFLICT_409 } + await server.registrations.accept(options) + await server.registrations.reject(options) + } + }) + }) + + describe('Registrations deletion', function () { + let id1: number + let id2: number + let id3: number + + before(async function () { + ({ id: id1 } = await server.registrations.requestRegistration({ username: 'request_4', registrationReason: 'toto' })); + ({ id: id2 } = await server.registrations.requestRegistration({ username: 'request_5', registrationReason: 'toto' })); + ({ id: id3 } = await server.registrations.requestRegistration({ username: 'request_6', registrationReason: 'toto' })) + + await server.registrations.accept({ id: id2, moderationResponse: 'tt' }) + await server.registrations.reject({ id: id3, moderationResponse: 'tt' }) + }) + + it('Should fail to delete registration without token', async function () { + await server.registrations.delete({ id: id1, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail to delete registration with a non moderator user', async function () { + await server.registrations.delete({ id: id1, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail to delete registration with a bad registration id', async function () { + await server.registrations.delete({ id: 't' as any, token: moderatorToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.registrations.delete({ id: 42, token: moderatorToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct params', async function () { + await server.registrations.delete({ id: id1, token: moderatorToken }) + await server.registrations.delete({ id: id2, token: moderatorToken }) + await server.registrations.delete({ id: id3, token: moderatorToken }) + }) + }) + + describe('Listing registrations', function () { + const path = '/api/v1/users/registrations' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a non authenticated user', async function () { + await server.registrations.list({ + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await server.registrations.list({ + token: userToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + await server.registrations.list({ + token: moderatorToken, + search: 'toto' + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/runners.ts b/packages/tests/src/api/check-params/runners.ts new file mode 100644 index 000000000..dd2d2f0a1 --- /dev/null +++ b/packages/tests/src/api/check-params/runners.ts @@ -0,0 +1,911 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ +import { basename } from 'path' +import { + HttpStatusCode, + HttpStatusCodeType, + isVideoStudioTaskIntro, + RunnerJob, + RunnerJobState, + RunnerJobStudioTranscodingPayload, + RunnerJobSuccessPayload, + RunnerJobUpdatePayload, + VideoPrivacy, + VideoStudioTaskIntro +} from '@peertube/peertube-models' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { + cleanupTests, + createSingleServer, + makePostBodyRequest, + PeerTubeServer, + sendRTMPStream, + setAccessTokensToServers, + setDefaultVideoChannel, + stopFfmpeg, + VideoStudioCommand, + waitJobs +} from '@peertube/peertube-server-commands' + +const badUUID = '910ec12a-d9e6-458b-a274-0abb655f9464' + +describe('Test managing runners', function () { + let server: PeerTubeServer + + let userToken: string + + let registrationTokenId: number + let registrationToken: string + + let runnerToken: string + let runnerToken2: string + + let completedJobToken: string + let completedJobUUID: string + + let cancelledJobToken: string + let cancelledJobUUID: string + + before(async function () { + this.timeout(120000) + + const config = { + rates_limit: { + api: { + max: 5000 + } + } + } + + server = await createSingleServer(1, config) + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + userToken = await server.users.generateUserAndToken('user1') + + const { data } = await server.runnerRegistrationTokens.list() + registrationToken = data[0].registrationToken + registrationTokenId = data[0].id + + await server.config.enableTranscoding({ hls: true, webVideo: true }) + await server.config.enableStudio() + await server.config.enableRemoteTranscoding() + await server.config.enableRemoteStudio() + + runnerToken = await server.runners.autoRegisterRunner() + runnerToken2 = await server.runners.autoRegisterRunner() + + { + await server.videos.quickUpload({ name: 'video 1' }) + await server.videos.quickUpload({ name: 'video 2' }) + + await waitJobs([ server ]) + + { + const job = await server.runnerJobs.autoProcessWebVideoJob(runnerToken) + completedJobToken = job.jobToken + completedJobUUID = job.uuid + } + + { + const { job } = await server.runnerJobs.autoAccept({ runnerToken }) + cancelledJobToken = job.jobToken + cancelledJobUUID = job.uuid + await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID }) + } + } + }) + + describe('Managing runner registration tokens', function () { + + describe('Common', function () { + + it('Should fail to generate, list or delete runner registration token without oauth token', async function () { + const expectedStatus = HttpStatusCode.UNAUTHORIZED_401 + + await server.runnerRegistrationTokens.generate({ token: null, expectedStatus }) + await server.runnerRegistrationTokens.list({ token: null, expectedStatus }) + await server.runnerRegistrationTokens.delete({ token: null, id: registrationTokenId, expectedStatus }) + }) + + it('Should fail to generate, list or delete runner registration token without admin rights', async function () { + const expectedStatus = HttpStatusCode.FORBIDDEN_403 + + await server.runnerRegistrationTokens.generate({ token: userToken, expectedStatus }) + await server.runnerRegistrationTokens.list({ token: userToken, expectedStatus }) + await server.runnerRegistrationTokens.delete({ token: userToken, id: registrationTokenId, expectedStatus }) + }) + }) + + describe('Delete', function () { + + it('Should fail to delete with a bad id', async function () { + await server.runnerRegistrationTokens.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + }) + + describe('List', function () { + const path = '/api/v1/runners/registration-tokens' + + it('Should fail to list with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail to list with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail to list with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should succeed to list with the correct params', async function () { + await server.runnerRegistrationTokens.list({ start: 0, count: 5, sort: '-createdAt' }) + }) + }) + }) + + describe('Managing runners', function () { + let toDeleteId: number + + describe('Register', function () { + const name = 'runner name' + + it('Should fail with a bad registration token', async function () { + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + await server.runners.register({ name, registrationToken: 'a'.repeat(4000), expectedStatus }) + await server.runners.register({ name, registrationToken: null, expectedStatus }) + }) + + it('Should fail with an unknown registration token', async function () { + await server.runners.register({ name, registrationToken: 'aaa', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a bad name', async function () { + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + await server.runners.register({ name: '', registrationToken, expectedStatus }) + await server.runners.register({ name: 'a'.repeat(200), registrationToken, expectedStatus }) + }) + + it('Should fail with an invalid description', async function () { + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + await server.runners.register({ name, description: '', registrationToken, expectedStatus }) + await server.runners.register({ name, description: 'a'.repeat(5000), registrationToken, expectedStatus }) + }) + + it('Should succeed with the correct params', async function () { + const { id } = await server.runners.register({ name, description: 'super description', registrationToken }) + + toDeleteId = id + }) + + it('Should fail with the same runner name', async function () { + await server.runners.register({ + name, + description: 'super description', + registrationToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + + describe('Delete', function () { + + it('Should fail without oauth token', async function () { + await server.runners.delete({ token: null, id: toDeleteId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail without admin rights', async function () { + await server.runners.delete({ token: userToken, id: toDeleteId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad id', async function () { + await server.runners.delete({ id: 'hi' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown id', async function () { + await server.runners.delete({ id: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct params', async function () { + await server.runners.delete({ id: toDeleteId }) + }) + }) + + describe('List', function () { + const path = '/api/v1/runners' + + it('Should fail without oauth token', async function () { + await server.runners.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail without admin rights', async function () { + await server.runners.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail to list with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail to list with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail to list with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an invalid state', async function () { + await server.runners.list({ start: 0, count: 5, sort: '-createdAt' }) + }) + + it('Should succeed to list with the correct params', async function () { + await server.runners.list({ start: 0, count: 5, sort: '-createdAt' }) + }) + }) + + }) + + describe('Runner jobs by admin', function () { + + describe('Cancel', function () { + let jobUUID: string + + before(async function () { + this.timeout(60000) + + await server.videos.quickUpload({ name: 'video' }) + await waitJobs([ server ]) + + const { availableJobs } = await server.runnerJobs.request({ runnerToken }) + jobUUID = availableJobs[0].uuid + }) + + it('Should fail without oauth token', async function () { + await server.runnerJobs.cancelByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail without admin rights', async function () { + await server.runnerJobs.cancelByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad job uuid', async function () { + await server.runnerJobs.cancelByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown job uuid', async function () { + const jobUUID = badUUID + await server.runnerJobs.cancelByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with an already cancelled job', async function () { + await server.runnerJobs.cancelByAdmin({ jobUUID: cancelledJobUUID, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct params', async function () { + await server.runnerJobs.cancelByAdmin({ jobUUID }) + }) + }) + + describe('List', function () { + const path = '/api/v1/runners/jobs' + + it('Should fail without oauth token', async function () { + await server.runnerJobs.list({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail without admin rights', async function () { + await server.runnerJobs.list({ token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail to list with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail to list with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail to list with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an invalid state', async function () { + await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: 42 as any }) + await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ 42 ] as any }) + }) + + it('Should succeed with the correct params', async function () { + await server.runnerJobs.list({ start: 0, count: 5, sort: '-createdAt', stateOneOf: [ RunnerJobState.COMPLETED ] }) + }) + }) + + describe('Delete', function () { + let jobUUID: string + + before(async function () { + this.timeout(60000) + + await server.videos.quickUpload({ name: 'video' }) + await waitJobs([ server ]) + + const { availableJobs } = await server.runnerJobs.request({ runnerToken }) + jobUUID = availableJobs[0].uuid + }) + + it('Should fail without oauth token', async function () { + await server.runnerJobs.deleteByAdmin({ token: null, jobUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail without admin rights', async function () { + await server.runnerJobs.deleteByAdmin({ token: userToken, jobUUID, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad job uuid', async function () { + await server.runnerJobs.deleteByAdmin({ jobUUID: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown job uuid', async function () { + const jobUUID = badUUID + await server.runnerJobs.deleteByAdmin({ jobUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct params', async function () { + await server.runnerJobs.deleteByAdmin({ jobUUID }) + }) + }) + + }) + + describe('Runner jobs by runners', function () { + let jobUUID: string + let jobToken: string + let videoUUID: string + + let jobUUID2: string + let jobToken2: string + + let videoUUID2: string + + let pendingUUID: string + + let videoStudioUUID: string + let studioFile: string + + let liveAcceptedJob: RunnerJob & { jobToken: string } + let studioAcceptedJob: RunnerJob & { jobToken: string } + + async function fetchVideoInputFiles (options: { + jobUUID: string + videoUUID: string + runnerToken: string + jobToken: string + expectedStatus: HttpStatusCodeType + }) { + const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken } = options + + const basePath = '/api/v1/runners/jobs/' + jobUUID + '/files/videos/' + videoUUID + const paths = [ `${basePath}/max-quality`, `${basePath}/previews/max-quality` ] + + for (const path of paths) { + await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus }) + } + } + + async function fetchStudioFiles (options: { + jobUUID: string + videoUUID: string + runnerToken: string + jobToken: string + studioFile?: string + expectedStatus: HttpStatusCodeType + }) { + const { jobUUID, expectedStatus, videoUUID, runnerToken, jobToken, studioFile } = options + + const path = `/api/v1/runners/jobs/${jobUUID}/files/videos/${videoUUID}/studio/task-files/${studioFile}` + + await makePostBodyRequest({ url: server.url, path, fields: { runnerToken, jobToken }, expectedStatus }) + } + + before(async function () { + this.timeout(120000) + + { + await server.runnerJobs.cancelAllJobs({ state: RunnerJobState.PENDING }) + } + + { + const { uuid } = await server.videos.quickUpload({ name: 'video' }) + videoUUID = uuid + + await waitJobs([ server ]) + + const { job } = await server.runnerJobs.autoAccept({ runnerToken }) + jobUUID = job.uuid + jobToken = job.jobToken + } + + { + const { uuid } = await server.videos.quickUpload({ name: 'video' }) + videoUUID2 = uuid + + await waitJobs([ server ]) + + const { job } = await server.runnerJobs.autoAccept({ runnerToken: runnerToken2 }) + jobUUID2 = job.uuid + jobToken2 = job.jobToken + } + + { + await server.videos.quickUpload({ name: 'video' }) + await waitJobs([ server ]) + + const { availableJobs } = await server.runnerJobs.request({ runnerToken }) + pendingUUID = availableJobs[0].uuid + } + + { + await server.config.disableTranscoding() + + const { uuid } = await server.videos.quickUpload({ name: 'video studio' }) + videoStudioUUID = uuid + + await server.config.enableTranscoding({ hls: true, webVideo: true }) + await server.config.enableStudio() + + await server.videoStudio.createEditionTasks({ + videoId: videoStudioUUID, + tasks: VideoStudioCommand.getComplexTask() + }) + + const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'video-studio-transcoding' }) + studioAcceptedJob = job + + const tasks = (job.payload as RunnerJobStudioTranscodingPayload).tasks + const fileUrl = (tasks.find(t => isVideoStudioTaskIntro(t)) as VideoStudioTaskIntro).options.file as string + studioFile = basename(fileUrl) + } + + { + await server.config.enableLive({ + allowReplay: false, + resolutions: 'max', + transcoding: true + }) + + const { live } = await server.live.quickCreate({ permanentLive: true, saveReplay: false, privacy: VideoPrivacy.PUBLIC }) + + const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) + await waitJobs([ server ]) + + await server.runnerJobs.requestLiveJob(runnerToken) + + const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'live-rtmp-hls-transcoding' }) + liveAcceptedJob = job + + await stopFfmpeg(ffmpegCommand) + } + }) + + describe('Common runner tokens validations', function () { + + async function testEndpoints (options: { + jobUUID: string + runnerToken: string + jobToken: string + expectedStatus: HttpStatusCodeType + }) { + await server.runnerJobs.abort({ ...options, reason: 'reason' }) + await server.runnerJobs.update({ ...options }) + await server.runnerJobs.error({ ...options, message: 'message' }) + await server.runnerJobs.success({ ...options, payload: { videoFile: 'video_short.mp4' } }) + } + + it('Should fail with an invalid job uuid', async function () { + const options = { jobUUID: 'a', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 } + + await testEndpoints({ ...options, jobToken }) + await fetchVideoInputFiles({ ...options, videoUUID, jobToken }) + await fetchStudioFiles({ ...options, videoUUID, jobToken: studioAcceptedJob.jobToken, studioFile }) + }) + + it('Should fail with an unknown job uuid', async function () { + const options = { jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + + await testEndpoints({ ...options, jobToken }) + await fetchVideoInputFiles({ ...options, videoUUID, jobToken }) + await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID, studioFile }) + }) + + it('Should fail with an invalid runner token', async function () { + const options = { runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 } + + await testEndpoints({ ...options, jobUUID, jobToken }) + await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken }) + await fetchStudioFiles({ + ...options, + jobToken: studioAcceptedJob.jobToken, + jobUUID: studioAcceptedJob.uuid, + videoUUID: videoStudioUUID, + studioFile + }) + }) + + it('Should fail with an unknown runner token', async function () { + const options = { runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + + await testEndpoints({ ...options, jobUUID, jobToken }) + await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken }) + await fetchStudioFiles({ + ...options, + jobToken: studioAcceptedJob.jobToken, + jobUUID: studioAcceptedJob.uuid, + videoUUID: videoStudioUUID, + studioFile + }) + }) + + it('Should fail with an invalid job token job uuid', async function () { + const options = { runnerToken, jobToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 } + + await testEndpoints({ ...options, jobUUID }) + await fetchVideoInputFiles({ ...options, jobUUID, videoUUID }) + await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile }) + }) + + it('Should fail with an unknown job token job uuid', async function () { + const options = { runnerToken, jobToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + + await testEndpoints({ ...options, jobUUID }) + await fetchVideoInputFiles({ ...options, jobUUID, videoUUID }) + await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile }) + }) + + it('Should fail with a runner token not associated to this job', async function () { + const options = { runnerToken: runnerToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + + await testEndpoints({ ...options, jobUUID, jobToken }) + await fetchVideoInputFiles({ ...options, jobUUID, videoUUID, jobToken }) + await fetchStudioFiles({ + ...options, + jobToken: studioAcceptedJob.jobToken, + jobUUID: studioAcceptedJob.uuid, + videoUUID: videoStudioUUID, + studioFile + }) + }) + + it('Should fail with a job uuid not associated to the job token', async function () { + { + const options = { jobUUID: jobUUID2, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + + await testEndpoints({ ...options, jobToken }) + await fetchVideoInputFiles({ ...options, jobToken, videoUUID }) + await fetchStudioFiles({ ...options, jobToken: studioAcceptedJob.jobToken, videoUUID: videoStudioUUID, studioFile }) + } + + { + const options = { runnerToken, jobToken: jobToken2, expectedStatus: HttpStatusCode.NOT_FOUND_404 } + + await testEndpoints({ ...options, jobUUID }) + await fetchVideoInputFiles({ ...options, jobUUID, videoUUID }) + await fetchStudioFiles({ ...options, jobUUID: studioAcceptedJob.uuid, videoUUID: videoStudioUUID, studioFile }) + } + }) + }) + + describe('Unregister', function () { + + it('Should fail without a runner token', async function () { + await server.runners.unregister({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a bad a runner token', async function () { + await server.runners.unregister({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown runner token', async function () { + await server.runners.unregister({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + }) + + describe('Request', function () { + + it('Should fail without a runner token', async function () { + await server.runnerJobs.request({ runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a bad a runner token', async function () { + await server.runnerJobs.request({ runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown runner token', async function () { + await server.runnerJobs.request({ runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + }) + + describe('Accept', function () { + + it('Should fail with a bad a job uuid', async function () { + await server.runnerJobs.accept({ jobUUID: '', runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown job uuid', async function () { + await server.runnerJobs.accept({ jobUUID: badUUID, runnerToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a job not in pending state', async function () { + await server.runnerJobs.accept({ jobUUID: completedJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.runnerJobs.accept({ jobUUID: cancelledJobUUID, runnerToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail without a runner token', async function () { + await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a bad a runner token', async function () { + await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown runner token', async function () { + await server.runnerJobs.accept({ jobUUID: pendingUUID, runnerToken: badUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + }) + + describe('Abort', function () { + + it('Should fail without a reason', async function () { + await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a bad reason', async function () { + const reason = 'reason'.repeat(5000) + await server.runnerJobs.abort({ jobUUID, jobToken, runnerToken, reason, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a job not in processing state', async function () { + await server.runnerJobs.abort({ + jobUUID: completedJobUUID, + jobToken: completedJobToken, + runnerToken, + reason: 'reason', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + + describe('Update', function () { + + describe('Common', function () { + + it('Should fail with an invalid progress', async function () { + await server.runnerJobs.update({ jobUUID, jobToken, runnerToken, progress: 101, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a job not in processing state', async function () { + await server.runnerJobs.update({ + jobUUID: cancelledJobUUID, + jobToken: cancelledJobToken, + runnerToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + }) + + describe('Live RTMP to HLS', function () { + const base: RunnerJobUpdatePayload = { + masterPlaylistFile: 'live/master.m3u8', + resolutionPlaylistFilename: '0.m3u8', + resolutionPlaylistFile: 'live/1.m3u8', + type: 'add-chunk', + videoChunkFile: 'live/1-000069.ts', + videoChunkFilename: '1-000068.ts' + } + + function testUpdate (payload: RunnerJobUpdatePayload) { + return server.runnerJobs.update({ + jobUUID: liveAcceptedJob.uuid, + jobToken: liveAcceptedJob.jobToken, + payload, + runnerToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + + it('Should fail with an invalid resolutionPlaylistFilename', async function () { + await testUpdate({ ...base, resolutionPlaylistFilename: undefined }) + await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' }) + await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' }) + }) + + it('Should fail with an invalid videoChunkFilename', async function () { + await testUpdate({ ...base, resolutionPlaylistFilename: undefined }) + await testUpdate({ ...base, resolutionPlaylistFilename: 'coucou/hello' }) + await testUpdate({ ...base, resolutionPlaylistFilename: 'hello' }) + }) + + it('Should fail with an invalid type', async function () { + await testUpdate({ ...base, type: undefined }) + await testUpdate({ ...base, type: 'toto' as any }) + }) + }) + }) + + describe('Error', function () { + + it('Should fail with a missing error message', async function () { + await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message: null, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an invalid error messgae', async function () { + const message = 'a'.repeat(6000) + await server.runnerJobs.error({ jobUUID, jobToken, runnerToken, message, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a job not in processing state', async function () { + await server.runnerJobs.error({ + jobUUID: completedJobUUID, + jobToken: completedJobToken, + message: 'my message', + runnerToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + + describe('Success', function () { + let vodJobUUID: string + let vodJobToken: string + + describe('Common', function () { + + it('Should fail with a job not in processing state', async function () { + await server.runnerJobs.success({ + jobUUID: completedJobUUID, + jobToken: completedJobToken, + payload: { videoFile: 'video_short.mp4' }, + runnerToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + + describe('VOD', function () { + + it('Should fail with an invalid vod web video payload', async function () { + const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-web-video-transcoding' }) + + await server.runnerJobs.success({ + jobUUID: job.uuid, + jobToken: job.jobToken, + payload: { hello: 'video_short.mp4' } as any, + runnerToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + vodJobUUID = job.uuid + vodJobToken = job.jobToken + }) + + it('Should fail with an invalid vod hls payload', async function () { + // To create HLS jobs + const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' } + await server.runnerJobs.success({ runnerToken, jobUUID: vodJobUUID, jobToken: vodJobToken, payload }) + + await waitJobs([ server ]) + + const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-hls-transcoding' }) + + await server.runnerJobs.success({ + jobUUID: job.uuid, + jobToken: job.jobToken, + payload: { videoFile: 'video_short.mp4' } as any, + runnerToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an invalid vod audio merge payload', async function () { + const attributes = { name: 'audio_with_preview', previewfile: 'custom-preview.jpg', fixture: 'sample.ogg' } + await server.videos.upload({ attributes, mode: 'legacy' }) + + await waitJobs([ server ]) + + const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'vod-audio-merge-transcoding' }) + + await server.runnerJobs.success({ + jobUUID: job.uuid, + jobToken: job.jobToken, + payload: { hello: 'video_short.mp4' } as any, + runnerToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + + describe('Video studio', function () { + + it('Should fail with an invalid video studio transcoding payload', async function () { + await server.runnerJobs.success({ + jobUUID: studioAcceptedJob.uuid, + jobToken: studioAcceptedJob.jobToken, + payload: { hello: 'video_short.mp4' } as any, + runnerToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + }) + + describe('Job files', function () { + + describe('Check video param for common job file routes', function () { + + async function fetchFiles (options: { + videoUUID?: string + expectedStatus: HttpStatusCodeType + }) { + await fetchVideoInputFiles({ videoUUID, ...options, jobToken, jobUUID, runnerToken }) + + await fetchStudioFiles({ + videoUUID: videoStudioUUID, + + ...options, + + jobToken: studioAcceptedJob.jobToken, + jobUUID: studioAcceptedJob.uuid, + runnerToken, + studioFile + }) + } + + it('Should fail with an invalid video id', async function () { + await fetchFiles({ + videoUUID: 'a', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown video id', async function () { + const videoUUID = '910ec12a-d9e6-458b-a274-0abb655f9464' + + await fetchFiles({ + videoUUID, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a video id not associated to this job', async function () { + await fetchFiles({ + videoUUID: videoUUID2, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + await fetchFiles({ expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('Video studio tasks file routes', function () { + + it('Should fail with an invalid studio filename', async function () { + await fetchStudioFiles({ + videoUUID: videoStudioUUID, + jobUUID: studioAcceptedJob.uuid, + runnerToken, + jobToken: studioAcceptedJob.jobToken, + studioFile: 'toto', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/search.ts b/packages/tests/src/api/check-params/search.ts new file mode 100644 index 000000000..b886cbc82 --- /dev/null +++ b/packages/tests/src/api/check-params/search.ts @@ -0,0 +1,278 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode } from '@peertube/peertube-models' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +function updateSearchIndex (server: PeerTubeServer, enabled: boolean, disableLocalSearch = false) { + return server.config.updateCustomSubConfig({ + newConfig: { + search: { + searchIndex: { + enabled, + disableLocalSearch + } + } + } + }) +} + +describe('Test videos API validator', function () { + let server: PeerTubeServer + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + }) + + describe('When searching videos', function () { + const path = '/api/v1/search/videos/' + + 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 succeed with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should fail with an invalid category', async function () { + const customQuery1 = { ...query, categoryOneOf: [ 'aa', 'b' ] } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + const customQuery2 = { ...query, categoryOneOf: 'a' } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with a valid category', async function () { + const customQuery1 = { ...query, categoryOneOf: [ 1, 7 ] } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) + + const customQuery2 = { ...query, categoryOneOf: 1 } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should fail with an invalid licence', async function () { + const customQuery1 = { ...query, licenceOneOf: [ 'aa', 'b' ] } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + const customQuery2 = { ...query, licenceOneOf: 'a' } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with a valid licence', async function () { + const customQuery1 = { ...query, licenceOneOf: [ 1, 2 ] } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) + + const customQuery2 = { ...query, licenceOneOf: 1 } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should succeed with a valid language', async function () { + const customQuery1 = { ...query, languageOneOf: [ 'fr', 'en' ] } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) + + const customQuery2 = { ...query, languageOneOf: 'fr' } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should succeed with valid tags', async function () { + const customQuery1 = { ...query, tagsOneOf: [ 'tag1', 'tag2' ] } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.OK_200 }) + + const customQuery2 = { ...query, tagsOneOf: 'tag1' } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.OK_200 }) + + const customQuery3 = { ...query, tagsAllOf: [ 'tag1', 'tag2' ] } + await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.OK_200 }) + + const customQuery4 = { ...query, tagsAllOf: 'tag1' } + await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should fail with invalid durations', async function () { + const customQuery1 = { ...query, durationMin: 'hello' } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + const customQuery2 = { ...query, durationMax: 'hello' } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with invalid dates', async function () { + const customQuery1 = { ...query, startDate: 'hello' } + await makeGetRequest({ url: server.url, path, query: customQuery1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + const customQuery2 = { ...query, endDate: 'hello' } + await makeGetRequest({ url: server.url, path, query: customQuery2, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + const customQuery3 = { ...query, originallyPublishedStartDate: 'hello' } + await makeGetRequest({ url: server.url, path, query: customQuery3, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + const customQuery4 = { ...query, originallyPublishedEndDate: 'hello' } + await makeGetRequest({ url: server.url, path, query: customQuery4, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an invalid host', async function () { + const customQuery = { ...query, host: '6565' } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with a host', async function () { + const customQuery = { ...query, host: 'example.com' } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should fail with invalid uuids', async function () { + const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with valid uuids', async function () { + const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When searching video playlists', function () { + const path = '/api/v1/search/video-playlists/' + + const query = { + search: 'coucou', + host: 'example.com' + } + + 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 fail with an invalid host', async function () { + await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with invalid uuids', async function () { + const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When searching video channels', function () { + const path = '/api/v1/search/video-channels/' + + const query = { + search: 'coucou', + host: 'example.com' + } + + 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 fail with an invalid host', async function () { + await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with invalid handles', async function () { + await makeGetRequest({ url: server.url, path, query: { ...query, handles: [ '' ] }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('Search target', function () { + + it('Should fail/succeed depending on the search target', async function () { + const query = { search: 'coucou' } + const paths = [ + '/api/v1/search/video-playlists/', + '/api/v1/search/video-channels/', + '/api/v1/search/videos/' + ] + + for (const path of paths) { + { + const customQuery = { ...query, searchTarget: 'hello' } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + } + + { + const customQuery = { ...query, searchTarget: undefined } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) + } + + { + const customQuery = { ...query, searchTarget: 'local' } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) + } + + { + const customQuery = { ...query, searchTarget: 'search-index' } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + } + + await updateSearchIndex(server, true, true) + + { + const customQuery = { ...query, searchTarget: 'search-index' } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) + } + + await updateSearchIndex(server, true, false) + + { + const customQuery = { ...query, searchTarget: 'local' } + await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) + } + + await updateSearchIndex(server, false, false) + } + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/services.ts b/packages/tests/src/api/check-params/services.ts new file mode 100644 index 000000000..0b0466d84 --- /dev/null +++ b/packages/tests/src/api/check-params/services.ts @@ -0,0 +1,207 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { + HttpStatusCode, + HttpStatusCodeType, + VideoCreateResult, + VideoPlaylistCreateResult, + VideoPlaylistPrivacy, + VideoPrivacy +} from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel +} from '@peertube/peertube-server-commands' + +describe('Test services API validators', function () { + let server: PeerTubeServer + let playlistUUID: string + + let privateVideo: VideoCreateResult + let unlistedVideo: VideoCreateResult + + let privatePlaylist: VideoPlaylistCreateResult + let unlistedPlaylist: VideoPlaylistCreateResult + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(60000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + server.store.videoCreated = await server.videos.upload({ attributes: { name: 'my super name' } }) + + privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE }) + unlistedVideo = await server.videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED }) + + { + const created = await server.playlists.create({ + attributes: { + displayName: 'super playlist', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: server.store.channel.id + } + }) + + playlistUUID = created.uuid + + privatePlaylist = await server.playlists.create({ + attributes: { + displayName: 'private', + privacy: VideoPlaylistPrivacy.PRIVATE, + videoChannelId: server.store.channel.id + } + }) + + unlistedPlaylist = await server.playlists.create({ + attributes: { + displayName: 'unlisted', + privacy: VideoPlaylistPrivacy.UNLISTED, + videoChannelId: server.store.channel.id + } + }) + } + }) + + describe('Test oEmbed API validators', function () { + + it('Should fail with an invalid url', async function () { + const embedUrl = 'hello.com' + await checkParamEmbed(server, embedUrl) + }) + + it('Should fail with an invalid host', async function () { + const embedUrl = 'http://hello.com/videos/watch/' + server.store.videoCreated.uuid + await checkParamEmbed(server, embedUrl) + }) + + it('Should fail with an invalid element id', async function () { + const embedUrl = `${server.url}/videos/watch/blabla` + await checkParamEmbed(server, embedUrl) + }) + + it('Should fail with an unknown element', async function () { + const embedUrl = `${server.url}/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c` + await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_FOUND_404) + }) + + it('Should fail with an invalid path', async function () { + const embedUrl = `${server.url}/videos/watchs/${server.store.videoCreated.uuid}` + + await checkParamEmbed(server, embedUrl) + }) + + it('Should fail with an invalid max height', async function () { + const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxheight: 'hello' }) + }) + + it('Should fail with an invalid max width', async function () { + const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { maxwidth: 'hello' }) + }) + + it('Should fail with an invalid format', async function () { + const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.BAD_REQUEST_400, { format: 'blabla' }) + }) + + it('Should fail with a non supported format', async function () { + const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.NOT_IMPLEMENTED_501, { format: 'xml' }) + }) + + it('Should fail with a private video', async function () { + const embedUrl = `${server.url}/videos/watch/${privateVideo.uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) + }) + + it('Should fail with an unlisted video with the int id', async function () { + const embedUrl = `${server.url}/videos/watch/${unlistedVideo.id}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) + }) + + it('Should succeed with an unlisted video using the uuid id', async function () { + for (const uuid of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) { + const embedUrl = `${server.url}/videos/watch/${uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200) + } + }) + + it('Should fail with a private playlist', async function () { + const embedUrl = `${server.url}/videos/watch/playlist/${privatePlaylist.uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) + }) + + it('Should fail with an unlisted playlist using the int id', async function () { + const embedUrl = `${server.url}/videos/watch/playlist/${unlistedPlaylist.id}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.FORBIDDEN_403) + }) + + it('Should succeed with an unlisted playlist using the uuid id', async function () { + for (const uuid of [ unlistedPlaylist.uuid, unlistedPlaylist.shortUUID ]) { + const embedUrl = `${server.url}/videos/watch/playlist/${uuid}` + + await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200) + } + }) + + it('Should succeed with the correct params with a video', async function () { + const embedUrl = `${server.url}/videos/watch/${server.store.videoCreated.uuid}` + const query = { + format: 'json', + maxheight: 400, + maxwidth: 400 + } + + await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query) + }) + + it('Should succeed with the correct params with a playlist', async function () { + const embedUrl = `${server.url}/videos/watch/playlist/${playlistUUID}` + const query = { + format: 'json', + maxheight: 400, + maxwidth: 400 + } + + await checkParamEmbed(server, embedUrl, HttpStatusCode.OK_200, query) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) + +function checkParamEmbed ( + server: PeerTubeServer, + embedUrl: string, + expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400, + query = {} +) { + const path = '/services/oembed' + + return makeGetRequest({ + url: server.url, + path, + query: Object.assign(query, { url: embedUrl }), + expectedStatus + }) +} diff --git a/packages/tests/src/api/check-params/transcoding.ts b/packages/tests/src/api/check-params/transcoding.ts new file mode 100644 index 000000000..50935c59e --- /dev/null +++ b/packages/tests/src/api/check-params/transcoding.ts @@ -0,0 +1,112 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode, UserRole } from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test transcoding API validators', function () { + let servers: PeerTubeServer[] + + let userToken: string + let moderatorToken: string + + let remoteId: string + let validId: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + + await doubleFollow(servers[0], servers[1]) + + userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) + moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) + + { + const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) + remoteId = uuid + } + + { + const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) + validId = uuid + } + + await waitJobs(servers) + + await servers[0].config.enableTranscoding() + }) + + it('Should not run transcoding of a unknown video', async function () { + await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'hls', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await servers[0].videos.runTranscoding({ videoId: 404, transcodingType: 'web-video', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should not run transcoding of a remote video', async function () { + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'hls', expectedStatus }) + await servers[0].videos.runTranscoding({ videoId: remoteId, transcodingType: 'web-video', expectedStatus }) + }) + + it('Should not run transcoding by a non admin user', async function () { + const expectedStatus = HttpStatusCode.FORBIDDEN_403 + + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', token: userToken, expectedStatus }) + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', token: moderatorToken, expectedStatus }) + }) + + it('Should not run transcoding without transcoding type', async function () { + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should not run transcoding with an incorrect transcoding type', async function () { + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'toto' as any, expectedStatus }) + }) + + it('Should not run transcoding if the instance disabled it', async function () { + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + await servers[0].config.disableTranscoding() + + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls', expectedStatus }) + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) + }) + + it('Should run transcoding', async function () { + this.timeout(120_000) + + await servers[0].config.enableTranscoding() + + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'hls' }) + await waitJobs(servers) + + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true }) + await waitJobs(servers) + }) + + it('Should not run transcoding on a video that is already being transcoded if forceTranscoding is not set', async function () { + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video' }) + + const expectedStatus = HttpStatusCode.CONFLICT_409 + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', expectedStatus }) + + await servers[0].videos.runTranscoding({ videoId: validId, transcodingType: 'web-video', forceTranscoding: true }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/api/check-params/two-factor.ts b/packages/tests/src/api/check-params/two-factor.ts new file mode 100644 index 000000000..0b1766eca --- /dev/null +++ b/packages/tests/src/api/check-params/two-factor.ts @@ -0,0 +1,294 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + setAccessTokensToServers, + TwoFactorCommand +} from '@peertube/peertube-server-commands' + +describe('Test two factor API validators', function () { + let server: PeerTubeServer + + let rootId: number + let rootPassword: string + let rootRequestToken: string + let rootOTPToken: string + + let userId: number + let userToken = '' + let userPassword: string + let userRequestToken: string + let userOTPToken: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + { + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + } + + { + const result = await server.users.generate('user1') + userToken = result.token + userId = result.userId + userPassword = result.password + } + + { + const { id } = await server.users.getMyInfo() + rootId = id + rootPassword = server.store.user.password + } + }) + + describe('When requesting two factor', function () { + + it('Should fail with an unknown user id', async function () { + await server.twoFactor.request({ userId: 42, currentPassword: rootPassword, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with an invalid user id', async function () { + await server.twoFactor.request({ + userId: 'invalid' as any, + currentPassword: rootPassword, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail to request another user two factor without the appropriate rights', async function () { + await server.twoFactor.request({ + userId: rootId, + token: userToken, + currentPassword: userPassword, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed to request another user two factor with the appropriate rights', async function () { + await server.twoFactor.request({ userId, currentPassword: rootPassword }) + }) + + it('Should fail to request two factor without a password', async function () { + await server.twoFactor.request({ + userId, + token: userToken, + currentPassword: undefined, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail to request two factor with an incorrect password', async function () { + await server.twoFactor.request({ + userId, + token: userToken, + currentPassword: rootPassword, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed to request two factor without a password when targeting a remote user with an admin account', async function () { + await server.twoFactor.request({ userId }) + }) + + it('Should fail to request two factor without a password when targeting myself with an admin account', async function () { + await server.twoFactor.request({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.twoFactor.request({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed to request my two factor auth', async function () { + { + const { otpRequest } = await server.twoFactor.request({ userId, token: userToken, currentPassword: userPassword }) + userRequestToken = otpRequest.requestToken + userOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() + } + + { + const { otpRequest } = await server.twoFactor.request({ userId: rootId, currentPassword: rootPassword }) + rootRequestToken = otpRequest.requestToken + rootOTPToken = TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate() + } + }) + }) + + describe('When confirming two factor request', function () { + + it('Should fail with an unknown user id', async function () { + await server.twoFactor.confirmRequest({ + userId: 42, + requestToken: rootRequestToken, + otpToken: rootOTPToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with an invalid user id', async function () { + await server.twoFactor.confirmRequest({ + userId: 'invalid' as any, + requestToken: rootRequestToken, + otpToken: rootOTPToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail to confirm another user two factor request without the appropriate rights', async function () { + await server.twoFactor.confirmRequest({ + userId: rootId, + token: userToken, + requestToken: rootRequestToken, + otpToken: rootOTPToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail without request token', async function () { + await server.twoFactor.confirmRequest({ + userId, + requestToken: undefined, + otpToken: userOTPToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an invalid request token', async function () { + await server.twoFactor.confirmRequest({ + userId, + requestToken: 'toto', + otpToken: userOTPToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with request token of another user', async function () { + await server.twoFactor.confirmRequest({ + userId, + requestToken: rootRequestToken, + otpToken: userOTPToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail without an otp token', async function () { + await server.twoFactor.confirmRequest({ + userId, + requestToken: userRequestToken, + otpToken: undefined, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad otp token', async function () { + await server.twoFactor.confirmRequest({ + userId, + requestToken: userRequestToken, + otpToken: '123456', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed to confirm another user two factor request with the appropriate rights', async function () { + await server.twoFactor.confirmRequest({ + userId, + requestToken: userRequestToken, + otpToken: userOTPToken + }) + + // Reinit + await server.twoFactor.disable({ userId, currentPassword: rootPassword }) + }) + + it('Should succeed to confirm my two factor request', async function () { + await server.twoFactor.confirmRequest({ + userId, + token: userToken, + requestToken: userRequestToken, + otpToken: userOTPToken + }) + }) + + it('Should fail to confirm again two factor request', async function () { + await server.twoFactor.confirmRequest({ + userId, + token: userToken, + requestToken: userRequestToken, + otpToken: userOTPToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + + describe('When disabling two factor', function () { + + it('Should fail with an unknown user id', async function () { + await server.twoFactor.disable({ + userId: 42, + currentPassword: rootPassword, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with an invalid user id', async function () { + await server.twoFactor.disable({ + userId: 'invalid' as any, + currentPassword: rootPassword, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail to disable another user two factor without the appropriate rights', async function () { + await server.twoFactor.disable({ + userId: rootId, + token: userToken, + currentPassword: userPassword, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail to disable two factor with an incorrect password', async function () { + await server.twoFactor.disable({ + userId, + token: userToken, + currentPassword: rootPassword, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed to disable two factor without a password when targeting a remote user with an admin account', async function () { + await server.twoFactor.disable({ userId }) + await server.twoFactor.requestAndConfirm({ userId }) + }) + + it('Should fail to disable two factor without a password when targeting myself with an admin account', async function () { + await server.twoFactor.disable({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.twoFactor.disable({ userId: rootId, currentPassword: 'bad', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed to disable another user two factor with the appropriate rights', async function () { + await server.twoFactor.disable({ userId, currentPassword: rootPassword }) + + await server.twoFactor.requestAndConfirm({ userId }) + }) + + it('Should succeed to update my two factor auth', async function () { + await server.twoFactor.disable({ userId, token: userToken, currentPassword: userPassword }) + }) + + it('Should fail to disable again two factor', async function () { + await server.twoFactor.disable({ + userId, + token: userToken, + currentPassword: userPassword, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/upload-quota.ts b/packages/tests/src/api/check-params/upload-quota.ts new file mode 100644 index 000000000..a77792822 --- /dev/null +++ b/packages/tests/src/api/check-params/upload-quota.ts @@ -0,0 +1,134 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { FIXTURE_URLS } from '@tests/shared/tests.js' +import { randomInt } from '@peertube/peertube-core-utils' +import { HttpStatusCode, VideoImportState, VideoPrivacy } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel, + VideosCommand, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test upload quota', function () { + let server: PeerTubeServer + let rootId: number + let command: VideosCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + const user = await server.users.getMyInfo() + rootId = user.id + + await server.users.update({ userId: rootId, videoQuota: 42 }) + + command = server.videos + }) + + describe('When having a video quota', function () { + + it('Should fail with a registered user having too many videos with legacy upload', async function () { + this.timeout(120000) + + const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } + await server.registrations.register(user) + const userToken = await server.login.getAccessToken(user) + + const attributes = { fixture: 'video_short2.webm' } + for (let i = 0; i < 5; i++) { + await command.upload({ token: userToken, attributes }) + } + + await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) + }) + + it('Should fail with a registered user having too many videos with resumable upload', async function () { + this.timeout(120000) + + const user = { username: 'registered' + randomInt(1, 1500), password: 'password' } + await server.registrations.register(user) + const userToken = await server.login.getAccessToken(user) + + const attributes = { fixture: 'video_short2.webm' } + for (let i = 0; i < 5; i++) { + await command.upload({ token: userToken, attributes }) + } + + await command.upload({ token: userToken, attributes, expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) + }) + + it('Should fail to import with HTTP/Torrent/magnet', async function () { + this.timeout(120_000) + + const baseAttributes = { + channelId: server.store.channel.id, + privacy: VideoPrivacy.PUBLIC + } + await server.imports.importVideo({ attributes: { ...baseAttributes, targetUrl: FIXTURE_URLS.goodVideo } }) + await server.imports.importVideo({ attributes: { ...baseAttributes, magnetUri: FIXTURE_URLS.magnet } }) + await server.imports.importVideo({ attributes: { ...baseAttributes, torrentfile: 'video-720p.torrent' as any } }) + + await waitJobs([ server ]) + + const { total, data: videoImports } = await server.imports.getMyVideoImports() + expect(total).to.equal(3) + + expect(videoImports).to.have.lengthOf(3) + + for (const videoImport of videoImports) { + expect(videoImport.state.id).to.equal(VideoImportState.FAILED) + expect(videoImport.error).not.to.be.undefined + expect(videoImport.error).to.contain('user video quota is exceeded') + } + }) + }) + + describe('When having a daily video quota', function () { + + it('Should fail with a user having too many videos daily', async function () { + await server.users.update({ userId: rootId, videoQuotaDaily: 42 }) + + await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) + await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) + }) + }) + + describe('When having an absolute and daily video quota', function () { + it('Should fail if exceeding total quota', async function () { + await server.users.update({ + userId: rootId, + videoQuota: 42, + videoQuotaDaily: 1024 * 1024 * 1024 + }) + + await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) + await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) + }) + + it('Should fail if exceeding daily quota', async function () { + await server.users.update({ + userId: rootId, + videoQuota: 1024 * 1024 * 1024, + videoQuotaDaily: 42 + }) + + await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'legacy' }) + await command.upload({ expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413, mode: 'resumable' }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/user-notifications.ts b/packages/tests/src/api/check-params/user-notifications.ts new file mode 100644 index 000000000..cf20324a1 --- /dev/null +++ b/packages/tests/src/api/check-params/user-notifications.ts @@ -0,0 +1,290 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { io } from 'socket.io-client' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { wait } from '@peertube/peertube-core-utils' +import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test user notifications API validators', function () { + let server: PeerTubeServer + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + }) + + describe('When listing my notifications', function () { + const path = '/api/v1/users/me/notifications' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect unread parameter', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { + unread: 'toto' + }, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When marking as read my notifications', function () { + const path = '/api/v1/users/me/notifications/read' + + it('Should fail with wrong ids parameters', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { + ids: [ 'hello' ] + }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { + ids: [ ] + }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makePostBodyRequest({ + url: server.url, + path, + fields: { + ids: 5 + }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a non authenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { + ids: [ 5 ] + }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { + ids: [ 5 ] + }, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When marking as read my notifications', function () { + const path = '/api/v1/users/me/notifications/read-all' + + it('Should fail with a non authenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When updating my notification settings', function () { + const path = '/api/v1/users/me/notification-settings' + const correctFields: UserNotificationSetting = { + newVideoFromSubscription: UserNotificationSettingValue.WEB, + newCommentOnMyVideo: UserNotificationSettingValue.WEB, + abuseAsModerator: UserNotificationSettingValue.WEB, + videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB, + blacklistOnMyVideo: UserNotificationSettingValue.WEB, + myVideoImportFinished: UserNotificationSettingValue.WEB, + myVideoPublished: UserNotificationSettingValue.WEB, + commentMention: UserNotificationSettingValue.WEB, + newFollow: UserNotificationSettingValue.WEB, + newUserRegistration: UserNotificationSettingValue.WEB, + newInstanceFollower: UserNotificationSettingValue.WEB, + autoInstanceFollowing: UserNotificationSettingValue.WEB, + abuseNewMessage: UserNotificationSettingValue.WEB, + abuseStateChange: UserNotificationSettingValue.WEB, + newPeerTubeVersion: UserNotificationSettingValue.WEB, + myVideoStudioEditionFinished: UserNotificationSettingValue.WEB, + newPluginVersion: UserNotificationSettingValue.WEB + } + + it('Should fail with missing fields', async function () { + await makePutBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with incorrect field values', async function () { + { + const fields = { ...correctFields, newCommentOnMyVideo: 15 } + + await makePutBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + + { + const fields = { ...correctFields, newCommentOnMyVideo: 'toto' } + + await makePutBodyRequest({ + url: server.url, + path, + fields, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + } + }) + + it('Should fail with a non authenticated user', async function () { + await makePutBodyRequest({ + url: server.url, + path, + fields: correctFields, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePutBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: correctFields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When connecting to my notification socket', function () { + + it('Should fail with no token', function (next) { + const socket = io(`${server.url}/user-notifications`, { reconnection: false }) + + socket.once('connect_error', function () { + socket.disconnect() + next() + }) + + socket.on('connect', () => { + socket.disconnect() + next(new Error('Connected with a missing token.')) + }) + }) + + it('Should fail with an invalid token', function (next) { + const socket = io(`${server.url}/user-notifications`, { + query: { accessToken: 'bad_access_token' }, + reconnection: false + }) + + socket.once('connect_error', function () { + socket.disconnect() + next() + }) + + socket.on('connect', () => { + socket.disconnect() + next(new Error('Connected with an invalid token.')) + }) + }) + + it('Should success with the correct token', function (next) { + const socket = io(`${server.url}/user-notifications`, { + query: { accessToken: server.accessToken }, + reconnection: false + }) + + function errorListener (err) { + next(new Error('Error in connection: ' + err)) + } + + socket.on('connect_error', errorListener) + + socket.once('connect', async () => { + socket.disconnect() + + await wait(500) + next() + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/user-subscriptions.ts b/packages/tests/src/api/check-params/user-subscriptions.ts new file mode 100644 index 000000000..e97f513a0 --- /dev/null +++ b/packages/tests/src/api/check-params/user-subscriptions.ts @@ -0,0 +1,298 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { + cleanupTests, + createSingleServer, + makeDeleteRequest, + makeGetRequest, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' +import { HttpStatusCode } from '@peertube/peertube-models' +import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js' + +describe('Test user subscriptions API validators', function () { + const path = '/api/v1/users/me/subscriptions' + let server: PeerTubeServer + let userAccessToken = '' + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const user = { + username: 'user1', + password: 'my super password' + } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + }) + + describe('When listing my subscriptions', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When listing my subscriptions videos', function () { + const path = '/api/v1/users/me/subscriptions/videos' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When adding a subscription', function () { + it('Should fail with a non authenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + fields: { uri: 'user1_channel@' + server.host }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with bad URIs', async function () { + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: { uri: 'root' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: { uri: 'root@' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: { uri: 'root@hello@' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + this.timeout(20000) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: { uri: 'user1_channel@' + server.host }, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + + await waitJobs([ server ]) + }) + }) + + describe('When getting a subscription', function () { + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path: path + '/user1_channel@' + server.host, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with bad URIs', async function () { + await makeGetRequest({ + url: server.url, + path: path + '/root', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeGetRequest({ + url: server.url, + path: path + '/root@', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeGetRequest({ + url: server.url, + path: path + '/root@hello@', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown subscription', async function () { + await makeGetRequest({ + url: server.url, + path: path + '/root1@' + server.host, + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path: path + '/user1_channel@' + server.host, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When checking if subscriptions exist', function () { + const existPath = path + '/exist' + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path: existPath, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with bad URIs', async function () { + await makeGetRequest({ + url: server.url, + path: existPath, + query: { uris: 'toto' }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeGetRequest({ + url: server.url, + path: existPath, + query: { 'uris[]': 1 }, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path: existPath, + query: { 'uris[]': 'coucou@' + server.host }, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When removing a subscription', function () { + it('Should fail with a non authenticated user', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user1_channel@' + server.host, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with bad URIs', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/root', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeDeleteRequest({ + url: server.url, + path: path + '/root@', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeDeleteRequest({ + url: server.url, + path: path + '/root@hello@', + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown subscription', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/root1@' + server.host, + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '/user1_channel@' + server.host, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/users-admin.ts b/packages/tests/src/api/check-params/users-admin.ts new file mode 100644 index 000000000..1ad222ddc --- /dev/null +++ b/packages/tests/src/api/check-params/users-admin.ts @@ -0,0 +1,457 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' +import { omit } from '@peertube/peertube-core-utils' +import { HttpStatusCode, UserAdminFlag, UserRole } from '@peertube/peertube-models' +import { + cleanupTests, + ConfigCommand, + createSingleServer, + killallServers, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test users admin API validators', function () { + const path = '/api/v1/users/' + let userId: number + let rootId: number + let moderatorId: number + let server: PeerTubeServer + let userToken = '' + let moderatorToken = '' + let emailPort: number + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + const emails: object[] = [] + emailPort = await MockSmtpServer.Instance.collectEmails(emails) + + { + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + } + + { + const result = await server.users.generate('user1') + userToken = result.token + userId = result.userId + } + + { + const result = await server.users.generate('moderator1', UserRole.MODERATOR) + moderatorToken = result.token + } + + { + const result = await server.users.generate('moderator2', UserRole.MODERATOR) + moderatorId = result.userId + } + }) + + describe('When listing users', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + describe('When adding a new user', function () { + const baseCorrectParams = { + username: 'user2', + email: 'test@example.com', + password: 'my super password', + videoQuota: -1, + videoQuotaDaily: -1, + role: UserRole.USER, + adminFlags: UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST + } + + it('Should fail with a too small username', async function () { + const fields = { ...baseCorrectParams, username: '' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a too long username', async function () { + const fields = { ...baseCorrectParams, username: 'super'.repeat(50) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a not lowercase username', async function () { + const fields = { ...baseCorrectParams, username: 'Toto' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with an incorrect username', async function () { + const fields = { ...baseCorrectParams, username: 'my username' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a missing email', async function () { + const fields = omit(baseCorrectParams, [ 'email' ]) + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with an invalid email', async function () { + const fields = { ...baseCorrectParams, email: 'test_example.com' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a too small password', async function () { + const fields = { ...baseCorrectParams, password: 'bla' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a too long password', async function () { + const fields = { ...baseCorrectParams, password: 'super'.repeat(61) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with empty password and no smtp configured', async function () { + const fields = { ...baseCorrectParams, password: '' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should succeed with no password on a server with smtp enabled', async function () { + this.timeout(20000) + + await killallServers([ server ]) + + await server.run(ConfigCommand.getEmailOverrideConfig(emailPort)) + + const fields = { + ...baseCorrectParams, + + password: '', + username: 'create_password', + email: 'create_password@example.com' + } + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should fail with invalid admin flags', async function () { + const fields = { ...baseCorrectParams, adminFlags: 'toto' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with an non authenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path, + token: 'super token', + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail if we add a user with the same username', async function () { + const fields = { ...baseCorrectParams, username: 'user1' } + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should fail if we add a user with the same email', async function () { + const fields = { ...baseCorrectParams, email: 'user1@example.com' } + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should fail with an invalid videoQuota', async function () { + const fields = { ...baseCorrectParams, videoQuota: -5 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with an invalid videoQuotaDaily', async function () { + const fields = { ...baseCorrectParams, videoQuotaDaily: -7 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail without a user role', async function () { + const fields = omit(baseCorrectParams, [ 'role' ]) + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with an invalid user role', async function () { + const fields = { ...baseCorrectParams, role: 88989 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a "peertube" username', async function () { + const fields = { ...baseCorrectParams, username: 'peertube' } + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should fail to create a moderator or an admin with a moderator', async function () { + for (const role of [ UserRole.MODERATOR, UserRole.ADMINISTRATOR ]) { + const fields = { ...baseCorrectParams, role } + + await makePostBodyRequest({ + url: server.url, + path, + token: moderatorToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + } + }) + + it('Should succeed to create a user with a moderator', async function () { + const fields = { ...baseCorrectParams, username: 'a4656', email: 'a4656@example.com', role: UserRole.USER } + + await makePostBodyRequest({ + url: server.url, + path, + token: moderatorToken, + fields, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should succeed with the correct params', async function () { + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should fail with a non admin user', async function () { + const user = { username: 'user1' } + userToken = await server.login.getAccessToken(user) + + const fields = { + username: 'user3', + email: 'test@example.com', + password: 'my super password', + videoQuota: 42000000 + } + await makePostBodyRequest({ url: server.url, path, token: userToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + }) + + describe('When getting a user', function () { + + it('Should fail with an non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path: path + userId, + token: 'super token', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makeGetRequest({ url: server.url, path, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ url: server.url, path: path + userId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When updating a user', function () { + + it('Should fail with an invalid email attribute', async function () { + const fields = { + email: 'blabla' + } + + await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) + }) + + it('Should fail with an invalid emailVerified attribute', async function () { + const fields = { + emailVerified: 'yes' + } + + await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) + }) + + it('Should fail with an invalid videoQuota attribute', async function () { + const fields = { + videoQuota: -90 + } + + await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) + }) + + it('Should fail with an invalid user role attribute', async function () { + const fields = { + role: 54878 + } + + await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) + }) + + it('Should fail with a too small password', async function () { + const fields = { + currentPassword: 'password', + password: 'bla' + } + + await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) + }) + + it('Should fail with a too long password', async function () { + const fields = { + currentPassword: 'password', + password: 'super'.repeat(61) + } + + await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) + }) + + it('Should fail with an non authenticated user', async function () { + const fields = { + videoQuota: 42 + } + + await makePutBodyRequest({ + url: server.url, + path: path + userId, + token: 'super token', + fields, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail when updating root role', async function () { + const fields = { + role: UserRole.MODERATOR + } + + await makePutBodyRequest({ url: server.url, path: path + rootId, token: server.accessToken, fields }) + }) + + it('Should fail with invalid admin flags', async function () { + const fields = { adminFlags: 'toto' } + + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail to update an admin with a moderator', async function () { + const fields = { + videoQuota: 42 + } + + await makePutBodyRequest({ + url: server.url, + path: path + moderatorId, + token: moderatorToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed to update a user with a moderator', async function () { + const fields = { + videoQuota: 42 + } + + await makePutBodyRequest({ + url: server.url, + path: path + userId, + token: moderatorToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should succeed with the correct params', async function () { + const fields = { + email: 'email@example.com', + emailVerified: true, + videoQuota: 42, + role: UserRole.USER + } + + await makePutBodyRequest({ + url: server.url, + path: path + userId, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + MockSmtpServer.Instance.kill() + + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/users-emails.ts b/packages/tests/src/api/check-params/users-emails.ts new file mode 100644 index 000000000..e382190ec --- /dev/null +++ b/packages/tests/src/api/check-params/users-emails.ts @@ -0,0 +1,122 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ +import { HttpStatusCode, UserRole } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test users API validators', function () { + let server: PeerTubeServer + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1, { + rates_limit: { + ask_send_email: { + max: 10 + } + } + }) + + await setAccessTokensToServers([ server ]) + await server.config.enableSignup(true) + + await server.users.generate('moderator2', UserRole.MODERATOR) + + await server.registrations.requestRegistration({ + username: 'request1', + registrationReason: 'tt' + }) + }) + + describe('When asking a password reset', function () { + const path = '/api/v1/users/ask-reset-password' + + it('Should fail with a missing email', async function () { + const fields = {} + + await makePostBodyRequest({ url: server.url, path, fields }) + }) + + it('Should fail with an invalid email', async function () { + const fields = { email: 'hello' } + + await makePostBodyRequest({ url: server.url, path, fields }) + }) + + it('Should success with the correct params', async function () { + const fields = { email: 'admin@example.com' } + + await makePostBodyRequest({ + url: server.url, + path, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When asking for an account verification email', function () { + const path = '/api/v1/users/ask-send-verify-email' + + it('Should fail with a missing email', async function () { + const fields = {} + + await makePostBodyRequest({ url: server.url, path, fields }) + }) + + it('Should fail with an invalid email', async function () { + const fields = { email: 'hello' } + + await makePostBodyRequest({ url: server.url, path, fields }) + }) + + it('Should succeed with the correct params', async function () { + const fields = { email: 'admin@example.com' } + + await makePostBodyRequest({ + url: server.url, + path, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When asking for a registration verification email', function () { + const path = '/api/v1/users/registrations/ask-send-verify-email' + + it('Should fail with a missing email', async function () { + const fields = {} + + await makePostBodyRequest({ url: server.url, path, fields }) + }) + + it('Should fail with an invalid email', async function () { + const fields = { email: 'hello' } + + await makePostBodyRequest({ url: server.url, path, fields }) + }) + + it('Should succeed with the correct params', async function () { + const fields = { email: 'request1@example.com' } + + await makePostBodyRequest({ + url: server.url, + path, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-blacklist.ts b/packages/tests/src/api/check-params/video-blacklist.ts new file mode 100644 index 000000000..6ec070b9b --- /dev/null +++ b/packages/tests/src/api/check-params/video-blacklist.ts @@ -0,0 +1,292 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode, VideoBlacklistType } from '@peertube/peertube-models' +import { + BlacklistCommand, + cleanupTests, + createMultipleServers, + doubleFollow, + makePostBodyRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test video blacklist API validators', function () { + let servers: PeerTubeServer[] + let notBlacklistedVideoId: string + let remoteVideoUUID: string + let userAccessToken1 = '' + let userAccessToken2 = '' + let command: BlacklistCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + servers = await createMultipleServers(2) + + await setAccessTokensToServers(servers) + await doubleFollow(servers[0], servers[1]) + + { + const username = 'user1' + const password = 'my super password' + await servers[0].users.create({ username, password }) + userAccessToken1 = await servers[0].login.getAccessToken({ username, password }) + } + + { + const username = 'user2' + const password = 'my super password' + await servers[0].users.create({ username, password }) + userAccessToken2 = await servers[0].login.getAccessToken({ username, password }) + } + + { + servers[0].store.videoCreated = await servers[0].videos.upload({ token: userAccessToken1 }) + } + + { + const { uuid } = await servers[0].videos.upload() + notBlacklistedVideoId = uuid + } + + { + const { uuid } = await servers[1].videos.upload() + remoteVideoUUID = uuid + } + + await waitJobs(servers) + + command = servers[0].blacklist + }) + + describe('When adding a video in blacklist', function () { + const basePath = '/api/v1/videos/' + + it('Should fail with nothing', async function () { + const path = basePath + servers[0].store.videoCreated + '/blacklist' + const fields = {} + await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) + }) + + it('Should fail with a wrong video', async function () { + const wrongPath = '/api/v1/videos/blabla/blacklist' + const fields = {} + await makePostBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) + }) + + it('Should fail with a non authenticated user', async function () { + const path = basePath + servers[0].store.videoCreated + '/blacklist' + const fields = {} + await makePostBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a non admin user', async function () { + const path = basePath + servers[0].store.videoCreated + '/blacklist' + const fields = {} + await makePostBodyRequest({ + url: servers[0].url, + path, + token: userAccessToken2, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid reason', async function () { + const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist' + const fields = { reason: 'a'.repeat(305) } + + await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) + }) + + it('Should fail to unfederate a remote video', async function () { + const path = basePath + remoteVideoUUID + '/blacklist' + const fields = { unfederate: true } + + await makePostBodyRequest({ + url: servers[0].url, + path, + token: servers[0].accessToken, + fields, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should succeed with the correct params', async function () { + const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist' + const fields = {} + + await makePostBodyRequest({ + url: servers[0].url, + path, + token: servers[0].accessToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When updating a video in blacklist', function () { + const basePath = '/api/v1/videos/' + + it('Should fail with a wrong video', async function () { + const wrongPath = '/api/v1/videos/blabla/blacklist' + const fields = {} + await makePutBodyRequest({ url: servers[0].url, path: wrongPath, token: servers[0].accessToken, fields }) + }) + + it('Should fail with a video not blacklisted', async function () { + const path = '/api/v1/videos/' + notBlacklistedVideoId + '/blacklist' + const fields = {} + await makePutBodyRequest({ + url: servers[0].url, + path, + token: servers[0].accessToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a non authenticated user', async function () { + const path = basePath + servers[0].store.videoCreated + '/blacklist' + const fields = {} + await makePutBodyRequest({ url: servers[0].url, path, token: 'hello', fields, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a non admin user', async function () { + const path = basePath + servers[0].store.videoCreated + '/blacklist' + const fields = {} + await makePutBodyRequest({ + url: servers[0].url, + path, + token: userAccessToken2, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid reason', async function () { + const path = basePath + servers[0].store.videoCreated.uuid + '/blacklist' + const fields = { reason: 'a'.repeat(305) } + + await makePutBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields }) + }) + + it('Should succeed with the correct params', async function () { + const path = basePath + servers[0].store.videoCreated.shortUUID + '/blacklist' + const fields = { reason: 'hello' } + + await makePutBodyRequest({ + url: servers[0].url, + path, + token: servers[0].accessToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When getting blacklisted video', function () { + + it('Should fail with a non authenticated user', async function () { + await servers[0].videos.get({ id: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with another user', async function () { + await servers[0].videos.getWithToken({ + token: userAccessToken2, + id: servers[0].store.videoCreated.uuid, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the owner authenticated user', async function () { + const video = await servers[0].videos.getWithToken({ token: userAccessToken1, id: servers[0].store.videoCreated.uuid }) + expect(video.blacklisted).to.be.true + }) + + it('Should succeed with an admin', async function () { + const video = servers[0].store.videoCreated + + for (const id of [ video.id, video.uuid, video.shortUUID ]) { + const video = await servers[0].videos.getWithToken({ id, expectedStatus: HttpStatusCode.OK_200 }) + expect(video.blacklisted).to.be.true + } + }) + }) + + describe('When removing a video in blacklist', function () { + + it('Should fail with a non authenticated user', async function () { + await command.remove({ + token: 'fake token', + videoId: servers[0].store.videoCreated.uuid, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await command.remove({ + token: userAccessToken2, + videoId: servers[0].store.videoCreated.uuid, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an incorrect id', async function () { + await command.remove({ videoId: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a not blacklisted video', async function () { + // The video was not added to the blacklist so it should fail + await command.remove({ videoId: notBlacklistedVideoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct params', async function () { + await command.remove({ videoId: servers[0].store.videoCreated.uuid, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + }) + }) + + describe('When listing videos in blacklist', function () { + const basePath = '/api/v1/videos/blacklist/' + + it('Should fail with a non authenticated user', async function () { + await servers[0].blacklist.list({ token: 'fake token', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a non admin user', async function () { + await servers[0].blacklist.list({ token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(servers[0].url, basePath, servers[0].accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(servers[0].url, basePath, servers[0].accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken) + }) + + it('Should fail with an invalid type', async function () { + await servers[0].blacklist.list({ type: 0 as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct parameters', async function () { + await servers[0].blacklist.list({ type: VideoBlacklistType.MANUAL }) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/api/check-params/video-captions.ts b/packages/tests/src/api/check-params/video-captions.ts new file mode 100644 index 000000000..4150b095f --- /dev/null +++ b/packages/tests/src/api/check-params/video-captions.ts @@ -0,0 +1,307 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeDeleteRequest, + makeGetRequest, + makeUploadRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test video captions API validator', function () { + const path = '/api/v1/videos/' + + let server: PeerTubeServer + let userAccessToken: string + let video: VideoCreateResult + let privateVideo: VideoCreateResult + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + video = await server.videos.upload() + privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } }) + + { + const user = { + username: 'user1', + password: 'my super password' + } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + } + }) + + describe('When adding video caption', function () { + const fields = { } + const attaches = { + captionfile: buildAbsoluteFixturePath('subtitle-good1.vtt') + } + + it('Should fail without a valid uuid', async function () { + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr', + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with an unknown id', async function () { + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr', + token: server.accessToken, + fields, + attaches, + expectedStatus: 404 + }) + }) + + it('Should fail with a missing language in path', async function () { + const captionPath = path + video.uuid + '/captions' + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: captionPath, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with an unknown language', async function () { + const captionPath = path + video.uuid + '/captions/15' + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: captionPath, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail without access token', async function () { + const captionPath = path + video.uuid + '/captions/fr' + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: captionPath, + fields, + attaches, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a bad access token', async function () { + const captionPath = path + video.uuid + '/captions/fr' + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: captionPath, + token: 'blabla', + fields, + attaches, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + // We accept any file now + // it('Should fail with an invalid captionfile extension', async function () { + // const attaches = { + // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.txt') + // } + // + // const captionPath = path + video.uuid + '/captions/fr' + // await makeUploadRequest({ + // method: 'PUT', + // url: server.url, + // path: captionPath, + // token: server.accessToken, + // fields, + // attaches, + // expectedStatus: HttpStatusCode.BAD_REQUEST_400 + // }) + // }) + + // We don't check the extension yet + // it('Should fail with an invalid captionfile extension and octet-stream mime type', async function () { + // await createVideoCaption({ + // url: server.url, + // accessToken: server.accessToken, + // language: 'zh', + // videoId: video.uuid, + // fixture: 'subtitle-bad.txt', + // mimeType: 'application/octet-stream', + // expectedStatus: HttpStatusCode.BAD_REQUEST_400 + // }) + // }) + + it('Should succeed with a valid captionfile extension and octet-stream mime type', async function () { + await server.captions.add({ + language: 'zh', + videoId: video.uuid, + fixture: 'subtitle-good.srt', + mimeType: 'application/octet-stream' + }) + }) + + // We don't check the file validity yet + // it('Should fail with an invalid captionfile srt', async function () { + // const attaches = { + // 'captionfile': buildAbsoluteFixturePath('subtitle-bad.srt') + // } + // + // const captionPath = path + video.uuid + '/captions/fr' + // await makeUploadRequest({ + // method: 'PUT', + // url: server.url, + // path: captionPath, + // token: server.accessToken, + // fields, + // attaches, + // expectedStatus: HttpStatusCode.INTERNAL_SERVER_ERROR_500 + // }) + // }) + + it('Should success with the correct parameters', async function () { + const captionPath = path + video.uuid + '/captions/fr' + await makeUploadRequest({ + method: 'PUT', + url: server.url, + path: captionPath, + token: server.accessToken, + fields, + attaches, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When listing video captions', function () { + it('Should fail without a valid uuid', async function () { + await makeGetRequest({ url: server.url, path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions' }) + }) + + it('Should fail with an unknown id', async function () { + await makeGetRequest({ + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a private video without token', async function () { + await makeGetRequest({ + url: server.url, + path: path + privateVideo.shortUUID + '/captions', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another user token', async function () { + await makeGetRequest({ + url: server.url, + token: userAccessToken, + path: path + privateVideo.shortUUID + '/captions', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path: path + video.shortUUID + '/captions', expectedStatus: HttpStatusCode.OK_200 }) + + await makeGetRequest({ + url: server.url, + path: path + privateVideo.shortUUID + '/captions', + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When deleting video caption', function () { + it('Should fail without a valid uuid', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df563d0b06/captions/fr', + token: server.accessToken + }) + }) + + it('Should fail with an unknown id', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/fr', + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with an invalid language', async function () { + await makeDeleteRequest({ + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/captions/16', + token: server.accessToken + }) + }) + + it('Should fail with a missing language', async function () { + const captionPath = path + video.shortUUID + '/captions' + await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) + }) + + it('Should fail with an unknown language', async function () { + const captionPath = path + video.shortUUID + '/captions/15' + await makeDeleteRequest({ url: server.url, path: captionPath, token: server.accessToken }) + }) + + it('Should fail without access token', async function () { + const captionPath = path + video.shortUUID + '/captions/fr' + await makeDeleteRequest({ url: server.url, path: captionPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a bad access token', async function () { + const captionPath = path + video.shortUUID + '/captions/fr' + await makeDeleteRequest({ url: server.url, path: captionPath, token: 'coucou', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with another user', async function () { + const captionPath = path + video.shortUUID + '/captions/fr' + await makeDeleteRequest({ + url: server.url, + path: captionPath, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should success with the correct parameters', async function () { + const captionPath = path + video.shortUUID + '/captions/fr' + await makeDeleteRequest({ + url: server.url, + path: captionPath, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-channel-syncs.ts b/packages/tests/src/api/check-params/video-channel-syncs.ts new file mode 100644 index 000000000..d95f3319a --- /dev/null +++ b/packages/tests/src/api/check-params/video-channel-syncs.ts @@ -0,0 +1,319 @@ +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { FIXTURE_URLS } from '@tests/shared/tests.js' +import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models' +import { + ChannelSyncsCommand, + createSingleServer, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel +} from '@peertube/peertube-server-commands' + +describe('Test video channel sync API validator', () => { + const path = '/api/v1/video-channel-syncs' + let server: PeerTubeServer + let command: ChannelSyncsCommand + let rootChannelId: number + let rootChannelSyncId: number + const userInfo = { + accessToken: '', + username: 'user1', + id: -1, + channelId: -1, + syncId: -1 + } + + async function withChannelSyncDisabled (callback: () => Promise): Promise { + try { + await server.config.disableChannelSync() + await callback() + } finally { + await server.config.enableChannelSync() + } + } + + async function withMaxSyncsPerUser (maxSync: number, callback: () => Promise): Promise { + const origConfig = await server.config.getCustomConfig() + + await server.config.updateExistingSubConfig({ + newConfig: { + import: { + videoChannelSynchronization: { + maxPerUser: maxSync + } + } + } + }) + + try { + await callback() + } finally { + await server.config.updateCustomConfig({ newCustomConfig: origConfig }) + } + } + + before(async function () { + this.timeout(30_000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + command = server.channelSyncs + + rootChannelId = server.store.channel.id + + { + userInfo.accessToken = await server.users.generateUserAndToken(userInfo.username) + + const { videoChannels, id: userId } = await server.users.getMyInfo({ token: userInfo.accessToken }) + userInfo.id = userId + userInfo.channelId = videoChannels[0].id + } + + await server.config.enableChannelSync() + }) + + describe('When creating a sync', function () { + let baseCorrectParams: VideoChannelSyncCreate + + before(function () { + baseCorrectParams = { + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + videoChannelId: rootChannelId + } + }) + + it('Should fail when sync is disabled', async function () { + await withChannelSyncDisabled(async () => { + await command.create({ + token: server.accessToken, + attributes: baseCorrectParams, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + }) + + it('Should fail with nothing', async function () { + const fields = {} + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with no authentication', async function () { + await command.create({ + token: null, + attributes: baseCorrectParams, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail without a target url', async function () { + const attributes: VideoChannelSyncCreate = { + ...baseCorrectParams, + externalChannelUrl: null + } + await command.create({ + token: server.accessToken, + attributes, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail without a channelId', async function () { + const attributes: VideoChannelSyncCreate = { + ...baseCorrectParams, + videoChannelId: null + } + await command.create({ + token: server.accessToken, + attributes, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a channelId refering nothing', async function () { + const attributes: VideoChannelSyncCreate = { + ...baseCorrectParams, + videoChannelId: 42 + } + await command.create({ + token: server.accessToken, + attributes, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail to create a sync when the user does not own the channel', async function () { + await command.create({ + token: userInfo.accessToken, + attributes: baseCorrectParams, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed to create a sync with root and for another user\'s channel', async function () { + const { videoChannelSync } = await command.create({ + token: server.accessToken, + attributes: { + ...baseCorrectParams, + videoChannelId: userInfo.channelId + }, + expectedStatus: HttpStatusCode.OK_200 + }) + userInfo.syncId = videoChannelSync.id + }) + + it('Should succeed with the correct parameters', async function () { + const { videoChannelSync } = await command.create({ + token: server.accessToken, + attributes: baseCorrectParams, + expectedStatus: HttpStatusCode.OK_200 + }) + rootChannelSyncId = videoChannelSync.id + }) + + it('Should fail when the user exceeds allowed number of synchronizations', async function () { + await withMaxSyncsPerUser(1, async () => { + await command.create({ + token: server.accessToken, + attributes: { + ...baseCorrectParams, + videoChannelId: userInfo.channelId + }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + }) + }) + + describe('When listing my channel syncs', function () { + const myPath = '/api/v1/accounts/root/video-channel-syncs' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, myPath, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, myPath, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, myPath, server.accessToken) + }) + + it('Should succeed with the correct parameters', async function () { + await command.listByAccount({ + accountName: 'root', + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should fail with no authentication', async function () { + await command.listByAccount({ + accountName: 'root', + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail when a simple user lists another user\'s synchronizations', async function () { + await command.listByAccount({ + accountName: 'root', + token: userInfo.accessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed when root lists another user\'s synchronizations', async function () { + await command.listByAccount({ + accountName: userInfo.username, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should succeed even with synchronization disabled', async function () { + await withChannelSyncDisabled(async function () { + await command.listByAccount({ + accountName: 'root', + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + }) + + describe('When triggering deletion', function () { + it('should fail with no authentication', async function () { + await command.delete({ + channelSyncId: userInfo.syncId, + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail when channelSyncId does not refer to any sync', async function () { + await command.delete({ + channelSyncId: 42, + token: server.accessToken, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail when sync is not owned by the user', async function () { + await command.delete({ + channelSyncId: rootChannelSyncId, + token: userInfo.accessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed when root delete a sync they do not own', async function () { + await command.delete({ + channelSyncId: userInfo.syncId, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('should succeed when user delete a sync they own', async function () { + const { videoChannelSync } = await command.create({ + attributes: { + externalChannelUrl: FIXTURE_URLS.youtubeChannel, + videoChannelId: userInfo.channelId + }, + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200 + }) + + await command.delete({ + channelSyncId: videoChannelSync.id, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should succeed even when synchronization is disabled', async function () { + await withChannelSyncDisabled(async function () { + await command.delete({ + channelSyncId: rootChannelSyncId, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + }) + + after(async function () { + await server?.kill() + }) +}) diff --git a/packages/tests/src/api/check-params/video-channels.ts b/packages/tests/src/api/check-params/video-channels.ts new file mode 100644 index 000000000..84b962b19 --- /dev/null +++ b/packages/tests/src/api/check-params/video-channels.ts @@ -0,0 +1,379 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { omit } from '@peertube/peertube-core-utils' +import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { + ChannelsCommand, + cleanupTests, + createSingleServer, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + makeUploadRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test video channels API validator', function () { + const videoChannelPath = '/api/v1/video-channels' + let server: PeerTubeServer + const userInfo = { + accessToken: '', + channelName: 'fake_channel', + id: -1, + videoQuota: -1, + videoQuotaDaily: -1 + } + let command: ChannelsCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const userCreds = { + username: 'fake', + password: 'fake_password' + } + + { + const user = await server.users.create({ username: userCreds.username, password: userCreds.password }) + userInfo.id = user.id + userInfo.accessToken = await server.login.getAccessToken(userCreds) + } + + command = server.channels + }) + + describe('When listing a video channels', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, videoChannelPath, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, videoChannelPath, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, videoChannelPath, server.accessToken) + }) + }) + + describe('When listing account video channels', function () { + const accountChannelPath = '/api/v1/accounts/fake/video-channels' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, accountChannelPath, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, accountChannelPath, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, accountChannelPath, server.accessToken) + }) + + it('Should fail with a unknown account', async function () { + await server.channels.listByAccount({ accountName: 'unknown', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path: accountChannelPath, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When adding a video channel', function () { + const baseCorrectParams = { + name: 'super_channel', + displayName: 'hello', + description: 'super description', + support: 'super support text' + } + + it('Should fail with a non authenticated user', async function () { + await makePostBodyRequest({ + url: server.url, + path: videoChannelPath, + token: 'none', + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with nothing', async function () { + const fields = {} + await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) + }) + + it('Should fail without a name', async function () { + const fields = omit(baseCorrectParams, [ 'name' ]) + await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) + }) + + it('Should fail with a bad name', async function () { + const fields = { ...baseCorrectParams, name: 'super name' } + await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) + }) + + it('Should fail without a name', async function () { + const fields = omit(baseCorrectParams, [ 'displayName' ]) + await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) + }) + + it('Should fail with a long name', async function () { + const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) } + await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) + }) + + it('Should fail with a long description', async function () { + const fields = { ...baseCorrectParams, description: 'super'.repeat(201) } + await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) + }) + + it('Should fail with a long support text', async function () { + const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } + await makePostBodyRequest({ url: server.url, path: videoChannelPath, token: server.accessToken, fields }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePostBodyRequest({ + url: server.url, + path: videoChannelPath, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should fail when adding a channel with the same username', async function () { + await makePostBodyRequest({ + url: server.url, + path: videoChannelPath, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + }) + + describe('When updating a video channel', function () { + const baseCorrectParams: VideoChannelUpdate = { + displayName: 'hello', + description: 'super description', + support: 'toto', + bulkVideosSupportUpdate: false + } + let path: string + + before(async function () { + path = videoChannelPath + '/super_channel' + }) + + it('Should fail with a non authenticated user', async function () { + await makePutBodyRequest({ + url: server.url, + path, + token: 'hi', + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another authenticated user', async function () { + await makePutBodyRequest({ + url: server.url, + path, + token: userInfo.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a long name', async function () { + const fields = { ...baseCorrectParams, displayName: 'super'.repeat(25) } + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a long description', async function () { + const fields = { ...baseCorrectParams, description: 'super'.repeat(201) } + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a long support text', async function () { + const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad bulkVideosSupportUpdate field', async function () { + const fields = { ...baseCorrectParams, bulkVideosSupportUpdate: 'super' } + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should succeed with the correct parameters', async function () { + await makePutBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When updating video channel avatars/banners', function () { + const types = [ 'avatar', 'banner' ] + let path: string + + before(async function () { + path = videoChannelPath + '/super_channel' + }) + + it('Should fail with an incorrect input file', async function () { + for (const type of types) { + const fields = {} + const attaches = { + [type + 'file']: buildAbsoluteFixturePath('video_short.mp4') + } + + await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) + } + }) + + it('Should fail with a big file', async function () { + for (const type of types) { + const fields = {} + const attaches = { + [type + 'file']: buildAbsoluteFixturePath('avatar-big.png') + } + await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches }) + } + }) + + it('Should fail with an unauthenticated user', async function () { + for (const type of types) { + const fields = {} + const attaches = { + [type + 'file']: buildAbsoluteFixturePath('avatar.png') + } + await makeUploadRequest({ + url: server.url, + path: `${path}/${type}/pick`, + fields, + attaches, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + } + }) + + it('Should succeed with the correct params', async function () { + for (const type of types) { + const fields = {} + const attaches = { + [type + 'file']: buildAbsoluteFixturePath('avatar.png') + } + await makeUploadRequest({ + url: server.url, + path: `${path}/${type}/pick`, + token: server.accessToken, + fields, + attaches, + expectedStatus: HttpStatusCode.OK_200 + }) + } + }) + }) + + describe('When getting a video channel', function () { + it('Should return the list of the video channels with nothing', async function () { + const res = await makeGetRequest({ + url: server.url, + path: videoChannelPath, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.data).to.be.an('array') + }) + + it('Should return 404 with an incorrect video channel', async function () { + await makeGetRequest({ + url: server.url, + path: videoChannelPath + '/super_channel2', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeGetRequest({ + url: server.url, + path: videoChannelPath + '/super_channel', + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When getting channel followers', function () { + const path = '/api/v1/video-channels/super_channel/followers' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a unauthenticated user', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a another user', async function () { + await makeGetRequest({ url: server.url, path, token: userInfo.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When deleting a video channel', function () { + it('Should fail with a non authenticated user', async function () { + await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with another authenticated user', async function () { + await command.delete({ token: userInfo.accessToken, channelName: 'super_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with an unknown video channel id', async function () { + await command.delete({ channelName: 'super_channel2', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct parameters', async function () { + await command.delete({ channelName: 'super_channel' }) + }) + + it('Should fail to delete the last user video channel', async function () { + await command.delete({ channelName: 'root_channel', expectedStatus: HttpStatusCode.CONFLICT_409 }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-comments.ts b/packages/tests/src/api/check-params/video-comments.ts new file mode 100644 index 000000000..177361606 --- /dev/null +++ b/packages/tests/src/api/check-params/video-comments.ts @@ -0,0 +1,484 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeDeleteRequest, + makeGetRequest, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test video comments API validator', function () { + let pathThread: string + let pathComment: string + + let server: PeerTubeServer + + let video: VideoCreateResult + + let userAccessToken: string + let userAccessToken2: string + + let commentId: number + let privateCommentId: number + let privateVideo: VideoCreateResult + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + { + video = await server.videos.upload({ attributes: {} }) + pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads' + } + + { + privateVideo = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PRIVATE } }) + } + + { + const created = await server.comments.createThread({ videoId: video.uuid, text: 'coucou' }) + commentId = created.id + pathComment = '/api/v1/videos/' + video.uuid + '/comments/' + commentId + } + + { + const created = await server.comments.createThread({ videoId: privateVideo.uuid, text: 'coucou' }) + privateCommentId = created.id + } + + { + const user = { username: 'user1', password: 'my super password' } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + } + + { + const user = { username: 'user2', password: 'my super password' } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken2 = await server.login.getAccessToken(user) + } + }) + + describe('When listing video comment threads', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, pathThread, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, pathThread, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, pathThread, server.accessToken) + }) + + it('Should fail with an incorrect video', async function () { + await makeGetRequest({ + url: server.url, + path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a private video without token', async function () { + await makeGetRequest({ + url: server.url, + path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another user token', async function () { + await makeGetRequest({ + url: server.url, + token: userAccessToken, + path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When listing comments of a thread', function () { + it('Should fail with an incorrect video', async function () { + await makeGetRequest({ + url: server.url, + path: '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads/' + commentId, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with an incorrect thread id', async function () { + await makeGetRequest({ + url: server.url, + path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/156', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a private video without token', async function () { + await makeGetRequest({ + url: server.url, + path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another user token', async function () { + await makeGetRequest({ + url: server.url, + token: userAccessToken, + path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should success with the correct params', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads/' + privateCommentId, + expectedStatus: HttpStatusCode.OK_200 + }) + + await makeGetRequest({ + url: server.url, + path: '/api/v1/videos/' + video.shortUUID + '/comment-threads/' + commentId, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When adding a video thread', function () { + + it('Should fail with a non authenticated user', async function () { + const fields = { + text: 'text' + } + await makePostBodyRequest({ + url: server.url, + path: pathThread, + token: 'none', + fields, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with nothing', async function () { + const fields = {} + await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields }) + }) + + it('Should fail with a short comment', async function () { + const fields = { + text: '' + } + await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields }) + }) + + it('Should fail with a long comment', async function () { + const fields = { + text: 'h'.repeat(10001) + } + await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields }) + }) + + it('Should fail with an incorrect video', async function () { + const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comment-threads' + const fields = { text: 'super comment' } + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a private video of another user', async function () { + const fields = { text: 'super comment' } + + await makePostBodyRequest({ + url: server.url, + path: '/api/v1/videos/' + privateVideo.shortUUID + '/comment-threads', + token: userAccessToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct parameters', async function () { + const fields = { text: 'super comment' } + + await makePostBodyRequest({ + url: server.url, + path: pathThread, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When adding a comment to a thread', function () { + + it('Should fail with a non authenticated user', async function () { + const fields = { + text: 'text' + } + await makePostBodyRequest({ + url: server.url, + path: pathComment, + token: 'none', + fields, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with nothing', async function () { + const fields = {} + await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields }) + }) + + it('Should fail with a short comment', async function () { + const fields = { + text: '' + } + await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields }) + }) + + it('Should fail with a long comment', async function () { + const fields = { + text: 'h'.repeat(10001) + } + await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields }) + }) + + it('Should fail with an incorrect video', async function () { + const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId + const fields = { + text: 'super comment' + } + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a private video of another user', async function () { + const fields = { text: 'super comment' } + + await makePostBodyRequest({ + url: server.url, + path: '/api/v1/videos/' + privateVideo.uuid + '/comments/' + privateCommentId, + token: userAccessToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an incorrect comment', async function () { + const path = '/api/v1/videos/' + video.uuid + '/comments/124' + const fields = { + text: 'super comment' + } + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct parameters', async function () { + const fields = { + text: 'super comment' + } + await makePostBodyRequest({ + url: server.url, + path: pathComment, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When removing video comments', function () { + it('Should fail with a non authenticated user', async function () { + await makeDeleteRequest({ url: server.url, path: pathComment, token: 'none', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with another user', async function () { + await makeDeleteRequest({ + url: server.url, + path: pathComment, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an incorrect video', async function () { + const path = '/api/v1/videos/ba708d62-e3d7-45d9-9d73-41b9097cc02d/comments/' + commentId + await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with an incorrect comment', async function () { + const path = '/api/v1/videos/' + video.uuid + '/comments/124' + await makeDeleteRequest({ url: server.url, path, token: server.accessToken, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the same user', async function () { + let commentToDelete: number + + { + const created = await server.comments.createThread({ videoId: video.uuid, token: userAccessToken, text: 'hello' }) + commentToDelete = created.id + } + + const path = '/api/v1/videos/' + video.uuid + '/comments/' + commentToDelete + + await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + }) + + it('Should succeed with the owner of the video', async function () { + let commentToDelete: number + let anotherVideoUUID: string + + { + const { uuid } = await server.videos.upload({ token: userAccessToken, attributes: { name: 'video' } }) + anotherVideoUUID = uuid + } + + { + const created = await server.comments.createThread({ videoId: anotherVideoUUID, text: 'hello' }) + commentToDelete = created.id + } + + const path = '/api/v1/videos/' + anotherVideoUUID + '/comments/' + commentToDelete + + await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await makeDeleteRequest({ url: server.url, path, token: userAccessToken, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeDeleteRequest({ + url: server.url, + path: pathComment, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When a video has comments disabled', function () { + before(async function () { + video = await server.videos.upload({ attributes: { commentsEnabled: false } }) + pathThread = '/api/v1/videos/' + video.uuid + '/comment-threads' + }) + + it('Should return an empty thread list', async function () { + const res = await makeGetRequest({ + url: server.url, + path: pathThread, + expectedStatus: HttpStatusCode.OK_200 + }) + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + }) + + it('Should return an thread comments list') + + it('Should return conflict on thread add', async function () { + const fields = { + text: 'super comment' + } + await makePostBodyRequest({ + url: server.url, + path: pathThread, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should return conflict on comment thread add') + }) + + describe('When listing admin comments threads', function () { + const path = '/api/v1/videos/comments' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a non authenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a non admin user', async function () { + await makeGetRequest({ + url: server.url, + path, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + path, + token: server.accessToken, + query: { + isLocal: false, + search: 'toto', + searchAccount: 'toto', + searchVideo: 'toto' + }, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-files.ts b/packages/tests/src/api/check-params/video-files.ts new file mode 100644 index 000000000..b5819ff19 --- /dev/null +++ b/packages/tests/src/api/check-params/video-files.ts @@ -0,0 +1,195 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { getAllFiles } from '@peertube/peertube-core-utils' +import { HttpStatusCode, UserRole, VideoDetails, VideoPrivacy } from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + makeRawRequest, + PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test videos files', function () { + let servers: PeerTubeServer[] + + let userToken: string + let moderatorToken: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(300_000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + + await doubleFollow(servers[0], servers[1]) + + userToken = await servers[0].users.generateUserAndToken('user', UserRole.USER) + moderatorToken = await servers[0].users.generateUserAndToken('moderator', UserRole.MODERATOR) + }) + + describe('Getting metadata', function () { + let video: VideoDetails + + before(async function () { + const { uuid } = await servers[0].videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) + video = await servers[0].videos.getWithToken({ id: uuid }) + }) + + it('Should not get metadata of private video without token', async function () { + for (const file of getAllFiles(video)) { + await makeRawRequest({ url: file.metadataUrl, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + } + }) + + it('Should not get metadata of private video without the appropriate token', async function () { + for (const file of getAllFiles(video)) { + await makeRawRequest({ url: file.metadataUrl, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + } + }) + + it('Should get metadata of private video with the appropriate token', async function () { + for (const file of getAllFiles(video)) { + await makeRawRequest({ url: file.metadataUrl, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 }) + } + }) + }) + + describe('Deleting files', function () { + let webVideoId: string + let hlsId: string + let remoteId: string + + let validId1: string + let validId2: string + + let hlsFileId: number + let webVideoFileId: number + + let remoteHLSFileId: number + let remoteWebVideoFileId: number + + before(async function () { + this.timeout(300_000) + + { + const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) + await waitJobs(servers) + + const video = await servers[1].videos.get({ id: uuid }) + remoteId = video.uuid + remoteHLSFileId = video.streamingPlaylists[0].files[0].id + remoteWebVideoFileId = video.files[0].id + } + + { + await servers[0].config.enableTranscoding({ hls: true, webVideo: true }) + + { + const { uuid } = await servers[0].videos.quickUpload({ name: 'both 1' }) + await waitJobs(servers) + + const video = await servers[0].videos.get({ id: uuid }) + validId1 = video.uuid + hlsFileId = video.streamingPlaylists[0].files[0].id + webVideoFileId = video.files[0].id + } + + { + const { uuid } = await servers[0].videos.quickUpload({ name: 'both 2' }) + validId2 = uuid + } + } + + await waitJobs(servers) + + { + await servers[0].config.enableTranscoding({ hls: true, webVideo: false }) + const { uuid } = await servers[0].videos.quickUpload({ name: 'hls' }) + hlsId = uuid + } + + await waitJobs(servers) + + { + await servers[0].config.enableTranscoding({ webVideo: true, hls: false }) + const { uuid } = await servers[0].videos.quickUpload({ name: 'web-video' }) + webVideoId = uuid + } + + await waitJobs(servers) + }) + + it('Should not delete files of a unknown video', async function () { + const expectedStatus = HttpStatusCode.NOT_FOUND_404 + + await servers[0].videos.removeHLSPlaylist({ videoId: 404, expectedStatus }) + await servers[0].videos.removeAllWebVideoFiles({ videoId: 404, expectedStatus }) + + await servers[0].videos.removeHLSFile({ videoId: 404, fileId: hlsFileId, expectedStatus }) + await servers[0].videos.removeWebVideoFile({ videoId: 404, fileId: webVideoFileId, expectedStatus }) + }) + + it('Should not delete unknown files', async function () { + const expectedStatus = HttpStatusCode.NOT_FOUND_404 + + await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: webVideoFileId, expectedStatus }) + await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: hlsFileId, expectedStatus }) + }) + + it('Should not delete files of a remote video', async function () { + const expectedStatus = HttpStatusCode.BAD_REQUEST_400 + + await servers[0].videos.removeHLSPlaylist({ videoId: remoteId, expectedStatus }) + await servers[0].videos.removeAllWebVideoFiles({ videoId: remoteId, expectedStatus }) + + await servers[0].videos.removeHLSFile({ videoId: remoteId, fileId: remoteHLSFileId, expectedStatus }) + await servers[0].videos.removeWebVideoFile({ videoId: remoteId, fileId: remoteWebVideoFileId, expectedStatus }) + }) + + it('Should not delete files by a non admin user', async function () { + const expectedStatus = HttpStatusCode.FORBIDDEN_403 + + await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: userToken, expectedStatus }) + await servers[0].videos.removeHLSPlaylist({ videoId: validId1, token: moderatorToken, expectedStatus }) + + await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: userToken, expectedStatus }) + await servers[0].videos.removeAllWebVideoFiles({ videoId: validId1, token: moderatorToken, expectedStatus }) + + await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: userToken, expectedStatus }) + await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId, token: moderatorToken, expectedStatus }) + + await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: userToken, expectedStatus }) + await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId, token: moderatorToken, expectedStatus }) + }) + + it('Should not delete files if the files are not available', async function () { + await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + await servers[0].videos.removeHLSFile({ videoId: hlsId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await servers[0].videos.removeWebVideoFile({ videoId: webVideoId, fileId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should not delete files if no both versions are available', async function () { + await servers[0].videos.removeHLSPlaylist({ videoId: hlsId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await servers[0].videos.removeAllWebVideoFiles({ videoId: webVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should delete files if both versions are available', async function () { + await servers[0].videos.removeHLSFile({ videoId: validId1, fileId: hlsFileId }) + await servers[0].videos.removeWebVideoFile({ videoId: validId1, fileId: webVideoFileId }) + + await servers[0].videos.removeHLSPlaylist({ videoId: validId1 }) + await servers[0].videos.removeAllWebVideoFiles({ videoId: validId2 }) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/api/check-params/video-imports.ts b/packages/tests/src/api/check-params/video-imports.ts new file mode 100644 index 000000000..e078cedd6 --- /dev/null +++ b/packages/tests/src/api/check-params/video-imports.ts @@ -0,0 +1,433 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { omit } from '@peertube/peertube-core-utils' +import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { FIXTURE_URLS } from '@tests/shared/tests.js' +import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makePostBodyRequest, + makeUploadRequest, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test video imports API validator', function () { + const path = '/api/v1/videos/imports' + let server: PeerTubeServer + let userAccessToken = '' + let channelId: number + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + const username = 'user1' + const password = 'my super password' + await server.users.create({ username, password }) + userAccessToken = await server.login.getAccessToken({ username, password }) + + { + const { videoChannels } = await server.users.getMyInfo() + channelId = videoChannels[0].id + } + }) + + describe('When listing my video imports', function () { + const myPath = '/api/v1/users/me/videos/imports' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, myPath, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, myPath, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, myPath, server.accessToken) + }) + + it('Should fail with a bad videoChannelSyncId param', async function () { + await makeGetRequest({ + url: server.url, + path: myPath, + query: { videoChannelSyncId: 'toto' }, + token: server.accessToken + }) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken }) + }) + }) + + describe('When adding a video import', function () { + let baseCorrectParams + + before(function () { + baseCorrectParams = { + targetUrl: FIXTURE_URLS.goodVideo, + name: 'my super name', + category: 5, + licence: 1, + language: 'pt', + nsfw: false, + commentsEnabled: true, + downloadEnabled: true, + waitTranscoding: true, + description: 'my super description', + support: 'my super support text', + tags: [ 'tag1', 'tag2' ], + privacy: VideoPrivacy.PUBLIC, + channelId + } + }) + + it('Should fail with nothing', async function () { + const fields = {} + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail without a target url', async function () { + const fields = omit(baseCorrectParams, [ 'targetUrl' ]) + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad target url', async function () { + const fields = { ...baseCorrectParams, targetUrl: 'htt://hello' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with localhost', async function () { + const fields = { ...baseCorrectParams, targetUrl: 'http://localhost:8000' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a private IP target urls', async function () { + const targetUrls = [ + 'http://127.0.0.1:8000', + 'http://127.0.0.1', + 'http://127.0.0.1/hello', + 'https://192.168.1.42', + 'http://192.168.1.42', + 'http://127.0.0.1.cpy.re' + ] + + for (const targetUrl of targetUrls) { + const fields = { ...baseCorrectParams, targetUrl } + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + } + }) + + it('Should fail with a long name', async function () { + const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad category', async function () { + const fields = { ...baseCorrectParams, category: 125 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad licence', async function () { + const fields = { ...baseCorrectParams, licence: 125 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad language', async function () { + const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a long description', async function () { + const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a long support text', async function () { + const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail without a channel', async function () { + const fields = omit(baseCorrectParams, [ 'channelId' ]) + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a bad channel', async function () { + const fields = { ...baseCorrectParams, channelId: 545454 } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with another user channel', async function () { + const user = { + username: 'fake', + password: 'fake_password' + } + await server.users.create({ username: user.username, password: user.password }) + + const accessTokenUser = await server.login.getAccessToken(user) + const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser }) + const customChannelId = videoChannels[0].id + + const fields = { ...baseCorrectParams, channelId: customChannelId } + + await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields }) + }) + + it('Should fail with too many tags', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a tag length too low', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with a tag length too big', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail with an incorrect thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a big thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with an incorrect preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('video_short.mp4') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with a big preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('custom-preview-big.png') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with an invalid torrent file', async function () { + const fields = omit(baseCorrectParams, [ 'targetUrl' ]) + const attaches = { + torrentfile: buildAbsoluteFixturePath('avatar-big.png') + } + + await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) + }) + + it('Should fail with an invalid magnet URI', async function () { + let fields = omit(baseCorrectParams, [ 'targetUrl' ]) + fields = { ...fields, magnetUri: 'blabla' } + + await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should succeed with the correct parameters', async function () { + this.timeout(120000) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should forbid to import http videos', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + import: { + videos: { + http: { + enabled: false + }, + torrent: { + enabled: true + } + } + } + } + }) + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields: baseCorrectParams, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + + it('Should forbid to import torrent videos', async function () { + await server.config.updateCustomSubConfig({ + newConfig: { + import: { + videos: { + http: { + enabled: true + }, + torrent: { + enabled: false + } + } + } + } + }) + + let fields = omit(baseCorrectParams, [ 'targetUrl' ]) + fields = { ...fields, magnetUri: FIXTURE_URLS.magnet } + + await makePostBodyRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + + fields = omit(fields, [ 'magnetUri' ]) + const attaches = { + torrentfile: buildAbsoluteFixturePath('video-720p.torrent') + } + + await makeUploadRequest({ + url: server.url, + path, + token: server.accessToken, + fields, + attaches, + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + }) + }) + + describe('Deleting/cancelling a video import', function () { + let importId: number + + async function importVideo () { + const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } + const res = await server.imports.importVideo({ attributes }) + + return res.id + } + + before(async function () { + importId = await importVideo() + }) + + it('Should fail with an invalid import id', async function () { + await server.imports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.imports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown import id', async function () { + await server.imports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await server.imports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail without token', async function () { + await server.imports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await server.imports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with another user token', async function () { + await server.imports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await server.imports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail to cancel non pending import', async function () { + this.timeout(60000) + + await waitJobs([ server ]) + + await server.imports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) + }) + + it('Should succeed to delete an import', async function () { + await server.imports.delete({ importId }) + }) + + it('Should fail to delete a pending import', async function () { + await server.jobs.pauseJobQueue() + + importId = await importVideo() + + await server.imports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) + }) + + it('Should succeed to cancel an import', async function () { + importId = await importVideo() + + await server.imports.cancel({ importId }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-passwords.ts b/packages/tests/src/api/check-params/video-passwords.ts new file mode 100644 index 000000000..3f57ebe74 --- /dev/null +++ b/packages/tests/src/api/check-params/video-passwords.ts @@ -0,0 +1,604 @@ +import { expect } from 'chai' +import { + HttpStatusCode, + HttpStatusCodeType, + PeerTubeProblemDocument, + ServerErrorCode, + VideoCreateResult, + VideoPrivacy +} from '@peertube/peertube-models' +import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { + cleanupTests, + createSingleServer, + makePostBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { FIXTURE_URLS } from '@tests/shared/tests.js' +import { checkUploadVideoParam } from '@tests/shared/videos.js' + +describe('Test video passwords validator', function () { + let path: string + let server: PeerTubeServer + let userAccessToken = '' + let video: VideoCreateResult + let channelId: number + let publicVideo: VideoCreateResult + let commentId: number + // --------------------------------------------------------------- + + before(async function () { + this.timeout(50000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + await server.config.updateCustomSubConfig({ + newConfig: { + live: { + enabled: true, + latencySetting: { + enabled: false + }, + allowReplay: false + }, + import: { + videos: { + http:{ + enabled: true + } + } + } + } + }) + + userAccessToken = await server.users.generateUserAndToken('user1') + + { + const body = await server.users.getMyInfo() + channelId = body.videoChannels[0].id + } + + { + video = await server.videos.quickUpload({ + name: 'password protected video', + privacy: VideoPrivacy.PASSWORD_PROTECTED, + videoPasswords: [ 'password1', 'password2' ] + }) + } + path = '/api/v1/videos/' + }) + + async function checkVideoPasswordOptions (options: { + server: PeerTubeServer + token: string + videoPasswords: string[] + expectedStatus: HttpStatusCodeType + mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live' + }) { + const { server, token, videoPasswords, expectedStatus = HttpStatusCode.OK_200, mode } = options + const attaches = { + fixture: buildAbsoluteFixturePath('video_short.webm') + } + const baseCorrectParams = { + name: 'my super name', + category: 5, + licence: 1, + language: 'pt', + nsfw: false, + commentsEnabled: true, + downloadEnabled: true, + waitTranscoding: true, + description: 'my super description', + support: 'my super support text', + tags: [ 'tag1', 'tag2' ], + privacy: VideoPrivacy.PASSWORD_PROTECTED, + channelId, + originallyPublishedAt: new Date().toISOString() + } + if (mode === 'uploadLegacy') { + const fields = { ...baseCorrectParams, videoPasswords } + return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'legacy' }) + } + + if (mode === 'uploadResumable') { + const fields = { ...baseCorrectParams, videoPasswords } + return checkUploadVideoParam({ server, token, attributes: { ...fields, ...attaches }, expectedStatus, mode: 'resumable' }) + } + + if (mode === 'import') { + const attributes = { ...baseCorrectParams, targetUrl: FIXTURE_URLS.goodVideo, videoPasswords } + return server.imports.importVideo({ attributes, expectedStatus }) + } + + if (mode === 'updateVideo') { + const attributes = { ...baseCorrectParams, videoPasswords } + return server.videos.update({ token, expectedStatus, id: video.id, attributes }) + } + + if (mode === 'updatePasswords') { + return server.videoPasswords.updateAll({ token, expectedStatus, videoId: video.id, passwords: videoPasswords }) + } + + if (mode === 'live') { + const fields = { ...baseCorrectParams, videoPasswords } + + return server.live.create({ fields, expectedStatus }) + } + } + + function validateVideoPasswordList (mode: 'uploadLegacy' | 'uploadResumable' | 'import' | 'updateVideo' | 'updatePasswords' | 'live') { + + it('Should fail with a password protected privacy without providing a password', async function () { + await checkVideoPasswordOptions({ + server, + token: server.accessToken, + videoPasswords: undefined, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + mode + }) + }) + + it('Should fail with a password protected privacy and an empty password list', async function () { + const videoPasswords = [] + + await checkVideoPasswordOptions({ + server, + token: server.accessToken, + videoPasswords, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + mode + }) + }) + + it('Should fail with a password protected privacy and a too short password', async function () { + const videoPasswords = [ 'p' ] + + await checkVideoPasswordOptions({ + server, + token: server.accessToken, + videoPasswords, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + mode + }) + }) + + it('Should fail with a password protected privacy and a too long password', async function () { + const videoPasswords = [ 'Very very very very very very very very very very very very very very very very very very long password' ] + + await checkVideoPasswordOptions({ + server, + token: server.accessToken, + videoPasswords, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + mode + }) + }) + + it('Should fail with a password protected privacy and an empty password', async function () { + const videoPasswords = [ '' ] + + await checkVideoPasswordOptions({ + server, + token: server.accessToken, + videoPasswords, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + mode + }) + }) + + it('Should fail with a password protected privacy and duplicated passwords', async function () { + const videoPasswords = [ 'password', 'password' ] + + await checkVideoPasswordOptions({ + server, + token: server.accessToken, + videoPasswords, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + mode + }) + }) + + if (mode === 'updatePasswords') { + it('Should fail for an unauthenticated user', async function () { + const videoPasswords = [ 'password' ] + await checkVideoPasswordOptions({ + server, + token: null, + videoPasswords, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401, + mode + }) + }) + + it('Should fail for an unauthorized user', async function () { + const videoPasswords = [ 'password' ] + await checkVideoPasswordOptions({ + server, + token: userAccessToken, + videoPasswords, + expectedStatus: HttpStatusCode.FORBIDDEN_403, + mode + }) + }) + } + + it('Should succeed with a password protected privacy and correct passwords', async function () { + const videoPasswords = [ 'password1', 'password2' ] + const expectedStatus = mode === 'updatePasswords' || mode === 'updateVideo' + ? HttpStatusCode.NO_CONTENT_204 + : HttpStatusCode.OK_200 + + await checkVideoPasswordOptions({ server, token: server.accessToken, videoPasswords, expectedStatus, mode }) + }) + } + + describe('When adding or updating a video', function () { + describe('Resumable upload', function () { + validateVideoPasswordList('uploadResumable') + }) + + describe('Legacy upload', function () { + validateVideoPasswordList('uploadLegacy') + }) + + describe('When importing a video', function () { + validateVideoPasswordList('import') + }) + + describe('When updating a video', function () { + validateVideoPasswordList('updateVideo') + }) + + describe('When updating the password list of a video', function () { + validateVideoPasswordList('updatePasswords') + }) + + describe('When creating a live', function () { + validateVideoPasswordList('live') + }) + }) + + async function checkVideoAccessOptions (options: { + server: PeerTubeServer + token?: string + videoPassword?: string + expectedStatus: HttpStatusCodeType + mode: 'get' | 'getWithPassword' | 'getWithToken' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token' + }) { + const { server, token = null, videoPassword, expectedStatus, mode } = options + + if (mode === 'get') { + return server.videos.get({ id: video.id, expectedStatus }) + } + + if (mode === 'getWithToken') { + return server.videos.getWithToken({ + id: video.id, + token, + expectedStatus + }) + } + + if (mode === 'getWithPassword') { + return server.videos.getWithPassword({ + id: video.id, + token, + expectedStatus, + password: videoPassword + }) + } + + if (mode === 'rate') { + return server.videos.rate({ + id: video.id, + token, + expectedStatus, + rating: 'like', + videoPassword + }) + } + + if (mode === 'createThread') { + const fields = { text: 'super comment' } + const headers = videoPassword !== undefined && videoPassword !== null + ? { 'x-peertube-video-password': videoPassword } + : undefined + const body = await makePostBodyRequest({ + url: server.url, + path: path + video.uuid + '/comment-threads', + token, + fields, + headers, + expectedStatus + }) + return JSON.parse(body.text) + } + + if (mode === 'replyThread') { + const fields = { text: 'super reply' } + const headers = videoPassword !== undefined && videoPassword !== null + ? { 'x-peertube-video-password': videoPassword } + : undefined + return makePostBodyRequest({ + url: server.url, + path: path + video.uuid + '/comments/' + commentId, + token, + fields, + headers, + expectedStatus + }) + } + if (mode === 'listThreads') { + return server.comments.listThreads({ + videoId: video.id, + token, + expectedStatus, + videoPassword + }) + } + + if (mode === 'listCaptions') { + return server.captions.list({ + videoId: video.id, + token, + expectedStatus, + videoPassword + }) + } + + if (mode === 'token') { + return server.videoToken.create({ + videoId: video.id, + token, + expectedStatus, + videoPassword + }) + } + } + + function checkVideoError (error: any, mode: 'providePassword' | 'incorrectPassword') { + const serverCode = mode === 'providePassword' + ? ServerErrorCode.VIDEO_REQUIRES_PASSWORD + : ServerErrorCode.INCORRECT_VIDEO_PASSWORD + + const message = mode === 'providePassword' + ? 'Please provide a password to access this password protected video' + : 'Incorrect video password. Access to the video is denied.' + + if (!error.code) { + error = JSON.parse(error.text) + } + + expect(error.code).to.equal(serverCode) + expect(error.detail).to.equal(message) + expect(error.error).to.equal(message) + + expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403) + } + + function validateVideoAccess (mode: 'get' | 'listCaptions' | 'createThread' | 'listThreads' | 'replyThread' | 'rate' | 'token') { + const requiresUserAuth = [ 'createThread', 'replyThread', 'rate' ].includes(mode) + let tokens: string[] + if (!requiresUserAuth) { + it('Should fail without providing a password for an unlogged user', async function () { + const body = await checkVideoAccessOptions({ server, expectedStatus: HttpStatusCode.FORBIDDEN_403, mode }) + const error = body as unknown as PeerTubeProblemDocument + + checkVideoError(error, 'providePassword') + }) + } + + it('Should fail without providing a password for an unauthorised user', async function () { + const tmp = mode === 'get' ? 'getWithToken' : mode + + const body = await checkVideoAccessOptions({ + server, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403, + mode: tmp + }) + + const error = body as unknown as PeerTubeProblemDocument + + checkVideoError(error, 'providePassword') + }) + + it('Should fail if a wrong password is entered', async function () { + const tmp = mode === 'get' ? 'getWithPassword' : mode + tokens = [ userAccessToken, server.accessToken ] + + if (!requiresUserAuth) tokens.push(null) + + for (const token of tokens) { + const body = await checkVideoAccessOptions({ + server, + token, + videoPassword: 'toto', + expectedStatus: HttpStatusCode.FORBIDDEN_403, + mode: tmp + }) + const error = body as unknown as PeerTubeProblemDocument + + checkVideoError(error, 'incorrectPassword') + } + }) + + it('Should fail if an empty password is entered', async function () { + const tmp = mode === 'get' ? 'getWithPassword' : mode + + for (const token of tokens) { + const body = await checkVideoAccessOptions({ + server, + token, + videoPassword: '', + expectedStatus: HttpStatusCode.FORBIDDEN_403, + mode: tmp + }) + const error = body as unknown as PeerTubeProblemDocument + + checkVideoError(error, 'incorrectPassword') + } + }) + + it('Should fail if an inccorect password containing the correct password is entered', async function () { + const tmp = mode === 'get' ? 'getWithPassword' : mode + + for (const token of tokens) { + const body = await checkVideoAccessOptions({ + server, + token, + videoPassword: 'password11', + expectedStatus: HttpStatusCode.FORBIDDEN_403, + mode: tmp + }) + const error = body as unknown as PeerTubeProblemDocument + + checkVideoError(error, 'incorrectPassword') + } + }) + + it('Should succeed without providing a password for an authorised user', async function () { + const tmp = mode === 'get' ? 'getWithToken' : mode + const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 + + const body = await checkVideoAccessOptions({ server, token: server.accessToken, expectedStatus, mode: tmp }) + + if (mode === 'createThread') commentId = body.comment.id + }) + + it('Should succeed using correct passwords', async function () { + const tmp = mode === 'get' ? 'getWithPassword' : mode + const expectedStatus = mode === 'rate' ? HttpStatusCode.NO_CONTENT_204 : HttpStatusCode.OK_200 + + for (const token of tokens) { + await checkVideoAccessOptions({ server, videoPassword: 'password1', token, expectedStatus, mode: tmp }) + await checkVideoAccessOptions({ server, videoPassword: 'password2', token, expectedStatus, mode: tmp }) + } + }) + } + + describe('When accessing password protected video', function () { + + describe('For getting a password protected video', function () { + validateVideoAccess('get') + }) + + describe('For rating a video', function () { + validateVideoAccess('rate') + }) + + describe('For creating a thread', function () { + validateVideoAccess('createThread') + }) + + describe('For replying to a thread', function () { + validateVideoAccess('replyThread') + }) + + describe('For listing threads', function () { + validateVideoAccess('listThreads') + }) + + describe('For getting captions', function () { + validateVideoAccess('listCaptions') + }) + + describe('For creating video file token', function () { + validateVideoAccess('token') + }) + }) + + describe('When listing passwords', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path + video.uuid + '/passwords', server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path + video.uuid + '/passwords', server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path + video.uuid + '/passwords', server.accessToken) + }) + + it('Should fail for unauthenticated user', async function () { + await server.videoPasswords.list({ + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401, + videoId: video.id + }) + }) + + it('Should fail for unauthorized user', async function () { + await server.videoPasswords.list({ + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403, + videoId: video.id + }) + }) + + it('Should succeed with the correct parameters', async function () { + await server.videoPasswords.list({ + token: server.accessToken, + expectedStatus: HttpStatusCode.OK_200, + videoId: video.id + }) + }) + }) + + describe('When deleting a password', async function () { + const passwords = (await server.videoPasswords.list({ videoId: video.id })).data + + it('Should fail with wrong password id', async function () { + await server.videoPasswords.remove({ id: -1, videoId: video.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail for unauthenticated user', async function () { + await server.videoPasswords.remove({ + id: passwords[0].id, + token: null, + videoId: video.id, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail for unauthorized user', async function () { + await server.videoPasswords.remove({ + id: passwords[0].id, + token: userAccessToken, + videoId: video.id, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail for non password protected video', async function () { + publicVideo = await server.videos.quickUpload({ name: 'public video' }) + await server.videoPasswords.remove({ id: passwords[0].id, videoId: publicVideo.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail for password not linked to correct video', async function () { + const video2 = await server.videos.quickUpload({ + name: 'password protected video', + privacy: VideoPrivacy.PASSWORD_PROTECTED, + videoPasswords: [ 'password1', 'password2' ] + }) + await server.videoPasswords.remove({ id: passwords[0].id, videoId: video2.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with correct parameter', async function () { + await server.videoPasswords.remove({ id: passwords[0].id, videoId: video.id, expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + }) + + it('Should fail for last password of a video', async function () { + await server.videoPasswords.remove({ id: passwords[1].id, videoId: video.id, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-playlists.ts b/packages/tests/src/api/check-params/video-playlists.ts new file mode 100644 index 000000000..7f5be18d4 --- /dev/null +++ b/packages/tests/src/api/check-params/video-playlists.ts @@ -0,0 +1,695 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { + HttpStatusCode, + VideoPlaylistCreate, + VideoPlaylistCreateResult, + VideoPlaylistElementCreate, + VideoPlaylistElementUpdate, + VideoPlaylistPrivacy, + VideoPlaylistReorder, + VideoPlaylistType +} from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + PlaylistsCommand, + setAccessTokensToServers, + setDefaultVideoChannel +} from '@peertube/peertube-server-commands' + +describe('Test video playlists API validator', function () { + let server: PeerTubeServer + let userAccessToken: string + + let playlist: VideoPlaylistCreateResult + let privatePlaylistUUID: string + + let watchLaterPlaylistId: number + let videoId: number + let elementId: number + + let command: PlaylistsCommand + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + userAccessToken = await server.users.generateUserAndToken('user1') + videoId = (await server.videos.quickUpload({ name: 'video 1' })).id + + command = server.playlists + + { + const { data } = await command.listByAccount({ + token: server.accessToken, + handle: 'root', + start: 0, + count: 5, + playlistType: VideoPlaylistType.WATCH_LATER + }) + watchLaterPlaylistId = data[0].id + } + + { + playlist = await command.create({ + attributes: { + displayName: 'super playlist', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: server.store.channel.id + } + }) + } + + { + const created = await command.create({ + attributes: { + displayName: 'private', + privacy: VideoPlaylistPrivacy.PRIVATE + } + }) + privatePlaylistUUID = created.uuid + } + }) + + 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' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, globalPath, server.accessToken) + await checkBadStartPagination(server.url, accountPath, server.accessToken) + await checkBadStartPagination(server.url, videoChannelPath, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, globalPath, server.accessToken) + await checkBadCountPagination(server.url, accountPath, server.accessToken) + await checkBadCountPagination(server.url, videoChannelPath, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, globalPath, server.accessToken) + await checkBadSortPagination(server.url, accountPath, server.accessToken) + await checkBadSortPagination(server.url, videoChannelPath, server.accessToken) + }) + + it('Should fail with a bad playlist type', async function () { + await makeGetRequest({ url: server.url, path: globalPath, query: { playlistType: 3 } }) + await makeGetRequest({ url: server.url, path: accountPath, query: { playlistType: 3 } }) + await makeGetRequest({ url: server.url, path: videoChannelPath, query: { playlistType: 3 } }) + }) + + it('Should fail with a bad account parameter', async function () { + const accountPath = '/api/v1/accounts/root2/video-playlists' + + await makeGetRequest({ + url: server.url, + path: accountPath, + expectedStatus: HttpStatusCode.NOT_FOUND_404, + token: server.accessToken + }) + }) + + it('Should fail with a bad video channel parameter', async function () { + const accountPath = '/api/v1/video-channels/bad_channel/video-playlists' + + await makeGetRequest({ + url: server.url, + path: accountPath, + expectedStatus: HttpStatusCode.NOT_FOUND_404, + token: server.accessToken + }) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path: globalPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken }) + await makeGetRequest({ url: server.url, path: accountPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken }) + await makeGetRequest({ + url: server.url, + path: videoChannelPath, + expectedStatus: HttpStatusCode.OK_200, + token: server.accessToken + }) + }) + }) + + describe('When listing videos of a playlist', function () { + const path = '/api/v1/video-playlists/' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path + playlist.shortUUID + '/videos', server.accessToken) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path: path + playlist.shortUUID + '/videos', expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When getting a video playlist', function () { + it('Should fail with a bad id or uuid', async function () { + await command.get({ playlistId: 'toto', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an unknown playlist', async function () { + await command.get({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail to get an unlisted playlist with the number id', async function () { + const playlist = await command.create({ + attributes: { + displayName: 'super playlist', + videoChannelId: server.store.channel.id, + privacy: VideoPlaylistPrivacy.UNLISTED + } + }) + + await command.get({ playlistId: playlist.id, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should succeed with the correct params', async function () { + await command.get({ playlistId: playlist.uuid, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When creating/updating a video playlist', function () { + const getBase = ( + attributes?: Partial, + wrapper?: Partial[0]> + ) => { + return { + attributes: { + displayName: 'display name', + privacy: VideoPlaylistPrivacy.UNLISTED, + thumbnailfile: 'custom-thumbnail.jpg', + videoChannelId: server.store.channel.id, + + ...attributes + }, + + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + + ...wrapper + } + } + const getUpdate = (params: any, playlistId: number | string) => { + return { ...params, playlistId } + } + + it('Should fail with an unauthenticated user', async function () { + const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + + await command.create(params) + await command.update(getUpdate(params, playlist.shortUUID)) + }) + + it('Should fail without displayName', async function () { + const params = getBase({ displayName: undefined }) + + await command.create(params) + }) + + it('Should fail with an incorrect display name', async function () { + const params = getBase({ displayName: 's'.repeat(300) }) + + await command.create(params) + await command.update(getUpdate(params, playlist.shortUUID)) + }) + + it('Should fail with an incorrect description', async function () { + const params = getBase({ description: 't' }) + + await command.create(params) + await command.update(getUpdate(params, playlist.shortUUID)) + }) + + it('Should fail with an incorrect privacy', async function () { + const params = getBase({ privacy: 45 as any }) + + await command.create(params) + await command.update(getUpdate(params, playlist.shortUUID)) + }) + + it('Should fail with an unknown video channel id', async function () { + const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + + await command.create(params) + await command.update(getUpdate(params, playlist.shortUUID)) + }) + + it('Should fail with an incorrect thumbnail file', async function () { + const params = getBase({ thumbnailfile: 'video_short.mp4' }) + + await command.create(params) + await command.update(getUpdate(params, playlist.shortUUID)) + }) + + it('Should fail with a thumbnail file too big', async function () { + const params = getBase({ thumbnailfile: 'custom-preview-big.png' }) + + await command.create(params) + await command.update(getUpdate(params, playlist.shortUUID)) + }) + + it('Should fail to set "public" a playlist not assigned to a channel', async function () { + const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: undefined }) + const params2 = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: 'null' as any }) + const params3 = getBase({ privacy: undefined, videoChannelId: 'null' as any }) + + await command.create(params) + await command.create(params2) + await command.update(getUpdate(params, privatePlaylistUUID)) + await command.update(getUpdate(params2, playlist.shortUUID)) + await command.update(getUpdate(params3, playlist.shortUUID)) + }) + + it('Should fail with an unknown playlist to update', async function () { + await command.update(getUpdate( + getBase({}, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }), + 42 + )) + }) + + it('Should fail to update a playlist of another user', async function () { + await command.update(getUpdate( + getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }), + playlist.shortUUID + )) + }) + + it('Should fail to update the watch later playlist', async function () { + await command.update(getUpdate( + getBase({}, { expectedStatus: HttpStatusCode.BAD_REQUEST_400 }), + watchLaterPlaylistId + )) + }) + + it('Should succeed with the correct params', async function () { + { + const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 }) + await command.create(params) + } + + { + const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + await command.update(getUpdate(params, playlist.shortUUID)) + } + }) + }) + + describe('When adding an element in a playlist', function () { + const getBase = ( + attributes?: Partial, + wrapper?: Partial[0]> + ) => { + return { + attributes: { + videoId, + startTimestamp: 2, + stopTimestamp: 3, + + ...attributes + }, + + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + playlistId: playlist.id, + + ...wrapper + } + } + + it('Should fail with an unauthenticated user', async function () { + const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await command.addElement(params) + }) + + it('Should fail with the playlist of another user', async function () { + const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await command.addElement(params) + }) + + it('Should fail with an unknown or incorrect playlist id', async function () { + { + const params = getBase({}, { playlistId: 'toto' }) + await command.addElement(params) + } + + { + const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.addElement(params) + } + }) + + it('Should fail with an unknown or incorrect video id', async function () { + const params = getBase({ videoId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.addElement(params) + }) + + it('Should fail with a bad start/stop timestamp', async function () { + { + const params = getBase({ startTimestamp: -42 }) + await command.addElement(params) + } + + { + const params = getBase({ stopTimestamp: 'toto' as any }) + await command.addElement(params) + } + }) + + it('Succeed with the correct params', async function () { + const params = getBase({}, { expectedStatus: HttpStatusCode.OK_200 }) + const created = await command.addElement(params) + elementId = created.id + }) + }) + + describe('When updating an element in a playlist', function () { + const getBase = ( + attributes?: Partial, + wrapper?: Partial[0]> + ) => { + return { + attributes: { + startTimestamp: 1, + stopTimestamp: 2, + + ...attributes + }, + + elementId, + playlistId: playlist.id, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + + ...wrapper + } + } + + it('Should fail with an unauthenticated user', async function () { + const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await command.updateElement(params) + }) + + it('Should fail with the playlist of another user', async function () { + const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await command.updateElement(params) + }) + + it('Should fail with an unknown or incorrect playlist id', async function () { + { + const params = getBase({}, { playlistId: 'toto' }) + await command.updateElement(params) + } + + { + const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.updateElement(params) + } + }) + + it('Should fail with an unknown or incorrect playlistElement id', async function () { + { + const params = getBase({}, { elementId: 'toto' }) + await command.updateElement(params) + } + + { + const params = getBase({}, { elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.updateElement(params) + } + }) + + it('Should fail with a bad start/stop timestamp', async function () { + { + const params = getBase({ startTimestamp: 'toto' as any }) + await command.updateElement(params) + } + + { + const params = getBase({ stopTimestamp: -42 }) + await command.updateElement(params) + } + }) + + it('Should fail with an unknown element', async function () { + const params = getBase({}, { elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.updateElement(params) + }) + + it('Succeed with the correct params', async function () { + const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + await command.updateElement(params) + }) + }) + + describe('When reordering elements of a playlist', function () { + let videoId3: number + let videoId4: number + + const getBase = ( + attributes?: Partial, + wrapper?: Partial[0]> + ) => { + return { + attributes: { + startPosition: 1, + insertAfterPosition: 2, + reorderLength: 3, + + ...attributes + }, + + playlistId: playlist.shortUUID, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + + ...wrapper + } + } + + before(async function () { + videoId3 = (await server.videos.quickUpload({ name: 'video 3' })).id + videoId4 = (await server.videos.quickUpload({ name: 'video 4' })).id + + for (const id of [ videoId3, videoId4 ]) { + await command.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: id } }) + } + }) + + it('Should fail with an unauthenticated user', async function () { + const params = getBase({}, { token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await command.reorderElements(params) + }) + + it('Should fail with the playlist of another user', async function () { + const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await command.reorderElements(params) + }) + + it('Should fail with an invalid playlist', async function () { + { + const params = getBase({}, { playlistId: 'toto' }) + await command.reorderElements(params) + } + + { + const params = getBase({}, { playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.reorderElements(params) + } + }) + + it('Should fail with an invalid start position', async function () { + { + const params = getBase({ startPosition: -1 }) + await command.reorderElements(params) + } + + { + const params = getBase({ startPosition: 'toto' as any }) + await command.reorderElements(params) + } + + { + const params = getBase({ startPosition: 42 }) + await command.reorderElements(params) + } + }) + + it('Should fail with an invalid insert after position', async function () { + { + const params = getBase({ insertAfterPosition: 'toto' as any }) + await command.reorderElements(params) + } + + { + const params = getBase({ insertAfterPosition: -2 }) + await command.reorderElements(params) + } + + { + const params = getBase({ insertAfterPosition: 42 }) + await command.reorderElements(params) + } + }) + + it('Should fail with an invalid reorder length', async function () { + { + const params = getBase({ reorderLength: 'toto' as any }) + await command.reorderElements(params) + } + + { + const params = getBase({ reorderLength: -2 }) + await command.reorderElements(params) + } + + { + const params = getBase({ reorderLength: 42 }) + await command.reorderElements(params) + } + }) + + it('Succeed with the correct params', async function () { + const params = getBase({}, { expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + await command.reorderElements(params) + }) + }) + + describe('When checking exists in playlist endpoint', function () { + const path = '/api/v1/users/me/video-playlists/videos-exist' + + it('Should fail with an unauthenticated user', async function () { + await makeGetRequest({ + url: server.url, + path, + query: { videoIds: [ 1, 2 ] }, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with invalid video ids', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path, + query: { videoIds: 'toto' } + }) + + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path, + query: { videoIds: [ 'toto' ] } + }) + + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path, + query: { videoIds: [ 1, 'toto' ] } + }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path, + query: { videoIds: [ 1, 2 ] }, + expectedStatus: HttpStatusCode.OK_200 + }) + }) + }) + + describe('When deleting an element in a playlist', function () { + const getBase = (wrapper: Partial[0]>) => { + return { + elementId, + playlistId: playlist.uuid, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + + ...wrapper + } + } + + it('Should fail with an unauthenticated user', async function () { + const params = getBase({ token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await command.removeElement(params) + }) + + it('Should fail with the playlist of another user', async function () { + const params = getBase({ token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await command.removeElement(params) + }) + + it('Should fail with an unknown or incorrect playlist id', async function () { + { + const params = getBase({ playlistId: 'toto' }) + await command.removeElement(params) + } + + { + const params = getBase({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.removeElement(params) + } + }) + + it('Should fail with an unknown or incorrect video id', async function () { + { + const params = getBase({ elementId: 'toto' as any }) + await command.removeElement(params) + } + + { + const params = getBase({ elementId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.removeElement(params) + } + }) + + it('Should fail with an unknown element', async function () { + const params = getBase({ elementId: 888, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await command.removeElement(params) + }) + + it('Succeed with the correct params', async function () { + const params = getBase({ expectedStatus: HttpStatusCode.NO_CONTENT_204 }) + await command.removeElement(params) + }) + }) + + describe('When deleting a playlist', function () { + it('Should fail with an unknown playlist', async function () { + await command.delete({ playlistId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a playlist of another user', async function () { + await command.delete({ token: userAccessToken, playlistId: playlist.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with the watch later playlist', async function () { + await command.delete({ playlistId: watchLaterPlaylistId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct params', async function () { + await command.delete({ playlistId: playlist.uuid }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-source.ts b/packages/tests/src/api/check-params/video-source.ts new file mode 100644 index 000000000..918182b8d --- /dev/null +++ b/packages/tests/src/api/check-params/video-source.ts @@ -0,0 +1,154 @@ +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test video sources API validator', function () { + let server: PeerTubeServer = null + let uuid: string + let userToken: string + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + userToken = await server.users.generateUserAndToken('user1') + }) + + describe('When getting latest source', function () { + + before(async function () { + const created = await server.videos.quickUpload({ name: 'video' }) + uuid = created.uuid + }) + + it('Should fail without a valid uuid', async function () { + await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should receive 404 when passing a non existing video id', async function () { + await server.videos.getSource({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should not get the source as unauthenticated', async function () { + await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) + }) + + it('Should not get the source with another user', async function () { + await server.videos.getSource({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: userToken }) + }) + + it('Should succeed with the correct parameters get the source as another user', async function () { + await server.videos.getSource({ id: uuid }) + }) + }) + + describe('When updating source video file', function () { + let userAccessToken: string + let userId: number + + let videoId: string + let userVideoId: string + + before(async function () { + const res = await server.users.generate('user2') + userAccessToken = res.token + userId = res.userId + + const { uuid } = await server.videos.quickUpload({ name: 'video' }) + videoId = uuid + + await waitJobs([ server ]) + }) + + it('Should fail if not enabled on the instance', async function () { + await server.config.disableFileUpdate() + + await server.videos.replaceSourceFile({ videoId, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail on an unknown video', async function () { + await server.config.enableFileUpdate() + + await server.videos.replaceSourceFile({ videoId: 404, fixture: 'video_short.mp4', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with an invalid video', async function () { + await server.config.enableLive({ allowReplay: false }) + + const { video } = await server.live.quickCreate({ saveReplay: false, permanentLive: true }) + await server.videos.replaceSourceFile({ + videoId: video.uuid, + fixture: 'video_short.mp4', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail without token', async function () { + await server.videos.replaceSourceFile({ + token: null, + videoId, + fixture: 'video_short.mp4', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another user', async function () { + await server.videos.replaceSourceFile({ + token: userAccessToken, + videoId, + fixture: 'video_short.mp4', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an incorrect input file', async function () { + await server.videos.replaceSourceFile({ + fixture: 'video_short_fake.webm', + videoId, + completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 + }) + + await server.videos.replaceSourceFile({ + fixture: 'video_short.mkv', + videoId, + expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 + }) + }) + + it('Should fail if quota is exceeded', async function () { + this.timeout(60000) + + const { uuid } = await server.videos.quickUpload({ name: 'user video' }) + userVideoId = uuid + await waitJobs([ server ]) + + await server.users.update({ userId, videoQuota: 1 }) + await server.videos.replaceSourceFile({ + token: userAccessToken, + videoId: uuid, + fixture: 'video_short.mp4', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct params', async function () { + this.timeout(60000) + + await server.users.update({ userId, videoQuota: 1000 * 1000 * 1000 }) + await server.videos.replaceSourceFile({ videoId: userVideoId, fixture: 'video_short.mp4' }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-storyboards.ts b/packages/tests/src/api/check-params/video-storyboards.ts new file mode 100644 index 000000000..f83b541d8 --- /dev/null +++ b/packages/tests/src/api/check-params/video-storyboards.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' +import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' + +describe('Test video storyboards API validator', function () { + let server: PeerTubeServer + + let publicVideo: { uuid: string } + let privateVideo: { uuid: string } + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + publicVideo = await server.videos.quickUpload({ name: 'public' }) + privateVideo = await server.videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE }) + }) + + it('Should fail without a valid uuid', async function () { + await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df563d0b0', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should receive 404 when passing a non existing video id', async function () { + await server.storyboard.list({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should not get the private storyboard without the appropriate token', async function () { + await server.storyboard.list({ id: privateVideo.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401, token: null }) + await server.storyboard.list({ id: publicVideo.uuid, expectedStatus: HttpStatusCode.OK_200, token: null }) + }) + + it('Should succeed with the correct parameters', async function () { + await server.storyboard.list({ id: privateVideo.uuid }) + await server.storyboard.list({ id: publicVideo.uuid }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-studio.ts b/packages/tests/src/api/check-params/video-studio.ts new file mode 100644 index 000000000..ae83f3590 --- /dev/null +++ b/packages/tests/src/api/check-params/video-studio.ts @@ -0,0 +1,392 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode, HttpStatusCodeType, VideoStudioTask } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + setAccessTokensToServers, + VideoStudioCommand, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test video studio API validator', function () { + let server: PeerTubeServer + let command: VideoStudioCommand + let userAccessToken: string + let videoUUID: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(120_000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + userAccessToken = await server.users.generateUserAndToken('user1') + + await server.config.enableMinimumTranscoding() + + const { uuid } = await server.videos.quickUpload({ name: 'video' }) + videoUUID = uuid + + command = server.videoStudio + + await waitJobs([ server ]) + }) + + describe('Task creation', function () { + + describe('Config settings', function () { + + it('Should fail if studio is disabled', async function () { + await server.config.updateExistingSubConfig({ + newConfig: { + videoStudio: { + enabled: false + } + } + }) + + await command.createEditionTasks({ + videoId: videoUUID, + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail to enable studio if transcoding is disabled', async function () { + await server.config.updateExistingSubConfig({ + newConfig: { + videoStudio: { + enabled: true + }, + transcoding: { + enabled: false + } + }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed to enable video studio', async function () { + await server.config.updateExistingSubConfig({ + newConfig: { + videoStudio: { + enabled: true + }, + transcoding: { + enabled: true + } + } + }) + }) + }) + + describe('Common tasks', function () { + + it('Should fail without token', async function () { + await command.createEditionTasks({ + token: null, + videoId: videoUUID, + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another user token', async function () { + await command.createEditionTasks({ + token: userAccessToken, + videoId: videoUUID, + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid video', async function () { + await command.createEditionTasks({ + videoId: 'tintin', + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an unknown video', async function () { + await command.createEditionTasks({ + videoId: 42, + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with an already in transcoding state video', async function () { + this.timeout(60000) + + const { uuid } = await server.videos.quickUpload({ name: 'transcoded video' }) + await waitJobs([ server ]) + + await server.jobs.pauseJobQueue() + await server.videos.runTranscoding({ videoId: uuid, transcodingType: 'hls' }) + + await command.createEditionTasks({ + videoId: uuid, + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + + await server.jobs.resumeJobQueue() + }) + + it('Should fail with a bad complex task', async function () { + await command.createEditionTasks({ + videoId: videoUUID, + tasks: [ + { + name: 'cut', + options: { + start: 1, + end: 2 + } + }, + { + name: 'hadock', + options: { + start: 1, + end: 2 + } + } + ] as any, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail without task', async function () { + await command.createEditionTasks({ + videoId: videoUUID, + tasks: [], + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with too many tasks', async function () { + const tasks: VideoStudioTask[] = [] + + for (let i = 0; i < 110; i++) { + tasks.push({ + name: 'cut', + options: { + start: 1 + } + }) + } + + await command.createEditionTasks({ + videoId: videoUUID, + tasks, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with correct parameters', async function () { + await server.jobs.pauseJobQueue() + + await command.createEditionTasks({ + videoId: videoUUID, + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should fail with a video that is already waiting for edition', async function () { + this.timeout(120000) + + await command.createEditionTasks({ + videoId: videoUUID, + tasks: VideoStudioCommand.getComplexTask(), + expectedStatus: HttpStatusCode.CONFLICT_409 + }) + + await server.jobs.resumeJobQueue() + + await waitJobs([ server ]) + }) + }) + + describe('Cut task', function () { + + async function cut (start: number, end: number, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { + await command.createEditionTasks({ + videoId: videoUUID, + tasks: [ + { + name: 'cut', + options: { + start, + end + } + } + ], + expectedStatus + }) + } + + it('Should fail with bad start/end', async function () { + const invalid = [ + 'tintin', + -1, + undefined + ] + + for (const value of invalid) { + await cut(value as any, undefined) + await cut(undefined, value as any) + } + }) + + it('Should fail with the same start/end', async function () { + await cut(2, 2) + }) + + it('Should fail with inconsistents start/end', async function () { + await cut(2, 1) + }) + + it('Should fail without start and end', async function () { + await cut(undefined, undefined) + }) + + it('Should succeed with the correct params', async function () { + this.timeout(120000) + + await cut(0, 2, HttpStatusCode.NO_CONTENT_204) + + await waitJobs([ server ]) + }) + }) + + describe('Watermark task', function () { + + async function addWatermark (file: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400) { + await command.createEditionTasks({ + videoId: videoUUID, + tasks: [ + { + name: 'add-watermark', + options: { + file + } + } + ], + expectedStatus + }) + } + + it('Should fail without waterkmark', async function () { + await addWatermark(undefined) + }) + + it('Should fail with an invalid watermark', async function () { + await addWatermark('video_short.mp4') + }) + + it('Should succeed with the correct params', async function () { + this.timeout(120000) + + await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204) + + await waitJobs([ server ]) + }) + }) + + describe('Intro/Outro task', function () { + + async function addIntroOutro ( + type: 'add-intro' | 'add-outro', + file: string, + expectedStatus: HttpStatusCodeType = HttpStatusCode.BAD_REQUEST_400 + ) { + await command.createEditionTasks({ + videoId: videoUUID, + tasks: [ + { + name: type, + options: { + file + } + } + ], + expectedStatus + }) + } + + it('Should fail without file', async function () { + await addIntroOutro('add-intro', undefined) + await addIntroOutro('add-outro', undefined) + }) + + it('Should fail with an invalid file', async function () { + await addIntroOutro('add-intro', 'custom-thumbnail.jpg') + await addIntroOutro('add-outro', 'custom-thumbnail.jpg') + }) + + it('Should fail with a file that does not contain video stream', async function () { + await addIntroOutro('add-intro', 'sample.ogg') + await addIntroOutro('add-outro', 'sample.ogg') + + }) + + it('Should succeed with the correct params', async function () { + this.timeout(120000) + + await addIntroOutro('add-intro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204) + await waitJobs([ server ]) + + await addIntroOutro('add-outro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204) + await waitJobs([ server ]) + }) + + it('Should check total quota when creating the task', async function () { + this.timeout(120000) + + const user = await server.users.create({ username: 'user_quota_1' }) + const token = await server.login.getAccessToken('user_quota_1') + const { uuid } = await server.videos.quickUpload({ token, name: 'video_quota_1', fixture: 'video_short.mp4' }) + + const addIntroOutroByUser = (type: 'add-intro' | 'add-outro', expectedStatus: HttpStatusCodeType) => { + return command.createEditionTasks({ + token, + videoId: uuid, + tasks: [ + { + name: type, + options: { + file: 'video_short.mp4' + } + } + ], + expectedStatus + }) + } + + await waitJobs([ server ]) + + const { videoQuotaUsed } = await server.users.getMyQuotaUsed({ token }) + await server.users.update({ userId: user.id, videoQuota: Math.round(videoQuotaUsed * 2.5) }) + + // Still valid + await addIntroOutroByUser('add-intro', HttpStatusCode.NO_CONTENT_204) + + await waitJobs([ server ]) + + // Too much quota + await addIntroOutroByUser('add-intro', HttpStatusCode.PAYLOAD_TOO_LARGE_413) + await addIntroOutroByUser('add-outro', HttpStatusCode.PAYLOAD_TOO_LARGE_413) + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-token.ts b/packages/tests/src/api/check-params/video-token.ts new file mode 100644 index 000000000..5f838102d --- /dev/null +++ b/packages/tests/src/api/check-params/video-token.ts @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' +import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@peertube/peertube-server-commands' + +describe('Test video tokens', function () { + let server: PeerTubeServer + let privateVideoId: string + let passwordProtectedVideoId: string + let userToken: string + + const videoPassword = 'password' + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(300_000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + { + const { uuid } = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE }) + privateVideoId = uuid + } + { + const { uuid } = await server.videos.quickUpload({ + name: 'password protected video', + privacy: VideoPrivacy.PASSWORD_PROTECTED, + videoPasswords: [ videoPassword ] + }) + passwordProtectedVideoId = uuid + } + userToken = await server.users.generateUserAndToken('user1') + }) + + it('Should not generate tokens on private video for unauthenticated user', async function () { + await server.videoToken.create({ videoId: privateVideoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should not generate tokens of unknown video', async function () { + await server.videoToken.create({ videoId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should not generate tokens with incorrect password', async function () { + await server.videoToken.create({ + videoId: passwordProtectedVideoId, + token: null, + expectedStatus: HttpStatusCode.FORBIDDEN_403, + videoPassword: 'incorrectPassword' + }) + }) + + it('Should not generate tokens of a non owned video', async function () { + await server.videoToken.create({ videoId: privateVideoId, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should generate token', async function () { + await server.videoToken.create({ videoId: privateVideoId }) + }) + + it('Should generate token on password protected video', async function () { + await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: null }) + await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword, token: userToken }) + await server.videoToken.create({ videoId: passwordProtectedVideoId, videoPassword }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/videos-common-filters.ts b/packages/tests/src/api/check-params/videos-common-filters.ts new file mode 100644 index 000000000..dbae3010c --- /dev/null +++ b/packages/tests/src/api/check-params/videos-common-filters.ts @@ -0,0 +1,171 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { + HttpStatusCode, + HttpStatusCodeType, + UserRole, + VideoInclude, + VideoIncludeType, + VideoPrivacy, + VideoPrivacyType +} from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel +} from '@peertube/peertube-server-commands' + +describe('Test video filters validators', function () { + let server: PeerTubeServer + let userAccessToken: string + let moderatorAccessToken: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + const user = { username: 'user1', password: 'my super password' } + await server.users.create({ username: user.username, password: user.password }) + userAccessToken = await server.login.getAccessToken(user) + + const moderator = { username: 'moderator', password: 'my super password' } + await server.users.create({ username: moderator.username, password: moderator.password, role: UserRole.MODERATOR }) + + moderatorAccessToken = await server.login.getAccessToken(moderator) + }) + + describe('When setting video filters', function () { + + const validIncludes = [ + VideoInclude.NONE, + VideoInclude.BLOCKED_OWNER, + VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED + ] + + async function testEndpoints (options: { + token?: string + isLocal?: boolean + include?: VideoIncludeType + privacyOneOf?: VideoPrivacyType[] + expectedStatus: HttpStatusCodeType + excludeAlreadyWatched?: boolean + unauthenticatedUser?: boolean + }) { + const paths = [ + '/api/v1/video-channels/root_channel/videos', + '/api/v1/accounts/root/videos', + '/api/v1/videos', + '/api/v1/search/videos' + ] + + for (const path of paths) { + const token = options.unauthenticatedUser + ? undefined + : options.token || server.accessToken + + await makeGetRequest({ + url: server.url, + path, + token, + query: { + isLocal: options.isLocal, + privacyOneOf: options.privacyOneOf, + include: options.include, + excludeAlreadyWatched: options.excludeAlreadyWatched + }, + expectedStatus: options.expectedStatus + }) + } + } + + it('Should fail with a bad privacyOneOf', async function () { + await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with a good privacyOneOf', async function () { + await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should fail to use privacyOneOf with a simple user', async function () { + await testEndpoints({ + privacyOneOf: [ VideoPrivacy.INTERNAL ], + token: userAccessToken, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a bad include', async function () { + await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with a good include', async function () { + for (const include of validIncludes) { + await testEndpoints({ include, expectedStatus: HttpStatusCode.OK_200 }) + } + }) + + it('Should fail to include more videos with a simple user', async function () { + for (const include of validIncludes) { + await testEndpoints({ token: userAccessToken, include, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + } + }) + + it('Should succeed to list all local/all with a moderator', async function () { + for (const include of validIncludes) { + await testEndpoints({ token: moderatorAccessToken, include, expectedStatus: HttpStatusCode.OK_200 }) + } + }) + + it('Should succeed to list all local/all with an admin', async function () { + for (const include of validIncludes) { + await testEndpoints({ token: server.accessToken, include, expectedStatus: HttpStatusCode.OK_200 }) + } + }) + + // Because we cannot authenticate the user on the RSS endpoint + it('Should fail on the feeds endpoint with the all filter', async function () { + for (const include of [ VideoInclude.NOT_PUBLISHED_STATE ]) { + await makeGetRequest({ + url: server.url, + path: '/feeds/videos.json', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401, + query: { + include + } + }) + } + }) + + it('Should succeed on the feeds endpoint with the local filter', async function () { + await makeGetRequest({ + url: server.url, + path: '/feeds/videos.json', + expectedStatus: HttpStatusCode.OK_200, + query: { + isLocal: true + } + }) + }) + + it('Should fail when trying to exclude already watched videos for an unlogged user', async function () { + await testEndpoints({ excludeAlreadyWatched: true, unauthenticatedUser: true, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed when trying to exclude already watched videos for a logged user', async function () { + await testEndpoints({ token: userAccessToken, excludeAlreadyWatched: true, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/videos-history.ts b/packages/tests/src/api/check-params/videos-history.ts new file mode 100644 index 000000000..65d1e9fac --- /dev/null +++ b/packages/tests/src/api/check-params/videos-history.ts @@ -0,0 +1,145 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { checkBadCountPagination, checkBadStartPagination } from '@tests/shared/checks.js' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeDeleteRequest, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test videos history API validator', function () { + const myHistoryPath = '/api/v1/users/me/history/videos' + const myHistoryRemove = myHistoryPath + '/remove' + let viewPath: string + let server: PeerTubeServer + let videoId: number + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + const { id, uuid } = await server.videos.upload() + viewPath = '/api/v1/videos/' + uuid + '/views' + videoId = id + }) + + describe('When notifying a user is watching a video', function () { + + it('Should fail with a bad token', async function () { + const fields = { currentTime: 5 } + await makePutBodyRequest({ url: server.url, path: viewPath, fields, token: 'bad', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should succeed with the correct parameters', async function () { + const fields = { currentTime: 5 } + + await makePutBodyRequest({ + url: server.url, + path: viewPath, + fields, + token: server.accessToken, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When listing user videos history', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, myHistoryPath, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, myHistoryPath, server.accessToken) + }) + + it('Should fail with an unauthenticated user', async function () { + await makeGetRequest({ url: server.url, path: myHistoryPath, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should succeed with the correct params', async function () { + await makeGetRequest({ url: server.url, token: server.accessToken, path: myHistoryPath, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When removing a specific user video history element', function () { + let path: string + + before(function () { + path = myHistoryPath + '/' + videoId + }) + + it('Should fail with an unauthenticated user', async function () { + await makeDeleteRequest({ url: server.url, path, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a bad videoId parameter', async function () { + await makeDeleteRequest({ + url: server.url, + token: server.accessToken, + path: myHistoryRemove + '/hi', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await makeDeleteRequest({ + url: server.url, + token: server.accessToken, + path, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When removing all user videos history', function () { + it('Should fail with an unauthenticated user', async function () { + await makePostBodyRequest({ url: server.url, path: myHistoryPath + '/remove', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with a bad beforeDate parameter', async function () { + const body = { beforeDate: '15' } + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path: myHistoryRemove, + fields: body, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with a valid beforeDate param', async function () { + const body = { beforeDate: new Date().toISOString() } + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path: myHistoryRemove, + fields: body, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + + it('Should succeed without body', async function () { + await makePostBodyRequest({ + url: server.url, + token: server.accessToken, + path: myHistoryRemove, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/videos-overviews.ts b/packages/tests/src/api/check-params/videos-overviews.ts new file mode 100644 index 000000000..ba6f6ac69 --- /dev/null +++ b/packages/tests/src/api/check-params/videos-overviews.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { cleanupTests, createSingleServer, PeerTubeServer } from '@peertube/peertube-server-commands' + +describe('Test videos overview API validator', function () { + let server: PeerTubeServer + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + }) + + describe('When getting videos overview', function () { + + it('Should fail with a bad pagination', async function () { + await server.overviews.getVideos({ page: 0, expectedStatus: 400 }) + await server.overviews.getVideos({ page: 100, expectedStatus: 400 }) + }) + + it('Should succeed with a good pagination', async function () { + await server.overviews.getVideos({ page: 1 }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/videos.ts b/packages/tests/src/api/check-params/videos.ts new file mode 100644 index 000000000..c349ed9fe --- /dev/null +++ b/packages/tests/src/api/check-params/videos.ts @@ -0,0 +1,883 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { join } from 'path' +import { omit, randomInt } from '@peertube/peertube-core-utils' +import { HttpStatusCode, PeerTubeProblemDocument, VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models' +import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { + cleanupTests, + createSingleServer, + makeDeleteRequest, + makeGetRequest, + makePutBodyRequest, + makeUploadRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' +import { checkBadStartPagination, checkBadCountPagination, checkBadSortPagination } from '@tests/shared/checks.js' +import { checkUploadVideoParam } from '@tests/shared/videos.js' + +describe('Test videos API validator', function () { + const path = '/api/v1/videos/' + let server: PeerTubeServer + let userAccessToken = '' + let accountName: string + let channelId: number + let channelName: string + let video: VideoCreateResult + let privateVideo: VideoCreateResult + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + userAccessToken = await server.users.generateUserAndToken('user1') + + { + const body = await server.users.getMyInfo() + channelId = body.videoChannels[0].id + channelName = body.videoChannels[0].name + accountName = body.account.name + '@' + body.account.host + } + + { + privateVideo = await server.videos.quickUpload({ name: 'private video', privacy: VideoPrivacy.PRIVATE }) + } + }) + + describe('When listing videos', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path) + }) + + it('Should fail with a bad skipVideos query', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: 'toto' } }) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200, query: { skipCount: false } }) + }) + }) + + describe('When searching a video', function () { + + it('Should fail with nothing', async function () { + await makeGetRequest({ + url: server.url, + path: join(path, 'search'), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, join(path, 'search', 'test')) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, join(path, 'search', 'test')) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, join(path, 'search', 'test')) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When listing my videos', function () { + const path = '/api/v1/users/me/videos' + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an invalid channel', async function () { + await makeGetRequest({ url: server.url, token: server.accessToken, path, query: { channelId: 'toto' } }) + }) + + it('Should fail with an unknown channel', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path, + query: { channelId: 89898 }, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, token: server.accessToken, path, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When listing account videos', function () { + let path: string + + before(async function () { + path = '/api/v1/accounts/' + accountName + '/videos' + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When listing video channel videos', function () { + let path: string + + before(async function () { + path = '/api/v1/video-channels/' + channelName + '/videos' + }) + + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + describe('When adding a video', function () { + let baseCorrectParams + const baseCorrectAttaches = { + fixture: buildAbsoluteFixturePath('video_short.webm') + } + + before(function () { + // Put in before to have channelId + baseCorrectParams = { + name: 'my super name', + category: 5, + licence: 1, + language: 'pt', + nsfw: false, + commentsEnabled: true, + downloadEnabled: true, + waitTranscoding: true, + description: 'my super description', + support: 'my super support text', + tags: [ 'tag1', 'tag2' ], + privacy: VideoPrivacy.PUBLIC, + channelId, + originallyPublishedAt: new Date().toISOString() + } + }) + + function runSuite (mode: 'legacy' | 'resumable') { + + const baseOptions = () => { + return { + server, + token: server.accessToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400, + mode + } + } + + it('Should fail with nothing', async function () { + const fields = {} + const attaches = {} + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail without name', async function () { + const fields = omit(baseCorrectParams, [ 'name' ]) + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a long name', async function () { + const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a bad category', async function () { + const fields = { ...baseCorrectParams, category: 125 } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a bad licence', async function () { + const fields = { ...baseCorrectParams, licence: 125 } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a bad language', async function () { + const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a long description', async function () { + const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a long support text', async function () { + const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail without a channel', async function () { + const fields = omit(baseCorrectParams, [ 'channelId' ]) + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a bad channel', async function () { + const fields = { ...baseCorrectParams, channelId: 545454 } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with another user channel', async function () { + const user = { + username: 'fake' + randomInt(0, 1500), + password: 'fake_password' + } + await server.users.create({ username: user.username, password: user.password }) + + const accessTokenUser = await server.login.getAccessToken(user) + const { videoChannels } = await server.users.getMyInfo({ token: accessTokenUser }) + const customChannelId = videoChannels[0].id + + const fields = { ...baseCorrectParams, channelId: customChannelId } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ + ...baseOptions(), + token: userAccessToken, + attributes: { ...fields, ...attaches } + }) + }) + + it('Should fail with too many tags', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a tag length too low', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a tag length too big', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a bad schedule update (miss updateAt)', async function () { + const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a bad schedule update (wrong updateAt)', async function () { + const fields = { + ...baseCorrectParams, + + scheduleUpdate: { + privacy: VideoPrivacy.PUBLIC, + updateAt: 'toto' + } + } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a bad originally published at attribute', async function () { + const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' } + const attaches = baseCorrectAttaches + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail without an input file', async function () { + const fields = baseCorrectParams + const attaches = {} + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with an incorrect input file', async function () { + const fields = baseCorrectParams + let attaches = { fixture: buildAbsoluteFixturePath('video_short_fake.webm') } + + await checkUploadVideoParam({ + ...baseOptions(), + attributes: { ...fields, ...attaches }, + // 200 for the init request, 422 when the file has finished being uploaded + expectedStatus: undefined, + completedExpectedStatus: HttpStatusCode.UNPROCESSABLE_ENTITY_422 + }) + + attaches = { fixture: buildAbsoluteFixturePath('video_short.mkv') } + await checkUploadVideoParam({ + ...baseOptions(), + attributes: { ...fields, ...attaches }, + expectedStatus: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415 + }) + }) + + it('Should fail with an incorrect thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('video_short.mp4'), + fixture: buildAbsoluteFixturePath('video_short.mp4') + } + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a big thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png'), + fixture: buildAbsoluteFixturePath('video_short.mp4') + } + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with an incorrect preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('video_short.mp4'), + fixture: buildAbsoluteFixturePath('video_short.mp4') + } + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should fail with a big preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('custom-preview-big.png'), + fixture: buildAbsoluteFixturePath('video_short.mp4') + } + + await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) + }) + + it('Should report the appropriate error', async function () { + const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } + const attaches = baseCorrectAttaches + + const attributes = { ...fields, ...attaches } + const body = await checkUploadVideoParam({ ...baseOptions(), attributes }) + + const error = body as unknown as PeerTubeProblemDocument + + if (mode === 'legacy') { + expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy') + } else { + expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit') + } + + expect(error.type).to.equal('about:blank') + expect(error.title).to.equal('Bad Request') + + expect(error.detail).to.equal('Incorrect request parameters: language') + expect(error.error).to.equal('Incorrect request parameters: language') + + expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) + expect(error['invalid-params'].language).to.exist + }) + + it('Should succeed with the correct parameters', async function () { + this.timeout(30000) + + const fields = baseCorrectParams + + { + const attaches = baseCorrectAttaches + await checkUploadVideoParam({ + ...baseOptions(), + attributes: { ...fields, ...attaches }, + expectedStatus: HttpStatusCode.OK_200 + }) + } + + { + const attaches = { + ...baseCorrectAttaches, + + videofile: buildAbsoluteFixturePath('video_short.mp4') + } + + await checkUploadVideoParam({ + ...baseOptions(), + attributes: { ...fields, ...attaches }, + expectedStatus: HttpStatusCode.OK_200 + }) + } + + { + const attaches = { + ...baseCorrectAttaches, + + videofile: buildAbsoluteFixturePath('video_short.ogv') + } + + await checkUploadVideoParam({ + ...baseOptions(), + attributes: { ...fields, ...attaches }, + expectedStatus: HttpStatusCode.OK_200 + }) + } + }) + } + + describe('Resumable upload', function () { + runSuite('resumable') + }) + + describe('Legacy upload', function () { + runSuite('legacy') + }) + }) + + describe('When updating a video', function () { + const baseCorrectParams = { + name: 'my super name', + category: 5, + licence: 2, + language: 'pt', + nsfw: false, + commentsEnabled: false, + downloadEnabled: false, + description: 'my super description', + privacy: VideoPrivacy.PUBLIC, + tags: [ 'tag1', 'tag2' ] + } + + before(async function () { + const { data } = await server.videos.list() + video = data[0] + }) + + it('Should fail with nothing', async function () { + const fields = {} + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) + }) + + it('Should fail without a valid uuid', async function () { + const fields = baseCorrectParams + await makePutBodyRequest({ url: server.url, path: path + 'blabla', token: server.accessToken, fields }) + }) + + it('Should fail with an unknown id', async function () { + const fields = baseCorrectParams + + await makePutBodyRequest({ + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df5630b06', + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a long name', async function () { + const fields = { ...baseCorrectParams, name: 'super'.repeat(65) } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a bad category', async function () { + const fields = { ...baseCorrectParams, category: 125 } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a bad licence', async function () { + const fields = { ...baseCorrectParams, licence: 125 } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a bad language', async function () { + const fields = { ...baseCorrectParams, language: 'a'.repeat(15) } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a long description', async function () { + const fields = { ...baseCorrectParams, description: 'super'.repeat(2500) } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a long support text', async function () { + const fields = { ...baseCorrectParams, support: 'super'.repeat(201) } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a bad channel', async function () { + const fields = { ...baseCorrectParams, channelId: 545454 } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with too many tags', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6' ] } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a tag length too low', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 't' ] } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a tag length too big', async function () { + const fields = { ...baseCorrectParams, tags: [ 'tag1', 'my_super_tag_too_long_long_long_long_long_long' ] } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a bad schedule update (miss updateAt)', async function () { + const fields = { ...baseCorrectParams, scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a bad schedule update (wrong updateAt)', async function () { + const fields = { ...baseCorrectParams, scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with a bad originally published at param', async function () { + const fields = { ...baseCorrectParams, originallyPublishedAt: 'toto' } + + await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + }) + + it('Should fail with an incorrect thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('video_short.mp4') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + video.shortUUID, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with a big thumbnail file', async function () { + const fields = baseCorrectParams + const attaches = { + thumbnailfile: buildAbsoluteFixturePath('custom-preview-big.png') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + video.shortUUID, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with an incorrect preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('video_short.mp4') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + video.shortUUID, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with a big preview file', async function () { + const fields = baseCorrectParams + const attaches = { + previewfile: buildAbsoluteFixturePath('custom-preview-big.png') + } + + await makeUploadRequest({ + url: server.url, + method: 'PUT', + path: path + video.shortUUID, + token: server.accessToken, + fields, + attaches + }) + }) + + it('Should fail with a video of another user without the appropriate right', async function () { + const fields = baseCorrectParams + + await makePutBodyRequest({ + url: server.url, + path: path + video.shortUUID, + token: userAccessToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with a video of another server') + + it('Shoud report the appropriate error', async function () { + const fields = { ...baseCorrectParams, licence: 125 } + + const res = await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) + const error = res.body as PeerTubeProblemDocument + + expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo') + + expect(error.type).to.equal('about:blank') + expect(error.title).to.equal('Bad Request') + + expect(error.detail).to.equal('Incorrect request parameters: licence') + expect(error.error).to.equal('Incorrect request parameters: licence') + + expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) + expect(error['invalid-params'].licence).to.exist + }) + + it('Should succeed with the correct parameters', async function () { + const fields = baseCorrectParams + + await makePutBodyRequest({ + url: server.url, + path: path + video.shortUUID, + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When getting a video', function () { + it('Should return the list of the videos with nothing', async function () { + const res = await makeGetRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.data).to.be.an('array') + expect(res.body.data.length).to.equal(6) + }) + + it('Should fail without a correct uuid', async function () { + await server.videos.get({ id: 'coucou', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should return 404 with an incorrect video', async function () { + await server.videos.get({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Shoud report the appropriate error', async function () { + const body = await server.videos.get({ id: 'hi', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + const error = body as unknown as PeerTubeProblemDocument + + expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo') + + expect(error.type).to.equal('about:blank') + expect(error.title).to.equal('Bad Request') + + expect(error.detail).to.equal('Incorrect request parameters: id') + expect(error.error).to.equal('Incorrect request parameters: id') + + expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) + expect(error['invalid-params'].id).to.exist + }) + + it('Should succeed with the correct parameters', async function () { + await server.videos.get({ id: video.shortUUID }) + }) + }) + + describe('When rating a video', function () { + let videoId: number + + before(async function () { + const { data } = await server.videos.list() + videoId = data[0].id + }) + + it('Should fail without a valid uuid', async function () { + const fields = { + rating: 'like' + } + await makePutBodyRequest({ url: server.url, path: path + 'blabla/rate', token: server.accessToken, fields }) + }) + + it('Should fail with an unknown id', async function () { + const fields = { + rating: 'like' + } + await makePutBodyRequest({ + url: server.url, + path: path + '4da6fde3-88f7-4d16-b119-108df5630b06/rate', + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with a wrong rating', async function () { + const fields = { + rating: 'likes' + } + await makePutBodyRequest({ url: server.url, path: path + videoId + '/rate', token: server.accessToken, fields }) + }) + + it('Should fail with a private video of another user', async function () { + const fields = { + rating: 'like' + } + await makePutBodyRequest({ + url: server.url, + path: path + privateVideo.uuid + '/rate', + token: userAccessToken, + fields, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should succeed with the correct parameters', async function () { + const fields = { + rating: 'like' + } + await makePutBodyRequest({ + url: server.url, + path: path + videoId + '/rate', + token: server.accessToken, + fields, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + }) + }) + + describe('When removing a video', function () { + it('Should have 404 with nothing', async function () { + await makeDeleteRequest({ + url: server.url, + path, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail without a correct uuid', async function () { + await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a video which does not exist', async function () { + await server.videos.remove({ id: '4da6fde3-88f7-4d16-b119-108df5630b06', expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a video of another user without the appropriate right', async function () { + await server.videos.remove({ token: userAccessToken, id: video.uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail with a video of another server') + + it('Shoud report the appropriate error', async function () { + const body = await server.videos.remove({ id: 'hello', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + const error = body as PeerTubeProblemDocument + + expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo') + + expect(error.type).to.equal('about:blank') + expect(error.title).to.equal('Bad Request') + + expect(error.detail).to.equal('Incorrect request parameters: id') + expect(error.error).to.equal('Incorrect request parameters: id') + + expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400) + expect(error['invalid-params'].id).to.exist + }) + + it('Should succeed with the correct parameters', async function () { + await server.videos.remove({ id: video.uuid }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/views.ts b/packages/tests/src/api/check-params/views.ts new file mode 100644 index 000000000..c454d4b80 --- /dev/null +++ b/packages/tests/src/api/check-params/views.ts @@ -0,0 +1,227 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel +} from '@peertube/peertube-server-commands' + +describe('Test videos views', function () { + let servers: PeerTubeServer[] + let liveVideoId: string + let videoId: string + let remoteVideoId: string + let userAccessToken: string + + before(async function () { + this.timeout(120000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + await setDefaultVideoChannel(servers) + + await servers[0].config.enableLive({ allowReplay: false, transcoding: false }); + + ({ uuid: videoId } = await servers[0].videos.quickUpload({ name: 'video' })); + ({ uuid: remoteVideoId } = await servers[1].videos.quickUpload({ name: 'video' })); + ({ uuid: liveVideoId } = await servers[0].live.create({ + fields: { + name: 'live', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id + } + })) + + userAccessToken = await servers[0].users.generateUserAndToken('user') + + await doubleFollow(servers[0], servers[1]) + }) + + describe('When viewing a video', async function () { + + it('Should fail without current time', async function () { + await servers[0].views.view({ id: videoId, currentTime: undefined, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an invalid current time', async function () { + await servers[0].views.view({ id: videoId, currentTime: -1, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await servers[0].views.view({ id: videoId, currentTime: 10, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with correct parameters', async function () { + await servers[0].views.view({ id: videoId, currentTime: 1 }) + }) + }) + + describe('When getting overall stats', function () { + + it('Should fail with a remote video', async function () { + await servers[0].videoStats.getOverallStats({ videoId: remoteVideoId, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should fail without token', async function () { + await servers[0].videoStats.getOverallStats({ videoId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with another token', async function () { + await servers[0].videoStats.getOverallStats({ + videoId, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid start date', async function () { + await servers[0].videoStats.getOverallStats({ + videoId, + startDate: 'fake' as any, + endDate: new Date().toISOString(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an invalid end date', async function () { + await servers[0].videoStats.getOverallStats({ + videoId, + startDate: new Date().toISOString(), + endDate: 'fake' as any, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await servers[0].videoStats.getOverallStats({ + videoId, + startDate: new Date().toISOString(), + endDate: new Date().toISOString() + }) + }) + }) + + describe('When getting timeserie stats', function () { + + it('Should fail with a remote video', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId: remoteVideoId, + metric: 'viewers', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail without token', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId, + token: null, + metric: 'viewers', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another token', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId, + token: userAccessToken, + metric: 'viewers', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an invalid metric', async function () { + await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'hello' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with an invalid start date', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId, + metric: 'viewers', + startDate: 'fake' as any, + endDate: new Date(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with an invalid end date', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId, + metric: 'viewers', + startDate: new Date(), + endDate: 'fake' as any, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if start date is specified but not end date', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId, + metric: 'viewers', + startDate: new Date(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail if end date is specified but not start date', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId, + metric: 'viewers', + endDate: new Date(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with a too big interval', async function () { + await servers[0].videoStats.getTimeserieStats({ + videoId, + metric: 'viewers', + startDate: new Date('2000-04-07T08:31:57.126Z'), + endDate: new Date(), + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await servers[0].videoStats.getTimeserieStats({ videoId, metric: 'viewers' }) + }) + }) + + describe('When getting retention stats', function () { + + it('Should fail with a remote video', async function () { + await servers[0].videoStats.getRetentionStats({ + videoId: remoteVideoId, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail without token', async function () { + await servers[0].videoStats.getRetentionStats({ + videoId, + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with another token', async function () { + await servers[0].videoStats.getRetentionStats({ + videoId, + token: userAccessToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail on live video', async function () { + await servers[0].videoStats.getRetentionStats({ videoId: liveVideoId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should succeed with the correct parameters', async function () { + await servers[0].videoStats.getRetentionStats({ videoId }) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) -- cgit v1.2.3