diff --git a/packages/server-commands/src/requests/requests.ts b/packages/server-commands/src/requests/requests.ts index ac143ea5d..bd05142ce 100644 --- a/packages/server-commands/src/requests/requests.ts +++ b/packages/server-commands/src/requests/requests.ts @@ -3,7 +3,7 @@ import { decode } from 'querystring' import request from 'supertest' import { URL } from 'url' -import { pick } from '@peertube/peertube-core-utils' +import { pick, queryParamsToObject } from '@peertube/peertube-core-utils' import { HttpStatusCode, HttpStatusCodeType } from '@peertube/peertube-models' import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' @@ -23,23 +23,33 @@ export type CommonRequestParams = { expectedStatus?: HttpStatusCodeType } -function makeRawRequest (options: { +export function makeRawRequest (options: { url: string token?: string expectedStatus?: HttpStatusCodeType + responseType?: string range?: string query?: { [ id: string ]: string } method?: 'GET' | 'POST' + accept?: string headers?: { [ name: string ]: string } + redirects?: number }) { - const { host, protocol, pathname } = new URL(options.url) + const { host, protocol, pathname, searchParams } = new URL(options.url) const reqOptions = { url: `${protocol}//${host}`, path: pathname, + contentType: undefined, - ...pick(options, [ 'expectedStatus', 'range', 'token', 'query', 'headers' ]) + query: { + ...(options.query || {}), + + ...queryParamsToObject(searchParams) + }, + + ...pick(options, [ 'expectedStatus', 'range', 'token', 'headers', 'responseType', 'accept', 'redirects' ]) } if (options.method === 'POST') { @@ -49,7 +59,7 @@ function makeRawRequest (options: { return makeGetRequest(reqOptions) } -function makeGetRequest (options: CommonRequestParams & { +export function makeGetRequest (options: CommonRequestParams & { query?: any rawQuery?: string }) { @@ -61,7 +71,7 @@ function makeGetRequest (options: CommonRequestParams & { return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) } -function makeHTMLRequest (url: string, path: string) { +export function makeHTMLRequest (url: string, path: string) { return makeGetRequest({ url, path, @@ -70,7 +80,9 @@ function makeHTMLRequest (url: string, path: string) { }) } -function makeActivityPubGetRequest (url: string, path: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) { +// --------------------------------------------------------------------------- + +export function makeActivityPubGetRequest (url: string, path: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) { return makeGetRequest({ url, path, @@ -79,7 +91,17 @@ function makeActivityPubGetRequest (url: string, path: string, expectedStatus: H }) } -function makeDeleteRequest (options: CommonRequestParams & { +export function makeActivityPubRawRequest (url: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) { + return makeRawRequest({ + url, + expectedStatus, + accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8' + }) +} + +// --------------------------------------------------------------------------- + +export function makeDeleteRequest (options: CommonRequestParams & { query?: any rawQuery?: string }) { @@ -91,7 +113,7 @@ function makeDeleteRequest (options: CommonRequestParams & { return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) } -function makeUploadRequest (options: CommonRequestParams & { +export function makeUploadRequest (options: CommonRequestParams & { method?: 'POST' | 'PUT' fields: { [ fieldName: string ]: any } @@ -119,7 +141,7 @@ function makeUploadRequest (options: CommonRequestParams & { return req } -function makePostBodyRequest (options: CommonRequestParams & { +export function makePostBodyRequest (options: CommonRequestParams & { fields?: { [ fieldName: string ]: any } }) { const req = request(options.url).post(options.path) @@ -128,7 +150,7 @@ function makePostBodyRequest (options: CommonRequestParams & { return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) } -function makePutBodyRequest (options: { +export function makePutBodyRequest (options: { url: string path: string token?: string @@ -142,21 +164,35 @@ function makePutBodyRequest (options: { return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options }) } -function decodeQueryString (path: string) { +// --------------------------------------------------------------------------- + +export async function getRedirectionUrl (url: string) { + const res = await makeRawRequest({ + url, + redirects: 0, + expectedStatus: HttpStatusCode.FOUND_302 + }) + + return res.headers['location'] +} + +// --------------------------------------------------------------------------- + +export function decodeQueryString (path: string) { return decode(path.split('?')[1]) } // --------------------------------------------------------------------------- -function unwrapBody (test: request.Test): Promise { +export function unwrapBody (test: request.Test): Promise { return test.then(res => res.body) } -function unwrapText (test: request.Test): Promise { +export function unwrapText (test: request.Test): Promise { return test.then(res => res.text) } -function unwrapBodyOrDecodeToJSON (test: request.Test): Promise { +export function unwrapBodyOrDecodeToJSON (test: request.Test): Promise { return test.then(res => { if (res.body instanceof Buffer) { try { @@ -180,28 +216,12 @@ function unwrapBodyOrDecodeToJSON (test: request.Test): Promise { }) } -function unwrapTextOrDecode (test: request.Test): Promise { +export function unwrapTextOrDecode (test: request.Test): Promise { return test.then(res => res.text || new TextDecoder().decode(res.body)) } // --------------------------------------------------------------------------- - -export { - makeHTMLRequest, - makeGetRequest, - decodeQueryString, - makeUploadRequest, - makePostBodyRequest, - makePutBodyRequest, - makeDeleteRequest, - makeRawRequest, - makeActivityPubGetRequest, - unwrapBody, - unwrapTextOrDecode, - unwrapBodyOrDecodeToJSON, - unwrapText -} - +// Private // --------------------------------------------------------------------------- function buildRequest (req: request.Test, options: CommonRequestParams) { diff --git a/packages/server-commands/src/server/config-command.ts b/packages/server-commands/src/server/config-command.ts index 80f532df1..ac2358abe 100644 --- a/packages/server-commands/src/server/config-command.ts +++ b/packages/server-commands/src/server/config-command.ts @@ -46,15 +46,15 @@ export class ConfigCommand extends AbstractCommand { // --------------------------------------------------------------------------- - disableImports () { - return this.setImportsEnabled(false) + disableVideoImports () { + return this.setVideoImportsEnabled(false) } - enableImports () { - return this.setImportsEnabled(true) + enableVideoImports () { + return this.setVideoImportsEnabled(true) } - private setImportsEnabled (enabled: boolean) { + private setVideoImportsEnabled (enabled: boolean) { return this.updateExistingSubConfig({ newConfig: { import: { @@ -118,6 +118,74 @@ export class ConfigCommand extends AbstractCommand { // --------------------------------------------------------------------------- + enableAutoBlacklist () { + return this.setAutoblacklistEnabled(true) + } + + disableAutoBlacklist () { + return this.setAutoblacklistEnabled(false) + } + + private setAutoblacklistEnabled (enabled: boolean) { + return this.updateExistingSubConfig({ + newConfig: { + autoBlacklist: { + videos: { + ofUsers: { + enabled + } + } + } + } + }) + } + + // --------------------------------------------------------------------------- + + enableUserImport () { + return this.setUserImportEnabled(true) + } + + disableUserImport () { + return this.setUserImportEnabled(false) + } + + private setUserImportEnabled (enabled: boolean) { + return this.updateExistingSubConfig({ + newConfig: { + import: { + users: { + enabled + } + } + } + }) + } + + // --------------------------------------------------------------------------- + + enableUserExport () { + return this.setUserExportEnabled(true) + } + + disableUserExport () { + return this.setUserExportEnabled(false) + } + + private setUserExportEnabled (enabled: boolean) { + return this.updateExistingSubConfig({ + newConfig: { + export: { + users: { + enabled + } + } + } + }) + } + + // --------------------------------------------------------------------------- + enableLive (options: { allowReplay?: boolean transcoding?: boolean @@ -552,6 +620,16 @@ export class ConfigCommand extends AbstractCommand { videoChannelSynchronization: { enabled: false, maxPerUser: 10 + }, + users: { + enabled: true + } + }, + export: { + users: { + enabled: true, + maxUserVideoQuota: 5242881, + exportExpiration: 1000 * 3600 } }, trending: { diff --git a/packages/server-commands/src/server/server.ts b/packages/server-commands/src/server/server.ts index 3911a6fad..ef0491c73 100644 --- a/packages/server-commands/src/server/server.ts +++ b/packages/server-commands/src/server/server.ts @@ -17,12 +17,14 @@ import { SocketIOCommand } from '../socket/index.js' import { AccountsCommand, BlocklistCommand, + UserExportsCommand, LoginCommand, NotificationsCommand, RegistrationsCommand, SubscriptionsCommand, TwoFactorCommand, - UsersCommand + UsersCommand, + UserImportsCommand } from '../users/index.js' import { BlacklistCommand, @@ -33,7 +35,7 @@ import { ChaptersCommand, CommentsCommand, HistoryCommand, - ImportsCommand, + VideoImportsCommand, LiveCommand, PlaylistsCommand, ServicesCommand, @@ -90,7 +92,6 @@ export class PeerTubeServer { user?: { username: string password: string - email?: string } channel?: VideoChannel @@ -134,7 +135,7 @@ export class PeerTubeServer { changeOwnership?: ChangeOwnershipCommand playlists?: PlaylistsCommand history?: HistoryCommand - imports?: ImportsCommand + videoImports?: VideoImportsCommand channelSyncs?: ChannelSyncsCommand streamingPlaylists?: StreamingPlaylistsCommand channels?: ChannelsCommand @@ -155,6 +156,9 @@ export class PeerTubeServer { storyboard?: StoryboardCommand chapters?: ChaptersCommand + userImports?: UserImportsCommand + userExports?: UserExportsCommand + runners?: RunnersCommand runnerRegistrationTokens?: RunnerRegistrationTokensCommand runnerJobs?: RunnerJobsCommand @@ -426,7 +430,7 @@ export class PeerTubeServer { this.changeOwnership = new ChangeOwnershipCommand(this) this.playlists = new PlaylistsCommand(this) this.history = new HistoryCommand(this) - this.imports = new ImportsCommand(this) + this.videoImports = new VideoImportsCommand(this) this.channelSyncs = new ChannelSyncsCommand(this) this.streamingPlaylists = new StreamingPlaylistsCommand(this) this.channels = new ChannelsCommand(this) @@ -446,6 +450,9 @@ export class PeerTubeServer { this.storyboard = new StoryboardCommand(this) this.chapters = new ChaptersCommand(this) + this.userExports = new UserExportsCommand(this) + this.userImports = new UserImportsCommand(this) + this.runners = new RunnersCommand(this) this.runnerRegistrationTokens = new RunnerRegistrationTokensCommand(this) this.runnerJobs = new RunnerJobsCommand(this) diff --git a/packages/server-commands/src/server/servers.ts b/packages/server-commands/src/server/servers.ts index 142973850..dc478929e 100644 --- a/packages/server-commands/src/server/servers.ts +++ b/packages/server-commands/src/server/servers.ts @@ -21,7 +21,7 @@ function createMultipleServers (totalServers: number, configOverride?: object, o } function killallServers (servers: PeerTubeServer[]) { - return Promise.all(servers.map(s => s.kill())) + return Promise.all(servers.filter(s => !!s).map(s => s.kill())) } async function cleanupTests (servers: PeerTubeServer[]) { @@ -33,6 +33,8 @@ async function cleanupTests (servers: PeerTubeServer[]) { let p: Promise[] = [] for (const server of servers) { + if (!server) continue + p = p.concat(server.servers.cleanupTests()) } diff --git a/packages/server-commands/src/shared/abstract-command.ts b/packages/server-commands/src/shared/abstract-command.ts index bb6522e07..116b02847 100644 --- a/packages/server-commands/src/shared/abstract-command.ts +++ b/packages/server-commands/src/shared/abstract-command.ts @@ -1,6 +1,7 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ + import { isAbsolute } from 'path' -import { HttpStatusCodeType } from '@peertube/peertube-models' -import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' +import { buildAbsoluteFixturePath, getFileSize } from '@peertube/peertube-node-utils' import { makeDeleteRequest, makeGetRequest, @@ -10,8 +11,12 @@ import { unwrapBody, unwrapText } from '../requests/requests.js' +import { expect } from 'chai' +import got, { Response as GotResponse } from 'got' +import { HttpStatusCode, HttpStatusCodeType } from '@peertube/peertube-models' import type { PeerTubeServer } from '../server/server.js' +import { createReadStream } from 'fs' export interface OverrideCommandOptions { token?: string @@ -48,7 +53,7 @@ interface InternalDeleteCommandOptions extends InternalCommonCommandOptions { rawQuery?: string } -abstract class AbstractCommand { +export abstract class AbstractCommand { constructor ( protected server: PeerTubeServer @@ -218,8 +223,221 @@ abstract class AbstractCommand { ? { 'x-peertube-video-password': videoPassword } : undefined } -} -export { - AbstractCommand + // --------------------------------------------------------------------------- + + protected async buildResumeUpload (options: OverrideCommandOptions & { + path: string + + fixture: string + attaches?: Record + fields?: Record + + completedExpectedStatus?: HttpStatusCodeType // When the upload is finished + }): Promise { + const { path, fixture, expectedStatus = HttpStatusCode.OK_200, completedExpectedStatus } = options + + let size = 0 + let videoFilePath: string + let mimetype = 'video/mp4' + + if (fixture) { + videoFilePath = buildAbsoluteFixturePath(fixture) + size = await getFileSize(videoFilePath) + + if (videoFilePath.endsWith('.mkv')) { + mimetype = 'video/x-matroska' + } else if (videoFilePath.endsWith('.webm')) { + mimetype = 'video/webm' + } else if (videoFilePath.endsWith('.zip')) { + mimetype = 'application/zip' + } + } + + // Do not check status automatically, we'll check it manually + const initializeSessionRes = await this.prepareResumableUpload({ + ...options, + + path, + expectedStatus: null, + + size, + mimetype + }) + const initStatus = initializeSessionRes.status + + if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) { + const locationHeader = initializeSessionRes.header['location'] + expect(locationHeader).to.not.be.undefined + + const pathUploadId = locationHeader.split('?')[1] + + const result = await this.sendResumableChunks({ + ...options, + + path, + pathUploadId, + videoFilePath, + size, + expectedStatus: completedExpectedStatus + }) + + if (result.statusCode === HttpStatusCode.OK_200) { + await this.endResumableUpload({ + ...options, + + expectedStatus: HttpStatusCode.NO_CONTENT_204, + path, + pathUploadId + }) + } + + return result.body as T + } + + const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200 + ? HttpStatusCode.CREATED_201 + : expectedStatus + + expect(initStatus).to.equal(expectedInitStatus) + + return initializeSessionRes.body.video || initializeSessionRes.body + } + + protected async prepareResumableUpload (options: OverrideCommandOptions & { + path: string + + fixture: string + size: number + mimetype: string + + attaches?: Record + fields?: Record + + originalName?: string + lastModified?: number + }) { + const { path, attaches = {}, fields = {}, originalName, lastModified, fixture, size, mimetype } = options + + const uploadOptions = { + ...options, + + path, + headers: { + 'X-Upload-Content-Type': mimetype, + 'X-Upload-Content-Length': size.toString() + }, + fields: { + filename: fixture, + originalName, + lastModified, + + ...fields + }, + + // Fixture will be sent later + attaches, + implicitToken: true, + + defaultExpectedStatus: null + } + + if (Object.keys(attaches).length === 0) return this.postBodyRequest(uploadOptions) + + return this.postUploadRequest(uploadOptions) + } + + protected async sendResumableChunks (options: OverrideCommandOptions & { + pathUploadId: string + path: string + videoFilePath: string + size: number + contentLength?: number + contentRangeBuilder?: (start: number, chunk: any) => string + digestBuilder?: (chunk: any) => string + }) { + const { + path, + pathUploadId, + videoFilePath, + size, + contentLength, + contentRangeBuilder, + digestBuilder, + expectedStatus = HttpStatusCode.OK_200 + } = options + + let start = 0 + + const token = this.buildCommonRequestToken({ ...options, implicitToken: true }) + + const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 }) + const server = this.server + return new Promise>((resolve, reject) => { + readable.on('data', async function onData (chunk) { + try { + readable.pause() + + const byterangeStart = start + chunk.length - 1 + + const headers = { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/octet-stream', + 'Content-Range': contentRangeBuilder + ? contentRangeBuilder(start, chunk) + : `bytes ${start}-${byterangeStart}/${size}`, + 'Content-Length': contentLength ? contentLength + '' : chunk.length + '' + } + + if (digestBuilder) { + Object.assign(headers, { digest: digestBuilder(chunk) }) + } + + const res = await got({ + url: new URL(path + '?' + pathUploadId, server.url).toString(), + method: 'put', + headers, + body: chunk, + responseType: 'json', + throwHttpErrors: false + }) + + start += chunk.length + + // Last request, check final status + if (byterangeStart + 1 === size) { + if (res.statusCode === expectedStatus) { + return resolve(res) + } + + if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) { + readable.off('data', onData) + + // eslint-disable-next-line max-len + const message = `Incorrect transient behaviour sending intermediary chunks. Status code is ${res.statusCode} instead of ${expectedStatus}` + return reject(new Error(message)) + } + } + + readable.resume() + } catch (err) { + reject(err) + } + }) + }) + } + + protected endResumableUpload (options: OverrideCommandOptions & { + path: string + pathUploadId: string + }) { + return this.deleteRequest({ + ...options, + + path: options.path, + rawQuery: options.pathUploadId, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } } diff --git a/packages/server-commands/src/users/index.ts b/packages/server-commands/src/users/index.ts index baa048a43..d6b86261d 100644 --- a/packages/server-commands/src/users/index.ts +++ b/packages/server-commands/src/users/index.ts @@ -1,10 +1,12 @@ export * from './accounts-command.js' export * from './accounts.js' export * from './blocklist-command.js' -export * from './login.js' export * from './login-command.js' +export * from './login.js' export * from './notifications-command.js' export * from './registrations-command.js' export * from './subscriptions-command.js' export * from './two-factor-command.js' +export * from './user-exports-command.js' +export * from './user-imports-command.js' export * from './users-command.js' diff --git a/packages/server-commands/src/users/login.ts b/packages/server-commands/src/users/login.ts index c48c42c72..930cb9888 100644 --- a/packages/server-commands/src/users/login.ts +++ b/packages/server-commands/src/users/login.ts @@ -1,19 +1,11 @@ import { PeerTubeServer } from '../server/server.js' -function setAccessTokensToServers (servers: PeerTubeServer[]) { - const tasks: Promise[] = [] +export function setAccessTokensToServers (servers: PeerTubeServer[]) { + return Promise.all( + servers.map(async server => { + const token = await server.login.getAccessToken() - for (const server of servers) { - const p = server.login.getAccessToken() - .then(t => { server.accessToken = t }) - tasks.push(p) - } - - return Promise.all(tasks) -} - -// --------------------------------------------------------------------------- - -export { - setAccessTokensToServers + server.accessToken = token + }) + ) } diff --git a/packages/server-commands/src/users/user-exports-command.ts b/packages/server-commands/src/users/user-exports-command.ts new file mode 100644 index 000000000..2cac8e6e6 --- /dev/null +++ b/packages/server-commands/src/users/user-exports-command.ts @@ -0,0 +1,77 @@ +import { HttpStatusCode, ResultList, UserExport, UserExportRequestResult, UserExportState } from '@peertube/peertube-models' +import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' +import { wait } from '@peertube/peertube-core-utils' +import { unwrapBody } from '../requests/requests.js' + +export class UserExportsCommand extends AbstractCommand { + + request (options: OverrideCommandOptions & { + userId: number + withVideoFiles: boolean + }) { + const { userId, withVideoFiles } = options + + return unwrapBody(this.postBodyRequest({ + ...options, + + path: `/api/v1/users/${userId}/exports/request`, + fields: { withVideoFiles }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + async waitForCreation (options: OverrideCommandOptions & { + userId: number + }) { + const { userId } = options + + while (true) { + const { data } = await this.list({ ...options, userId }) + + if (data.some(e => e.state.id === UserExportState.COMPLETED)) break + + await wait(250) + } + } + + list (options: OverrideCommandOptions & { + userId: number + }) { + const { userId } = options + + return this.getRequestBody>({ + ...options, + + path: `/api/v1/users/${userId}/exports`, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + async deleteAllArchives (options: OverrideCommandOptions & { + userId: number + }) { + const { data } = await this.list(options) + + for (const { id } of data) { + await this.delete({ ...options, exportId: id }) + } + } + + delete (options: OverrideCommandOptions & { + exportId: number + userId: number + }) { + const { userId, exportId } = options + + return this.deleteRequest({ + ...options, + + path: `/api/v1/users/${userId}/exports/${exportId}`, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + +} diff --git a/packages/server-commands/src/users/user-imports-command.ts b/packages/server-commands/src/users/user-imports-command.ts new file mode 100644 index 000000000..31f5793b9 --- /dev/null +++ b/packages/server-commands/src/users/user-imports-command.ts @@ -0,0 +1,31 @@ +import { HttpStatusCode, HttpStatusCodeType, UserImport, UserImportUploadResult } from '@peertube/peertube-models' +import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' + +export class UserImportsCommand extends AbstractCommand { + + importArchive (options: OverrideCommandOptions & { + userId: number + fixture: string + completedExpectedStatus?: HttpStatusCodeType + }) { + return this.buildResumeUpload({ + ...options, + + path: `/api/v1/users/${options.userId}/imports/import-resumable`, + fixture: options.fixture, + completedExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getLatestImport (options: OverrideCommandOptions & { + userId: number + }) { + return this.getRequestBody({ + ...options, + + path: `/api/v1/users/${options.userId}/imports/latest`, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/packages/server-commands/src/videos/index.ts b/packages/server-commands/src/videos/index.ts index 8d193e24c..7835ba242 100644 --- a/packages/server-commands/src/videos/index.ts +++ b/packages/server-commands/src/videos/index.ts @@ -7,7 +7,7 @@ export * from './chapters-command.js' export * from './channel-syncs-command.js' export * from './comments-command.js' export * from './history-command.js' -export * from './imports-command.js' +export * from './video-imports-command.js' export * from './live-command.js' export * from './live.js' export * from './playlists-command.js' diff --git a/packages/server-commands/src/videos/playlists-command.ts b/packages/server-commands/src/videos/playlists-command.ts index 99f3e3c0d..c687ab001 100644 --- a/packages/server-commands/src/videos/playlists-command.ts +++ b/packages/server-commands/src/videos/playlists-command.ts @@ -11,6 +11,8 @@ import { VideoPlaylistElementCreate, VideoPlaylistElementCreateResult, VideoPlaylistElementUpdate, + VideoPlaylistPrivacy, + VideoPlaylistPrivacyType, VideoPlaylistReorder, VideoPlaylistType_Type, VideoPlaylistUpdate @@ -156,6 +158,27 @@ export class PlaylistsCommand extends AbstractCommand { return body.videoPlaylist } + async quickCreate (options: OverrideCommandOptions & { + displayName: string + privacy?: VideoPlaylistPrivacyType + }) { + const { displayName, privacy = VideoPlaylistPrivacy.PUBLIC } = options + + const { videoChannels } = await this.server.users.getMyInfo({ token: options.token }) + + return this.create({ + ...options, + + attributes: { + displayName, + privacy, + videoChannelId: privacy === VideoPlaylistPrivacy.PUBLIC + ? videoChannels[0].id + : undefined + } + }) + } + update (options: OverrideCommandOptions & { attributes: VideoPlaylistUpdate playlistId: number | string diff --git a/packages/server-commands/src/videos/imports-command.ts b/packages/server-commands/src/videos/video-imports-command.ts similarity index 97% rename from packages/server-commands/src/videos/imports-command.ts rename to packages/server-commands/src/videos/video-imports-command.ts index d51248181..c60d787c9 100644 --- a/packages/server-commands/src/videos/imports-command.ts +++ b/packages/server-commands/src/videos/video-imports-command.ts @@ -2,7 +2,7 @@ import { HttpStatusCode, ResultList, VideoImport, VideoImportCreate } from '@pee import { unwrapBody } from '../requests/index.js' import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' -export class ImportsCommand extends AbstractCommand { +export class VideoImportsCommand extends AbstractCommand { importVideo (options: OverrideCommandOptions & { attributes: (VideoImportCreate | { torrentfile?: string, previewfile?: string, thumbnailfile?: string }) diff --git a/packages/server-commands/src/videos/videos-command.ts b/packages/server-commands/src/videos/videos-command.ts index 72dc58a4b..c43eec253 100644 --- a/packages/server-commands/src/videos/videos-command.ts +++ b/packages/server-commands/src/videos/videos-command.ts @@ -1,9 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ -import { expect } from 'chai' -import { createReadStream } from 'fs' -import { stat } from 'fs/promises' -import got, { Response as GotResponse } from 'got' import validator from 'validator' import { getAllPrivacies, omit, pick, wait } from '@peertube/peertube-core-utils' import { @@ -429,7 +425,13 @@ export class VideosCommand extends AbstractCommand { const created = mode === 'legacy' ? await this.buildLegacyUpload({ ...options, attributes }) - : await this.buildResumeUpload({ ...options, path: '/api/v1/videos/upload-resumable', attributes }) + : await this.buildResumeVideoUpload({ + ...options, + path: '/api/v1/videos/upload-resumable', + fixture: attributes.fixture, + attaches: this.buildUploadAttaches(attributes, false), + fields: this.buildUploadFields(attributes) + }) // Wait torrent generation const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 }) @@ -456,231 +458,26 @@ export class VideosCommand extends AbstractCommand { path, fields: this.buildUploadFields(options.attributes), - attaches: this.buildUploadAttaches(options.attributes), + attaches: this.buildUploadAttaches(options.attributes, true), implicitToken: true, defaultExpectedStatus: HttpStatusCode.OK_200 })).then(body => body.video || body as any) } - async buildResumeUpload (options: OverrideCommandOptions & { - path: string - attributes: { fixture?: string } & { [id: string]: any } - completedExpectedStatus?: HttpStatusCodeType // When the upload is finished - }): Promise { - const { path, attributes, expectedStatus = HttpStatusCode.OK_200, completedExpectedStatus } = options - - let size = 0 - let videoFilePath: string - let mimetype = 'video/mp4' - - if (attributes.fixture) { - videoFilePath = buildAbsoluteFixturePath(attributes.fixture) - size = (await stat(videoFilePath)).size - - if (videoFilePath.endsWith('.mkv')) { - mimetype = 'video/x-matroska' - } else if (videoFilePath.endsWith('.webm')) { - mimetype = 'video/webm' - } - } - - // Do not check status automatically, we'll check it manually - const initializeSessionRes = await this.prepareResumableUpload({ - ...options, - - path, - expectedStatus: null, - attributes, - size, - mimetype - }) - const initStatus = initializeSessionRes.status - - if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) { - const locationHeader = initializeSessionRes.header['location'] - expect(locationHeader).to.not.be.undefined - - const pathUploadId = locationHeader.split('?')[1] - - const result = await this.sendResumableChunks({ - ...options, - - path, - pathUploadId, - videoFilePath, - size, - expectedStatus: completedExpectedStatus - }) - - if (result.statusCode === HttpStatusCode.OK_200) { - await this.endResumableUpload({ - ...options, - - expectedStatus: HttpStatusCode.NO_CONTENT_204, - path, - pathUploadId - }) - } - - return result.body?.video || result.body as any - } - - const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200 - ? HttpStatusCode.CREATED_201 - : expectedStatus - - expect(initStatus).to.equal(expectedInitStatus) - - return initializeSessionRes.body.video || initializeSessionRes.body - } - - async prepareResumableUpload (options: OverrideCommandOptions & { - path: string - attributes: { fixture?: string } & { [id: string]: any } - size: number - mimetype: string - - originalName?: string - lastModified?: number - }) { - const { path, attributes, originalName, lastModified, size, mimetype } = options - - const attaches = this.buildUploadAttaches(omit(options.attributes, [ 'fixture' ])) - - const uploadOptions = { - ...options, - - path, - headers: { - 'X-Upload-Content-Type': mimetype, - 'X-Upload-Content-Length': size.toString() - }, - fields: { - filename: attributes.fixture, - originalName, - lastModified, - - ...this.buildUploadFields(options.attributes) - }, - - // Fixture will be sent later - attaches: this.buildUploadAttaches(omit(options.attributes, [ 'fixture' ])), - implicitToken: true, - - defaultExpectedStatus: null - } - - if (Object.keys(attaches).length === 0) return this.postBodyRequest(uploadOptions) - - return this.postUploadRequest(uploadOptions) - } - - sendResumableChunks (options: OverrideCommandOptions & { - pathUploadId: string - path: string - videoFilePath: string - size: number - contentLength?: number - contentRangeBuilder?: (start: number, chunk: any) => string - digestBuilder?: (chunk: any) => string - }) { - const { - path, - pathUploadId, - videoFilePath, - size, - contentLength, - contentRangeBuilder, - digestBuilder, - expectedStatus = HttpStatusCode.OK_200 - } = options - - let start = 0 - - const token = this.buildCommonRequestToken({ ...options, implicitToken: true }) - - const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 }) - const server = this.server - return new Promise>((resolve, reject) => { - readable.on('data', async function onData (chunk) { - try { - readable.pause() - - const byterangeStart = start + chunk.length - 1 - - const headers = { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/octet-stream', - 'Content-Range': contentRangeBuilder - ? contentRangeBuilder(start, chunk) - : `bytes ${start}-${byterangeStart}/${size}`, - 'Content-Length': contentLength ? contentLength + '' : chunk.length + '' - } - - if (digestBuilder) { - Object.assign(headers, { digest: digestBuilder(chunk) }) - } - - const res = await got<{ video: VideoCreateResult }>({ - url: new URL(path + '?' + pathUploadId, server.url).toString(), - method: 'put', - headers, - body: chunk, - responseType: 'json', - throwHttpErrors: false - }) - - start += chunk.length - - // Last request, check final status - if (byterangeStart + 1 === size) { - if (res.statusCode === expectedStatus) { - return resolve(res) - } - - if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) { - readable.off('data', onData) - - // eslint-disable-next-line max-len - const message = `Incorrect transient behaviour sending intermediary chunks. Status code is ${res.statusCode} instead of ${expectedStatus}` - return reject(new Error(message)) - } - } - - readable.resume() - } catch (err) { - reject(err) - } - }) - }) - } - - endResumableUpload (options: OverrideCommandOptions & { - path: string - pathUploadId: string - }) { - return this.deleteRequest({ - ...options, - - path: options.path, - rawQuery: options.pathUploadId, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - quickUpload (options: OverrideCommandOptions & { name: string nsfw?: boolean privacy?: VideoPrivacyType fixture?: string videoPasswords?: string[] + channelId?: number }) { const attributes: VideoEdit = { name: options.name } if (options.nsfw) attributes.nsfw = options.nsfw if (options.privacy) attributes.privacy = options.privacy if (options.fixture) attributes.fixture = options.fixture if (options.videoPasswords) attributes.videoPasswords = options.videoPasswords + if (options.channelId) attributes.channelId = options.channelId return this.upload({ ...options, attributes }) } @@ -713,7 +510,7 @@ export class VideosCommand extends AbstractCommand { ...options, path: '/api/v1/videos/' + options.videoId + '/source/replace-resumable', - attributes: { fixture: options.fixture } + fixture: options.fixture }) } @@ -813,19 +610,38 @@ export class VideosCommand extends AbstractCommand { ]) } - private buildUploadFields (attributes: VideoEdit) { + buildUploadFields (attributes: VideoEdit) { return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ]) } - private buildUploadAttaches (attributes: VideoEdit) { + buildUploadAttaches (attributes: VideoEdit, includeFixture: boolean) { const attaches: { [ name: string ]: string } = {} for (const key of [ 'thumbnailfile', 'previewfile' ]) { if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key]) } - if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture) + if (includeFixture && attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture) return attaches } + + // Make these methods public, needed by some offensive tests + sendResumableVideoChunks (options: Parameters[0]) { + return super.sendResumableChunks<{ video: VideoCreateResult }>(options) + } + + async buildResumeVideoUpload (options: Parameters[0]) { + const result = await super.buildResumeUpload<{ video: VideoCreateResult }>(options) + + return result?.video || undefined + } + + prepareVideoResumableUpload (options: Parameters[0]) { + return super.prepareResumableUpload(options) + } + + endVideoResumableUpload (options: Parameters[0]) { + return super.endResumableUpload(options) + } } diff --git a/packages/server-commands/tsconfig.json b/packages/server-commands/tsconfig.json index eb942f295..e29fc20a3 100644 --- a/packages/server-commands/tsconfig.json +++ b/packages/server-commands/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "baseUrl": "./", "outDir": "./dist", "rootDir": "src", "tsBuildInfoFile": "./dist/.tsbuildinfo" diff --git a/packages/tests/fixtures/custom-thumbnail-from-preview.jpg b/packages/tests/fixtures/custom-thumbnail-from-preview.jpg new file mode 100644 index 000000000..fe8545b48 Binary files /dev/null and b/packages/tests/fixtures/custom-thumbnail-from-preview.jpg differ diff --git a/packages/tests/fixtures/export-bad-structure.zip b/packages/tests/fixtures/export-bad-structure.zip new file mode 100644 index 000000000..e3b60b990 Binary files /dev/null and b/packages/tests/fixtures/export-bad-structure.zip differ diff --git a/packages/tests/fixtures/export-bad-video-file.zip b/packages/tests/fixtures/export-bad-video-file.zip new file mode 100644 index 000000000..2648a01b6 Binary files /dev/null and b/packages/tests/fixtures/export-bad-video-file.zip differ diff --git a/packages/tests/fixtures/export-bad-video.zip b/packages/tests/fixtures/export-bad-video.zip new file mode 100644 index 000000000..b2497f41a Binary files /dev/null and b/packages/tests/fixtures/export-bad-video.zip differ diff --git a/packages/tests/fixtures/export-with-files.zip b/packages/tests/fixtures/export-with-files.zip new file mode 100644 index 000000000..ed299d84e Binary files /dev/null and b/packages/tests/fixtures/export-with-files.zip differ diff --git a/packages/tests/fixtures/export-without-files.zip b/packages/tests/fixtures/export-without-files.zip new file mode 100644 index 000000000..76c5a4c71 Binary files /dev/null and b/packages/tests/fixtures/export-without-files.zip differ diff --git a/packages/tests/fixtures/export-without-videos.zip b/packages/tests/fixtures/export-without-videos.zip new file mode 100644 index 000000000..1d0e55e50 Binary files /dev/null and b/packages/tests/fixtures/export-without-videos.zip differ diff --git a/packages/tests/fixtures/peertube-plugin-test/main.js b/packages/tests/fixtures/peertube-plugin-test/main.js index e16bf0ca3..5f8d24505 100644 --- a/packages/tests/fixtures/peertube-plugin-test/main.js +++ b/packages/tests/fixtures/peertube-plugin-test/main.js @@ -212,6 +212,16 @@ async function register ({ registerHook, registerSetting, settingsManager, stora } }) + registerHook({ + target: 'filter:api.video.user-import.accept.result', + handler: ({ accepted }, { videoBody }) => { + if (!accepted) return { accepted: false } + if (videoBody.name === 'video 1') return { accepted: false, errorMessage: 'bad word' } + + return { accepted: true } + } + }) + // --------------------------------------------------------------------------- registerHook({ @@ -402,7 +412,8 @@ async function register ({ registerHook, registerSetting, settingsManager, stora 'filter:api.video.upload.video-attribute.result', 'filter:api.video.import-url.video-attribute.result', 'filter:api.video.import-torrent.video-attribute.result', - 'filter:api.video.live.video-attribute.result' + 'filter:api.video.live.video-attribute.result', + 'filter:api.video.user-import.video-attribute.result' ]) { registerHook({ target, diff --git a/packages/tests/src/api/check-params/channel-import-videos.ts b/packages/tests/src/api/check-params/channel-import-videos.ts index 0e897dad7..678ad8ff5 100644 --- a/packages/tests/src/api/check-params/channel-import-videos.ts +++ b/packages/tests/src/api/check-params/channel-import-videos.ts @@ -35,7 +35,7 @@ describe('Test videos import in a channel API validator', function () { await setAccessTokensToServers([ server ]) await setDefaultVideoChannel([ server ]) - await server.config.enableImports() + await server.config.enableVideoImports() await server.config.enableChannelSync() const userCreds = { @@ -68,7 +68,7 @@ describe('Test videos import in a channel API validator', function () { it('Should fail when HTTP upload is disabled', async function () { await server.config.disableChannelSync() - await server.config.disableImports() + await server.config.disableVideoImports() await command.importVideos({ channelName: server.store.channel.name, @@ -77,7 +77,7 @@ describe('Test videos import in a channel API validator', function () { expectedStatus: HttpStatusCode.FORBIDDEN_403 }) - await server.config.enableImports() + await server.config.enableVideoImports() }) it('Should fail when externalChannelUrl is not provided', async function () { diff --git a/packages/tests/src/api/check-params/config.ts b/packages/tests/src/api/check-params/config.ts index b2fbd4c3e..b5b282200 100644 --- a/packages/tests/src/api/check-params/config.ts +++ b/packages/tests/src/api/check-params/config.ts @@ -192,6 +192,16 @@ describe('Test config API validators', function () { videoChannelSynchronization: { enabled: false, maxPerUser: 10 + }, + users: { + enabled: false + } + }, + export: { + users: { + enabled: false, + maxUserVideoQuota: 40, + exportExpiration: 10 } }, trending: { diff --git a/packages/tests/src/api/check-params/index.ts b/packages/tests/src/api/check-params/index.ts index d7867e8a5..8fc73544e 100644 --- a/packages/tests/src/api/check-params/index.ts +++ b/packages/tests/src/api/check-params/index.ts @@ -8,6 +8,8 @@ import './contact-form.js' import './custom-pages.js' import './debug.js' import './follows.js' +import './user-export.js' +import './user-import.js.js' import './jobs.js' import './live.js' import './logs.js' diff --git a/packages/tests/src/api/check-params/upload-quota.ts b/packages/tests/src/api/check-params/upload-quota.ts index a77792822..89c9a25ee 100644 --- a/packages/tests/src/api/check-params/upload-quota.ts +++ b/packages/tests/src/api/check-params/upload-quota.ts @@ -75,13 +75,13 @@ describe('Test upload quota', function () { channelId: server.store.channel.id, privacy: VideoPrivacy.PUBLIC } - await server.imports.importVideo({ attributes: { ...baseAttributes, targetUrl: FIXTURE_URLS.goodVideo } }) - await server.imports.importVideo({ attributes: { ...baseAttributes, magnetUri: FIXTURE_URLS.magnet } }) - await server.imports.importVideo({ attributes: { ...baseAttributes, torrentfile: 'video-720p.torrent' as any } }) + await server.videoImports.importVideo({ attributes: { ...baseAttributes, targetUrl: FIXTURE_URLS.goodVideo } }) + await server.videoImports.importVideo({ attributes: { ...baseAttributes, magnetUri: FIXTURE_URLS.magnet } }) + await server.videoImports.importVideo({ attributes: { ...baseAttributes, torrentfile: 'video-720p.torrent' as any } }) await waitJobs([ server ]) - const { total, data: videoImports } = await server.imports.getMyVideoImports() + const { total, data: videoImports } = await server.videoImports.getMyVideoImports() expect(total).to.equal(3) expect(videoImports).to.have.lengthOf(3) diff --git a/packages/tests/src/api/check-params/user-export.ts b/packages/tests/src/api/check-params/user-export.ts new file mode 100644 index 000000000..498975c74 --- /dev/null +++ b/packages/tests/src/api/check-params/user-export.ts @@ -0,0 +1,339 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { wait } from '@peertube/peertube-core-utils' +import { HttpStatusCode } from '@peertube/peertube-models' +import { + cleanupTests, + createSingleServer, + makeGetRequest, + makeRawRequest, + PeerTubeServer, + setAccessTokensToServers +} from '@peertube/peertube-server-commands' + +describe('Test user export API validators', function () { + let server: PeerTubeServer + let rootId: number + + let userId: number + let userToken: string + + let exportId: number + let userExportId: number + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + { + const user = await server.users.getMyInfo() + rootId = user.id + } + + { + userToken = await server.users.generateUserAndToken('user') + const user = await server.users.getMyInfo({ token: userToken }) + userId = user.id + } + }) + + describe('Request export', function () { + + it('Should fail if export is disabled', async function () { + await server.config.disableUserExport() + + await server.userExports.request({ userId: rootId, withVideoFiles: false, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + await server.config.enableUserExport() + }) + + it('Should fail without token', async function () { + await server.userExports.request({ + userId: rootId, + withVideoFiles: false, + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with invalid token', async function () { + await server.userExports.request({ + userId: rootId, + withVideoFiles: false, + token: 'hello', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a token of another user', async function () { + await server.userExports.request({ + userId: rootId, + withVideoFiles: false, + token: userToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an unknown user', async function () { + await server.userExports.request({ userId: 404, withVideoFiles: false, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail if user quota is too big', async function () { + const { videoQuotaUsed } = await server.users.getMyQuotaUsed() + + await server.config.updateExistingSubConfig({ + newConfig: { + export: { + users: { maxUserVideoQuota: videoQuotaUsed - 1 } + } + } + }) + + await server.userExports.request({ userId: rootId, withVideoFiles: true, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await server.userExports.request({ userId: rootId, withVideoFiles: false, expectedStatus: HttpStatusCode.OK_200 }) + + // Cleanup + await server.userExports.waitForCreation({ userId: rootId }) + await server.userExports.deleteAllArchives({ userId: rootId }) + + await server.config.updateExistingSubConfig({ + newConfig: { + export: { + users: { maxUserVideoQuota: 1000 * 1000 * 1000 * 1000 } + } + } + }) + }) + + it('Should succeed with the appropriate token', async function () { + const { export: { id } } = await server.userExports.request({ userId: rootId, withVideoFiles: false }) + + exportId = id + }) + + it('Should fail if there is already an export', async function () { + await server.userExports.request({ + userId: rootId, + withVideoFiles: false, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should succeed after a delete with an admin token', async function () { + await server.userExports.waitForCreation({ userId: rootId }) + await server.userExports.delete({ userId: rootId, exportId }) + + const { export: { id } } = await server.userExports.request({ userId: rootId, withVideoFiles: false }) + exportId = id + }) + }) + + describe('List exports', function () { + + it('Should fail if export is disabled', async function () { + await server.config.disableUserExport() + + await server.userExports.list({ userId: rootId, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + await server.config.enableUserExport() + }) + + it('Should fail without token', async function () { + await server.userExports.list({ + userId: rootId, + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with invalid token', async function () { + await server.userExports.list({ + userId: rootId, + token: 'toto', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a token of another user', async function () { + await server.userExports.list({ + userId: rootId, + token: userToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an unknown user', async function () { + await server.userExports.list({ userId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should succeed with the correct parameters', async function () { + // User token + await server.userExports.list({ userId, token: userToken }) + // Root token + await server.userExports.list({ userId }) + }) + }) + + describe('Deleting export', function () { + + before(async function () { + const { export: { id } } = await server.userExports.request({ userId, withVideoFiles: true }) + userExportId = id + + await server.userExports.waitForCreation({ userId }) + }) + + it('Should fail if export is disabled', async function () { + await server.config.disableUserExport() + + await server.userExports.delete({ userId, exportId: userExportId, token: userToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + + await server.config.enableUserExport() + }) + + it('Should fail without token', async function () { + await server.userExports.delete({ + userId: rootId, + exportId, + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with invalid token', async function () { + await server.userExports.delete({ + userId: rootId, + exportId, + token: 'toto', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a token of another user', async function () { + await server.userExports.delete({ + userId: rootId, + exportId, + token: userToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an export id of another user', async function () { + await server.userExports.delete({ + userId, + exportId, + token: userToken, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an unknown user', async function () { + await server.userExports.delete({ + userId: 404, + exportId, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail with an unknown export id', async function () { + await server.userExports.delete({ + userId, + exportId: 404, + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should succeed with the correct parameters', async function () { + await server.userExports.delete({ + userId, + exportId: userExportId, + token: userToken + }) + }) + }) + + describe('Downloading an export', function () { + + before(async function () { + await server.userExports.request({ userId, withVideoFiles: true }) + await server.userExports.waitForCreation({ userId }) + }) + + it('Should fail without jwt token', async function () { + const { data } = await server.userExports.list({ userId }) + + const url = data[0].privateDownloadUrl.replace('jwt=', 'toto=') + await makeRawRequest({ url, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + }) + + it('Should fail with a wrong jwt token', async function () { + const { data } = await server.userExports.list({ userId }) + + // Invalid format + { + const url = data[0].privateDownloadUrl.replace('jwt=', 'jwt=hello.coucou') + await makeRawRequest({ url, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + } + + // Invalid content + { + const url = data[0].privateDownloadUrl.replace('jwt=', 'jwt=a') + await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + } + }) + + it('Should fail with a jwt token of another export', async function () { + let userQuery: string + + // Save user JWT token + { + const { data } = await server.userExports.list({ userId }) + + const { pathname, search } = new URL(data[0].privateDownloadUrl) + const rawQuery = search.replace('?', '') + userQuery = rawQuery + + await makeGetRequest({ url: server.url, path: pathname, rawQuery, expectedStatus: HttpStatusCode.OK_200 }) + } + + // This user JWT token must not be used to download an export of another user + { + const { data } = await server.userExports.list({ userId: rootId }) + + const { pathname, search } = new URL(data[0].privateDownloadUrl) + const rawQuery = search.replace('?', '') + + await makeGetRequest({ url: server.url, path: pathname, rawQuery, expectedStatus: HttpStatusCode.OK_200 }) + await makeGetRequest({ url: server.url, path: pathname, rawQuery: userQuery, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + } + }) + + it('Should fail with an invalid filename', async function () { + const { data } = await server.userExports.list({ userId }) + + const url = data[0].privateDownloadUrl.replace('.zip', '.tar') + await makeRawRequest({ url, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with an expired JWT token', async function () { + const { data } = await server.userExports.list({ userId }) + + await wait(3000) + await makeRawRequest({ url: data[0].privateDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct params', async function () { + const { data } = await server.userExports.list({ userId }) + await makeRawRequest({ url: data[0].privateDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/user-import.ts b/packages/tests/src/api/check-params/user-import.ts new file mode 100644 index 000000000..fa47d6fd0 --- /dev/null +++ b/packages/tests/src/api/check-params/user-import.ts @@ -0,0 +1,169 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { + cleanupTests, + createSingleServer, PeerTubeServer, + setAccessTokensToServers, + waitJobs +} from '@peertube/peertube-server-commands' +import { HttpStatusCode } from '../../../../models/src/http/http-status-codes.js' +import { expect } from 'chai' + +describe('Test user import API validators', function () { + let server: PeerTubeServer + let userId: number + let rootId: number + let token: string + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(30000) + + server = await createSingleServer(1) + + await setAccessTokensToServers([ server ]) + + { + const result = await server.users.generate('user') + userId = result.userId + token = result.token + } + + { + const { id } = await server.users.getMyInfo() + rootId = id + } + }) + + describe('Request import', function () { + + it('Should fail if import is disabled', async function () { + await server.config.disableUserImport() + + await server.userImports.importArchive({ + userId, + fixture: 'export-without-files.zip', + token, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await server.config.enableUserImport() + }) + + it('Should fail without token', async function () { + await server.userImports.importArchive({ + userId, + fixture: 'export-without-files.zip', + token: null, + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with invalid token', async function () { + await server.userImports.importArchive({ + userId, + fixture: 'export-without-files.zip', + token: 'invalid', + expectedStatus: HttpStatusCode.UNAUTHORIZED_401 + }) + }) + + it('Should fail with a token of another user', async function () { + await server.userImports.importArchive({ + userId: rootId, + fixture: 'export-without-files.zip', + token, + expectedStatus: HttpStatusCode.FORBIDDEN_403 + }) + }) + + it('Should fail with an unknown user', async function () { + await server.userImports.importArchive({ + userId: 404, + fixture: 'export-without-files.zip', + expectedStatus: HttpStatusCode.NOT_FOUND_404 + }) + }) + + it('Should fail if user quota is exceeded', async function () { + await server.users.update({ userId, videoQuota: 100 }) + + await server.userImports.importArchive({ + userId, + fixture: 'export-without-files.zip', + expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413 + }) + + await server.users.update({ userId, videoQuota: -1 }) + }) + + it('Should succeed with the correct params', async function () { + await server.userImports.importArchive({ userId, fixture: 'export-without-files.zip' }) + + await waitJobs([ server ]) + }) + + it('Should fail with an import that is already being processed', async function () { + await server.userImports.importArchive({ userId, fixture: 'export-without-files.zip' }) + await server.userImports.importArchive({ + userId, + fixture: 'export-without-files.zip', + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + }) + + it('Should fail with invalid ZIPs', async function () { + this.timeout(120000) + + const toTest = [ + 'export-bad-video-file.zip', + 'export-bad-video.zip', + 'export-without-videos.zip', + 'export-bad-structure.zip', + 'export-bad-structure.zip' + ] + + const tokens: string[] = [] + + for (let i = 0; i < toTest.length; i++) { + const { token, userId } = await server.users.generate('import' + i) + await server.userImports.importArchive({ userId, token, fixture: toTest[i] }) + } + + await waitJobs([ server ]) + + for (const token of tokens) { + const { data } = await server.videos.listMyVideos({ token }) + expect(data).to.have.lengthOf(0) + } + }) + }) + + describe('Get latest import status', function () { + + it('Should fail without token', async function () { + await server.userImports.getLatestImport({ userId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with invalid token', async function () { + await server.userImports.getLatestImport({ userId, token: 'invalid', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + }) + + it('Should fail with an unknown user', async function () { + await server.userImports.getLatestImport({ userId: 404, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + }) + + it('Should fail with a token of another user', async function () { + await server.userImports.getLatestImport({ userId: rootId, token, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + }) + + it('Should succeed with the correct parameters', async function () { + await server.userImports.getLatestImport({ userId, token }) + }) + }) + + after(async function () { + await cleanupTests([ server ]) + }) +}) diff --git a/packages/tests/src/api/check-params/video-imports.ts b/packages/tests/src/api/check-params/video-imports.ts index e078cedd6..e183301b3 100644 --- a/packages/tests/src/api/check-params/video-imports.ts +++ b/packages/tests/src/api/check-params/video-imports.ts @@ -371,7 +371,7 @@ describe('Test video imports API validator', function () { async function importVideo () { const attributes = { channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } - const res = await server.imports.importVideo({ attributes }) + const res = await server.videoImports.importVideo({ attributes }) return res.id } @@ -381,23 +381,23 @@ describe('Test video imports API validator', function () { }) it('Should fail with an invalid import id', async function () { - await server.imports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) - await server.imports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.videoImports.cancel({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) + await server.videoImports.delete({ importId: 'artyom' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) }) it('Should fail with an unknown import id', async function () { - await server.imports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) - await server.imports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await server.videoImports.cancel({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + await server.videoImports.delete({ importId: 42, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) }) it('Should fail without token', async function () { - await server.imports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) - await server.imports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await server.videoImports.cancel({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) + await server.videoImports.delete({ importId, token: null, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) }) it('Should fail with another user token', async function () { - await server.imports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) - await server.imports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await server.videoImports.cancel({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await server.videoImports.delete({ importId, token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) }) it('Should fail to cancel non pending import', async function () { @@ -405,11 +405,11 @@ describe('Test video imports API validator', function () { await waitJobs([ server ]) - await server.imports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) + await server.videoImports.cancel({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) }) it('Should succeed to delete an import', async function () { - await server.imports.delete({ importId }) + await server.videoImports.delete({ importId }) }) it('Should fail to delete a pending import', async function () { @@ -417,13 +417,13 @@ describe('Test video imports API validator', function () { importId = await importVideo() - await server.imports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) + await server.videoImports.delete({ importId, expectedStatus: HttpStatusCode.CONFLICT_409 }) }) it('Should succeed to cancel an import', async function () { importId = await importVideo() - await server.imports.cancel({ importId }) + await server.videoImports.cancel({ importId }) }) }) diff --git a/packages/tests/src/api/check-params/video-passwords.ts b/packages/tests/src/api/check-params/video-passwords.ts index 3f57ebe74..91db22438 100644 --- a/packages/tests/src/api/check-params/video-passwords.ts +++ b/packages/tests/src/api/check-params/video-passwords.ts @@ -111,7 +111,7 @@ describe('Test video passwords validator', function () { if (mode === 'import') { const attributes = { ...baseCorrectParams, targetUrl: FIXTURE_URLS.goodVideo, videoPasswords } - return server.imports.importVideo({ attributes, expectedStatus }) + return server.videoImports.importVideo({ attributes, expectedStatus }) } if (mode === 'updateVideo') { diff --git a/packages/tests/src/api/moderation/video-blacklist.ts b/packages/tests/src/api/moderation/video-blacklist.ts index 341dadad0..3a19b8204 100644 --- a/packages/tests/src/api/moderation/video-blacklist.ts +++ b/packages/tests/src/api/moderation/video-blacklist.ts @@ -8,9 +8,7 @@ import { BlacklistCommand, cleanupTests, createMultipleServers, - doubleFollow, - killallServers, - PeerTubeServer, + doubleFollow, PeerTubeServer, setAccessTokensToServers, setDefaultChannelAvatar, waitJobs @@ -321,18 +319,7 @@ describe('Test video blacklist', function () { before(async function () { this.timeout(20000) - await killallServers([ servers[0] ]) - - const config = { - auto_blacklist: { - videos: { - of_users: { - enabled: true - } - } - } - } - await servers[0].run(config) + await servers[0].config.enableAutoBlacklist() { const user = { username: 'user_without_flag', password: 'password' } @@ -380,7 +367,7 @@ describe('Test video blacklist', function () { name: 'URL import', channelId: channelOfUserWithoutFlag } - await servers[0].imports.importVideo({ token: userWithoutFlag, attributes }) + await servers[0].videoImports.importVideo({ token: userWithoutFlag, attributes }) const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED }) expect(body.total).to.equal(2) @@ -393,7 +380,7 @@ describe('Test video blacklist', function () { name: 'Torrent import', channelId: channelOfUserWithoutFlag } - await servers[0].imports.importVideo({ token: userWithoutFlag, attributes }) + await servers[0].videoImports.importVideo({ token: userWithoutFlag, attributes }) const body = await command.list({ sort: 'createdAt', type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED }) expect(body.total).to.equal(3) diff --git a/packages/tests/src/api/notifications/moderation-notifications.ts b/packages/tests/src/api/notifications/moderation-notifications.ts index e5c1cbb89..6c726162c 100644 --- a/packages/tests/src/api/notifications/moderation-notifications.ts +++ b/packages/tests/src/api/notifications/moderation-notifications.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import { wait } from '@peertube/peertube-core-utils' -import { AbuseState, CustomConfig, UserNotification, UserRole, VideoPrivacy } from '@peertube/peertube-models' +import { AbuseState, UserNotification, UserRole, VideoPrivacy } from '@peertube/peertube-models' import { buildUUID } from '@peertube/peertube-node-utils' import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' @@ -425,7 +425,6 @@ describe('Test moderation notifications', function () { let uuid: string let shortUUID: string let videoName: string - let currentCustomConfig: CustomConfig before(async function () { @@ -450,23 +449,7 @@ describe('Test moderation notifications', function () { token: userToken1 } - currentCustomConfig = await servers[0].config.getCustomConfig() - - const autoBlacklistTestsCustomConfig = { - ...currentCustomConfig, - - autoBlacklist: { - videos: { - ofUsers: { - enabled: true - } - } - } - } - - // enable transcoding otherwise own publish notification after transcoding not expected - autoBlacklistTestsCustomConfig.transcoding.enabled = true - await servers[0].config.updateCustomConfig({ newCustomConfig: autoBlacklistTestsCustomConfig }) + await servers[0].config.enableAutoBlacklist() await servers[0].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host }) await servers[1].subscriptions.add({ targetUri: 'user_1_channel@' + servers[0].host }) @@ -594,8 +577,6 @@ describe('Test moderation notifications', function () { }) after(async () => { - await servers[0].config.updateCustomConfig({ newCustomConfig: currentCustomConfig }) - await servers[0].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host }) await servers[1].subscriptions.remove({ uri: 'user_1_channel@' + servers[0].host }) }) diff --git a/packages/tests/src/api/notifications/user-notifications.ts b/packages/tests/src/api/notifications/user-notifications.ts index 20532e6ee..b62466716 100644 --- a/packages/tests/src/api/notifications/user-notifications.ts +++ b/packages/tests/src/api/notifications/user-notifications.ts @@ -205,7 +205,7 @@ describe('Test user notifications', function () { privacy: VideoPrivacy.PUBLIC, targetUrl: FIXTURE_URLS.goodVideo } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) await waitJobs(servers) @@ -349,7 +349,7 @@ describe('Test user notifications', function () { targetUrl: FIXTURE_URLS.goodVideo, waitTranscoding: true } - const { video } = await servers[1].imports.importVideo({ attributes }) + const { video } = await servers[1].videoImports.importVideo({ attributes }) await waitJobs(servers) await checkMyVideoIsPublished({ ...baseParams, videoName: name, shortUUID: video.shortUUID, checkType: 'presence' }) @@ -524,7 +524,7 @@ describe('Test user notifications', function () { privacy: VideoPrivacy.PRIVATE, targetUrl: FIXTURE_URLS.badVideo } - const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes }) + const { video: { shortUUID } } = await servers[0].videoImports.importVideo({ attributes }) await waitJobs(servers) @@ -543,7 +543,7 @@ describe('Test user notifications', function () { privacy: VideoPrivacy.PRIVATE, targetUrl: FIXTURE_URLS.goodVideo } - const { video: { shortUUID } } = await servers[0].imports.importVideo({ attributes }) + const { video: { shortUUID } } = await servers[0].videoImports.importVideo({ attributes }) await waitJobs(servers) diff --git a/packages/tests/src/api/object-storage/video-imports.ts b/packages/tests/src/api/object-storage/video-imports.ts index 43f769842..78f94bce1 100644 --- a/packages/tests/src/api/object-storage/video-imports.ts +++ b/packages/tests/src/api/object-storage/video-imports.ts @@ -24,7 +24,7 @@ async function importVideo (server: PeerTubeServer) { targetUrl: FIXTURE_URLS.goodVideo720 } - const { video: { uuid } } = await server.imports.importVideo({ attributes }) + const { video: { uuid } } = await server.videoImports.importVideo({ attributes }) return uuid } @@ -45,7 +45,7 @@ describe('Object storage for video import', function () { await setAccessTokensToServers([ server ]) await setDefaultVideoChannel([ server ]) - await server.config.enableImports() + await server.config.enableVideoImports() }) describe('Without transcoding', async function () { diff --git a/packages/tests/src/api/server/config-defaults.ts b/packages/tests/src/api/server/config-defaults.ts index e874af012..fe425976b 100644 --- a/packages/tests/src/api/server/config-defaults.ts +++ b/packages/tests/src/api/server/config-defaults.ts @@ -59,7 +59,7 @@ describe('Test config defaults', function () { before(async function () { await server.config.disableTranscoding() - await server.config.enableImports() + await server.config.enableVideoImports() await server.config.enableLive({ allowReplay: false, transcoding: false }) }) @@ -82,7 +82,7 @@ describe('Test config defaults', function () { }) it('Should respect default values when importing a video using URL', async function () { - const { video: { id } } = await server.imports.importVideo({ + const { video: { id } } = await server.videoImports.importVideo({ attributes: { ...attributes, channelId, @@ -95,7 +95,7 @@ describe('Test config defaults', function () { }) it('Should respect default values when importing a video using magnet URI', async function () { - const { video: { id } } = await server.imports.importVideo({ + const { video: { id } } = await server.videoImports.importVideo({ attributes: { ...attributes, channelId, diff --git a/packages/tests/src/api/server/config.ts b/packages/tests/src/api/server/config.ts index baa9b0574..2b108a462 100644 --- a/packages/tests/src/api/server/config.ts +++ b/packages/tests/src/api/server/config.ts @@ -112,6 +112,8 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { expect(data.import.videos.concurrency).to.equal(2) expect(data.import.videos.http.enabled).to.be.true expect(data.import.videos.torrent.enabled).to.be.true + expect(data.import.videoChannelSynchronization.enabled).to.be.false + expect(data.import.users.enabled).to.be.true expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false expect(data.followers.instance.enabled).to.be.true @@ -127,6 +129,10 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { expect(data.broadcastMessage.dismissable).to.be.false expect(data.storyboards.enabled).to.be.true + + expect(data.export.users.enabled).to.be.true + expect(data.export.users.exportExpiration).to.equal(1000 * 3600 * 48) + expect(data.export.users.maxUserVideoQuota).to.equal(10737418240) } function checkUpdatedConfig (data: CustomConfig) { @@ -227,6 +233,8 @@ function checkUpdatedConfig (data: CustomConfig) { expect(data.import.videos.concurrency).to.equal(4) expect(data.import.videos.http.enabled).to.be.false expect(data.import.videos.torrent.enabled).to.be.false + expect(data.import.videoChannelSynchronization.enabled).to.be.false + expect(data.import.users.enabled).to.be.false expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true expect(data.followers.instance.enabled).to.be.false @@ -242,6 +250,10 @@ function checkUpdatedConfig (data: CustomConfig) { expect(data.broadcastMessage.dismissable).to.be.true expect(data.storyboards.enabled).to.be.false + + expect(data.export.users.enabled).to.be.false + expect(data.export.users.exportExpiration).to.equal(43) + expect(data.export.users.maxUserVideoQuota).to.equal(42) } const newCustomConfig: CustomConfig = { @@ -415,6 +427,9 @@ const newCustomConfig: CustomConfig = { videoChannelSynchronization: { enabled: false, maxPerUser: 10 + }, + users: { + enabled: false } }, trending: { @@ -469,6 +484,13 @@ const newCustomConfig: CustomConfig = { }, storyboards: { enabled: false + }, + export: { + users: { + enabled: false, + exportExpiration: 43, + maxUserVideoQuota: 42 + } } } diff --git a/packages/tests/src/api/server/proxy.ts b/packages/tests/src/api/server/proxy.ts index c7d13f4ab..b95825187 100644 --- a/packages/tests/src/api/server/proxy.ts +++ b/packages/tests/src/api/server/proxy.ts @@ -85,7 +85,7 @@ describe('Test proxy', function () { describe('Videos import', async function () { function quickImport (expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) { - return servers[0].imports.importVideo({ + return servers[0].videoImports.importVideo({ attributes: { name: 'video import', channelId: servers[0].store.channel.id, diff --git a/packages/tests/src/api/users/index.ts b/packages/tests/src/api/users/index.ts index 830d4da62..7bda4a3e0 100644 --- a/packages/tests/src/api/users/index.ts +++ b/packages/tests/src/api/users/index.ts @@ -1,6 +1,8 @@ import './oauth.js' import './registrations`.js' import './two-factor.js' +import './user-export.js' +import './user-import.js' import './user-subscriptions.js' import './user-videos.js' import './users.js' diff --git a/packages/tests/src/api/users/user-export.ts b/packages/tests/src/api/users/user-export.ts new file mode 100644 index 000000000..a416ccd35 --- /dev/null +++ b/packages/tests/src/api/users/user-export.ts @@ -0,0 +1,746 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' +import { + cleanupTests, getRedirectionUrl, makeActivityPubRawRequest, + makeRawRequest, + ObjectStorageCommand, + PeerTubeServer, + waitJobs +} from '@peertube/peertube-server-commands' +import { expect } from 'chai' +import { + AccountExportJSON, ActivityPubActor, + ActivityPubOrderedCollection, + BlocklistExportJSON, + ChannelExportJSON, + CommentsExportJSON, + DislikesExportJSON, + FollowersExportJSON, + FollowingExportJSON, + HttpStatusCode, + LikesExportJSON, + UserExportState, + UserNotificationSettingValue, + UserSettingsExportJSON, + VideoCommentObject, + VideoCreateResult, + VideoExportJSON, VideoPlaylistCreateResult, + VideoPlaylistPrivacy, + VideoPlaylistsExportJSON, + VideoPlaylistType, + VideoPrivacy +} from '@peertube/peertube-models' +import { + checkExportFileExists, + checkFileExistsInZIP, + downloadZIP, + findVideoObjectInOutbox, + parseAPOutbox, + parseZIPJSONFile, + prepareImportExportTests, + regenerateExport +} from '@tests/shared/import-export.js' +import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' +import { wait } from '@peertube/peertube-core-utils' + +function runTest (withObjectStorage: boolean) { + let server: PeerTubeServer + let remoteServer: PeerTubeServer + + let noahToken: string + + let rootId: number + let noahId: number + let remoteRootId: number + + const emails: object[] = [] + + let externalVideo: VideoCreateResult + let noahPrivateVideo: VideoCreateResult + let noahVideo: VideoCreateResult + let mouskaVideo: VideoCreateResult + + let noahPlaylist: VideoPlaylistCreateResult + + let noahExportId: number + + before(async function () { + this.timeout(240000) + + const objectStorage = withObjectStorage + ? new ObjectStorageCommand() + : undefined; + + ({ + rootId, + noahId, + remoteRootId, + noahPlaylist, + externalVideo, + noahPrivateVideo, + mouskaVideo, + noahVideo, + noahToken, + server, + remoteServer + } = await prepareImportExportTests({ emails, objectStorage, withBlockedServer: false })) + }) + + it('Should export root account', async function () { + this.timeout(60000) + + { + const { data, total } = await server.userExports.list({ userId: rootId }) + expect(total).to.equal(0) + expect(data).to.have.lengthOf(0) + } + + const beforeRequest = new Date() + await server.userExports.request({ userId: rootId, withVideoFiles: false }) + const afterRequest = new Date() + + { + const { data, total } = await server.userExports.list({ userId: rootId }) + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) + + expect(data[0].id).to.exist + expect(new Date(data[0].createdAt)).to.be.greaterThan(beforeRequest) + expect(new Date(data[0].createdAt)).to.be.below(afterRequest) + + await server.userExports.waitForCreation({ userId: rootId }) + } + + { + const { data, total } = await server.userExports.list({ userId: rootId }) + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) + + expect(data[0].privateDownloadUrl).to.exist + expect(data[0].size).to.be.greaterThan(0) + expect(data[0].state.id).to.equal(UserExportState.COMPLETED) + expect(data[0].state.label).to.equal('Completed') + } + + await waitJobs([ server ]) + }) + + it('Should have received an email on archive creation', async function () { + const email = emails.find(e => { + return e['to'][0]['address'] === 'admin' + server.internalServerNumber + '@example.com' && + e['subject'].includes('export archive has been created') + }) + + expect(email).to.exist + + expect(email['text']).to.contain('has been created') + expect(email['text']).to.contain(server.url + '/my-account/import-export') + }) + + it('Should have a valid ZIP for root account', async function () { + this.timeout(120000) + + const zip = await downloadZIP(server, rootId) + + const files = [ + 'activity-pub/actor.json', + 'activity-pub/dislikes.json', + 'activity-pub/following.json', + 'activity-pub/likes.json', + 'activity-pub/outbox.json', + + 'peertube/account.json', + 'peertube/blocklist.json', + 'peertube/channels.json', + 'peertube/comments.json', + 'peertube/dislikes.json', + 'peertube/follower.json', + 'peertube/following.json', + 'peertube/likes.json', + 'peertube/user-settings.json', + 'peertube/video-playlists.json', + 'peertube/videos.json' + ] + + for (const file of files) { + expect(zip.files[file]).to.exist + + const string = await zip.file(file).async('string') + expect(string).to.have.length.greaterThan(0) + + expect(JSON.parse(string)).to.not.throw + } + + const filepaths = Object.keys(zip.files) + const staticFilepaths = filepaths.filter(p => p.startsWith('files/')) + expect(staticFilepaths).to.have.lengthOf(0) + }) + + it('Should export Noah account', async function () { + this.timeout(120000) + + await server.userExports.request({ userId: noahId, withVideoFiles: true }) + await server.userExports.waitForCreation({ userId: noahId }) + + const zip = await downloadZIP(server, noahId) + + for (const file of Object.keys(zip.files)) { + await checkFileExistsInZIP(zip, file) + } + }) + + it('Should have a valid ActivityPub export', async function () { + this.timeout(120000) + + const zip = await downloadZIP(server, noahId) + + { + const actor = await parseZIPJSONFile(zip, 'activity-pub/actor.json') + + expect(actor['@context']).to.exist + expect(actor.type).to.equal('Person') + expect(actor.id).to.equal(server.url + '/accounts/noah') + expect(actor.following).to.equal('following.json') + expect(actor.outbox).to.equal('outbox.json') + expect(actor.preferredUsername).to.equal('noah') + expect(actor.publicKey).to.exist + + expect(actor.icon).to.have.lengthOf(0) + + expect(actor.likes).to.equal('likes.json') + expect(actor.dislikes).to.equal('dislikes.json') + } + + { + const dislikes = await parseZIPJSONFile>(zip, 'activity-pub/dislikes.json') + expect(dislikes['@context']).to.exist + expect(dislikes.id).to.equal('dislikes.json') + expect(dislikes.type).to.equal('OrderedCollection') + expect(dislikes.totalItems).to.equal(1) + expect(dislikes.orderedItems).to.have.lengthOf(1) + expect(dislikes.orderedItems[0]).to.equal(remoteServer.url + '/videos/watch/' + externalVideo.uuid) + } + + { + const likes = await parseZIPJSONFile>(zip, 'activity-pub/likes.json') + expect(likes['@context']).to.exist + expect(likes.id).to.equal('likes.json') + expect(likes.type).to.equal('OrderedCollection') + expect(likes.totalItems).to.equal(2) + expect(likes.orderedItems).to.have.lengthOf(2) + expect(likes.orderedItems.find(i => i === server.url + '/videos/watch/' + noahVideo.uuid)).to.exist + } + + { + const following = await parseZIPJSONFile>(zip, 'activity-pub/following.json') + expect(following['@context']).to.exist + expect(following.id).to.equal('following.json') + expect(following.type).to.equal('OrderedCollection') + expect(following.totalItems).to.equal(2) + expect(following.orderedItems).to.have.lengthOf(2) + expect(following.orderedItems.find(i => i === remoteServer.url + '/video-channels/root_channel')).to.exist + } + + { + const outbox = await parseAPOutbox(zip) + expect(outbox['@context']).to.exist + expect(outbox.id).to.equal('outbox.json') + expect(outbox.type).to.equal('OrderedCollection') + + // 3 videos and 2 comments + expect(outbox.totalItems).to.equal(5) + expect(outbox.orderedItems).to.have.lengthOf(5) + + expect(outbox.orderedItems.filter(i => i.object.type === 'Video')).to.have.lengthOf(3) + expect(outbox.orderedItems.filter(i => i.object.type === 'Note')).to.have.lengthOf(2) + + const { object: video } = findVideoObjectInOutbox(outbox, 'noah public video') + + // Thumbnail + expect(video.icon).to.have.lengthOf(1) + expect(video.icon[0].url).to.equal('../files/videos/thumbnails/' + noahVideo.uuid + '.jpg') + + await checkFileExistsInZIP(zip, video.icon[0].url, '/activity-pub') + + // Subtitles + expect(video.subtitleLanguage).to.have.lengthOf(2) + for (const subtitle of video.subtitleLanguage) { + await checkFileExistsInZIP(zip, subtitle.url, '/activity-pub') + } + + expect(video.attachment).to.have.lengthOf(1) + expect(video.attachment[0].url).to.equal('../files/videos/video-files/' + noahVideo.uuid + '.webm') + await checkFileExistsInZIP(zip, video.attachment[0].url, '/activity-pub') + } + }) + + it('Should have a valid export in PeerTube format', async function () { + this.timeout(120000) + + const zip = await downloadZIP(server, noahId) + + { + const json = await parseZIPJSONFile(zip, 'peertube/blocklist.json') + + expect(json.instances).to.have.lengthOf(0) + expect(json.actors).to.have.lengthOf(0) + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/follower.json') + expect(json.followers).to.have.lengthOf(2) + + const follower = json.followers.find(f => { + return f.handle === 'root@' + remoteServer.host + }) + + expect(follower).to.exist + expect(follower.targetHandle).to.equal('noah_channel@' + server.host) + expect(follower.createdAt).to.exist + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/following.json') + expect(json.following).to.have.lengthOf(2) + + const following = json.following.find(f => { + return f.targetHandle === 'mouska_channel@' + server.host + }) + + expect(following).to.exist + expect(following.handle).to.equal('noah@' + server.host) + expect(following.createdAt).to.exist + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/likes.json') + expect(json.likes).to.have.lengthOf(2) + + const like = json.likes.find(l => { + return l.videoUrl === server.url + '/videos/watch/' + mouskaVideo.uuid + }) + expect(like).to.exist + expect(like.createdAt).to.exist + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/dislikes.json') + expect(json.dislikes).to.have.lengthOf(1) + + const dislike = json.dislikes.find(l => { + return l.videoUrl === remoteServer.url + '/videos/watch/' + externalVideo.uuid + }) + expect(dislike).to.exist + expect(dislike.createdAt).to.exist + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/user-settings.json') + expect(json.email).to.equal('noah@example.com') + expect(json.p2pEnabled).to.be.false + expect(json.notificationSettings.myVideoPublished).to.equal(UserNotificationSettingValue.NONE) + expect(json.notificationSettings.commentMention).to.equal(UserNotificationSettingValue.EMAIL) + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/account.json') + expect(json.displayName).to.equal('noah') + expect(json.description).to.equal('super noah description') + expect(json.name).to.equal('noah') + expect(json.avatars).to.have.lengthOf(0) + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/video-playlists.json') + + expect(json.videoPlaylists).to.have.lengthOf(3) + + // Watch later + { + expect(json.videoPlaylists.find(p => p.type === VideoPlaylistType.WATCH_LATER)).to.exist + } + + { + const playlist1 = json.videoPlaylists.find(p => p.displayName === 'noah playlist 1') + expect(playlist1.privacy).to.equal(VideoPlaylistPrivacy.PUBLIC) + expect(playlist1.channel.name).to.equal('noah_channel') + expect(playlist1.elements).to.have.lengthOf(3) + expect(playlist1.type).to.equal(VideoPlaylistType.REGULAR) + + await makeRawRequest({ url: playlist1.thumbnailUrl, expectedStatus: HttpStatusCode.OK_200 }) + + expect(playlist1.elements.find(e => e.videoUrl === server.url + '/videos/watch/' + mouskaVideo.uuid)).to.exist + expect(playlist1.elements.find(e => e.videoUrl === server.url + '/videos/watch/' + noahPrivateVideo.uuid)).to.exist + } + + { + const playlist2 = json.videoPlaylists.find(p => p.displayName === 'noah playlist 2') + expect(playlist2.privacy).to.equal(VideoPlaylistPrivacy.PRIVATE) + expect(playlist2.channel.name).to.not.exist + expect(playlist2.elements).to.have.lengthOf(0) + expect(playlist2.type).to.equal(VideoPlaylistType.REGULAR) + expect(playlist2.thumbnailUrl).to.not.exist + } + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/channels.json') + + expect(json.channels).to.have.lengthOf(2) + + { + const mainChannel = json.channels.find(c => c.name === 'noah_channel') + expect(mainChannel.displayName).to.equal('Main noah channel') + expect(mainChannel.avatars).to.have.lengthOf(0) + expect(mainChannel.banners).to.have.lengthOf(0) + } + + { + const secondaryChannel = json.channels.find(c => c.name === 'noah_second_channel') + expect(secondaryChannel.displayName).to.equal('noah display name') + expect(secondaryChannel.description).to.equal('noah description') + expect(secondaryChannel.support).to.equal('noah support') + + expect(secondaryChannel.avatars).to.have.lengthOf(2) + expect(secondaryChannel.banners).to.have.lengthOf(1) + + const urls = [ ...secondaryChannel.avatars, ...secondaryChannel.banners ].map(a => a.url) + for (const url of urls) { + await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) + } + } + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/comments.json') + + expect(json.comments).to.have.lengthOf(2) + + { + const thread = json.comments.find(c => c.text === 'noah comment') + + expect(thread.videoUrl).to.equal(server.url + '/videos/watch/' + mouskaVideo.uuid) + expect(thread.inReplyToCommentUrl).to.not.exist + } + + { + const reply = json.comments.find(c => c.text === 'noah reply') + + expect(reply.videoUrl).to.equal(server.url + '/videos/watch/' + noahVideo.uuid) + expect(reply.inReplyToCommentUrl).to.exist + + const { body } = await makeActivityPubRawRequest(reply.inReplyToCommentUrl) + expect((body as VideoCommentObject).content).to.equal('local comment') + } + } + + { + const json = await parseZIPJSONFile(zip, 'peertube/videos.json') + + expect(json.videos).to.have.lengthOf(3) + + { + const privateVideo = json.videos.find(v => v.name === 'noah private video') + expect(privateVideo).to.exist + + expect(privateVideo.channel.name).to.equal('noah_channel') + expect(privateVideo.privacy).to.equal(VideoPrivacy.PRIVATE) + + expect(privateVideo.captions).to.have.lengthOf(0) + } + + { + const publicVideo = json.videos.find(v => v.name === 'noah public video') + expect(publicVideo).to.exist + + expect(publicVideo.channel.name).to.equal('noah_channel') + expect(publicVideo.privacy).to.equal(VideoPrivacy.PUBLIC) + + expect(publicVideo.files).to.have.lengthOf(1) + expect(publicVideo.streamingPlaylists).to.have.lengthOf(0) + + expect(publicVideo.captions).to.have.lengthOf(2) + + expect(publicVideo.captions.find(c => c.language === 'ar')).to.exist + expect(publicVideo.captions.find(c => c.language === 'fr')).to.exist + + const urls = [ + ...publicVideo.captions.map(c => c.fileUrl), + ...publicVideo.files.map(f => f.fileUrl), + publicVideo.thumbnailUrl + ] + + for (const url of urls) { + await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) + } + } + + { + const secondaryChannelVideo = json.videos.find(v => v.name === 'noah public video second channel') + expect(secondaryChannelVideo.channel.name).to.equal('noah_second_channel') + } + } + }) + + it('Should have a valid export of static files', async function () { + this.timeout(60000) + + const zip = await downloadZIP(server, noahId) + const files = Object.keys(zip.files) + + { + expect(zip.files['files/account/avatars/noah.jpg']).to.not.exist + } + + { + const playlistFiles = files.filter(f => f.startsWith('files/video-playlists/thumbnails/')) + expect(playlistFiles).to.have.lengthOf(1) + + await checkFileExistsInZIP(zip, 'files/video-playlists/thumbnails/' + noahPlaylist.uuid + '.jpg') + } + + { + const channelAvatarFiles = files.filter(f => f.startsWith('files/channels/avatars/')) + expect(channelAvatarFiles).to.have.lengthOf(1) + + const channelBannerFiles = files.filter(f => f.startsWith('files/channels/banners/')) + expect(channelBannerFiles).to.have.lengthOf(1) + + await checkFileExistsInZIP(zip, 'files/channels/avatars/noah_second_channel.png') + await checkFileExistsInZIP(zip, 'files/channels/banners/noah_second_channel.jpg') + } + + { + const videoThumbnails = files.filter(f => f.startsWith('files/videos/thumbnails/')) + expect(videoThumbnails).to.have.lengthOf(3) + + const videoFiles = files.filter(f => f.startsWith('files/videos/video-files/')) + expect(videoFiles).to.have.lengthOf(3) + + await checkFileExistsInZIP(zip, 'files/videos/thumbnails/' + noahPrivateVideo.uuid + '.jpg') + await checkFileExistsInZIP(zip, 'files/videos/video-files/' + noahPrivateVideo.uuid + '.webm') + } + }) + + it('Should not export Noah videos', async function () { + this.timeout(60000) + + await regenerateExport({ server, userId: noahId, withVideoFiles: false }) + + const zip = await downloadZIP(server, noahId) + + { + const outbox = await parseAPOutbox(zip) + const { object: video } = findVideoObjectInOutbox(outbox, 'noah public video') + + expect(video.attachment).to.not.exist + } + + { + const files = Object.keys(zip.files) + + const videoFiles = files.filter(f => f.startsWith('files/videos/video-files/')) + expect(videoFiles).to.have.lengthOf(0) + } + }) + + it('Should update my avatar and include it in the archive', async function () { + this.timeout(60000) + + await server.users.updateMyAvatar({ token: noahToken, fixture: 'avatar.png' }) + + await regenerateExport({ server, userId: noahId, withVideoFiles: false }) + + const zip = await downloadZIP(server, noahId) + + // AP + { + const actor = await parseZIPJSONFile(zip, 'activity-pub/actor.json') + + expect(actor.icon).to.have.lengthOf(1) + + await checkFileExistsInZIP(zip, actor.icon[0].url, '/activity-pub') + } + + // PeerTube format + { + const json = await parseZIPJSONFile(zip, 'peertube/account.json') + expect(json.avatars).to.have.lengthOf(2) + + for (const avatar of json.avatars) { + await makeRawRequest({ url: avatar.url, expectedStatus: HttpStatusCode.OK_200 }) + } + } + + { + await checkFileExistsInZIP(zip, 'files/account/avatars/noah.png') + } + }) + + it('Should add account and server in blocklist and include it in the archive', async function () { + this.timeout(60000) + + const blocks = [ + { account: 'root' }, + { account: 'root@' + remoteServer.host }, + { server: remoteServer.host } + ] + + for (const toBlock of blocks) { + await server.blocklist.addToMyBlocklist({ token: noahToken, ...toBlock }) + } + + const { export: { id } } = await regenerateExport({ server, userId: noahId, withVideoFiles: false }) + noahExportId = id + + const zip = await downloadZIP(server, noahId) + const json = await parseZIPJSONFile(zip, 'peertube/blocklist.json') + + expect(json.instances).to.have.lengthOf(1) + expect(json.instances[0].host).to.equal(remoteServer.host) + + expect(json.actors).to.have.lengthOf(2) + expect(json.actors.find(a => a.handle === 'root@' + server.host)).to.exist + expect(json.actors.find(a => a.handle === 'root@' + remoteServer.host)).to.exist + + for (const toBlock of blocks) { + await server.blocklist.removeFromMyBlocklist({ token: noahToken, ...toBlock }) + } + }) + + it('Should export videos on instance with transcoding enabled', async function () { + await regenerateExport({ server: remoteServer, userId: remoteRootId, withVideoFiles: true }) + + const zip = await downloadZIP(remoteServer, remoteRootId) + + { + const json = await parseZIPJSONFile(zip, 'peertube/videos.json') + + expect(json.videos).to.have.lengthOf(1) + const video = json.videos[0] + + expect(video.files).to.have.lengthOf(4) + expect(video.streamingPlaylists).to.have.lengthOf(1) + expect(video.streamingPlaylists[0].files).to.have.lengthOf(4) + } + + { + const outbox = await parseAPOutbox(zip) + const { object: video } = findVideoObjectInOutbox(outbox, 'external video') + + expect(video.attachment).to.have.lengthOf(1) + expect(video.attachment[0].url).to.equal('../files/videos/video-files/' + externalVideo.uuid + '.mp4') + await checkFileExistsInZIP(zip, video.attachment[0].url, '/activity-pub') + } + }) + + it('Should delete the export and clean up the disk', async function () { + const { data, total } = await server.userExports.list({ userId: noahId }) + expect(data).to.have.lengthOf(1) + expect(total).to.equal(1) + + const userExport = data[0] + const redirectedUrl = withObjectStorage + ? await getRedirectionUrl(userExport.privateDownloadUrl) + : undefined + + await checkExportFileExists({ exists: true, server, userExport, redirectedUrl, withObjectStorage }) + + await server.userExports.delete({ userId: noahId, exportId: noahExportId, token: noahToken }) + + { + const { data, total } = await server.userExports.list({ userId: noahId }) + expect(data).to.have.lengthOf(0) + expect(total).to.equal(0) + + await checkExportFileExists({ exists: false, server, userExport, redirectedUrl, withObjectStorage }) + } + }) + + it('Should remove the user and cleanup the disk', async function () { + this.timeout(60000) + + const { token, userId } = await server.users.generate('to_delete') + await server.userExports.request({ userId, token, withVideoFiles: false }) + await server.userExports.waitForCreation({ userId, token }) + + const { data } = await server.userExports.list({ userId }) + + const userExport = data[0] + const redirectedUrl = withObjectStorage + ? await getRedirectionUrl(userExport.privateDownloadUrl) + : undefined + + await checkExportFileExists({ exists: true, server, userExport, redirectedUrl, withObjectStorage }) + + await server.users.remove({ userId }) + + await checkExportFileExists({ exists: false, server, userExport, redirectedUrl, withObjectStorage }) + }) + + it('Should expire old archives', async function () { + this.timeout(60000) + + await server.userExports.request({ userId: noahId, withVideoFiles: true }) + await server.userExports.waitForCreation({ userId: noahId }) + + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + + const { data } = await server.userExports.list({ userId: noahId }) + expect(new Date(data[0].expiresOn)).to.be.greaterThan(tomorrow) + + const userExport = data[0] + const redirectedUrl = withObjectStorage + ? await getRedirectionUrl(userExport.privateDownloadUrl) + : undefined + + await checkExportFileExists({ exists: true, server, userExport, withObjectStorage, redirectedUrl }) + + await server.config.updateCustomSubConfig({ + newConfig: { + export: { + users: { + exportExpiration: 1000 + } + } + } + }) + + await server.debug.sendCommand({ + body: { + command: 'remove-expired-user-exports' + } + }) + + // File deletion + await wait(500) + + { + const { data } = await server.userExports.list({ userId: noahId }) + expect(data).to.have.lengthOf(0) + + await checkExportFileExists({ exists: false, server, userExport, withObjectStorage, redirectedUrl }) + } + }) + + after(async function () { + MockSmtpServer.Instance.kill() + + await cleanupTests([ server, remoteServer ]) + }) +} + +describe('Test user export', function () { + + describe('From filesystem', function () { + runTest(false) + }) + + describe('From object storage', function () { + if (areMockObjectStorageTestsDisabled()) return + + runTest(true) + }) +}) diff --git a/packages/tests/src/api/users/user-import.ts b/packages/tests/src/api/users/user-import.ts new file mode 100644 index 000000000..7ad65a41a --- /dev/null +++ b/packages/tests/src/api/users/user-import.ts @@ -0,0 +1,556 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { MockSmtpServer } from '@tests/shared/mock-servers/index.js' +import { + cleanupTests, makeRawRequest, + ObjectStorageCommand, + PeerTubeServer, waitJobs +} from '@peertube/peertube-server-commands' +import { + HttpStatusCode, + UserImportState, + UserNotificationSettingValue, + VideoCreateResult, + VideoPlaylistPrivacy, + VideoPlaylistType, + VideoPrivacy +} from '@peertube/peertube-models' +import { prepareImportExportTests } from '@tests/shared/import-export.js' +import { areMockObjectStorageTestsDisabled } from '@peertube/peertube-node-utils' +import { writeFile } from 'fs/promises' +import { join } from 'path' +import { expect } from 'chai' +import { testImage, testImageSize } from '@tests/shared/checks.js' +import { completeVideoCheck } from '@tests/shared/videos.js' +import { completeCheckHlsPlaylist } from '@tests/shared/streaming-playlists.js' + +function runTest (withObjectStorage: boolean) { + let server: PeerTubeServer + let remoteServer: PeerTubeServer + let blockedServer: PeerTubeServer + + let noahToken: string + + let noahId: number + + const emails: object[] = [] + + let externalVideo: VideoCreateResult + let noahVideo: VideoCreateResult + let mouskaVideo: VideoCreateResult + + let remoteNoahToken: string + let remoteNoahId: number + + let archivePath: string + + let objectStorage: ObjectStorageCommand + + let latestImportId: number + + before(async function () { + this.timeout(240000) + + objectStorage = withObjectStorage + ? new ObjectStorageCommand() + : undefined; + + ({ + noahId, + externalVideo, + noahVideo, + noahToken, + server, + remoteNoahId, + remoteNoahToken, + remoteServer, + mouskaVideo, + blockedServer + } = await prepareImportExportTests({ emails, objectStorage, withBlockedServer: true })) + + await blockedServer.videos.quickUpload({ name: 'blocked video' }) + await waitJobs([ blockedServer ]) + + // Also add some blocks + const blocks = [ + { account: 'mouska' }, + { account: 'root@' + blockedServer.host }, + { server: blockedServer.host } + ] + + for (const toBlock of blocks) { + await server.blocklist.addToMyBlocklist({ token: noahToken, ...toBlock }) + } + + // Add avatars + await server.users.updateMyAvatar({ token: noahToken, fixture: 'avatar.gif' }) + + // Add password protected video + await server.videos.upload({ + token: noahToken, + attributes: { + name: 'noah password video', + privacy: VideoPrivacy.PASSWORD_PROTECTED, + videoPasswords: [ 'password1', 'password2' ] + } + }) + + // Add a video in watch later playlist + const { data: playlists } = await server.playlists.listByAccount({ + token: noahToken, + handle: 'noah', + playlistType: VideoPlaylistType.WATCH_LATER + }) + + await server.playlists.addElement({ + playlistId: playlists[0].id, + attributes: { videoId: noahVideo.uuid } + }) + + await waitJobs([ server, remoteServer, blockedServer ]) + + // --------------------------------------------------------------------------- + + await server.userExports.request({ userId: noahId, withVideoFiles: true }) + await server.userExports.waitForCreation({ userId: noahId }) + + const { data } = await server.userExports.list({ userId: noahId }) + + const res = await makeRawRequest({ + url: data[0].privateDownloadUrl, + responseType: 'arraybuffer', + redirects: 1, + expectedStatus: HttpStatusCode.OK_200 + }) + + archivePath = join(server.getDirectoryPath('tmp'), 'archive.zip') + await writeFile(archivePath, res.body) + }) + + it('Should import an archive with video files', async function () { + this.timeout(240000) + + const { userImport } = await remoteServer.userImports.importArchive({ fixture: archivePath, userId: remoteNoahId }) + latestImportId = userImport.id + + await waitJobs([ server, remoteServer ]) + }) + + it('Should have a valid import status', async function () { + const userImport = await remoteServer.userImports.getLatestImport({ userId: remoteNoahId, token: remoteNoahToken }) + + expect(userImport.id).to.equal(latestImportId) + expect(userImport.state.id).to.equal(UserImportState.COMPLETED) + expect(userImport.state.label).to.equal('Completed') + }) + + it('Should have correctly imported blocklist', async function () { + { + const { data } = await remoteServer.blocklist.listMyAccountBlocklist({ start: 0, count: 5, token: remoteNoahToken }) + + expect(data).to.have.lengthOf(2) + expect(data.find(a => a.blockedAccount.host === server.host && a.blockedAccount.name === 'mouska')).to.exist + expect(data.find(a => a.blockedAccount.host === blockedServer.host && a.blockedAccount.name === 'root')).to.exist + } + + { + const { data } = await remoteServer.blocklist.listMyServerBlocklist({ start: 0, count: 5, token: remoteNoahToken }) + + expect(data).to.have.lengthOf(1) + expect(data.find(a => a.blockedServer.host === blockedServer.host)).to.exist + } + }) + + it('Should have correctly imported account', async function () { + const me = await remoteServer.users.getMyInfo({ token: remoteNoahToken }) + + expect(me.account.displayName).to.equal('noah') + expect(me.username).to.equal('noah_remote') + expect(me.account.description).to.equal('super noah description') + + for (const avatar of me.account.avatars) { + await testImageSize(remoteServer.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.gif') + } + }) + + it('Should have correctly imported user settings', async function () { + { + const me = await remoteServer.users.getMyInfo({ token: remoteNoahToken }) + + expect(me.p2pEnabled).to.be.false + + const settings = me.notificationSettings + + expect(settings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL) + expect(settings.myVideoPublished).to.equal(UserNotificationSettingValue.NONE) + expect(settings.commentMention).to.equal(UserNotificationSettingValue.EMAIL) + } + }) + + it('Should have correctly imported channels', async function () { + const { data: channels } = await remoteServer.channels.listByAccount({ token: remoteNoahToken, accountName: 'noah_remote' }) + + // One default + 2 imported + expect(channels).to.have.lengthOf(3) + + await remoteServer.channels.get({ token: remoteNoahToken, channelName: 'noah_remote_channel' }) + + const importedMain = await remoteServer.channels.get({ token: remoteNoahToken, channelName: 'noah_channel' }) + expect(importedMain.displayName).to.equal('Main noah channel') + expect(importedMain.avatars).to.have.lengthOf(0) + expect(importedMain.banners).to.have.lengthOf(0) + + const importedSecond = await remoteServer.channels.get({ token: remoteNoahToken, channelName: 'noah_second_channel' }) + expect(importedSecond.displayName).to.equal('noah display name') + expect(importedSecond.description).to.equal('noah description') + expect(importedSecond.support).to.equal('noah support') + + await testImage(remoteServer.url, 'banner-resized', importedSecond.banners[0].path) + + for (const avatar of importedSecond.avatars) { + await testImage(remoteServer.url, `avatar-resized-${avatar.width}x${avatar.width}`, avatar.path, '.png') + } + + { + // Also check the correct count on origin server + const { data: channels } = await server.channels.listByAccount({ accountName: 'noah_remote@' + remoteServer.host }) + expect(channels).to.have.lengthOf(2) // noah_remote_channel doesn't have videos so it has not been federated + } + }) + + it('Should have correctly imported following', async function () { + const { data } = await remoteServer.subscriptions.list({ token: remoteNoahToken }) + + expect(data).to.have.lengthOf(2) + expect(data.find(f => f.name === 'mouska_channel' && f.host === server.host)).to.exist + expect(data.find(f => f.name === 'root_channel' && f.host === remoteServer.host)).to.exist + }) + + it('Should not have reimported followers (it is not a migration)', async function () { + for (const checkServer of [ server, remoteServer ]) { + const { data } = await checkServer.channels.listFollowers({ channelName: 'noah_channel@' + remoteServer.host }) + + expect(data).to.have.lengthOf(0) + } + }) + + it('Should not have imported comments (it is not a migration)', async function () { + for (const checkServer of [ server, remoteServer ]) { + { + const threads = await checkServer.comments.listThreads({ videoId: noahVideo.uuid }) + expect(threads.total).to.equal(2) + } + + { + const threads = await checkServer.comments.listThreads({ videoId: mouskaVideo.uuid }) + expect(threads.total).to.equal(1) + } + } + }) + + it('Should have correctly imported likes/dislikes', async function () { + { + const { rating } = await remoteServer.users.getMyRating({ videoId: mouskaVideo.uuid, token: remoteNoahToken }) + expect(rating).to.equal('like') + + for (const checkServer of [ server, remoteServer ]) { + const video = await checkServer.videos.get({ id: mouskaVideo.uuid }) + expect(video.likes).to.equal(2) // Old account + new account rates + expect(video.dislikes).to.equal(0) + } + } + + { + const { rating } = await remoteServer.users.getMyRating({ videoId: noahVideo.uuid, token: remoteNoahToken }) + expect(rating).to.equal('like') + } + + { + const { rating } = await remoteServer.users.getMyRating({ videoId: externalVideo.uuid, token: remoteNoahToken }) + expect(rating).to.equal('dislike') + } + }) + + it('Should have correctly imported user video playlists', async function () { + const { data } = await remoteServer.playlists.listByAccount({ handle: 'noah_remote', token: remoteNoahToken }) + + // Should merge the watch later playlists + expect(data).to.have.lengthOf(3) + + { + const watchLater = data.find(p => p.type.id === VideoPlaylistType.WATCH_LATER) + expect(watchLater).to.exist + expect(watchLater.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) + + // Playlists were merged + expect(watchLater.videosLength).to.equal(1) + + const { data: videos } = await remoteServer.playlists.listVideos({ playlistId: watchLater.id, token: remoteNoahToken }) + expect(videos[0].position).to.equal(1) + expect(videos[0].video.uuid).to.equal(noahVideo.uuid) + + // Not federated + await server.playlists.get({ playlistId: watchLater.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } + + { + const playlist1 = data.find(p => p.displayName === 'noah playlist 1') + expect(playlist1).to.exist + + expect(playlist1.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) + expect(playlist1.videosLength).to.equal(2) // 1 private video could not be imported + + const { data: videos } = await remoteServer.playlists.listVideos({ playlistId: playlist1.id, token: remoteNoahToken }) + expect(videos[0].position).to.equal(1) + expect(videos[0].startTimestamp).to.equal(2) + expect(videos[0].stopTimestamp).to.equal(3) + expect(videos[0].video).to.not.exist // Mouska is blocked + + expect(videos[1].position).to.equal(2) + expect(videos[1].video.uuid).to.equal(noahVideo.uuid) + + // Federated + await server.playlists.get({ playlistId: playlist1.uuid }) + } + + { + const playlist2 = data.find(p => p.displayName === 'noah playlist 2') + expect(playlist2).to.exist + + expect(playlist2.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) + expect(playlist2.videosLength).to.equal(0) + + // Federated + await server.playlists.get({ playlistId: playlist2.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } + }) + + it('Should have correctly imported user videos', async function () { + const { data } = await remoteServer.videos.listMyVideos({ token: remoteNoahToken }) + expect(data).to.have.lengthOf(4) + + { + const privateVideo = data.find(v => v.name === 'noah private video') + expect(privateVideo).to.exist + expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE) + + // Not federated + await server.videos.get({ id: privateVideo.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } + + { + const publicVideo = data.find(v => v.name === 'noah public video') + expect(publicVideo).to.exist + expect(publicVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) + + // Federated + await server.videos.get({ id: publicVideo.uuid }) + } + + { + const passwordVideo = data.find(v => v.name === 'noah password video') + expect(passwordVideo).to.exist + expect(passwordVideo.privacy.id).to.equal(VideoPrivacy.PASSWORD_PROTECTED) + + const { data: passwords } = await remoteServer.videoPasswords.list({ videoId: passwordVideo.uuid }) + expect(passwords.map(p => p.password).sort()).to.deep.equal([ 'password1', 'password2' ]) + + // Not federated + await server.videos.get({ id: passwordVideo.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } + + { + const otherVideo = data.find(v => v.name === 'noah public video second channel') + expect(otherVideo).to.exist + + for (const checkServer of [ server, remoteServer ]) { + await completeVideoCheck({ + server: checkServer, + originServer: remoteServer, + videoUUID: otherVideo.uuid, + objectStorageBaseUrl: objectStorage?.getMockWebVideosBaseUrl(), + + attributes: { + name: 'noah public video second channel', + privacy: (VideoPrivacy.PUBLIC), + category: (12), + tags: [ 'tag1', 'tag2' ], + commentsEnabled: false, + downloadEnabled: false, + nsfw: false, + description: ('video description'), + support: ('video support'), + language: 'fr', + licence: 1, + originallyPublishedAt: new Date(0).toISOString(), + account: { + name: 'noah_remote', + host: remoteServer.host + }, + isLocal: checkServer === remoteServer, + likes: 0, + dislikes: 0, + duration: 5, + channel: { + displayName: 'noah display name', + name: 'noah_second_channel', + description: 'noah description', + isLocal: checkServer === remoteServer + }, + fixture: 'video_short.webm', + files: [ + { + resolution: 720, + size: 61000 + }, + { + resolution: 480, + size: 40000 + }, + { + resolution: 360, + size: 32000 + }, + { + resolution: 240, + size: 23000 + } + ], + thumbnailfile: 'custom-thumbnail-from-preview', + previewfile: 'custom-preview' + } + }) + } + + await completeCheckHlsPlaylist({ + hlsOnly: false, + servers: [ remoteServer, server ], + videoUUID: otherVideo.uuid, + objectStorageBaseUrl: objectStorage?.getMockPlaylistBaseUrl(), + resolutions: [ 720, 480, 360, 240 ] + }) + + const source = await remoteServer.videos.getSource({ id: otherVideo.uuid }) + expect(source.filename).to.equal('video_short.webm') + } + }) + + it('Should re-import the same file', async function () { + this.timeout(240000) + + const { userImport } = await remoteServer.userImports.importArchive({ fixture: archivePath, userId: remoteNoahId }) + await waitJobs([ remoteServer ]) + latestImportId = userImport.id + }) + + it('Should have the status of this new reimport', async function () { + const userImport = await remoteServer.userImports.getLatestImport({ userId: remoteNoahId, token: remoteNoahToken }) + + expect(userImport.id).to.equal(latestImportId) + expect(userImport.state.id).to.equal(UserImportState.COMPLETED) + expect(userImport.state.label).to.equal('Completed') + }) + + it('Should not have duplicated data', async function () { + // Blocklist + { + { + const { data } = await remoteServer.blocklist.listMyAccountBlocklist({ start: 0, count: 5, token: remoteNoahToken }) + expect(data).to.have.lengthOf(2) + } + + { + const { data } = await remoteServer.blocklist.listMyServerBlocklist({ start: 0, count: 5, token: remoteNoahToken }) + expect(data).to.have.lengthOf(1) + } + } + + // My avatars + { + const me = await remoteServer.users.getMyInfo({ token: remoteNoahToken }) + expect(me.account.avatars).to.have.lengthOf(2) + } + + // Channels + { + const { data: channels } = await remoteServer.channels.listByAccount({ token: remoteNoahToken, accountName: 'noah_remote' }) + expect(channels).to.have.lengthOf(3) + } + + // Following + { + const { data } = await remoteServer.subscriptions.list({ token: remoteNoahToken }) + expect(data).to.have.lengthOf(2) + } + + // Likes/dislikes + { + const video = await remoteServer.videos.get({ id: mouskaVideo.uuid }) + expect(video.likes).to.equal(2) + expect(video.dislikes).to.equal(0) + + const { rating } = await remoteServer.users.getMyRating({ videoId: mouskaVideo.uuid, token: remoteNoahToken }) + expect(rating).to.equal('like') + } + + // Playlists + { + const { data } = await remoteServer.playlists.listByAccount({ handle: 'noah_remote', token: remoteNoahToken }) + expect(data).to.have.lengthOf(3) + } + + // Videos + { + const { data } = await remoteServer.videos.listMyVideos({ token: remoteNoahToken }) + expect(data).to.have.lengthOf(4) + } + }) + + it('Should have received an email on finished import', async function () { + const email = emails.reverse().find(e => { + return e['to'][0]['address'] === 'noah_remote@example.com' && + e['subject'].includes('archive import has finished') + }) + + expect(email).to.exist + expect(email['text']).to.contain('as considered duplicate: 4') // 4 videos are considered as duplicates + }) + + it('Should auto blacklist imported videos if enabled by the administrator', async function () { + this.timeout(240000) + + await blockedServer.config.enableAutoBlacklist() + + const { token, userId } = await blockedServer.users.generate('blocked_user') + await blockedServer.userImports.importArchive({ fixture: archivePath, userId, token }) + await waitJobs([ blockedServer ]) + + { + const { data } = await blockedServer.videos.listMyVideos({ token }) + expect(data).to.have.lengthOf(4) + + for (const video of data) { + expect(video.blacklisted).to.be.true + } + } + }) + + after(async function () { + MockSmtpServer.Instance.kill() + + await cleanupTests([ server, remoteServer, blockedServer ]) + }) +} + +describe('Test user import', function () { + + describe('From filesystem', function () { + runTest(false) + }) + + describe('From object storage', function () { + if (areMockObjectStorageTestsDisabled()) return + + runTest(true) + }) +}) diff --git a/packages/tests/src/api/videos/channel-import-videos.ts b/packages/tests/src/api/videos/channel-import-videos.ts index d0e47fe95..b37b68065 100644 --- a/packages/tests/src/api/videos/channel-import-videos.ts +++ b/packages/tests/src/api/videos/channel-import-videos.ts @@ -42,7 +42,7 @@ describe('Test videos import in a channel', function () { }) it('These imports should not have a sync id', async function () { - const { total, data } = await server.imports.getMyVideoImports() + const { total, data } = await server.videoImports.getMyVideoImports() expect(total).to.equal(2) expect(data).to.have.lengthOf(2) @@ -83,7 +83,7 @@ describe('Test videos import in a channel', function () { }) it('These imports should have a sync id', async function () { - const { total, data } = await server.imports.getMyVideoImports() + const { total, data } = await server.videoImports.getMyVideoImports() expect(total).to.equal(4) expect(data).to.have.lengthOf(4) @@ -98,7 +98,7 @@ describe('Test videos import in a channel', function () { }) it('Should be able to filter imports by this sync id', async function () { - const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: server.store.videoChannelSync.id }) + const { total, data } = await server.videoImports.getMyVideoImports({ videoChannelSyncId: server.store.videoChannelSync.id }) expect(total).to.equal(2) expect(data).to.have.lengthOf(2) diff --git a/packages/tests/src/api/videos/resumable-upload.ts b/packages/tests/src/api/videos/resumable-upload.ts index 9ee328c5e..e2f2adecb 100644 --- a/packages/tests/src/api/videos/resumable-upload.ts +++ b/packages/tests/src/api/videos/resumable-upload.ts @@ -42,16 +42,22 @@ describe('Test resumable upload', function () { const size = await buildSize(defaultFixture, options.size) - const attributes = { - name: 'video', - channelId: options.channelId ?? server.store.channel.id, - privacy: VideoPrivacy.PUBLIC, - fixture: defaultFixture - } - const mimetype = 'video/mp4' - const res = await server.videos.prepareResumableUpload({ path, token, attributes, size, mimetype, originalName, lastModified }) + const res = await server.videos.prepareVideoResumableUpload({ + path, + token, + fixture: defaultFixture, + fields: { + name: 'video', + channelId: options.channelId ?? server.store.channel.id, + privacy: VideoPrivacy.PUBLIC + }, + size, + mimetype, + originalName, + lastModified + }) return res.header['location'].split('?')[1] } @@ -71,7 +77,7 @@ describe('Test resumable upload', function () { const size = await buildSize(defaultFixture, options.size) const absoluteFilePath = buildAbsoluteFixturePath(defaultFixture) - return server.videos.sendResumableChunks({ + return server.videos.sendResumableVideoChunks({ token, path, pathUploadId, @@ -133,7 +139,7 @@ describe('Test resumable upload', function () { it('Should correctly delete files after an upload', async function () { const uploadId = await prepareUpload() await sendChunks({ pathUploadId: uploadId }) - await server.videos.endResumableUpload({ path, pathUploadId: uploadId }) + await server.videos.endVideoResumableUpload({ path, pathUploadId: uploadId }) expect(await countResumableUploads()).to.equal(0) }) diff --git a/packages/tests/src/api/videos/video-channel-syncs.ts b/packages/tests/src/api/videos/video-channel-syncs.ts index 54212bcb5..28f2f80ba 100644 --- a/packages/tests/src/api/videos/video-channel-syncs.ts +++ b/packages/tests/src/api/videos/video-channel-syncs.ts @@ -92,7 +92,7 @@ describe('Test channel synchronizations', function () { this.timeout(120_000) { - const { video } = await servers[0].imports.importVideo({ + const { video } = await servers[0].videoImports.importVideo({ attributes: { channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC, @@ -210,7 +210,7 @@ describe('Test channel synchronizations', function () { }) it('Should list imports of a channel synchronization', async function () { - const { total, data } = await servers[0].imports.getMyVideoImports({ videoChannelSyncId: rootChannelSyncId }) + const { total, data } = await servers[0].videoImports.getMyVideoImports({ videoChannelSyncId: rootChannelSyncId }) expect(total).to.equal(1) expect(data).to.have.lengthOf(1) diff --git a/packages/tests/src/api/videos/video-chapters.ts b/packages/tests/src/api/videos/video-chapters.ts index bed1ccef8..7e7d653f9 100644 --- a/packages/tests/src/api/videos/video-chapters.ts +++ b/packages/tests/src/api/videos/video-chapters.ts @@ -237,7 +237,7 @@ describe('Test video chapters', function () { targetUrl: FIXTURE_URLS.youtubeChapters, description: 'this is a super description\n' } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) await waitJobs(servers) @@ -277,7 +277,7 @@ describe('Test video chapters', function () { '00:03 chapter 2\n' + '00:04 chapter 3\n' } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) await waitJobs(servers) @@ -309,7 +309,7 @@ describe('Test video chapters', function () { privacy: VideoPrivacy.PUBLIC, targetUrl: FIXTURE_URLS.chatersVideo } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) await waitJobs(servers) diff --git a/packages/tests/src/api/videos/video-imports.ts b/packages/tests/src/api/videos/video-imports.ts index 3ffaa225c..6cfae80b4 100644 --- a/packages/tests/src/api/videos/video-imports.ts +++ b/packages/tests/src/api/videos/video-imports.ts @@ -118,7 +118,7 @@ describe('Test video imports', function () { { const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) expect(video.name).to.equal('small video - youtube') { @@ -174,7 +174,7 @@ describe('Test video imports', function () { description: 'this is a super torrent description', tags: [ 'tag_torrent1', 'tag_torrent2' ] } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) expect(video.name).to.equal('super peertube2 video') } @@ -185,7 +185,7 @@ describe('Test video imports', function () { description: 'this is a super torrent description', tags: [ 'tag_torrent1', 'tag_torrent2' ] } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) expect(video.name).to.equal('你好 世界 720p.mp4') } }) @@ -202,7 +202,7 @@ describe('Test video imports', function () { }) it('Should list the videos to import in my imports on server 1', async function () { - const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' }) + const { total, data: videoImports } = await servers[0].videoImports.getMyVideoImports({ sort: '-createdAt' }) expect(total).to.equal(3) expect(videoImports).to.have.lengthOf(3) @@ -224,7 +224,7 @@ describe('Test video imports', function () { }) it('Should filter my imports on target URL', async function () { - const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ targetUrl: FIXTURE_URLS.youtube }) + const { total, data: videoImports } = await servers[0].videoImports.getMyVideoImports({ targetUrl: FIXTURE_URLS.youtube }) expect(total).to.equal(1) expect(videoImports).to.have.lengthOf(1) @@ -233,7 +233,7 @@ describe('Test video imports', function () { it('Should search in my imports', async function () { { - const { total, data } = await servers[0].imports.getMyVideoImports({ search: 'peertube2' }) + const { total, data } = await servers[0].videoImports.getMyVideoImports({ search: 'peertube2' }) expect(total).to.equal(1) expect(data).to.have.lengthOf(1) @@ -242,7 +242,7 @@ describe('Test video imports', function () { } { - const { total, data } = await servers[0].imports.getMyVideoImports({ search: FIXTURE_URLS.magnet }) + const { total, data } = await servers[0].videoImports.getMyVideoImports({ search: FIXTURE_URLS.magnet }) expect(total).to.equal(1) expect(data).to.have.lengthOf(1) @@ -269,7 +269,7 @@ describe('Test video imports', function () { it('Should import a video on server 2 with some fields', async function () { this.timeout(60_000) - const { video } = await servers[1].imports.importVideo({ + const { video } = await servers[1].videoImports.importVideo({ attributes: { targetUrl: FIXTURE_URLS.youtube, channelId: servers[1].store.channel.id, @@ -312,7 +312,7 @@ describe('Test video imports', function () { channelId: servers[1].store.channel.id, privacy: VideoPrivacy.PUBLIC } - const { video } = await servers[1].imports.importVideo({ attributes }) + const { video } = await servers[1].videoImports.importVideo({ attributes }) const videoUUID = video.uuid await waitJobs(servers) @@ -354,7 +354,7 @@ describe('Test video imports', function () { channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } - const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) + const { video: videoImported } = await servers[0].videoImports.importVideo({ attributes }) const videoUUID = videoImported.uuid await waitJobs(servers) @@ -394,7 +394,7 @@ describe('Test video imports', function () { channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } - const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) + const { video: videoImported } = await servers[0].videoImports.importVideo({ attributes }) const videoUUID = videoImported.uuid await waitJobs(servers) @@ -422,7 +422,7 @@ describe('Test video imports', function () { channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } - const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) + const { video: videoImported } = await servers[0].videoImports.importVideo({ attributes }) const videoUUID = videoImported.uuid await waitJobs(servers) @@ -454,7 +454,7 @@ describe('Test video imports', function () { channelId: servers[0].store.channel.id, privacy: VideoPrivacy.PUBLIC } - const { video } = await servers[0].imports.importVideo({ attributes }) + const { video } = await servers[0].videoImports.importVideo({ attributes }) const videoUUID = video.uuid await waitJobs(servers) @@ -497,7 +497,7 @@ describe('Test video imports', function () { async function importVideo (name: string) { const attributes = { name, channelId: server.store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } - const res = await server.imports.importVideo({ attributes }) + const res = await server.videoImports.importVideo({ attributes }) return res.id } @@ -516,16 +516,16 @@ describe('Test video imports', function () { await server.jobs.pauseJobQueue() pendingImportId = await importVideo('pending') - const { data } = await server.imports.getMyVideoImports() + const { data } = await server.videoImports.getMyVideoImports() expect(data).to.have.lengthOf(2) finishedVideo = data.find(i => i.id === finishedImportId).video }) it('Should delete a video import', async function () { - await server.imports.delete({ importId: finishedImportId }) + await server.videoImports.delete({ importId: finishedImportId }) - const { data } = await server.imports.getMyVideoImports() + const { data } = await server.videoImports.getMyVideoImports() expect(data).to.have.lengthOf(1) expect(data[0].id).to.equal(pendingImportId) expect(data[0].state.id).to.equal(VideoImportState.PENDING) @@ -538,9 +538,9 @@ describe('Test video imports', function () { }) it('Should cancel a video import', async function () { - await server.imports.cancel({ importId: pendingImportId }) + await server.videoImports.cancel({ importId: pendingImportId }) - const { data } = await server.imports.getMyVideoImports() + const { data } = await server.videoImports.getMyVideoImports() expect(data).to.have.lengthOf(1) expect(data[0].id).to.equal(pendingImportId) expect(data[0].state.id).to.equal(VideoImportState.CANCELLED) @@ -553,7 +553,7 @@ describe('Test video imports', function () { await waitJobs([ server ]) - const { data } = await server.imports.getMyVideoImports() + const { data } = await server.videoImports.getMyVideoImports() expect(data).to.have.lengthOf(1) expect(data[0].id).to.equal(pendingImportId) expect(data[0].state.id).to.equal(VideoImportState.CANCELLED) @@ -561,8 +561,8 @@ describe('Test video imports', function () { }) it('Should delete the cancelled video import', async function () { - await server.imports.delete({ importId: pendingImportId }) - const { data } = await server.imports.getMyVideoImports() + await server.videoImports.delete({ importId: pendingImportId }) + const { data } = await server.videoImports.getMyVideoImports() expect(data).to.have.lengthOf(0) }) @@ -581,7 +581,7 @@ describe('Test video imports', function () { privacy: VideoPrivacy.PUBLIC } - return server.imports.importVideo({ attributes }) + return server.videoImports.importVideo({ attributes }) } async function testBinaryUpdate (releaseUrl: string, releaseName: string) { diff --git a/packages/tests/src/api/videos/video-source.ts b/packages/tests/src/api/videos/video-source.ts index 3a13f7279..b33e84e66 100644 --- a/packages/tests/src/api/videos/video-source.ts +++ b/packages/tests/src/api/videos/video-source.ts @@ -263,20 +263,6 @@ describe('Test a video file replacement', function () { describe('Autoblacklist', function () { - function updateAutoBlacklist (enabled: boolean) { - return servers[0].config.updateExistingSubConfig({ - newConfig: { - autoBlacklist: { - videos: { - ofUsers: { - enabled - } - } - } - } - }) - } - async function expectBlacklist (uuid: string, value: boolean) { const video = await servers[0].videos.getWithToken({ id: uuid }) @@ -284,7 +270,7 @@ describe('Test a video file replacement', function () { } before(async function () { - await updateAutoBlacklist(true) + await servers[0].config.enableAutoBlacklist() }) it('Should auto blacklist an unblacklisted video after file replacement', async function () { @@ -326,7 +312,7 @@ describe('Test a video file replacement', function () { await servers[0].blacklist.remove({ videoId: uuid }) await expectBlacklist(uuid, false) - await updateAutoBlacklist(false) + await servers[0].config.disableAutoBlacklist() await servers[0].videos.replaceSourceFile({ videoId: uuid, token: userToken, fixture: 'video_short1.webm' }) await waitJobs(servers) diff --git a/packages/tests/src/api/videos/video-storyboard.ts b/packages/tests/src/api/videos/video-storyboard.ts index 32314d005..881240909 100644 --- a/packages/tests/src/api/videos/video-storyboard.ts +++ b/packages/tests/src/api/videos/video-storyboard.ts @@ -126,7 +126,7 @@ describe('Test video storyboard', function () { if (areHttpImportTestsDisabled()) return // 3s video - const { video } = await servers[0].imports.importVideo({ + const { video } = await servers[0].videoImports.importVideo({ attributes: { targetUrl: FIXTURE_URLS.goodVideo, channelId: servers[0].store.channel.id, @@ -146,7 +146,7 @@ describe('Test video storyboard', function () { if (areHttpImportTestsDisabled()) return // 10s video - const { video } = await servers[0].imports.importVideo({ + const { video } = await servers[0].videoImports.importVideo({ attributes: { magnetUri: FIXTURE_URLS.magnet, channelId: servers[0].store.channel.id, diff --git a/packages/tests/src/plugins/filter-hooks.ts b/packages/tests/src/plugins/filter-hooks.ts index 88cfee631..26ba149be 100644 --- a/packages/tests/src/plugins/filter-hooks.ts +++ b/packages/tests/src/plugins/filter-hooks.ts @@ -24,12 +24,14 @@ import { waitJobs } from '@peertube/peertube-server-commands' import { FIXTURE_URLS } from '../shared/tests.js' +import { expectEndWith } from '@tests/shared/checks.js' describe('Test plugin filter hooks', function () { let servers: PeerTubeServer[] let videoUUID: string let threadId: number let videoPlaylistUUID: string + let importUserToken: string before(async function () { this.timeout(120000) @@ -78,8 +80,18 @@ describe('Test plugin filter hooks', function () { } }) + { + const { userId, token } = await servers[0].users.generate('to_import') + importUserToken = token + await servers[0].users.update({ userId, videoQuota: -1, videoQuotaDaily: -1 }) + + await servers[0].userImports.importArchive({ userId, token, fixture: 'export-with-files.zip' }) + } + // Root subscribes to itself await servers[0].subscriptions.add({ targetUri: 'root_channel@' + servers[0].host }) + + await waitJobs(servers) }) describe('Videos', function () { @@ -95,7 +107,7 @@ describe('Test plugin filter hooks', function () { const { total } = await servers[0].videos.list({ start: 0, count: 0 }) // Plugin do +1 to the total result - expect(total).to.equal(11) + expect(total).to.equal(12) }) it('Should run filter:api.video-playlist.videos.list.params', async function () { @@ -215,7 +227,7 @@ describe('Test plugin filter hooks', function () { channelId: servers[0].store.channel.id, targetUrl: FIXTURE_URLS.goodVideo + 'bad' } - await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await servers[0].videoImports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) }) it('Should run filter:api.video.pre-import-torrent.accept.result', async function () { @@ -225,7 +237,7 @@ describe('Test plugin filter hooks', function () { channelId: servers[0].store.channel.id, torrentfile: 'video-720p.torrent' as any } - await servers[0].imports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) + await servers[0].videoImports.importVideo({ attributes, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) }) it('Should run filter:api.video.post-import-url.accept.result', async function () { @@ -240,14 +252,14 @@ describe('Test plugin filter hooks', function () { channelId: servers[0].store.channel.id, targetUrl: FIXTURE_URLS.goodVideo } - const body = await servers[0].imports.importVideo({ attributes }) + const body = await servers[0].videoImports.importVideo({ attributes }) videoImportId = body.id } await waitJobs(servers) { - const body = await servers[0].imports.getMyVideoImports() + const body = await servers[0].videoImports.getMyVideoImports() const videoImports = body.data const videoImport = videoImports.find(i => i.id === videoImportId) @@ -269,14 +281,14 @@ describe('Test plugin filter hooks', function () { channelId: servers[0].store.channel.id, torrentfile: 'video-720p.torrent' as any } - const body = await servers[0].imports.importVideo({ attributes }) + const body = await servers[0].videoImports.importVideo({ attributes }) videoImportId = body.id } await waitJobs(servers) { - const { data: videoImports } = await servers[0].imports.getMyVideoImports() + const { data: videoImports } = await servers[0].videoImports.getMyVideoImports() const videoImport = videoImports.find(i => i.id === videoImportId) @@ -284,6 +296,14 @@ describe('Test plugin filter hooks', function () { expect(videoImport.state.label).to.equal('Rejected') } }) + + it('Should run filter:api.video.user-import.video-attribute.result', async function () { + const { data } = await servers[0].videos.listMyVideos({ token: importUserToken }) + expect(data).to.have.lengthOf(1) + + // We filter out video 1 in the plugin + expect(data[0].name).to.not.equal('video 1') + }) }) describe('Video comments accept', function () { @@ -413,7 +433,7 @@ describe('Test plugin filter hooks', function () { targetUrl: FIXTURE_URLS.goodVideo, channelId: servers[0].store.channel.id } - const body = await servers[0].imports.importVideo({ attributes }) + const body = await servers[0].videoImports.importVideo({ attributes }) await checkIsBlacklisted(body.video.uuid, true) }) @@ -739,7 +759,7 @@ describe('Test plugin filter hooks', function () { before(async function () { await servers[0].config.enableLive({ transcoding: false, allowReplay: false }) - await servers[0].config.enableImports() + await servers[0].config.enableVideoImports() await servers[0].config.disableTranscoding() }) @@ -760,7 +780,7 @@ describe('Test plugin filter hooks', function () { targetUrl: FIXTURE_URLS.goodVideo, privacy: VideoPrivacy.PUBLIC } - const { video: { id } } = await servers[0].imports.importVideo({ attributes }) + const { video: { id } } = await servers[0].videoImports.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') @@ -774,7 +794,7 @@ describe('Test plugin filter hooks', function () { magnetUri: FIXTURE_URLS.magnet, privacy: VideoPrivacy.PUBLIC } - const { video: { id } } = await servers[0].imports.importVideo({ attributes }) + const { video: { id } } = await servers[0].videoImports.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') @@ -792,6 +812,16 @@ describe('Test plugin filter hooks', function () { const video = await servers[0].videos.get({ id }) expect(video.description).to.equal('live - filter:api.video.live.video-attribute.result') }) + + it('Should run filter:api.video.user-import.video-attribute.result', async function () { + this.timeout(60000) + + const { data } = await servers[0].videos.listMyVideos({ token: importUserToken }) + + for (const video of data) { + expectEndWith(video.description, ' - filter:api.video.user-import.video-attribute.result') + } + }) }) describe('Stats filters', function () { @@ -876,7 +906,7 @@ describe('Test plugin filter hooks', function () { const { total } = await servers[0].channels.list({ start: 0, count: 1 }) // plugin do +1 to the total parameter - expect(total).to.equal(4) + expect(total).to.equal(6) }) it('Should run filter:api.video-channel.get.result', async function () { diff --git a/packages/tests/src/shared/import-export.ts b/packages/tests/src/shared/import-export.ts new file mode 100644 index 000000000..f0ae2db75 --- /dev/null +++ b/packages/tests/src/shared/import-export.ts @@ -0,0 +1,298 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { + ActivityCreate, + ActivityPubOrderedCollection, + HttpStatusCode, + UserExport, + UserNotificationSettingValue, + VideoCommentObject, + VideoObject, + VideoPlaylistPrivacy, + VideoPrivacy +} from '@peertube/peertube-models' +import { + ConfigCommand, + ObjectStorageCommand, + PeerTubeServer, + createSingleServer, + doubleFollow, makeRawRequest, + setAccessTokensToServers, + setDefaultVideoChannel, + waitJobs +} from '@peertube/peertube-server-commands' +import { expect } from 'chai' +import JSZip from 'jszip' +import { resolve } from 'path' +import { MockSmtpServer } from './mock-servers/mock-email.js' +import { getAllNotificationsSettings } from './notifications.js' +import { getFilenameFromUrl } from '@peertube/peertube-node-utils' +import { testFileExistsOrNot } from './checks.js' + +type ExportOutbox = ActivityPubOrderedCollection> + +export async function downloadZIP (server: PeerTubeServer, userId: number) { + const { data } = await server.userExports.list({ userId }) + + const res = await makeRawRequest({ + url: data[0].privateDownloadUrl, + responseType: 'arraybuffer', + redirects: 1, + expectedStatus: HttpStatusCode.OK_200 + }) + + return JSZip.loadAsync(res.body) +} + +export async function parseZIPJSONFile (zip: JSZip, path: string) { + return JSON.parse(await zip.file(path).async('string')) as T +} + +export async function checkFileExistsInZIP (zip: JSZip, path: string, base = '/') { + const innerPath = resolve(base, path).substring(1) // Remove '/' at the beginning of the string + + expect(zip.files[innerPath], `${innerPath} does not exist`).to.exist + + const buf = await zip.file(innerPath).async('arraybuffer') + expect(buf.byteLength, `${innerPath} is empty`).to.be.greaterThan(0) +} + +// --------------------------------------------------------------------------- + +export function parseAPOutbox (zip: JSZip) { + return parseZIPJSONFile(zip, 'activity-pub/outbox.json') +} + +export function findVideoObjectInOutbox (outbox: ExportOutbox, videoName: string) { + return outbox.orderedItems.find(i => { + return i.type === 'Create' && i.object.type === 'Video' && i.object.name === videoName + }) as ActivityCreate +} + +// --------------------------------------------------------------------------- + +export async function regenerateExport (options: { + server: PeerTubeServer + userId: number + withVideoFiles: boolean +}) { + const { server, userId, withVideoFiles } = options + + await server.userExports.deleteAllArchives({ userId }) + const res = await server.userExports.request({ userId, withVideoFiles }) + await server.userExports.waitForCreation({ userId }) + + return res +} + +export async function checkExportFileExists (options: { + server: PeerTubeServer + userExport: UserExport + redirectedUrl: string + exists: boolean + withObjectStorage: boolean +}) { + const { server, exists, userExport, redirectedUrl, withObjectStorage } = options + + const filename = getFilenameFromUrl(userExport.privateDownloadUrl) + + if (exists === true) { + if (withObjectStorage) { + return makeRawRequest({ url: redirectedUrl, expectedStatus: HttpStatusCode.OK_200 }) + } + + return testFileExistsOrNot(server, 'tmp-persistent', filename, true) + } + + await testFileExistsOrNot(server, 'tmp-persistent', filename, false) + + if (withObjectStorage) { + await makeRawRequest({ url: redirectedUrl, expectedStatus: HttpStatusCode.NOT_FOUND_404 }) + } +} + +export async function prepareImportExportTests (options: { + objectStorage: ObjectStorageCommand + emails: object[] + withBlockedServer: boolean +}) { + const { emails, objectStorage, withBlockedServer } = options + + let objectStorageConfig: any = {} + if (objectStorage) { + await objectStorage.prepareDefaultMockBuckets() + + objectStorageConfig = objectStorage.getDefaultMockConfig() + } + + const emailPort = await MockSmtpServer.Instance.collectEmails(emails) + + const [ server, remoteServer, blockedServer ] = await Promise.all([ + await createSingleServer(1, { ...objectStorageConfig, ...ConfigCommand.getEmailOverrideConfig(emailPort) }), + await createSingleServer(2, { ...objectStorageConfig, ...ConfigCommand.getEmailOverrideConfig(emailPort) }), + + withBlockedServer + ? await createSingleServer(3) + : Promise.resolve(undefined) + ]) + + const servers = [ server, remoteServer, blockedServer ].filter(s => !!s) + + await setAccessTokensToServers(servers) + await setDefaultVideoChannel(servers) + + await Promise.all([ + await doubleFollow(server, remoteServer), + + withBlockedServer + ? await doubleFollow(server, blockedServer) + : Promise.resolve(undefined), + + withBlockedServer + ? await doubleFollow(remoteServer, blockedServer) + : Promise.resolve(undefined) + ]) + + const mouskaToken = await server.users.generateUserAndToken('mouska') + const noahToken = await server.users.generateUserAndToken('noah') + const remoteNoahToken = await remoteServer.users.generateUserAndToken('noah_remote') + + // Channel + const { id: noahSecondChannelId } = await server.channels.create({ + token: noahToken, + attributes: { + name: 'noah_second_channel', + displayName: 'noah display name', + description: 'noah description', + support: 'noah support' + } + }) + + await server.channels.updateImage({ + channelName: 'noah_second_channel', + fixture: 'banner.jpg', + type: 'banner' + }) + + await server.channels.updateImage({ + channelName: 'noah_second_channel', + fixture: 'avatar.png', + type: 'avatar' + }) + + // Videos + const externalVideo = await remoteServer.videos.quickUpload({ name: 'external video', privacy: VideoPrivacy.PUBLIC }) + + // eslint-disable-next-line max-len + const noahPrivateVideo = await server.videos.quickUpload({ name: 'noah private video', token: noahToken, privacy: VideoPrivacy.PRIVATE }) + const noahVideo = await server.videos.quickUpload({ name: 'noah public video', token: noahToken, privacy: VideoPrivacy.PUBLIC }) + // eslint-disable-next-line max-len + await server.videos.upload({ + token: noahToken, + attributes: { + fixture: 'video_short.webm', + name: 'noah public video second channel', + category: 12, + tags: [ 'tag1', 'tag2' ], + commentsEnabled: false, + description: 'video description', + downloadEnabled: false, + language: 'fr', + licence: 1, + nsfw: false, + originallyPublishedAt: new Date(0).toISOString(), + support: 'video support', + waitTranscoding: true, + channelId: noahSecondChannelId, + privacy: VideoPrivacy.PUBLIC, + thumbnailfile: 'custom-thumbnail.jpg', + previewfile: 'custom-preview.jpg' + } + }) + + await server.videos.quickUpload({ name: 'mouska private video', token: mouskaToken, privacy: VideoPrivacy.PRIVATE }) + const mouskaVideo = await server.videos.quickUpload({ name: 'mouska public video', token: mouskaToken, privacy: VideoPrivacy.PUBLIC }) + + // Captions + await server.captions.add({ language: 'ar', videoId: noahVideo.uuid, fixture: 'subtitle-good1.vtt' }) + await server.captions.add({ language: 'fr', videoId: noahVideo.uuid, fixture: 'subtitle-good1.vtt' }) + + // My settings + await server.users.updateMe({ token: noahToken, description: 'super noah description', p2pEnabled: false }) + + // My notification settings + await server.notifications.updateMySettings({ + token: noahToken, + + settings: { + ...getAllNotificationsSettings(), + + myVideoPublished: UserNotificationSettingValue.NONE, + commentMention: UserNotificationSettingValue.EMAIL + } + }) + + // Rate + await waitJobs([ server, remoteServer ]) + + await server.videos.rate({ id: mouskaVideo.uuid, token: noahToken, rating: 'like' }) + await server.videos.rate({ id: noahVideo.uuid, token: noahToken, rating: 'like' }) + await server.videos.rate({ id: externalVideo.uuid, token: noahToken, rating: 'dislike' }) + + await server.videos.rate({ id: noahVideo.uuid, token: mouskaToken, rating: 'like' }) + + // 2 followers + await remoteServer.subscriptions.add({ targetUri: 'noah_channel@' + server.host }) + await server.subscriptions.add({ targetUri: 'noah_channel@' + server.host }) + + // 2 following + await server.subscriptions.add({ token: noahToken, targetUri: 'mouska_channel@' + server.host }) + await server.subscriptions.add({ token: noahToken, targetUri: 'root_channel@' + remoteServer.host }) + + // 2 playlists + await server.playlists.quickCreate({ displayName: 'root playlist' }) + const noahPlaylist = await server.playlists.quickCreate({ displayName: 'noah playlist 1', token: noahToken }) + await server.playlists.quickCreate({ displayName: 'noah playlist 2', token: noahToken, privacy: VideoPlaylistPrivacy.PRIVATE }) + + // eslint-disable-next-line max-len + await server.playlists.addElement({ playlistId: noahPlaylist.uuid, token: noahToken, attributes: { videoId: mouskaVideo.uuid, startTimestamp: 2, stopTimestamp: 3 } }) + await server.playlists.addElement({ playlistId: noahPlaylist.uuid, token: noahToken, attributes: { videoId: noahVideo.uuid } }) + await server.playlists.addElement({ playlistId: noahPlaylist.uuid, token: noahToken, attributes: { videoId: noahPrivateVideo.uuid } }) + + // 3 threads and some replies + await remoteServer.comments.createThread({ videoId: noahVideo.uuid, text: 'remote comment' }) + await waitJobs([ server, remoteServer ]) + + await server.comments.createThread({ videoId: noahVideo.uuid, text: 'local comment' }) + await server.comments.addReplyToLastThread({ token: noahToken, text: 'noah reply' }) + + await server.comments.createThread({ videoId: mouskaVideo.uuid, token: noahToken, text: 'noah comment' }) + + // Fetch user ids + const rootId = (await server.users.getMyInfo()).id + const noahId = (await server.users.getMyInfo({ token: noahToken })).id + const remoteRootId = (await remoteServer.users.getMyInfo()).id + const remoteNoahId = (await remoteServer.users.getMyInfo({ token: remoteNoahToken })).id + + return { + rootId, + + mouskaToken, + mouskaVideo, + + remoteRootId, + remoteNoahId, + remoteNoahToken, + + externalVideo, + + noahId, + noahToken, + noahPlaylist, + noahPrivateVideo, + noahVideo, + + server, + remoteServer, + blockedServer + } +} diff --git a/packages/tests/src/shared/videos.ts b/packages/tests/src/shared/videos.ts index 62a72b436..4d438d133 100644 --- a/packages/tests/src/shared/videos.ts +++ b/packages/tests/src/shared/videos.ts @@ -113,6 +113,8 @@ async function completeVideoCheck (options: { server: PeerTubeServer originServer: PeerTubeServer videoUUID: string + objectStorageBaseUrl?: string + attributes: { name: string category: number @@ -150,7 +152,7 @@ async function completeVideoCheck (options: { previewfile?: string } }) { - const { attributes, originServer, server, videoUUID } = options + const { attributes, originServer, server, videoUUID, objectStorageBaseUrl } = options await loadLanguages() @@ -215,7 +217,13 @@ async function completeVideoCheck (options: { await testImageGeneratedByFFmpeg(server.url, attributes.previewfile, video.previewPath) } - await completeWebVideoFilesCheck({ server, originServer, videoUUID: video.uuid, ...pick(attributes, [ 'fixture', 'files' ]) }) + await completeWebVideoFilesCheck({ + server, + originServer, + videoUUID: video.uuid, + objectStorageBaseUrl, + ...pick(attributes, [ 'fixture', 'files' ]) + }) } async function checkVideoFilesWereRemoved (options: { @@ -290,9 +298,11 @@ function checkUploadVideoParam (options: { return mode === 'legacy' ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus: expectedStatus || completedExpectedStatus }) - : server.videos.buildResumeUpload({ + : server.videos.buildResumeVideoUpload({ token, - attributes, + fixture: attributes.fixture, + attaches: this.buildUploadAttaches(attributes), + fields: this.buildUploadFields(attributes), expectedStatus, completedExpectedStatus, path: '/api/v1/videos/upload-resumable' diff --git a/packages/tests/tsconfig.json b/packages/tests/tsconfig.json index 106171fea..fc3490da4 100644 --- a/packages/tests/tsconfig.json +++ b/packages/tests/tsconfig.json @@ -2,11 +2,10 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", - "baseUrl": "./", "rootDir": "src", "tsBuildInfoFile": "./dist/.tsbuildinfo", "paths": { - "@tests/*": [ "src/*" ], + "@tests/*": [ "./src/*" ], "@server/*": [ "../../server/core/*" ] } },