shared/ typescript types dir server-commands

This commit is contained in:
Chocobozzz
2021-12-17 09:29:23 +01:00
parent 6b5f72beda
commit bf54587a3e
242 changed files with 228 additions and 172 deletions

View File

@@ -0,0 +1,353 @@
import { merge } from 'lodash'
import { DeepPartial } from '@shared/typescript-utils'
import { About, HttpStatusCode, ServerConfig } from '@shared/models'
import { CustomConfig } from '../../models/server/custom-config.model'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class ConfigCommand extends AbstractCommand {
static getCustomConfigResolutions (enabled: boolean) {
return {
'144p': enabled,
'240p': enabled,
'360p': enabled,
'480p': enabled,
'720p': enabled,
'1080p': enabled,
'1440p': enabled,
'2160p': enabled
}
}
enableImports () {
return this.updateExistingSubConfig({
newConfig: {
import: {
videos: {
http: {
enabled: true
},
torrent: {
enabled: true
}
}
}
}
})
}
enableLive (options: {
allowReplay?: boolean
transcoding?: boolean
} = {}) {
return this.updateExistingSubConfig({
newConfig: {
live: {
enabled: true,
allowReplay: options.allowReplay ?? true,
transcoding: {
enabled: options.transcoding ?? true,
resolutions: ConfigCommand.getCustomConfigResolutions(true)
}
}
}
})
}
disableTranscoding () {
return this.updateExistingSubConfig({
newConfig: {
transcoding: {
enabled: false
}
}
})
}
enableTranscoding (webtorrent = true, hls = true) {
return this.updateExistingSubConfig({
newConfig: {
transcoding: {
enabled: true,
resolutions: ConfigCommand.getCustomConfigResolutions(true),
webtorrent: {
enabled: webtorrent
},
hls: {
enabled: hls
}
}
}
})
}
getConfig (options: OverrideCommandOptions = {}) {
const path = '/api/v1/config'
return this.getRequestBody<ServerConfig>({
...options,
path,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
getAbout (options: OverrideCommandOptions = {}) {
const path = '/api/v1/config/about'
return this.getRequestBody<About>({
...options,
path,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
getCustomConfig (options: OverrideCommandOptions = {}) {
const path = '/api/v1/config/custom'
return this.getRequestBody<CustomConfig>({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
updateCustomConfig (options: OverrideCommandOptions & {
newCustomConfig: CustomConfig
}) {
const path = '/api/v1/config/custom'
return this.putBodyRequest({
...options,
path,
fields: options.newCustomConfig,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
deleteCustomConfig (options: OverrideCommandOptions = {}) {
const path = '/api/v1/config/custom'
return this.deleteRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
async updateExistingSubConfig (options: OverrideCommandOptions & {
newConfig: DeepPartial<CustomConfig>
}) {
const existing = await this.getCustomConfig(options)
return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) })
}
updateCustomSubConfig (options: OverrideCommandOptions & {
newConfig: DeepPartial<CustomConfig>
}) {
const newCustomConfig: CustomConfig = {
instance: {
name: 'PeerTube updated',
shortDescription: 'my short description',
description: 'my super description',
terms: 'my super terms',
codeOfConduct: 'my super coc',
creationReason: 'my super creation reason',
moderationInformation: 'my super moderation information',
administrator: 'Kuja',
maintenanceLifetime: 'forever',
businessModel: 'my super business model',
hardwareInformation: '2vCore 3GB RAM',
languages: [ 'en', 'es' ],
categories: [ 1, 2 ],
isNSFW: true,
defaultNSFWPolicy: 'blur',
defaultClientRoute: '/videos/recently-added',
customizations: {
javascript: 'alert("coucou")',
css: 'body { background-color: red; }'
}
},
theme: {
default: 'default'
},
services: {
twitter: {
username: '@MySuperUsername',
whitelisted: true
}
},
client: {
videos: {
miniature: {
preferAuthorDisplayName: false
}
},
menu: {
login: {
redirectOnSingleExternalAuth: false
}
}
},
cache: {
previews: {
size: 2
},
captions: {
size: 3
},
torrents: {
size: 4
}
},
signup: {
enabled: false,
limit: 5,
requiresEmailVerification: false,
minimumAge: 16
},
admin: {
email: 'superadmin1@example.com'
},
contactForm: {
enabled: true
},
user: {
videoQuota: 5242881,
videoQuotaDaily: 318742
},
videoChannels: {
maxPerUser: 20
},
transcoding: {
enabled: true,
allowAdditionalExtensions: true,
allowAudioFiles: true,
threads: 1,
concurrency: 3,
profile: 'default',
resolutions: {
'0p': false,
'144p': false,
'240p': false,
'360p': true,
'480p': true,
'720p': false,
'1080p': false,
'1440p': false,
'2160p': false
},
webtorrent: {
enabled: true
},
hls: {
enabled: false
}
},
live: {
enabled: true,
allowReplay: false,
maxDuration: -1,
maxInstanceLives: -1,
maxUserLives: 50,
transcoding: {
enabled: true,
threads: 4,
profile: 'default',
resolutions: {
'144p': true,
'240p': true,
'360p': true,
'480p': true,
'720p': true,
'1080p': true,
'1440p': true,
'2160p': true
}
}
},
import: {
videos: {
concurrency: 3,
http: {
enabled: false
},
torrent: {
enabled: false
}
}
},
trending: {
videos: {
algorithms: {
enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
default: 'hot'
}
}
},
autoBlacklist: {
videos: {
ofUsers: {
enabled: false
}
}
},
followers: {
instance: {
enabled: true,
manualApproval: false
}
},
followings: {
instance: {
autoFollowBack: {
enabled: false
},
autoFollowIndex: {
indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
enabled: false
}
}
},
broadcastMessage: {
enabled: true,
level: 'warning',
message: 'hello',
dismissable: true
},
search: {
remoteUri: {
users: true,
anonymous: true
},
searchIndex: {
enabled: true,
url: 'https://search.joinpeertube.org',
disableLocalSearch: true,
isDefaultSearch: true
}
}
}
merge(newCustomConfig, options.newConfig)
return this.updateCustomConfig({ ...options, newCustomConfig })
}
}

View File

@@ -0,0 +1,31 @@
import { HttpStatusCode } from '@shared/models'
import { ContactForm } from '../../models/server'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class ContactFormCommand extends AbstractCommand {
send (options: OverrideCommandOptions & {
fromEmail: string
fromName: string
subject: string
body: string
}) {
const path = '/api/v1/server/contact'
const body: ContactForm = {
fromEmail: options.fromEmail,
fromName: options.fromName,
subject: options.subject,
body: options.body
}
return this.postBodyRequest({
...options,
path,
fields: body,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
}

View File

@@ -0,0 +1,33 @@
import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class DebugCommand extends AbstractCommand {
getDebug (options: OverrideCommandOptions = {}) {
const path = '/api/v1/server/debug'
return this.getRequestBody<Debug>({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
sendCommand (options: OverrideCommandOptions & {
body: SendDebugCommand
}) {
const { body } = options
const path = '/api/v1/server/debug/run-command'
return this.postBodyRequest({
...options,
path,
fields: body,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
}

View File

@@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { pathExists, readdir } from 'fs-extra'
import { join } from 'path'
import { root } from '@shared/core-utils'
import { PeerTubeServer } from './server'
async function checkTmpIsEmpty (server: PeerTubeServer) {
await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
await checkDirectoryIsEmpty(server, 'tmp/hls')
}
}
async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
const testDirectory = 'test' + server.internalServerNumber
const directoryPath = join(root(), testDirectory, 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 {
checkTmpIsEmpty,
checkDirectoryIsEmpty
}

View File

@@ -0,0 +1,139 @@
import { pick } from '@shared/core-utils'
import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
import { PeerTubeServer } from './server'
export class FollowsCommand extends AbstractCommand {
getFollowers (options: OverrideCommandOptions & {
start: number
count: number
sort: string
search?: string
actorType?: ActivityPubActorType
state?: FollowState
}) {
const path = '/api/v1/server/followers'
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
return this.getRequestBody<ResultList<ActorFollow>>({
...options,
path,
query,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
getFollowings (options: OverrideCommandOptions & {
start?: number
count?: number
sort?: string
search?: string
actorType?: ActivityPubActorType
state?: FollowState
} = {}) {
const path = '/api/v1/server/following'
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
return this.getRequestBody<ResultList<ActorFollow>>({
...options,
path,
query,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
follow (options: OverrideCommandOptions & {
hosts?: string[]
handles?: string[]
}) {
const path = '/api/v1/server/following'
const fields: ServerFollowCreate = {}
if (options.hosts) {
fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
}
if (options.handles) {
fields.handles = options.handles
}
return this.postBodyRequest({
...options,
path,
fields,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
async unfollow (options: OverrideCommandOptions & {
target: PeerTubeServer | string
}) {
const { target } = options
const handle = typeof target === 'string'
? target
: target.host
const path = '/api/v1/server/following/' + handle
return this.deleteRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
acceptFollower (options: OverrideCommandOptions & {
follower: string
}) {
const path = '/api/v1/server/followers/' + options.follower + '/accept'
return this.postBodyRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
rejectFollower (options: OverrideCommandOptions & {
follower: string
}) {
const path = '/api/v1/server/followers/' + options.follower + '/reject'
return this.postBodyRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
removeFollower (options: OverrideCommandOptions & {
follower: PeerTubeServer
}) {
const path = '/api/v1/server/followers/peertube@' + options.follower.host
return this.deleteRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
}

View File

@@ -0,0 +1,20 @@
import { waitJobs } from './jobs'
import { PeerTubeServer } from './server'
async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
await Promise.all([
server1.follows.follow({ hosts: [ server2.url ] }),
server2.follows.follow({ hosts: [ server1.url ] })
])
// Wait request propagation
await waitJobs([ server1, server2 ])
return true
}
// ---------------------------------------------------------------------------
export {
doubleFollow
}

View File

@@ -0,0 +1,17 @@
export * from './config-command'
export * from './contact-form-command'
export * from './debug-command'
export * from './directories'
export * from './follows-command'
export * from './follows'
export * from './jobs'
export * from './jobs-command'
export * from './object-storage-command'
export * from './plugins-command'
export * from './plugins'
export * from './redundancy-command'
export * from './server'
export * from './servers-command'
export * from './servers'
export * from './stats-command'
export * from './tracker'

View File

@@ -0,0 +1,61 @@
import { pick } from '@shared/core-utils'
import { HttpStatusCode } from '@shared/models'
import { Job, JobState, JobType, ResultList } from '../../models'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class JobsCommand extends AbstractCommand {
async getLatest (options: OverrideCommandOptions & {
jobType: JobType
}) {
const { data } = await this.list({ ...options, start: 0, count: 1, sort: '-createdAt' })
if (data.length === 0) return undefined
return data[0]
}
list (options: OverrideCommandOptions & {
state?: JobState
jobType?: JobType
start?: number
count?: number
sort?: string
} = {}) {
const path = this.buildJobsUrl(options.state)
const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
return this.getRequestBody<ResultList<Job>>({
...options,
path,
query,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
listFailed (options: OverrideCommandOptions & {
jobType?: JobType
}) {
const path = this.buildJobsUrl('failed')
return this.getRequestBody<ResultList<Job>>({
...options,
path,
query: { start: 0, count: 50 },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
private buildJobsUrl (state?: JobState) {
let path = '/api/v1/jobs'
if (state) path += '/' + state
return path
}
}

View File

@@ -0,0 +1,84 @@
import { expect } from 'chai'
import { JobState, JobType } from '../../models'
import { wait } from '../miscs'
import { PeerTubeServer } from './server'
async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) {
const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
: 250
let servers: PeerTubeServer[]
if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
else servers = serversArg as PeerTubeServer[]
const states: JobState[] = [ 'waiting', 'active' ]
if (!skipDelayed) states.push('delayed')
const repeatableJobs: JobType[] = [ 'videos-views-stats', 'activitypub-cleaner' ]
let pendingRequests: boolean
function tasksBuilder () {
const tasks: Promise<any>[] = []
// Check if each server has pending request
for (const server of servers) {
for (const state of states) {
const p = server.jobs.list({
state,
start: 0,
count: 10,
sort: '-createdAt'
}).then(body => body.data)
.then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
.then(jobs => {
if (jobs.length !== 0) {
pendingRequests = true
}
})
tasks.push(p)
}
const p = server.debug.getDebug()
.then(obj => {
if (obj.activityPubMessagesWaiting !== 0) {
pendingRequests = true
}
})
tasks.push(p)
}
return tasks
}
do {
pendingRequests = false
await Promise.all(tasksBuilder())
// Retry, in case of new jobs were created
if (pendingRequests === false) {
await wait(pendingJobWait)
await Promise.all(tasksBuilder())
}
if (pendingRequests) {
await wait(pendingJobWait)
}
} while (pendingRequests)
}
async function expectNoFailedTranscodingJob (server: PeerTubeServer) {
const { data } = await server.jobs.listFailed({ jobType: 'video-transcoding' })
expect(data).to.have.lengthOf(0)
}
// ---------------------------------------------------------------------------
export {
waitJobs,
expectNoFailedTranscodingJob
}

View File

@@ -0,0 +1,77 @@
import { HttpStatusCode } from '@shared/models'
import { makePostBodyRequest } from '../requests'
import { AbstractCommand } from '../shared'
export class ObjectStorageCommand extends AbstractCommand {
static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists'
static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos'
static getDefaultConfig () {
return {
object_storage: {
enabled: true,
endpoint: 'http://' + this.getEndpointHost(),
region: this.getRegion(),
credentials: this.getCredentialsConfig(),
streaming_playlists: {
bucket_name: this.DEFAULT_PLAYLIST_BUCKET
},
videos: {
bucket_name: this.DEFAULT_WEBTORRENT_BUCKET
}
}
}
}
static getCredentialsConfig () {
return {
access_key_id: 'AKIAIOSFODNN7EXAMPLE',
secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
}
}
static getEndpointHost () {
return 'localhost:9444'
}
static getRegion () {
return 'us-east-1'
}
static getWebTorrentBaseUrl () {
return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/`
}
static getPlaylistBaseUrl () {
return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/`
}
static async prepareDefaultBuckets () {
await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET)
await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET)
}
static async createBucket (name: string) {
await makePostBodyRequest({
url: this.getEndpointHost(),
path: '/ui/' + name + '?delete',
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
})
await makePostBodyRequest({
url: this.getEndpointHost(),
path: '/ui/' + name + '?create',
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
})
await makePostBodyRequest({
url: this.getEndpointHost(),
path: '/ui/' + name + '?make-public',
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
})
}
}

View File

@@ -0,0 +1,257 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { readJSON, writeJSON } from 'fs-extra'
import { join } from 'path'
import { root } from '@shared/core-utils'
import {
HttpStatusCode,
PeerTubePlugin,
PeerTubePluginIndex,
PeertubePluginIndexList,
PluginPackageJson,
PluginTranslation,
PluginType,
PublicServerSetting,
RegisteredServerSettings,
ResultList
} from '@shared/models'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class PluginsCommand extends AbstractCommand {
static getPluginTestPath (suffix = '') {
return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
}
list (options: OverrideCommandOptions & {
start?: number
count?: number
sort?: string
pluginType?: PluginType
uninstalled?: boolean
}) {
const { start, count, sort, pluginType, uninstalled } = options
const path = '/api/v1/plugins'
return this.getRequestBody<ResultList<PeerTubePlugin>>({
...options,
path,
query: {
start,
count,
sort,
pluginType,
uninstalled
},
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
listAvailable (options: OverrideCommandOptions & {
start?: number
count?: number
sort?: string
pluginType?: PluginType
currentPeerTubeEngine?: string
search?: string
expectedStatus?: HttpStatusCode
}) {
const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options
const path = '/api/v1/plugins/available'
const query: PeertubePluginIndexList = {
start,
count,
sort,
pluginType,
currentPeerTubeEngine,
search
}
return this.getRequestBody<ResultList<PeerTubePluginIndex>>({
...options,
path,
query,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
get (options: OverrideCommandOptions & {
npmName: string
}) {
const path = '/api/v1/plugins/' + options.npmName
return this.getRequestBody<PeerTubePlugin>({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
updateSettings (options: OverrideCommandOptions & {
npmName: string
settings: any
}) {
const { npmName, settings } = options
const path = '/api/v1/plugins/' + npmName + '/settings'
return this.putBodyRequest({
...options,
path,
fields: { settings },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
getRegisteredSettings (options: OverrideCommandOptions & {
npmName: string
}) {
const path = '/api/v1/plugins/' + options.npmName + '/registered-settings'
return this.getRequestBody<RegisteredServerSettings>({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
getPublicSettings (options: OverrideCommandOptions & {
npmName: string
}) {
const { npmName } = options
const path = '/api/v1/plugins/' + npmName + '/public-settings'
return this.getRequestBody<PublicServerSetting>({
...options,
path,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
getTranslations (options: OverrideCommandOptions & {
locale: string
}) {
const { locale } = options
const path = '/plugins/translations/' + locale + '.json'
return this.getRequestBody<PluginTranslation>({
...options,
path,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
install (options: OverrideCommandOptions & {
path?: string
npmName?: string
pluginVersion?: string
}) {
const { npmName, path, pluginVersion } = options
const apiPath = '/api/v1/plugins/install'
return this.postBodyRequest({
...options,
path: apiPath,
fields: { npmName, path, pluginVersion },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
update (options: OverrideCommandOptions & {
path?: string
npmName?: string
}) {
const { npmName, path } = options
const apiPath = '/api/v1/plugins/update'
return this.postBodyRequest({
...options,
path: apiPath,
fields: { npmName, path },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
uninstall (options: OverrideCommandOptions & {
npmName: string
}) {
const { npmName } = options
const apiPath = '/api/v1/plugins/uninstall'
return this.postBodyRequest({
...options,
path: apiPath,
fields: { npmName },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
getCSS (options: OverrideCommandOptions = {}) {
const path = '/plugins/global.css'
return this.getRequestText({
...options,
path,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
getExternalAuth (options: OverrideCommandOptions & {
npmName: string
npmVersion: string
authName: string
query?: any
}) {
const { npmName, npmVersion, authName, query } = options
const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
return this.getRequest({
...options,
path,
query,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200,
redirects: 0
})
}
updatePackageJSON (npmName: string, json: any) {
const path = this.getPackageJSONPath(npmName)
return writeJSON(path, json)
}
getPackageJSON (npmName: string): Promise<PluginPackageJson> {
const path = this.getPackageJSONPath(npmName)
return readJSON(path)
}
private getPackageJSONPath (npmName: string) {
return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json'))
}
}

View File

@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { PeerTubeServer } from './server'
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
}

View File

@@ -0,0 +1,80 @@
import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class RedundancyCommand extends AbstractCommand {
updateRedundancy (options: OverrideCommandOptions & {
host: string
redundancyAllowed: boolean
}) {
const { host, redundancyAllowed } = options
const path = '/api/v1/server/redundancy/' + host
return this.putBodyRequest({
...options,
path,
fields: { redundancyAllowed },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
listVideos (options: OverrideCommandOptions & {
target: VideoRedundanciesTarget
start?: number
count?: number
sort?: string
}) {
const path = '/api/v1/server/redundancy/videos'
const { target, start, count, sort } = options
return this.getRequestBody<ResultList<VideoRedundancy>>({
...options,
path,
query: {
start: start ?? 0,
count: count ?? 5,
sort: sort ?? 'name',
target
},
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
addVideo (options: OverrideCommandOptions & {
videoId: number
}) {
const path = '/api/v1/server/redundancy/videos'
const { videoId } = options
return this.postBodyRequest({
...options,
path,
fields: { videoId },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
removeVideo (options: OverrideCommandOptions & {
redundancyId: number
}) {
const { redundancyId } = options
const path = '/api/v1/server/redundancy/videos/' + redundancyId
return this.deleteRequest({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
})
}
}

View File

@@ -0,0 +1,392 @@
import { ChildProcess, fork } from 'child_process'
import { copy } from 'fs-extra'
import { join } from 'path'
import { root, randomInt } from '@shared/core-utils'
import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '../../models/videos'
import { BulkCommand } from '../bulk'
import { CLICommand } from '../cli'
import { CustomPagesCommand } from '../custom-pages'
import { FeedCommand } from '../feeds'
import { LogsCommand } from '../logs'
import { parallelTests, SQLCommand } from '../miscs'
import { AbusesCommand } from '../moderation'
import { OverviewsCommand } from '../overviews'
import { SearchCommand } from '../search'
import { SocketIOCommand } from '../socket'
import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users'
import {
BlacklistCommand,
CaptionsCommand,
ChangeOwnershipCommand,
ChannelsCommand,
HistoryCommand,
ImportsCommand,
LiveCommand,
PlaylistsCommand,
ServicesCommand,
StreamingPlaylistsCommand,
VideosCommand
} from '../videos'
import { CommentsCommand } from '../videos/comments-command'
import { ConfigCommand } from './config-command'
import { ContactFormCommand } from './contact-form-command'
import { DebugCommand } from './debug-command'
import { FollowsCommand } from './follows-command'
import { JobsCommand } from './jobs-command'
import { PluginsCommand } from './plugins-command'
import { RedundancyCommand } from './redundancy-command'
import { ServersCommand } from './servers-command'
import { StatsCommand } from './stats-command'
import { ObjectStorageCommand } from './object-storage-command'
export type RunServerOptions = {
hideLogs?: boolean
nodeArgs?: string[]
peertubeArgs?: string[]
env?: { [ id: string ]: string }
}
export class PeerTubeServer {
app?: ChildProcess
url: string
host?: string
hostname?: string
port?: number
rtmpPort?: number
rtmpsPort?: number
parallel?: boolean
internalServerNumber: number
serverNumber?: number
customConfigFile?: string
store?: {
client?: {
id?: string
secret?: string
}
user?: {
username: string
password: string
email?: string
}
channel?: VideoChannel
video?: Video
videoCreated?: VideoCreateResult
videoDetails?: VideoDetails
videos?: { id: number, uuid: string }[]
}
accessToken?: string
refreshToken?: string
bulk?: BulkCommand
cli?: CLICommand
customPage?: CustomPagesCommand
feed?: FeedCommand
logs?: LogsCommand
abuses?: AbusesCommand
overviews?: OverviewsCommand
search?: SearchCommand
contactForm?: ContactFormCommand
debug?: DebugCommand
follows?: FollowsCommand
jobs?: JobsCommand
plugins?: PluginsCommand
redundancy?: RedundancyCommand
stats?: StatsCommand
config?: ConfigCommand
socketIO?: SocketIOCommand
accounts?: AccountsCommand
blocklist?: BlocklistCommand
subscriptions?: SubscriptionsCommand
live?: LiveCommand
services?: ServicesCommand
blacklist?: BlacklistCommand
captions?: CaptionsCommand
changeOwnership?: ChangeOwnershipCommand
playlists?: PlaylistsCommand
history?: HistoryCommand
imports?: ImportsCommand
streamingPlaylists?: StreamingPlaylistsCommand
channels?: ChannelsCommand
comments?: CommentsCommand
sql?: SQLCommand
notifications?: NotificationsCommand
servers?: ServersCommand
login?: LoginCommand
users?: UsersCommand
objectStorage?: ObjectStorageCommand
videos?: VideosCommand
constructor (options: { serverNumber: number } | { url: string }) {
if ((options as any).url) {
this.setUrl((options as any).url)
} else {
this.setServerNumber((options as any).serverNumber)
}
this.store = {
client: {
id: null,
secret: null
},
user: {
username: null,
password: null
}
}
this.assignCommands()
}
setServerNumber (serverNumber: number) {
this.serverNumber = serverNumber
this.parallel = parallelTests()
this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
this.rtmpsPort = this.parallel ? this.randomRTMP() : 1937
this.port = 9000 + this.internalServerNumber
this.url = `http://localhost:${this.port}`
this.host = `localhost:${this.port}`
this.hostname = 'localhost'
}
setUrl (url: string) {
const parsed = new URL(url)
this.url = url
this.host = parsed.host
this.hostname = parsed.hostname
this.port = parseInt(parsed.port)
}
async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
await ServersCommand.flushTests(this.internalServerNumber)
return this.run(configOverride, options)
}
async run (configOverrideArg?: any, options: RunServerOptions = {}) {
// These actions are async so we need to be sure that they have both been done
const serverRunString = {
'HTTP server listening': false
}
const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
serverRunString[key] = false
const regexps = {
client_id: 'Client id: (.+)',
client_secret: 'Client secret: (.+)',
user_username: 'Username: (.+)',
user_password: 'User password: (.+)'
}
await this.assignCustomConfigFile()
const configOverride = this.buildConfigOverride()
if (configOverrideArg !== undefined) {
Object.assign(configOverride, configOverrideArg)
}
// Share the environment
const env = Object.create(process.env)
env['NODE_ENV'] = 'test'
env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
env['NODE_CONFIG'] = JSON.stringify(configOverride)
if (options.env) {
Object.assign(env, options.env)
}
const forkOptions = {
silent: true,
env,
detached: true,
execArgv: options.nodeArgs || []
}
return new Promise<void>((res, rej) => {
const self = this
let aggregatedLogs = ''
this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs))
const onParentExit = () => {
if (!this.app || !this.app.pid) return
try {
process.kill(self.app.pid)
} catch { /* empty */ }
}
this.app.on('exit', onPeerTubeExit)
process.on('exit', onParentExit)
this.app.stdout.on('data', function onStdout (data) {
let dontContinue = false
const log: string = data.toString()
aggregatedLogs += log
// Capture things if we want to
for (const key of Object.keys(regexps)) {
const regexp = regexps[key]
const matches = log.match(regexp)
if (matches !== null) {
if (key === 'client_id') self.store.client.id = matches[1]
else if (key === 'client_secret') self.store.client.secret = matches[1]
else if (key === 'user_username') self.store.user.username = matches[1]
else if (key === 'user_password') self.store.user.password = matches[1]
}
}
// Check if all required sentences are here
for (const key of Object.keys(serverRunString)) {
if (log.includes(key)) serverRunString[key] = true
if (serverRunString[key] === false) dontContinue = true
}
// If no, there is maybe one thing not already initialized (client/user credentials generation...)
if (dontContinue === true) return
if (options.hideLogs === false) {
console.log(log)
} else {
process.removeListener('exit', onParentExit)
self.app.stdout.removeListener('data', onStdout)
self.app.removeListener('exit', onPeerTubeExit)
}
res()
})
})
}
async kill () {
if (!this.app) return
await this.sql.cleanup()
process.kill(-this.app.pid)
this.app = null
}
private randomServer () {
const low = 10
const high = 10000
return randomInt(low, high)
}
private randomRTMP () {
const low = 1900
const high = 2100
return randomInt(low, high)
}
private async assignCustomConfigFile () {
if (this.internalServerNumber === this.serverNumber) return
const basePath = join(root(), 'config')
const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
this.customConfigFile = tmpConfigFile
}
private buildConfigOverride () {
if (!this.parallel) return {}
return {
listen: {
port: this.port
},
webserver: {
port: this.port
},
database: {
suffix: '_test' + this.internalServerNumber
},
storage: {
tmp: `test${this.internalServerNumber}/tmp/`,
bin: `test${this.internalServerNumber}/bin/`,
avatars: `test${this.internalServerNumber}/avatars/`,
videos: `test${this.internalServerNumber}/videos/`,
streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
redundancy: `test${this.internalServerNumber}/redundancy/`,
logs: `test${this.internalServerNumber}/logs/`,
previews: `test${this.internalServerNumber}/previews/`,
thumbnails: `test${this.internalServerNumber}/thumbnails/`,
torrents: `test${this.internalServerNumber}/torrents/`,
captions: `test${this.internalServerNumber}/captions/`,
cache: `test${this.internalServerNumber}/cache/`,
plugins: `test${this.internalServerNumber}/plugins/`
},
admin: {
email: `admin${this.internalServerNumber}@example.com`
},
live: {
rtmp: {
port: this.rtmpPort
}
}
}
}
private assignCommands () {
this.bulk = new BulkCommand(this)
this.cli = new CLICommand(this)
this.customPage = new CustomPagesCommand(this)
this.feed = new FeedCommand(this)
this.logs = new LogsCommand(this)
this.abuses = new AbusesCommand(this)
this.overviews = new OverviewsCommand(this)
this.search = new SearchCommand(this)
this.contactForm = new ContactFormCommand(this)
this.debug = new DebugCommand(this)
this.follows = new FollowsCommand(this)
this.jobs = new JobsCommand(this)
this.plugins = new PluginsCommand(this)
this.redundancy = new RedundancyCommand(this)
this.stats = new StatsCommand(this)
this.config = new ConfigCommand(this)
this.socketIO = new SocketIOCommand(this)
this.accounts = new AccountsCommand(this)
this.blocklist = new BlocklistCommand(this)
this.subscriptions = new SubscriptionsCommand(this)
this.live = new LiveCommand(this)
this.services = new ServicesCommand(this)
this.blacklist = new BlacklistCommand(this)
this.captions = new CaptionsCommand(this)
this.changeOwnership = new ChangeOwnershipCommand(this)
this.playlists = new PlaylistsCommand(this)
this.history = new HistoryCommand(this)
this.imports = new ImportsCommand(this)
this.streamingPlaylists = new StreamingPlaylistsCommand(this)
this.channels = new ChannelsCommand(this)
this.comments = new CommentsCommand(this)
this.sql = new SQLCommand(this)
this.notifications = new NotificationsCommand(this)
this.servers = new ServersCommand(this)
this.login = new LoginCommand(this)
this.users = new UsersCommand(this)
this.videos = new VideosCommand(this)
this.objectStorage = new ObjectStorageCommand(this)
}
}

View File

@@ -0,0 +1,92 @@
import { exec } from 'child_process'
import { copy, ensureDir, readFile, remove } from 'fs-extra'
import { basename, join } from 'path'
import { root } from '@shared/core-utils'
import { HttpStatusCode } from '@shared/models'
import { getFileSize, isGithubCI, wait } from '../miscs'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class ServersCommand extends AbstractCommand {
static flushTests (internalServerNumber: number) {
return new Promise<void>((res, rej) => {
const suffix = ` -- ${internalServerNumber}`
return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
if (err || stderr) return rej(err || new Error(stderr))
return res()
})
})
}
ping (options: OverrideCommandOptions = {}) {
return this.getRequestBody({
...options,
path: '/api/v1/ping',
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
async cleanupTests () {
const p: Promise<any>[] = []
if (isGithubCI()) {
await ensureDir('artifacts')
const origin = this.buildDirectory('logs/peertube.log')
const destname = `peertube-${this.server.internalServerNumber}.log`
console.log('Saving logs %s.', destname)
await copy(origin, join('artifacts', destname))
}
if (this.server.parallel) {
p.push(ServersCommand.flushTests(this.server.internalServerNumber))
}
if (this.server.customConfigFile) {
p.push(remove(this.server.customConfigFile))
}
return p
}
async waitUntilLog (str: string, count = 1, strictCount = true) {
const logfile = this.buildDirectory('logs/peertube.log')
while (true) {
const buf = await readFile(logfile)
const matches = buf.toString().match(new RegExp(str, 'g'))
if (matches && matches.length === count) return
if (matches && strictCount === false && matches.length >= count) return
await wait(1000)
}
}
buildDirectory (directory: string) {
return join(root(), 'test' + this.server.internalServerNumber, directory)
}
buildWebTorrentFilePath (fileUrl: string) {
return this.buildDirectory(join('videos', basename(fileUrl)))
}
buildFragmentedFilePath (videoUUID: string, fileUrl: string) {
return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
}
getLogContent () {
return readFile(this.buildDirectory('logs/peertube.log'))
}
async getServerFileSize (subPath: string) {
const path = this.server.servers.buildDirectory(subPath)
return getFileSize(path)
}
}

View File

@@ -0,0 +1,49 @@
import { ensureDir } from 'fs-extra'
import { isGithubCI } from '../miscs'
import { PeerTubeServer, RunServerOptions } from './server'
async function createSingleServer (serverNumber: number, configOverride?: Object, options: RunServerOptions = {}) {
const server = new PeerTubeServer({ serverNumber })
await server.flushAndRun(configOverride, options)
return server
}
function createMultipleServers (totalServers: number, configOverride?: Object, options: RunServerOptions = {}) {
const serverPromises: Promise<PeerTubeServer>[] = []
for (let i = 1; i <= totalServers; i++) {
serverPromises.push(createSingleServer(i, configOverride, options))
}
return Promise.all(serverPromises)
}
async function killallServers (servers: PeerTubeServer[]) {
return Promise.all(servers.map(s => s.kill()))
}
async function cleanupTests (servers: PeerTubeServer[]) {
await killallServers(servers)
if (isGithubCI()) {
await ensureDir('artifacts')
}
let p: Promise<any>[] = []
for (const server of servers) {
p = p.concat(server.servers.cleanupTests())
}
return Promise.all(p)
}
// ---------------------------------------------------------------------------
export {
createSingleServer,
createMultipleServers,
cleanupTests,
killallServers
}

View File

@@ -0,0 +1,25 @@
import { HttpStatusCode, ServerStats } from '@shared/models'
import { AbstractCommand, OverrideCommandOptions } from '../shared'
export class StatsCommand extends AbstractCommand {
get (options: OverrideCommandOptions & {
useCache?: boolean // default false
} = {}) {
const { useCache = false } = options
const path = '/api/v1/server/stats'
const query = {
t: useCache ? undefined : new Date().getTime()
}
return this.getRequestBody<ServerStats>({
...options,
path,
query,
implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
}

View File

@@ -0,0 +1,27 @@
import { expect } from 'chai'
import { sha1 } from '@shared/core-utils/crypto'
import { makeGetRequest } from '../requests'
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
}