Add user import/export tests

pull/6266/head
Chocobozzz 2024-02-12 10:49:45 +01:00 committed by Chocobozzz
parent 8573e5a80a
commit f6af3f701c
51 changed files with 2852 additions and 433 deletions

View File

@ -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 <T> (test: request.Test): Promise<T> {
export function unwrapBody <T> (test: request.Test): Promise<T> {
return test.then(res => res.body)
}
function unwrapText (test: request.Test): Promise<string> {
export function unwrapText (test: request.Test): Promise<string> {
return test.then(res => res.text)
}
function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
export function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
return test.then(res => {
if (res.body instanceof Buffer) {
try {
@ -180,28 +216,12 @@ function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
})
}
function unwrapTextOrDecode (test: request.Test): Promise<string> {
export function unwrapTextOrDecode (test: request.Test): Promise<string> {
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) {

View File

@ -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: {

View File

@ -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)

View File

@ -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<any>[] = []
for (const server of servers) {
if (!server) continue
p = p.concat(server.servers.cleanupTests())
}

View File

@ -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 <T> (options: OverrideCommandOptions & {
path: string
fixture: string
attaches?: Record<string, string>
fields?: Record<string, any>
completedExpectedStatus?: HttpStatusCodeType // When the upload is finished
}): Promise<T> {
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<string, string>
fields?: Record<string, any>
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 <T> (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<GotResponse<T>>((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<T>({
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
})
}
}

View File

@ -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'

View File

@ -1,19 +1,11 @@
import { PeerTubeServer } from '../server/server.js'
function setAccessTokensToServers (servers: PeerTubeServer[]) {
const tasks: Promise<any>[] = []
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
})
)
}

View File

@ -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<UserExportRequestResult>(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<ResultList<UserExport>>({
...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
})
}
}

View File

@ -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<UserImportUploadResult>({
...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<UserImport>({
...options,
path: `/api/v1/users/${options.userId}/imports/latest`,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
}

View File

@ -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'

View File

@ -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

View File

@ -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 })

View File

@ -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<VideoCreateResult> {
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<GotResponse<{ video: VideoCreateResult }>>((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<AbstractCommand['sendResumableChunks']>[0]) {
return super.sendResumableChunks<{ video: VideoCreateResult }>(options)
}
async buildResumeVideoUpload (options: Parameters<AbstractCommand['buildResumeUpload']>[0]) {
const result = await super.buildResumeUpload<{ video: VideoCreateResult }>(options)
return result?.video || undefined
}
prepareVideoResumableUpload (options: Parameters<AbstractCommand['prepareResumableUpload']>[0]) {
return super.prepareResumableUpload(options)
}
endVideoResumableUpload (options: Parameters<AbstractCommand['endResumableUpload']>[0]) {
return super.endResumableUpload(options)
}
}

View File

@ -1,7 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist",
"rootDir": "src",
"tsBuildInfoFile": "./dist/.tsbuildinfo"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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,

View File

@ -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 () {

View File

@ -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: {

View File

@ -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'

View File

@ -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)

View File

@ -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 ])
})
})

View File

@ -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 ])
})
})

View File

@ -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 })
})
})

View File

@ -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') {

View File

@ -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)

View File

@ -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 })
})

View File

@ -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)

View File

@ -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 () {

View File

@ -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,

View File

@ -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
}
}
}

View File

@ -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,

View File

@ -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'

View File

@ -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<ActivityPubActor>(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<ActivityPubOrderedCollection<string>>(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<ActivityPubOrderedCollection<string>>(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<ActivityPubOrderedCollection<string>>(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<BlocklistExportJSON>(zip, 'peertube/blocklist.json')
expect(json.instances).to.have.lengthOf(0)
expect(json.actors).to.have.lengthOf(0)
}
{
const json = await parseZIPJSONFile<FollowersExportJSON>(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<FollowingExportJSON>(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<LikesExportJSON>(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<DislikesExportJSON>(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<UserSettingsExportJSON>(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<AccountExportJSON>(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<VideoPlaylistsExportJSON>(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<ChannelExportJSON>(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<CommentsExportJSON>(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<VideoExportJSON>(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<ActivityPubActor>(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<AccountExportJSON>(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<BlocklistExportJSON>(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<VideoExportJSON>(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)
})
})

View File

@ -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)
})
})

View File

@ -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)

View File

@ -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)
})

View File

@ -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)

View File

@ -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)

View File

@ -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) {

View File

@ -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)

View File

@ -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,

View File

@ -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 () {

View File

@ -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<ActivityCreate<VideoObject | VideoCommentObject>>
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 <T> (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<ExportOutbox>(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<VideoObject>
}
// ---------------------------------------------------------------------------
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
}
}

View File

@ -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'

View File

@ -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/*" ]
}
},