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/plugins/action-hooks.ts | 298 ++++++++ packages/tests/src/plugins/external-auth.ts | 436 +++++++++++ packages/tests/src/plugins/filter-hooks.ts | 909 +++++++++++++++++++++++ packages/tests/src/plugins/html-injection.ts | 73 ++ packages/tests/src/plugins/id-and-pass-auth.ts | 248 +++++++ packages/tests/src/plugins/index.ts | 13 + packages/tests/src/plugins/plugin-helpers.ts | 383 ++++++++++ packages/tests/src/plugins/plugin-router.ts | 105 +++ packages/tests/src/plugins/plugin-storage.ts | 95 +++ packages/tests/src/plugins/plugin-transcoding.ts | 279 +++++++ packages/tests/src/plugins/plugin-unloading.ts | 75 ++ packages/tests/src/plugins/plugin-websocket.ts | 76 ++ packages/tests/src/plugins/translations.ts | 80 ++ packages/tests/src/plugins/video-constants.ts | 180 +++++ 14 files changed, 3250 insertions(+) create mode 100644 packages/tests/src/plugins/action-hooks.ts create mode 100644 packages/tests/src/plugins/external-auth.ts create mode 100644 packages/tests/src/plugins/filter-hooks.ts create mode 100644 packages/tests/src/plugins/html-injection.ts create mode 100644 packages/tests/src/plugins/id-and-pass-auth.ts create mode 100644 packages/tests/src/plugins/index.ts create mode 100644 packages/tests/src/plugins/plugin-helpers.ts create mode 100644 packages/tests/src/plugins/plugin-router.ts create mode 100644 packages/tests/src/plugins/plugin-storage.ts create mode 100644 packages/tests/src/plugins/plugin-transcoding.ts create mode 100644 packages/tests/src/plugins/plugin-unloading.ts create mode 100644 packages/tests/src/plugins/plugin-websocket.ts create mode 100644 packages/tests/src/plugins/translations.ts create mode 100644 packages/tests/src/plugins/video-constants.ts (limited to 'packages/tests/src/plugins') diff --git a/packages/tests/src/plugins/action-hooks.ts b/packages/tests/src/plugins/action-hooks.ts new file mode 100644 index 000000000..136c7671b --- /dev/null +++ b/packages/tests/src/plugins/action-hooks.ts @@ -0,0 +1,298 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { ServerHookName, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + killallServers, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers, + setDefaultVideoChannel, + stopFfmpeg, + waitJobs +} from '@peertube/peertube-server-commands' + +describe('Test plugin action hooks', function () { + let servers: PeerTubeServer[] + let videoUUID: string + let threadId: number + + function checkHook (hook: ServerHookName, strictCount = true, count = 1) { + return servers[0].servers.waitUntilLog('Run hook ' + hook, count, strictCount) + } + + before(async function () { + this.timeout(120000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + await setDefaultVideoChannel(servers) + + await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() }) + + await killallServers([ servers[0] ]) + + await servers[0].run({ + live: { + enabled: true + } + }) + + await servers[0].config.enableFileUpdate() + + await doubleFollow(servers[0], servers[1]) + }) + + describe('Application hooks', function () { + it('Should run action:application.listening', async function () { + await checkHook('action:application.listening') + }) + }) + + describe('Videos hooks', function () { + + it('Should run action:api.video.uploaded', async function () { + const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } }) + videoUUID = uuid + + await checkHook('action:api.video.uploaded') + }) + + it('Should run action:api.video.updated', async function () { + await servers[0].videos.update({ id: videoUUID, attributes: { name: 'video updated' } }) + + await checkHook('action:api.video.updated') + }) + + it('Should run action:api.video.viewed', async function () { + await servers[0].views.simulateView({ id: videoUUID }) + + await checkHook('action:api.video.viewed') + }) + + it('Should run action:api.video.file-updated', async function () { + await servers[0].videos.replaceSourceFile({ videoId: videoUUID, fixture: 'video_short.mp4' }) + + await checkHook('action:api.video.file-updated') + }) + + it('Should run action:api.video.deleted', async function () { + await servers[0].videos.remove({ id: videoUUID }) + + await checkHook('action:api.video.deleted') + }) + + after(async function () { + const { uuid } = await servers[0].videos.quickUpload({ name: 'video' }) + videoUUID = uuid + }) + }) + + describe('Video channel hooks', function () { + const channelName = 'my_super_channel' + + it('Should run action:api.video-channel.created', async function () { + await servers[0].channels.create({ attributes: { name: channelName } }) + + await checkHook('action:api.video-channel.created') + }) + + it('Should run action:api.video-channel.updated', async function () { + await servers[0].channels.update({ channelName, attributes: { displayName: 'my display name' } }) + + await checkHook('action:api.video-channel.updated') + }) + + it('Should run action:api.video-channel.deleted', async function () { + await servers[0].channels.delete({ channelName }) + + await checkHook('action:api.video-channel.deleted') + }) + }) + + describe('Live hooks', function () { + + it('Should run action:api.live-video.created', async function () { + const attributes = { + name: 'live', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id + } + + await servers[0].live.create({ fields: attributes }) + + await checkHook('action:api.live-video.created') + }) + + it('Should run action:live.video.state.updated', async function () { + this.timeout(60000) + + const attributes = { + name: 'live', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id + } + + const { uuid: liveVideoId } = await servers[0].live.create({ fields: attributes }) + const ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveVideoId }) + await servers[0].live.waitUntilPublished({ videoId: liveVideoId }) + await waitJobs(servers) + + await checkHook('action:live.video.state.updated', true, 1) + + await stopFfmpeg(ffmpegCommand) + await servers[0].live.waitUntilEnded({ videoId: liveVideoId }) + await waitJobs(servers) + + await checkHook('action:live.video.state.updated', true, 2) + }) + }) + + describe('Comments hooks', function () { + it('Should run action:api.video-thread.created', async function () { + const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' }) + threadId = created.id + + await checkHook('action:api.video-thread.created') + }) + + it('Should run action:api.video-comment-reply.created', async function () { + await servers[0].comments.addReply({ videoId: videoUUID, toCommentId: threadId, text: 'reply' }) + + await checkHook('action:api.video-comment-reply.created') + }) + + it('Should run action:api.video-comment.deleted', async function () { + await servers[0].comments.delete({ videoId: videoUUID, commentId: threadId }) + + await checkHook('action:api.video-comment.deleted') + }) + }) + + describe('Captions hooks', function () { + it('Should run action:api.video-caption.created', async function () { + await servers[0].captions.add({ videoId: videoUUID, language: 'en', fixture: 'subtitle-good.srt' }) + + await checkHook('action:api.video-caption.created') + }) + + it('Should run action:api.video-caption.deleted', async function () { + await servers[0].captions.delete({ videoId: videoUUID, language: 'en' }) + + await checkHook('action:api.video-caption.deleted') + }) + }) + + describe('Users hooks', function () { + let userId: number + + it('Should run action:api.user.registered', async function () { + await servers[0].registrations.register({ username: 'registered_user' }) + + await checkHook('action:api.user.registered') + }) + + it('Should run action:api.user.created', async function () { + const user = await servers[0].users.create({ username: 'created_user' }) + userId = user.id + + await checkHook('action:api.user.created') + }) + + it('Should run action:api.user.oauth2-got-token', async function () { + await servers[0].login.login({ user: { username: 'created_user' } }) + + await checkHook('action:api.user.oauth2-got-token') + }) + + it('Should run action:api.user.blocked', async function () { + await servers[0].users.banUser({ userId }) + + await checkHook('action:api.user.blocked') + }) + + it('Should run action:api.user.unblocked', async function () { + await servers[0].users.unbanUser({ userId }) + + await checkHook('action:api.user.unblocked') + }) + + it('Should run action:api.user.updated', async function () { + await servers[0].users.update({ userId, videoQuota: 50 }) + + await checkHook('action:api.user.updated') + }) + + it('Should run action:api.user.deleted', async function () { + await servers[0].users.remove({ userId }) + + await checkHook('action:api.user.deleted') + }) + }) + + describe('Playlist hooks', function () { + let playlistId: number + let videoId: number + + before(async function () { + { + const { id } = await servers[0].playlists.create({ + attributes: { + displayName: 'My playlist', + privacy: VideoPlaylistPrivacy.PRIVATE + } + }) + playlistId = id + } + + { + const { id } = await servers[0].videos.upload({ attributes: { name: 'my super name' } }) + videoId = id + } + }) + + it('Should run action:api.video-playlist-element.created', async function () { + await servers[0].playlists.addElement({ playlistId, attributes: { videoId } }) + + await checkHook('action:api.video-playlist-element.created') + }) + }) + + describe('Notification hook', function () { + + it('Should run action:notifier.notification.created', async function () { + await checkHook('action:notifier.notification.created', false) + }) + }) + + describe('Activity Pub hooks', function () { + let videoUUID: string + + it('Should run action:activity-pub.remote-video.created', async function () { + this.timeout(30000) + + const { uuid } = await servers[1].videos.quickUpload({ name: 'remote video' }) + videoUUID = uuid + + await servers[0].servers.waitUntilLog('action:activity-pub.remote-video.created - AP remote video - video remote video') + }) + + it('Should run action:activity-pub.remote-video.updated', async function () { + this.timeout(30000) + + await servers[1].videos.update({ id: videoUUID, attributes: { name: 'remote video updated' } }) + + await servers[0].servers.waitUntilLog( + 'action:activity-pub.remote-video.updated - AP remote video updated - video remote video updated', + 1, + false + ) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/plugins/external-auth.ts b/packages/tests/src/plugins/external-auth.ts new file mode 100644 index 000000000..c7fe22185 --- /dev/null +++ b/packages/tests/src/plugins/external-auth.ts @@ -0,0 +1,436 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { wait } from '@peertube/peertube-core-utils' +import { HttpStatusCode, HttpStatusCodeType, UserAdminFlag, UserRole } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + decodeQueryString, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +async function loginExternal (options: { + server: PeerTubeServer + npmName: string + authName: string + username: string + query?: any + expectedStatus?: HttpStatusCodeType + expectedStatusStep2?: HttpStatusCodeType +}) { + const res = await options.server.plugins.getExternalAuth({ + npmName: options.npmName, + npmVersion: '0.0.1', + authName: options.authName, + query: options.query, + expectedStatus: options.expectedStatus || HttpStatusCode.FOUND_302 + }) + + if (res.status !== HttpStatusCode.FOUND_302) return + + const location = res.header.location + const { externalAuthToken } = decodeQueryString(location) + + const resLogin = await options.server.login.loginUsingExternalToken({ + username: options.username, + externalAuthToken: externalAuthToken as string, + expectedStatus: options.expectedStatusStep2 + }) + + return resLogin.body +} + +describe('Test external auth plugins', function () { + let server: PeerTubeServer + + let cyanAccessToken: string + let cyanRefreshToken: string + + let kefkaAccessToken: string + let kefkaRefreshToken: string + let kefkaId: number + + let externalAuthToken: string + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1, { + rates_limit: { + login: { + max: 30 + } + } + }) + + await setAccessTokensToServers([ server ]) + + for (const suffix of [ 'one', 'two', 'three' ]) { + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-external-auth-' + suffix) }) + } + }) + + it('Should display the correct configuration', async function () { + const config = await server.config.getConfig() + + const auths = config.plugin.registeredExternalAuths + expect(auths).to.have.lengthOf(9) + + const auth2 = auths.find((a) => a.authName === 'external-auth-2') + expect(auth2).to.exist + expect(auth2.authDisplayName).to.equal('External Auth 2') + expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one') + }) + + it('Should redirect for a Cyan login', async function () { + const res = await server.plugins.getExternalAuth({ + npmName: 'test-external-auth-one', + npmVersion: '0.0.1', + authName: 'external-auth-1', + query: { + username: 'cyan' + }, + expectedStatus: HttpStatusCode.FOUND_302 + }) + + const location = res.header.location + expect(location.startsWith('/login?')).to.be.true + + const searchParams = decodeQueryString(location) + + expect(searchParams.externalAuthToken).to.exist + expect(searchParams.username).to.equal('cyan') + + externalAuthToken = searchParams.externalAuthToken as string + }) + + it('Should reject auto external login with a missing or invalid token', async function () { + const command = server.login + + await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: '', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.loginUsingExternalToken({ username: 'cyan', externalAuthToken: 'blabla', expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should reject auto external login with a missing or invalid username', async function () { + const command = server.login + + await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.loginUsingExternalToken({ username: '', externalAuthToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should reject auto external login with an expired token', async function () { + this.timeout(15000) + + await wait(5000) + + await server.login.loginUsingExternalToken({ + username: 'cyan', + externalAuthToken, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await server.servers.waitUntilLog('expired external auth token', 4) + }) + + it('Should auto login Cyan, create the user and use the token', async function () { + { + const res = await loginExternal({ + server, + npmName: 'test-external-auth-one', + authName: 'external-auth-1', + query: { + username: 'cyan' + }, + username: 'cyan' + }) + + cyanAccessToken = res.access_token + cyanRefreshToken = res.refresh_token + } + + { + const body = await server.users.getMyInfo({ token: cyanAccessToken }) + expect(body.username).to.equal('cyan') + expect(body.account.displayName).to.equal('cyan') + expect(body.email).to.equal('cyan@example.com') + expect(body.role.id).to.equal(UserRole.USER) + expect(body.adminFlags).to.equal(UserAdminFlag.NONE) + expect(body.videoQuota).to.equal(5242880) + expect(body.videoQuotaDaily).to.equal(-1) + } + }) + + it('Should auto login Kefka, create the user and use the token', async function () { + { + const res = await loginExternal({ + server, + npmName: 'test-external-auth-one', + authName: 'external-auth-2', + username: 'kefka' + }) + + kefkaAccessToken = res.access_token + kefkaRefreshToken = res.refresh_token + } + + { + const body = await server.users.getMyInfo({ token: kefkaAccessToken }) + expect(body.username).to.equal('kefka') + expect(body.account.displayName).to.equal('Kefka Palazzo') + expect(body.email).to.equal('kefka@example.com') + expect(body.role.id).to.equal(UserRole.ADMINISTRATOR) + expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) + expect(body.videoQuota).to.equal(42000) + expect(body.videoQuotaDaily).to.equal(42100) + + kefkaId = body.id + } + }) + + it('Should refresh Cyan token, but not Kefka token', async function () { + { + const resRefresh = await server.login.refreshToken({ refreshToken: cyanRefreshToken }) + cyanAccessToken = resRefresh.body.access_token + cyanRefreshToken = resRefresh.body.refresh_token + + const body = await server.users.getMyInfo({ token: cyanAccessToken }) + expect(body.username).to.equal('cyan') + } + + { + await server.login.refreshToken({ refreshToken: kefkaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + } + }) + + it('Should update Cyan profile', async function () { + await server.users.updateMe({ + token: cyanAccessToken, + displayName: 'Cyan Garamonde', + description: 'Retainer to the king of Doma' + }) + + const body = await server.users.getMyInfo({ token: cyanAccessToken }) + expect(body.account.displayName).to.equal('Cyan Garamonde') + expect(body.account.description).to.equal('Retainer to the king of Doma') + }) + + it('Should logout Cyan', async function () { + await server.login.logout({ token: cyanAccessToken }) + }) + + it('Should have logged out Cyan', async function () { + await server.servers.waitUntilLog('On logout cyan') + + await server.users.getMyInfo({ token: cyanAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should login Cyan and keep the old existing profile', async function () { + { + const res = await loginExternal({ + server, + npmName: 'test-external-auth-one', + authName: 'external-auth-1', + query: { + username: 'cyan' + }, + username: 'cyan' + }) + + cyanAccessToken = res.access_token + } + + const body = await server.users.getMyInfo({ token: cyanAccessToken }) + expect(body.username).to.equal('cyan') + expect(body.account.displayName).to.equal('Cyan Garamonde') + expect(body.account.description).to.equal('Retainer to the king of Doma') + expect(body.role.id).to.equal(UserRole.USER) + }) + + it('Should login Kefka and update the profile', async function () { + { + await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 }) + await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' }) + + const body = await server.users.getMyInfo({ token: kefkaAccessToken }) + expect(body.username).to.equal('kefka') + expect(body.account.displayName).to.equal('kefka updated') + expect(body.videoQuota).to.equal(43000) + expect(body.videoQuotaDaily).to.equal(43100) + } + + { + const res = await loginExternal({ + server, + npmName: 'test-external-auth-one', + authName: 'external-auth-2', + username: 'kefka' + }) + + kefkaAccessToken = res.access_token + kefkaRefreshToken = res.refresh_token + + const body = await server.users.getMyInfo({ token: kefkaAccessToken }) + expect(body.username).to.equal('kefka') + expect(body.account.displayName).to.equal('Kefka Palazzo') + expect(body.videoQuota).to.equal(42000) + expect(body.videoQuotaDaily).to.equal(43100) + } + }) + + it('Should not update an external auth email', async function () { + await server.users.updateMe({ + token: cyanAccessToken, + email: 'toto@example.com', + currentPassword: 'toto', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should reject token of Kefka by the plugin hook', async function () { + await wait(5000) + + await server.users.getMyInfo({ token: kefkaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should unregister external-auth-2 and do not login existing Kefka', async function () { + await server.plugins.updateSettings({ + npmName: 'peertube-plugin-test-external-auth-one', + settings: { disableKefka: true } + }) + + await server.login.login({ user: { username: 'kefka', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + await loginExternal({ + server, + npmName: 'test-external-auth-one', + authName: 'external-auth-2', + query: { + username: 'kefka' + }, + username: 'kefka', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should have disabled this auth', async function () { + const config = await server.config.getConfig() + + const auths = config.plugin.registeredExternalAuths + expect(auths).to.have.lengthOf(8) + + const auth1 = auths.find(a => a.authName === 'external-auth-2') + expect(auth1).to.not.exist + }) + + it('Should uninstall the plugin one and do not login Cyan', async function () { + await server.plugins.uninstall({ npmName: 'peertube-plugin-test-external-auth-one' }) + + await loginExternal({ + server, + npmName: 'test-external-auth-one', + authName: 'external-auth-1', + query: { + username: 'cyan' + }, + username: 'cyan', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + + await server.login.login({ user: { username: 'cyan', password: null }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.login.login({ user: { username: 'cyan', password: '' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.login.login({ user: { username: 'cyan', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should not login kefka with another plugin', async function () { + await loginExternal({ + server, + npmName: 'test-external-auth-two', + authName: 'external-auth-4', + username: 'kefka2', + expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 + }) + + await loginExternal({ + server, + npmName: 'test-external-auth-two', + authName: 'external-auth-4', + username: 'kefka', + expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should not login an existing user email', async function () { + await server.users.create({ username: 'existing_user', password: 'super_password' }) + + await loginExternal({ + server, + npmName: 'test-external-auth-two', + authName: 'external-auth-6', + username: 'existing_user', + expectedStatusStep2: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should be able to login an existing user username and channel', async function () { + await server.users.create({ username: 'existing_user2' }) + await server.users.create({ username: 'existing_user2-1_channel' }) + + // Test twice to ensure we don't generate a username on every login + for (let i = 0; i < 2; i++) { + const res = await loginExternal({ + server, + npmName: 'test-external-auth-two', + authName: 'external-auth-7', + username: 'existing_user2' + }) + + const token = res.access_token + + const myInfo = await server.users.getMyInfo({ token }) + expect(myInfo.username).to.equal('existing_user2-1') + + expect(myInfo.videoChannels[0].name).to.equal('existing_user2-1_channel-1') + } + }) + + it('Should display the correct configuration', async function () { + const config = await server.config.getConfig() + + const auths = config.plugin.registeredExternalAuths + expect(auths).to.have.lengthOf(7) + + const auth2 = auths.find((a) => a.authName === 'external-auth-2') + expect(auth2).to.not.exist + }) + + after(async function () { + await cleanupTests([ server ]) + }) + + it('Should forward the redirectUrl if the plugin returns one', async function () { + const resLogin = await loginExternal({ + server, + npmName: 'test-external-auth-three', + authName: 'external-auth-7', + username: 'cid' + }) + + const { redirectUrl } = await server.login.logout({ token: resLogin.access_token }) + expect(redirectUrl).to.equal('https://example.com/redirectUrl') + }) + + it('Should call the plugin\'s onLogout method with the request', async function () { + const resLogin = await loginExternal({ + server, + npmName: 'test-external-auth-three', + authName: 'external-auth-8', + username: 'cid' + }) + + const { redirectUrl } = await server.login.logout({ token: resLogin.access_token }) + expect(redirectUrl).to.equal('https://example.com/redirectUrl?access_token=' + resLogin.access_token) + }) +}) diff --git a/packages/tests/src/plugins/filter-hooks.ts b/packages/tests/src/plugins/filter-hooks.ts new file mode 100644 index 000000000..88cfee631 --- /dev/null +++ b/packages/tests/src/plugins/filter-hooks.ts @@ -0,0 +1,909 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { + HttpStatusCode, + PeerTubeProblemDocument, + VideoDetails, + VideoImportState, + VideoPlaylist, + VideoPlaylistPrivacy, + VideoPrivacy +} from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + makeActivityPubGetRequest, + makeGetRequest, + makeRawRequest, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers, + setDefaultVideoChannel, + waitJobs +} from '@peertube/peertube-server-commands' +import { FIXTURE_URLS } from '../shared/tests.js' + +describe('Test plugin filter hooks', function () { + let servers: PeerTubeServer[] + let videoUUID: string + let threadId: number + let videoPlaylistUUID: string + + before(async function () { + this.timeout(120000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + await setDefaultVideoChannel(servers) + await doubleFollow(servers[0], servers[1]) + + await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath() }) + await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') }) + { + ({ uuid: videoPlaylistUUID } = await servers[0].playlists.create({ + attributes: { + displayName: 'my super playlist', + privacy: VideoPlaylistPrivacy.PUBLIC, + description: 'my super description', + videoChannelId: servers[0].store.channel.id + } + })) + } + + for (let i = 0; i < 10; i++) { + const video = await servers[0].videos.upload({ attributes: { name: 'default video ' + i } }) + await servers[0].playlists.addElement({ playlistId: videoPlaylistUUID, attributes: { videoId: video.id } }) + } + + const { data } = await servers[0].videos.list() + videoUUID = data[0].uuid + + await servers[0].config.updateCustomSubConfig({ + newConfig: { + live: { enabled: true }, + signup: { enabled: true }, + videoFile: { + update: { + enabled: true + } + }, + import: { + videos: { + http: { enabled: true }, + torrent: { enabled: true } + } + } + } + }) + + // Root subscribes to itself + await servers[0].subscriptions.add({ targetUri: 'root_channel@' + servers[0].host }) + }) + + describe('Videos', function () { + + it('Should run filter:api.videos.list.params', async function () { + const { data } = await servers[0].videos.list({ start: 0, count: 2 }) + + // 2 plugins do +1 to the count parameter + expect(data).to.have.lengthOf(4) + }) + + it('Should run filter:api.videos.list.result', async function () { + const { total } = await servers[0].videos.list({ start: 0, count: 0 }) + + // Plugin do +1 to the total result + expect(total).to.equal(11) + }) + + it('Should run filter:api.video-playlist.videos.list.params', async function () { + const { data } = await servers[0].playlists.listVideos({ + count: 2, + playlistId: videoPlaylistUUID + }) + + // 1 plugin do +1 to the count parameter + expect(data).to.have.lengthOf(3) + }) + + it('Should run filter:api.video-playlist.videos.list.result', async function () { + const { total } = await servers[0].playlists.listVideos({ + count: 0, + playlistId: videoPlaylistUUID + }) + + // Plugin do +1 to the total result + expect(total).to.equal(11) + }) + + it('Should run filter:api.accounts.videos.list.params', async function () { + const { data } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) + + // 1 plugin do +1 to the count parameter + expect(data).to.have.lengthOf(3) + }) + + it('Should run filter:api.accounts.videos.list.result', async function () { + const { total } = await servers[0].videos.listByAccount({ handle: 'root', start: 0, count: 2 }) + + // Plugin do +2 to the total result + expect(total).to.equal(12) + }) + + it('Should run filter:api.video-channels.videos.list.params', async function () { + const { data } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) + + // 1 plugin do +3 to the count parameter + expect(data).to.have.lengthOf(5) + }) + + it('Should run filter:api.video-channels.videos.list.result', async function () { + const { total } = await servers[0].videos.listByChannel({ handle: 'root_channel', start: 0, count: 2 }) + + // Plugin do +3 to the total result + expect(total).to.equal(13) + }) + + it('Should run filter:api.user.me.videos.list.params', async function () { + const { data } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) + + // 1 plugin do +4 to the count parameter + expect(data).to.have.lengthOf(6) + }) + + it('Should run filter:api.user.me.videos.list.result', async function () { + const { total } = await servers[0].videos.listMyVideos({ start: 0, count: 2 }) + + // Plugin do +4 to the total result + expect(total).to.equal(14) + }) + + it('Should run filter:api.user.me.subscription-videos.list.params', async function () { + const { data } = await servers[0].videos.listMySubscriptionVideos({ start: 0, count: 2 }) + + // 1 plugin do +1 to the count parameter + expect(data).to.have.lengthOf(3) + }) + + it('Should run filter:api.user.me.subscription-videos.list.result', async function () { + const { total } = await servers[0].videos.listMySubscriptionVideos({ start: 0, count: 2 }) + + // Plugin do +4 to the total result + expect(total).to.equal(14) + }) + + it('Should run filter:api.video.get.result', async function () { + const video = await servers[0].videos.get({ id: videoUUID }) + expect(video.name).to.contain('<3') + }) + }) + + describe('Video/live/import accept', function () { + + it('Should run filter:api.video.upload.accept.result', async function () { + const options = { attributes: { name: 'video with bad word' }, expectedStatus: HttpStatusCode.FORBIDDEN_403 } + await servers[0].videos.upload({ mode: 'legacy', ...options }) + await servers[0].videos.upload({ mode: 'resumable', ...options }) + }) + + it('Should run filter:api.video.update-file.accept.result', async function () { + const res = await servers[0].videos.replaceSourceFile({ + videoId: videoUUID, + fixture: 'video_short1.webm', + completedExpectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + + expect((res as any)?.error).to.equal('no webm') + }) + + it('Should run filter:api.live-video.create.accept.result', async function () { + const attributes = { + name: 'video with bad word', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id + } + + await servers[0].live.create({ fields: attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should run filter:api.video.pre-import-url.accept.result', async function () { + const attributes = { + name: 'normal title', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id, + targetUrl: FIXTURE_URLS.goodVideo + 'bad' + } + await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should run filter:api.video.pre-import-torrent.accept.result', async function () { + const attributes = { + name: 'bad torrent', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id, + torrentfile: 'video-720p.torrent' as any + } + await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should run filter:api.video.post-import-url.accept.result', async function () { + this.timeout(60000) + + let videoImportId: number + + { + const attributes = { + name: 'title with bad word', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id, + targetUrl: FIXTURE_URLS.goodVideo + } + const body = await servers[0].imports.importVideo({ attributes }) + videoImportId = body.id + } + + await waitJobs(servers) + + { + const body = await servers[0].imports.getMyVideoImports() + const videoImports = body.data + + const videoImport = videoImports.find(i => i.id === videoImportId) + + expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) + expect(videoImport.state.label).to.equal('Rejected') + } + }) + + it('Should run filter:api.video.post-import-torrent.accept.result', async function () { + this.timeout(60000) + + let videoImportId: number + + { + const attributes = { + name: 'title with bad word', + privacy: VideoPrivacy.PUBLIC, + channelId: servers[0].store.channel.id, + torrentfile: 'video-720p.torrent' as any + } + const body = await servers[0].imports.importVideo({ attributes }) + videoImportId = body.id + } + + await waitJobs(servers) + + { + const { data: videoImports } = await servers[0].imports.getMyVideoImports() + + const videoImport = videoImports.find(i => i.id === videoImportId) + + expect(videoImport.state.id).to.equal(VideoImportState.REJECTED) + expect(videoImport.state.label).to.equal('Rejected') + } + }) + }) + + describe('Video comments accept', function () { + + it('Should run filter:api.video-thread.create.accept.result', async function () { + await servers[0].comments.createThread({ + videoId: videoUUID, + text: 'comment with bad word', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should run filter:api.video-comment-reply.create.accept.result', async function () { + const created = await servers[0].comments.createThread({ videoId: videoUUID, text: 'thread' }) + threadId = created.id + + await servers[0].comments.addReply({ + videoId: videoUUID, + toCommentId: threadId, + text: 'comment with bad word', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + await servers[0].comments.addReply({ + videoId: videoUUID, + toCommentId: threadId, + text: 'comment with good word', + expectedStatus: HttpStatusCode.OK_200 + }) + }) + + it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a thread creation', async function () { + this.timeout(30000) + + await servers[1].comments.createThread({ videoId: videoUUID, text: 'comment with bad word' }) + + await waitJobs(servers) + + { + const thread = await servers[0].comments.listThreads({ videoId: videoUUID }) + expect(thread.data).to.have.lengthOf(1) + expect(thread.data[0].text).to.not.include(' bad ') + } + + { + const thread = await servers[1].comments.listThreads({ videoId: videoUUID }) + expect(thread.data).to.have.lengthOf(2) + } + }) + + it('Should run filter:activity-pub.remote-video-comment.create.accept.result on a reply creation', async function () { + this.timeout(30000) + + const { data } = await servers[1].comments.listThreads({ videoId: videoUUID }) + const threadIdServer2 = data.find(t => t.text === 'thread').id + + await servers[1].comments.addReply({ + videoId: videoUUID, + toCommentId: threadIdServer2, + text: 'comment with bad word' + }) + + await waitJobs(servers) + + { + const tree = await servers[0].comments.getThread({ videoId: videoUUID, threadId }) + expect(tree.children).to.have.lengthOf(1) + expect(tree.children[0].comment.text).to.not.include(' bad ') + } + + { + const tree = await servers[1].comments.getThread({ videoId: videoUUID, threadId: threadIdServer2 }) + expect(tree.children).to.have.lengthOf(2) + } + }) + }) + + describe('Video comments', function () { + + it('Should run filter:api.video-threads.list.params', async function () { + const { data } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) + + // our plugin do +1 to the count parameter + expect(data).to.have.lengthOf(1) + }) + + it('Should run filter:api.video-threads.list.result', async function () { + const { total } = await servers[0].comments.listThreads({ videoId: videoUUID, start: 0, count: 0 }) + + // Plugin do +1 to the total result + expect(total).to.equal(2) + }) + + it('Should run filter:api.video-thread-comments.list.params') + + it('Should run filter:api.video-thread-comments.list.result', async function () { + const thread = await servers[0].comments.getThread({ videoId: videoUUID, threadId }) + + expect(thread.comment.text.endsWith(' <3')).to.be.true + }) + + it('Should run filter:api.overviews.videos.list.{params,result}', async function () { + await servers[0].overviews.getVideos({ page: 1 }) + + // 3 because we get 3 samples per page + await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.params', 3) + await servers[0].servers.waitUntilLog('Run hook filter:api.overviews.videos.list.result', 3) + }) + }) + + describe('filter:video.auto-blacklist.result', function () { + + async function checkIsBlacklisted (id: number | string, value: boolean) { + const video = await servers[0].videos.getWithToken({ id }) + expect(video.blacklisted).to.equal(value) + } + + it('Should blacklist on upload', async function () { + const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video please blacklist me' } }) + await checkIsBlacklisted(uuid, true) + }) + + it('Should blacklist on import', async function () { + this.timeout(15000) + + const attributes = { + name: 'video please blacklist me', + targetUrl: FIXTURE_URLS.goodVideo, + channelId: servers[0].store.channel.id + } + const body = await servers[0].imports.importVideo({ attributes }) + await checkIsBlacklisted(body.video.uuid, true) + }) + + it('Should blacklist on update', async function () { + const { uuid } = await servers[0].videos.upload({ attributes: { name: 'video' } }) + await checkIsBlacklisted(uuid, false) + + await servers[0].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } }) + await checkIsBlacklisted(uuid, true) + }) + + it('Should blacklist on remote upload', async function () { + this.timeout(120000) + + const { uuid } = await servers[1].videos.upload({ attributes: { name: 'remote please blacklist me' } }) + await waitJobs(servers) + + await checkIsBlacklisted(uuid, true) + }) + + it('Should blacklist on remote update', async function () { + this.timeout(120000) + + const { uuid } = await servers[1].videos.upload({ attributes: { name: 'video' } }) + await waitJobs(servers) + + await checkIsBlacklisted(uuid, false) + + await servers[1].videos.update({ id: uuid, attributes: { name: 'please blacklist me' } }) + await waitJobs(servers) + + await checkIsBlacklisted(uuid, true) + }) + }) + + describe('Should run filter:api.user.signup.allowed.result', function () { + + before(async function () { + await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: false } } }) + }) + + it('Should run on config endpoint', async function () { + const body = await servers[0].config.getConfig() + expect(body.signup.allowed).to.be.true + }) + + it('Should allow a signup', async function () { + await servers[0].registrations.register({ username: 'john1' }) + }) + + it('Should not allow a signup', async function () { + const res = await servers[0].registrations.register({ + username: 'jma 1', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + + expect(res.body.error).to.equal('No jma 1') + }) + }) + + describe('Should run filter:api.user.request-signup.allowed.result', function () { + + before(async function () { + await servers[0].config.updateExistingSubConfig({ newConfig: { signup: { requiresApproval: true } } }) + }) + + it('Should run on config endpoint', async function () { + const body = await servers[0].config.getConfig() + expect(body.signup.allowed).to.be.true + }) + + it('Should allow a signup request', async function () { + await servers[0].registrations.requestRegistration({ username: 'john2', registrationReason: 'tt' }) + }) + + it('Should not allow a signup request', async function () { + const body = await servers[0].registrations.requestRegistration({ + username: 'jma 2', + registrationReason: 'tt', + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + + expect((body as unknown as PeerTubeProblemDocument).error).to.equal('No jma 2') + }) + }) + + describe('Download hooks', function () { + const downloadVideos: VideoDetails[] = [] + let downloadVideo2Token: string + + before(async function () { + this.timeout(120000) + + await servers[0].config.updateCustomSubConfig({ + newConfig: { + transcoding: { + webVideos: { + enabled: true + }, + hls: { + enabled: true + } + } + } + }) + + const uuids: string[] = [] + + for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) { + const uuid = (await servers[0].videos.quickUpload({ name })).uuid + uuids.push(uuid) + } + + await waitJobs(servers) + + for (const uuid of uuids) { + downloadVideos.push(await servers[0].videos.get({ id: uuid })) + } + + downloadVideo2Token = await servers[0].videoToken.getVideoFileToken({ videoId: downloadVideos[2].uuid }) + }) + + it('Should run filter:api.download.torrent.allowed.result', async function () { + const res = await makeRawRequest({ url: downloadVideos[0].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + expect(res.body.error).to.equal('Liu Bei') + + await makeRawRequest({ url: downloadVideos[1].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) + await makeRawRequest({ url: downloadVideos[2].files[0].torrentDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) + }) + + it('Should run filter:api.download.video.allowed.result', async function () { + { + const refused = downloadVideos[1].files[0].fileDownloadUrl + const allowed = [ + downloadVideos[0].files[0].fileDownloadUrl, + downloadVideos[2].files[0].fileDownloadUrl + ] + + const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + expect(res.body.error).to.equal('Cao Cao') + + for (const url of allowed) { + await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) + await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) + } + } + + { + const refused = downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl + + const allowed = [ + downloadVideos[2].files[0].fileDownloadUrl, + downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, + downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl + ] + + // Only streaming playlist is refuse + const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + expect(res.body.error).to.equal('Sun Jian') + + // But not we there is a user in res + await makeRawRequest({ url: refused, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 }) + await makeRawRequest({ url: refused, query: { videoFileToken: downloadVideo2Token }, expectedStatus: HttpStatusCode.OK_200 }) + + // Other files work + for (const url of allowed) { + await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) + } + } + }) + }) + + describe('Embed filters', function () { + const embedVideos: VideoDetails[] = [] + const embedPlaylists: VideoPlaylist[] = [] + + before(async function () { + this.timeout(60000) + + await servers[0].config.disableTranscoding() + + for (const name of [ 'bad embed', 'good embed' ]) { + { + const uuid = (await servers[0].videos.quickUpload({ name })).uuid + embedVideos.push(await servers[0].videos.get({ id: uuid })) + } + + { + const attributes = { displayName: name, videoChannelId: servers[0].store.channel.id, privacy: VideoPlaylistPrivacy.PUBLIC } + const { id } = await servers[0].playlists.create({ attributes }) + + const playlist = await servers[0].playlists.get({ playlistId: id }) + embedPlaylists.push(playlist) + } + } + }) + + it('Should run filter:html.embed.video.allowed.result', async function () { + const res = await makeGetRequest({ url: servers[0].url, path: embedVideos[0].embedPath, expectedStatus: HttpStatusCode.OK_200 }) + expect(res.text).to.equal('Lu Bu') + }) + + it('Should run filter:html.embed.video-playlist.allowed.result', async function () { + const res = await makeGetRequest({ url: servers[0].url, path: embedPlaylists[0].embedPath, expectedStatus: HttpStatusCode.OK_200 }) + expect(res.text).to.equal('Diao Chan') + }) + }) + + describe('Client HTML filters', function () { + let videoUUID: string + + before(async function () { + this.timeout(60000) + + const { uuid } = await servers[0].videos.quickUpload({ name: 'html video' }) + videoUUID = uuid + }) + + it('Should run filter:html.client.json-ld.result', async function () { + const res = await makeGetRequest({ url: servers[0].url, path: '/w/' + videoUUID, expectedStatus: HttpStatusCode.OK_200 }) + expect(res.text).to.contain('"recordedAt":"http://example.com/recordedAt"') + }) + + it('Should not run filter:html.client.json-ld.result with an account', async function () { + const res = await makeGetRequest({ url: servers[0].url, path: '/a/root', expectedStatus: HttpStatusCode.OK_200 }) + expect(res.text).not.to.contain('"recordedAt":"http://example.com/recordedAt"') + }) + }) + + describe('Search filters', function () { + + before(async function () { + await servers[0].config.updateCustomSubConfig({ + newConfig: { + search: { + searchIndex: { + enabled: true, + isDefaultSearch: false, + disableLocalSearch: false + } + } + } + }) + }) + + it('Should run filter:api.search.videos.local.list.{params,result}', async function () { + await servers[0].search.advancedVideoSearch({ + search: { + search: 'Sun Quan' + } + }) + + await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1) + }) + + it('Should run filter:api.search.videos.index.list.{params,result}', async function () { + await servers[0].search.advancedVideoSearch({ + search: { + search: 'Sun Quan', + searchTarget: 'search-index' + } + }) + + await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.local.list.result', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.videos.index.list.result', 1) + }) + + it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () { + await servers[0].search.advancedChannelSearch({ + search: { + search: 'Sun Ce' + } + }) + + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1) + }) + + it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () { + await servers[0].search.advancedChannelSearch({ + search: { + search: 'Sun Ce', + searchTarget: 'search-index' + } + }) + + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.local.list.result', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-channels.index.list.result', 1) + }) + + it('Should run filter:api.search.video-playlists.local.list.{params,result}', async function () { + await servers[0].search.advancedPlaylistSearch({ + search: { + search: 'Sun Jian' + } + }) + + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1) + }) + + it('Should run filter:api.search.video-playlists.index.list.{params,result}', async function () { + await servers[0].search.advancedPlaylistSearch({ + search: { + search: 'Sun Jian', + searchTarget: 'search-index' + } + }) + + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.local.list.result', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.params', 1) + await servers[0].servers.waitUntilLog('Run hook filter:api.search.video-playlists.index.list.result', 1) + }) + }) + + describe('Upload/import/live attributes filters', function () { + + before(async function () { + await servers[0].config.enableLive({ transcoding: false, allowReplay: false }) + await servers[0].config.enableImports() + await servers[0].config.disableTranscoding() + }) + + it('Should run filter:api.video.upload.video-attribute.result', async function () { + for (const mode of [ 'legacy' as 'legacy', 'resumable' as 'resumable' ]) { + const { id } = await servers[0].videos.upload({ attributes: { name: 'video', description: 'upload' }, mode }) + + const video = await servers[0].videos.get({ id }) + expect(video.description).to.equal('upload - filter:api.video.upload.video-attribute.result') + } + }) + + it('Should run filter:api.video.import-url.video-attribute.result', async function () { + const attributes = { + name: 'video', + description: 'import url', + channelId: servers[0].store.channel.id, + targetUrl: FIXTURE_URLS.goodVideo, + privacy: VideoPrivacy.PUBLIC + } + const { video: { id } } = await servers[0].imports.importVideo({ attributes }) + + const video = await servers[0].videos.get({ id }) + expect(video.description).to.equal('import url - filter:api.video.import-url.video-attribute.result') + }) + + it('Should run filter:api.video.import-torrent.video-attribute.result', async function () { + const attributes = { + name: 'video', + description: 'import torrent', + channelId: servers[0].store.channel.id, + magnetUri: FIXTURE_URLS.magnet, + privacy: VideoPrivacy.PUBLIC + } + const { video: { id } } = await servers[0].imports.importVideo({ attributes }) + + const video = await servers[0].videos.get({ id }) + expect(video.description).to.equal('import torrent - filter:api.video.import-torrent.video-attribute.result') + }) + + it('Should run filter:api.video.live.video-attribute.result', async function () { + const fields = { + name: 'live', + description: 'live', + channelId: servers[0].store.channel.id, + privacy: VideoPrivacy.PUBLIC + } + const { id } = await servers[0].live.create({ fields }) + + const video = await servers[0].videos.get({ id }) + expect(video.description).to.equal('live - filter:api.video.live.video-attribute.result') + }) + }) + + describe('Stats filters', function () { + + it('Should run filter:api.server.stats.get.result', async function () { + const data = await servers[0].stats.get() + + expect((data as any).customStats).to.equal(14) + }) + + }) + + describe('Job queue filters', function () { + let videoUUID: string + + before(async function () { + this.timeout(120_000) + + await servers[0].config.enableMinimumTranscoding() + const { uuid } = await servers[0].videos.quickUpload({ name: 'studio' }) + + const video = await servers[0].videos.get({ id: uuid }) + expect(video.duration).at.least(2) + videoUUID = video.uuid + + await waitJobs(servers) + + await servers[0].config.enableStudio() + }) + + it('Should run filter:job-queue.process.params', async function () { + this.timeout(120_000) + + await servers[0].videoStudio.createEditionTasks({ + videoId: videoUUID, + tasks: [ + { + name: 'add-intro', + options: { + file: 'video_very_short_240p.mp4' + } + } + ] + }) + + await waitJobs(servers) + + await servers[0].servers.waitUntilLog('Run hook filter:job-queue.process.params', 1, false) + + const video = await servers[0].videos.get({ id: videoUUID }) + expect(video.duration).at.most(2) + }) + + it('Should run filter:job-queue.process.result', async function () { + await servers[0].servers.waitUntilLog('Run hook filter:job-queue.process.result', 1, false) + }) + }) + + describe('Transcoding filters', async function () { + + it('Should run filter:transcoding.auto.resolutions-to-transcode.result', async function () { + const { uuid } = await servers[0].videos.quickUpload({ name: 'transcode-filter' }) + + await waitJobs(servers) + + const video = await servers[0].videos.get({ id: uuid }) + expect(video.files).to.have.lengthOf(2) + expect(video.files.find(f => f.resolution.id === 100 as any)).to.exist + }) + }) + + describe('Video channel filters', async function () { + + it('Should run filter:api.video-channels.list.params', async function () { + const { data } = await servers[0].channels.list({ start: 0, count: 0 }) + + // plugin do +1 to the count parameter + expect(data).to.have.lengthOf(1) + }) + + it('Should run filter:api.video-channels.list.result', async function () { + const { total } = await servers[0].channels.list({ start: 0, count: 1 }) + + // plugin do +1 to the total parameter + expect(total).to.equal(4) + }) + + it('Should run filter:api.video-channel.get.result', async function () { + const channel = await servers[0].channels.get({ channelName: 'root_channel' }) + expect(channel.displayName).to.equal('Main root channel <3') + }) + }) + + describe('Activity Pub', function () { + + it('Should run filter:activity-pub.activity.context.build.result', async function () { + const { body } = await makeActivityPubGetRequest(servers[0].url, '/w/' + videoUUID) + expect(body.type).to.equal('Video') + + expect(body['@context'].some(c => { + return typeof c === 'object' && c.recordedAt === 'https://schema.org/recordedAt' + })).to.be.true + }) + + it('Should run filter:activity-pub.video.json-ld.build.result', async function () { + const { body } = await makeActivityPubGetRequest(servers[0].url, '/w/' + videoUUID) + expect(body.name).to.equal('default video 0') + expect(body.videoName).to.equal('default video 0') + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/plugins/html-injection.ts b/packages/tests/src/plugins/html-injection.ts new file mode 100644 index 000000000..269a45b98 --- /dev/null +++ b/packages/tests/src/plugins/html-injection.ts @@ -0,0 +1,73 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { + cleanupTests, + createSingleServer, + makeHTMLRequest, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test plugins HTML injection', function () { + let server: PeerTubeServer = null + let command: PluginsCommand + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + command = server.plugins + }) + + it('Should not inject global css file in HTML', async function () { + { + const text = await command.getCSS() + expect(text).to.be.empty + } + + for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) { + const res = await makeHTMLRequest(server.url, path) + expect(res.text).to.not.include('link rel="stylesheet" href="/plugins/global.css') + } + }) + + it('Should install a plugin and a theme', async function () { + this.timeout(30000) + + await command.install({ npmName: 'peertube-plugin-hello-world' }) + }) + + it('Should have the correct global css', async function () { + { + const text = await command.getCSS() + expect(text).to.contain('background-color: red') + } + + for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) { + const res = await makeHTMLRequest(server.url, path) + expect(res.text).to.include('link rel="stylesheet" href="/plugins/global.css') + } + }) + + it('Should have an empty global css on uninstall', async function () { + await command.uninstall({ npmName: 'peertube-plugin-hello-world' }) + + { + const text = await command.getCSS() + expect(text).to.be.empty + } + + for (const path of [ '/', '/videos/embed/1', '/video-playlists/embed/1' ]) { + const res = await makeHTMLRequest(server.url, path) + expect(res.text).to.not.include('link rel="stylesheet" href="/plugins/global.css') + } + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/id-and-pass-auth.ts b/packages/tests/src/plugins/id-and-pass-auth.ts new file mode 100644 index 000000000..a332f0eec --- /dev/null +++ b/packages/tests/src/plugins/id-and-pass-auth.ts @@ -0,0 +1,248 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { wait } from '@peertube/peertube-core-utils' +import { HttpStatusCode, UserRole } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test id and pass auth plugins', function () { + let server: PeerTubeServer + + let crashAccessToken: string + let crashRefreshToken: string + + let lagunaAccessToken: string + let lagunaRefreshToken: string + let lagunaId: number + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + for (const suffix of [ 'one', 'two', 'three' ]) { + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-id-pass-auth-' + suffix) }) + } + }) + + it('Should display the correct configuration', async function () { + const config = await server.config.getConfig() + + const auths = config.plugin.registeredIdAndPassAuths + expect(auths).to.have.lengthOf(8) + + const crashAuth = auths.find(a => a.authName === 'crash-auth') + expect(crashAuth).to.exist + expect(crashAuth.npmName).to.equal('peertube-plugin-test-id-pass-auth-one') + expect(crashAuth.weight).to.equal(50) + }) + + it('Should not login', async function () { + await server.login.login({ user: { username: 'toto', password: 'password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should login Spyro, create the user and use the token', async function () { + const accessToken = await server.login.getAccessToken({ username: 'spyro', password: 'spyro password' }) + + const body = await server.users.getMyInfo({ token: accessToken }) + + expect(body.username).to.equal('spyro') + expect(body.account.displayName).to.equal('Spyro the Dragon') + expect(body.role.id).to.equal(UserRole.USER) + }) + + it('Should login Crash, create the user and use the token', async function () { + { + const body = await server.login.login({ user: { username: 'crash', password: 'crash password' } }) + crashAccessToken = body.access_token + crashRefreshToken = body.refresh_token + } + + { + const body = await server.users.getMyInfo({ token: crashAccessToken }) + + expect(body.username).to.equal('crash') + expect(body.account.displayName).to.equal('Crash Bandicoot') + expect(body.role.id).to.equal(UserRole.MODERATOR) + } + }) + + it('Should login the first Laguna, create the user and use the token', async function () { + { + const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } }) + lagunaAccessToken = body.access_token + lagunaRefreshToken = body.refresh_token + } + + { + const body = await server.users.getMyInfo({ token: lagunaAccessToken }) + + expect(body.username).to.equal('laguna') + expect(body.account.displayName).to.equal('Laguna Loire') + expect(body.role.id).to.equal(UserRole.USER) + + lagunaId = body.id + } + }) + + it('Should refresh crash token, but not laguna token', async function () { + { + const resRefresh = await server.login.refreshToken({ refreshToken: crashRefreshToken }) + crashAccessToken = resRefresh.body.access_token + crashRefreshToken = resRefresh.body.refresh_token + + const body = await server.users.getMyInfo({ token: crashAccessToken }) + expect(body.username).to.equal('crash') + } + + { + await server.login.refreshToken({ refreshToken: lagunaRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + } + }) + + it('Should update Crash profile', async function () { + await server.users.updateMe({ + token: crashAccessToken, + displayName: 'Beautiful Crash', + description: 'Mutant eastern barred bandicoot' + }) + + const body = await server.users.getMyInfo({ token: crashAccessToken }) + + expect(body.account.displayName).to.equal('Beautiful Crash') + expect(body.account.description).to.equal('Mutant eastern barred bandicoot') + }) + + it('Should logout Crash', async function () { + await server.login.logout({ token: crashAccessToken }) + }) + + it('Should have logged out Crash', async function () { + await server.servers.waitUntilLog('On logout for auth 1 - 2') + + await server.users.getMyInfo({ token: crashAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should login Crash and keep the old existing profile', async function () { + crashAccessToken = await server.login.getAccessToken({ username: 'crash', password: 'crash password' }) + + const body = await server.users.getMyInfo({ token: crashAccessToken }) + + expect(body.username).to.equal('crash') + expect(body.account.displayName).to.equal('Beautiful Crash') + expect(body.account.description).to.equal('Mutant eastern barred bandicoot') + expect(body.role.id).to.equal(UserRole.MODERATOR) + }) + + it('Should login Laguna and update the profile', async function () { + { + await server.users.update({ userId: lagunaId, videoQuota: 43000, videoQuotaDaily: 43100 }) + await server.users.updateMe({ token: lagunaAccessToken, displayName: 'laguna updated' }) + + const body = await server.users.getMyInfo({ token: lagunaAccessToken }) + expect(body.username).to.equal('laguna') + expect(body.account.displayName).to.equal('laguna updated') + expect(body.videoQuota).to.equal(43000) + expect(body.videoQuotaDaily).to.equal(43100) + } + + { + const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } }) + lagunaAccessToken = body.access_token + lagunaRefreshToken = body.refresh_token + } + + { + const body = await server.users.getMyInfo({ token: lagunaAccessToken }) + expect(body.username).to.equal('laguna') + expect(body.account.displayName).to.equal('Laguna Loire') + expect(body.videoQuota).to.equal(42000) + expect(body.videoQuotaDaily).to.equal(43100) + } + }) + + it('Should reject token of laguna by the plugin hook', async function () { + await wait(5000) + + await server.users.getMyInfo({ token: lagunaAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should reject an invalid username, email, role or display name', async function () { + const command = server.login + + await command.login({ user: { username: 'ward', password: 'ward password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.servers.waitUntilLog('valid username') + + await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.servers.waitUntilLog('valid displayName') + + await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.servers.waitUntilLog('valid role') + + await command.login({ user: { username: 'ellone', password: 'elonne password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.servers.waitUntilLog('valid email') + }) + + it('Should unregister spyro-auth and do not login existing Spyro', async function () { + await server.plugins.updateSettings({ + npmName: 'peertube-plugin-test-id-pass-auth-one', + settings: { disableSpyro: true } + }) + + const command = server.login + await command.login({ user: { username: 'spyro', password: 'spyro password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await command.login({ user: { username: 'spyro', password: 'fake' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should have disabled this auth', async function () { + const config = await server.config.getConfig() + + const auths = config.plugin.registeredIdAndPassAuths + expect(auths).to.have.lengthOf(7) + + const spyroAuth = auths.find(a => a.authName === 'spyro-auth') + expect(spyroAuth).to.not.exist + }) + + it('Should uninstall the plugin one and do not login existing Crash', async function () { + await server.plugins.uninstall({ npmName: 'peertube-plugin-test-id-pass-auth-one' }) + + await server.login.login({ + user: { username: 'crash', password: 'crash password' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should display the correct configuration', async function () { + const config = await server.config.getConfig() + + const auths = config.plugin.registeredIdAndPassAuths + expect(auths).to.have.lengthOf(6) + + const crashAuth = auths.find(a => a.authName === 'crash-auth') + expect(crashAuth).to.not.exist + }) + + it('Should display plugin auth information in users list', async function () { + const { data } = await server.users.list() + + const root = data.find(u => u.username === 'root') + const crash = data.find(u => u.username === 'crash') + const laguna = data.find(u => u.username === 'laguna') + + expect(root.pluginAuth).to.be.null + expect(crash.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-one') + expect(laguna.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-two') + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/index.ts b/packages/tests/src/plugins/index.ts new file mode 100644 index 000000000..210af7236 --- /dev/null +++ b/packages/tests/src/plugins/index.ts @@ -0,0 +1,13 @@ +import './action-hooks' +import './external-auth' +import './filter-hooks' +import './html-injection' +import './id-and-pass-auth' +import './plugin-helpers' +import './plugin-router' +import './plugin-storage' +import './plugin-transcoding' +import './plugin-unloading' +import './plugin-websocket' +import './translations' +import './video-constants' diff --git a/packages/tests/src/plugins/plugin-helpers.ts b/packages/tests/src/plugins/plugin-helpers.ts new file mode 100644 index 000000000..d2bd8596e --- /dev/null +++ b/packages/tests/src/plugins/plugin-helpers.ts @@ -0,0 +1,383 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { pathExists } from 'fs-extra/esm' +import { HttpStatusCode, ThumbnailType } from '@peertube/peertube-models' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + makeGetRequest, + makePostBodyRequest, + makeRawRequest, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' +import { checkVideoFilesWereRemoved } from '@tests/shared/videos.js' + +function postCommand (server: PeerTubeServer, command: string, bodyArg?: object) { + const body = { command } + if (bodyArg) Object.assign(body, bodyArg) + + return makePostBodyRequest({ + url: server.url, + path: '/plugins/test-four/router/commander', + fields: body, + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) +} + +describe('Test plugin helpers', function () { + let servers: PeerTubeServer[] + + before(async function () { + this.timeout(60000) + + servers = await createMultipleServers(2) + await setAccessTokensToServers(servers) + + await doubleFollow(servers[0], servers[1]) + + await servers[0].plugins.install({ path: PluginsCommand.getPluginTestPath('-four') }) + }) + + describe('Logger', function () { + + it('Should have logged things', async function () { + await servers[0].servers.waitUntilLog(servers[0].host + ' peertube-plugin-test-four', 1, false) + await servers[0].servers.waitUntilLog('Hello world from plugin four', 1) + }) + }) + + describe('Database', function () { + + it('Should have made a query', async function () { + await servers[0].servers.waitUntilLog(`root email is admin${servers[0].internalServerNumber}@example.com`) + }) + }) + + describe('Config', function () { + + it('Should have the correct webserver url', async function () { + await servers[0].servers.waitUntilLog(`server url is ${servers[0].url}`) + }) + + it('Should have the correct listening config', async function () { + const res = await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/server-listening-config', + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.config).to.exist + expect(res.body.config.hostname).to.equal('::') + expect(res.body.config.port).to.equal(servers[0].port) + }) + + it('Should have the correct config', async function () { + const res = await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/server-config', + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.serverConfig).to.exist + expect(res.body.serverConfig.instance.name).to.equal('PeerTube') + }) + }) + + describe('Server', function () { + + it('Should get the server actor', async function () { + await servers[0].servers.waitUntilLog('server actor name is peertube') + }) + }) + + describe('Socket', function () { + + it('Should sendNotification without any exceptions', async () => { + const user = await servers[0].users.create({ username: 'notis_redding', password: 'secret1234?' }) + await makePostBodyRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/send-notification', + fields: { + userId: user.id + }, + expectedStatus: HttpStatusCode.CREATED_201 + }) + }) + + it('Should sendVideoLiveNewState without any exceptions', async () => { + const res = await servers[0].videos.quickUpload({ name: 'video server 1' }) + + await makePostBodyRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/send-video-live-new-state/' + res.uuid, + expectedStatus: HttpStatusCode.CREATED_201 + }) + + await servers[0].videos.remove({ id: res.uuid }) + }) + }) + + describe('Plugin', function () { + + it('Should get the base static route', async function () { + const res = await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/static-route', + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.staticRoute).to.equal('/plugins/test-four/0.0.1/static/') + }) + + it('Should get the base static route', async function () { + const baseRouter = '/plugins/test-four/0.0.1/router/' + + const res = await makeGetRequest({ + url: servers[0].url, + path: baseRouter + 'router-route', + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.routerRoute).to.equal(baseRouter) + }) + }) + + describe('User', function () { + let rootId: number + + it('Should not get a user if not authenticated', async function () { + await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/user', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should get a user if authenticated', async function () { + const res = await makeGetRequest({ + url: servers[0].url, + token: servers[0].accessToken, + path: '/plugins/test-four/router/user', + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.username).to.equal('root') + expect(res.body.displayName).to.equal('root') + expect(res.body.isAdmin).to.be.true + expect(res.body.isModerator).to.be.false + expect(res.body.isUser).to.be.false + + rootId = res.body.id + }) + + it('Should load a user by id', async function () { + { + const res = await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/user/' + rootId, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.username).to.equal('root') + } + + { + await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/user/42', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + } + }) + }) + + describe('Moderation', function () { + let videoUUIDServer1: string + + before(async function () { + this.timeout(60000) + + { + const res = await servers[0].videos.quickUpload({ name: 'video server 1' }) + videoUUIDServer1 = res.uuid + } + + { + await servers[1].videos.quickUpload({ name: 'video server 2' }) + } + + await waitJobs(servers) + + const { data } = await servers[0].videos.list() + + expect(data).to.have.lengthOf(2) + }) + + it('Should mute server 2', async function () { + await postCommand(servers[0], 'blockServer', { hostToBlock: servers[1].host }) + + const { data } = await servers[0].videos.list() + + expect(data).to.have.lengthOf(1) + expect(data[0].name).to.equal('video server 1') + }) + + it('Should unmute server 2', async function () { + await postCommand(servers[0], 'unblockServer', { hostToUnblock: servers[1].host }) + + const { data } = await servers[0].videos.list() + + expect(data).to.have.lengthOf(2) + }) + + it('Should mute account of server 2', async function () { + await postCommand(servers[0], 'blockAccount', { handleToBlock: `root@${servers[1].host}` }) + + const { data } = await servers[0].videos.list() + + expect(data).to.have.lengthOf(1) + expect(data[0].name).to.equal('video server 1') + }) + + it('Should unmute account of server 2', async function () { + await postCommand(servers[0], 'unblockAccount', { handleToUnblock: `root@${servers[1].host}` }) + + const { data } = await servers[0].videos.list() + + expect(data).to.have.lengthOf(2) + }) + + it('Should blacklist video', async function () { + await postCommand(servers[0], 'blacklist', { videoUUID: videoUUIDServer1, unfederate: true }) + + await waitJobs(servers) + + for (const server of servers) { + const { data } = await server.videos.list() + + expect(data).to.have.lengthOf(1) + expect(data[0].name).to.equal('video server 2') + } + }) + + it('Should unblacklist video', async function () { + await postCommand(servers[0], 'unblacklist', { videoUUID: videoUUIDServer1 }) + + await waitJobs(servers) + + for (const server of servers) { + const { data } = await server.videos.list() + + expect(data).to.have.lengthOf(2) + } + }) + }) + + describe('Videos', function () { + let videoUUID: string + let videoPath: string + + before(async function () { + this.timeout(240000) + + await servers[0].config.enableTranscoding() + + const res = await servers[0].videos.quickUpload({ name: 'video1' }) + videoUUID = res.uuid + + await waitJobs(servers) + }) + + it('Should get video files', async function () { + const { body } = await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/video-files/' + videoUUID, + expectedStatus: HttpStatusCode.OK_200 + }) + + // Video files check + { + expect(body.webVideo.videoFiles).to.be.an('array') + expect(body.hls.videoFiles).to.be.an('array') + + for (const resolution of [ 144, 240, 360, 480, 720 ]) { + for (const files of [ body.webVideo.videoFiles, body.hls.videoFiles ]) { + const file = files.find(f => f.resolution === resolution) + expect(file).to.exist + + expect(file.size).to.be.a('number') + expect(file.fps).to.equal(25) + + expect(await pathExists(file.path)).to.be.true + await makeRawRequest({ url: file.url, expectedStatus: HttpStatusCode.OK_200 }) + } + } + + videoPath = body.webVideo.videoFiles[0].path + } + + // Thumbnails check + { + expect(body.thumbnails).to.be.an('array') + + const miniature = body.thumbnails.find(t => t.type === ThumbnailType.MINIATURE) + expect(miniature).to.exist + expect(await pathExists(miniature.path)).to.be.true + await makeRawRequest({ url: miniature.url, expectedStatus: HttpStatusCode.OK_200 }) + + const preview = body.thumbnails.find(t => t.type === ThumbnailType.PREVIEW) + expect(preview).to.exist + expect(await pathExists(preview.path)).to.be.true + await makeRawRequest({ url: preview.url, expectedStatus: HttpStatusCode.OK_200 }) + } + }) + + it('Should probe a file', async function () { + const { body } = await makeGetRequest({ + url: servers[0].url, + path: '/plugins/test-four/router/ffprobe', + query: { + path: videoPath + }, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(body.streams).to.be.an('array') + expect(body.streams).to.have.lengthOf(2) + }) + + it('Should remove a video after a view', async function () { + this.timeout(40000) + + // Should not throw -> video exists + const video = await servers[0].videos.get({ id: videoUUID }) + // Should delete the video + await servers[0].views.simulateView({ id: videoUUID }) + + await servers[0].servers.waitUntilLog('Video deleted by plugin four.') + + try { + // Should throw because the video should have been deleted + await servers[0].videos.get({ id: videoUUID }) + throw new Error('Video exists') + } catch (err) { + if (err.message.includes('exists')) throw err + } + + await checkVideoFilesWereRemoved({ server: servers[0], video }) + }) + + it('Should have fetched the video by URL', async function () { + await servers[0].servers.waitUntilLog(`video from DB uuid is ${videoUUID}`) + }) + }) + + after(async function () { + await cleanupTests(servers) + }) +}) diff --git a/packages/tests/src/plugins/plugin-router.ts b/packages/tests/src/plugins/plugin-router.ts new file mode 100644 index 000000000..6f3571c05 --- /dev/null +++ b/packages/tests/src/plugins/plugin-router.ts @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makePostBodyRequest, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' +import { HttpStatusCode } from '@peertube/peertube-models' + +describe('Test plugin helpers', function () { + let server: PeerTubeServer + const basePaths = [ + '/plugins/test-five/router/', + '/plugins/test-five/0.0.1/router/' + ] + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-five') }) + }) + + it('Should answer "pong"', async function () { + for (const path of basePaths) { + const res = await makeGetRequest({ + url: server.url, + path: path + 'ping', + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.message).to.equal('pong') + } + }) + + it('Should check if authenticated', async function () { + for (const path of basePaths) { + const res = await makeGetRequest({ + url: server.url, + path: path + 'is-authenticated', + token: server.accessToken, + expectedStatus: 200 + }) + + expect(res.body.isAuthenticated).to.equal(true) + + const secRes = await makeGetRequest({ + url: server.url, + path: path + 'is-authenticated', + expectedStatus: 200 + }) + + expect(secRes.body.isAuthenticated).to.equal(false) + } + }) + + it('Should mirror post body', async function () { + const body = { + hello: 'world', + riri: 'fifi', + loulou: 'picsou' + } + + for (const path of basePaths) { + const res = await makePostBodyRequest({ + url: server.url, + path: path + 'form/post/mirror', + fields: body, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body).to.deep.equal(body) + } + }) + + it('Should remove the plugin and remove the routes', async function () { + await server.plugins.uninstall({ npmName: 'peertube-plugin-test-five' }) + + for (const path of basePaths) { + await makeGetRequest({ + url: server.url, + path: path + 'ping', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + + await makePostBodyRequest({ + url: server.url, + path: path + 'ping', + fields: {}, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + } + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/plugin-storage.ts b/packages/tests/src/plugins/plugin-storage.ts new file mode 100644 index 000000000..f9b0ead0c --- /dev/null +++ b/packages/tests/src/plugins/plugin-storage.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { pathExists } from 'fs-extra/esm' +import { readdir, readFile } from 'fs/promises' +import { join } from 'path' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test plugin storage', function () { + let server: PeerTubeServer + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') }) + }) + + describe('DB storage', function () { + it('Should correctly store a subkey', async function () { + await server.servers.waitUntilLog('superkey stored value is toto') + }) + + it('Should correctly retrieve an array as array from the storage.', async function () { + await server.servers.waitUntilLog('storedArrayKey isArray is true') + await server.servers.waitUntilLog('storedArrayKey stored value is toto, toto2') + }) + }) + + describe('Disk storage', function () { + let dataPath: string + let pluginDataPath: string + + async function getFileContent () { + const files = await readdir(pluginDataPath) + expect(files).to.have.lengthOf(1) + + return readFile(join(pluginDataPath, files[0]), 'utf8') + } + + before(function () { + dataPath = server.servers.buildDirectory('plugins/data') + pluginDataPath = join(dataPath, 'peertube-plugin-test-six') + }) + + it('Should have created the directory on install', async function () { + const dataPath = server.servers.buildDirectory('plugins/data') + const pluginDataPath = join(dataPath, 'peertube-plugin-test-six') + + expect(await pathExists(dataPath)).to.be.true + expect(await pathExists(pluginDataPath)).to.be.true + expect(await readdir(pluginDataPath)).to.have.lengthOf(0) + }) + + it('Should have created a file', async function () { + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path: '/plugins/test-six/router/create-file', + expectedStatus: HttpStatusCode.OK_200 + }) + + const content = await getFileContent() + expect(content).to.equal('Prince Ali') + }) + + it('Should still have the file after an uninstallation', async function () { + await server.plugins.uninstall({ npmName: 'peertube-plugin-test-six' }) + + const content = await getFileContent() + expect(content).to.equal('Prince Ali') + }) + + it('Should still have the file after the reinstallation', async function () { + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-six') }) + + const content = await getFileContent() + expect(content).to.equal('Prince Ali') + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/plugin-transcoding.ts b/packages/tests/src/plugins/plugin-transcoding.ts new file mode 100644 index 000000000..2f50f65ff --- /dev/null +++ b/packages/tests/src/plugins/plugin-transcoding.ts @@ -0,0 +1,279 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { getAudioStream, getVideoStream, getVideoStreamFPS } from '@peertube/peertube-ffmpeg' +import { VideoPrivacy } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers, + setDefaultVideoChannel, + testFfmpegStreamError, + waitJobs +} from '@peertube/peertube-server-commands' + +async function createLiveWrapper (server: PeerTubeServer) { + const liveAttributes = { + name: 'live video', + channelId: server.store.channel.id, + privacy: VideoPrivacy.PUBLIC + } + + const { uuid } = await server.live.create({ fields: liveAttributes }) + + return uuid +} + +function updateConf (server: PeerTubeServer, vodProfile: string, liveProfile: string) { + return server.config.updateCustomSubConfig({ + newConfig: { + transcoding: { + enabled: true, + profile: vodProfile, + hls: { + enabled: true + }, + webVideos: { + enabled: true + }, + resolutions: { + '240p': true, + '360p': false, + '480p': false, + '720p': true + } + }, + live: { + transcoding: { + profile: liveProfile, + enabled: true, + resolutions: { + '240p': true, + '360p': false, + '480p': false, + '720p': true + } + } + } + } + }) +} + +describe('Test transcoding plugins', function () { + let server: PeerTubeServer + + before(async function () { + this.timeout(60000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + + await updateConf(server, 'default', 'default') + }) + + describe('When using a plugin adding profiles to existing encoders', function () { + + async function checkVideoFPS (uuid: string, type: 'above' | 'below', fps: number) { + const video = await server.videos.get({ id: uuid }) + const files = video.files.concat(...video.streamingPlaylists.map(p => p.files)) + + for (const file of files) { + if (type === 'above') { + expect(file.fps).to.be.above(fps) + } else { + expect(file.fps).to.be.below(fps) + } + } + } + + async function checkLiveFPS (uuid: string, type: 'above' | 'below', fps: number) { + const playlistUrl = `${server.url}/static/streaming-playlists/hls/${uuid}/0.m3u8` + const videoFPS = await getVideoStreamFPS(playlistUrl) + + if (type === 'above') { + expect(videoFPS).to.be.above(fps) + } else { + expect(videoFPS).to.be.below(fps) + } + } + + before(async function () { + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-one') }) + }) + + it('Should have the appropriate available profiles', async function () { + const config = await server.config.getConfig() + + expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ]) + expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'high-live', 'input-options-live', 'bad-scale-live' ]) + }) + + describe('VOD', function () { + + it('Should not use the plugin profile if not chosen by the admin', async function () { + this.timeout(240000) + + const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid + await waitJobs([ server ]) + + await checkVideoFPS(videoUUID, 'above', 20) + }) + + it('Should use the vod profile', async function () { + this.timeout(240000) + + await updateConf(server, 'low-vod', 'default') + + const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid + await waitJobs([ server ]) + + await checkVideoFPS(videoUUID, 'below', 12) + }) + + it('Should apply input options in vod profile', async function () { + this.timeout(240000) + + await updateConf(server, 'input-options-vod', 'default') + + const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid + await waitJobs([ server ]) + + await checkVideoFPS(videoUUID, 'below', 6) + }) + + it('Should apply the scale filter in vod profile', async function () { + this.timeout(240000) + + await updateConf(server, 'bad-scale-vod', 'default') + + const videoUUID = (await server.videos.quickUpload({ name: 'video' })).uuid + await waitJobs([ server ]) + + // Transcoding failed + const video = await server.videos.get({ id: videoUUID }) + expect(video.files).to.have.lengthOf(1) + expect(video.streamingPlaylists).to.have.lengthOf(0) + }) + }) + + describe('Live', function () { + + it('Should not use the plugin profile if not chosen by the admin', async function () { + this.timeout(240000) + + const liveVideoId = await createLiveWrapper(server) + + await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) + await server.live.waitUntilPublished({ videoId: liveVideoId }) + await waitJobs([ server ]) + + await checkLiveFPS(liveVideoId, 'above', 20) + }) + + it('Should use the live profile', async function () { + this.timeout(240000) + + await updateConf(server, 'low-vod', 'high-live') + + const liveVideoId = await createLiveWrapper(server) + + await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) + await server.live.waitUntilPublished({ videoId: liveVideoId }) + await waitJobs([ server ]) + + await checkLiveFPS(liveVideoId, 'above', 45) + }) + + it('Should apply the input options on live profile', async function () { + this.timeout(240000) + + await updateConf(server, 'low-vod', 'input-options-live') + + const liveVideoId = await createLiveWrapper(server) + + await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) + await server.live.waitUntilPublished({ videoId: liveVideoId }) + await waitJobs([ server ]) + + await checkLiveFPS(liveVideoId, 'above', 45) + }) + + it('Should apply the scale filter name on live profile', async function () { + this.timeout(240000) + + await updateConf(server, 'low-vod', 'bad-scale-live') + + const liveVideoId = await createLiveWrapper(server) + + const command = await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_very_short_240p.mp4' }) + await testFfmpegStreamError(command, true) + }) + + it('Should default to the default profile if the specified profile does not exist', async function () { + this.timeout(240000) + + await server.plugins.uninstall({ npmName: 'peertube-plugin-test-transcoding-one' }) + + const config = await server.config.getConfig() + + expect(config.transcoding.availableProfiles).to.deep.equal([ 'default' ]) + expect(config.live.transcoding.availableProfiles).to.deep.equal([ 'default' ]) + + const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid + await waitJobs([ server ]) + + await checkVideoFPS(videoUUID, 'above', 20) + }) + }) + + }) + + describe('When using a plugin adding new encoders', function () { + + before(async function () { + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-transcoding-two') }) + + await updateConf(server, 'test-vod-profile', 'test-live-profile') + }) + + it('Should use the new vod encoders', async function () { + this.timeout(240000) + + const videoUUID = (await server.videos.quickUpload({ name: 'video', fixture: 'video_very_short_240p.mp4' })).uuid + await waitJobs([ server ]) + + const video = await server.videos.get({ id: videoUUID }) + + const path = server.servers.buildWebVideoFilePath(video.files[0].fileUrl) + const audioProbe = await getAudioStream(path) + expect(audioProbe.audioStream.codec_name).to.equal('opus') + + const videoProbe = await getVideoStream(path) + expect(videoProbe.codec_name).to.equal('vp9') + }) + + it('Should use the new live encoders', async function () { + this.timeout(240000) + + const liveVideoId = await createLiveWrapper(server) + + await server.live.sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) + await server.live.waitUntilPublished({ videoId: liveVideoId }) + await waitJobs([ server ]) + + const playlistUrl = `${server.url}/static/streaming-playlists/hls/${liveVideoId}/0.m3u8` + const audioProbe = await getAudioStream(playlistUrl) + expect(audioProbe.audioStream.codec_name).to.equal('opus') + + const videoProbe = await getVideoStream(playlistUrl) + expect(videoProbe.codec_name).to.equal('h264') + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/plugin-unloading.ts b/packages/tests/src/plugins/plugin-unloading.ts new file mode 100644 index 000000000..70310bc8c --- /dev/null +++ b/packages/tests/src/plugins/plugin-unloading.ts @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' +import { HttpStatusCode } from '@peertube/peertube-models' + +describe('Test plugins module unloading', function () { + let server: PeerTubeServer = null + const requestPath = '/plugins/test-unloading/router/get' + let value: string = null + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') }) + }) + + it('Should return a numeric value', async function () { + const res = await makeGetRequest({ + url: server.url, + path: requestPath, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.message).to.match(/^\d+$/) + value = res.body.message + }) + + it('Should return the same value the second time', async function () { + const res = await makeGetRequest({ + url: server.url, + path: requestPath, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.message).to.be.equal(value) + }) + + it('Should uninstall the plugin and free the route', async function () { + await server.plugins.uninstall({ npmName: 'peertube-plugin-test-unloading' }) + + await makeGetRequest({ + url: server.url, + path: requestPath, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should return a different numeric value', async function () { + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-unloading') }) + + const res = await makeGetRequest({ + url: server.url, + path: requestPath, + expectedStatus: HttpStatusCode.OK_200 + }) + + expect(res.body.message).to.match(/^\d+$/) + expect(res.body.message).to.be.not.equal(value) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/plugin-websocket.ts b/packages/tests/src/plugins/plugin-websocket.ts new file mode 100644 index 000000000..832dcebd0 --- /dev/null +++ b/packages/tests/src/plugins/plugin-websocket.ts @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import WebSocket from 'ws' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +function buildWebSocket (server: PeerTubeServer, path: string) { + return new WebSocket('ws://' + server.host + path) +} + +function expectErrorOrTimeout (server: PeerTubeServer, path: string, expectedTimeout: number) { + return new Promise((res, rej) => { + const ws = buildWebSocket(server, path) + ws.on('error', () => res()) + + const timeout = setTimeout(() => res(), expectedTimeout) + + ws.on('open', () => { + clearTimeout(timeout) + + return rej(new Error('Connect did not timeout')) + }) + }) +} + +describe('Test plugin websocket', function () { + let server: PeerTubeServer + const basePaths = [ + '/plugins/test-websocket/ws/', + '/plugins/test-websocket/0.0.1/ws/' + ] + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-websocket') }) + }) + + it('Should not connect to the websocket without the appropriate path', async function () { + const paths = [ + '/plugins/unknown/ws/', + '/plugins/unknown/0.0.1/ws/' + ] + + for (const path of paths) { + await expectErrorOrTimeout(server, path, 1000) + } + }) + + it('Should not connect to the websocket without the appropriate sub path', async function () { + for (const path of basePaths) { + await expectErrorOrTimeout(server, path + '/unknown', 1000) + } + }) + + it('Should connect to the websocket and receive pong', function (done) { + const ws = buildWebSocket(server, basePaths[0]) + + ws.on('open', () => ws.send('ping')) + ws.on('message', data => { + if (data.toString() === 'pong') return done() + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/translations.ts b/packages/tests/src/plugins/translations.ts new file mode 100644 index 000000000..a69e14134 --- /dev/null +++ b/packages/tests/src/plugins/translations.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { + cleanupTests, + createSingleServer, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test plugin translations', function () { + let server: PeerTubeServer + let command: PluginsCommand + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + command = server.plugins + + await command.install({ path: PluginsCommand.getPluginTestPath() }) + await command.install({ path: PluginsCommand.getPluginTestPath('-filter-translations') }) + }) + + it('Should not have translations for locale pt', async function () { + const body = await command.getTranslations({ locale: 'pt' }) + + expect(body).to.deep.equal({}) + }) + + it('Should have translations for locale fr', async function () { + const body = await command.getTranslations({ locale: 'fr-FR' }) + + expect(body).to.deep.equal({ + 'peertube-plugin-test': { + Hi: 'Coucou' + }, + 'peertube-plugin-test-filter-translations': { + 'Hello world': 'Bonjour le monde' + } + }) + }) + + it('Should have translations of locale it', async function () { + const body = await command.getTranslations({ locale: 'it-IT' }) + + expect(body).to.deep.equal({ + 'peertube-plugin-test-filter-translations': { + 'Hello world': 'Ciao, mondo!' + } + }) + }) + + it('Should remove the plugin and remove the locales', async function () { + await command.uninstall({ npmName: 'peertube-plugin-test-filter-translations' }) + + { + const body = await command.getTranslations({ locale: 'fr-FR' }) + + expect(body).to.deep.equal({ + 'peertube-plugin-test': { + Hi: 'Coucou' + } + }) + } + + { + const body = await command.getTranslations({ locale: 'it-IT' }) + + expect(body).to.deep.equal({}) + } + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/plugins/video-constants.ts b/packages/tests/src/plugins/video-constants.ts new file mode 100644 index 000000000..b81240a64 --- /dev/null +++ b/packages/tests/src/plugins/video-constants.ts @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + PeerTubeServer, + PluginsCommand, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' +import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models' + +describe('Test plugin altering video constants', function () { + let server: PeerTubeServer + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + await setAccessTokensToServers([ server ]) + + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') }) + }) + + it('Should have updated languages', async function () { + const languages = await server.videos.getLanguages() + + expect(languages['en']).to.not.exist + expect(languages['fr']).to.not.exist + + expect(languages['al_bhed']).to.equal('Al Bhed') + expect(languages['al_bhed2']).to.equal('Al Bhed 2') + expect(languages['al_bhed3']).to.not.exist + }) + + it('Should have updated categories', async function () { + const categories = await server.videos.getCategories() + + expect(categories[1]).to.not.exist + expect(categories[2]).to.not.exist + + expect(categories[42]).to.equal('Best category') + expect(categories[43]).to.equal('High best category') + }) + + it('Should have updated licences', async function () { + const licences = await server.videos.getLicences() + + expect(licences[1]).to.not.exist + expect(licences[7]).to.not.exist + + expect(licences[42]).to.equal('Best licence') + expect(licences[43]).to.equal('High best licence') + }) + + it('Should have updated video privacies', async function () { + const privacies = await server.videos.getPrivacies() + + expect(privacies[1]).to.exist + expect(privacies[2]).to.not.exist + expect(privacies[3]).to.exist + expect(privacies[4]).to.exist + }) + + it('Should have updated playlist privacies', async function () { + const playlistPrivacies = await server.playlists.getPrivacies() + + expect(playlistPrivacies[1]).to.exist + expect(playlistPrivacies[2]).to.exist + expect(playlistPrivacies[3]).to.not.exist + }) + + it('Should not be able to create a video with this privacy', async function () { + const attributes = { name: 'video', privacy: VideoPrivacy.UNLISTED } + await server.videos.upload({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should not be able to create a video with this privacy', async function () { + const attributes = { displayName: 'video playlist', privacy: VideoPlaylistPrivacy.PRIVATE } + await server.playlists.create({ attributes, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should be able to upload a video with these values', async function () { + const attributes = { name: 'video', category: 42, licence: 42, language: 'al_bhed2' } + const { uuid } = await server.videos.upload({ attributes }) + + const video = await server.videos.get({ id: uuid }) + expect(video.language.label).to.equal('Al Bhed 2') + expect(video.licence.label).to.equal('Best licence') + expect(video.category.label).to.equal('Best category') + }) + + it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () { + await server.plugins.uninstall({ npmName: 'peertube-plugin-test-video-constants' }) + + { + const languages = await server.videos.getLanguages() + + expect(languages['en']).to.equal('English') + expect(languages['fr']).to.equal('French') + + expect(languages['al_bhed']).to.not.exist + expect(languages['al_bhed2']).to.not.exist + expect(languages['al_bhed3']).to.not.exist + } + + { + const categories = await server.videos.getCategories() + + expect(categories[1]).to.equal('Music') + expect(categories[2]).to.equal('Films') + + expect(categories[42]).to.not.exist + expect(categories[43]).to.not.exist + } + + { + const licences = await server.videos.getLicences() + + expect(licences[1]).to.equal('Attribution') + expect(licences[7]).to.equal('Public Domain Dedication') + + expect(licences[42]).to.not.exist + expect(licences[43]).to.not.exist + } + + { + const privacies = await server.videos.getPrivacies() + + expect(privacies[1]).to.exist + expect(privacies[2]).to.exist + expect(privacies[3]).to.exist + expect(privacies[4]).to.exist + } + + { + const playlistPrivacies = await server.playlists.getPrivacies() + + expect(playlistPrivacies[1]).to.exist + expect(playlistPrivacies[2]).to.exist + expect(playlistPrivacies[3]).to.exist + } + }) + + it('Should be able to reset categories', async function () { + await server.plugins.install({ path: PluginsCommand.getPluginTestPath('-video-constants') }) + + { + const categories = await server.videos.getCategories() + + expect(categories[1]).to.not.exist + expect(categories[2]).to.not.exist + + expect(categories[42]).to.exist + expect(categories[43]).to.exist + } + + await makeGetRequest({ + url: server.url, + token: server.accessToken, + path: '/plugins/test-video-constants/router/reset-categories', + expectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + + { + const categories = await server.videos.getCategories() + + expect(categories[1]).to.exist + expect(categories[2]).to.exist + + expect(categories[42]).to.not.exist + expect(categories[43]).to.not.exist + } + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) -- cgit v1.2.3