はじまりの大地
このコミットが含まれているのは:
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { pathExists } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { Account, VideoChannel } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
|
||||
async function expectChannelsFollows (options: {
|
||||
server: PeerTubeServer
|
||||
handle: string
|
||||
followers: number
|
||||
following: number
|
||||
}) {
|
||||
const { server } = options
|
||||
const { data } = await server.channels.list()
|
||||
|
||||
return expectActorFollow({ ...options, data })
|
||||
}
|
||||
|
||||
async function expectAccountFollows (options: {
|
||||
server: PeerTubeServer
|
||||
handle: string
|
||||
followers: number
|
||||
following: number
|
||||
}) {
|
||||
const { server } = options
|
||||
const { data } = await server.accounts.list()
|
||||
|
||||
return expectActorFollow({ ...options, data })
|
||||
}
|
||||
|
||||
async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) {
|
||||
for (const directory of [ 'avatars' ]) {
|
||||
const directoryPath = server.getDirectoryPath(directory)
|
||||
|
||||
const directoryExists = await pathExists(directoryPath)
|
||||
expect(directoryExists).to.be.true
|
||||
|
||||
const files = await readdir(directoryPath)
|
||||
for (const file of files) {
|
||||
expect(file).to.not.contain(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
expectAccountFollows,
|
||||
expectChannelsFollows,
|
||||
checkActorFilesWereRemoved
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function expectActorFollow (options: {
|
||||
server: PeerTubeServer
|
||||
data: (Account | VideoChannel)[]
|
||||
handle: string
|
||||
followers: number
|
||||
following: number
|
||||
}) {
|
||||
const { server, data, handle, followers, following } = options
|
||||
|
||||
const actor = data.find(a => a.name + '@' + a.host === handle)
|
||||
const message = `${handle} on ${server.url}`
|
||||
|
||||
expect(actor, message).to.exist
|
||||
expect(actor.followersCount).to.equal(followers, message)
|
||||
expect(actor.followingCount).to.equal(following, message)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect } from 'chai'
|
||||
import request from 'supertest'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) {
|
||||
const res = await request(url)
|
||||
.get(captionPath)
|
||||
.expect(HttpStatusCode.OK_200)
|
||||
|
||||
if (toTest instanceof RegExp) {
|
||||
expect(res.text).to.match(toTest)
|
||||
} else {
|
||||
expect(res.text).to.contain(toTest)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
testCaptionFile
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { pathExists } from 'fs-extra/esm'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { join, parse } from 'path'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { makeGetRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
|
||||
// Default interval -> 5 minutes
|
||||
function dateIsValid (dateString: string | Date, interval = 300000) {
|
||||
const dateToCheck = new Date(dateString)
|
||||
const now = new Date()
|
||||
|
||||
return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
|
||||
}
|
||||
|
||||
function expectStartWith (str: string, start: string) {
|
||||
expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true
|
||||
}
|
||||
|
||||
function expectNotStartWith (str: string, start: string) {
|
||||
expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.false
|
||||
}
|
||||
|
||||
function expectEndWith (str: string, end: string) {
|
||||
expect(str.endsWith(end), `${str} does not end with ${end}`).to.be.true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function expectLogDoesNotContain (server: PeerTubeServer, str: string) {
|
||||
const content = await server.servers.getLogContent()
|
||||
|
||||
expect(content.toString()).to.not.contain(str)
|
||||
}
|
||||
|
||||
async function expectLogContain (server: PeerTubeServer, str: string) {
|
||||
const content = await server.servers.getLogContent()
|
||||
|
||||
expect(content.toString()).to.contain(str)
|
||||
}
|
||||
|
||||
async function testAvatarSize (options: {
|
||||
url: string
|
||||
imageName: string
|
||||
avatar: {
|
||||
width: number
|
||||
path: string
|
||||
}
|
||||
}) {
|
||||
const { url, imageName, avatar } = options
|
||||
|
||||
const { body } = await makeGetRequest({
|
||||
url,
|
||||
path: avatar.path,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
const extension = parse(avatar.path).ext
|
||||
// We don't test big GIF avatars
|
||||
if (extension === '.gif' && avatar.width > 150) return
|
||||
|
||||
const data = await readFile(buildAbsoluteFixturePath(imageName + extension))
|
||||
const minLength = data.length - ((40 * data.length) / 100)
|
||||
const maxLength = data.length + ((40 * data.length) / 100)
|
||||
|
||||
expect(body.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
|
||||
expect(body.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
|
||||
}
|
||||
|
||||
async function testImageGeneratedByFFmpeg (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') {
|
||||
if (process.env.ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS !== 'true') {
|
||||
console.log(
|
||||
'Pixel comparison of image generated by ffmpeg is disabled. ' +
|
||||
'You can enable it using `ENABLE_FFMPEG_THUMBNAIL_PIXEL_COMPARISON_TESTS=true env variable')
|
||||
return
|
||||
}
|
||||
|
||||
return testImage(url, imageName, imageHTTPPath, extension)
|
||||
}
|
||||
|
||||
async function testImage (url: string, imageName: string, imageHTTPPath: string, extension = '.jpg') {
|
||||
const res = await makeGetRequest({
|
||||
url,
|
||||
path: imageHTTPPath,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
const body = res.body
|
||||
const data = await readFile(buildAbsoluteFixturePath(imageName + extension))
|
||||
|
||||
const { PNG } = await import('pngjs')
|
||||
const JPEG = await import('jpeg-js')
|
||||
const pixelmatch = (await import('pixelmatch')).default
|
||||
|
||||
const img1 = imageHTTPPath.endsWith('.png')
|
||||
? PNG.sync.read(body)
|
||||
: JPEG.decode(body)
|
||||
|
||||
const img2 = extension === '.png'
|
||||
? PNG.sync.read(data)
|
||||
: JPEG.decode(data)
|
||||
|
||||
const errorMsg = `${imageHTTPPath} image is not the same as ${imageName}${extension}`
|
||||
|
||||
try {
|
||||
const result = pixelmatch(img1.data, img2.data, null, img1.width, img1.height, { threshold: 0.1 })
|
||||
|
||||
expect(result).to.equal(0, errorMsg)
|
||||
} catch (err) {
|
||||
throw new Error(`${errorMsg}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function testFileExistsOnFSOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
|
||||
const base = server.servers.buildDirectory(directory)
|
||||
|
||||
expect(await pathExists(join(base, filePath))).to.equal(exist)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
|
||||
return makeGetRequest({
|
||||
url,
|
||||
path,
|
||||
token,
|
||||
query: { ...query, start: 'hello' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
async function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
|
||||
await makeGetRequest({
|
||||
url,
|
||||
path,
|
||||
token,
|
||||
query: { ...query, count: 'hello' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await makeGetRequest({
|
||||
url,
|
||||
path,
|
||||
token,
|
||||
query: { ...query, count: 2000 },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
|
||||
return makeGetRequest({
|
||||
url,
|
||||
path,
|
||||
token,
|
||||
query: { ...query, sort: 'hello' },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkVideoDuration (server: PeerTubeServer, videoUUID: string, duration: number) {
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
expect(video.duration).to.be.approximately(duration, 1)
|
||||
|
||||
for (const file of video.files) {
|
||||
const metadata = await server.videos.getFileMetadata({ url: file.metadataUrl })
|
||||
|
||||
for (const stream of metadata.streams) {
|
||||
expect(Math.round(stream.duration)).to.be.approximately(duration, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
dateIsValid,
|
||||
testImageGeneratedByFFmpeg,
|
||||
testAvatarSize,
|
||||
testImage,
|
||||
expectLogDoesNotContain,
|
||||
testFileExistsOnFSOrNot,
|
||||
expectStartWith,
|
||||
expectNotStartWith,
|
||||
expectEndWith,
|
||||
checkBadStartPagination,
|
||||
checkBadCountPagination,
|
||||
checkBadSortPagination,
|
||||
checkVideoDuration,
|
||||
expectLogContain
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
VideoPrivacy,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPlaylistCreateResult,
|
||||
Account,
|
||||
HTMLServerConfig,
|
||||
ServerConfig
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
createMultipleServers,
|
||||
setAccessTokensToServers,
|
||||
doubleFollow,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
export function getWatchVideoBasePaths () {
|
||||
return [ '/videos/watch/', '/w/' ]
|
||||
}
|
||||
|
||||
export function getWatchPlaylistBasePaths () {
|
||||
return [ '/videos/watch/playlist/', '/w/p/' ]
|
||||
}
|
||||
|
||||
export function checkIndexTags (html: string, title: string, description: string, css: string, config: ServerConfig) {
|
||||
expect(html).to.contain('<title>' + title + '</title>')
|
||||
expect(html).to.contain('<meta name="description" content="' + description + '" />')
|
||||
|
||||
if (css) {
|
||||
expect(html).to.contain('<style class="custom-css-style">' + css + '</style>')
|
||||
}
|
||||
|
||||
const htmlConfig: HTMLServerConfig = omit(config, [ 'signup' ])
|
||||
const configObjectString = JSON.stringify(htmlConfig)
|
||||
const configEscapedString = JSON.stringify(configObjectString)
|
||||
|
||||
expect(html).to.contain(`<script type="application/javascript">window.PeerTubeServerConfig = ${configEscapedString}</script>`)
|
||||
}
|
||||
|
||||
export async function prepareClientTests () {
|
||||
const servers = await createMultipleServers(2)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
let account: Account
|
||||
|
||||
let videoIds: (string | number)[] = []
|
||||
let privateVideoId: string
|
||||
let internalVideoId: string
|
||||
let unlistedVideoId: string
|
||||
let passwordProtectedVideoId: string
|
||||
|
||||
let playlistIds: (string | number)[] = []
|
||||
let privatePlaylistId: string
|
||||
let unlistedPlaylistId: string
|
||||
|
||||
const instanceDescription = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
|
||||
|
||||
const videoName = 'my super name for server 1'
|
||||
const videoDescription = 'my<br> super __description__ for *server* 1<p></p>'
|
||||
const videoDescriptionPlainText = 'my super description for server 1'
|
||||
|
||||
const playlistName = 'super playlist name'
|
||||
const playlistDescription = 'super playlist description'
|
||||
let playlist: VideoPlaylistCreateResult
|
||||
|
||||
const channelDescription = 'my super channel description'
|
||||
|
||||
await servers[0].channels.update({
|
||||
channelName: servers[0].store.channel.name,
|
||||
attributes: { description: channelDescription }
|
||||
})
|
||||
|
||||
// Public video
|
||||
|
||||
{
|
||||
const attributes = { name: videoName, description: videoDescription }
|
||||
await servers[0].videos.upload({ attributes })
|
||||
|
||||
const { data } = await servers[0].videos.list()
|
||||
expect(data.length).to.equal(1)
|
||||
|
||||
const video = data[0]
|
||||
servers[0].store.video = video
|
||||
videoIds = [ video.id, video.uuid, video.shortUUID ]
|
||||
}
|
||||
|
||||
{
|
||||
({ uuid: privateVideoId } = await servers[0].videos.quickUpload({ name: 'private', privacy: VideoPrivacy.PRIVATE }));
|
||||
({ uuid: unlistedVideoId } = await servers[0].videos.quickUpload({ name: 'unlisted', privacy: VideoPrivacy.UNLISTED }));
|
||||
({ uuid: internalVideoId } = await servers[0].videos.quickUpload({ name: 'internal', privacy: VideoPrivacy.INTERNAL }));
|
||||
({ uuid: passwordProtectedVideoId } = await servers[0].videos.quickUpload({
|
||||
name: 'password protected',
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED,
|
||||
videoPasswords: [ 'password' ]
|
||||
}))
|
||||
}
|
||||
|
||||
// Playlists
|
||||
{
|
||||
// Public playlist
|
||||
{
|
||||
const attributes = {
|
||||
displayName: playlistName,
|
||||
description: playlistDescription,
|
||||
privacy: VideoPlaylistPrivacy.PUBLIC,
|
||||
videoChannelId: servers[0].store.channel.id
|
||||
}
|
||||
|
||||
playlist = await servers[0].playlists.create({ attributes })
|
||||
playlistIds = [ playlist.id, playlist.shortUUID, playlist.uuid ]
|
||||
|
||||
await servers[0].playlists.addElement({ playlistId: playlist.shortUUID, attributes: { videoId: servers[0].store.video.id } })
|
||||
}
|
||||
|
||||
// Unlisted playlist
|
||||
{
|
||||
const attributes = {
|
||||
displayName: 'unlisted',
|
||||
privacy: VideoPlaylistPrivacy.UNLISTED,
|
||||
videoChannelId: servers[0].store.channel.id
|
||||
}
|
||||
|
||||
const { uuid } = await servers[0].playlists.create({ attributes })
|
||||
unlistedPlaylistId = uuid
|
||||
}
|
||||
|
||||
{
|
||||
const attributes = {
|
||||
displayName: 'private',
|
||||
privacy: VideoPlaylistPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
const { uuid } = await servers[0].playlists.create({ attributes })
|
||||
privatePlaylistId = uuid
|
||||
}
|
||||
}
|
||||
|
||||
// Account
|
||||
{
|
||||
await servers[0].users.updateMe({ description: 'my account description' })
|
||||
|
||||
account = await servers[0].accounts.get({ accountName: `${servers[0].store.user.username}@${servers[0].host}` })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
return {
|
||||
servers,
|
||||
|
||||
instanceDescription,
|
||||
|
||||
account,
|
||||
|
||||
channelDescription,
|
||||
|
||||
playlist,
|
||||
playlistName,
|
||||
playlistIds,
|
||||
playlistDescription,
|
||||
|
||||
privatePlaylistId,
|
||||
unlistedPlaylistId,
|
||||
|
||||
privateVideoId,
|
||||
unlistedVideoId,
|
||||
internalVideoId,
|
||||
passwordProtectedVideoId,
|
||||
|
||||
videoName,
|
||||
videoDescription,
|
||||
videoDescriptionPlainText,
|
||||
videoIds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createLogger, transports } from 'winston'
|
||||
|
||||
export function createConsoleLogger () {
|
||||
return createLogger({ transports: [ new transports.Console() ] })
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { pathExists } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { homedir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { PeerTubeRunnerProcess } from './peertube-runner-process.js'
|
||||
|
||||
export async function checkTmpIsEmpty (server: PeerTubeServer) {
|
||||
await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
|
||||
|
||||
if (await pathExists(server.getDirectoryPath('tmp/hls'))) {
|
||||
await checkDirectoryIsEmpty(server, 'tmp/hls')
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkPersistentTmpIsEmpty (server: PeerTubeServer) {
|
||||
await checkDirectoryIsEmpty(server, 'tmp-persistent')
|
||||
}
|
||||
|
||||
export async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
|
||||
const directoryPath = server.getDirectoryPath(directory)
|
||||
|
||||
const directoryExists = await pathExists(directoryPath)
|
||||
expect(directoryExists).to.be.true
|
||||
|
||||
const files = await readdir(directoryPath)
|
||||
const filtered = files.filter(f => exceptions.includes(f) === false)
|
||||
|
||||
expect(filtered).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
export async function checkPeerTubeRunnerCacheIsEmpty (runner: PeerTubeRunnerProcess, subDir: 'transcoding' | 'transcription') {
|
||||
const directoryPath = join(homedir(), '.cache', 'peertube-runner-nodejs', runner.getId(), subDir)
|
||||
|
||||
const directoryExists = await pathExists(directoryPath)
|
||||
expect(directoryExists).to.be.true
|
||||
|
||||
const files = await readdir(directoryPath)
|
||||
|
||||
expect(files, `Sub-directory ${subDir} content: ${files.join(', ')}`).to.have.lengthOf(0)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export const FIXTURE_URLS = {
|
||||
peertube_long: 'https://peertube2.cpy.re/videos/watch/122d093a-1ede-43bd-bd34-59d2931ffc5e',
|
||||
peertube_short: 'https://peertube2.cpy.re/w/3fbif9S3WmtTP8gGsC5HBd',
|
||||
|
||||
youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM',
|
||||
youtubeChapters: 'https://www.youtube.com/watch?v=TL9P-Er7ils',
|
||||
|
||||
/**
|
||||
* The video is used to check format-selection correctness wrt. HDR,
|
||||
* which brings its own set of oddities outside of a MediaSource.
|
||||
*
|
||||
* The video needs to have the following format_ids:
|
||||
* (which you can check by using `youtube-dl <url> -F`):
|
||||
* - (webm vp9)
|
||||
* - (mp4 avc1)
|
||||
* - (webm vp9.2 HDR)
|
||||
*/
|
||||
youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4',
|
||||
|
||||
youtubeChannel: 'https://youtube.com/channel/UCtnlZdXv3-xQzxiqfn6cjIA',
|
||||
youtubePlaylist: 'https://youtube.com/playlist?list=PLRGXHPrcPd2yc2KdswlAWOxIJ8G3vgy4h',
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4',
|
||||
|
||||
badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
|
||||
goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
|
||||
goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4',
|
||||
transcriptionVideo: 'https://download.cpy.re/peertube/the_last_man_on_earth.mp4',
|
||||
|
||||
chatersVideo: 'https://download.cpy.re/peertube/video_chapters.mp4',
|
||||
|
||||
file4K: 'https://download.cpy.re/peertube/4k_file.txt',
|
||||
|
||||
transcriptionModels: 'https://download.cpy.re/peertube/transcription-models.zip'
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect } from 'chai'
|
||||
import { ensureDir, pathExists } from 'fs-extra/esm'
|
||||
import { dirname } from 'path'
|
||||
import { getMaxTheoreticalBitrate } from '@peertube/peertube-core-utils'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { getVideoStreamBitrate, getVideoStreamDimensionsInfo, getVideoStreamFPS } from '@peertube/peertube-ffmpeg'
|
||||
|
||||
async function ensureHasTooBigBitrate (fixturePath: string) {
|
||||
const bitrate = await getVideoStreamBitrate(fixturePath)
|
||||
const dataResolution = await getVideoStreamDimensionsInfo(fixturePath)
|
||||
const fps = await getVideoStreamFPS(fixturePath)
|
||||
|
||||
const maxBitrate = getMaxTheoreticalBitrate({ ...dataResolution, fps })
|
||||
expect(bitrate).to.be.above(maxBitrate)
|
||||
}
|
||||
|
||||
async function generateHighBitrateVideo () {
|
||||
const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
|
||||
|
||||
await ensureDir(dirname(tempFixturePath))
|
||||
|
||||
const exists = await pathExists(tempFixturePath)
|
||||
|
||||
if (!exists) {
|
||||
const ffmpeg = (await import('fluent-ffmpeg')).default
|
||||
|
||||
console.log('Generating high bitrate video.')
|
||||
|
||||
// Generate a random, high bitrate video on the fly, so we don't have to include
|
||||
// a large file in the repo. The video needs to have a certain minimum length so
|
||||
// that FFmpeg properly applies bitrate limits.
|
||||
// https://stackoverflow.com/a/15795112
|
||||
return new Promise<string>((res, rej) => {
|
||||
ffmpeg()
|
||||
.outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
|
||||
.outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
|
||||
.outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
|
||||
.output(tempFixturePath)
|
||||
.on('error', rej)
|
||||
.on('end', () => res(tempFixturePath))
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
await ensureHasTooBigBitrate(tempFixturePath)
|
||||
|
||||
return tempFixturePath
|
||||
}
|
||||
|
||||
async function generateVideoWithFramerate (fps = 60) {
|
||||
const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
|
||||
|
||||
await ensureDir(dirname(tempFixturePath))
|
||||
|
||||
const exists = await pathExists(tempFixturePath)
|
||||
if (!exists) {
|
||||
const ffmpeg = (await import('fluent-ffmpeg')).default
|
||||
|
||||
console.log('Generating video with framerate %d.', fps)
|
||||
|
||||
return new Promise<string>((res, rej) => {
|
||||
ffmpeg()
|
||||
.outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
|
||||
.outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
|
||||
.outputOptions([ `-r ${fps}` ])
|
||||
.output(tempFixturePath)
|
||||
.on('error', rej)
|
||||
.on('end', () => res(tempFixturePath))
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
return tempFixturePath
|
||||
}
|
||||
|
||||
export {
|
||||
generateHighBitrateVideo,
|
||||
generateVideoWithFramerate
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
import {
|
||||
ActivityCreate,
|
||||
ActivityPubOrderedCollection,
|
||||
HttpStatusCode,
|
||||
LiveVideoLatencyMode,
|
||||
UserExport,
|
||||
UserNotificationSettingValue,
|
||||
VideoCommentObject,
|
||||
VideoCommentPolicy,
|
||||
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 { testFileExistsOnFSOrNot } 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 testFileExistsOnFSOrNot(server, 'tmp-persistent', filename, true)
|
||||
}
|
||||
|
||||
await testFileExistsOnFSOrNot(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 overrideConfig = {
|
||||
...objectStorageConfig,
|
||||
...ConfigCommand.getEmailOverrideConfig(emailPort),
|
||||
...ConfigCommand.getDisableRatesLimitOverrideConfig()
|
||||
}
|
||||
|
||||
const [ server, remoteServer, blockedServer ] = await Promise.all([
|
||||
createSingleServer(1, overrideConfig),
|
||||
createSingleServer(2, overrideConfig),
|
||||
|
||||
withBlockedServer
|
||||
? createSingleServer(3)
|
||||
: Promise.resolve(undefined)
|
||||
])
|
||||
|
||||
const servers = [ server, remoteServer, blockedServer ].filter(s => !!s)
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await remoteServer.config.enableMinimumTranscoding()
|
||||
|
||||
await Promise.all([
|
||||
doubleFollow(server, remoteServer),
|
||||
|
||||
withBlockedServer
|
||||
? doubleFollow(server, blockedServer)
|
||||
: Promise.resolve(undefined),
|
||||
|
||||
withBlockedServer
|
||||
? 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' ],
|
||||
commentsPolicy: VideoCommentPolicy.DISABLED,
|
||||
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' })
|
||||
|
||||
// Chapters
|
||||
await server.chapters.update({
|
||||
videoId: noahVideo.uuid,
|
||||
chapters: [
|
||||
{ timecode: 1, title: 'chapter 1' },
|
||||
{ timecode: 3, title: 'chapter 2' }
|
||||
]
|
||||
})
|
||||
|
||||
// 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
|
||||
|
||||
// Lives
|
||||
await server.config.enableMinimumTranscoding()
|
||||
await server.config.enableLive({ allowReplay: true })
|
||||
|
||||
const noahLive = await server.live.create({
|
||||
fields: {
|
||||
permanentLive: true,
|
||||
saveReplay: true,
|
||||
latencyMode: LiveVideoLatencyMode.SMALL_LATENCY,
|
||||
replaySettings: {
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
},
|
||||
videoPasswords: [ 'password1' ],
|
||||
channelId: noahSecondChannelId,
|
||||
name: 'noah live video',
|
||||
privacy: VideoPrivacy.PASSWORD_PROTECTED
|
||||
},
|
||||
token: noahToken
|
||||
})
|
||||
|
||||
// Views
|
||||
await server.views.view({ id: noahVideo.uuid, token: noahToken, currentTime: 4 })
|
||||
await server.views.view({ id: externalVideo.uuid, token: noahToken, currentTime: 2 })
|
||||
|
||||
// Watched words and auto tag policies
|
||||
await servers[0].watchedWordsLists.createList({
|
||||
token: noahToken,
|
||||
listName: 'forbidden-list',
|
||||
words: [ 'forbidden' ],
|
||||
accountName: 'noah'
|
||||
})
|
||||
|
||||
await servers[0].watchedWordsLists.createList({
|
||||
token: noahToken,
|
||||
listName: 'allowed-list',
|
||||
words: [ 'allowed', 'allowed2' ],
|
||||
accountName: 'noah'
|
||||
})
|
||||
|
||||
await servers[0].autoTags.updateCommentPolicies({
|
||||
accountName: 'noah',
|
||||
review: [ 'external-link', 'forbidden-list' ],
|
||||
token: noahToken
|
||||
})
|
||||
|
||||
return {
|
||||
rootId,
|
||||
|
||||
mouskaToken,
|
||||
mouskaVideo,
|
||||
|
||||
remoteRootId,
|
||||
remoteNoahId,
|
||||
remoteNoahToken,
|
||||
|
||||
externalVideo,
|
||||
|
||||
noahId,
|
||||
noahToken,
|
||||
noahPlaylist,
|
||||
noahPrivateVideo,
|
||||
noahVideo,
|
||||
noahLive,
|
||||
|
||||
server,
|
||||
remoteServer,
|
||||
blockedServer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { pathExists } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { sha1 } from '@peertube/peertube-node-utils'
|
||||
import { LiveVideo, VideoStreamingPlaylistType } from '@peertube/peertube-models'
|
||||
import { ObjectStorageCommand, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { SQLCommand } from './sql-command.js'
|
||||
import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist } from './streaming-playlists.js'
|
||||
|
||||
async function checkLiveCleanup (options: {
|
||||
server: PeerTubeServer
|
||||
videoUUID: string
|
||||
permanent: boolean
|
||||
savedResolutions?: number[]
|
||||
}) {
|
||||
const { server, videoUUID, permanent, savedResolutions = [] } = options
|
||||
|
||||
const basePath = server.servers.buildDirectory('streaming-playlists')
|
||||
const hlsPath = join(basePath, 'hls', videoUUID)
|
||||
|
||||
if (permanent) {
|
||||
if (!await pathExists(hlsPath)) return
|
||||
|
||||
const files = await readdir(hlsPath)
|
||||
expect(files).to.have.lengthOf(0)
|
||||
return
|
||||
}
|
||||
|
||||
if (savedResolutions.length === 0) {
|
||||
return checkUnsavedLiveCleanup(server, videoUUID, hlsPath)
|
||||
}
|
||||
|
||||
return checkSavedLiveCleanup(hlsPath, savedResolutions)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function testLiveVideoResolutions (options: {
|
||||
sqlCommand: SQLCommand
|
||||
originServer: PeerTubeServer
|
||||
|
||||
servers: PeerTubeServer[]
|
||||
liveVideoId: string
|
||||
resolutions: number[]
|
||||
transcoded: boolean
|
||||
|
||||
objectStorage?: ObjectStorageCommand
|
||||
objectStorageBaseUrl?: string
|
||||
}) {
|
||||
const {
|
||||
originServer,
|
||||
sqlCommand,
|
||||
servers,
|
||||
liveVideoId,
|
||||
resolutions,
|
||||
transcoded,
|
||||
objectStorage,
|
||||
objectStorageBaseUrl = objectStorage?.getMockPlaylistBaseUrl()
|
||||
} = options
|
||||
|
||||
for (const server of servers) {
|
||||
const { data } = await server.videos.list()
|
||||
expect(data.find(v => v.uuid === liveVideoId)).to.exist
|
||||
|
||||
const video = await server.videos.get({ id: liveVideoId })
|
||||
|
||||
expect(video.aspectRatio).to.equal(1.7778)
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const hlsPlaylist = video.streamingPlaylists.find(s => s.type === VideoStreamingPlaylistType.HLS)
|
||||
expect(hlsPlaylist).to.exist
|
||||
expect(hlsPlaylist.files).to.have.lengthOf(0) // Only fragmented mp4 files are displayed
|
||||
|
||||
await checkResolutionsInMasterPlaylist({
|
||||
server,
|
||||
playlistUrl: hlsPlaylist.playlistUrl,
|
||||
resolutions,
|
||||
transcoded,
|
||||
withRetry: !!objectStorage
|
||||
})
|
||||
|
||||
if (objectStorage) {
|
||||
expect(hlsPlaylist.playlistUrl).to.contain(objectStorageBaseUrl)
|
||||
}
|
||||
|
||||
for (let i = 0; i < resolutions.length; i++) {
|
||||
const segmentNum = 3
|
||||
const segmentName = `${i}-00000${segmentNum}.ts`
|
||||
await originServer.live.waitUntilSegmentGeneration({
|
||||
server: originServer,
|
||||
videoUUID: video.uuid,
|
||||
playlistNumber: i,
|
||||
segment: segmentNum,
|
||||
objectStorage,
|
||||
objectStorageBaseUrl
|
||||
})
|
||||
|
||||
const baseUrl = objectStorage
|
||||
? join(objectStorageBaseUrl, 'hls')
|
||||
: originServer.url + '/static/streaming-playlists/hls'
|
||||
|
||||
if (objectStorage) {
|
||||
expect(hlsPlaylist.segmentsSha256Url).to.contain(objectStorageBaseUrl)
|
||||
}
|
||||
|
||||
const subPlaylist = await originServer.streamingPlaylists.get({
|
||||
url: `${baseUrl}/${video.uuid}/${i}.m3u8`,
|
||||
withRetry: !!objectStorage // With object storage, the request may fail because of inconsistent data in S3
|
||||
})
|
||||
|
||||
expect(subPlaylist).to.contain(segmentName)
|
||||
|
||||
await checkLiveSegmentHash({
|
||||
server,
|
||||
baseUrlSegment: baseUrl,
|
||||
videoUUID: video.uuid,
|
||||
segmentName,
|
||||
hlsPlaylist,
|
||||
withRetry: !!objectStorage // With object storage, the request may fail because of inconsistent data in S3
|
||||
})
|
||||
|
||||
if (originServer.internalServerNumber === server.internalServerNumber) {
|
||||
const infohash = sha1(`${2 + hlsPlaylist.playlistUrl}+V${i}`)
|
||||
const dbInfohashes = await sqlCommand.getPlaylistInfohash(hlsPlaylist.id)
|
||||
|
||||
expect(dbInfohashes).to.include(infohash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
checkLiveCleanup,
|
||||
testLiveVideoResolutions
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkSavedLiveCleanup (hlsPath: string, savedResolutions: number[] = []) {
|
||||
const files = await readdir(hlsPath)
|
||||
|
||||
// fragmented file and playlist per resolution + master playlist + segments sha256 json file
|
||||
expect(files, `Directory content: ${files.join(', ')}`).to.have.lengthOf(savedResolutions.length * 2 + 2)
|
||||
|
||||
for (const resolution of savedResolutions) {
|
||||
const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
|
||||
expect(fragmentedFile).to.exist
|
||||
|
||||
const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
|
||||
expect(playlistFile).to.exist
|
||||
}
|
||||
|
||||
const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
|
||||
expect(masterPlaylistFile).to.exist
|
||||
|
||||
const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
|
||||
expect(shaFile).to.exist
|
||||
}
|
||||
|
||||
async function checkUnsavedLiveCleanup (server: PeerTubeServer, videoUUID: string, hlsPath: string) {
|
||||
let live: LiveVideo
|
||||
|
||||
try {
|
||||
live = await server.live.get({ videoId: videoUUID })
|
||||
} catch {}
|
||||
|
||||
if (live?.permanentLive) {
|
||||
expect(await pathExists(hlsPath)).to.be.true
|
||||
|
||||
const hlsFiles = await readdir(hlsPath)
|
||||
expect(hlsFiles).to.have.lengthOf(1) // Only replays directory
|
||||
|
||||
const replayDir = join(hlsPath, 'replay')
|
||||
expect(await pathExists(replayDir)).to.be.true
|
||||
|
||||
const replayFiles = await readdir(join(hlsPath, 'replay'))
|
||||
expect(replayFiles).to.have.lengthOf(0)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
expect(await pathExists(hlsPath)).to.be.false
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './mock-429.js'
|
||||
export * from './mock-email.js'
|
||||
export * from './mock-http.js'
|
||||
export * from './mock-instances-index.js'
|
||||
export * from './mock-joinpeertube-versions.js'
|
||||
export * from './mock-object-storage.js'
|
||||
export * from './mock-plugin-blocklist.js'
|
||||
export * from './mock-proxy.js'
|
||||
@@ -0,0 +1,33 @@
|
||||
import express from 'express'
|
||||
import { Server } from 'http'
|
||||
import { getPort, randomListen, terminateServer } from './shared.js'
|
||||
|
||||
export class Mock429 {
|
||||
private server: Server
|
||||
private responseSent = false
|
||||
|
||||
async initialize () {
|
||||
const app = express()
|
||||
|
||||
app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
|
||||
if (!this.responseSent) {
|
||||
this.responseSent = true
|
||||
|
||||
// Retry after 5 seconds
|
||||
res.header('retry-after', '2')
|
||||
return res.sendStatus(429)
|
||||
}
|
||||
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
|
||||
this.server = await randomListen(app)
|
||||
|
||||
return getPort(this.server)
|
||||
}
|
||||
|
||||
terminate () {
|
||||
return terminateServer(this.server)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import MailDev from '@peertube/maildev'
|
||||
import { randomInt } from '@peertube/peertube-core-utils'
|
||||
import { parallelTests } from '@peertube/peertube-node-utils'
|
||||
|
||||
class MockSmtpServer {
|
||||
|
||||
private static instance: MockSmtpServer
|
||||
private started = false
|
||||
private maildev: any
|
||||
private emails: object[]
|
||||
|
||||
private constructor () { }
|
||||
|
||||
collectEmails (emailsCollection: object[]) {
|
||||
return new Promise<number>((res, rej) => {
|
||||
const port = parallelTests() ? randomInt(1025, 2000) : 1025
|
||||
this.emails = emailsCollection
|
||||
|
||||
if (this.started) {
|
||||
return res(undefined)
|
||||
}
|
||||
|
||||
this.maildev = new MailDev({
|
||||
ip: '127.0.0.1',
|
||||
smtp: port,
|
||||
disableWeb: true,
|
||||
silent: true
|
||||
})
|
||||
|
||||
this.maildev.on('new', email => {
|
||||
this.emails.push(email)
|
||||
})
|
||||
|
||||
this.maildev.listen(err => {
|
||||
if (err) return rej(err)
|
||||
|
||||
this.started = true
|
||||
|
||||
return res(port)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
kill () {
|
||||
if (!this.maildev) return
|
||||
|
||||
this.maildev.close()
|
||||
|
||||
this.maildev = null
|
||||
MockSmtpServer.instance = null
|
||||
}
|
||||
|
||||
static get Instance () {
|
||||
return this.instance || (this.instance = new this())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
MockSmtpServer
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import express from 'express'
|
||||
import { Server } from 'http'
|
||||
import { getPort, randomListen, terminateServer } from './shared.js'
|
||||
|
||||
export class MockHTTP {
|
||||
private server: Server
|
||||
|
||||
async initialize () {
|
||||
const app = express()
|
||||
|
||||
app.get('/*', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
return res.sendStatus(200)
|
||||
})
|
||||
|
||||
this.server = await randomListen(app)
|
||||
|
||||
return getPort(this.server)
|
||||
}
|
||||
|
||||
terminate () {
|
||||
return terminateServer(this.server)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import express from 'express'
|
||||
import { Server } from 'http'
|
||||
import { getPort, randomListen, terminateServer } from './shared.js'
|
||||
|
||||
export class MockInstancesIndex {
|
||||
private server: Server
|
||||
|
||||
private readonly indexInstances: { host: string, createdAt: string }[] = []
|
||||
|
||||
async initialize () {
|
||||
const app = express()
|
||||
|
||||
app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
|
||||
|
||||
return next()
|
||||
})
|
||||
|
||||
app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => {
|
||||
const since = req.query.since
|
||||
|
||||
const filtered = this.indexInstances.filter(i => {
|
||||
if (!since) return true
|
||||
|
||||
return i.createdAt > since
|
||||
})
|
||||
|
||||
return res.json({
|
||||
total: filtered.length,
|
||||
data: filtered
|
||||
})
|
||||
})
|
||||
|
||||
this.server = await randomListen(app)
|
||||
|
||||
return getPort(this.server)
|
||||
}
|
||||
|
||||
addInstance (host: string) {
|
||||
this.indexInstances.push({ host, createdAt: new Date().toISOString() })
|
||||
}
|
||||
|
||||
terminate () {
|
||||
return terminateServer(this.server)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import express from 'express'
|
||||
import { Server } from 'http'
|
||||
import { getPort, randomListen } from './shared.js'
|
||||
|
||||
export class MockJoinPeerTubeVersions {
|
||||
private server: Server
|
||||
private latestVersion: string
|
||||
|
||||
async initialize () {
|
||||
const app = express()
|
||||
|
||||
app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
|
||||
|
||||
return next()
|
||||
})
|
||||
|
||||
app.get('/versions.json', (req: express.Request, res: express.Response) => {
|
||||
return res.json({
|
||||
peertube: {
|
||||
latestVersion: this.latestVersion
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.server = await randomListen(app)
|
||||
|
||||
return getPort(this.server)
|
||||
}
|
||||
|
||||
setLatestVersion (latestVersion: string) {
|
||||
this.latestVersion = latestVersion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import got, { RequestError } from 'got'
|
||||
import { Server } from 'http'
|
||||
import { pipeline } from 'stream'
|
||||
import { ObjectStorageCommand } from '@peertube/peertube-server-commands'
|
||||
import { getPort, randomListen, terminateServer } from './shared.js'
|
||||
|
||||
export class MockObjectStorageProxy {
|
||||
private server: Server
|
||||
|
||||
async initialize () {
|
||||
const app = express()
|
||||
|
||||
app.get('/:bucketName/:path(*)', (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const url = `http://${req.params.bucketName}.${ObjectStorageCommand.getMockEndpointHost()}/${req.params.path}`
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log('Receiving request on mocked server %s.', req.url)
|
||||
console.log('Proxifying request to %s', url)
|
||||
}
|
||||
|
||||
return pipeline(
|
||||
got.stream(url, { throwHttpErrors: false }),
|
||||
res,
|
||||
(err: RequestError) => {
|
||||
if (!err) return
|
||||
|
||||
console.error('Pipeline failed.', err)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
this.server = await randomListen(app)
|
||||
|
||||
return getPort(this.server)
|
||||
}
|
||||
|
||||
terminate () {
|
||||
return terminateServer(this.server)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import express, { Request, Response } from 'express'
|
||||
import { Server } from 'http'
|
||||
import { getPort, randomListen, terminateServer } from './shared.js'
|
||||
|
||||
type BlocklistResponse = {
|
||||
data: {
|
||||
value: string
|
||||
action?: 'add' | 'remove'
|
||||
updatedAt?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export class MockBlocklist {
|
||||
private body: BlocklistResponse
|
||||
private server: Server
|
||||
|
||||
async initialize () {
|
||||
const app = express()
|
||||
|
||||
app.get('/blocklist', (req: Request, res: Response) => {
|
||||
return res.json(this.body)
|
||||
})
|
||||
|
||||
this.server = await randomListen(app)
|
||||
|
||||
return getPort(this.server)
|
||||
}
|
||||
|
||||
replace (body: BlocklistResponse) {
|
||||
this.body = body
|
||||
}
|
||||
|
||||
terminate () {
|
||||
return terminateServer(this.server)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createServer, Server } from 'http'
|
||||
import { createProxy } from 'proxy'
|
||||
import { getPort, terminateServer } from './shared.js'
|
||||
|
||||
class MockProxy {
|
||||
private server: Server
|
||||
|
||||
initialize () {
|
||||
return new Promise<number>(res => {
|
||||
this.server = createProxy(createServer())
|
||||
this.server.listen(0, () => res(getPort(this.server)))
|
||||
})
|
||||
}
|
||||
|
||||
terminate () {
|
||||
return terminateServer(this.server)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
MockProxy
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Express } from 'express'
|
||||
import { Server } from 'http'
|
||||
import { AddressInfo } from 'net'
|
||||
|
||||
function randomListen (app: Express) {
|
||||
return new Promise<Server>(res => {
|
||||
const server = app.listen(0, () => res(server))
|
||||
})
|
||||
}
|
||||
|
||||
function getPort (server: Server) {
|
||||
const address = server.address() as AddressInfo
|
||||
|
||||
return address.port
|
||||
}
|
||||
|
||||
function terminateServer (server: Server) {
|
||||
if (!server) return Promise.resolve()
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
server.close(err => {
|
||||
if (err) return rej(err)
|
||||
|
||||
return res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
randomListen,
|
||||
getPort,
|
||||
terminateServer
|
||||
}
|
||||
@@ -0,0 +1,989 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
AbuseState,
|
||||
AbuseStateType,
|
||||
PluginType_Type,
|
||||
UserNotification,
|
||||
UserNotificationSetting,
|
||||
UserNotificationSettingValue,
|
||||
UserNotificationType,
|
||||
UserNotificationType_Type
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
ConfigCommand,
|
||||
PeerTubeServer,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
setDefaultChannelAvatar,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
import { inspect } from 'util'
|
||||
import { MockSmtpServer } from './mock-servers/index.js'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
|
||||
type CheckerBaseParams = {
|
||||
server: PeerTubeServer
|
||||
emails: any[]
|
||||
socketNotifications: UserNotification[]
|
||||
token: string
|
||||
check?: { web: boolean, mail: boolean }
|
||||
}
|
||||
|
||||
type CheckerType = 'presence' | 'absence'
|
||||
|
||||
function getAllNotificationsSettings (): UserNotificationSetting {
|
||||
return {
|
||||
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
myVideoTranscriptionGenerated: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||
newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilNotification (options: {
|
||||
server: PeerTubeServer
|
||||
notificationType: UserNotificationType_Type
|
||||
token: string
|
||||
fromDate: Date
|
||||
}) {
|
||||
const { server, fromDate, notificationType, token } = options
|
||||
|
||||
do {
|
||||
const { data } = await server.notifications.list({ start: 0, count: 5, token })
|
||||
if (data.some(n => n.type === notificationType && new Date(n.createdAt) >= fromDate)) break
|
||||
|
||||
await wait(500)
|
||||
} while (true)
|
||||
|
||||
await waitJobs([ server ])
|
||||
}
|
||||
|
||||
async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkVideo(notification.video, videoName, shortUUID)
|
||||
checkActor(notification.video.channel)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewLiveFromSubscription (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkVideo(notification.video, videoName, shortUUID)
|
||||
checkActor(notification.video.channel)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n === undefined || n.type !== UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION || n.video.name !== videoName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkMyVideoIsPublished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkVideo(notification.video, videoName, shortUUID)
|
||||
checkActor(notification.video.channel)
|
||||
} else {
|
||||
expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
return text.includes(shortUUID) && text.includes('Your video')
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkVideoStudioEditionIsFinished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { videoName, shortUUID } = options
|
||||
const notificationType = UserNotificationType.MY_VIDEO_STUDIO_EDITION_FINISHED
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkVideo(notification.video, videoName, shortUUID)
|
||||
checkActor(notification.video.channel)
|
||||
} else {
|
||||
expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
return text.includes(shortUUID) && text.includes('Edition of your video')
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
url: string
|
||||
success: boolean
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { videoName, shortUUID, url, success } = options
|
||||
|
||||
const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.videoImport.targetUrl).to.equal(url)
|
||||
|
||||
if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
|
||||
} else {
|
||||
expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
const toFind = success ? ' finished' : ' error'
|
||||
|
||||
return text.includes(url) && text.includes(toFind)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserRegistered (options: CheckerBaseParams & {
|
||||
username: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { username } = options
|
||||
const notificationType = UserNotificationType.NEW_USER_REGISTRATION
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkActor(notification.account, { withAvatar: false })
|
||||
expect(notification.account.name).to.equal(username)
|
||||
} else {
|
||||
expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(' registered.') && text.includes(username)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkRegistrationRequest (options: CheckerBaseParams & {
|
||||
username: string
|
||||
registrationReason: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { username, registrationReason } = options
|
||||
const notificationType = UserNotificationType.NEW_USER_REGISTRATION_REQUEST
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.registration.username).to.equal(username)
|
||||
} else {
|
||||
expect(notification).to.satisfy(n => n.type !== notificationType || n.registration.username !== username)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(' wants to register ') && text.includes(username) && text.includes(registrationReason)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkNewActorFollow (options: CheckerBaseParams & {
|
||||
followType: 'channel' | 'account'
|
||||
followerName: string
|
||||
followerDisplayName: string
|
||||
followingDisplayName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { followType, followerName, followerDisplayName, followingDisplayName } = options
|
||||
const notificationType = UserNotificationType.NEW_FOLLOW
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkActor(notification.actorFollow.follower)
|
||||
expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
|
||||
expect(notification.actorFollow.follower.name).to.equal(followerName)
|
||||
expect(notification.actorFollow.follower.host).to.not.be.undefined
|
||||
|
||||
const following = notification.actorFollow.following
|
||||
expect(following.displayName).to.equal(followingDisplayName)
|
||||
expect(following.type).to.equal(followType)
|
||||
} else {
|
||||
expect(notification).to.satisfy(n => {
|
||||
return n.type !== notificationType ||
|
||||
(n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewInstanceFollower (options: CheckerBaseParams & {
|
||||
followerHost: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { followerHost } = options
|
||||
const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkActor(notification.actorFollow.follower, { withAvatar: false })
|
||||
expect(notification.actorFollow.follower.name).to.equal('peertube')
|
||||
expect(notification.actorFollow.follower.host).to.equal(followerHost)
|
||||
|
||||
expect(notification.actorFollow.following.name).to.equal('peertube')
|
||||
} else {
|
||||
expect(notification).to.satisfy(n => {
|
||||
return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes('instance has a new follower') && text.includes(followerHost)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
|
||||
followerHost: string
|
||||
followingHost: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { followerHost, followingHost } = options
|
||||
const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
const following = notification.actorFollow.following
|
||||
|
||||
checkActor(following, { withAvatar: false })
|
||||
expect(following.name).to.equal('peertube')
|
||||
expect(following.host).to.equal(followingHost)
|
||||
|
||||
expect(notification.actorFollow.follower.name).to.equal('peertube')
|
||||
expect(notification.actorFollow.follower.host).to.equal(followerHost)
|
||||
} else {
|
||||
expect(notification).to.satisfy(n => {
|
||||
return n.type !== notificationType || n.actorFollow.following.host !== followingHost
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(' automatically followed a new instance') && text.includes(followingHost)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkCommentMention (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
commentId: number
|
||||
threadId: number
|
||||
byAccountDisplayName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { shortUUID, commentId, threadId, byAccountDisplayName } = options
|
||||
const notificationType = UserNotificationType.COMMENT_MENTION
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkComment(notification.comment, commentId, threadId)
|
||||
checkActor(notification.comment.account)
|
||||
expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
|
||||
|
||||
checkVideo(notification.comment.video, undefined, shortUUID)
|
||||
} else {
|
||||
expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
|
||||
return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
let lastEmailCount = 0
|
||||
|
||||
async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
commentId: number
|
||||
threadId: number
|
||||
checkType: CheckerType
|
||||
approval?: boolean // default false
|
||||
}) {
|
||||
const { server, shortUUID, commentId, threadId, checkType, emails, approval = false } = options
|
||||
const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
checkComment(notification.comment, commentId, threadId)
|
||||
checkActor(notification.comment.account)
|
||||
checkVideo(notification.comment.video, undefined, shortUUID)
|
||||
|
||||
expect(notification.comment.heldForReview).to.equal(approval)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.comment === undefined || n.comment.id !== commentId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const commentUrl = approval
|
||||
? `${server.url}/my-account/videos/comments?search=heldForReview:true`
|
||||
: `${server.url}/w/${shortUUID};threadId=${threadId}`
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
|
||||
return text.includes(commentUrl) &&
|
||||
(approval && text.includes('requires approval')) ||
|
||||
(!approval && !text.includes('requires approval'))
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
|
||||
if (checkType === 'presence') {
|
||||
// We cannot detect email duplicates, so check we received another email
|
||||
expect(emails).to.have.length.above(lastEmailCount)
|
||||
lastEmailCount = emails.length
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { shortUUID, videoName } = options
|
||||
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.abuse.id).to.be.a('number')
|
||||
checkVideo(notification.abuse.video, videoName, shortUUID)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewAbuseMessage (options: CheckerBaseParams & {
|
||||
abuseId: number
|
||||
message: string
|
||||
toEmail: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { abuseId, message, toEmail } = options
|
||||
const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.abuse.id).to.equal(abuseId)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
const to = email['to'].filter(t => t.address === toEmail)
|
||||
|
||||
return text.indexOf(message) !== -1 && to.length !== 0
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkAbuseStateChange (options: CheckerBaseParams & {
|
||||
abuseId: number
|
||||
state: AbuseStateType
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { abuseId, state } = options
|
||||
const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.abuse.id).to.equal(abuseId)
|
||||
expect(notification.abuse.state).to.equal(state)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.abuse === undefined || n.abuse.id !== abuseId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
|
||||
const contains = state === AbuseState.ACCEPTED
|
||||
? ' accepted'
|
||||
: ' rejected'
|
||||
|
||||
return text.indexOf(contains) !== -1
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { shortUUID, videoName } = options
|
||||
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.abuse.id).to.be.a('number')
|
||||
checkVideo(notification.abuse.comment.video, videoName, shortUUID)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
|
||||
displayName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { displayName } = options
|
||||
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.abuse.id).to.be.a('number')
|
||||
expect(notification.abuse.account.displayName).to.equal(displayName)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.abuse === undefined || n.abuse.account.displayName !== displayName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { shortUUID, videoName } = options
|
||||
const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.videoBlacklist.video.id).to.be.a('number')
|
||||
checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.video === undefined || n.video.shortUUID !== shortUUID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
|
||||
shortUUID: string
|
||||
videoName: string
|
||||
blacklistType: 'blacklist' | 'unblacklist'
|
||||
}) {
|
||||
const { videoName, shortUUID, blacklistType } = options
|
||||
const notificationType = blacklistType === 'blacklist'
|
||||
? UserNotificationType.BLACKLIST_ON_MY_VIDEO
|
||||
: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
|
||||
|
||||
function notificationChecker (notification: UserNotification) {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
|
||||
|
||||
checkVideo(video, videoName, shortUUID)
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
const blacklistText = blacklistType === 'blacklist'
|
||||
? 'blacklisted'
|
||||
: 'unblacklisted'
|
||||
|
||||
return text.includes(shortUUID) && text.includes(blacklistText)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
|
||||
}
|
||||
|
||||
async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
|
||||
latestVersion: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { latestVersion } = options
|
||||
const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.peertube).to.exist
|
||||
expect(notification.peertube.latestVersion).to.equal(latestVersion)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.peertube === undefined || n.peertube.latestVersion !== latestVersion
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
|
||||
return text.includes(latestVersion)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkNewPluginVersion (options: CheckerBaseParams & {
|
||||
pluginType: PluginType_Type
|
||||
pluginName: string
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { pluginName, pluginType } = options
|
||||
const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.plugin.name).to.equal(pluginName)
|
||||
expect(notification.plugin.type).to.equal(pluginType)
|
||||
} else {
|
||||
expect(notification).to.satisfy((n: UserNotification) => {
|
||||
return n?.plugin === undefined || n.plugin.name !== pluginName
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text = email['text']
|
||||
|
||||
return text.includes(pluginName)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function checkMyVideoTranscriptionGenerated (options: CheckerBaseParams & {
|
||||
videoName: string
|
||||
shortUUID: string
|
||||
language: {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { videoName, shortUUID, language } = options
|
||||
const notificationType = UserNotificationType.MY_VIDEO_TRANSCRIPTION_GENERATED
|
||||
|
||||
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||
if (checkType === 'presence') {
|
||||
expect(notification).to.not.be.undefined
|
||||
expect(notification.type).to.equal(notificationType)
|
||||
|
||||
expect(notification.videoCaption).to.exist
|
||||
expect(notification.videoCaption.language.id).to.equal(language.id)
|
||||
expect(notification.videoCaption.language.label).to.equal(language.label)
|
||||
checkVideo(notification.videoCaption.video, videoName, shortUUID)
|
||||
} else {
|
||||
expect(notification.videoCaption).to.satisfy(c => c === undefined || c.Video.shortUUID !== shortUUID)
|
||||
}
|
||||
}
|
||||
|
||||
function emailNotificationFinder (email: object) {
|
||||
const text: string = email['text']
|
||||
return text.includes(shortUUID) && text.includes('Transcription in ' + language.label)
|
||||
}
|
||||
|
||||
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||
}
|
||||
|
||||
async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
|
||||
const userNotifications: UserNotification[] = []
|
||||
const adminNotifications: UserNotification[] = []
|
||||
const adminNotificationsServer2: UserNotification[] = []
|
||||
const emails: object[] = []
|
||||
|
||||
const port = await MockSmtpServer.Instance.collectEmails(emails)
|
||||
|
||||
const overrideConfig = {
|
||||
...ConfigCommand.getEmailOverrideConfig(port),
|
||||
|
||||
signup: {
|
||||
limit: 20
|
||||
}
|
||||
}
|
||||
const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
|
||||
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
await setDefaultChannelAvatar(servers)
|
||||
await setDefaultAccountAvatar(servers)
|
||||
|
||||
if (servers[1]) {
|
||||
await servers[1].config.enableStudio()
|
||||
await servers[1].config.enableLive({ allowReplay: true, transcoding: false })
|
||||
}
|
||||
|
||||
if (serversCount > 1) {
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
}
|
||||
|
||||
const user = { username: 'user_1', password: 'super password' }
|
||||
await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
|
||||
const userAccessToken = await servers[0].login.getAccessToken(user)
|
||||
|
||||
await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
|
||||
await servers[0].users.updateMyAvatar({ token: userAccessToken, fixture: 'avatar.png' })
|
||||
await servers[0].channels.updateImage({ channelName: 'user_1_channel', token: userAccessToken, fixture: 'avatar.png', type: 'avatar' })
|
||||
|
||||
await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
|
||||
|
||||
if (serversCount > 1) {
|
||||
await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
|
||||
}
|
||||
|
||||
{
|
||||
const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
|
||||
socket.on('new-notification', n => userNotifications.push(n))
|
||||
}
|
||||
{
|
||||
const socket = servers[0].socketIO.getUserNotificationSocket()
|
||||
socket.on('new-notification', n => adminNotifications.push(n))
|
||||
}
|
||||
|
||||
if (serversCount > 1) {
|
||||
const socket = servers[1].socketIO.getUserNotificationSocket()
|
||||
socket.on('new-notification', n => adminNotificationsServer2.push(n))
|
||||
}
|
||||
|
||||
const { videoChannels } = await servers[0].users.getMyInfo()
|
||||
const channelId = videoChannels[0].id
|
||||
|
||||
return {
|
||||
userNotifications,
|
||||
adminNotifications,
|
||||
adminNotificationsServer2,
|
||||
userAccessToken,
|
||||
emails,
|
||||
servers,
|
||||
channelId,
|
||||
baseOverrideConfig: overrideConfig
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
type CheckerType,
|
||||
type CheckerBaseParams,
|
||||
|
||||
getAllNotificationsSettings,
|
||||
|
||||
waitUntilNotification,
|
||||
|
||||
checkMyVideoImportIsFinished,
|
||||
checkUserRegistered,
|
||||
checkAutoInstanceFollowing,
|
||||
checkMyVideoIsPublished,
|
||||
checkNewLiveFromSubscription,
|
||||
checkNewVideoFromSubscription,
|
||||
checkNewActorFollow,
|
||||
checkNewCommentOnMyVideo,
|
||||
checkNewBlacklistOnMyVideo,
|
||||
checkCommentMention,
|
||||
checkNewVideoAbuseForModerators,
|
||||
checkVideoAutoBlacklistForModerators,
|
||||
checkNewAbuseMessage,
|
||||
checkAbuseStateChange,
|
||||
checkNewInstanceFollower,
|
||||
prepareNotificationsTest,
|
||||
checkNewCommentAbuseForModerators,
|
||||
checkNewAccountAbuseForModerators,
|
||||
checkNewPeerTubeVersion,
|
||||
checkNewPluginVersion,
|
||||
checkVideoStudioEditionIsFinished,
|
||||
checkRegistrationRequest,
|
||||
checkMyVideoTranscriptionGenerated
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkNotification (options: CheckerBaseParams & {
|
||||
notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
|
||||
emailNotificationFinder: (email: object) => boolean
|
||||
checkType: CheckerType
|
||||
}) {
|
||||
const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
|
||||
|
||||
const check = options.check || { web: true, mail: true }
|
||||
|
||||
if (check.web) {
|
||||
const notification = await server.notifications.getLatest({ token })
|
||||
|
||||
if (notification || checkType !== 'absence') {
|
||||
notificationChecker(notification, checkType)
|
||||
}
|
||||
|
||||
const socketNotification = socketNotifications.find(n => {
|
||||
try {
|
||||
notificationChecker(n, 'presence')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (checkType === 'presence') {
|
||||
const obj = inspect(socketNotifications, { depth: 5 })
|
||||
expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
|
||||
} else {
|
||||
const obj = inspect(socketNotification, { depth: 5 })
|
||||
expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (check.mail) {
|
||||
// Last email
|
||||
const email = emails.slice()
|
||||
.reverse()
|
||||
.find(e => emailNotificationFinder(e))
|
||||
|
||||
if (checkType === 'presence') {
|
||||
const texts = emails.map(e => e.text)
|
||||
expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined
|
||||
} else {
|
||||
expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkVideo (video: any, videoName?: string, shortUUID?: string) {
|
||||
if (videoName) {
|
||||
expect(video.name).to.be.a('string')
|
||||
expect(video.name).to.not.be.empty
|
||||
expect(video.name).to.equal(videoName)
|
||||
}
|
||||
|
||||
if (shortUUID) {
|
||||
expect(video.shortUUID).to.be.a('string')
|
||||
expect(video.shortUUID).to.not.be.empty
|
||||
expect(video.shortUUID).to.equal(shortUUID)
|
||||
}
|
||||
|
||||
expect(video.id).to.be.a('number')
|
||||
}
|
||||
|
||||
function checkActor (actor: any, options: { withAvatar?: boolean } = {}) {
|
||||
const { withAvatar = true } = options
|
||||
|
||||
expect(actor.displayName).to.be.a('string')
|
||||
expect(actor.displayName).to.not.be.empty
|
||||
expect(actor.host).to.not.be.undefined
|
||||
|
||||
if (withAvatar) {
|
||||
expect(actor.avatars).to.be.an('array')
|
||||
expect(actor.avatars).to.have.lengthOf(4)
|
||||
expect(actor.avatars[0].path).to.exist.and.not.empty
|
||||
}
|
||||
}
|
||||
|
||||
function checkComment (comment: any, commentId: number, threadId: number) {
|
||||
expect(comment.id).to.equal(commentId)
|
||||
expect(comment.threadId).to.equal(threadId)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { ChildProcess, fork, ForkOptions } from 'child_process'
|
||||
import { execaNode } from 'execa'
|
||||
import { join } from 'path'
|
||||
import { root } from '@peertube/peertube-node-utils'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { RunnerJobType } from '../../../models/src/runners/runner-job-type.type.js'
|
||||
|
||||
export class PeerTubeRunnerProcess {
|
||||
private app?: ChildProcess
|
||||
|
||||
constructor (private readonly server: PeerTubeServer) {
|
||||
|
||||
}
|
||||
|
||||
runServer (options: {
|
||||
jobType?: RunnerJobType
|
||||
hideLogs?: boolean // default true
|
||||
} = {}) {
|
||||
const { jobType, hideLogs = true } = options
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
const args = [ 'server', '--verbose', ...this.buildIdArg() ]
|
||||
|
||||
if (jobType) {
|
||||
args.push('--enable-job')
|
||||
args.push(jobType)
|
||||
}
|
||||
|
||||
const forkOptions: ForkOptions = {
|
||||
detached: false,
|
||||
silent: true,
|
||||
execArgv: [] // Don't inject parent node options
|
||||
}
|
||||
|
||||
this.app = fork(this.getRunnerPath(), args, forkOptions)
|
||||
|
||||
this.app.stderr.on('data', data => {
|
||||
console.error(data.toString())
|
||||
})
|
||||
|
||||
this.app.stdout.on('data', data => {
|
||||
const str = data.toString() as string
|
||||
|
||||
if (!hideLogs) {
|
||||
console.log(str)
|
||||
}
|
||||
})
|
||||
|
||||
res()
|
||||
})
|
||||
}
|
||||
|
||||
registerPeerTubeInstance (options: {
|
||||
registrationToken: string
|
||||
runnerName: string
|
||||
runnerDescription?: string
|
||||
}) {
|
||||
const { registrationToken, runnerName, runnerDescription } = options
|
||||
|
||||
const args = [
|
||||
'register',
|
||||
'--url', this.server.url,
|
||||
'--registration-token', registrationToken,
|
||||
'--runner-name', runnerName,
|
||||
...this.buildIdArg()
|
||||
]
|
||||
|
||||
if (runnerDescription) {
|
||||
args.push('--runner-description')
|
||||
args.push(runnerDescription)
|
||||
}
|
||||
|
||||
return this.runCommand(this.getRunnerPath(), args)
|
||||
}
|
||||
|
||||
unregisterPeerTubeInstance (options: {
|
||||
runnerName: string
|
||||
}) {
|
||||
const { runnerName } = options
|
||||
|
||||
const args = [ 'unregister', '--url', this.server.url, '--runner-name', runnerName, ...this.buildIdArg() ]
|
||||
return this.runCommand(this.getRunnerPath(), args)
|
||||
}
|
||||
|
||||
async listRegisteredPeerTubeInstances () {
|
||||
const args = [ 'list-registered', ...this.buildIdArg() ]
|
||||
const { stdout } = await this.runCommand(this.getRunnerPath(), args)
|
||||
|
||||
return stdout
|
||||
}
|
||||
|
||||
kill () {
|
||||
if (!this.app) return
|
||||
|
||||
process.kill(this.app.pid)
|
||||
|
||||
this.app = null
|
||||
}
|
||||
|
||||
getId () {
|
||||
return 'test-' + this.server.internalServerNumber
|
||||
}
|
||||
|
||||
private getRunnerPath () {
|
||||
return join(root(), 'apps', 'peertube-runner', 'dist', 'peertube-runner.js')
|
||||
}
|
||||
|
||||
private buildIdArg () {
|
||||
return [ '--id', this.getId() ]
|
||||
}
|
||||
|
||||
private runCommand (path: string, args: string[]) {
|
||||
return execaNode(path, args, { env: { ...process.env, NODE_OPTIONS: '' } })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
|
||||
async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
|
||||
const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
|
||||
|
||||
const registeredSettings = body.registeredSettings
|
||||
expect(registeredSettings).to.have.length.at.least(1)
|
||||
|
||||
const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
|
||||
expect(adminNameSettings).to.not.be.undefined
|
||||
}
|
||||
|
||||
export {
|
||||
testHelloWorldRegisteredSettings
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { doRequest } from '@peertube/peertube-server/core/helpers/requests.js'
|
||||
|
||||
export function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
|
||||
const options = {
|
||||
method: 'POST' as 'POST',
|
||||
json: body,
|
||||
httpSignature,
|
||||
headers
|
||||
}
|
||||
|
||||
return doRequest(url, options)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { QueryTypes, Sequelize } from 'sequelize'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { FileStorageType } from '@peertube/peertube-models'
|
||||
|
||||
export class SQLCommand {
|
||||
private sequelize: Sequelize
|
||||
|
||||
constructor (private readonly server: PeerTubeServer) {
|
||||
|
||||
}
|
||||
|
||||
deleteAll (table: string) {
|
||||
const seq = this.getSequelize()
|
||||
|
||||
const options = { type: QueryTypes.DELETE }
|
||||
|
||||
return seq.query(`DELETE FROM "${table}"`, options)
|
||||
}
|
||||
|
||||
async getVideoShareCount () {
|
||||
const [ { total } ] = await this.selectQuery<{ total: string }>(`SELECT COUNT(*) as total FROM "videoShare"`)
|
||||
if (total === null) return 0
|
||||
|
||||
return parseInt(total, 10)
|
||||
}
|
||||
|
||||
async getInternalFileUrl (fileId: number) {
|
||||
return this.selectQuery<{ fileUrl: string }>(`SELECT "fileUrl" FROM "videoFile" WHERE id = :fileId`, { fileId })
|
||||
.then(rows => rows[0].fileUrl)
|
||||
}
|
||||
|
||||
setActorField (to: string, field: string, value: string) {
|
||||
return this.updateQuery(`UPDATE actor SET ${this.escapeColumnName(field)} = :value WHERE url = :to`, { value, to })
|
||||
}
|
||||
|
||||
setVideoField (uuid: string, field: string, value: string) {
|
||||
return this.updateQuery(`UPDATE video SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid })
|
||||
}
|
||||
|
||||
setPlaylistField (uuid: string, field: string, value: string) {
|
||||
return this.updateQuery(`UPDATE "videoPlaylist" SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid })
|
||||
}
|
||||
|
||||
async countVideoViewsOf (uuid: string) {
|
||||
const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
|
||||
`INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = :uuid`
|
||||
|
||||
const [ { total } ] = await this.selectQuery<{ total: number }>(query, { uuid })
|
||||
if (!total) return 0
|
||||
|
||||
return forceNumber(total)
|
||||
}
|
||||
|
||||
getActorImage (filename: string) {
|
||||
return this.selectQuery<{ width: number, height: number }>(`SELECT * FROM "actorImage" WHERE filename = :filename`, { filename })
|
||||
.then(rows => rows[0])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async setVideoFileStorageOf (uuid: string, storage: FileStorageType) {
|
||||
await this.updateQuery(
|
||||
`UPDATE "videoFile" SET storage = :storage ` +
|
||||
`WHERE "videoId" IN (SELECT id FROM "video" WHERE uuid = :uuid) OR ` +
|
||||
// eslint-disable-next-line max-len
|
||||
`"videoStreamingPlaylistId" IN (` +
|
||||
`SELECT "videoStreamingPlaylist".id FROM "videoStreamingPlaylist" ` +
|
||||
`INNER JOIN video ON video.id = "videoStreamingPlaylist"."videoId" AND "video".uuid = :uuid` +
|
||||
`)`,
|
||||
{ storage, uuid }
|
||||
)
|
||||
|
||||
await this.updateQuery(
|
||||
`UPDATE "videoSource" SET storage = :storage WHERE "videoId" IN (SELECT id FROM "video" WHERE uuid = :uuid)`,
|
||||
{ storage, uuid }
|
||||
)
|
||||
}
|
||||
|
||||
async setUserExportStorageOf (userId: number, storage: FileStorageType) {
|
||||
await this.updateQuery(`UPDATE "userExport" SET storage = :storage WHERE "userId" = :userId`, { storage, userId })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
setPluginVersion (pluginName: string, newVersion: string) {
|
||||
return this.setPluginField(pluginName, 'version', newVersion)
|
||||
}
|
||||
|
||||
setPluginLatestVersion (pluginName: string, newVersion: string) {
|
||||
return this.setPluginField(pluginName, 'latestVersion', newVersion)
|
||||
}
|
||||
|
||||
setPluginField (pluginName: string, field: string, value: string) {
|
||||
return this.updateQuery(
|
||||
`UPDATE "plugin" SET ${this.escapeColumnName(field)} = :value WHERE "name" = :pluginName`,
|
||||
{ pluginName, value }
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
selectQuery <T extends object> (query: string, replacements: { [id: string]: string | number } = {}) {
|
||||
const seq = this.getSequelize()
|
||||
const options = {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
replacements
|
||||
}
|
||||
|
||||
return seq.query<T>(query, options)
|
||||
}
|
||||
|
||||
updateQuery (query: string, replacements: { [id: string]: string | number } = {}) {
|
||||
const seq = this.getSequelize()
|
||||
const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE, replacements }
|
||||
|
||||
return seq.query(query, options)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async getPlaylistInfohash (playlistId: number) {
|
||||
const query = 'SELECT "p2pMediaLoaderInfohashes" FROM "videoStreamingPlaylist" WHERE id = :playlistId'
|
||||
|
||||
const result = await this.selectQuery<{ p2pMediaLoaderInfohashes: string }>(query, { playlistId })
|
||||
if (!result || result.length === 0) return []
|
||||
|
||||
return result[0].p2pMediaLoaderInfohashes
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
setActorFollowScores (newScore: number) {
|
||||
return this.updateQuery(`UPDATE "actorFollow" SET "score" = :newScore`, { newScore })
|
||||
}
|
||||
|
||||
setTokenField (accessToken: string, field: string, value: string) {
|
||||
return this.updateQuery(
|
||||
`UPDATE "oAuthToken" SET ${this.escapeColumnName(field)} = :value WHERE "accessToken" = :accessToken`,
|
||||
{ value, accessToken }
|
||||
)
|
||||
}
|
||||
|
||||
async cleanup () {
|
||||
if (!this.sequelize) return
|
||||
|
||||
await this.sequelize.close()
|
||||
this.sequelize = undefined
|
||||
}
|
||||
|
||||
private getSequelize () {
|
||||
if (this.sequelize) return this.sequelize
|
||||
|
||||
const dbname = 'peertube_test' + this.server.internalServerNumber
|
||||
const username = 'peertube'
|
||||
const password = 'peertube'
|
||||
const host = '127.0.0.1'
|
||||
const port = 5432
|
||||
|
||||
this.sequelize = new Sequelize(dbname, username, password, {
|
||||
dialect: 'postgres',
|
||||
host,
|
||||
port,
|
||||
logging: false
|
||||
})
|
||||
|
||||
return this.sequelize
|
||||
}
|
||||
|
||||
private escapeColumnName (columnName: string) {
|
||||
return this.getSequelize().escape(columnName)
|
||||
.replace(/^'/, '"')
|
||||
.replace(/'$/, '"')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { basename, dirname, join } from 'path'
|
||||
import { removeFragmentedMP4Ext, uuidRegex } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
VideoPrivacy,
|
||||
VideoResolution,
|
||||
VideoStreamingPlaylist,
|
||||
VideoStreamingPlaylistType
|
||||
} from '@peertube/peertube-models'
|
||||
import { sha256 } from '@peertube/peertube-node-utils'
|
||||
import { makeRawRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { expectStartWith } from './checks.js'
|
||||
import { hlsInfohashExist } from './tracker.js'
|
||||
import { checkWebTorrentWorks } from './webtorrent.js'
|
||||
|
||||
async function checkSegmentHash (options: {
|
||||
server: PeerTubeServer
|
||||
baseUrlPlaylist: string
|
||||
baseUrlSegment: string
|
||||
resolution: number
|
||||
hlsPlaylist: VideoStreamingPlaylist
|
||||
token?: string
|
||||
}) {
|
||||
const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist, token } = options
|
||||
const command = server.streamingPlaylists
|
||||
|
||||
const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
|
||||
const videoName = basename(file.fileUrl)
|
||||
|
||||
const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8`, token })
|
||||
|
||||
const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
|
||||
|
||||
const length = parseInt(matches[1], 10)
|
||||
const offset = parseInt(matches[2], 10)
|
||||
const range = `${offset}-${offset + length - 1}`
|
||||
|
||||
const segmentBody = await command.getFragmentedSegment({
|
||||
url: `${baseUrlSegment}/${videoName}`,
|
||||
expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
|
||||
range: `bytes=${range}`,
|
||||
token
|
||||
})
|
||||
|
||||
const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, token })
|
||||
expect(sha256(segmentBody)).to.equal(shaBody[videoName][range], `Invalid sha256 result for ${videoName} range ${range}`)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkLiveSegmentHash (options: {
|
||||
server: PeerTubeServer
|
||||
baseUrlSegment: string
|
||||
videoUUID: string
|
||||
segmentName: string
|
||||
hlsPlaylist: VideoStreamingPlaylist
|
||||
withRetry?: boolean
|
||||
}) {
|
||||
const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist, withRetry = false } = options
|
||||
const command = server.streamingPlaylists
|
||||
|
||||
const segmentBody = await command.getFragmentedSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}`, withRetry })
|
||||
const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry })
|
||||
|
||||
expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkResolutionsInMasterPlaylist (options: {
|
||||
server: PeerTubeServer
|
||||
playlistUrl: string
|
||||
resolutions: number[]
|
||||
token?: string
|
||||
transcoded?: boolean // default true
|
||||
withRetry?: boolean // default false
|
||||
}) {
|
||||
const { server, playlistUrl, resolutions, token, withRetry = false, transcoded = true } = options
|
||||
|
||||
const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl, token, withRetry })
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
const base = '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution
|
||||
|
||||
if (resolution === VideoResolution.H_NOVIDEO) {
|
||||
expect(masterPlaylist).to.match(new RegExp(`${base},CODECS="mp4a.40.2"`))
|
||||
} else if (transcoded) {
|
||||
expect(masterPlaylist).to.match(new RegExp(`${base},(FRAME-RATE=\\d+,)?CODECS="avc1.6400[0-f]{2},mp4a.40.2"`))
|
||||
} else {
|
||||
expect(masterPlaylist).to.match(new RegExp(`${base}`))
|
||||
}
|
||||
}
|
||||
|
||||
const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH='))
|
||||
expect(playlistsLength).to.have.lengthOf(resolutions.length)
|
||||
}
|
||||
|
||||
async function completeCheckHlsPlaylist (options: {
|
||||
servers: PeerTubeServer[]
|
||||
videoUUID: string
|
||||
hlsOnly: boolean
|
||||
|
||||
resolutions?: number[]
|
||||
objectStorageBaseUrl?: string
|
||||
}) {
|
||||
const { videoUUID, hlsOnly, objectStorageBaseUrl } = options
|
||||
|
||||
const resolutions = options.resolutions ?? [ 240, 360, 480, 720 ]
|
||||
|
||||
for (const server of options.servers) {
|
||||
const videoDetails = await server.videos.getWithToken({ id: videoUUID })
|
||||
const requiresAuth = videoDetails.privacy.id === VideoPrivacy.PRIVATE || videoDetails.privacy.id === VideoPrivacy.INTERNAL
|
||||
|
||||
const privatePath = requiresAuth
|
||||
? 'private/'
|
||||
: ''
|
||||
const token = requiresAuth
|
||||
? server.accessToken
|
||||
: undefined
|
||||
|
||||
const baseUrl = `http://${videoDetails.account.host}`
|
||||
|
||||
expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
|
||||
|
||||
const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||
expect(hlsPlaylist).to.not.be.undefined
|
||||
|
||||
const hlsFiles = hlsPlaylist.files
|
||||
expect(hlsFiles).to.have.lengthOf(resolutions.length)
|
||||
|
||||
if (hlsOnly) expect(videoDetails.files).to.have.lengthOf(0)
|
||||
else expect(videoDetails.files).to.have.lengthOf(resolutions.length)
|
||||
|
||||
// Check JSON files
|
||||
for (const resolution of resolutions) {
|
||||
const file = hlsFiles.find(f => f.resolution.id === resolution)
|
||||
expect(file).to.not.be.undefined
|
||||
|
||||
if (file.resolution.id === VideoResolution.H_NOVIDEO) {
|
||||
expect(file.resolution.label).to.equal('Audio')
|
||||
} else {
|
||||
expect(file.resolution.label).to.equal(resolution + 'p')
|
||||
}
|
||||
|
||||
if (resolution === 0) {
|
||||
expect(file.height).to.equal(0)
|
||||
expect(file.width).to.equal(0)
|
||||
} else {
|
||||
expect(Math.min(file.height, file.width)).to.equal(resolution)
|
||||
expect(Math.max(file.height, file.width)).to.be.greaterThan(resolution)
|
||||
}
|
||||
|
||||
expect(file.magnetUri).to.have.lengthOf.above(2)
|
||||
await checkWebTorrentWorks(file.magnetUri)
|
||||
|
||||
{
|
||||
const nameReg = `${uuidRegex}-${file.resolution.id}`
|
||||
|
||||
expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}-hls.torrent`))
|
||||
|
||||
if (objectStorageBaseUrl && requiresAuth) {
|
||||
// eslint-disable-next-line max-len
|
||||
expect(file.fileUrl).to.match(new RegExp(`${server.url}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`))
|
||||
} else if (objectStorageBaseUrl) {
|
||||
expectStartWith(file.fileUrl, objectStorageBaseUrl)
|
||||
} else {
|
||||
expect(file.fileUrl).to.match(
|
||||
new RegExp(`${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoDetails.uuid}/${nameReg}-fragmented.mp4`)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
await Promise.all([
|
||||
makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
|
||||
makeRawRequest({
|
||||
url: file.fileDownloadUrl,
|
||||
token,
|
||||
expectedStatus: objectStorageBaseUrl
|
||||
? HttpStatusCode.FOUND_302
|
||||
: HttpStatusCode.OK_200
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// Check master playlist
|
||||
{
|
||||
await checkResolutionsInMasterPlaylist({ server, token, playlistUrl: hlsPlaylist.playlistUrl, resolutions })
|
||||
|
||||
const masterPlaylist = await server.streamingPlaylists.get({ url: hlsPlaylist.playlistUrl, token })
|
||||
|
||||
let i = 0
|
||||
for (const resolution of resolutions) {
|
||||
expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
|
||||
expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
|
||||
|
||||
const url = 'http://' + videoDetails.account.host
|
||||
await hlsInfohashExist(url, hlsPlaylist.playlistUrl, i)
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// Check resolution playlists
|
||||
{
|
||||
for (const resolution of resolutions) {
|
||||
const file = hlsFiles.find(f => f.resolution.id === resolution)
|
||||
const playlistName = removeFragmentedMP4Ext(basename(file.fileUrl)) + '.m3u8'
|
||||
|
||||
let url: string
|
||||
if (objectStorageBaseUrl && requiresAuth) {
|
||||
url = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
|
||||
} else if (objectStorageBaseUrl) {
|
||||
url = `${objectStorageBaseUrl}hls/${videoUUID}/${playlistName}`
|
||||
} else {
|
||||
url = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}/${playlistName}`
|
||||
}
|
||||
|
||||
const subPlaylist = await server.streamingPlaylists.get({ url, token })
|
||||
|
||||
expect(subPlaylist).to.match(new RegExp(`${uuidRegex}-${resolution}-fragmented.mp4`))
|
||||
expect(subPlaylist).to.contain(basename(file.fileUrl))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let baseUrlAndPath: string
|
||||
if (objectStorageBaseUrl && requiresAuth) {
|
||||
baseUrlAndPath = `${baseUrl}/object-storage-proxy/streaming-playlists/hls/${privatePath}${videoUUID}`
|
||||
} else if (objectStorageBaseUrl) {
|
||||
baseUrlAndPath = `${objectStorageBaseUrl}hls/${videoUUID}`
|
||||
} else {
|
||||
baseUrlAndPath = `${baseUrl}/static/streaming-playlists/hls/${privatePath}${videoUUID}`
|
||||
}
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
await checkSegmentHash({
|
||||
server,
|
||||
token,
|
||||
baseUrlPlaylist: baseUrlAndPath,
|
||||
baseUrlSegment: baseUrlAndPath,
|
||||
resolution,
|
||||
hlsPlaylist
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkVideoFileTokenReinjection (options: {
|
||||
server: PeerTubeServer
|
||||
videoUUID: string
|
||||
videoFileToken: string
|
||||
resolutions: number[]
|
||||
isLive: boolean
|
||||
}) {
|
||||
const { server, resolutions, videoFileToken, videoUUID, isLive } = options
|
||||
|
||||
const video = await server.videos.getWithToken({ id: videoUUID })
|
||||
const hls = video.streamingPlaylists[0]
|
||||
|
||||
const query = { videoFileToken, reinjectVideoFileToken: 'true' }
|
||||
const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
for (let i = 0; i < resolutions.length; i++) {
|
||||
const resolution = resolutions[i]
|
||||
|
||||
const suffix = isLive
|
||||
? i
|
||||
: `-${resolution}`
|
||||
|
||||
expect(text).to.contain(`${suffix}.m3u8?videoFileToken=${videoFileToken}&reinjectVideoFileToken=true`)
|
||||
}
|
||||
|
||||
const resolutionPlaylists = extractResolutionPlaylistUrls(hls.playlistUrl, text)
|
||||
expect(resolutionPlaylists).to.have.lengthOf(resolutions.length)
|
||||
|
||||
for (const url of resolutionPlaylists) {
|
||||
const { text } = await makeRawRequest({ url, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const extension = isLive
|
||||
? '.ts'
|
||||
: '.mp4'
|
||||
|
||||
expect(text).to.contain(`${extension}?videoFileToken=${videoFileToken}`)
|
||||
expect(text).not.to.contain(`reinjectVideoFileToken=true`)
|
||||
}
|
||||
}
|
||||
|
||||
function extractResolutionPlaylistUrls (masterPath: string, masterContent: string) {
|
||||
return masterContent.match(/^([^.]+\.m3u8.*)/mg)
|
||||
.map(filename => join(dirname(masterPath), filename))
|
||||
}
|
||||
|
||||
export {
|
||||
checkSegmentHash,
|
||||
checkLiveSegmentHash,
|
||||
checkResolutionsInMasterPlaylist,
|
||||
completeCheckHlsPlaylist,
|
||||
extractResolutionPlaylistUrls,
|
||||
checkVideoFileTokenReinjection
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { expect } from 'chai'
|
||||
import { sha1 } from '@peertube/peertube-node-utils'
|
||||
import { makeGetRequest } from '@peertube/peertube-server-commands'
|
||||
|
||||
async function hlsInfohashExist (serverUrl: string, masterPlaylistUrl: string, fileNumber: number) {
|
||||
const path = '/tracker/announce'
|
||||
|
||||
const infohash = sha1(`2${masterPlaylistUrl}+V${fileNumber}`)
|
||||
|
||||
// From bittorrent-tracker
|
||||
const infohashBinary = escape(Buffer.from(infohash, 'hex').toString('binary')).replace(/[@*/+]/g, function (char) {
|
||||
return '%' + char.charCodeAt(0).toString(16).toUpperCase()
|
||||
})
|
||||
|
||||
const res = await makeGetRequest({
|
||||
url: serverUrl,
|
||||
path,
|
||||
rawQuery: `peer_id=-WW0105-NkvYO/egUAr4&info_hash=${infohashBinary}&port=42100`,
|
||||
expectedStatus: 200
|
||||
})
|
||||
|
||||
expect(res.text).to.not.contain('failure')
|
||||
}
|
||||
|
||||
export {
|
||||
hlsInfohashExist
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { makeGetRequest, PeerTubeServer, VideoEdit } from '@peertube/peertube-server-commands'
|
||||
import { downloadFile, unzip } from '@peertube/peertube-transcription-devtools'
|
||||
import { expect } from 'chai'
|
||||
import { ensureDir, pathExists } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { testCaptionFile } from './captions.js'
|
||||
import { FIXTURE_URLS } from './fixture-urls.js'
|
||||
|
||||
type CustomModelName = 'tiny.pt' | 'faster-whisper-tiny'
|
||||
|
||||
export async function downloadCustomModelsIfNeeded (modelName: CustomModelName) {
|
||||
if (await pathExists(getCustomModelPath(modelName))) return
|
||||
|
||||
await ensureDir(getCustomModelDirectory())
|
||||
await unzip(await downloadFile(FIXTURE_URLS.transcriptionModels, getCustomModelDirectory()))
|
||||
}
|
||||
|
||||
export function getCustomModelDirectory () {
|
||||
return buildAbsoluteFixturePath(join('transcription', 'models-v1'))
|
||||
}
|
||||
|
||||
export function getCustomModelPath (modelName: CustomModelName) {
|
||||
return join(getCustomModelDirectory(), 'models', modelName)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkAutoCaption (
|
||||
servers: PeerTubeServer[],
|
||||
uuid: string,
|
||||
captionContains = new RegExp('^WEBVTT\\n\\n00:00.\\d{3} --> 00:')
|
||||
) {
|
||||
for (const server of servers) {
|
||||
const body = await server.captions.list({ videoId: uuid })
|
||||
expect(body.total).to.equal(1)
|
||||
expect(body.data).to.have.lengthOf(1)
|
||||
|
||||
const caption = body.data[0]
|
||||
expect(caption.language.id).to.equal('en')
|
||||
expect(caption.language.label).to.equal('English')
|
||||
expect(caption.automaticallyGenerated).to.be.true
|
||||
|
||||
{
|
||||
await testCaptionFile(server.url, caption.captionPath, captionContains)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkNoCaption (servers: PeerTubeServer[], uuid: string) {
|
||||
for (const server of servers) {
|
||||
const body = await server.captions.list({ videoId: uuid })
|
||||
expect(body.total).to.equal(0)
|
||||
expect(body.data).to.have.lengthOf(0)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCaptionContent (server: PeerTubeServer, videoId: string, language: string) {
|
||||
const { data } = await server.captions.list({ videoId })
|
||||
|
||||
const caption = data.find(c => c.language.id === language)
|
||||
|
||||
const { text } = await makeGetRequest({ url: server.url, path: caption.captionPath, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkLanguage (servers: PeerTubeServer[], uuid: string, expected: string | null) {
|
||||
for (const server of servers) {
|
||||
const video = await server.videos.get({ id: uuid })
|
||||
|
||||
if (expected) {
|
||||
expect(video.language.id).to.equal(expected)
|
||||
} else {
|
||||
expect(video.language.id).to.be.null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadForTranscription (server: PeerTubeServer, body: Partial<VideoEdit> = {}) {
|
||||
const { uuid } = await server.videos.upload({
|
||||
attributes: {
|
||||
name: 'video',
|
||||
fixture: join('transcription', 'videos', 'the_last_man_on_earth.mp4'),
|
||||
language: undefined,
|
||||
|
||||
...body
|
||||
}
|
||||
})
|
||||
|
||||
return uuid
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { expect } from 'chai'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
|
||||
async function checkPlaylistFilesWereRemoved (
|
||||
playlistUUID: string,
|
||||
server: PeerTubeServer,
|
||||
directories = [ 'thumbnails' ]
|
||||
) {
|
||||
for (const directory of directories) {
|
||||
const directoryPath = server.getDirectoryPath(directory)
|
||||
|
||||
const files = await readdir(directoryPath)
|
||||
for (const file of files) {
|
||||
expect(file).to.not.contain(playlistUUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
checkPlaylistFilesWereRemoved
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
|
||||
|
||||
import { uuidRegex } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
VideoCaption,
|
||||
VideoCommentPolicy,
|
||||
VideoCommentPolicyType,
|
||||
VideoDetails,
|
||||
VideoPrivacy,
|
||||
VideoResolution
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath, getFileSize, getFilenameFromUrl, getLowercaseExtension } from '@peertube/peertube-node-utils'
|
||||
import { PeerTubeServer, VideoEdit, getRedirectionUrl, makeRawRequest, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import {
|
||||
VIDEO_CATEGORIES,
|
||||
VIDEO_LANGUAGES,
|
||||
VIDEO_LICENCES,
|
||||
VIDEO_PRIVACIES,
|
||||
loadLanguages
|
||||
} from '@peertube/peertube-server/core/initializers/constants.js'
|
||||
import { expect } from 'chai'
|
||||
import { pathExists } from 'fs-extra/esm'
|
||||
import { readdir } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { dateIsValid, expectStartWith, testImageGeneratedByFFmpeg } from './checks.js'
|
||||
import { completeCheckHlsPlaylist } from './streaming-playlists.js'
|
||||
import { checkWebTorrentWorks } from './webtorrent.js'
|
||||
|
||||
export async function completeWebVideoFilesCheck (options: {
|
||||
server: PeerTubeServer
|
||||
originServer: PeerTubeServer
|
||||
videoUUID: string
|
||||
fixture: string
|
||||
files: {
|
||||
resolution: number
|
||||
width?: number
|
||||
height?: number
|
||||
size?: number
|
||||
}[]
|
||||
objectStorageBaseUrl?: string
|
||||
}) {
|
||||
const { originServer, server, videoUUID, files, fixture, objectStorageBaseUrl } = options
|
||||
const video = await server.videos.getWithToken({ id: videoUUID })
|
||||
const serverConfig = await originServer.config.getConfig()
|
||||
const requiresAuth = video.privacy.id === VideoPrivacy.PRIVATE || video.privacy.id === VideoPrivacy.INTERNAL
|
||||
|
||||
const transcodingEnabled = serverConfig.transcoding.web_videos.enabled
|
||||
|
||||
expect(files).to.have.lengthOf(files.length)
|
||||
|
||||
for (const attributeFile of files) {
|
||||
const file = video.files.find(f => f.resolution.id === attributeFile.resolution)
|
||||
expect(file, `resolution ${attributeFile.resolution} does not exist`).not.to.be.undefined
|
||||
|
||||
let extension = getLowercaseExtension(fixture)
|
||||
// Transcoding enabled: extension will always be .mp4
|
||||
if (transcodingEnabled) extension = '.mp4'
|
||||
|
||||
expect(file.id).to.exist
|
||||
expect(file.magnetUri).to.have.lengthOf.above(2)
|
||||
|
||||
{
|
||||
const privatePath = requiresAuth
|
||||
? 'private/'
|
||||
: ''
|
||||
const nameReg = `${uuidRegex}-${file.resolution.id}`
|
||||
|
||||
expect(file.torrentDownloadUrl).to.match(new RegExp(`${server.url}/download/torrents/${nameReg}.torrent`))
|
||||
expect(file.torrentUrl).to.match(new RegExp(`${server.url}/lazy-static/torrents/${nameReg}.torrent`))
|
||||
|
||||
if (objectStorageBaseUrl && requiresAuth) {
|
||||
const regexp = new RegExp(`${originServer.url}/object-storage-proxy/web-videos/${privatePath}${nameReg}${extension}`)
|
||||
expect(file.fileUrl).to.match(regexp)
|
||||
} else if (objectStorageBaseUrl) {
|
||||
expectStartWith(file.fileUrl, objectStorageBaseUrl)
|
||||
} else {
|
||||
expect(file.fileUrl).to.match(new RegExp(`${originServer.url}/static/web-videos/${privatePath}${nameReg}${extension}`))
|
||||
}
|
||||
|
||||
expect(file.fileDownloadUrl).to.match(new RegExp(`${originServer.url}/download/videos/${nameReg}${extension}`))
|
||||
}
|
||||
|
||||
{
|
||||
const token = requiresAuth
|
||||
? server.accessToken
|
||||
: undefined
|
||||
|
||||
await Promise.all([
|
||||
makeRawRequest({ url: file.torrentUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
makeRawRequest({ url: file.torrentDownloadUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
makeRawRequest({ url: file.metadataUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
makeRawRequest({ url: file.fileUrl, token, expectedStatus: HttpStatusCode.OK_200 }),
|
||||
makeRawRequest({
|
||||
url: file.fileDownloadUrl,
|
||||
token,
|
||||
expectedStatus: objectStorageBaseUrl
|
||||
? HttpStatusCode.FOUND_302
|
||||
: HttpStatusCode.OK_200
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
expect(file.resolution.id).to.equal(attributeFile.resolution)
|
||||
|
||||
if (file.resolution.id === VideoResolution.H_NOVIDEO) {
|
||||
expect(file.resolution.label).to.equal('Audio')
|
||||
} else {
|
||||
expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
|
||||
}
|
||||
|
||||
if (attributeFile.width !== undefined) expect(file.width).to.equal(attributeFile.width)
|
||||
if (attributeFile.height !== undefined) expect(file.height).to.equal(attributeFile.height)
|
||||
|
||||
if (file.resolution.id === VideoResolution.H_NOVIDEO) {
|
||||
expect(file.height).to.equal(0)
|
||||
expect(file.width).to.equal(0)
|
||||
} else {
|
||||
expect(Math.min(file.height, file.width)).to.equal(file.resolution.id)
|
||||
expect(Math.max(file.height, file.width)).to.be.greaterThan(file.resolution.id)
|
||||
}
|
||||
|
||||
if (attributeFile.size) {
|
||||
const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
|
||||
const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
|
||||
expect(
|
||||
file.size,
|
||||
'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
|
||||
).to.be.above(minSize).and.below(maxSize)
|
||||
}
|
||||
|
||||
await checkWebTorrentWorks(file.magnetUri)
|
||||
}
|
||||
}
|
||||
|
||||
export async function completeVideoCheck (options: {
|
||||
server: PeerTubeServer
|
||||
originServer: PeerTubeServer
|
||||
|
||||
videoUUID: string
|
||||
|
||||
objectStorageBaseUrl?: string
|
||||
|
||||
attributes: {
|
||||
name: string
|
||||
category: number
|
||||
licence: number
|
||||
language: string
|
||||
nsfw: boolean
|
||||
commentsPolicy: VideoCommentPolicyType
|
||||
downloadEnabled: boolean
|
||||
description: string
|
||||
support: string
|
||||
duration: number
|
||||
tags: string[]
|
||||
privacy: number
|
||||
|
||||
publishedAt?: string
|
||||
originallyPublishedAt?: string
|
||||
|
||||
account: {
|
||||
name: string
|
||||
host: string
|
||||
}
|
||||
|
||||
likes?: number
|
||||
dislikes?: number
|
||||
|
||||
channel: {
|
||||
displayName: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
fixture: string
|
||||
|
||||
thumbnailfile?: string
|
||||
previewfile?: string
|
||||
|
||||
files?: {
|
||||
resolution: number
|
||||
size: number
|
||||
width: number
|
||||
height: number
|
||||
}[]
|
||||
|
||||
hls?: {
|
||||
hlsOnly: boolean
|
||||
resolutions: number[]
|
||||
}
|
||||
}
|
||||
}) {
|
||||
const { attributes, originServer, server, videoUUID, objectStorageBaseUrl } = options
|
||||
|
||||
await loadLanguages()
|
||||
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
|
||||
if (!attributes.likes) attributes.likes = 0
|
||||
if (!attributes.dislikes) attributes.dislikes = 0
|
||||
|
||||
expect(video.name).to.equal(attributes.name)
|
||||
expect(video.category.id).to.equal(attributes.category)
|
||||
expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Unknown')
|
||||
|
||||
expect(video.licence.id).to.equal(attributes.licence)
|
||||
expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
|
||||
|
||||
expect(video.language.id).to.equal(attributes.language)
|
||||
expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
|
||||
|
||||
expect(video.privacy.id).to.deep.equal(attributes.privacy)
|
||||
expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
|
||||
|
||||
expect(video.nsfw).to.equal(attributes.nsfw)
|
||||
expect(video.description).to.equal(attributes.description)
|
||||
|
||||
expect(video.likes).to.equal(attributes.likes)
|
||||
expect(video.dislikes).to.equal(attributes.dislikes)
|
||||
|
||||
expect(video.isLocal).to.equal(server.url === originServer.url)
|
||||
expect(video.duration).to.equal(attributes.duration)
|
||||
expect(video.url).to.contain(originServer.host)
|
||||
expect(video.tags).to.deep.equal(attributes.tags)
|
||||
|
||||
expect(video.commentsEnabled).to.equal(attributes.commentsPolicy !== VideoCommentPolicy.DISABLED)
|
||||
expect(video.commentsPolicy.id).to.equal(attributes.commentsPolicy)
|
||||
expect(video.downloadEnabled).to.equal(attributes.downloadEnabled)
|
||||
|
||||
expect(dateIsValid(video.createdAt)).to.be.true
|
||||
expect(dateIsValid(video.publishedAt)).to.be.true
|
||||
expect(dateIsValid(video.updatedAt)).to.be.true
|
||||
|
||||
if (attributes.publishedAt) {
|
||||
expect(video.publishedAt).to.equal(attributes.publishedAt)
|
||||
}
|
||||
|
||||
if (attributes.originallyPublishedAt) {
|
||||
expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
|
||||
} else {
|
||||
expect(video.originallyPublishedAt).to.be.null
|
||||
}
|
||||
|
||||
expect(video.account.id).to.be.a('number')
|
||||
expect(video.account.name).to.equal(attributes.account.name)
|
||||
expect(video.account.host).to.equal(attributes.account.host)
|
||||
|
||||
expect(video.channel.displayName).to.equal(attributes.channel.displayName)
|
||||
expect(video.channel.name).to.equal(attributes.channel.name)
|
||||
expect(video.channel.host).to.equal(attributes.account.host)
|
||||
expect(video.channel.isLocal).to.equal(server.url === originServer.url)
|
||||
expect(video.channel.createdAt).to.exist
|
||||
expect(dateIsValid(video.channel.updatedAt.toString())).to.be.true
|
||||
|
||||
expect(video.thumbnailPath).to.exist
|
||||
await testImageGeneratedByFFmpeg(server.url, attributes.thumbnailfile || attributes.fixture, video.thumbnailPath)
|
||||
|
||||
if (attributes.previewfile) {
|
||||
expect(video.previewPath).to.exist
|
||||
await testImageGeneratedByFFmpeg(server.url, attributes.previewfile, video.previewPath)
|
||||
}
|
||||
|
||||
if (attributes.files) {
|
||||
await completeWebVideoFilesCheck({
|
||||
server,
|
||||
originServer,
|
||||
videoUUID: video.uuid,
|
||||
objectStorageBaseUrl,
|
||||
|
||||
files: attributes.files,
|
||||
fixture: attributes.fixture
|
||||
})
|
||||
}
|
||||
|
||||
if (attributes.hls) {
|
||||
await completeCheckHlsPlaylist({
|
||||
objectStorageBaseUrl,
|
||||
servers: [ server ],
|
||||
videoUUID: video.uuid,
|
||||
hlsOnly: attributes.hls.hlsOnly,
|
||||
resolutions: attributes.hls.resolutions
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkVideoFilesWereRemoved (options: {
|
||||
server: PeerTubeServer
|
||||
video: VideoDetails
|
||||
captions?: VideoCaption[]
|
||||
onlyVideoFiles?: boolean // default false
|
||||
}) {
|
||||
const { video, server, captions = [], onlyVideoFiles = false } = options
|
||||
|
||||
const webVideoFiles = video.files || []
|
||||
const hlsFiles = video.streamingPlaylists[0]?.files || []
|
||||
|
||||
const thumbnailName = basename(video.thumbnailPath)
|
||||
const previewName = basename(video.previewPath)
|
||||
|
||||
const torrentNames = webVideoFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
|
||||
|
||||
const captionNames = captions.map(c => basename(c.captionPath))
|
||||
|
||||
const webVideoFilenames = webVideoFiles.map(f => basename(f.fileUrl))
|
||||
const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
|
||||
|
||||
let directories: { [ directory: string ]: string[] } = {
|
||||
videos: webVideoFilenames,
|
||||
redundancy: webVideoFilenames,
|
||||
[join('playlists', 'hls')]: hlsFilenames,
|
||||
[join('redundancy', 'hls')]: hlsFilenames
|
||||
}
|
||||
|
||||
if (onlyVideoFiles !== true) {
|
||||
directories = {
|
||||
...directories,
|
||||
|
||||
thumbnails: [ thumbnailName ],
|
||||
previews: [ previewName ],
|
||||
torrents: torrentNames,
|
||||
captions: captionNames
|
||||
}
|
||||
}
|
||||
|
||||
for (const directory of Object.keys(directories)) {
|
||||
const directoryPath = server.servers.buildDirectory(directory)
|
||||
|
||||
const directoryExists = await pathExists(directoryPath)
|
||||
if (directoryExists === false) continue
|
||||
|
||||
const existingFiles = await readdir(directoryPath)
|
||||
for (const existingFile of existingFiles) {
|
||||
for (const shouldNotExist of directories[directory]) {
|
||||
expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
|
||||
for (const server of servers) {
|
||||
server.store.videoDetails = await server.videos.get({ id: uuid })
|
||||
}
|
||||
}
|
||||
|
||||
export function checkUploadVideoParam (options: {
|
||||
server: PeerTubeServer
|
||||
token: string
|
||||
attributes: Partial<VideoEdit>
|
||||
expectedStatus?: HttpStatusCodeType
|
||||
completedExpectedStatus?: HttpStatusCodeType
|
||||
mode?: 'legacy' | 'resumable'
|
||||
}) {
|
||||
const { server, token, attributes, completedExpectedStatus, expectedStatus, mode = 'legacy' } = options
|
||||
|
||||
return mode === 'legacy'
|
||||
? server.videos.buildLegacyUpload({ token, attributes, expectedStatus: expectedStatus || completedExpectedStatus })
|
||||
: server.videos.buildResumeVideoUpload({
|
||||
token,
|
||||
fixture: attributes.fixture,
|
||||
attaches: server.videos.buildUploadAttaches(attributes, false),
|
||||
fields: server.videos.buildUploadFields(attributes),
|
||||
expectedStatus,
|
||||
completedExpectedStatus,
|
||||
path: '/api/v1/videos/upload-resumable'
|
||||
})
|
||||
}
|
||||
|
||||
// serverNumber starts from 1
|
||||
export async function uploadRandomVideoOnServers (
|
||||
servers: PeerTubeServer[],
|
||||
serverNumber: number,
|
||||
additionalParams?: VideoEdit & { prefixName?: string }
|
||||
) {
|
||||
const server = servers.find(s => s.serverNumber === serverNumber)
|
||||
const res = await server.videos.randomUpload({ wait: false, additionalParams })
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
export async function checkSourceFile (options: {
|
||||
server: PeerTubeServer
|
||||
fsCount: number
|
||||
uuid: string
|
||||
fixture: string
|
||||
objectStorageBaseUrl?: string // default false
|
||||
}) {
|
||||
const { server, fsCount, fixture, uuid, objectStorageBaseUrl } = options
|
||||
|
||||
const source = await server.videos.getSource({ id: uuid })
|
||||
const fixtureFileSize = await getFileSize(buildAbsoluteFixturePath(fixture))
|
||||
|
||||
if (fsCount > 0) {
|
||||
expect(await server.servers.countFiles('original-video-files')).to.equal(fsCount)
|
||||
|
||||
const keptFilePath = join(server.servers.buildDirectory('original-video-files'), getFilenameFromUrl(source.fileDownloadUrl))
|
||||
expect(await getFileSize(keptFilePath)).to.equal(fixtureFileSize)
|
||||
}
|
||||
|
||||
expect(source.fileDownloadUrl).to.exist
|
||||
if (objectStorageBaseUrl) {
|
||||
const token = await server.videoToken.getVideoFileToken({ videoId: uuid })
|
||||
expectStartWith(await getRedirectionUrl(source.fileDownloadUrl + '?videoFileToken=' + token), objectStorageBaseUrl)
|
||||
}
|
||||
|
||||
const { body } = await makeRawRequest({
|
||||
url: source.fileDownloadUrl,
|
||||
token: server.accessToken,
|
||||
redirects: 1,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
expect(body).to.have.lengthOf(fixtureFileSize)
|
||||
|
||||
return source
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { FfmpegCommand } from 'fluent-ffmpeg'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { VideoCreateResult, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import {
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel,
|
||||
waitJobs,
|
||||
waitUntilLivePublishedOnAllServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
async function processViewersStats (servers: PeerTubeServer[]) {
|
||||
await wait(6000)
|
||||
|
||||
for (const server of servers) {
|
||||
await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
|
||||
await server.debug.sendCommand({ body: { command: 'process-video-viewers' } })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
async function processViewsBuffer (servers: PeerTubeServer[]) {
|
||||
for (const server of servers) {
|
||||
await server.debug.sendCommand({ body: { command: 'process-video-views-buffer' } })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
}
|
||||
|
||||
async function prepareViewsServers (options: {
|
||||
viewersFederationV2?: boolean
|
||||
viewExpiration?: string // default 1 second
|
||||
trustViewerSessionId?: boolean // default true
|
||||
} = {}) {
|
||||
const { viewExpiration = '1 second', trustViewerSessionId = true } = options
|
||||
|
||||
const env = options?.viewersFederationV2 === true
|
||||
? { USE_VIEWERS_FEDERATION_V2: 'true' }
|
||||
: undefined
|
||||
|
||||
const config = {
|
||||
views: {
|
||||
videos: {
|
||||
view_expiration: viewExpiration,
|
||||
trust_viewer_session_id: trustViewerSessionId,
|
||||
count_view_after: '10 seconds'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const servers = await createMultipleServers(2, config, { env })
|
||||
await setAccessTokensToServers(servers)
|
||||
await setDefaultVideoChannel(servers)
|
||||
|
||||
await servers[0].config.enableMinimumTranscoding()
|
||||
await servers[0].config.enableLive({ allowReplay: true, transcoding: false })
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
async function prepareViewsVideos (options: {
|
||||
servers: PeerTubeServer[]
|
||||
live: boolean
|
||||
vod: boolean
|
||||
}) {
|
||||
const { servers } = options
|
||||
|
||||
const liveAttributes = {
|
||||
name: 'live video',
|
||||
channelId: servers[0].store.channel.id,
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
}
|
||||
|
||||
let ffmpegCommand: FfmpegCommand
|
||||
let live: VideoCreateResult
|
||||
let vod: VideoCreateResult
|
||||
|
||||
if (options.live) {
|
||||
live = await servers[0].live.create({ fields: liveAttributes })
|
||||
|
||||
ffmpegCommand = await servers[0].live.sendRTMPStreamInVideo({ videoId: live.uuid })
|
||||
await waitUntilLivePublishedOnAllServers(servers, live.uuid)
|
||||
}
|
||||
|
||||
if (options.vod) {
|
||||
vod = await servers[0].videos.quickUpload({ name: 'video' })
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
return { liveVideoId: live?.uuid, vodVideoId: vod?.uuid, ffmpegCommand }
|
||||
}
|
||||
|
||||
export {
|
||||
processViewersStats,
|
||||
prepareViewsServers,
|
||||
processViewsBuffer,
|
||||
prepareViewsVideos
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { expect } from 'chai'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import type { Instance, Torrent } from 'webtorrent'
|
||||
import { VideoFile } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import type { Instance as MagnetUriInstance } from 'magnet-uri'
|
||||
|
||||
let webtorrent: Instance
|
||||
|
||||
export async function checkWebTorrentWorks (magnetUri: string, pathMatch?: RegExp) {
|
||||
const torrent = await webtorrentAdd(magnetUri, true)
|
||||
|
||||
expect(torrent.files).to.be.an('array')
|
||||
expect(torrent.files.length).to.equal(1)
|
||||
expect(torrent.files[0].path).to.exist.and.to.not.equal('')
|
||||
|
||||
if (pathMatch) {
|
||||
expect(torrent.files[0].path).match(pathMatch)
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) {
|
||||
const torrentName = basename(file.torrentUrl)
|
||||
const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
|
||||
|
||||
const data = await readFile(torrentPath)
|
||||
|
||||
return (await import('parse-torrent')).default(data)
|
||||
}
|
||||
|
||||
export async function magnetUriDecode (data: string) {
|
||||
return (await import('magnet-uri')).decode(data)
|
||||
}
|
||||
|
||||
export async function magnetUriEncode (data: MagnetUriInstance) {
|
||||
return (await import('magnet-uri')).encode(data)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function webtorrentAdd (torrentId: string, refreshWebTorrent = false) {
|
||||
const WebTorrent = (await import('webtorrent')).default
|
||||
|
||||
if (webtorrent && refreshWebTorrent) webtorrent.destroy()
|
||||
if (!webtorrent || refreshWebTorrent) webtorrent = new WebTorrent()
|
||||
|
||||
webtorrent.on('error', err => console.error('Error in webtorrent', err))
|
||||
|
||||
return new Promise<Torrent>(res => {
|
||||
const torrent = webtorrent.add(torrentId, res)
|
||||
|
||||
torrent.on('error', err => console.error('Error in webtorrent torrent', err))
|
||||
torrent.on('warning', warn => {
|
||||
const msg = typeof warn === 'string'
|
||||
? warn
|
||||
: warn.message
|
||||
|
||||
if (msg.includes('Unsupported')) return
|
||||
|
||||
console.error('Warning in webtorrent torrent', warn)
|
||||
})
|
||||
})
|
||||
}
|
||||
新しい課題から参照
ユーザをブロックする