はじまりの大地

このコミットが含まれているのは:
2024-07-15 09:14:04 +09:00
コミット 6632905f32
3501個のファイルの変更1439465行の追加0行の削除
+70
ファイルの表示
@@ -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)
}
+21
ファイルの表示
@@ -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
}
+194
ファイルの表示
@@ -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
}
+181
ファイルの表示
@@ -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
}
}
+5
ファイルの表示
@@ -0,0 +1,5 @@
import { createLogger, transports } from 'winston'
export function createConsoleLogger () {
return createLogger({ transports: [ new transports.Console() ] })
}
+44
ファイルの表示
@@ -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)
}
+36
ファイルの表示
@@ -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'
}
+79
ファイルの表示
@@ -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
}
+363
ファイルの表示
@@ -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
}
}
+188
ファイルの表示
@@ -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
}
+8
ファイルの表示
@@ -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'
+33
ファイルの表示
@@ -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)
}
}
+62
ファイルの表示
@@ -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
}
+23
ファイルの表示
@@ -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)
}
}
+46
ファイルの表示
@@ -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)
}
}
+34
ファイルの表示
@@ -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
}
}
+41
ファイルの表示
@@ -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)
}
}
+36
ファイルの表示
@@ -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)
}
}
+24
ファイルの表示
@@ -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
}
+33
ファイルの表示
@@ -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
}
+989
ファイルの表示
@@ -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)
}
+115
ファイルの表示
@@ -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: '' } })
}
}
+18
ファイルの表示
@@ -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
}
+12
ファイルの表示
@@ -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)
}
+175
ファイルの表示
@@ -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(/'$/, '"')
}
}
+310
ファイルの表示
@@ -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
}
+27
ファイルの表示
@@ -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
}
+97
ファイルの表示
@@ -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
}
+22
ファイルの表示
@@ -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
}
+418
ファイルの表示
@@ -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
}
+104
ファイルの表示
@@ -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
}
+66
ファイルの表示
@@ -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)
})
})
}