aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-07-21 15:51:30 +0200
committerChocobozzz <me@florianbigard.com>2021-07-21 15:51:30 +0200
commita24bd1ed41b43790bab6ba789580bb4e85f07d85 (patch)
treea54b0f6c921ba83a6e909cd0ced325b2d4b8863c /shared/extra-utils
parent5f26f13b3c16ac5ae0a3b0a7142d84a9528cf565 (diff)
parentc63830f15403ac4e750829f27d8bbbdc9a59282c (diff)
downloadPeerTube-a24bd1ed41b43790bab6ba789580bb4e85f07d85.tar.gz
PeerTube-a24bd1ed41b43790bab6ba789580bb4e85f07d85.tar.zst
PeerTube-a24bd1ed41b43790bab6ba789580bb4e85f07d85.zip
Merge branch 'next' into develop
Diffstat (limited to 'shared/extra-utils')
-rw-r--r--shared/extra-utils/bulk/bulk-command.ts20
-rw-r--r--shared/extra-utils/bulk/bulk.ts25
-rw-r--r--shared/extra-utils/bulk/index.ts1
-rw-r--r--shared/extra-utils/cli/cli-command.ts23
-rw-r--r--shared/extra-utils/cli/cli.ts24
-rw-r--r--shared/extra-utils/cli/index.ts1
-rw-r--r--shared/extra-utils/custom-pages/custom-pages-command.ts33
-rw-r--r--shared/extra-utils/custom-pages/custom-pages.ts31
-rw-r--r--shared/extra-utils/custom-pages/index.ts1
-rw-r--r--shared/extra-utils/feeds/feeds-command.ts44
-rw-r--r--shared/extra-utils/feeds/feeds.ts33
-rw-r--r--shared/extra-utils/feeds/index.ts1
-rw-r--r--shared/extra-utils/index.ts66
-rw-r--r--shared/extra-utils/logs/index.ts1
-rw-r--r--shared/extra-utils/logs/logs-command.ts43
-rw-r--r--shared/extra-utils/logs/logs.ts32
-rw-r--r--shared/extra-utils/miscs/checks.ts46
-rw-r--r--shared/extra-utils/miscs/generate.ts61
-rw-r--r--shared/extra-utils/miscs/index.ts5
-rw-r--r--shared/extra-utils/miscs/miscs.ts170
-rw-r--r--shared/extra-utils/miscs/sql-command.ts142
-rw-r--r--shared/extra-utils/miscs/sql.ts161
-rw-r--r--shared/extra-utils/miscs/stubs.ts14
-rw-r--r--shared/extra-utils/miscs/tests.ts94
-rw-r--r--shared/extra-utils/miscs/webtorrent.ts30
-rw-r--r--shared/extra-utils/mock-servers/index.ts4
-rw-r--r--shared/extra-utils/mock-servers/mock-email.ts (renamed from shared/extra-utils/miscs/email.ts)4
-rw-r--r--shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts (renamed from shared/extra-utils/mock-servers/joinpeertube-versions.ts)0
-rw-r--r--shared/extra-utils/mock-servers/mock-plugin-blocklist.ts (renamed from shared/extra-utils/plugins/mock-blocklist.ts)0
-rw-r--r--shared/extra-utils/moderation/abuses-command.ts228
-rw-r--r--shared/extra-utils/moderation/abuses.ts244
-rw-r--r--shared/extra-utils/moderation/index.ts1
-rw-r--r--shared/extra-utils/overviews/index.ts1
-rw-r--r--shared/extra-utils/overviews/overviews-command.ts23
-rw-r--r--shared/extra-utils/overviews/overviews.ts34
-rw-r--r--shared/extra-utils/requests/activitypub.ts2
-rw-r--r--shared/extra-utils/requests/check-api-params.ts19
-rw-r--r--shared/extra-utils/requests/index.ts3
-rw-r--r--shared/extra-utils/requests/requests.ts238
-rw-r--r--shared/extra-utils/search/index.ts1
-rw-r--r--shared/extra-utils/search/search-command.ts98
-rw-r--r--shared/extra-utils/search/video-channels.ts36
-rw-r--r--shared/extra-utils/search/video-playlists.ts36
-rw-r--r--shared/extra-utils/search/videos.ts64
-rw-r--r--shared/extra-utils/server/activitypub.ts15
-rw-r--r--shared/extra-utils/server/clients.ts20
-rw-r--r--shared/extra-utils/server/config-command.ts263
-rw-r--r--shared/extra-utils/server/config.ts260
-rw-r--r--shared/extra-utils/server/contact-form-command.ts31
-rw-r--r--shared/extra-utils/server/contact-form.ts31
-rw-r--r--shared/extra-utils/server/debug-command.ts33
-rw-r--r--shared/extra-utils/server/debug.ts33
-rw-r--r--shared/extra-utils/server/directories.ts34
-rw-r--r--shared/extra-utils/server/follows-command.ts141
-rw-r--r--shared/extra-utils/server/follows.ts133
-rw-r--r--shared/extra-utils/server/index.ts15
-rw-r--r--shared/extra-utils/server/jobs-command.ts36
-rw-r--r--shared/extra-utils/server/jobs.ts82
-rw-r--r--shared/extra-utils/server/plugins-command.ts256
-rw-r--r--shared/extra-utils/server/plugins.ts299
-rw-r--r--shared/extra-utils/server/redundancy-command.ts80
-rw-r--r--shared/extra-utils/server/redundancy.ts88
-rw-r--r--shared/extra-utils/server/server.ts378
-rw-r--r--shared/extra-utils/server/servers-command.ts81
-rw-r--r--shared/extra-utils/server/servers.ts375
-rw-r--r--shared/extra-utils/server/stats-command.ts25
-rw-r--r--shared/extra-utils/server/stats.ts23
-rw-r--r--shared/extra-utils/shared/abstract-command.ts199
-rw-r--r--shared/extra-utils/shared/index.ts1
-rw-r--r--shared/extra-utils/socket/index.ts1
-rw-r--r--shared/extra-utils/socket/socket-io-command.ts15
-rw-r--r--shared/extra-utils/socket/socket-io.ts18
-rw-r--r--shared/extra-utils/users/accounts-command.ts56
-rw-r--r--shared/extra-utils/users/accounts.ts87
-rw-r--r--shared/extra-utils/users/actors.ts73
-rw-r--r--shared/extra-utils/users/blocklist-command.ts139
-rw-r--r--shared/extra-utils/users/blocklist.ts238
-rw-r--r--shared/extra-utils/users/index.ts9
-rw-r--r--shared/extra-utils/users/login-command.ts132
-rw-r--r--shared/extra-utils/users/login.ts124
-rw-r--r--shared/extra-utils/users/notifications-command.ts86
-rw-r--r--shared/extra-utils/users/notifications.ts (renamed from shared/extra-utils/users/user-notifications.ts)167
-rw-r--r--shared/extra-utils/users/subscriptions-command.ts99
-rw-r--r--shared/extra-utils/users/user-subscriptions.ts93
-rw-r--r--shared/extra-utils/users/users-command.ts414
-rw-r--r--shared/extra-utils/users/users.ts415
-rw-r--r--shared/extra-utils/videos/blacklist-command.ts76
-rw-r--r--shared/extra-utils/videos/captions-command.ts65
-rw-r--r--shared/extra-utils/videos/captions.ts17
-rw-r--r--shared/extra-utils/videos/change-ownership-command.ts68
-rw-r--r--shared/extra-utils/videos/channels-command.ts156
-rw-r--r--shared/extra-utils/videos/channels.ts18
-rw-r--r--shared/extra-utils/videos/comments-command.ts152
-rw-r--r--shared/extra-utils/videos/history-command.ts58
-rw-r--r--shared/extra-utils/videos/imports-command.ts47
-rw-r--r--shared/extra-utils/videos/index.ts19
-rw-r--r--shared/extra-utils/videos/live-command.ts154
-rw-r--r--shared/extra-utils/videos/live.ts147
-rw-r--r--shared/extra-utils/videos/playlists-command.ts279
-rw-r--r--shared/extra-utils/videos/playlists.ts25
-rw-r--r--shared/extra-utils/videos/services-command.ts29
-rw-r--r--shared/extra-utils/videos/services.ts24
-rw-r--r--shared/extra-utils/videos/streaming-playlists-command.ts44
-rw-r--r--shared/extra-utils/videos/streaming-playlists.ts75
-rw-r--r--shared/extra-utils/videos/video-blacklist.ts79
-rw-r--r--shared/extra-utils/videos/video-captions.ts72
-rw-r--r--shared/extra-utils/videos/video-change-ownership.ts72
-rw-r--r--shared/extra-utils/videos/video-channels.ts192
-rw-r--r--shared/extra-utils/videos/video-comments.ts138
-rw-r--r--shared/extra-utils/videos/video-history.ts49
-rw-r--r--shared/extra-utils/videos/video-imports.ts90
-rw-r--r--shared/extra-utils/videos/video-playlists.ts320
-rw-r--r--shared/extra-utils/videos/video-streaming-playlists.ts82
-rw-r--r--shared/extra-utils/videos/videos-command.ts598
-rw-r--r--shared/extra-utils/videos/videos.ts761
115 files changed, 5646 insertions, 5496 deletions
diff --git a/shared/extra-utils/bulk/bulk-command.ts b/shared/extra-utils/bulk/bulk-command.ts
new file mode 100644
index 000000000..b5c5673ce
--- /dev/null
+++ b/shared/extra-utils/bulk/bulk-command.ts
@@ -0,0 +1,20 @@
1import { BulkRemoveCommentsOfBody, HttpStatusCode } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class BulkCommand extends AbstractCommand {
5
6 removeCommentsOf (options: OverrideCommandOptions & {
7 attributes: BulkRemoveCommentsOfBody
8 }) {
9 const { attributes } = options
10
11 return this.postBodyRequest({
12 ...options,
13
14 path: '/api/v1/bulk/remove-comments-of',
15 fields: attributes,
16 implicitToken: true,
17 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
18 })
19 }
20}
diff --git a/shared/extra-utils/bulk/bulk.ts b/shared/extra-utils/bulk/bulk.ts
deleted file mode 100644
index b6f437b8b..000000000
--- a/shared/extra-utils/bulk/bulk.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import { BulkRemoveCommentsOfBody } from "@shared/models/bulk/bulk-remove-comments-of-body.model"
2import { makePostBodyRequest } from "../requests/requests"
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function bulkRemoveCommentsOf (options: {
6 url: string
7 token: string
8 attributes: BulkRemoveCommentsOfBody
9 expectedStatus?: number
10}) {
11 const { url, token, attributes, expectedStatus } = options
12 const path = '/api/v1/bulk/remove-comments-of'
13
14 return makePostBodyRequest({
15 url,
16 path,
17 token,
18 fields: attributes,
19 statusCodeExpected: expectedStatus || HttpStatusCode.NO_CONTENT_204
20 })
21}
22
23export {
24 bulkRemoveCommentsOf
25}
diff --git a/shared/extra-utils/bulk/index.ts b/shared/extra-utils/bulk/index.ts
new file mode 100644
index 000000000..391597243
--- /dev/null
+++ b/shared/extra-utils/bulk/index.ts
@@ -0,0 +1 @@
export * from './bulk-command'
diff --git a/shared/extra-utils/cli/cli-command.ts b/shared/extra-utils/cli/cli-command.ts
new file mode 100644
index 000000000..bc1dddc68
--- /dev/null
+++ b/shared/extra-utils/cli/cli-command.ts
@@ -0,0 +1,23 @@
1import { exec } from 'child_process'
2import { AbstractCommand } from '../shared'
3
4export class CLICommand extends AbstractCommand {
5
6 static exec (command: string) {
7 return new Promise<string>((res, rej) => {
8 exec(command, (err, stdout, _stderr) => {
9 if (err) return rej(err)
10
11 return res(stdout)
12 })
13 })
14 }
15
16 getEnv () {
17 return `NODE_ENV=test NODE_APP_INSTANCE=${this.server.internalServerNumber}`
18 }
19
20 async execWithEnv (command: string) {
21 return CLICommand.exec(`${this.getEnv()} ${command}`)
22 }
23}
diff --git a/shared/extra-utils/cli/cli.ts b/shared/extra-utils/cli/cli.ts
deleted file mode 100644
index c62e170bb..000000000
--- a/shared/extra-utils/cli/cli.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import { exec } from 'child_process'
2
3import { ServerInfo } from '../server/servers'
4
5function getEnvCli (server?: ServerInfo) {
6 return `NODE_ENV=test NODE_APP_INSTANCE=${server.internalServerNumber}`
7}
8
9async function execCLI (command: string) {
10 return new Promise<string>((res, rej) => {
11 exec(command, (err, stdout, stderr) => {
12 if (err) return rej(err)
13
14 return res(stdout)
15 })
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 execCLI,
23 getEnvCli
24}
diff --git a/shared/extra-utils/cli/index.ts b/shared/extra-utils/cli/index.ts
new file mode 100644
index 000000000..91b5abfbe
--- /dev/null
+++ b/shared/extra-utils/cli/index.ts
@@ -0,0 +1 @@
export * from './cli-command'
diff --git a/shared/extra-utils/custom-pages/custom-pages-command.ts b/shared/extra-utils/custom-pages/custom-pages-command.ts
new file mode 100644
index 000000000..cd869a8de
--- /dev/null
+++ b/shared/extra-utils/custom-pages/custom-pages-command.ts
@@ -0,0 +1,33 @@
1import { CustomPage, HttpStatusCode } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class CustomPagesCommand extends AbstractCommand {
5
6 getInstanceHomepage (options: OverrideCommandOptions = {}) {
7 const path = '/api/v1/custom-pages/homepage/instance'
8
9 return this.getRequestBody<CustomPage>({
10 ...options,
11
12 path,
13 implicitToken: false,
14 defaultExpectedStatus: HttpStatusCode.OK_200
15 })
16 }
17
18 updateInstanceHomepage (options: OverrideCommandOptions & {
19 content: string
20 }) {
21 const { content } = options
22 const path = '/api/v1/custom-pages/homepage/instance'
23
24 return this.putBodyRequest({
25 ...options,
26
27 path,
28 fields: { content },
29 implicitToken: true,
30 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
31 })
32 }
33}
diff --git a/shared/extra-utils/custom-pages/custom-pages.ts b/shared/extra-utils/custom-pages/custom-pages.ts
deleted file mode 100644
index bf2d16c70..000000000
--- a/shared/extra-utils/custom-pages/custom-pages.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
2import { makeGetRequest, makePutBodyRequest } from '../requests/requests'
3
4function getInstanceHomepage (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
5 const path = '/api/v1/custom-pages/homepage/instance'
6
7 return makeGetRequest({
8 url,
9 path,
10 statusCodeExpected
11 })
12}
13
14function updateInstanceHomepage (url: string, token: string, content: string) {
15 const path = '/api/v1/custom-pages/homepage/instance'
16
17 return makePutBodyRequest({
18 url,
19 path,
20 token,
21 fields: { content },
22 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
23 })
24}
25
26// ---------------------------------------------------------------------------
27
28export {
29 getInstanceHomepage,
30 updateInstanceHomepage
31}
diff --git a/shared/extra-utils/custom-pages/index.ts b/shared/extra-utils/custom-pages/index.ts
new file mode 100644
index 000000000..58aed04f2
--- /dev/null
+++ b/shared/extra-utils/custom-pages/index.ts
@@ -0,0 +1 @@
export * from './custom-pages-command'
diff --git a/shared/extra-utils/feeds/feeds-command.ts b/shared/extra-utils/feeds/feeds-command.ts
new file mode 100644
index 000000000..3c95f9536
--- /dev/null
+++ b/shared/extra-utils/feeds/feeds-command.ts
@@ -0,0 +1,44 @@
1
2import { HttpStatusCode } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5type FeedType = 'videos' | 'video-comments' | 'subscriptions'
6
7export class FeedCommand extends AbstractCommand {
8
9 getXML (options: OverrideCommandOptions & {
10 feed: FeedType
11 format?: string
12 }) {
13 const { feed, format } = options
14 const path = '/feeds/' + feed + '.xml'
15
16 return this.getRequestText({
17 ...options,
18
19 path,
20 query: format ? { format } : undefined,
21 accept: 'application/xml',
22 implicitToken: false,
23 defaultExpectedStatus: HttpStatusCode.OK_200
24 })
25 }
26
27 getJSON (options: OverrideCommandOptions & {
28 feed: FeedType
29 query?: { [ id: string ]: any }
30 }) {
31 const { feed, query } = options
32 const path = '/feeds/' + feed + '.json'
33
34 return this.getRequestText({
35 ...options,
36
37 path,
38 query,
39 accept: 'application/json',
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 })
43 }
44}
diff --git a/shared/extra-utils/feeds/feeds.ts b/shared/extra-utils/feeds/feeds.ts
deleted file mode 100644
index ce0a98c6d..000000000
--- a/shared/extra-utils/feeds/feeds.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4type FeedType = 'videos' | 'video-comments' | 'subscriptions'
5
6function getXMLfeed (url: string, feed: FeedType, format?: string) {
7 const path = '/feeds/' + feed + '.xml'
8
9 return request(url)
10 .get(path)
11 .query((format) ? { format: format } : {})
12 .set('Accept', 'application/xml')
13 .expect(HttpStatusCode.OK_200)
14 .expect('Content-Type', /xml/)
15}
16
17function getJSONfeed (url: string, feed: FeedType, query: any = {}, statusCodeExpected = HttpStatusCode.OK_200) {
18 const path = '/feeds/' + feed + '.json'
19
20 return request(url)
21 .get(path)
22 .query(query)
23 .set('Accept', 'application/json')
24 .expect(statusCodeExpected)
25 .expect('Content-Type', /json/)
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 getXMLfeed,
32 getJSONfeed
33}
diff --git a/shared/extra-utils/feeds/index.ts b/shared/extra-utils/feeds/index.ts
new file mode 100644
index 000000000..662a22b6f
--- /dev/null
+++ b/shared/extra-utils/feeds/index.ts
@@ -0,0 +1 @@
export * from './feeds-command'
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
index 87ee8abba..4b3636d06 100644
--- a/shared/extra-utils/index.ts
+++ b/shared/extra-utils/index.ts
@@ -1,51 +1,15 @@
1export * from './bulk/bulk' 1export * from './bulk'
2 2export * from './cli'
3export * from './cli/cli' 3export * from './custom-pages'
4 4export * from './feeds'
5export * from './custom-pages/custom-pages' 5export * from './logs'
6 6export * from './miscs'
7export * from './feeds/feeds' 7export * from './mock-servers'
8 8export * from './moderation'
9export * from './mock-servers/mock-instances-index' 9export * from './overviews'
10 10export * from './requests'
11export * from './miscs/email' 11export * from './search'
12export * from './miscs/sql' 12export * from './server'
13export * from './miscs/miscs' 13export * from './socket'
14export * from './miscs/stubs' 14export * from './users'
15 15export * from './videos'
16export * from './moderation/abuses'
17export * from './plugins/mock-blocklist'
18
19export * from './requests/check-api-params'
20export * from './requests/requests'
21
22export * from './search/video-channels'
23export * from './search/video-playlists'
24export * from './search/videos'
25
26export * from './server/activitypub'
27export * from './server/clients'
28export * from './server/config'
29export * from './server/debug'
30export * from './server/follows'
31export * from './server/jobs'
32export * from './server/plugins'
33export * from './server/servers'
34
35export * from './users/accounts'
36export * from './users/blocklist'
37export * from './users/login'
38export * from './users/user-notifications'
39export * from './users/user-subscriptions'
40export * from './users/users'
41
42export * from './videos/live'
43export * from './videos/services'
44export * from './videos/video-blacklist'
45export * from './videos/video-captions'
46export * from './videos/video-change-ownership'
47export * from './videos/video-channels'
48export * from './videos/video-comments'
49export * from './videos/video-playlists'
50export * from './videos/video-streaming-playlists'
51export * from './videos/videos'
diff --git a/shared/extra-utils/logs/index.ts b/shared/extra-utils/logs/index.ts
new file mode 100644
index 000000000..69452d7f0
--- /dev/null
+++ b/shared/extra-utils/logs/index.ts
@@ -0,0 +1 @@
export * from './logs-command'
diff --git a/shared/extra-utils/logs/logs-command.ts b/shared/extra-utils/logs/logs-command.ts
new file mode 100644
index 000000000..5912e814f
--- /dev/null
+++ b/shared/extra-utils/logs/logs-command.ts
@@ -0,0 +1,43 @@
1import { HttpStatusCode } from '@shared/models'
2import { LogLevel } from '../../models/server/log-level.type'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class LogsCommand extends AbstractCommand {
6
7 getLogs (options: OverrideCommandOptions & {
8 startDate: Date
9 endDate?: Date
10 level?: LogLevel
11 }) {
12 const { startDate, endDate, level } = options
13 const path = '/api/v1/server/logs'
14
15 return this.getRequestBody({
16 ...options,
17
18 path,
19 query: { startDate, endDate, level },
20 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.OK_200
22 })
23 }
24
25 getAuditLogs (options: OverrideCommandOptions & {
26 startDate: Date
27 endDate?: Date
28 }) {
29 const { startDate, endDate } = options
30
31 const path = '/api/v1/server/audit-logs'
32
33 return this.getRequestBody({
34 ...options,
35
36 path,
37 query: { startDate, endDate },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.OK_200
40 })
41 }
42
43}
diff --git a/shared/extra-utils/logs/logs.ts b/shared/extra-utils/logs/logs.ts
deleted file mode 100644
index 8d741276c..000000000
--- a/shared/extra-utils/logs/logs.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import { makeGetRequest } from '../requests/requests'
2import { LogLevel } from '../../models/server/log-level.type'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function getLogs (url: string, accessToken: string, startDate: Date, endDate?: Date, level?: LogLevel) {
6 const path = '/api/v1/server/logs'
7
8 return makeGetRequest({
9 url,
10 path,
11 token: accessToken,
12 query: { startDate, endDate, level },
13 statusCodeExpected: HttpStatusCode.OK_200
14 })
15}
16
17function getAuditLogs (url: string, accessToken: string, startDate: Date, endDate?: Date) {
18 const path = '/api/v1/server/audit-logs'
19
20 return makeGetRequest({
21 url,
22 path,
23 token: accessToken,
24 query: { startDate, endDate },
25 statusCodeExpected: HttpStatusCode.OK_200
26 })
27}
28
29export {
30 getLogs,
31 getAuditLogs
32}
diff --git a/shared/extra-utils/miscs/checks.ts b/shared/extra-utils/miscs/checks.ts
new file mode 100644
index 000000000..7fc92f804
--- /dev/null
+++ b/shared/extra-utils/miscs/checks.ts
@@ -0,0 +1,46 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { pathExists, readFile } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@server/helpers/core-utils'
7import { HttpStatusCode } from '@shared/models'
8import { makeGetRequest } from '../requests'
9import { PeerTubeServer } from '../server'
10
11// Default interval -> 5 minutes
12function dateIsValid (dateString: string, interval = 300000) {
13 const dateToCheck = new Date(dateString)
14 const now = new Date()
15
16 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
17}
18
19async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
20 const res = await makeGetRequest({
21 url,
22 path: imagePath,
23 expectedStatus: HttpStatusCode.OK_200
24 })
25
26 const body = res.body
27
28 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
29 const minLength = body.length - ((30 * body.length) / 100)
30 const maxLength = body.length + ((30 * body.length) / 100)
31
32 expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
33 expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
34}
35
36async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
37 const base = server.servers.buildDirectory(directory)
38
39 expect(await pathExists(join(base, filePath))).to.equal(exist)
40}
41
42export {
43 dateIsValid,
44 testImage,
45 testFileExistsOrNot
46}
diff --git a/shared/extra-utils/miscs/generate.ts b/shared/extra-utils/miscs/generate.ts
new file mode 100644
index 000000000..8d6435481
--- /dev/null
+++ b/shared/extra-utils/miscs/generate.ts
@@ -0,0 +1,61 @@
1import * as ffmpeg from 'fluent-ffmpeg'
2import { ensureDir, pathExists } from 'fs-extra'
3import { dirname } from 'path'
4import { buildAbsoluteFixturePath } from './tests'
5
6async function generateHighBitrateVideo () {
7 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
8
9 await ensureDir(dirname(tempFixturePath))
10
11 const exists = await pathExists(tempFixturePath)
12 if (!exists) {
13 console.log('Generating high bitrate video.')
14
15 // Generate a random, high bitrate video on the fly, so we don't have to include
16 // a large file in the repo. The video needs to have a certain minimum length so
17 // that FFmpeg properly applies bitrate limits.
18 // https://stackoverflow.com/a/15795112
19 return new Promise<string>((res, rej) => {
20 ffmpeg()
21 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
22 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
23 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
24 .output(tempFixturePath)
25 .on('error', rej)
26 .on('end', () => res(tempFixturePath))
27 .run()
28 })
29 }
30
31 return tempFixturePath
32}
33
34async function generateVideoWithFramerate (fps = 60) {
35 const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
36
37 await ensureDir(dirname(tempFixturePath))
38
39 const exists = await pathExists(tempFixturePath)
40 if (!exists) {
41 console.log('Generating video with framerate %d.', fps)
42
43 return new Promise<string>((res, rej) => {
44 ffmpeg()
45 .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
46 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
47 .outputOptions([ `-r ${fps}` ])
48 .output(tempFixturePath)
49 .on('error', rej)
50 .on('end', () => res(tempFixturePath))
51 .run()
52 })
53 }
54
55 return tempFixturePath
56}
57
58export {
59 generateHighBitrateVideo,
60 generateVideoWithFramerate
61}
diff --git a/shared/extra-utils/miscs/index.ts b/shared/extra-utils/miscs/index.ts
new file mode 100644
index 000000000..4474661de
--- /dev/null
+++ b/shared/extra-utils/miscs/index.ts
@@ -0,0 +1,5 @@
1export * from './checks'
2export * from './generate'
3export * from './sql-command'
4export * from './tests'
5export * from './webtorrent'
diff --git a/shared/extra-utils/miscs/miscs.ts b/shared/extra-utils/miscs/miscs.ts
deleted file mode 100644
index 462b914d4..000000000
--- a/shared/extra-utils/miscs/miscs.ts
+++ /dev/null
@@ -1,170 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as chai from 'chai'
4import * as ffmpeg from 'fluent-ffmpeg'
5import { ensureDir, pathExists, readFile, stat } from 'fs-extra'
6import { basename, dirname, isAbsolute, join, resolve } from 'path'
7import * as request from 'supertest'
8import * as WebTorrent from 'webtorrent'
9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
10
11const expect = chai.expect
12let webtorrent: WebTorrent.Instance
13
14function immutableAssign<T, U> (target: T, source: U) {
15 return Object.assign<{}, T, U>({}, target, source)
16}
17
18// Default interval -> 5 minutes
19function dateIsValid (dateString: string, interval = 300000) {
20 const dateToCheck = new Date(dateString)
21 const now = new Date()
22
23 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
24}
25
26function wait (milliseconds: number) {
27 return new Promise(resolve => setTimeout(resolve, milliseconds))
28}
29
30function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
31 const WebTorrent = require('webtorrent')
32
33 if (!webtorrent) webtorrent = new WebTorrent()
34 if (refreshWebTorrent === true) webtorrent = new WebTorrent()
35
36 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
37}
38
39function root () {
40 // We are in /miscs
41 let root = join(__dirname, '..', '..', '..')
42
43 if (basename(root) === 'dist') root = resolve(root, '..')
44
45 return root
46}
47
48function buildServerDirectory (server: { internalServerNumber: number }, directory: string) {
49 return join(root(), 'test' + server.internalServerNumber, directory)
50}
51
52async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
53 const res = await request(url)
54 .get(imagePath)
55 .expect(HttpStatusCode.OK_200)
56
57 const body = res.body
58
59 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
60 const minLength = body.length - ((30 * body.length) / 100)
61 const maxLength = body.length + ((30 * body.length) / 100)
62
63 expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
64 expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
65}
66
67async function testFileExistsOrNot (server: { internalServerNumber: number }, directory: string, filePath: string, exist: boolean) {
68 const base = buildServerDirectory(server, directory)
69
70 expect(await pathExists(join(base, filePath))).to.equal(exist)
71}
72
73function isGithubCI () {
74 return !!process.env.GITHUB_WORKSPACE
75}
76
77function buildAbsoluteFixturePath (path: string, customCIPath = false) {
78 if (isAbsolute(path)) return path
79
80 if (customCIPath && process.env.GITHUB_WORKSPACE) {
81 return join(process.env.GITHUB_WORKSPACE, 'fixtures', path)
82 }
83
84 return join(root(), 'server', 'tests', 'fixtures', path)
85}
86
87function areHttpImportTestsDisabled () {
88 const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true'
89
90 if (disabled) console.log('Import tests are disabled')
91
92 return disabled
93}
94
95async function generateHighBitrateVideo () {
96 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
97
98 await ensureDir(dirname(tempFixturePath))
99
100 const exists = await pathExists(tempFixturePath)
101 if (!exists) {
102 console.log('Generating high bitrate video.')
103
104 // Generate a random, high bitrate video on the fly, so we don't have to include
105 // a large file in the repo. The video needs to have a certain minimum length so
106 // that FFmpeg properly applies bitrate limits.
107 // https://stackoverflow.com/a/15795112
108 return new Promise<string>((res, rej) => {
109 ffmpeg()
110 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
111 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
112 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
113 .output(tempFixturePath)
114 .on('error', rej)
115 .on('end', () => res(tempFixturePath))
116 .run()
117 })
118 }
119
120 return tempFixturePath
121}
122
123async function generateVideoWithFramerate (fps = 60) {
124 const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
125
126 await ensureDir(dirname(tempFixturePath))
127
128 const exists = await pathExists(tempFixturePath)
129 if (!exists) {
130 console.log('Generating video with framerate %d.', fps)
131
132 return new Promise<string>((res, rej) => {
133 ffmpeg()
134 .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
135 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
136 .outputOptions([ `-r ${fps}` ])
137 .output(tempFixturePath)
138 .on('error', rej)
139 .on('end', () => res(tempFixturePath))
140 .run()
141 })
142 }
143
144 return tempFixturePath
145}
146
147async function getFileSize (path: string) {
148 const stats = await stat(path)
149
150 return stats.size
151}
152
153// ---------------------------------------------------------------------------
154
155export {
156 dateIsValid,
157 wait,
158 areHttpImportTestsDisabled,
159 buildServerDirectory,
160 webtorrentAdd,
161 getFileSize,
162 immutableAssign,
163 testImage,
164 isGithubCI,
165 buildAbsoluteFixturePath,
166 testFileExistsOrNot,
167 root,
168 generateHighBitrateVideo,
169 generateVideoWithFramerate
170}
diff --git a/shared/extra-utils/miscs/sql-command.ts b/shared/extra-utils/miscs/sql-command.ts
new file mode 100644
index 000000000..80c8cd271
--- /dev/null
+++ b/shared/extra-utils/miscs/sql-command.ts
@@ -0,0 +1,142 @@
1import { QueryTypes, Sequelize } from 'sequelize'
2import { AbstractCommand } from '../shared/abstract-command'
3
4export class SQLCommand extends AbstractCommand {
5 private sequelize: Sequelize
6
7 deleteAll (table: string) {
8 const seq = this.getSequelize()
9
10 const options = { type: QueryTypes.DELETE }
11
12 return seq.query(`DELETE FROM "${table}"`, options)
13 }
14
15 async getCount (table: string) {
16 const seq = this.getSequelize()
17
18 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
19
20 const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options)
21 if (total === null) return 0
22
23 return parseInt(total, 10)
24 }
25
26 setActorField (to: string, field: string, value: string) {
27 const seq = this.getSequelize()
28
29 const options = { type: QueryTypes.UPDATE }
30
31 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
32 }
33
34 setVideoField (uuid: string, field: string, value: string) {
35 const seq = this.getSequelize()
36
37 const options = { type: QueryTypes.UPDATE }
38
39 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
40 }
41
42 setPlaylistField (uuid: string, field: string, value: string) {
43 const seq = this.getSequelize()
44
45 const options = { type: QueryTypes.UPDATE }
46
47 return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
48 }
49
50 async countVideoViewsOf (uuid: string) {
51 const seq = this.getSequelize()
52
53 // tslint:disable
54 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
55 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
56
57 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
58 const [ { total } ] = await seq.query<{ total: number }>(query, options)
59
60 if (!total) return 0
61
62 return parseInt(total + '', 10)
63 }
64
65 getActorImage (filename: string) {
66 return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
67 .then(rows => rows[0])
68 }
69
70 selectQuery (query: string) {
71 const seq = this.getSequelize()
72 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
73
74 return seq.query<any>(query, options)
75 }
76
77 updateQuery (query: string) {
78 const seq = this.getSequelize()
79 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE }
80
81 return seq.query(query, options)
82 }
83
84 setPluginField (pluginName: string, field: string, value: string) {
85 const seq = this.getSequelize()
86
87 const options = { type: QueryTypes.UPDATE }
88
89 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
90 }
91
92 setPluginVersion (pluginName: string, newVersion: string) {
93 return this.setPluginField(pluginName, 'version', newVersion)
94 }
95
96 setPluginLatestVersion (pluginName: string, newVersion: string) {
97 return this.setPluginField(pluginName, 'latestVersion', newVersion)
98 }
99
100 setActorFollowScores (newScore: number) {
101 const seq = this.getSequelize()
102
103 const options = { type: QueryTypes.UPDATE }
104
105 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
106 }
107
108 setTokenField (accessToken: string, field: string, value: string) {
109 const seq = this.getSequelize()
110
111 const options = { type: QueryTypes.UPDATE }
112
113 return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
114 }
115
116 async cleanup () {
117 if (!this.sequelize) return
118
119 await this.sequelize.close()
120 this.sequelize = undefined
121 }
122
123 private getSequelize () {
124 if (this.sequelize) return this.sequelize
125
126 const dbname = 'peertube_test' + this.server.internalServerNumber
127 const username = 'peertube'
128 const password = 'peertube'
129 const host = 'localhost'
130 const port = 5432
131
132 this.sequelize = new Sequelize(dbname, username, password, {
133 dialect: 'postgres',
134 host,
135 port,
136 logging: false
137 })
138
139 return this.sequelize
140 }
141
142}
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts
deleted file mode 100644
index 65a0aa5fe..000000000
--- a/shared/extra-utils/miscs/sql.ts
+++ /dev/null
@@ -1,161 +0,0 @@
1import { QueryTypes, Sequelize } from 'sequelize'
2import { ServerInfo } from '../server/servers'
3
4const sequelizes: { [ id: number ]: Sequelize } = {}
5
6function getSequelize (internalServerNumber: number) {
7 if (sequelizes[internalServerNumber]) return sequelizes[internalServerNumber]
8
9 const dbname = 'peertube_test' + internalServerNumber
10 const username = 'peertube'
11 const password = 'peertube'
12 const host = 'localhost'
13 const port = 5432
14
15 const seq = new Sequelize(dbname, username, password, {
16 dialect: 'postgres',
17 host,
18 port,
19 logging: false
20 })
21
22 sequelizes[internalServerNumber] = seq
23
24 return seq
25}
26
27function deleteAll (internalServerNumber: number, table: string) {
28 const seq = getSequelize(internalServerNumber)
29
30 const options = { type: QueryTypes.DELETE }
31
32 return seq.query(`DELETE FROM "${table}"`, options)
33}
34
35async function getCount (internalServerNumber: number, table: string) {
36 const seq = getSequelize(internalServerNumber)
37
38 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
39
40 const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options)
41 if (total === null) return 0
42
43 return parseInt(total, 10)
44}
45
46function setActorField (internalServerNumber: number, to: string, field: string, value: string) {
47 const seq = getSequelize(internalServerNumber)
48
49 const options = { type: QueryTypes.UPDATE }
50
51 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
52}
53
54function setVideoField (internalServerNumber: number, uuid: string, field: string, value: string) {
55 const seq = getSequelize(internalServerNumber)
56
57 const options = { type: QueryTypes.UPDATE }
58
59 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
60}
61
62function setPlaylistField (internalServerNumber: number, uuid: string, field: string, value: string) {
63 const seq = getSequelize(internalServerNumber)
64
65 const options = { type: QueryTypes.UPDATE }
66
67 return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
68}
69
70async function countVideoViewsOf (internalServerNumber: number, uuid: string) {
71 const seq = getSequelize(internalServerNumber)
72
73 // tslint:disable
74 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
75 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
76
77 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
78 const [ { total } ] = await seq.query<{ total: number }>(query, options)
79
80 if (!total) return 0
81
82 return parseInt(total + '', 10)
83}
84
85function getActorImage (internalServerNumber: number, filename: string) {
86 return selectQuery(internalServerNumber, `SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
87 .then(rows => rows[0])
88}
89
90function selectQuery (internalServerNumber: number, query: string) {
91 const seq = getSequelize(internalServerNumber)
92 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
93
94 return seq.query<any>(query, options)
95}
96
97function updateQuery (internalServerNumber: number, query: string) {
98 const seq = getSequelize(internalServerNumber)
99 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE }
100
101 return seq.query(query, options)
102}
103
104async function closeAllSequelize (servers: ServerInfo[]) {
105 for (const server of servers) {
106 if (sequelizes[server.internalServerNumber]) {
107 await sequelizes[server.internalServerNumber].close()
108 // eslint-disable-next-line
109 delete sequelizes[server.internalServerNumber]
110 }
111 }
112}
113
114function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
115 const seq = getSequelize(internalServerNumber)
116
117 const options = { type: QueryTypes.UPDATE }
118
119 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
120}
121
122function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
123 return setPluginField(internalServerNumber, pluginName, 'version', newVersion)
124}
125
126function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
127 return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion)
128}
129
130function setActorFollowScores (internalServerNumber: number, newScore: number) {
131 const seq = getSequelize(internalServerNumber)
132
133 const options = { type: QueryTypes.UPDATE }
134
135 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
136}
137
138function setTokenField (internalServerNumber: number, accessToken: string, field: string, value: string) {
139 const seq = getSequelize(internalServerNumber)
140
141 const options = { type: QueryTypes.UPDATE }
142
143 return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
144}
145
146export {
147 setVideoField,
148 setPlaylistField,
149 setActorField,
150 countVideoViewsOf,
151 setPluginVersion,
152 setPluginLatestVersion,
153 selectQuery,
154 getActorImage,
155 deleteAll,
156 setTokenField,
157 updateQuery,
158 setActorFollowScores,
159 closeAllSequelize,
160 getCount
161}
diff --git a/shared/extra-utils/miscs/stubs.ts b/shared/extra-utils/miscs/stubs.ts
deleted file mode 100644
index d1eb0e3b2..000000000
--- a/shared/extra-utils/miscs/stubs.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
12 buildResponseStub,
13 buildRequestStub
14}
diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts
new file mode 100644
index 000000000..3dfb2487e
--- /dev/null
+++ b/shared/extra-utils/miscs/tests.ts
@@ -0,0 +1,94 @@
1import { stat } from 'fs-extra'
2import { basename, isAbsolute, join, resolve } from 'path'
3
4const FIXTURE_URLS = {
5 youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM',
6
7 /**
8 * The video is used to check format-selection correctness wrt. HDR,
9 * which brings its own set of oddities outside of a MediaSource.
10 * FIXME: refactor once HDR is supported at playback
11 *
12 * The video needs to have the following format_ids:
13 * (which you can check by using `youtube-dl <url> -F`):
14 * - 303 (1080p webm vp9)
15 * - 299 (1080p mp4 avc1)
16 * - 335 (1080p webm vp9.2 HDR)
17 *
18 * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING
19 * - 400 (1080p mp4 av01)
20 * - 315 (2160p webm vp9 HDR)
21 * - 337 (2160p webm vp9.2 HDR)
22 * - 401 (2160p mp4 av01 HDR)
23 */
24 youtubeHDR: 'https://www.youtube.com/watch?v=qR5vOXbZsI4',
25
26 // eslint-disable-next-line max-len
27 magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4',
28
29 badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
30 goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
31 video4K: 'https://download.cpy.re/peertube/4k_file.txt'
32}
33
34function parallelTests () {
35 return process.env.MOCHA_PARALLEL === 'true'
36}
37
38function isGithubCI () {
39 return !!process.env.GITHUB_WORKSPACE
40}
41
42function areHttpImportTestsDisabled () {
43 const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true'
44
45 if (disabled) console.log('Import tests are disabled')
46
47 return disabled
48}
49
50function buildAbsoluteFixturePath (path: string, customCIPath = false) {
51 if (isAbsolute(path)) return path
52
53 if (customCIPath && process.env.GITHUB_WORKSPACE) {
54 return join(process.env.GITHUB_WORKSPACE, 'fixtures', path)
55 }
56
57 return join(root(), 'server', 'tests', 'fixtures', path)
58}
59
60function root () {
61 // We are in /miscs
62 let root = join(__dirname, '..', '..', '..')
63
64 if (basename(root) === 'dist') root = resolve(root, '..')
65
66 return root
67}
68
69function wait (milliseconds: number) {
70 return new Promise(resolve => setTimeout(resolve, milliseconds))
71}
72
73async function getFileSize (path: string) {
74 const stats = await stat(path)
75
76 return stats.size
77}
78
79function buildRequestStub (): any {
80 return { }
81}
82
83export {
84 FIXTURE_URLS,
85
86 parallelTests,
87 isGithubCI,
88 areHttpImportTestsDisabled,
89 buildAbsoluteFixturePath,
90 getFileSize,
91 buildRequestStub,
92 wait,
93 root
94}
diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts
new file mode 100644
index 000000000..815ea3d56
--- /dev/null
+++ b/shared/extra-utils/miscs/webtorrent.ts
@@ -0,0 +1,30 @@
1import { readFile } from 'fs-extra'
2import * as parseTorrent from 'parse-torrent'
3import { join } from 'path'
4import * as WebTorrent from 'webtorrent'
5import { PeerTubeServer } from '../server'
6
7let webtorrent: WebTorrent.Instance
8
9function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
10 const WebTorrent = require('webtorrent')
11
12 if (!webtorrent) webtorrent = new WebTorrent()
13 if (refreshWebTorrent === true) webtorrent = new WebTorrent()
14
15 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
16}
17
18async function parseTorrentVideo (server: PeerTubeServer, videoUUID: string, resolution: number) {
19 const torrentName = videoUUID + '-' + resolution + '.torrent'
20 const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
21
22 const data = await readFile(torrentPath)
23
24 return parseTorrent(data)
25}
26
27export {
28 webtorrentAdd,
29 parseTorrentVideo
30}
diff --git a/shared/extra-utils/mock-servers/index.ts b/shared/extra-utils/mock-servers/index.ts
new file mode 100644
index 000000000..0ec07f685
--- /dev/null
+++ b/shared/extra-utils/mock-servers/index.ts
@@ -0,0 +1,4 @@
1export * from './mock-email'
2export * from './mock-instances-index'
3export * from './mock-joinpeertube-versions'
4export * from './mock-plugin-blocklist'
diff --git a/shared/extra-utils/miscs/email.ts b/shared/extra-utils/mock-servers/mock-email.ts
index 9fc9a5ad0..ffd62e325 100644
--- a/shared/extra-utils/miscs/email.ts
+++ b/shared/extra-utils/mock-servers/mock-email.ts
@@ -1,6 +1,6 @@
1import { ChildProcess } from 'child_process' 1import { ChildProcess } from 'child_process'
2import { randomInt } from '../../core-utils/miscs/miscs' 2import { randomInt } from '@shared/core-utils'
3import { parallelTests } from '../server/servers' 3import { parallelTests } from '../miscs'
4 4
5const MailDev = require('maildev') 5const MailDev = require('maildev')
6 6
diff --git a/shared/extra-utils/mock-servers/joinpeertube-versions.ts b/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts
index 5ea432ecf..5ea432ecf 100644
--- a/shared/extra-utils/mock-servers/joinpeertube-versions.ts
+++ b/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts
diff --git a/shared/extra-utils/plugins/mock-blocklist.ts b/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts
index d18f8224f..d18f8224f 100644
--- a/shared/extra-utils/plugins/mock-blocklist.ts
+++ b/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts
diff --git a/shared/extra-utils/moderation/abuses-command.ts b/shared/extra-utils/moderation/abuses-command.ts
new file mode 100644
index 000000000..7b3abb056
--- /dev/null
+++ b/shared/extra-utils/moderation/abuses-command.ts
@@ -0,0 +1,228 @@
1import { pick } from 'lodash'
2import {
3 AbuseFilter,
4 AbuseMessage,
5 AbusePredefinedReasonsString,
6 AbuseState,
7 AbuseUpdate,
8 AbuseVideoIs,
9 AdminAbuse,
10 HttpStatusCode,
11 ResultList,
12 UserAbuse
13} from '@shared/models'
14import { unwrapBody } from '../requests/requests'
15import { AbstractCommand, OverrideCommandOptions } from '../shared'
16
17export class AbusesCommand extends AbstractCommand {
18
19 report (options: OverrideCommandOptions & {
20 reason: string
21
22 accountId?: number
23 videoId?: number
24 commentId?: number
25
26 predefinedReasons?: AbusePredefinedReasonsString[]
27
28 startAt?: number
29 endAt?: number
30 }) {
31 const path = '/api/v1/abuses'
32
33 const video = options.videoId
34 ? {
35 id: options.videoId,
36 startAt: options.startAt,
37 endAt: options.endAt
38 }
39 : undefined
40
41 const comment = options.commentId
42 ? { id: options.commentId }
43 : undefined
44
45 const account = options.accountId
46 ? { id: options.accountId }
47 : undefined
48
49 const body = {
50 account,
51 video,
52 comment,
53
54 reason: options.reason,
55 predefinedReasons: options.predefinedReasons
56 }
57
58 return unwrapBody<{ abuse: { id: number } }>(this.postBodyRequest({
59 ...options,
60
61 path,
62 fields: body,
63 implicitToken: true,
64 defaultExpectedStatus: HttpStatusCode.OK_200
65 }))
66 }
67
68 getAdminList (options: OverrideCommandOptions & {
69 start?: number
70 count?: number
71 sort?: string
72
73 id?: number
74 predefinedReason?: AbusePredefinedReasonsString
75 search?: string
76 filter?: AbuseFilter
77 state?: AbuseState
78 videoIs?: AbuseVideoIs
79 searchReporter?: string
80 searchReportee?: string
81 searchVideo?: string
82 searchVideoChannel?: string
83 } = {}) {
84 const toPick = [
85 'count',
86 'filter',
87 'id',
88 'predefinedReason',
89 'search',
90 'searchReportee',
91 'searchReporter',
92 'searchVideo',
93 'searchVideoChannel',
94 'sort',
95 'start',
96 'state',
97 'videoIs'
98 ]
99
100 const path = '/api/v1/abuses'
101
102 const defaultQuery = { sort: 'createdAt' }
103 const query = { ...defaultQuery, ...pick(options, toPick) }
104
105 return this.getRequestBody<ResultList<AdminAbuse>>({
106 ...options,
107
108 path,
109 query,
110 implicitToken: true,
111 defaultExpectedStatus: HttpStatusCode.OK_200
112 })
113 }
114
115 getUserList (options: OverrideCommandOptions & {
116 start?: number
117 count?: number
118 sort?: string
119
120 id?: number
121 search?: string
122 state?: AbuseState
123 }) {
124 const toPick = [
125 'id',
126 'search',
127 'state',
128 'start',
129 'count',
130 'sort'
131 ]
132
133 const path = '/api/v1/users/me/abuses'
134
135 const defaultQuery = { sort: 'createdAt' }
136 const query = { ...defaultQuery, ...pick(options, toPick) }
137
138 return this.getRequestBody<ResultList<UserAbuse>>({
139 ...options,
140
141 path,
142 query,
143 implicitToken: true,
144 defaultExpectedStatus: HttpStatusCode.OK_200
145 })
146 }
147
148 update (options: OverrideCommandOptions & {
149 abuseId: number
150 body: AbuseUpdate
151 }) {
152 const { abuseId, body } = options
153 const path = '/api/v1/abuses/' + abuseId
154
155 return this.putBodyRequest({
156 ...options,
157
158 path,
159 fields: body,
160 implicitToken: true,
161 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
162 })
163 }
164
165 delete (options: OverrideCommandOptions & {
166 abuseId: number
167 }) {
168 const { abuseId } = options
169 const path = '/api/v1/abuses/' + abuseId
170
171 return this.deleteRequest({
172 ...options,
173
174 path,
175 implicitToken: true,
176 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
177 })
178 }
179
180 listMessages (options: OverrideCommandOptions & {
181 abuseId: number
182 }) {
183 const { abuseId } = options
184 const path = '/api/v1/abuses/' + abuseId + '/messages'
185
186 return this.getRequestBody<ResultList<AbuseMessage>>({
187 ...options,
188
189 path,
190 implicitToken: true,
191 defaultExpectedStatus: HttpStatusCode.OK_200
192 })
193 }
194
195 deleteMessage (options: OverrideCommandOptions & {
196 abuseId: number
197 messageId: number
198 }) {
199 const { abuseId, messageId } = options
200 const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
201
202 return this.deleteRequest({
203 ...options,
204
205 path,
206 implicitToken: true,
207 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
208 })
209 }
210
211 addMessage (options: OverrideCommandOptions & {
212 abuseId: number
213 message: string
214 }) {
215 const { abuseId, message } = options
216 const path = '/api/v1/abuses/' + abuseId + '/messages'
217
218 return this.postBodyRequest({
219 ...options,
220
221 path,
222 fields: { message },
223 implicitToken: true,
224 defaultExpectedStatus: HttpStatusCode.OK_200
225 })
226 }
227
228}
diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts
deleted file mode 100644
index c0fda722f..000000000
--- a/shared/extra-utils/moderation/abuses.ts
+++ /dev/null
@@ -1,244 +0,0 @@
1import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
2import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function reportAbuse (options: {
6 url: string
7 token: string
8
9 reason: string
10
11 accountId?: number
12 videoId?: number
13 commentId?: number
14
15 predefinedReasons?: AbusePredefinedReasonsString[]
16
17 startAt?: number
18 endAt?: number
19
20 statusCodeExpected?: number
21}) {
22 const path = '/api/v1/abuses'
23
24 const video = options.videoId
25 ? {
26 id: options.videoId,
27 startAt: options.startAt,
28 endAt: options.endAt
29 }
30 : undefined
31
32 const comment = options.commentId
33 ? { id: options.commentId }
34 : undefined
35
36 const account = options.accountId
37 ? { id: options.accountId }
38 : undefined
39
40 const body = {
41 account,
42 video,
43 comment,
44
45 reason: options.reason,
46 predefinedReasons: options.predefinedReasons
47 }
48
49 return makePostBodyRequest({
50 url: options.url,
51 path,
52 token: options.token,
53
54 fields: body,
55 statusCodeExpected: options.statusCodeExpected || HttpStatusCode.OK_200
56 })
57}
58
59function getAdminAbusesList (options: {
60 url: string
61 token: string
62
63 start?: number
64 count?: number
65 sort?: string
66
67 id?: number
68 predefinedReason?: AbusePredefinedReasonsString
69 search?: string
70 filter?: AbuseFilter
71 state?: AbuseState
72 videoIs?: AbuseVideoIs
73 searchReporter?: string
74 searchReportee?: string
75 searchVideo?: string
76 searchVideoChannel?: string
77}) {
78 const {
79 url,
80 token,
81 start,
82 count,
83 sort,
84 id,
85 predefinedReason,
86 search,
87 filter,
88 state,
89 videoIs,
90 searchReporter,
91 searchReportee,
92 searchVideo,
93 searchVideoChannel
94 } = options
95 const path = '/api/v1/abuses'
96
97 const query = {
98 id,
99 predefinedReason,
100 search,
101 state,
102 filter,
103 videoIs,
104 start,
105 count,
106 sort: sort || 'createdAt',
107 searchReporter,
108 searchReportee,
109 searchVideo,
110 searchVideoChannel
111 }
112
113 return makeGetRequest({
114 url,
115 path,
116 token,
117 query,
118 statusCodeExpected: HttpStatusCode.OK_200
119 })
120}
121
122function getUserAbusesList (options: {
123 url: string
124 token: string
125
126 start?: number
127 count?: number
128 sort?: string
129
130 id?: number
131 search?: string
132 state?: AbuseState
133}) {
134 const {
135 url,
136 token,
137 start,
138 count,
139 sort,
140 id,
141 search,
142 state
143 } = options
144 const path = '/api/v1/users/me/abuses'
145
146 const query = {
147 id,
148 search,
149 state,
150 start,
151 count,
152 sort: sort || 'createdAt'
153 }
154
155 return makeGetRequest({
156 url,
157 path,
158 token,
159 query,
160 statusCodeExpected: HttpStatusCode.OK_200
161 })
162}
163
164function updateAbuse (
165 url: string,
166 token: string,
167 abuseId: number,
168 body: AbuseUpdate,
169 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
170) {
171 const path = '/api/v1/abuses/' + abuseId
172
173 return makePutBodyRequest({
174 url,
175 token,
176 path,
177 fields: body,
178 statusCodeExpected
179 })
180}
181
182function deleteAbuse (url: string, token: string, abuseId: number, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
183 const path = '/api/v1/abuses/' + abuseId
184
185 return makeDeleteRequest({
186 url,
187 token,
188 path,
189 statusCodeExpected
190 })
191}
192
193function listAbuseMessages (url: string, token: string, abuseId: number, statusCodeExpected = HttpStatusCode.OK_200) {
194 const path = '/api/v1/abuses/' + abuseId + '/messages'
195
196 return makeGetRequest({
197 url,
198 token,
199 path,
200 statusCodeExpected
201 })
202}
203
204function deleteAbuseMessage (
205 url: string,
206 token: string,
207 abuseId: number,
208 messageId: number,
209 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
210) {
211 const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
212
213 return makeDeleteRequest({
214 url,
215 token,
216 path,
217 statusCodeExpected
218 })
219}
220
221function addAbuseMessage (url: string, token: string, abuseId: number, message: string, statusCodeExpected = HttpStatusCode.OK_200) {
222 const path = '/api/v1/abuses/' + abuseId + '/messages'
223
224 return makePostBodyRequest({
225 url,
226 token,
227 path,
228 fields: { message },
229 statusCodeExpected
230 })
231}
232
233// ---------------------------------------------------------------------------
234
235export {
236 reportAbuse,
237 getAdminAbusesList,
238 updateAbuse,
239 deleteAbuse,
240 getUserAbusesList,
241 listAbuseMessages,
242 deleteAbuseMessage,
243 addAbuseMessage
244}
diff --git a/shared/extra-utils/moderation/index.ts b/shared/extra-utils/moderation/index.ts
new file mode 100644
index 000000000..b37643956
--- /dev/null
+++ b/shared/extra-utils/moderation/index.ts
@@ -0,0 +1 @@
export * from './abuses-command'
diff --git a/shared/extra-utils/overviews/index.ts b/shared/extra-utils/overviews/index.ts
new file mode 100644
index 000000000..e19551907
--- /dev/null
+++ b/shared/extra-utils/overviews/index.ts
@@ -0,0 +1 @@
export * from './overviews-command'
diff --git a/shared/extra-utils/overviews/overviews-command.ts b/shared/extra-utils/overviews/overviews-command.ts
new file mode 100644
index 000000000..06b4892d2
--- /dev/null
+++ b/shared/extra-utils/overviews/overviews-command.ts
@@ -0,0 +1,23 @@
1import { HttpStatusCode, VideosOverview } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class OverviewsCommand extends AbstractCommand {
5
6 getVideos (options: OverrideCommandOptions & {
7 page: number
8 }) {
9 const { page } = options
10 const path = '/api/v1/overviews/videos'
11
12 const query = { page }
13
14 return this.getRequestBody<VideosOverview>({
15 ...options,
16
17 path,
18 query,
19 implicitToken: false,
20 defaultExpectedStatus: HttpStatusCode.OK_200
21 })
22 }
23}
diff --git a/shared/extra-utils/overviews/overviews.ts b/shared/extra-utils/overviews/overviews.ts
deleted file mode 100644
index 5e1a13e5e..000000000
--- a/shared/extra-utils/overviews/overviews.ts
+++ /dev/null
@@ -1,34 +0,0 @@
1import { makeGetRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getVideosOverview (url: string, page: number, statusCodeExpected = HttpStatusCode.OK_200) {
5 const path = '/api/v1/overviews/videos'
6
7 const query = { page }
8
9 return makeGetRequest({
10 url,
11 path,
12 query,
13 statusCodeExpected
14 })
15}
16
17function getVideosOverviewWithToken (url: string, page: number, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
18 const path = '/api/v1/overviews/videos'
19
20 const query = { page }
21
22 return makeGetRequest({
23 url,
24 path,
25 query,
26 token,
27 statusCodeExpected
28 })
29}
30
31export {
32 getVideosOverview,
33 getVideosOverviewWithToken
34}
diff --git a/shared/extra-utils/requests/activitypub.ts b/shared/extra-utils/requests/activitypub.ts
index ecd8ce823..4ae878384 100644
--- a/shared/extra-utils/requests/activitypub.ts
+++ b/shared/extra-utils/requests/activitypub.ts
@@ -1,7 +1,7 @@
1import { activityPubContextify } from '../../../server/helpers/activitypub'
1import { doRequest } from '../../../server/helpers/requests' 2import { doRequest } from '../../../server/helpers/requests'
2import { HTTP_SIGNATURE } from '../../../server/initializers/constants' 3import { HTTP_SIGNATURE } from '../../../server/initializers/constants'
3import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils' 4import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../server/helpers/activitypub'
5 5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { 6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = { 7 const options = {
diff --git a/shared/extra-utils/requests/check-api-params.ts b/shared/extra-utils/requests/check-api-params.ts
index 7f5ff775c..26ba1e913 100644
--- a/shared/extra-utils/requests/check-api-params.ts
+++ b/shared/extra-utils/requests/check-api-params.ts
@@ -1,14 +1,13 @@
1import { HttpStatusCode } from '@shared/models'
1import { makeGetRequest } from './requests' 2import { makeGetRequest } from './requests'
2import { immutableAssign } from '../miscs/miscs'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4 3
5function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { 4function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
6 return makeGetRequest({ 5 return makeGetRequest({
7 url, 6 url,
8 path, 7 path,
9 token, 8 token,
10 query: immutableAssign(query, { start: 'hello' }), 9 query: { ...query, start: 'hello' },
11 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 10 expectedStatus: HttpStatusCode.BAD_REQUEST_400
12 }) 11 })
13} 12}
14 13
@@ -17,16 +16,16 @@ async function checkBadCountPagination (url: string, path: string, token?: strin
17 url, 16 url,
18 path, 17 path,
19 token, 18 token,
20 query: immutableAssign(query, { count: 'hello' }), 19 query: { ...query, count: 'hello' },
21 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 20 expectedStatus: HttpStatusCode.BAD_REQUEST_400
22 }) 21 })
23 22
24 await makeGetRequest({ 23 await makeGetRequest({
25 url, 24 url,
26 path, 25 path,
27 token, 26 token,
28 query: immutableAssign(query, { count: 2000 }), 27 query: { ...query, count: 2000 },
29 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 28 expectedStatus: HttpStatusCode.BAD_REQUEST_400
30 }) 29 })
31} 30}
32 31
@@ -35,8 +34,8 @@ function checkBadSortPagination (url: string, path: string, token?: string, quer
35 url, 34 url,
36 path, 35 path,
37 token, 36 token,
38 query: immutableAssign(query, { sort: 'hello' }), 37 query: { ...query, sort: 'hello' },
39 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 38 expectedStatus: HttpStatusCode.BAD_REQUEST_400
40 }) 39 })
41} 40}
42 41
diff --git a/shared/extra-utils/requests/index.ts b/shared/extra-utils/requests/index.ts
new file mode 100644
index 000000000..501163f92
--- /dev/null
+++ b/shared/extra-utils/requests/index.ts
@@ -0,0 +1,3 @@
1// Don't include activitypub that import stuff from server
2export * from './check-api-params'
3export * from './requests'
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts
index 38e24d897..70f790222 100644
--- a/shared/extra-utils/requests/requests.ts
+++ b/shared/extra-utils/requests/requests.ts
@@ -1,103 +1,82 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1/* eslint-disable @typescript-eslint/no-floating-promises */
2 2
3import { decode } from 'querystring'
3import * as request from 'supertest' 4import * as request from 'supertest'
4import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
5import { isAbsolute, join } from 'path'
6import { URL } from 'url' 5import { URL } from 'url'
7import { decode } from 'querystring' 6import { HttpStatusCode } from '@shared/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 7import { buildAbsoluteFixturePath } from '../miscs/tests'
9 8
10function get4KFileUrl () { 9export type CommonRequestParams = {
11 return 'https://download.cpy.re/peertube/4k_file.txt' 10 url: string
11 path?: string
12 contentType?: string
13 range?: string
14 redirects?: number
15 accept?: string
16 host?: string
17 token?: string
18 headers?: { [ name: string ]: string }
19 type?: string
20 xForwardedFor?: string
21 expectedStatus?: HttpStatusCode
12} 22}
13 23
14function makeRawRequest (url: string, statusCodeExpected?: HttpStatusCode, range?: string) { 24function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) {
15 const { host, protocol, pathname } = new URL(url) 25 const { host, protocol, pathname } = new URL(url)
16 26
17 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range }) 27 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range })
18} 28}
19 29
20function makeGetRequest (options: { 30function makeGetRequest (options: CommonRequestParams & {
21 url: string
22 path?: string
23 query?: any 31 query?: any
24 token?: string
25 statusCodeExpected?: HttpStatusCode
26 contentType?: string
27 range?: string
28 redirects?: number
29 accept?: string
30}) { 32}) {
31 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
32 if (options.contentType === undefined) options.contentType = 'application/json'
33
34 const req = request(options.url).get(options.path) 33 const req = request(options.url).get(options.path)
34 .query(options.query)
35 35
36 if (options.contentType) req.set('Accept', options.contentType) 36 return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
37 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
38 if (options.query) req.query(options.query)
39 if (options.range) req.set('Range', options.range)
40 if (options.accept) req.set('Accept', options.accept)
41 if (options.redirects) req.redirects(options.redirects)
42
43 return req.expect(options.statusCodeExpected)
44} 37}
45 38
46function makeDeleteRequest (options: { 39function makeHTMLRequest (url: string, path: string) {
47 url: string 40 return makeGetRequest({
48 path: string 41 url,
49 token?: string 42 path,
50 statusCodeExpected?: HttpStatusCode 43 accept: 'text/html',
51}) { 44 expectedStatus: HttpStatusCode.OK_200
52 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 45 })
46}
53 47
54 const req = request(options.url) 48function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
55 .delete(options.path) 49 return makeGetRequest({
56 .set('Accept', 'application/json') 50 url,
51 path,
52 expectedStatus: expectedStatus,
53 accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8'
54 })
55}
57 56
58 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 57function makeDeleteRequest (options: CommonRequestParams) {
58 const req = request(options.url).delete(options.path)
59 59
60 return req.expect(options.statusCodeExpected) 60 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
61} 61}
62 62
63function makeUploadRequest (options: { 63function makeUploadRequest (options: CommonRequestParams & {
64 url: string
65 method?: 'POST' | 'PUT' 64 method?: 'POST' | 'PUT'
66 path: string 65
67 token?: string
68 fields: { [ fieldName: string ]: any } 66 fields: { [ fieldName: string ]: any }
69 attaches?: { [ attachName: string ]: any | any[] } 67 attaches?: { [ attachName: string ]: any | any[] }
70 statusCodeExpected?: HttpStatusCode
71}) { 68}) {
72 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 69 let req = options.method === 'PUT'
73 70 ? request(options.url).put(options.path)
74 let req: request.Test 71 : request(options.url).post(options.path)
75 if (options.method === 'PUT') {
76 req = request(options.url).put(options.path)
77 } else {
78 req = request(options.url).post(options.path)
79 }
80
81 req.set('Accept', 'application/json')
82 72
83 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 73 req = buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
84 74
85 Object.keys(options.fields).forEach(field => { 75 buildFields(req, options.fields)
86 const value = options.fields[field]
87
88 if (value === undefined) return
89
90 if (Array.isArray(value)) {
91 for (let i = 0; i < value.length; i++) {
92 req.field(field + '[' + i + ']', value[i])
93 }
94 } else {
95 req.field(field, value)
96 }
97 })
98 76
99 Object.keys(options.attaches || {}).forEach(attach => { 77 Object.keys(options.attaches || {}).forEach(attach => {
100 const value = options.attaches[attach] 78 const value = options.attaches[attach]
79
101 if (Array.isArray(value)) { 80 if (Array.isArray(value)) {
102 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) 81 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
103 } else { 82 } else {
@@ -105,27 +84,16 @@ function makeUploadRequest (options: {
105 } 84 }
106 }) 85 })
107 86
108 return req.expect(options.statusCodeExpected) 87 return req
109} 88}
110 89
111function makePostBodyRequest (options: { 90function makePostBodyRequest (options: CommonRequestParams & {
112 url: string
113 path: string
114 token?: string
115 fields?: { [ fieldName: string ]: any } 91 fields?: { [ fieldName: string ]: any }
116 statusCodeExpected?: HttpStatusCode
117}) { 92}) {
118 if (!options.fields) options.fields = {} 93 const req = request(options.url).post(options.path)
119 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 94 .send(options.fields)
120
121 const req = request(options.url)
122 .post(options.path)
123 .set('Accept', 'application/json')
124 95
125 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 96 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
126
127 return req.send(options.fields)
128 .expect(options.statusCodeExpected)
129} 97}
130 98
131function makePutBodyRequest (options: { 99function makePutBodyRequest (options: {
@@ -133,59 +101,29 @@ function makePutBodyRequest (options: {
133 path: string 101 path: string
134 token?: string 102 token?: string
135 fields: { [ fieldName: string ]: any } 103 fields: { [ fieldName: string ]: any }
136 statusCodeExpected?: HttpStatusCode 104 expectedStatus?: HttpStatusCode
137}) { 105}) {
138 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 106 const req = request(options.url).put(options.path)
139 107 .send(options.fields)
140 const req = request(options.url)
141 .put(options.path)
142 .set('Accept', 'application/json')
143 108
144 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 109 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
145
146 return req.send(options.fields)
147 .expect(options.statusCodeExpected)
148} 110}
149 111
150function makeHTMLRequest (url: string, path: string) { 112function decodeQueryString (path: string) {
151 return request(url) 113 return decode(path.split('?')[1])
152 .get(path)
153 .set('Accept', 'text/html')
154 .expect(HttpStatusCode.OK_200)
155} 114}
156 115
157function updateImageRequest (options: { 116function unwrapBody <T> (test: request.Test): Promise<T> {
158 url: string 117 return test.then(res => res.body)
159 path: string
160 accessToken: string
161 fixture: string
162 fieldname: string
163}) {
164 let filePath = ''
165 if (isAbsolute(options.fixture)) {
166 filePath = options.fixture
167 } else {
168 filePath = join(root(), 'server', 'tests', 'fixtures', options.fixture)
169 }
170
171 return makeUploadRequest({
172 url: options.url,
173 path: options.path,
174 token: options.accessToken,
175 fields: {},
176 attaches: { [options.fieldname]: filePath },
177 statusCodeExpected: HttpStatusCode.OK_200
178 })
179} 118}
180 119
181function decodeQueryString (path: string) { 120function unwrapText (test: request.Test): Promise<string> {
182 return decode(path.split('?')[1]) 121 return test.then(res => res.text)
183} 122}
184 123
185// --------------------------------------------------------------------------- 124// ---------------------------------------------------------------------------
186 125
187export { 126export {
188 get4KFileUrl,
189 makeHTMLRequest, 127 makeHTMLRequest,
190 makeGetRequest, 128 makeGetRequest,
191 decodeQueryString, 129 decodeQueryString,
@@ -194,5 +132,51 @@ export {
194 makePutBodyRequest, 132 makePutBodyRequest,
195 makeDeleteRequest, 133 makeDeleteRequest,
196 makeRawRequest, 134 makeRawRequest,
197 updateImageRequest 135 makeActivityPubGetRequest,
136 unwrapBody,
137 unwrapText
138}
139
140// ---------------------------------------------------------------------------
141
142function buildRequest (req: request.Test, options: CommonRequestParams) {
143 if (options.contentType) req.set('Accept', options.contentType)
144 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
145 if (options.range) req.set('Range', options.range)
146 if (options.accept) req.set('Accept', options.accept)
147 if (options.host) req.set('Host', options.host)
148 if (options.redirects) req.redirects(options.redirects)
149 if (options.expectedStatus) req.expect(options.expectedStatus)
150 if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
151 if (options.type) req.type(options.type)
152
153 Object.keys(options.headers || {}).forEach(name => {
154 req.set(name, options.headers[name])
155 })
156
157 return req
158}
159
160function buildFields (req: request.Test, fields: { [ fieldName: string ]: any }, namespace?: string) {
161 if (!fields) return
162
163 let formKey: string
164
165 for (const key of Object.keys(fields)) {
166 if (namespace) formKey = `${namespace}[${key}]`
167 else formKey = key
168
169 if (fields[key] === undefined) continue
170
171 if (Array.isArray(fields[key]) && fields[key].length === 0) {
172 req.field(key, null)
173 continue
174 }
175
176 if (fields[key] !== null && typeof fields[key] === 'object') {
177 buildFields(req, fields[key], formKey)
178 } else {
179 req.field(formKey, fields[key])
180 }
181 }
198} 182}
diff --git a/shared/extra-utils/search/index.ts b/shared/extra-utils/search/index.ts
new file mode 100644
index 000000000..48dbe8ae9
--- /dev/null
+++ b/shared/extra-utils/search/index.ts
@@ -0,0 +1 @@
export * from './search-command'
diff --git a/shared/extra-utils/search/search-command.ts b/shared/extra-utils/search/search-command.ts
new file mode 100644
index 000000000..0fbbcd6ef
--- /dev/null
+++ b/shared/extra-utils/search/search-command.ts
@@ -0,0 +1,98 @@
1import {
2 HttpStatusCode,
3 ResultList,
4 Video,
5 VideoChannel,
6 VideoChannelsSearchQuery,
7 VideoPlaylist,
8 VideoPlaylistsSearchQuery,
9 VideosSearchQuery
10} from '@shared/models'
11import { AbstractCommand, OverrideCommandOptions } from '../shared'
12
13export class SearchCommand extends AbstractCommand {
14
15 searchChannels (options: OverrideCommandOptions & {
16 search: string
17 }) {
18 return this.advancedChannelSearch({
19 ...options,
20
21 search: { search: options.search }
22 })
23 }
24
25 advancedChannelSearch (options: OverrideCommandOptions & {
26 search: VideoChannelsSearchQuery
27 }) {
28 const { search } = options
29 const path = '/api/v1/search/video-channels'
30
31 return this.getRequestBody<ResultList<VideoChannel>>({
32 ...options,
33
34 path,
35 query: search,
36 implicitToken: false,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 searchPlaylists (options: OverrideCommandOptions & {
42 search: string
43 }) {
44 return this.advancedPlaylistSearch({
45 ...options,
46
47 search: { search: options.search }
48 })
49 }
50
51 advancedPlaylistSearch (options: OverrideCommandOptions & {
52 search: VideoPlaylistsSearchQuery
53 }) {
54 const { search } = options
55 const path = '/api/v1/search/video-playlists'
56
57 return this.getRequestBody<ResultList<VideoPlaylist>>({
58 ...options,
59
60 path,
61 query: search,
62 implicitToken: false,
63 defaultExpectedStatus: HttpStatusCode.OK_200
64 })
65 }
66
67 searchVideos (options: OverrideCommandOptions & {
68 search: string
69 sort?: string
70 }) {
71 const { search, sort } = options
72
73 return this.advancedVideoSearch({
74 ...options,
75
76 search: {
77 search: search,
78 sort: sort ?? '-publishedAt'
79 }
80 })
81 }
82
83 advancedVideoSearch (options: OverrideCommandOptions & {
84 search: VideosSearchQuery
85 }) {
86 const { search } = options
87 const path = '/api/v1/search/videos'
88
89 return this.getRequestBody<ResultList<Video>>({
90 ...options,
91
92 path,
93 query: search,
94 implicitToken: false,
95 defaultExpectedStatus: HttpStatusCode.OK_200
96 })
97 }
98}
diff --git a/shared/extra-utils/search/video-channels.ts b/shared/extra-utils/search/video-channels.ts
deleted file mode 100644
index 8e0f42578..000000000
--- a/shared/extra-utils/search/video-channels.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import { VideoChannelsSearchQuery } from '@shared/models'
2import { makeGetRequest } from '../requests/requests'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = HttpStatusCode.OK_200) {
6 const path = '/api/v1/search/video-channels'
7
8 return makeGetRequest({
9 url,
10 path,
11 query: {
12 sort: '-createdAt',
13 search
14 },
15 token,
16 statusCodeExpected
17 })
18}
19
20function advancedVideoChannelSearch (url: string, search: VideoChannelsSearchQuery) {
21 const path = '/api/v1/search/video-channels'
22
23 return makeGetRequest({
24 url,
25 path,
26 query: search,
27 statusCodeExpected: HttpStatusCode.OK_200
28 })
29}
30
31// ---------------------------------------------------------------------------
32
33export {
34 searchVideoChannel,
35 advancedVideoChannelSearch
36}
diff --git a/shared/extra-utils/search/video-playlists.ts b/shared/extra-utils/search/video-playlists.ts
deleted file mode 100644
index c22831df7..000000000
--- a/shared/extra-utils/search/video-playlists.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import { VideoPlaylistsSearchQuery } from '@shared/models'
2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
3import { makeGetRequest } from '../requests/requests'
4
5function searchVideoPlaylists (url: string, search: string, token?: string, statusCodeExpected = HttpStatusCode.OK_200) {
6 const path = '/api/v1/search/video-playlists'
7
8 return makeGetRequest({
9 url,
10 path,
11 query: {
12 sort: '-createdAt',
13 search
14 },
15 token,
16 statusCodeExpected
17 })
18}
19
20function advancedVideoPlaylistSearch (url: string, search: VideoPlaylistsSearchQuery) {
21 const path = '/api/v1/search/video-playlists'
22
23 return makeGetRequest({
24 url,
25 path,
26 query: search,
27 statusCodeExpected: HttpStatusCode.OK_200
28 })
29}
30
31// ---------------------------------------------------------------------------
32
33export {
34 searchVideoPlaylists,
35 advancedVideoPlaylistSearch
36}
diff --git a/shared/extra-utils/search/videos.ts b/shared/extra-utils/search/videos.ts
deleted file mode 100644
index db6edbd58..000000000
--- a/shared/extra-utils/search/videos.ts
+++ /dev/null
@@ -1,64 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as request from 'supertest'
4import { VideosSearchQuery } from '../../models/search'
5import { immutableAssign } from '../miscs/miscs'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7
8function searchVideo (url: string, search: string, sort = '-publishedAt') {
9 const path = '/api/v1/search/videos'
10
11 const query = { sort, search: search }
12 const req = request(url)
13 .get(path)
14 .query(query)
15 .set('Accept', 'application/json')
16
17 return req.expect(HttpStatusCode.OK_200)
18 .expect('Content-Type', /json/)
19}
20
21function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
22 const path = '/api/v1/search/videos'
23 const req = request(url)
24 .get(path)
25 .set('Authorization', 'Bearer ' + token)
26 .query(immutableAssign(query, { sort: '-publishedAt', search }))
27 .set('Accept', 'application/json')
28
29 return req.expect(HttpStatusCode.OK_200)
30 .expect('Content-Type', /json/)
31}
32
33function searchVideoWithSort (url: string, search: string, sort: string) {
34 const path = '/api/v1/search/videos'
35
36 const query = { search, sort }
37
38 return request(url)
39 .get(path)
40 .query(query)
41 .set('Accept', 'application/json')
42 .expect(HttpStatusCode.OK_200)
43 .expect('Content-Type', /json/)
44}
45
46function advancedVideosSearch (url: string, options: VideosSearchQuery) {
47 const path = '/api/v1/search/videos'
48
49 return request(url)
50 .get(path)
51 .query(options)
52 .set('Accept', 'application/json')
53 .expect(HttpStatusCode.OK_200)
54 .expect('Content-Type', /json/)
55}
56
57// ---------------------------------------------------------------------------
58
59export {
60 searchVideo,
61 advancedVideosSearch,
62 searchVideoWithToken,
63 searchVideoWithSort
64}
diff --git a/shared/extra-utils/server/activitypub.ts b/shared/extra-utils/server/activitypub.ts
deleted file mode 100644
index cf967ed7d..000000000
--- a/shared/extra-utils/server/activitypub.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
5 return request(url)
6 .get(path)
7 .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8')
8 .expect(expectedStatus)
9}
10
11// ---------------------------------------------------------------------------
12
13export {
14 makeActivityPubGetRequest
15}
diff --git a/shared/extra-utils/server/clients.ts b/shared/extra-utils/server/clients.ts
deleted file mode 100644
index 894fe4911..000000000
--- a/shared/extra-utils/server/clients.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import * as request from 'supertest'
2import { URL } from 'url'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function getClient (url: string) {
6 const path = '/api/v1/oauth-clients/local'
7
8 return request(url)
9 .get(path)
10 .set('Host', new URL(url).host)
11 .set('Accept', 'application/json')
12 .expect(HttpStatusCode.OK_200)
13 .expect('Content-Type', /json/)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 getClient
20}
diff --git a/shared/extra-utils/server/config-command.ts b/shared/extra-utils/server/config-command.ts
new file mode 100644
index 000000000..11148aa46
--- /dev/null
+++ b/shared/extra-utils/server/config-command.ts
@@ -0,0 +1,263 @@
1import { merge } from 'lodash'
2import { DeepPartial } from '@shared/core-utils'
3import { About, HttpStatusCode, ServerConfig } from '@shared/models'
4import { CustomConfig } from '../../models/server/custom-config.model'
5import { AbstractCommand, OverrideCommandOptions } from '../shared'
6
7export class ConfigCommand extends AbstractCommand {
8
9 static getCustomConfigResolutions (enabled: boolean) {
10 return {
11 '240p': enabled,
12 '360p': enabled,
13 '480p': enabled,
14 '720p': enabled,
15 '1080p': enabled,
16 '1440p': enabled,
17 '2160p': enabled
18 }
19 }
20
21 getConfig (options: OverrideCommandOptions = {}) {
22 const path = '/api/v1/config'
23
24 return this.getRequestBody<ServerConfig>({
25 ...options,
26
27 path,
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 })
31 }
32
33 getAbout (options: OverrideCommandOptions = {}) {
34 const path = '/api/v1/config/about'
35
36 return this.getRequestBody<About>({
37 ...options,
38
39 path,
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 })
43 }
44
45 getCustomConfig (options: OverrideCommandOptions = {}) {
46 const path = '/api/v1/config/custom'
47
48 return this.getRequestBody<CustomConfig>({
49 ...options,
50
51 path,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 updateCustomConfig (options: OverrideCommandOptions & {
58 newCustomConfig: CustomConfig
59 }) {
60 const path = '/api/v1/config/custom'
61
62 return this.putBodyRequest({
63 ...options,
64
65 path,
66 fields: options.newCustomConfig,
67 implicitToken: true,
68 defaultExpectedStatus: HttpStatusCode.OK_200
69 })
70 }
71
72 deleteCustomConfig (options: OverrideCommandOptions = {}) {
73 const path = '/api/v1/config/custom'
74
75 return this.deleteRequest({
76 ...options,
77
78 path,
79 implicitToken: true,
80 defaultExpectedStatus: HttpStatusCode.OK_200
81 })
82 }
83
84 updateCustomSubConfig (options: OverrideCommandOptions & {
85 newConfig: DeepPartial<CustomConfig>
86 }) {
87 const newCustomConfig: CustomConfig = {
88 instance: {
89 name: 'PeerTube updated',
90 shortDescription: 'my short description',
91 description: 'my super description',
92 terms: 'my super terms',
93 codeOfConduct: 'my super coc',
94
95 creationReason: 'my super creation reason',
96 moderationInformation: 'my super moderation information',
97 administrator: 'Kuja',
98 maintenanceLifetime: 'forever',
99 businessModel: 'my super business model',
100 hardwareInformation: '2vCore 3GB RAM',
101
102 languages: [ 'en', 'es' ],
103 categories: [ 1, 2 ],
104
105 isNSFW: true,
106 defaultNSFWPolicy: 'blur',
107
108 defaultClientRoute: '/videos/recently-added',
109
110 customizations: {
111 javascript: 'alert("coucou")',
112 css: 'body { background-color: red; }'
113 }
114 },
115 theme: {
116 default: 'default'
117 },
118 services: {
119 twitter: {
120 username: '@MySuperUsername',
121 whitelisted: true
122 }
123 },
124 cache: {
125 previews: {
126 size: 2
127 },
128 captions: {
129 size: 3
130 },
131 torrents: {
132 size: 4
133 }
134 },
135 signup: {
136 enabled: false,
137 limit: 5,
138 requiresEmailVerification: false,
139 minimumAge: 16
140 },
141 admin: {
142 email: 'superadmin1@example.com'
143 },
144 contactForm: {
145 enabled: true
146 },
147 user: {
148 videoQuota: 5242881,
149 videoQuotaDaily: 318742
150 },
151 transcoding: {
152 enabled: true,
153 allowAdditionalExtensions: true,
154 allowAudioFiles: true,
155 threads: 1,
156 concurrency: 3,
157 profile: 'default',
158 resolutions: {
159 '0p': false,
160 '240p': false,
161 '360p': true,
162 '480p': true,
163 '720p': false,
164 '1080p': false,
165 '1440p': false,
166 '2160p': false
167 },
168 webtorrent: {
169 enabled: true
170 },
171 hls: {
172 enabled: false
173 }
174 },
175 live: {
176 enabled: true,
177 allowReplay: false,
178 maxDuration: -1,
179 maxInstanceLives: -1,
180 maxUserLives: 50,
181 transcoding: {
182 enabled: true,
183 threads: 4,
184 profile: 'default',
185 resolutions: {
186 '240p': true,
187 '360p': true,
188 '480p': true,
189 '720p': true,
190 '1080p': true,
191 '1440p': true,
192 '2160p': true
193 }
194 }
195 },
196 import: {
197 videos: {
198 concurrency: 3,
199 http: {
200 enabled: false
201 },
202 torrent: {
203 enabled: false
204 }
205 }
206 },
207 trending: {
208 videos: {
209 algorithms: {
210 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
211 default: 'hot'
212 }
213 }
214 },
215 autoBlacklist: {
216 videos: {
217 ofUsers: {
218 enabled: false
219 }
220 }
221 },
222 followers: {
223 instance: {
224 enabled: true,
225 manualApproval: false
226 }
227 },
228 followings: {
229 instance: {
230 autoFollowBack: {
231 enabled: false
232 },
233 autoFollowIndex: {
234 indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
235 enabled: false
236 }
237 }
238 },
239 broadcastMessage: {
240 enabled: true,
241 level: 'warning',
242 message: 'hello',
243 dismissable: true
244 },
245 search: {
246 remoteUri: {
247 users: true,
248 anonymous: true
249 },
250 searchIndex: {
251 enabled: true,
252 url: 'https://search.joinpeertube.org',
253 disableLocalSearch: true,
254 isDefaultSearch: true
255 }
256 }
257 }
258
259 merge(newCustomConfig, options.newConfig)
260
261 return this.updateCustomConfig({ ...options, newCustomConfig })
262 }
263}
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
deleted file mode 100644
index 9fcfb31fd..000000000
--- a/shared/extra-utils/server/config.ts
+++ /dev/null
@@ -1,260 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../models/server/custom-config.model'
3import { DeepPartial, HttpStatusCode } from '@shared/core-utils'
4import { merge } from 'lodash'
5
6function getConfig (url: string) {
7 const path = '/api/v1/config'
8
9 return makeGetRequest({
10 url,
11 path,
12 statusCodeExpected: HttpStatusCode.OK_200
13 })
14}
15
16function getAbout (url: string) {
17 const path = '/api/v1/config/about'
18
19 return makeGetRequest({
20 url,
21 path,
22 statusCodeExpected: HttpStatusCode.OK_200
23 })
24}
25
26function getCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
27 const path = '/api/v1/config/custom'
28
29 return makeGetRequest({
30 url,
31 token,
32 path,
33 statusCodeExpected
34 })
35}
36
37function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = HttpStatusCode.OK_200) {
38 const path = '/api/v1/config/custom'
39
40 return makePutBodyRequest({
41 url,
42 token,
43 path,
44 fields: newCustomConfig,
45 statusCodeExpected
46 })
47}
48
49function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
50 const updateParams: CustomConfig = {
51 instance: {
52 name: 'PeerTube updated',
53 shortDescription: 'my short description',
54 description: 'my super description',
55 terms: 'my super terms',
56 codeOfConduct: 'my super coc',
57
58 creationReason: 'my super creation reason',
59 moderationInformation: 'my super moderation information',
60 administrator: 'Kuja',
61 maintenanceLifetime: 'forever',
62 businessModel: 'my super business model',
63 hardwareInformation: '2vCore 3GB RAM',
64
65 languages: [ 'en', 'es' ],
66 categories: [ 1, 2 ],
67
68 isNSFW: true,
69 defaultNSFWPolicy: 'blur',
70
71 defaultClientRoute: '/videos/recently-added',
72
73 customizations: {
74 javascript: 'alert("coucou")',
75 css: 'body { background-color: red; }'
76 }
77 },
78 theme: {
79 default: 'default'
80 },
81 services: {
82 twitter: {
83 username: '@MySuperUsername',
84 whitelisted: true
85 }
86 },
87 cache: {
88 previews: {
89 size: 2
90 },
91 captions: {
92 size: 3
93 },
94 torrents: {
95 size: 4
96 }
97 },
98 signup: {
99 enabled: false,
100 limit: 5,
101 requiresEmailVerification: false,
102 minimumAge: 16
103 },
104 admin: {
105 email: 'superadmin1@example.com'
106 },
107 contactForm: {
108 enabled: true
109 },
110 user: {
111 videoQuota: 5242881,
112 videoQuotaDaily: 318742
113 },
114 transcoding: {
115 enabled: true,
116 allowAdditionalExtensions: true,
117 allowAudioFiles: true,
118 threads: 1,
119 concurrency: 3,
120 profile: 'default',
121 resolutions: {
122 '0p': false,
123 '240p': false,
124 '360p': true,
125 '480p': true,
126 '720p': false,
127 '1080p': false,
128 '1440p': false,
129 '2160p': false
130 },
131 webtorrent: {
132 enabled: true
133 },
134 hls: {
135 enabled: false
136 }
137 },
138 live: {
139 enabled: true,
140 allowReplay: false,
141 maxDuration: -1,
142 maxInstanceLives: -1,
143 maxUserLives: 50,
144 transcoding: {
145 enabled: true,
146 threads: 4,
147 profile: 'default',
148 resolutions: {
149 '240p': true,
150 '360p': true,
151 '480p': true,
152 '720p': true,
153 '1080p': true,
154 '1440p': true,
155 '2160p': true
156 }
157 }
158 },
159 import: {
160 videos: {
161 concurrency: 3,
162 http: {
163 enabled: false
164 },
165 torrent: {
166 enabled: false
167 }
168 }
169 },
170 trending: {
171 videos: {
172 algorithms: {
173 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
174 default: 'hot'
175 }
176 }
177 },
178 autoBlacklist: {
179 videos: {
180 ofUsers: {
181 enabled: false
182 }
183 }
184 },
185 followers: {
186 instance: {
187 enabled: true,
188 manualApproval: false
189 }
190 },
191 followings: {
192 instance: {
193 autoFollowBack: {
194 enabled: false
195 },
196 autoFollowIndex: {
197 indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
198 enabled: false
199 }
200 }
201 },
202 broadcastMessage: {
203 enabled: true,
204 level: 'warning',
205 message: 'hello',
206 dismissable: true
207 },
208 search: {
209 remoteUri: {
210 users: true,
211 anonymous: true
212 },
213 searchIndex: {
214 enabled: true,
215 url: 'https://search.joinpeertube.org',
216 disableLocalSearch: true,
217 isDefaultSearch: true
218 }
219 }
220 }
221
222 merge(updateParams, newConfig)
223
224 return updateCustomConfig(url, token, updateParams)
225}
226
227function getCustomConfigResolutions (enabled: boolean) {
228 return {
229 '240p': enabled,
230 '360p': enabled,
231 '480p': enabled,
232 '720p': enabled,
233 '1080p': enabled,
234 '1440p': enabled,
235 '2160p': enabled
236 }
237}
238
239function deleteCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
240 const path = '/api/v1/config/custom'
241
242 return makeDeleteRequest({
243 url,
244 token,
245 path,
246 statusCodeExpected
247 })
248}
249
250// ---------------------------------------------------------------------------
251
252export {
253 getConfig,
254 getCustomConfig,
255 updateCustomConfig,
256 getAbout,
257 deleteCustomConfig,
258 updateCustomSubConfig,
259 getCustomConfigResolutions
260}
diff --git a/shared/extra-utils/server/contact-form-command.ts b/shared/extra-utils/server/contact-form-command.ts
new file mode 100644
index 000000000..0e8fd6d84
--- /dev/null
+++ b/shared/extra-utils/server/contact-form-command.ts
@@ -0,0 +1,31 @@
1import { HttpStatusCode } from '@shared/models'
2import { ContactForm } from '../../models/server'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class ContactFormCommand extends AbstractCommand {
6
7 send (options: OverrideCommandOptions & {
8 fromEmail: string
9 fromName: string
10 subject: string
11 body: string
12 }) {
13 const path = '/api/v1/server/contact'
14
15 const body: ContactForm = {
16 fromEmail: options.fromEmail,
17 fromName: options.fromName,
18 subject: options.subject,
19 body: options.body
20 }
21
22 return this.postBodyRequest({
23 ...options,
24
25 path,
26 fields: body,
27 implicitToken: false,
28 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
29 })
30 }
31}
diff --git a/shared/extra-utils/server/contact-form.ts b/shared/extra-utils/server/contact-form.ts
deleted file mode 100644
index 6c9232cc6..000000000
--- a/shared/extra-utils/server/contact-form.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as request from 'supertest'
2import { ContactForm } from '../../models/server'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function sendContactForm (options: {
6 url: string
7 fromEmail: string
8 fromName: string
9 subject: string
10 body: string
11 expectedStatus?: number
12}) {
13 const path = '/api/v1/server/contact'
14
15 const body: ContactForm = {
16 fromEmail: options.fromEmail,
17 fromName: options.fromName,
18 subject: options.subject,
19 body: options.body
20 }
21 return request(options.url)
22 .post(path)
23 .send(body)
24 .expect(options.expectedStatus || HttpStatusCode.NO_CONTENT_204)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 sendContactForm
31}
diff --git a/shared/extra-utils/server/debug-command.ts b/shared/extra-utils/server/debug-command.ts
new file mode 100644
index 000000000..3c5a785bb
--- /dev/null
+++ b/shared/extra-utils/server/debug-command.ts
@@ -0,0 +1,33 @@
1import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class DebugCommand extends AbstractCommand {
5
6 getDebug (options: OverrideCommandOptions = {}) {
7 const path = '/api/v1/server/debug'
8
9 return this.getRequestBody<Debug>({
10 ...options,
11
12 path,
13 implicitToken: true,
14 defaultExpectedStatus: HttpStatusCode.OK_200
15 })
16 }
17
18 sendCommand (options: OverrideCommandOptions & {
19 body: SendDebugCommand
20 }) {
21 const { body } = options
22 const path = '/api/v1/server/debug/run-command'
23
24 return this.postBodyRequest({
25 ...options,
26
27 path,
28 fields: body,
29 implicitToken: true,
30 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
31 })
32 }
33}
diff --git a/shared/extra-utils/server/debug.ts b/shared/extra-utils/server/debug.ts
deleted file mode 100644
index f196812b7..000000000
--- a/shared/extra-utils/server/debug.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import { makeGetRequest, makePostBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
3import { SendDebugCommand } from '@shared/models'
4
5function getDebug (url: string, token: string) {
6 const path = '/api/v1/server/debug'
7
8 return makeGetRequest({
9 url,
10 path,
11 token,
12 statusCodeExpected: HttpStatusCode.OK_200
13 })
14}
15
16function sendDebugCommand (url: string, token: string, body: SendDebugCommand) {
17 const path = '/api/v1/server/debug/run-command'
18
19 return makePostBodyRequest({
20 url,
21 path,
22 token,
23 fields: body,
24 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
25 })
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 getDebug,
32 sendDebugCommand
33}
diff --git a/shared/extra-utils/server/directories.ts b/shared/extra-utils/server/directories.ts
new file mode 100644
index 000000000..b6465cbf4
--- /dev/null
+++ b/shared/extra-utils/server/directories.ts
@@ -0,0 +1,34 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@server/helpers/core-utils'
7import { PeerTubeServer } from './server'
8
9async function checkTmpIsEmpty (server: PeerTubeServer) {
10 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
11
12 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
13 await checkDirectoryIsEmpty(server, 'tmp/hls')
14 }
15}
16
17async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
18 const testDirectory = 'test' + server.internalServerNumber
19
20 const directoryPath = join(root(), testDirectory, directory)
21
22 const directoryExists = await pathExists(directoryPath)
23 expect(directoryExists).to.be.true
24
25 const files = await readdir(directoryPath)
26 const filtered = files.filter(f => exceptions.includes(f) === false)
27
28 expect(filtered).to.have.lengthOf(0)
29}
30
31export {
32 checkTmpIsEmpty,
33 checkDirectoryIsEmpty
34}
diff --git a/shared/extra-utils/server/follows-command.ts b/shared/extra-utils/server/follows-command.ts
new file mode 100644
index 000000000..2b889cf66
--- /dev/null
+++ b/shared/extra-utils/server/follows-command.ts
@@ -0,0 +1,141 @@
1import { pick } from 'lodash'
2import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4import { PeerTubeServer } from './server'
5
6export class FollowsCommand extends AbstractCommand {
7
8 getFollowers (options: OverrideCommandOptions & {
9 start: number
10 count: number
11 sort: string
12 search?: string
13 actorType?: ActivityPubActorType
14 state?: FollowState
15 }) {
16 const path = '/api/v1/server/followers'
17
18 const toPick = [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]
19 const query = pick(options, toPick)
20
21 return this.getRequestBody<ResultList<ActorFollow>>({
22 ...options,
23
24 path,
25 query,
26 implicitToken: false,
27 defaultExpectedStatus: HttpStatusCode.OK_200
28 })
29 }
30
31 getFollowings (options: OverrideCommandOptions & {
32 start?: number
33 count?: number
34 sort?: string
35 search?: string
36 actorType?: ActivityPubActorType
37 state?: FollowState
38 } = {}) {
39 const path = '/api/v1/server/following'
40
41 const toPick = [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ]
42 const query = pick(options, toPick)
43
44 return this.getRequestBody<ResultList<ActorFollow>>({
45 ...options,
46
47 path,
48 query,
49 implicitToken: false,
50 defaultExpectedStatus: HttpStatusCode.OK_200
51 })
52 }
53
54 follow (options: OverrideCommandOptions & {
55 hosts?: string[]
56 handles?: string[]
57 }) {
58 const path = '/api/v1/server/following'
59
60 const fields: ServerFollowCreate = {}
61
62 if (options.hosts) {
63 fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
64 }
65
66 if (options.handles) {
67 fields.handles = options.handles
68 }
69
70 return this.postBodyRequest({
71 ...options,
72
73 path,
74 fields,
75 implicitToken: true,
76 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
77 })
78 }
79
80 async unfollow (options: OverrideCommandOptions & {
81 target: PeerTubeServer | string
82 }) {
83 const { target } = options
84
85 const handle = typeof target === 'string'
86 ? target
87 : target.host
88
89 const path = '/api/v1/server/following/' + handle
90
91 return this.deleteRequest({
92 ...options,
93
94 path,
95 implicitToken: true,
96 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
97 })
98 }
99
100 acceptFollower (options: OverrideCommandOptions & {
101 follower: string
102 }) {
103 const path = '/api/v1/server/followers/' + options.follower + '/accept'
104
105 return this.postBodyRequest({
106 ...options,
107
108 path,
109 implicitToken: true,
110 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
111 })
112 }
113
114 rejectFollower (options: OverrideCommandOptions & {
115 follower: string
116 }) {
117 const path = '/api/v1/server/followers/' + options.follower + '/reject'
118
119 return this.postBodyRequest({
120 ...options,
121
122 path,
123 implicitToken: true,
124 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
125 })
126 }
127
128 removeFollower (options: OverrideCommandOptions & {
129 follower: PeerTubeServer
130 }) {
131 const path = '/api/v1/server/followers/peertube@' + options.follower.host
132
133 return this.deleteRequest({
134 ...options,
135
136 path,
137 implicitToken: true,
138 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
139 })
140 }
141}
diff --git a/shared/extra-utils/server/follows.ts b/shared/extra-utils/server/follows.ts
index 6aae4a31d..698238f29 100644
--- a/shared/extra-utils/server/follows.ts
+++ b/shared/extra-utils/server/follows.ts
@@ -1,126 +1,10 @@
1import * as request from 'supertest'
2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs' 1import { waitJobs } from './jobs'
4import { makePostBodyRequest } from '../requests/requests' 2import { PeerTubeServer } from './server'
5import { ActivityPubActorType, FollowState } from '@shared/models'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7 3
8function getFollowersListPaginationAndSort (options: { 4async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
9 url: string
10 start: number
11 count: number
12 sort: string
13 search?: string
14 actorType?: ActivityPubActorType
15 state?: FollowState
16}) {
17 const { url, start, count, sort, search, state, actorType } = options
18 const path = '/api/v1/server/followers'
19
20 const query = {
21 start,
22 count,
23 sort,
24 search,
25 state,
26 actorType
27 }
28
29 return request(url)
30 .get(path)
31 .query(query)
32 .set('Accept', 'application/json')
33 .expect(HttpStatusCode.OK_200)
34 .expect('Content-Type', /json/)
35}
36
37function acceptFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
38 const path = '/api/v1/server/followers/' + follower + '/accept'
39
40 return makePostBodyRequest({
41 url,
42 token,
43 path,
44 statusCodeExpected
45 })
46}
47
48function rejectFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
49 const path = '/api/v1/server/followers/' + follower + '/reject'
50
51 return makePostBodyRequest({
52 url,
53 token,
54 path,
55 statusCodeExpected
56 })
57}
58
59function getFollowingListPaginationAndSort (options: {
60 url: string
61 start: number
62 count: number
63 sort: string
64 search?: string
65 actorType?: ActivityPubActorType
66 state?: FollowState
67}) {
68 const { url, start, count, sort, search, state, actorType } = options
69 const path = '/api/v1/server/following'
70
71 const query = {
72 start,
73 count,
74 sort,
75 search,
76 state,
77 actorType
78 }
79
80 return request(url)
81 .get(path)
82 .query(query)
83 .set('Accept', 'application/json')
84 .expect(HttpStatusCode.OK_200)
85 .expect('Content-Type', /json/)
86}
87
88function follow (follower: string, following: string[], accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
89 const path = '/api/v1/server/following'
90
91 const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
92 return request(follower)
93 .post(path)
94 .set('Accept', 'application/json')
95 .set('Authorization', 'Bearer ' + accessToken)
96 .send({ hosts: followingHosts })
97 .expect(expectedStatus)
98}
99
100async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
101 const path = '/api/v1/server/following/' + target.host
102
103 return request(url)
104 .delete(path)
105 .set('Accept', 'application/json')
106 .set('Authorization', 'Bearer ' + accessToken)
107 .expect(expectedStatus)
108}
109
110function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
111 const path = '/api/v1/server/followers/peertube@' + follower.host
112
113 return request(url)
114 .delete(path)
115 .set('Accept', 'application/json')
116 .set('Authorization', 'Bearer ' + accessToken)
117 .expect(expectedStatus)
118}
119
120async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
121 await Promise.all([ 5 await Promise.all([
122 follow(server1.url, [ server2.url ], server1.accessToken), 6 server1.follows.follow({ hosts: [ server2.url ] }),
123 follow(server2.url, [ server1.url ], server2.accessToken) 7 server2.follows.follow({ hosts: [ server1.url ] })
124 ]) 8 ])
125 9
126 // Wait request propagation 10 // Wait request propagation
@@ -132,12 +16,5 @@ async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
132// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
133 17
134export { 18export {
135 getFollowersListPaginationAndSort, 19 doubleFollow
136 getFollowingListPaginationAndSort,
137 unfollow,
138 removeFollower,
139 follow,
140 doubleFollow,
141 acceptFollower,
142 rejectFollower
143} 20}
diff --git a/shared/extra-utils/server/index.ts b/shared/extra-utils/server/index.ts
new file mode 100644
index 000000000..9055dfc57
--- /dev/null
+++ b/shared/extra-utils/server/index.ts
@@ -0,0 +1,15 @@
1export * from './config-command'
2export * from './contact-form-command'
3export * from './debug-command'
4export * from './directories'
5export * from './follows-command'
6export * from './follows'
7export * from './jobs'
8export * from './jobs-command'
9export * from './plugins-command'
10export * from './plugins'
11export * from './redundancy-command'
12export * from './server'
13export * from './servers-command'
14export * from './servers'
15export * from './stats-command'
diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts
new file mode 100644
index 000000000..09a299e5b
--- /dev/null
+++ b/shared/extra-utils/server/jobs-command.ts
@@ -0,0 +1,36 @@
1import { pick } from 'lodash'
2import { HttpStatusCode } from '@shared/models'
3import { Job, JobState, JobType, ResultList } from '../../models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class JobsCommand extends AbstractCommand {
7
8 getJobsList (options: OverrideCommandOptions & {
9 state?: JobState
10 jobType?: JobType
11 start?: number
12 count?: number
13 sort?: string
14 } = {}) {
15 const path = this.buildJobsUrl(options.state)
16
17 const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
18
19 return this.getRequestBody<ResultList<Job>>({
20 ...options,
21
22 path,
23 query,
24 implicitToken: true,
25 defaultExpectedStatus: HttpStatusCode.OK_200
26 })
27 }
28
29 private buildJobsUrl (state?: JobState) {
30 let path = '/api/v1/jobs'
31
32 if (state) path += '/' + state
33
34 return path
35 }
36}
diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts
index 763374e03..64a0353eb 100644
--- a/shared/extra-utils/server/jobs.ts
+++ b/shared/extra-utils/server/jobs.ts
@@ -1,66 +1,17 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { getDebug, makeGetRequest } from '../../../shared/extra-utils'
4import { Job, JobState, JobType, ServerDebug } from '../../models'
5import { wait } from '../miscs/miscs'
6import { ServerInfo } from './servers'
7 1
8function buildJobsUrl (state?: JobState) { 2import { JobState } from '../../models'
9 let path = '/api/v1/jobs' 3import { wait } from '../miscs'
4import { PeerTubeServer } from './server'
10 5
11 if (state) path += '/' + state 6async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer) {
12
13 return path
14}
15
16function getJobsList (url: string, accessToken: string, state?: JobState) {
17 const path = buildJobsUrl(state)
18
19 return request(url)
20 .get(path)
21 .set('Accept', 'application/json')
22 .set('Authorization', 'Bearer ' + accessToken)
23 .expect(HttpStatusCode.OK_200)
24 .expect('Content-Type', /json/)
25}
26
27function getJobsListPaginationAndSort (options: {
28 url: string
29 accessToken: string
30 start: number
31 count: number
32 sort: string
33 state?: JobState
34 jobType?: JobType
35}) {
36 const { url, accessToken, state, start, count, sort, jobType } = options
37 const path = buildJobsUrl(state)
38
39 const query = {
40 start,
41 count,
42 sort,
43 jobType
44 }
45
46 return makeGetRequest({
47 url,
48 path,
49 token: accessToken,
50 statusCodeExpected: HttpStatusCode.OK_200,
51 query
52 })
53}
54
55async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
56 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT 7 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
57 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) 8 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
58 : 250 9 : 250
59 10
60 let servers: ServerInfo[] 11 let servers: PeerTubeServer[]
61 12
62 if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ] 13 if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
63 else servers = serversArg as ServerInfo[] 14 else servers = serversArg as PeerTubeServer[]
64 15
65 const states: JobState[] = [ 'waiting', 'active', 'delayed' ] 16 const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
66 const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ] 17 const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ]
@@ -72,15 +23,13 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
72 // Check if each server has pending request 23 // Check if each server has pending request
73 for (const server of servers) { 24 for (const server of servers) {
74 for (const state of states) { 25 for (const state of states) {
75 const p = getJobsListPaginationAndSort({ 26 const p = server.jobs.getJobsList({
76 url: server.url, 27 state,
77 accessToken: server.accessToken,
78 state: state,
79 start: 0, 28 start: 0,
80 count: 10, 29 count: 10,
81 sort: '-createdAt' 30 sort: '-createdAt'
82 }).then(res => res.body.data) 31 }).then(body => body.data)
83 .then((jobs: Job[]) => jobs.filter(j => !repeatableJobs.includes(j.type))) 32 .then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
84 .then(jobs => { 33 .then(jobs => {
85 if (jobs.length !== 0) { 34 if (jobs.length !== 0) {
86 pendingRequests = true 35 pendingRequests = true
@@ -90,9 +39,8 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
90 tasks.push(p) 39 tasks.push(p)
91 } 40 }
92 41
93 const p = getDebug(server.url, server.accessToken) 42 const p = server.debug.getDebug()
94 .then(res => res.body) 43 .then(obj => {
95 .then((obj: ServerDebug) => {
96 if (obj.activityPubMessagesWaiting !== 0) { 44 if (obj.activityPubMessagesWaiting !== 0) {
97 pendingRequests = true 45 pendingRequests = true
98 } 46 }
@@ -123,7 +71,5 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
123// --------------------------------------------------------------------------- 71// ---------------------------------------------------------------------------
124 72
125export { 73export {
126 getJobsList, 74 waitJobs
127 waitJobs,
128 getJobsListPaginationAndSort
129} 75}
diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts
new file mode 100644
index 000000000..b944475a2
--- /dev/null
+++ b/shared/extra-utils/server/plugins-command.ts
@@ -0,0 +1,256 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readJSON, writeJSON } from 'fs-extra'
4import { join } from 'path'
5import { root } from '@server/helpers/core-utils'
6import {
7 HttpStatusCode,
8 PeerTubePlugin,
9 PeerTubePluginIndex,
10 PeertubePluginIndexList,
11 PluginPackageJson,
12 PluginTranslation,
13 PluginType,
14 PublicServerSetting,
15 RegisteredServerSettings,
16 ResultList
17} from '@shared/models'
18import { AbstractCommand, OverrideCommandOptions } from '../shared'
19
20export class PluginsCommand extends AbstractCommand {
21
22 static getPluginTestPath (suffix = '') {
23 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
24 }
25
26 list (options: OverrideCommandOptions & {
27 start?: number
28 count?: number
29 sort?: string
30 pluginType?: PluginType
31 uninstalled?: boolean
32 }) {
33 const { start, count, sort, pluginType, uninstalled } = options
34 const path = '/api/v1/plugins'
35
36 return this.getRequestBody<ResultList<PeerTubePlugin>>({
37 ...options,
38
39 path,
40 query: {
41 start,
42 count,
43 sort,
44 pluginType,
45 uninstalled
46 },
47 implicitToken: true,
48 defaultExpectedStatus: HttpStatusCode.OK_200
49 })
50 }
51
52 listAvailable (options: OverrideCommandOptions & {
53 start?: number
54 count?: number
55 sort?: string
56 pluginType?: PluginType
57 currentPeerTubeEngine?: string
58 search?: string
59 expectedStatus?: HttpStatusCode
60 }) {
61 const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options
62 const path = '/api/v1/plugins/available'
63
64 const query: PeertubePluginIndexList = {
65 start,
66 count,
67 sort,
68 pluginType,
69 currentPeerTubeEngine,
70 search
71 }
72
73 return this.getRequestBody<ResultList<PeerTubePluginIndex>>({
74 ...options,
75
76 path,
77 query,
78 implicitToken: true,
79 defaultExpectedStatus: HttpStatusCode.OK_200
80 })
81 }
82
83 get (options: OverrideCommandOptions & {
84 npmName: string
85 }) {
86 const path = '/api/v1/plugins/' + options.npmName
87
88 return this.getRequestBody<PeerTubePlugin>({
89 ...options,
90
91 path,
92 implicitToken: true,
93 defaultExpectedStatus: HttpStatusCode.OK_200
94 })
95 }
96
97 updateSettings (options: OverrideCommandOptions & {
98 npmName: string
99 settings: any
100 }) {
101 const { npmName, settings } = options
102 const path = '/api/v1/plugins/' + npmName + '/settings'
103
104 return this.putBodyRequest({
105 ...options,
106
107 path,
108 fields: { settings },
109 implicitToken: true,
110 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
111 })
112 }
113
114 getRegisteredSettings (options: OverrideCommandOptions & {
115 npmName: string
116 }) {
117 const path = '/api/v1/plugins/' + options.npmName + '/registered-settings'
118
119 return this.getRequestBody<RegisteredServerSettings>({
120 ...options,
121
122 path,
123 implicitToken: true,
124 defaultExpectedStatus: HttpStatusCode.OK_200
125 })
126 }
127
128 getPublicSettings (options: OverrideCommandOptions & {
129 npmName: string
130 }) {
131 const { npmName } = options
132 const path = '/api/v1/plugins/' + npmName + '/public-settings'
133
134 return this.getRequestBody<PublicServerSetting>({
135 ...options,
136
137 path,
138 implicitToken: false,
139 defaultExpectedStatus: HttpStatusCode.OK_200
140 })
141 }
142
143 getTranslations (options: OverrideCommandOptions & {
144 locale: string
145 }) {
146 const { locale } = options
147 const path = '/plugins/translations/' + locale + '.json'
148
149 return this.getRequestBody<PluginTranslation>({
150 ...options,
151
152 path,
153 implicitToken: false,
154 defaultExpectedStatus: HttpStatusCode.OK_200
155 })
156 }
157
158 install (options: OverrideCommandOptions & {
159 path?: string
160 npmName?: string
161 }) {
162 const { npmName, path } = options
163 const apiPath = '/api/v1/plugins/install'
164
165 return this.postBodyRequest({
166 ...options,
167
168 path: apiPath,
169 fields: { npmName, path },
170 implicitToken: true,
171 defaultExpectedStatus: HttpStatusCode.OK_200
172 })
173 }
174
175 update (options: OverrideCommandOptions & {
176 path?: string
177 npmName?: string
178 }) {
179 const { npmName, path } = options
180 const apiPath = '/api/v1/plugins/update'
181
182 return this.postBodyRequest({
183 ...options,
184
185 path: apiPath,
186 fields: { npmName, path },
187 implicitToken: true,
188 defaultExpectedStatus: HttpStatusCode.OK_200
189 })
190 }
191
192 uninstall (options: OverrideCommandOptions & {
193 npmName: string
194 }) {
195 const { npmName } = options
196 const apiPath = '/api/v1/plugins/uninstall'
197
198 return this.postBodyRequest({
199 ...options,
200
201 path: apiPath,
202 fields: { npmName },
203 implicitToken: true,
204 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
205 })
206 }
207
208 getCSS (options: OverrideCommandOptions = {}) {
209 const path = '/plugins/global.css'
210
211 return this.getRequestText({
212 ...options,
213
214 path,
215 implicitToken: false,
216 defaultExpectedStatus: HttpStatusCode.OK_200
217 })
218 }
219
220 getExternalAuth (options: OverrideCommandOptions & {
221 npmName: string
222 npmVersion: string
223 authName: string
224 query?: any
225 }) {
226 const { npmName, npmVersion, authName, query } = options
227
228 const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
229
230 return this.getRequest({
231 ...options,
232
233 path,
234 query,
235 implicitToken: false,
236 defaultExpectedStatus: HttpStatusCode.OK_200,
237 redirects: 0
238 })
239 }
240
241 updatePackageJSON (npmName: string, json: any) {
242 const path = this.getPackageJSONPath(npmName)
243
244 return writeJSON(path, json)
245 }
246
247 getPackageJSON (npmName: string): Promise<PluginPackageJson> {
248 const path = this.getPackageJSONPath(npmName)
249
250 return readJSON(path)
251 }
252
253 private getPackageJSONPath (npmName: string) {
254 return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json'))
255 }
256}
diff --git a/shared/extra-utils/server/plugins.ts b/shared/extra-utils/server/plugins.ts
index d53e5b382..0f5fabd5a 100644
--- a/shared/extra-utils/server/plugins.ts
+++ b/shared/extra-utils/server/plugins.ts
@@ -1,307 +1,18 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { readJSON, writeJSON } from 'fs-extra' 4import { PeerTubeServer } from '../server/server'
5import { join } from 'path'
6import { RegisteredServerSettings } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
8import { PeertubePluginIndexList } from '../../models/plugins/plugin-index/peertube-plugin-index-list.model'
9import { PluginType } from '../../models/plugins/plugin.type'
10import { buildServerDirectory, root } from '../miscs/miscs'
11import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
12import { ServerInfo } from './servers'
13 5
14function listPlugins (parameters: { 6async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
15 url: string 7 const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
16 accessToken: string
17 start?: number
18 count?: number
19 sort?: string
20 pluginType?: PluginType
21 uninstalled?: boolean
22 expectedStatus?: HttpStatusCode
23}) {
24 const { url, accessToken, start, count, sort, pluginType, uninstalled, expectedStatus = HttpStatusCode.OK_200 } = parameters
25 const path = '/api/v1/plugins'
26
27 return makeGetRequest({
28 url,
29 path,
30 token: accessToken,
31 query: {
32 start,
33 count,
34 sort,
35 pluginType,
36 uninstalled
37 },
38 statusCodeExpected: expectedStatus
39 })
40}
41
42function listAvailablePlugins (parameters: {
43 url: string
44 accessToken: string
45 start?: number
46 count?: number
47 sort?: string
48 pluginType?: PluginType
49 currentPeerTubeEngine?: string
50 search?: string
51 expectedStatus?: HttpStatusCode
52}) {
53 const {
54 url,
55 accessToken,
56 start,
57 count,
58 sort,
59 pluginType,
60 search,
61 currentPeerTubeEngine,
62 expectedStatus = HttpStatusCode.OK_200
63 } = parameters
64 const path = '/api/v1/plugins/available'
65
66 const query: PeertubePluginIndexList = {
67 start,
68 count,
69 sort,
70 pluginType,
71 currentPeerTubeEngine,
72 search
73 }
74
75 return makeGetRequest({
76 url,
77 path,
78 token: accessToken,
79 query,
80 statusCodeExpected: expectedStatus
81 })
82}
83
84function getPlugin (parameters: {
85 url: string
86 accessToken: string
87 npmName: string
88 expectedStatus?: HttpStatusCode
89}) {
90 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
91 const path = '/api/v1/plugins/' + npmName
92
93 return makeGetRequest({
94 url,
95 path,
96 token: accessToken,
97 statusCodeExpected: expectedStatus
98 })
99}
100
101function updatePluginSettings (parameters: {
102 url: string
103 accessToken: string
104 npmName: string
105 settings: any
106 expectedStatus?: HttpStatusCode
107}) {
108 const { url, accessToken, npmName, settings, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
109 const path = '/api/v1/plugins/' + npmName + '/settings'
110
111 return makePutBodyRequest({
112 url,
113 path,
114 token: accessToken,
115 fields: { settings },
116 statusCodeExpected: expectedStatus
117 })
118}
119
120function getPluginRegisteredSettings (parameters: {
121 url: string
122 accessToken: string
123 npmName: string
124 expectedStatus?: HttpStatusCode
125}) {
126 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
127 const path = '/api/v1/plugins/' + npmName + '/registered-settings'
128
129 return makeGetRequest({
130 url,
131 path,
132 token: accessToken,
133 statusCodeExpected: expectedStatus
134 })
135}
136
137async function testHelloWorldRegisteredSettings (server: ServerInfo) {
138 const res = await getPluginRegisteredSettings({
139 url: server.url,
140 accessToken: server.accessToken,
141 npmName: 'peertube-plugin-hello-world'
142 })
143
144 const registeredSettings = (res.body as RegisteredServerSettings).registeredSettings
145 8
9 const registeredSettings = body.registeredSettings
146 expect(registeredSettings).to.have.length.at.least(1) 10 expect(registeredSettings).to.have.length.at.least(1)
147 11
148 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name') 12 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
149 expect(adminNameSettings).to.not.be.undefined 13 expect(adminNameSettings).to.not.be.undefined
150} 14}
151 15
152function getPublicSettings (parameters: {
153 url: string
154 npmName: string
155 expectedStatus?: HttpStatusCode
156}) {
157 const { url, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
158 const path = '/api/v1/plugins/' + npmName + '/public-settings'
159
160 return makeGetRequest({
161 url,
162 path,
163 statusCodeExpected: expectedStatus
164 })
165}
166
167function getPluginTranslations (parameters: {
168 url: string
169 locale: string
170 expectedStatus?: HttpStatusCode
171}) {
172 const { url, locale, expectedStatus = HttpStatusCode.OK_200 } = parameters
173 const path = '/plugins/translations/' + locale + '.json'
174
175 return makeGetRequest({
176 url,
177 path,
178 statusCodeExpected: expectedStatus
179 })
180}
181
182function installPlugin (parameters: {
183 url: string
184 accessToken: string
185 path?: string
186 npmName?: string
187 expectedStatus?: HttpStatusCode
188}) {
189 const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
190 const apiPath = '/api/v1/plugins/install'
191
192 return makePostBodyRequest({
193 url,
194 path: apiPath,
195 token: accessToken,
196 fields: { npmName, path },
197 statusCodeExpected: expectedStatus
198 })
199}
200
201function updatePlugin (parameters: {
202 url: string
203 accessToken: string
204 path?: string
205 npmName?: string
206 expectedStatus?: HttpStatusCode
207}) {
208 const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
209 const apiPath = '/api/v1/plugins/update'
210
211 return makePostBodyRequest({
212 url,
213 path: apiPath,
214 token: accessToken,
215 fields: { npmName, path },
216 statusCodeExpected: expectedStatus
217 })
218}
219
220function uninstallPlugin (parameters: {
221 url: string
222 accessToken: string
223 npmName: string
224 expectedStatus?: HttpStatusCode
225}) {
226 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
227 const apiPath = '/api/v1/plugins/uninstall'
228
229 return makePostBodyRequest({
230 url,
231 path: apiPath,
232 token: accessToken,
233 fields: { npmName },
234 statusCodeExpected: expectedStatus
235 })
236}
237
238function getPluginsCSS (url: string) {
239 const path = '/plugins/global.css'
240
241 return makeGetRequest({
242 url,
243 path,
244 statusCodeExpected: HttpStatusCode.OK_200
245 })
246}
247
248function getPackageJSONPath (server: ServerInfo, npmName: string) {
249 return buildServerDirectory(server, join('plugins', 'node_modules', npmName, 'package.json'))
250}
251
252function updatePluginPackageJSON (server: ServerInfo, npmName: string, json: any) {
253 const path = getPackageJSONPath(server, npmName)
254
255 return writeJSON(path, json)
256}
257
258function getPluginPackageJSON (server: ServerInfo, npmName: string) {
259 const path = getPackageJSONPath(server, npmName)
260
261 return readJSON(path)
262}
263
264function getPluginTestPath (suffix = '') {
265 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
266}
267
268function getExternalAuth (options: {
269 url: string
270 npmName: string
271 npmVersion: string
272 authName: string
273 query?: any
274 statusCodeExpected?: HttpStatusCode
275}) {
276 const { url, npmName, npmVersion, authName, statusCodeExpected, query } = options
277
278 const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
279
280 return makeGetRequest({
281 url,
282 path,
283 query,
284 statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200,
285 redirects: 0
286 })
287}
288
289export { 16export {
290 listPlugins, 17 testHelloWorldRegisteredSettings
291 listAvailablePlugins,
292 installPlugin,
293 getPluginTranslations,
294 getPluginsCSS,
295 updatePlugin,
296 getPlugin,
297 uninstallPlugin,
298 testHelloWorldRegisteredSettings,
299 updatePluginSettings,
300 getPluginRegisteredSettings,
301 getPackageJSONPath,
302 updatePluginPackageJSON,
303 getPluginPackageJSON,
304 getPluginTestPath,
305 getPublicSettings,
306 getExternalAuth
307} 18}
diff --git a/shared/extra-utils/server/redundancy-command.ts b/shared/extra-utils/server/redundancy-command.ts
new file mode 100644
index 000000000..e7a8b3c29
--- /dev/null
+++ b/shared/extra-utils/server/redundancy-command.ts
@@ -0,0 +1,80 @@
1import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class RedundancyCommand extends AbstractCommand {
5
6 updateRedundancy (options: OverrideCommandOptions & {
7 host: string
8 redundancyAllowed: boolean
9 }) {
10 const { host, redundancyAllowed } = options
11 const path = '/api/v1/server/redundancy/' + host
12
13 return this.putBodyRequest({
14 ...options,
15
16 path,
17 fields: { redundancyAllowed },
18 implicitToken: true,
19 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
20 })
21 }
22
23 listVideos (options: OverrideCommandOptions & {
24 target: VideoRedundanciesTarget
25 start?: number
26 count?: number
27 sort?: string
28 }) {
29 const path = '/api/v1/server/redundancy/videos'
30
31 const { target, start, count, sort } = options
32
33 return this.getRequestBody<ResultList<VideoRedundancy>>({
34 ...options,
35
36 path,
37
38 query: {
39 start: start ?? 0,
40 count: count ?? 5,
41 sort: sort ?? 'name',
42 target
43 },
44
45 implicitToken: true,
46 defaultExpectedStatus: HttpStatusCode.OK_200
47 })
48 }
49
50 addVideo (options: OverrideCommandOptions & {
51 videoId: number
52 }) {
53 const path = '/api/v1/server/redundancy/videos'
54 const { videoId } = options
55
56 return this.postBodyRequest({
57 ...options,
58
59 path,
60 fields: { videoId },
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
63 })
64 }
65
66 removeVideo (options: OverrideCommandOptions & {
67 redundancyId: number
68 }) {
69 const { redundancyId } = options
70 const path = '/api/v1/server/redundancy/videos/' + redundancyId
71
72 return this.deleteRequest({
73 ...options,
74
75 path,
76 implicitToken: true,
77 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
78 })
79 }
80}
diff --git a/shared/extra-utils/server/redundancy.ts b/shared/extra-utils/server/redundancy.ts
deleted file mode 100644
index b83815a37..000000000
--- a/shared/extra-utils/server/redundancy.ts
+++ /dev/null
@@ -1,88 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2import { VideoRedundanciesTarget } from '@shared/models'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function updateRedundancy (
6 url: string,
7 accessToken: string,
8 host: string,
9 redundancyAllowed: boolean,
10 expectedStatus = HttpStatusCode.NO_CONTENT_204
11) {
12 const path = '/api/v1/server/redundancy/' + host
13
14 return makePutBodyRequest({
15 url,
16 path,
17 token: accessToken,
18 fields: { redundancyAllowed },
19 statusCodeExpected: expectedStatus
20 })
21}
22
23function listVideoRedundancies (options: {
24 url: string
25 accessToken: string
26 target: VideoRedundanciesTarget
27 start?: number
28 count?: number
29 sort?: string
30 statusCodeExpected?: HttpStatusCode
31}) {
32 const path = '/api/v1/server/redundancy/videos'
33
34 const { url, accessToken, target, statusCodeExpected, start, count, sort } = options
35
36 return makeGetRequest({
37 url,
38 token: accessToken,
39 path,
40 query: {
41 start: start ?? 0,
42 count: count ?? 5,
43 sort: sort ?? 'name',
44 target
45 },
46 statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200
47 })
48}
49
50function addVideoRedundancy (options: {
51 url: string
52 accessToken: string
53 videoId: number
54}) {
55 const path = '/api/v1/server/redundancy/videos'
56 const { url, accessToken, videoId } = options
57
58 return makePostBodyRequest({
59 url,
60 token: accessToken,
61 path,
62 fields: { videoId },
63 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
64 })
65}
66
67function removeVideoRedundancy (options: {
68 url: string
69 accessToken: string
70 redundancyId: number
71}) {
72 const { url, accessToken, redundancyId } = options
73 const path = '/api/v1/server/redundancy/videos/' + redundancyId
74
75 return makeDeleteRequest({
76 url,
77 token: accessToken,
78 path,
79 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
80 })
81}
82
83export {
84 updateRedundancy,
85 listVideoRedundancies,
86 addVideoRedundancy,
87 removeVideoRedundancy
88}
diff --git a/shared/extra-utils/server/server.ts b/shared/extra-utils/server/server.ts
new file mode 100644
index 000000000..b33bb9d1e
--- /dev/null
+++ b/shared/extra-utils/server/server.ts
@@ -0,0 +1,378 @@
1import { ChildProcess, fork } from 'child_process'
2import { copy } from 'fs-extra'
3import { join } from 'path'
4import { root } from '@server/helpers/core-utils'
5import { randomInt } from '../../core-utils/miscs/miscs'
6import { VideoChannel } from '../../models/videos'
7import { BulkCommand } from '../bulk'
8import { CLICommand } from '../cli'
9import { CustomPagesCommand } from '../custom-pages'
10import { FeedCommand } from '../feeds'
11import { LogsCommand } from '../logs'
12import { parallelTests, SQLCommand } from '../miscs'
13import { AbusesCommand } from '../moderation'
14import { OverviewsCommand } from '../overviews'
15import { SearchCommand } from '../search'
16import { SocketIOCommand } from '../socket'
17import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users'
18import {
19 BlacklistCommand,
20 CaptionsCommand,
21 ChangeOwnershipCommand,
22 ChannelsCommand,
23 HistoryCommand,
24 ImportsCommand,
25 LiveCommand,
26 PlaylistsCommand,
27 ServicesCommand,
28 StreamingPlaylistsCommand,
29 VideosCommand
30} from '../videos'
31import { CommentsCommand } from '../videos/comments-command'
32import { ConfigCommand } from './config-command'
33import { ContactFormCommand } from './contact-form-command'
34import { DebugCommand } from './debug-command'
35import { FollowsCommand } from './follows-command'
36import { JobsCommand } from './jobs-command'
37import { PluginsCommand } from './plugins-command'
38import { RedundancyCommand } from './redundancy-command'
39import { ServersCommand } from './servers-command'
40import { StatsCommand } from './stats-command'
41
42export type RunServerOptions = {
43 hideLogs?: boolean
44 execArgv?: string[]
45}
46
47export class PeerTubeServer {
48 app?: ChildProcess
49
50 url: string
51 host?: string
52 hostname?: string
53 port?: number
54
55 rtmpPort?: number
56
57 parallel?: boolean
58 internalServerNumber: number
59
60 serverNumber?: number
61 customConfigFile?: string
62
63 store?: {
64 client?: {
65 id?: string
66 secret?: string
67 }
68
69 user?: {
70 username: string
71 password: string
72 email?: string
73 }
74
75 channel?: VideoChannel
76
77 video?: {
78 id: number
79 uuid: string
80 shortUUID: string
81 name?: string
82 url?: string
83
84 account?: {
85 name: string
86 }
87
88 embedPath?: string
89 }
90
91 videos?: { id: number, uuid: string }[]
92 }
93
94 accessToken?: string
95 refreshToken?: string
96
97 bulk?: BulkCommand
98 cli?: CLICommand
99 customPage?: CustomPagesCommand
100 feed?: FeedCommand
101 logs?: LogsCommand
102 abuses?: AbusesCommand
103 overviews?: OverviewsCommand
104 search?: SearchCommand
105 contactForm?: ContactFormCommand
106 debug?: DebugCommand
107 follows?: FollowsCommand
108 jobs?: JobsCommand
109 plugins?: PluginsCommand
110 redundancy?: RedundancyCommand
111 stats?: StatsCommand
112 config?: ConfigCommand
113 socketIO?: SocketIOCommand
114 accounts?: AccountsCommand
115 blocklist?: BlocklistCommand
116 subscriptions?: SubscriptionsCommand
117 live?: LiveCommand
118 services?: ServicesCommand
119 blacklist?: BlacklistCommand
120 captions?: CaptionsCommand
121 changeOwnership?: ChangeOwnershipCommand
122 playlists?: PlaylistsCommand
123 history?: HistoryCommand
124 imports?: ImportsCommand
125 streamingPlaylists?: StreamingPlaylistsCommand
126 channels?: ChannelsCommand
127 comments?: CommentsCommand
128 sql?: SQLCommand
129 notifications?: NotificationsCommand
130 servers?: ServersCommand
131 login?: LoginCommand
132 users?: UsersCommand
133 videos?: VideosCommand
134
135 constructor (options: { serverNumber: number } | { url: string }) {
136 if ((options as any).url) {
137 this.setUrl((options as any).url)
138 } else {
139 this.setServerNumber((options as any).serverNumber)
140 }
141
142 this.store = {
143 client: {
144 id: null,
145 secret: null
146 },
147 user: {
148 username: null,
149 password: null
150 }
151 }
152
153 this.assignCommands()
154 }
155
156 setServerNumber (serverNumber: number) {
157 this.serverNumber = serverNumber
158
159 this.parallel = parallelTests()
160
161 this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
162 this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
163 this.port = 9000 + this.internalServerNumber
164
165 this.url = `http://localhost:${this.port}`
166 this.host = `localhost:${this.port}`
167 this.hostname = 'localhost'
168 }
169
170 setUrl (url: string) {
171 const parsed = new URL(url)
172
173 this.url = url
174 this.host = parsed.host
175 this.hostname = parsed.hostname
176 this.port = parseInt(parsed.port)
177 }
178
179 async flushAndRun (configOverride?: Object, args = [], options: RunServerOptions = {}) {
180 await ServersCommand.flushTests(this.internalServerNumber)
181
182 return this.run(configOverride, args, options)
183 }
184
185 async run (configOverrideArg?: any, args = [], options: RunServerOptions = {}) {
186 // These actions are async so we need to be sure that they have both been done
187 const serverRunString = {
188 'HTTP server listening': false
189 }
190 const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
191 serverRunString[key] = false
192
193 const regexps = {
194 client_id: 'Client id: (.+)',
195 client_secret: 'Client secret: (.+)',
196 user_username: 'Username: (.+)',
197 user_password: 'User password: (.+)'
198 }
199
200 await this.assignCustomConfigFile()
201
202 const configOverride = this.buildConfigOverride()
203
204 if (configOverrideArg !== undefined) {
205 Object.assign(configOverride, configOverrideArg)
206 }
207
208 // Share the environment
209 const env = Object.create(process.env)
210 env['NODE_ENV'] = 'test'
211 env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
212 env['NODE_CONFIG'] = JSON.stringify(configOverride)
213
214 const forkOptions = {
215 silent: true,
216 env,
217 detached: true,
218 execArgv: options.execArgv || []
219 }
220
221 return new Promise<void>(res => {
222 const self = this
223
224 this.app = fork(join(root(), 'dist', 'server.js'), args, forkOptions)
225 this.app.stdout.on('data', function onStdout (data) {
226 let dontContinue = false
227
228 // Capture things if we want to
229 for (const key of Object.keys(regexps)) {
230 const regexp = regexps[key]
231 const matches = data.toString().match(regexp)
232 if (matches !== null) {
233 if (key === 'client_id') self.store.client.id = matches[1]
234 else if (key === 'client_secret') self.store.client.secret = matches[1]
235 else if (key === 'user_username') self.store.user.username = matches[1]
236 else if (key === 'user_password') self.store.user.password = matches[1]
237 }
238 }
239
240 // Check if all required sentences are here
241 for (const key of Object.keys(serverRunString)) {
242 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
243 if (serverRunString[key] === false) dontContinue = true
244 }
245
246 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
247 if (dontContinue === true) return
248
249 if (options.hideLogs === false) {
250 console.log(data.toString())
251 } else {
252 self.app.stdout.removeListener('data', onStdout)
253 }
254
255 process.on('exit', () => {
256 try {
257 process.kill(self.app.pid)
258 } catch { /* empty */ }
259 })
260
261 res()
262 })
263 })
264 }
265
266 async kill () {
267 if (!this.app) return
268
269 await this.sql.cleanup()
270
271 process.kill(-this.app.pid)
272
273 this.app = null
274 }
275
276 private randomServer () {
277 const low = 10
278 const high = 10000
279
280 return randomInt(low, high)
281 }
282
283 private randomRTMP () {
284 const low = 1900
285 const high = 2100
286
287 return randomInt(low, high)
288 }
289
290 private async assignCustomConfigFile () {
291 if (this.internalServerNumber === this.serverNumber) return
292
293 const basePath = join(root(), 'config')
294
295 const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
296 await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
297
298 this.customConfigFile = tmpConfigFile
299 }
300
301 private buildConfigOverride () {
302 if (!this.parallel) return {}
303
304 return {
305 listen: {
306 port: this.port
307 },
308 webserver: {
309 port: this.port
310 },
311 database: {
312 suffix: '_test' + this.internalServerNumber
313 },
314 storage: {
315 tmp: `test${this.internalServerNumber}/tmp/`,
316 avatars: `test${this.internalServerNumber}/avatars/`,
317 videos: `test${this.internalServerNumber}/videos/`,
318 streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
319 redundancy: `test${this.internalServerNumber}/redundancy/`,
320 logs: `test${this.internalServerNumber}/logs/`,
321 previews: `test${this.internalServerNumber}/previews/`,
322 thumbnails: `test${this.internalServerNumber}/thumbnails/`,
323 torrents: `test${this.internalServerNumber}/torrents/`,
324 captions: `test${this.internalServerNumber}/captions/`,
325 cache: `test${this.internalServerNumber}/cache/`,
326 plugins: `test${this.internalServerNumber}/plugins/`
327 },
328 admin: {
329 email: `admin${this.internalServerNumber}@example.com`
330 },
331 live: {
332 rtmp: {
333 port: this.rtmpPort
334 }
335 }
336 }
337 }
338
339 private assignCommands () {
340 this.bulk = new BulkCommand(this)
341 this.cli = new CLICommand(this)
342 this.customPage = new CustomPagesCommand(this)
343 this.feed = new FeedCommand(this)
344 this.logs = new LogsCommand(this)
345 this.abuses = new AbusesCommand(this)
346 this.overviews = new OverviewsCommand(this)
347 this.search = new SearchCommand(this)
348 this.contactForm = new ContactFormCommand(this)
349 this.debug = new DebugCommand(this)
350 this.follows = new FollowsCommand(this)
351 this.jobs = new JobsCommand(this)
352 this.plugins = new PluginsCommand(this)
353 this.redundancy = new RedundancyCommand(this)
354 this.stats = new StatsCommand(this)
355 this.config = new ConfigCommand(this)
356 this.socketIO = new SocketIOCommand(this)
357 this.accounts = new AccountsCommand(this)
358 this.blocklist = new BlocklistCommand(this)
359 this.subscriptions = new SubscriptionsCommand(this)
360 this.live = new LiveCommand(this)
361 this.services = new ServicesCommand(this)
362 this.blacklist = new BlacklistCommand(this)
363 this.captions = new CaptionsCommand(this)
364 this.changeOwnership = new ChangeOwnershipCommand(this)
365 this.playlists = new PlaylistsCommand(this)
366 this.history = new HistoryCommand(this)
367 this.imports = new ImportsCommand(this)
368 this.streamingPlaylists = new StreamingPlaylistsCommand(this)
369 this.channels = new ChannelsCommand(this)
370 this.comments = new CommentsCommand(this)
371 this.sql = new SQLCommand(this)
372 this.notifications = new NotificationsCommand(this)
373 this.servers = new ServersCommand(this)
374 this.login = new LoginCommand(this)
375 this.users = new UsersCommand(this)
376 this.videos = new VideosCommand(this)
377 }
378}
diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts
new file mode 100644
index 000000000..107e2b4ad
--- /dev/null
+++ b/shared/extra-utils/server/servers-command.ts
@@ -0,0 +1,81 @@
1import { exec } from 'child_process'
2import { copy, ensureDir, readFile, remove } from 'fs-extra'
3import { join } from 'path'
4import { root } from '@server/helpers/core-utils'
5import { HttpStatusCode } from '@shared/models'
6import { getFileSize } from '@uploadx/core'
7import { isGithubCI, wait } from '../miscs'
8import { AbstractCommand, OverrideCommandOptions } from '../shared'
9
10export class ServersCommand extends AbstractCommand {
11
12 static flushTests (internalServerNumber: number) {
13 return new Promise<void>((res, rej) => {
14 const suffix = ` -- ${internalServerNumber}`
15
16 return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
17 if (err || stderr) return rej(err || new Error(stderr))
18
19 return res()
20 })
21 })
22 }
23
24 ping (options: OverrideCommandOptions = {}) {
25 return this.getRequestBody({
26 ...options,
27
28 path: '/api/v1/ping',
29 implicitToken: false,
30 defaultExpectedStatus: HttpStatusCode.OK_200
31 })
32 }
33
34 async cleanupTests () {
35 const p: Promise<any>[] = []
36
37 if (isGithubCI()) {
38 await ensureDir('artifacts')
39
40 const origin = this.buildDirectory('logs/peertube.log')
41 const destname = `peertube-${this.server.internalServerNumber}.log`
42 console.log('Saving logs %s.', destname)
43
44 await copy(origin, join('artifacts', destname))
45 }
46
47 if (this.server.parallel) {
48 p.push(ServersCommand.flushTests(this.server.internalServerNumber))
49 }
50
51 if (this.server.customConfigFile) {
52 p.push(remove(this.server.customConfigFile))
53 }
54
55 return p
56 }
57
58 async waitUntilLog (str: string, count = 1, strictCount = true) {
59 const logfile = this.server.servers.buildDirectory('logs/peertube.log')
60
61 while (true) {
62 const buf = await readFile(logfile)
63
64 const matches = buf.toString().match(new RegExp(str, 'g'))
65 if (matches && matches.length === count) return
66 if (matches && strictCount === false && matches.length >= count) return
67
68 await wait(1000)
69 }
70 }
71
72 buildDirectory (directory: string) {
73 return join(root(), 'test' + this.server.internalServerNumber, directory)
74 }
75
76 async getServerFileSize (subPath: string) {
77 const path = this.server.servers.buildDirectory(subPath)
78
79 return getFileSize(path)
80 }
81}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index 28e431e94..87d7e9449 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -1,384 +1,49 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1import { ensureDir } from 'fs-extra'
2import { isGithubCI } from '../miscs'
3import { PeerTubeServer, RunServerOptions } from './server'
2 4
3import { expect } from 'chai' 5async function createSingleServer (serverNumber: number, configOverride?: Object, args = [], options: RunServerOptions = {}) {
4import { ChildProcess, exec, fork } from 'child_process' 6 const server = new PeerTubeServer({ serverNumber })
5import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
6import { join } from 'path'
7import { randomInt } from '../../core-utils/miscs/miscs'
8import { VideoChannel } from '../../models/videos'
9import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
10import { makeGetRequest } from '../requests/requests'
11 7
12interface ServerInfo { 8 await server.flushAndRun(configOverride, args, options)
13 app: ChildProcess
14
15 url: string
16 host: string
17 hostname: string
18 port: number
19
20 rtmpPort: number
21
22 parallel: boolean
23 internalServerNumber: number
24 serverNumber: number
25
26 client: {
27 id: string
28 secret: string
29 }
30
31 user: {
32 username: string
33 password: string
34 email?: string
35 }
36
37 customConfigFile?: string
38
39 accessToken?: string
40 refreshToken?: string
41 videoChannel?: VideoChannel
42
43 video?: {
44 id: number
45 uuid: string
46 shortUUID: string
47 name?: string
48 url?: string
49
50 account?: {
51 name: string
52 }
53
54 embedPath?: string
55 }
56
57 remoteVideo?: {
58 id: number
59 uuid: string
60 }
61
62 videos?: { id: number, uuid: string }[]
63}
64
65function parallelTests () {
66 return process.env.MOCHA_PARALLEL === 'true'
67}
68
69function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
70 const apps = []
71 let i = 0
72
73 return new Promise<ServerInfo[]>(res => {
74 function anotherServerDone (serverNumber, app) {
75 apps[serverNumber - 1] = app
76 i++
77 if (i === totalServers) {
78 return res(apps)
79 }
80 }
81
82 for (let j = 1; j <= totalServers; j++) {
83 flushAndRunServer(j, configOverride).then(app => anotherServerDone(j, app))
84 }
85 })
86}
87
88function flushTests (serverNumber?: number) {
89 return new Promise<void>((res, rej) => {
90 const suffix = serverNumber ? ` -- ${serverNumber}` : ''
91
92 return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
93 if (err || stderr) return rej(err || new Error(stderr))
94
95 return res()
96 })
97 })
98}
99
100function randomServer () {
101 const low = 10
102 const high = 10000
103
104 return randomInt(low, high)
105}
106
107function randomRTMP () {
108 const low = 1900
109 const high = 2100
110
111 return randomInt(low, high)
112}
113
114type RunServerOptions = {
115 hideLogs?: boolean
116 execArgv?: string[]
117}
118
119async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = [], options: RunServerOptions = {}) {
120 const parallel = parallelTests()
121
122 const internalServerNumber = parallel ? randomServer() : serverNumber
123 const rtmpPort = parallel ? randomRTMP() : 1936
124 const port = 9000 + internalServerNumber
125
126 await flushTests(internalServerNumber)
127
128 const server: ServerInfo = {
129 app: null,
130 port,
131 internalServerNumber,
132 rtmpPort,
133 parallel,
134 serverNumber,
135 url: `http://localhost:${port}`,
136 host: `localhost:${port}`,
137 hostname: 'localhost',
138 client: {
139 id: null,
140 secret: null
141 },
142 user: {
143 username: null,
144 password: null
145 }
146 }
147
148 return runServer(server, configOverride, args, options)
149}
150
151async function runServer (server: ServerInfo, configOverrideArg?: any, args = [], options: RunServerOptions = {}) {
152 // These actions are async so we need to be sure that they have both been done
153 const serverRunString = {
154 'HTTP server listening': false
155 }
156 const key = 'Database peertube_test' + server.internalServerNumber + ' is ready'
157 serverRunString[key] = false
158
159 const regexps = {
160 client_id: 'Client id: (.+)',
161 client_secret: 'Client secret: (.+)',
162 user_username: 'Username: (.+)',
163 user_password: 'User password: (.+)'
164 }
165
166 if (server.internalServerNumber !== server.serverNumber) {
167 const basePath = join(root(), 'config')
168
169 const tmpConfigFile = join(basePath, `test-${server.internalServerNumber}.yaml`)
170 await copy(join(basePath, `test-${server.serverNumber}.yaml`), tmpConfigFile)
171
172 server.customConfigFile = tmpConfigFile
173 }
174
175 const configOverride: any = {}
176
177 if (server.parallel) {
178 Object.assign(configOverride, {
179 listen: {
180 port: server.port
181 },
182 webserver: {
183 port: server.port
184 },
185 database: {
186 suffix: '_test' + server.internalServerNumber
187 },
188 storage: {
189 tmp: `test${server.internalServerNumber}/tmp/`,
190 avatars: `test${server.internalServerNumber}/avatars/`,
191 videos: `test${server.internalServerNumber}/videos/`,
192 streaming_playlists: `test${server.internalServerNumber}/streaming-playlists/`,
193 redundancy: `test${server.internalServerNumber}/redundancy/`,
194 logs: `test${server.internalServerNumber}/logs/`,
195 previews: `test${server.internalServerNumber}/previews/`,
196 thumbnails: `test${server.internalServerNumber}/thumbnails/`,
197 torrents: `test${server.internalServerNumber}/torrents/`,
198 captions: `test${server.internalServerNumber}/captions/`,
199 cache: `test${server.internalServerNumber}/cache/`,
200 plugins: `test${server.internalServerNumber}/plugins/`
201 },
202 admin: {
203 email: `admin${server.internalServerNumber}@example.com`
204 },
205 live: {
206 rtmp: {
207 port: server.rtmpPort
208 }
209 }
210 })
211 }
212
213 if (configOverrideArg !== undefined) {
214 Object.assign(configOverride, configOverrideArg)
215 }
216
217 // Share the environment
218 const env = Object.create(process.env)
219 env['NODE_ENV'] = 'test'
220 env['NODE_APP_INSTANCE'] = server.internalServerNumber.toString()
221 env['NODE_CONFIG'] = JSON.stringify(configOverride)
222
223 const forkOptions = {
224 silent: true,
225 env,
226 detached: true,
227 execArgv: options.execArgv || []
228 }
229
230 return new Promise<ServerInfo>(res => {
231 server.app = fork(join(root(), 'dist', 'server.js'), args, forkOptions)
232 server.app.stdout.on('data', function onStdout (data) {
233 let dontContinue = false
234
235 // Capture things if we want to
236 for (const key of Object.keys(regexps)) {
237 const regexp = regexps[key]
238 const matches = data.toString().match(regexp)
239 if (matches !== null) {
240 if (key === 'client_id') server.client.id = matches[1]
241 else if (key === 'client_secret') server.client.secret = matches[1]
242 else if (key === 'user_username') server.user.username = matches[1]
243 else if (key === 'user_password') server.user.password = matches[1]
244 }
245 }
246
247 // Check if all required sentences are here
248 for (const key of Object.keys(serverRunString)) {
249 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
250 if (serverRunString[key] === false) dontContinue = true
251 }
252
253 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
254 if (dontContinue === true) return
255
256 if (options.hideLogs === false) {
257 console.log(data.toString())
258 } else {
259 server.app.stdout.removeListener('data', onStdout)
260 }
261
262 process.on('exit', () => {
263 try {
264 process.kill(server.app.pid)
265 } catch { /* empty */ }
266 })
267
268 res(server)
269 })
270 })
271}
272
273async function reRunServer (server: ServerInfo, configOverride?: any) {
274 const newServer = await runServer(server, configOverride)
275 server.app = newServer.app
276 9
277 return server 10 return server
278} 11}
279 12
280async function checkTmpIsEmpty (server: ServerInfo) { 13function createMultipleServers (totalServers: number, configOverride?: Object) {
281 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ]) 14 const serverPromises: Promise<PeerTubeServer>[] = []
282 15
283 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) { 16 for (let i = 1; i <= totalServers; i++) {
284 await checkDirectoryIsEmpty(server, 'tmp/hls') 17 serverPromises.push(createSingleServer(i, configOverride))
285 } 18 }
286}
287
288async function checkDirectoryIsEmpty (server: ServerInfo, directory: string, exceptions: string[] = []) {
289 const testDirectory = 'test' + server.internalServerNumber
290
291 const directoryPath = join(root(), testDirectory, directory)
292 19
293 const directoryExists = await pathExists(directoryPath) 20 return Promise.all(serverPromises)
294 expect(directoryExists).to.be.true
295
296 const files = await readdir(directoryPath)
297 const filtered = files.filter(f => exceptions.includes(f) === false)
298
299 expect(filtered).to.have.lengthOf(0)
300} 21}
301 22
302function killallServers (servers: ServerInfo[]) { 23async function killallServers (servers: PeerTubeServer[]) {
303 for (const server of servers) { 24 return Promise.all(servers.map(s => s.kill()))
304 if (!server.app) continue
305
306 process.kill(-server.app.pid)
307 server.app = null
308 }
309} 25}
310 26
311async function cleanupTests (servers: ServerInfo[]) { 27async function cleanupTests (servers: PeerTubeServer[]) {
312 killallServers(servers) 28 await killallServers(servers)
313 29
314 if (isGithubCI()) { 30 if (isGithubCI()) {
315 await ensureDir('artifacts') 31 await ensureDir('artifacts')
316 } 32 }
317 33
318 const p: Promise<any>[] = [] 34 let p: Promise<any>[] = []
319 for (const server of servers) { 35 for (const server of servers) {
320 if (isGithubCI()) { 36 p = p.concat(server.servers.cleanupTests())
321 const origin = await buildServerDirectory(server, 'logs/peertube.log')
322 const destname = `peertube-${server.internalServerNumber}.log`
323 console.log('Saving logs %s.', destname)
324
325 await copy(origin, join('artifacts', destname))
326 }
327
328 if (server.parallel) {
329 p.push(flushTests(server.internalServerNumber))
330 }
331
332 if (server.customConfigFile) {
333 p.push(remove(server.customConfigFile))
334 }
335 } 37 }
336 38
337 return Promise.all(p) 39 return Promise.all(p)
338} 40}
339 41
340async function waitUntilLog (server: ServerInfo, str: string, count = 1, strictCount = true) {
341 const logfile = buildServerDirectory(server, 'logs/peertube.log')
342
343 while (true) {
344 const buf = await readFile(logfile)
345
346 const matches = buf.toString().match(new RegExp(str, 'g'))
347 if (matches && matches.length === count) return
348 if (matches && strictCount === false && matches.length >= count) return
349
350 await wait(1000)
351 }
352}
353
354async function getServerFileSize (server: ServerInfo, subPath: string) {
355 const path = buildServerDirectory(server, subPath)
356
357 return getFileSize(path)
358}
359
360function makePingRequest (server: ServerInfo) {
361 return makeGetRequest({
362 url: server.url,
363 path: '/api/v1/ping',
364 statusCodeExpected: 200
365 })
366}
367
368// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
369 43
370export { 44export {
371 checkDirectoryIsEmpty, 45 createSingleServer,
372 checkTmpIsEmpty, 46 createMultipleServers,
373 getServerFileSize,
374 ServerInfo,
375 parallelTests,
376 cleanupTests, 47 cleanupTests,
377 flushAndRunMultipleServers, 48 killallServers
378 flushTests,
379 makePingRequest,
380 flushAndRunServer,
381 killallServers,
382 reRunServer,
383 waitUntilLog
384} 49}
diff --git a/shared/extra-utils/server/stats-command.ts b/shared/extra-utils/server/stats-command.ts
new file mode 100644
index 000000000..64a452306
--- /dev/null
+++ b/shared/extra-utils/server/stats-command.ts
@@ -0,0 +1,25 @@
1import { HttpStatusCode, ServerStats } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class StatsCommand extends AbstractCommand {
5
6 get (options: OverrideCommandOptions & {
7 useCache?: boolean // default false
8 } = {}) {
9 const { useCache = false } = options
10 const path = '/api/v1/server/stats'
11
12 const query = {
13 t: useCache ? undefined : new Date().getTime()
14 }
15
16 return this.getRequestBody<ServerStats>({
17 ...options,
18
19 path,
20 query,
21 implicitToken: false,
22 defaultExpectedStatus: HttpStatusCode.OK_200
23 })
24 }
25}
diff --git a/shared/extra-utils/server/stats.ts b/shared/extra-utils/server/stats.ts
deleted file mode 100644
index b9dae24e2..000000000
--- a/shared/extra-utils/server/stats.ts
+++ /dev/null
@@ -1,23 +0,0 @@
1import { makeGetRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getStats (url: string, useCache = false) {
5 const path = '/api/v1/server/stats'
6
7 const query = {
8 t: useCache ? undefined : new Date().getTime()
9 }
10
11 return makeGetRequest({
12 url,
13 path,
14 query,
15 statusCodeExpected: HttpStatusCode.OK_200
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 getStats
23}
diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts
new file mode 100644
index 000000000..021045e49
--- /dev/null
+++ b/shared/extra-utils/shared/abstract-command.ts
@@ -0,0 +1,199 @@
1import { isAbsolute, join } from 'path'
2import { root } from '../miscs/tests'
3import {
4 makeDeleteRequest,
5 makeGetRequest,
6 makePostBodyRequest,
7 makePutBodyRequest,
8 makeUploadRequest,
9 unwrapBody,
10 unwrapText
11} from '../requests/requests'
12import { PeerTubeServer } from '../server/server'
13
14export interface OverrideCommandOptions {
15 token?: string
16 expectedStatus?: number
17}
18
19interface InternalCommonCommandOptions extends OverrideCommandOptions {
20 // Default to server.url
21 url?: string
22
23 path: string
24 // If we automatically send the server token if the token is not provided
25 implicitToken: boolean
26 defaultExpectedStatus: number
27
28 // Common optional request parameters
29 contentType?: string
30 accept?: string
31 redirects?: number
32 range?: string
33 host?: string
34 headers?: { [ name: string ]: string }
35 requestType?: string
36 xForwardedFor?: string
37}
38
39interface InternalGetCommandOptions extends InternalCommonCommandOptions {
40 query?: { [ id: string ]: any }
41}
42
43abstract class AbstractCommand {
44
45 constructor (
46 protected server: PeerTubeServer
47 ) {
48
49 }
50
51 protected getRequestBody <T> (options: InternalGetCommandOptions) {
52 return unwrapBody<T>(this.getRequest(options))
53 }
54
55 protected getRequestText (options: InternalGetCommandOptions) {
56 return unwrapText(this.getRequest(options))
57 }
58
59 protected getRawRequest (options: Omit<InternalGetCommandOptions, 'path'>) {
60 const { url, range } = options
61 const { host, protocol, pathname } = new URL(url)
62
63 return this.getRequest({
64 ...options,
65
66 token: this.buildCommonRequestToken(options),
67 defaultExpectedStatus: this.buildExpectedStatus(options),
68
69 url: `${protocol}//${host}`,
70 path: pathname,
71 range
72 })
73 }
74
75 protected getRequest (options: InternalGetCommandOptions) {
76 const { query } = options
77
78 return makeGetRequest({
79 ...this.buildCommonRequestOptions(options),
80
81 query
82 })
83 }
84
85 protected deleteRequest (options: InternalCommonCommandOptions) {
86 return makeDeleteRequest(this.buildCommonRequestOptions(options))
87 }
88
89 protected putBodyRequest (options: InternalCommonCommandOptions & {
90 fields?: { [ fieldName: string ]: any }
91 }) {
92 const { fields } = options
93
94 return makePutBodyRequest({
95 ...this.buildCommonRequestOptions(options),
96
97 fields
98 })
99 }
100
101 protected postBodyRequest (options: InternalCommonCommandOptions & {
102 fields?: { [ fieldName: string ]: any }
103 }) {
104 const { fields } = options
105
106 return makePostBodyRequest({
107 ...this.buildCommonRequestOptions(options),
108
109 fields
110 })
111 }
112
113 protected postUploadRequest (options: InternalCommonCommandOptions & {
114 fields?: { [ fieldName: string ]: any }
115 attaches?: { [ fieldName: string ]: any }
116 }) {
117 const { fields, attaches } = options
118
119 return makeUploadRequest({
120 ...this.buildCommonRequestOptions(options),
121
122 method: 'POST',
123 fields,
124 attaches
125 })
126 }
127
128 protected putUploadRequest (options: InternalCommonCommandOptions & {
129 fields?: { [ fieldName: string ]: any }
130 attaches?: { [ fieldName: string ]: any }
131 }) {
132 const { fields, attaches } = options
133
134 return makeUploadRequest({
135 ...this.buildCommonRequestOptions(options),
136
137 method: 'PUT',
138 fields,
139 attaches
140 })
141 }
142
143 protected updateImageRequest (options: InternalCommonCommandOptions & {
144 fixture: string
145 fieldname: string
146 }) {
147 const filePath = isAbsolute(options.fixture)
148 ? options.fixture
149 : join(root(), 'server', 'tests', 'fixtures', options.fixture)
150
151 return this.postUploadRequest({
152 ...options,
153
154 fields: {},
155 attaches: { [options.fieldname]: filePath }
156 })
157 }
158
159 protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
160 const { url, path, redirects, contentType, accept, range, host, headers, requestType, xForwardedFor } = options
161
162 return {
163 url: url ?? this.server.url,
164 path,
165
166 token: this.buildCommonRequestToken(options),
167 expectedStatus: this.buildExpectedStatus(options),
168
169 redirects,
170 contentType,
171 range,
172 host,
173 accept,
174 headers,
175 type: requestType,
176 xForwardedFor
177 }
178 }
179
180 protected buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
181 const { token } = options
182
183 const fallbackToken = options.implicitToken
184 ? this.server.accessToken
185 : undefined
186
187 return token !== undefined ? token : fallbackToken
188 }
189
190 protected buildExpectedStatus (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
191 const { expectedStatus, defaultExpectedStatus } = options
192
193 return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus
194 }
195}
196
197export {
198 AbstractCommand
199}
diff --git a/shared/extra-utils/shared/index.ts b/shared/extra-utils/shared/index.ts
new file mode 100644
index 000000000..e807ab4f7
--- /dev/null
+++ b/shared/extra-utils/shared/index.ts
@@ -0,0 +1 @@
export * from './abstract-command'
diff --git a/shared/extra-utils/socket/index.ts b/shared/extra-utils/socket/index.ts
new file mode 100644
index 000000000..594329b2f
--- /dev/null
+++ b/shared/extra-utils/socket/index.ts
@@ -0,0 +1 @@
export * from './socket-io-command'
diff --git a/shared/extra-utils/socket/socket-io-command.ts b/shared/extra-utils/socket/socket-io-command.ts
new file mode 100644
index 000000000..c277ead28
--- /dev/null
+++ b/shared/extra-utils/socket/socket-io-command.ts
@@ -0,0 +1,15 @@
1import { io } from 'socket.io-client'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class SocketIOCommand extends AbstractCommand {
5
6 getUserNotificationSocket (options: OverrideCommandOptions = {}) {
7 return io(this.server.url + '/user-notifications', {
8 query: { accessToken: options.token ?? this.server.accessToken }
9 })
10 }
11
12 getLiveNotificationSocket () {
13 return io(this.server.url + '/live-videos')
14 }
15}
diff --git a/shared/extra-utils/socket/socket-io.ts b/shared/extra-utils/socket/socket-io.ts
deleted file mode 100644
index 4ca93f453..000000000
--- a/shared/extra-utils/socket/socket-io.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { io } from 'socket.io-client'
2
3function getUserNotificationSocket (serverUrl: string, accessToken: string) {
4 return io(serverUrl + '/user-notifications', {
5 query: { accessToken }
6 })
7}
8
9function getLiveNotificationSocket (serverUrl: string) {
10 return io(serverUrl + '/live-videos')
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 getUserNotificationSocket,
17 getLiveNotificationSocket
18}
diff --git a/shared/extra-utils/users/accounts-command.ts b/shared/extra-utils/users/accounts-command.ts
new file mode 100644
index 000000000..2f586104e
--- /dev/null
+++ b/shared/extra-utils/users/accounts-command.ts
@@ -0,0 +1,56 @@
1import { HttpStatusCode, ResultList } from '@shared/models'
2import { Account } from '../../models/actors'
3import { AccountVideoRate, VideoRateType } from '../../models/videos'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class AccountsCommand extends AbstractCommand {
7
8 list (options: OverrideCommandOptions & {
9 sort?: string // default -createdAt
10 } = {}) {
11 const { sort = '-createdAt' } = options
12 const path = '/api/v1/accounts'
13
14 return this.getRequestBody<ResultList<Account>>({
15 ...options,
16
17 path,
18 query: { sort },
19 implicitToken: false,
20 defaultExpectedStatus: HttpStatusCode.OK_200
21 })
22 }
23
24 get (options: OverrideCommandOptions & {
25 accountName: string
26 }) {
27 const path = '/api/v1/accounts/' + options.accountName
28
29 return this.getRequestBody<Account>({
30 ...options,
31
32 path,
33 implicitToken: false,
34 defaultExpectedStatus: HttpStatusCode.OK_200
35 })
36 }
37
38 listRatings (options: OverrideCommandOptions & {
39 accountName: string
40 rating?: VideoRateType
41 }) {
42 const { rating, accountName } = options
43 const path = '/api/v1/accounts/' + accountName + '/ratings'
44
45 const query = { rating }
46
47 return this.getRequestBody<ResultList<AccountVideoRate>>({
48 ...options,
49
50 path,
51 query,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56}
diff --git a/shared/extra-utils/users/accounts.ts b/shared/extra-utils/users/accounts.ts
deleted file mode 100644
index 4ea7f1402..000000000
--- a/shared/extra-utils/users/accounts.ts
+++ /dev/null
@@ -1,87 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as request from 'supertest'
4import { expect } from 'chai'
5import { existsSync, readdir } from 'fs-extra'
6import { join } from 'path'
7import { Account } from '../../models/actors'
8import { root } from '../miscs/miscs'
9import { makeGetRequest } from '../requests/requests'
10import { VideoRateType } from '../../models/videos'
11import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
12
13function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = HttpStatusCode.OK_200) {
14 const path = '/api/v1/accounts'
15
16 return makeGetRequest({
17 url,
18 query: { sort },
19 path,
20 statusCodeExpected
21 })
22}
23
24function getAccount (url: string, accountName: string, statusCodeExpected = HttpStatusCode.OK_200) {
25 const path = '/api/v1/accounts/' + accountName
26
27 return makeGetRequest({
28 url,
29 path,
30 statusCodeExpected
31 })
32}
33
34async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) {
35 const res = await getAccountsList(url)
36 const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain)
37
38 const message = `${nameWithDomain} on ${url}`
39 expect(account.followersCount).to.equal(followersCount, message)
40 expect(account.followingCount).to.equal(followingCount, message)
41}
42
43async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
44 const testDirectory = 'test' + serverNumber
45
46 for (const directory of [ 'avatars' ]) {
47 const directoryPath = join(root(), testDirectory, directory)
48
49 const directoryExists = existsSync(directoryPath)
50 expect(directoryExists).to.be.true
51
52 const files = await readdir(directoryPath)
53 for (const file of files) {
54 expect(file).to.not.contain(filename)
55 }
56 }
57}
58
59function getAccountRatings (
60 url: string,
61 accountName: string,
62 accessToken: string,
63 rating?: VideoRateType,
64 statusCodeExpected = HttpStatusCode.OK_200
65) {
66 const path = '/api/v1/accounts/' + accountName + '/ratings'
67
68 const query = rating ? { rating } : {}
69
70 return request(url)
71 .get(path)
72 .query(query)
73 .set('Accept', 'application/json')
74 .set('Authorization', 'Bearer ' + accessToken)
75 .expect(statusCodeExpected)
76 .expect('Content-Type', /json/)
77}
78
79// ---------------------------------------------------------------------------
80
81export {
82 getAccount,
83 expectAccountFollows,
84 getAccountsList,
85 checkActorFilesWereRemoved,
86 getAccountRatings
87}
diff --git a/shared/extra-utils/users/actors.ts b/shared/extra-utils/users/actors.ts
new file mode 100644
index 000000000..cfcc7d0a7
--- /dev/null
+++ b/shared/extra-utils/users/actors.ts
@@ -0,0 +1,73 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@server/helpers/core-utils'
7import { Account, VideoChannel } from '@shared/models'
8import { PeerTubeServer } from '../server'
9
10async function expectChannelsFollows (options: {
11 server: PeerTubeServer
12 handle: string
13 followers: number
14 following: number
15}) {
16 const { server } = options
17 const { data } = await server.channels.list()
18
19 return expectActorFollow({ ...options, data })
20}
21
22async function expectAccountFollows (options: {
23 server: PeerTubeServer
24 handle: string
25 followers: number
26 following: number
27}) {
28 const { server } = options
29 const { data } = await server.accounts.list()
30
31 return expectActorFollow({ ...options, data })
32}
33
34async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
35 const testDirectory = 'test' + serverNumber
36
37 for (const directory of [ 'avatars' ]) {
38 const directoryPath = join(root(), testDirectory, directory)
39
40 const directoryExists = await pathExists(directoryPath)
41 expect(directoryExists).to.be.true
42
43 const files = await readdir(directoryPath)
44 for (const file of files) {
45 expect(file).to.not.contain(filename)
46 }
47 }
48}
49
50export {
51 expectAccountFollows,
52 expectChannelsFollows,
53 checkActorFilesWereRemoved
54}
55
56// ---------------------------------------------------------------------------
57
58function expectActorFollow (options: {
59 server: PeerTubeServer
60 data: (Account | VideoChannel)[]
61 handle: string
62 followers: number
63 following: number
64}) {
65 const { server, data, handle, followers, following } = options
66
67 const actor = data.find(a => a.name + '@' + a.host === handle)
68 const message = `${handle} on ${server.url}`
69
70 expect(actor, message).to.exist
71 expect(actor.followersCount).to.equal(followers, message)
72 expect(actor.followingCount).to.equal(following, message)
73}
diff --git a/shared/extra-utils/users/blocklist-command.ts b/shared/extra-utils/users/blocklist-command.ts
new file mode 100644
index 000000000..14491a1ae
--- /dev/null
+++ b/shared/extra-utils/users/blocklist-command.ts
@@ -0,0 +1,139 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { AccountBlock, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6type ListBlocklistOptions = OverrideCommandOptions & {
7 start: number
8 count: number
9 sort: string // default -createdAt
10}
11
12export class BlocklistCommand extends AbstractCommand {
13
14 listMyAccountBlocklist (options: ListBlocklistOptions) {
15 const path = '/api/v1/users/me/blocklist/accounts'
16
17 return this.listBlocklist<AccountBlock>(options, path)
18 }
19
20 listMyServerBlocklist (options: ListBlocklistOptions) {
21 const path = '/api/v1/users/me/blocklist/servers'
22
23 return this.listBlocklist<ServerBlock>(options, path)
24 }
25
26 listServerAccountBlocklist (options: ListBlocklistOptions) {
27 const path = '/api/v1/server/blocklist/accounts'
28
29 return this.listBlocklist<AccountBlock>(options, path)
30 }
31
32 listServerServerBlocklist (options: ListBlocklistOptions) {
33 const path = '/api/v1/server/blocklist/servers'
34
35 return this.listBlocklist<ServerBlock>(options, path)
36 }
37
38 // ---------------------------------------------------------------------------
39
40 addToMyBlocklist (options: OverrideCommandOptions & {
41 account?: string
42 server?: string
43 }) {
44 const { account, server } = options
45
46 const path = account
47 ? '/api/v1/users/me/blocklist/accounts'
48 : '/api/v1/users/me/blocklist/servers'
49
50 return this.postBodyRequest({
51 ...options,
52
53 path,
54 fields: {
55 accountName: account,
56 host: server
57 },
58 implicitToken: true,
59 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
60 })
61 }
62
63 addToServerBlocklist (options: OverrideCommandOptions & {
64 account?: string
65 server?: string
66 }) {
67 const { account, server } = options
68
69 const path = account
70 ? '/api/v1/server/blocklist/accounts'
71 : '/api/v1/server/blocklist/servers'
72
73 return this.postBodyRequest({
74 ...options,
75
76 path,
77 fields: {
78 accountName: account,
79 host: server
80 },
81 implicitToken: true,
82 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
83 })
84 }
85
86 // ---------------------------------------------------------------------------
87
88 removeFromMyBlocklist (options: OverrideCommandOptions & {
89 account?: string
90 server?: string
91 }) {
92 const { account, server } = options
93
94 const path = account
95 ? '/api/v1/users/me/blocklist/accounts/' + account
96 : '/api/v1/users/me/blocklist/servers/' + server
97
98 return this.deleteRequest({
99 ...options,
100
101 path,
102 implicitToken: true,
103 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
104 })
105 }
106
107 removeFromServerBlocklist (options: OverrideCommandOptions & {
108 account?: string
109 server?: string
110 }) {
111 const { account, server } = options
112
113 const path = account
114 ? '/api/v1/server/blocklist/accounts/' + account
115 : '/api/v1/server/blocklist/servers/' + server
116
117 return this.deleteRequest({
118 ...options,
119
120 path,
121 implicitToken: true,
122 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
123 })
124 }
125
126 private listBlocklist <T> (options: ListBlocklistOptions, path: string) {
127 const { start, count, sort = '-createdAt' } = options
128
129 return this.getRequestBody<ResultList<T>>({
130 ...options,
131
132 path,
133 query: { start, count, sort },
134 implicitToken: true,
135 defaultExpectedStatus: HttpStatusCode.OK_200
136 })
137 }
138
139}
diff --git a/shared/extra-utils/users/blocklist.ts b/shared/extra-utils/users/blocklist.ts
deleted file mode 100644
index bdf7ee58a..000000000
--- a/shared/extra-utils/users/blocklist.ts
+++ /dev/null
@@ -1,238 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function getAccountBlocklistByAccount (
7 url: string,
8 token: string,
9 start: number,
10 count: number,
11 sort = '-createdAt',
12 statusCodeExpected = HttpStatusCode.OK_200
13) {
14 const path = '/api/v1/users/me/blocklist/accounts'
15
16 return makeGetRequest({
17 url,
18 token,
19 query: { start, count, sort },
20 path,
21 statusCodeExpected
22 })
23}
24
25function addAccountToAccountBlocklist (
26 url: string,
27 token: string,
28 accountToBlock: string,
29 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
30) {
31 const path = '/api/v1/users/me/blocklist/accounts'
32
33 return makePostBodyRequest({
34 url,
35 path,
36 token,
37 fields: {
38 accountName: accountToBlock
39 },
40 statusCodeExpected
41 })
42}
43
44function removeAccountFromAccountBlocklist (
45 url: string,
46 token: string,
47 accountToUnblock: string,
48 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
49) {
50 const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
51
52 return makeDeleteRequest({
53 url,
54 path,
55 token,
56 statusCodeExpected
57 })
58}
59
60function getServerBlocklistByAccount (
61 url: string,
62 token: string,
63 start: number,
64 count: number,
65 sort = '-createdAt',
66 statusCodeExpected = HttpStatusCode.OK_200
67) {
68 const path = '/api/v1/users/me/blocklist/servers'
69
70 return makeGetRequest({
71 url,
72 token,
73 query: { start, count, sort },
74 path,
75 statusCodeExpected
76 })
77}
78
79function addServerToAccountBlocklist (
80 url: string,
81 token: string,
82 serverToBlock: string,
83 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
84) {
85 const path = '/api/v1/users/me/blocklist/servers'
86
87 return makePostBodyRequest({
88 url,
89 path,
90 token,
91 fields: {
92 host: serverToBlock
93 },
94 statusCodeExpected
95 })
96}
97
98function removeServerFromAccountBlocklist (
99 url: string,
100 token: string,
101 serverToBlock: string,
102 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
103) {
104 const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
105
106 return makeDeleteRequest({
107 url,
108 path,
109 token,
110 statusCodeExpected
111 })
112}
113
114function getAccountBlocklistByServer (
115 url: string,
116 token: string,
117 start: number,
118 count: number,
119 sort = '-createdAt',
120 statusCodeExpected = HttpStatusCode.OK_200
121) {
122 const path = '/api/v1/server/blocklist/accounts'
123
124 return makeGetRequest({
125 url,
126 token,
127 query: { start, count, sort },
128 path,
129 statusCodeExpected
130 })
131}
132
133function addAccountToServerBlocklist (
134 url: string,
135 token: string,
136 accountToBlock: string,
137 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
138) {
139 const path = '/api/v1/server/blocklist/accounts'
140
141 return makePostBodyRequest({
142 url,
143 path,
144 token,
145 fields: {
146 accountName: accountToBlock
147 },
148 statusCodeExpected
149 })
150}
151
152function removeAccountFromServerBlocklist (
153 url: string,
154 token: string,
155 accountToUnblock: string,
156 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
157) {
158 const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock
159
160 return makeDeleteRequest({
161 url,
162 path,
163 token,
164 statusCodeExpected
165 })
166}
167
168function getServerBlocklistByServer (
169 url: string,
170 token: string,
171 start: number,
172 count: number,
173 sort = '-createdAt',
174 statusCodeExpected = HttpStatusCode.OK_200
175) {
176 const path = '/api/v1/server/blocklist/servers'
177
178 return makeGetRequest({
179 url,
180 token,
181 query: { start, count, sort },
182 path,
183 statusCodeExpected
184 })
185}
186
187function addServerToServerBlocklist (
188 url: string,
189 token: string,
190 serverToBlock: string,
191 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
192) {
193 const path = '/api/v1/server/blocklist/servers'
194
195 return makePostBodyRequest({
196 url,
197 path,
198 token,
199 fields: {
200 host: serverToBlock
201 },
202 statusCodeExpected
203 })
204}
205
206function removeServerFromServerBlocklist (
207 url: string,
208 token: string,
209 serverToBlock: string,
210 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
211) {
212 const path = '/api/v1/server/blocklist/servers/' + serverToBlock
213
214 return makeDeleteRequest({
215 url,
216 path,
217 token,
218 statusCodeExpected
219 })
220}
221
222// ---------------------------------------------------------------------------
223
224export {
225 getAccountBlocklistByAccount,
226 addAccountToAccountBlocklist,
227 removeAccountFromAccountBlocklist,
228 getServerBlocklistByAccount,
229 addServerToAccountBlocklist,
230 removeServerFromAccountBlocklist,
231
232 getAccountBlocklistByServer,
233 addAccountToServerBlocklist,
234 removeAccountFromServerBlocklist,
235 getServerBlocklistByServer,
236 addServerToServerBlocklist,
237 removeServerFromServerBlocklist
238}
diff --git a/shared/extra-utils/users/index.ts b/shared/extra-utils/users/index.ts
new file mode 100644
index 000000000..460a06f70
--- /dev/null
+++ b/shared/extra-utils/users/index.ts
@@ -0,0 +1,9 @@
1export * from './accounts-command'
2export * from './actors'
3export * from './blocklist-command'
4export * from './login'
5export * from './login-command'
6export * from './notifications'
7export * from './notifications-command'
8export * from './subscriptions-command'
9export * from './users-command'
diff --git a/shared/extra-utils/users/login-command.ts b/shared/extra-utils/users/login-command.ts
new file mode 100644
index 000000000..143f72a59
--- /dev/null
+++ b/shared/extra-utils/users/login-command.ts
@@ -0,0 +1,132 @@
1import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models'
2import { unwrapBody } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class LoginCommand extends AbstractCommand {
6
7 login (options: OverrideCommandOptions & {
8 client?: { id?: string, secret?: string }
9 user?: { username: string, password?: string }
10 } = {}) {
11 const { client = this.server.store.client, user = this.server.store.user } = options
12 const path = '/api/v1/users/token'
13
14 const body = {
15 client_id: client.id,
16 client_secret: client.secret,
17 username: user.username,
18 password: user.password ?? 'password',
19 response_type: 'code',
20 grant_type: 'password',
21 scope: 'upload'
22 }
23
24 return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({
25 ...options,
26
27 path,
28 requestType: 'form',
29 fields: body,
30 implicitToken: false,
31 defaultExpectedStatus: HttpStatusCode.OK_200
32 }))
33 }
34
35 getAccessToken (arg1?: { username: string, password?: string }): Promise<string>
36 getAccessToken (arg1: string, password?: string): Promise<string>
37 async getAccessToken (arg1?: { username: string, password?: string } | string, password?: string) {
38 let user: { username: string, password?: string }
39
40 if (!arg1) user = this.server.store.user
41 else if (typeof arg1 === 'object') user = arg1
42 else user = { username: arg1, password }
43
44 try {
45 const body = await this.login({ user })
46
47 return body.access_token
48 } catch (err) {
49 throw new Error(`Cannot authenticate. Please check your username/password. (${err})`)
50 }
51 }
52
53 loginUsingExternalToken (options: OverrideCommandOptions & {
54 username: string
55 externalAuthToken: string
56 }) {
57 const { username, externalAuthToken } = options
58 const path = '/api/v1/users/token'
59
60 const body = {
61 client_id: this.server.store.client.id,
62 client_secret: this.server.store.client.secret,
63 username: username,
64 response_type: 'code',
65 grant_type: 'password',
66 scope: 'upload',
67 externalAuthToken
68 }
69
70 return this.postBodyRequest({
71 ...options,
72
73 path,
74 requestType: 'form',
75 fields: body,
76 implicitToken: false,
77 defaultExpectedStatus: HttpStatusCode.OK_200
78 })
79 }
80
81 logout (options: OverrideCommandOptions & {
82 token: string
83 }) {
84 const path = '/api/v1/users/revoke-token'
85
86 return unwrapBody<{ redirectUrl: string }>(this.postBodyRequest({
87 ...options,
88
89 path,
90 requestType: 'form',
91 implicitToken: false,
92 defaultExpectedStatus: HttpStatusCode.OK_200
93 }))
94 }
95
96 refreshToken (options: OverrideCommandOptions & {
97 refreshToken: string
98 }) {
99 const path = '/api/v1/users/token'
100
101 const body = {
102 client_id: this.server.store.client.id,
103 client_secret: this.server.store.client.secret,
104 refresh_token: options.refreshToken,
105 response_type: 'code',
106 grant_type: 'refresh_token'
107 }
108
109 return this.postBodyRequest({
110 ...options,
111
112 path,
113 requestType: 'form',
114 fields: body,
115 implicitToken: false,
116 defaultExpectedStatus: HttpStatusCode.OK_200
117 })
118 }
119
120 getClient (options: OverrideCommandOptions = {}) {
121 const path = '/api/v1/oauth-clients/local'
122
123 return this.getRequestBody<{ client_id: string, client_secret: string }>({
124 ...options,
125
126 path,
127 host: this.server.host,
128 implicitToken: false,
129 defaultExpectedStatus: HttpStatusCode.OK_200
130 })
131 }
132}
diff --git a/shared/extra-utils/users/login.ts b/shared/extra-utils/users/login.ts
index 39e1a2747..f1df027d3 100644
--- a/shared/extra-utils/users/login.ts
+++ b/shared/extra-utils/users/login.ts
@@ -1,133 +1,19 @@
1import * as request from 'supertest' 1import { PeerTubeServer } from '../server/server'
2 2
3import { ServerInfo } from '../server/servers' 3function setAccessTokensToServers (servers: PeerTubeServer[]) {
4import { getClient } from '../server/clients'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7type Client = { id: string, secret: string }
8type User = { username: string, password: string }
9type Server = { url: string, client: Client, user: User }
10
11function login (url: string, client: Client, user: User, expectedStatus = HttpStatusCode.OK_200) {
12 const path = '/api/v1/users/token'
13
14 const body = {
15 client_id: client.id,
16 client_secret: client.secret,
17 username: user.username,
18 password: user.password,
19 response_type: 'code',
20 grant_type: 'password',
21 scope: 'upload'
22 }
23
24 return request(url)
25 .post(path)
26 .type('form')
27 .send(body)
28 .expect(expectedStatus)
29}
30
31function logout (url: string, token: string, expectedStatus = HttpStatusCode.OK_200) {
32 const path = '/api/v1/users/revoke-token'
33
34 return request(url)
35 .post(path)
36 .set('Authorization', 'Bearer ' + token)
37 .type('form')
38 .expect(expectedStatus)
39}
40
41async function serverLogin (server: Server) {
42 const res = await login(server.url, server.client, server.user, HttpStatusCode.OK_200)
43
44 return res.body.access_token as string
45}
46
47function refreshToken (server: ServerInfo, refreshToken: string, expectedStatus = HttpStatusCode.OK_200) {
48 const path = '/api/v1/users/token'
49
50 const body = {
51 client_id: server.client.id,
52 client_secret: server.client.secret,
53 refresh_token: refreshToken,
54 response_type: 'code',
55 grant_type: 'refresh_token'
56 }
57
58 return request(server.url)
59 .post(path)
60 .type('form')
61 .send(body)
62 .expect(expectedStatus)
63}
64
65async function userLogin (server: Server, user: User, expectedStatus = HttpStatusCode.OK_200) {
66 const res = await login(server.url, server.client, user, expectedStatus)
67
68 return res.body.access_token as string
69}
70
71async function getAccessToken (url: string, username: string, password: string) {
72 const resClient = await getClient(url)
73 const client = {
74 id: resClient.body.client_id,
75 secret: resClient.body.client_secret
76 }
77
78 const user = { username, password }
79
80 try {
81 const res = await login(url, client, user)
82 return res.body.access_token
83 } catch (err) {
84 throw new Error('Cannot authenticate. Please check your username/password.')
85 }
86}
87
88function setAccessTokensToServers (servers: ServerInfo[]) {
89 const tasks: Promise<any>[] = [] 4 const tasks: Promise<any>[] = []
90 5
91 for (const server of servers) { 6 for (const server of servers) {
92 const p = serverLogin(server).then(t => { server.accessToken = t }) 7 const p = server.login.getAccessToken()
8 .then(t => { server.accessToken = t })
93 tasks.push(p) 9 tasks.push(p)
94 } 10 }
95 11
96 return Promise.all(tasks) 12 return Promise.all(tasks)
97} 13}
98 14
99function loginUsingExternalToken (server: Server, username: string, externalAuthToken: string, expectedStatus = HttpStatusCode.OK_200) {
100 const path = '/api/v1/users/token'
101
102 const body = {
103 client_id: server.client.id,
104 client_secret: server.client.secret,
105 username: username,
106 response_type: 'code',
107 grant_type: 'password',
108 scope: 'upload',
109 externalAuthToken
110 }
111
112 return request(server.url)
113 .post(path)
114 .type('form')
115 .send(body)
116 .expect(expectedStatus)
117}
118
119// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
120 16
121export { 17export {
122 login, 18 setAccessTokensToServers
123 logout,
124 serverLogin,
125 refreshToken,
126 userLogin,
127 getAccessToken,
128 setAccessTokensToServers,
129 Server,
130 Client,
131 User,
132 loginUsingExternalToken
133} 19}
diff --git a/shared/extra-utils/users/notifications-command.ts b/shared/extra-utils/users/notifications-command.ts
new file mode 100644
index 000000000..2d79a3747
--- /dev/null
+++ b/shared/extra-utils/users/notifications-command.ts
@@ -0,0 +1,86 @@
1import { HttpStatusCode, ResultList } from '@shared/models'
2import { UserNotification, UserNotificationSetting } from '../../models/users'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class NotificationsCommand extends AbstractCommand {
6
7 updateMySettings (options: OverrideCommandOptions & {
8 settings: UserNotificationSetting
9 }) {
10 const path = '/api/v1/users/me/notification-settings'
11
12 return this.putBodyRequest({
13 ...options,
14
15 path,
16 fields: options.settings,
17 implicitToken: true,
18 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
19 })
20 }
21
22 list (options: OverrideCommandOptions & {
23 start?: number
24 count?: number
25 unread?: boolean
26 sort?: string
27 }) {
28 const { start, count, unread, sort = '-createdAt' } = options
29 const path = '/api/v1/users/me/notifications'
30
31 return this.getRequestBody<ResultList<UserNotification>>({
32 ...options,
33
34 path,
35 query: {
36 start,
37 count,
38 sort,
39 unread
40 },
41 implicitToken: true,
42 defaultExpectedStatus: HttpStatusCode.OK_200
43 })
44 }
45
46 markAsRead (options: OverrideCommandOptions & {
47 ids: number[]
48 }) {
49 const { ids } = options
50 const path = '/api/v1/users/me/notifications/read'
51
52 return this.postBodyRequest({
53 ...options,
54
55 path,
56 fields: { ids },
57 implicitToken: true,
58 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
59 })
60 }
61
62 markAsReadAll (options: OverrideCommandOptions) {
63 const path = '/api/v1/users/me/notifications/read-all'
64
65 return this.postBodyRequest({
66 ...options,
67
68 path,
69 implicitToken: true,
70 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
71 })
72 }
73
74 async getLastest (options: OverrideCommandOptions = {}) {
75 const { total, data } = await this.list({
76 ...options,
77 start: 0,
78 count: 1,
79 sort: '-createdAt'
80 })
81
82 if (total === 0) return undefined
83
84 return data[0]
85 }
86}
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/notifications.ts
index 844f4442d..4c42fad3e 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/notifications.ts
@@ -3,91 +3,36 @@
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { inspect } from 'util' 4import { inspect } from 'util'
5import { AbuseState, PluginType } from '@shared/models' 5import { AbuseState, PluginType } from '@shared/models'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' 6import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
8import { MockSmtpServer } from '../miscs/email' 7import { MockSmtpServer } from '../mock-servers/mock-email'
9import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' 8import { PeerTubeServer } from '../server'
10import { doubleFollow } from '../server/follows' 9import { doubleFollow } from '../server/follows'
11import { flushAndRunMultipleServers, ServerInfo } from '../server/servers' 10import { createMultipleServers } from '../server/servers'
12import { getUserNotificationSocket } from '../socket/socket-io' 11import { setAccessTokensToServers } from './login'
13import { setAccessTokensToServers, userLogin } from './login'
14import { createUser, getMyUserInformation } from './users'
15 12
16function updateMyNotificationSettings ( 13function getAllNotificationsSettings (): UserNotificationSetting {
17 url: string, 14 return {
18 token: string, 15 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
19 settings: UserNotificationSetting, 16 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
20 statusCodeExpected = HttpStatusCode.NO_CONTENT_204 17 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
21) { 18 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
22 const path = '/api/v1/users/me/notification-settings' 19 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
23 20 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
24 return makePutBodyRequest({ 21 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
25 url, 22 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
26 path, 23 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
27 token, 24 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
28 fields: settings, 25 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
29 statusCodeExpected 26 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
30 }) 27 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
31} 28 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
32 29 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
33async function getUserNotifications ( 30 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
34 url: string, 31 }
35 token: string,
36 start: number,
37 count: number,
38 unread?: boolean,
39 sort = '-createdAt',
40 statusCodeExpected = HttpStatusCode.OK_200
41) {
42 const path = '/api/v1/users/me/notifications'
43
44 return makeGetRequest({
45 url,
46 path,
47 token,
48 query: {
49 start,
50 count,
51 sort,
52 unread
53 },
54 statusCodeExpected
55 })
56}
57
58function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
59 const path = '/api/v1/users/me/notifications/read'
60
61 return makePostBodyRequest({
62 url,
63 path,
64 token,
65 fields: { ids },
66 statusCodeExpected
67 })
68}
69
70function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
71 const path = '/api/v1/users/me/notifications/read-all'
72
73 return makePostBodyRequest({
74 url,
75 path,
76 token,
77 statusCodeExpected
78 })
79}
80
81async function getLastNotification (serverUrl: string, accessToken: string) {
82 const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
83
84 if (res.body.total === 0) return undefined
85
86 return res.body.data[0] as UserNotification
87} 32}
88 33
89type CheckerBaseParams = { 34type CheckerBaseParams = {
90 server: ServerInfo 35 server: PeerTubeServer
91 emails: any[] 36 emails: any[]
92 socketNotifications: UserNotification[] 37 socketNotifications: UserNotification[]
93 token: string 38 token: string
@@ -105,7 +50,7 @@ async function checkNotification (
105 const check = base.check || { web: true, mail: true } 50 const check = base.check || { web: true, mail: true }
106 51
107 if (check.web) { 52 if (check.web) {
108 const notification = await getLastNotification(base.server.url, base.token) 53 const notification = await base.server.notifications.getLastest({ token: base.token })
109 54
110 if (notification || checkType !== 'absence') { 55 if (notification || checkType !== 'absence') {
111 notificationChecker(notification, checkType) 56 notificationChecker(notification, checkType)
@@ -681,27 +626,6 @@ async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: Plugi
681 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 626 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
682} 627}
683 628
684function getAllNotificationsSettings (): UserNotificationSetting {
685 return {
686 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
687 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
688 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
689 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
690 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
691 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
692 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
693 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
694 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
695 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
696 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
697 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
698 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
699 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
700 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
701 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
702 }
703}
704
705async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) { 629async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
706 const userNotifications: UserNotification[] = [] 630 const userNotifications: UserNotification[] = []
707 const adminNotifications: UserNotification[] = [] 631 const adminNotifications: UserNotification[] = []
@@ -719,7 +643,7 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
719 limit: 20 643 limit: 20
720 } 644 }
721 } 645 }
722 const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) 646 const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
723 647
724 await setAccessTokensToServers(servers) 648 await setAccessTokensToServers(servers)
725 649
@@ -727,42 +651,33 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
727 await doubleFollow(servers[0], servers[1]) 651 await doubleFollow(servers[0], servers[1])
728 } 652 }
729 653
730 const user = { 654 const user = { username: 'user_1', password: 'super password' }
731 username: 'user_1', 655 await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
732 password: 'super password' 656 const userAccessToken = await servers[0].login.getAccessToken(user)
733 }
734 await createUser({
735 url: servers[0].url,
736 accessToken: servers[0].accessToken,
737 username: user.username,
738 password: user.password,
739 videoQuota: 10 * 1000 * 1000
740 })
741 const userAccessToken = await userLogin(servers[0], user)
742 657
743 await updateMyNotificationSettings(servers[0].url, userAccessToken, getAllNotificationsSettings()) 658 await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
744 await updateMyNotificationSettings(servers[0].url, servers[0].accessToken, getAllNotificationsSettings()) 659 await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
745 660
746 if (serversCount > 1) { 661 if (serversCount > 1) {
747 await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, getAllNotificationsSettings()) 662 await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
748 } 663 }
749 664
750 { 665 {
751 const socket = getUserNotificationSocket(servers[0].url, userAccessToken) 666 const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
752 socket.on('new-notification', n => userNotifications.push(n)) 667 socket.on('new-notification', n => userNotifications.push(n))
753 } 668 }
754 { 669 {
755 const socket = getUserNotificationSocket(servers[0].url, servers[0].accessToken) 670 const socket = servers[0].socketIO.getUserNotificationSocket()
756 socket.on('new-notification', n => adminNotifications.push(n)) 671 socket.on('new-notification', n => adminNotifications.push(n))
757 } 672 }
758 673
759 if (serversCount > 1) { 674 if (serversCount > 1) {
760 const socket = getUserNotificationSocket(servers[1].url, servers[1].accessToken) 675 const socket = servers[1].socketIO.getUserNotificationSocket()
761 socket.on('new-notification', n => adminNotificationsServer2.push(n)) 676 socket.on('new-notification', n => adminNotificationsServer2.push(n))
762 } 677 }
763 678
764 const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken) 679 const { videoChannels } = await servers[0].users.getMyInfo()
765 const channelId = resChannel.body.videoChannels[0].id 680 const channelId = videoChannels[0].id
766 681
767 return { 682 return {
768 userNotifications, 683 userNotifications,
@@ -778,11 +693,11 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
778// --------------------------------------------------------------------------- 693// ---------------------------------------------------------------------------
779 694
780export { 695export {
696 getAllNotificationsSettings,
697
781 CheckerBaseParams, 698 CheckerBaseParams,
782 CheckerType, 699 CheckerType,
783 getAllNotificationsSettings,
784 checkNotification, 700 checkNotification,
785 markAsReadAllNotifications,
786 checkMyVideoImportIsFinished, 701 checkMyVideoImportIsFinished,
787 checkUserRegistered, 702 checkUserRegistered,
788 checkAutoInstanceFollowing, 703 checkAutoInstanceFollowing,
@@ -792,14 +707,10 @@ export {
792 checkNewCommentOnMyVideo, 707 checkNewCommentOnMyVideo,
793 checkNewBlacklistOnMyVideo, 708 checkNewBlacklistOnMyVideo,
794 checkCommentMention, 709 checkCommentMention,
795 updateMyNotificationSettings,
796 checkNewVideoAbuseForModerators, 710 checkNewVideoAbuseForModerators,
797 checkVideoAutoBlacklistForModerators, 711 checkVideoAutoBlacklistForModerators,
798 checkNewAbuseMessage, 712 checkNewAbuseMessage,
799 checkAbuseStateChange, 713 checkAbuseStateChange,
800 getUserNotifications,
801 markAsReadNotifications,
802 getLastNotification,
803 checkNewInstanceFollower, 714 checkNewInstanceFollower,
804 prepareNotificationsTest, 715 prepareNotificationsTest,
805 checkNewCommentAbuseForModerators, 716 checkNewCommentAbuseForModerators,
diff --git a/shared/extra-utils/users/subscriptions-command.ts b/shared/extra-utils/users/subscriptions-command.ts
new file mode 100644
index 000000000..edc60e612
--- /dev/null
+++ b/shared/extra-utils/users/subscriptions-command.ts
@@ -0,0 +1,99 @@
1import { HttpStatusCode, ResultList, Video, VideoChannel } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class SubscriptionsCommand extends AbstractCommand {
5
6 add (options: OverrideCommandOptions & {
7 targetUri: string
8 }) {
9 const path = '/api/v1/users/me/subscriptions'
10
11 return this.postBodyRequest({
12 ...options,
13
14 path,
15 fields: { uri: options.targetUri },
16 implicitToken: true,
17 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
18 })
19 }
20
21 list (options: OverrideCommandOptions & {
22 sort?: string // default -createdAt
23 search?: string
24 } = {}) {
25 const { sort = '-createdAt', search } = options
26 const path = '/api/v1/users/me/subscriptions'
27
28 return this.getRequestBody<ResultList<VideoChannel>>({
29 ...options,
30
31 path,
32 query: {
33 sort,
34 search
35 },
36 implicitToken: true,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 listVideos (options: OverrideCommandOptions & {
42 sort?: string // default -createdAt
43 } = {}) {
44 const { sort = '-createdAt' } = options
45 const path = '/api/v1/users/me/subscriptions/videos'
46
47 return this.getRequestBody<ResultList<Video>>({
48 ...options,
49
50 path,
51 query: { sort },
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 get (options: OverrideCommandOptions & {
58 uri: string
59 }) {
60 const path = '/api/v1/users/me/subscriptions/' + options.uri
61
62 return this.getRequestBody<VideoChannel>({
63 ...options,
64
65 path,
66 implicitToken: true,
67 defaultExpectedStatus: HttpStatusCode.OK_200
68 })
69 }
70
71 remove (options: OverrideCommandOptions & {
72 uri: string
73 }) {
74 const path = '/api/v1/users/me/subscriptions/' + options.uri
75
76 return this.deleteRequest({
77 ...options,
78
79 path,
80 implicitToken: true,
81 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
82 })
83 }
84
85 exist (options: OverrideCommandOptions & {
86 uris: string[]
87 }) {
88 const path = '/api/v1/users/me/subscriptions/exist'
89
90 return this.getRequestBody<{ [id: string ]: boolean }>({
91 ...options,
92
93 path,
94 query: { 'uris[]': options.uris },
95 implicitToken: true,
96 defaultExpectedStatus: HttpStatusCode.OK_200
97 })
98 }
99}
diff --git a/shared/extra-utils/users/user-subscriptions.ts b/shared/extra-utils/users/user-subscriptions.ts
deleted file mode 100644
index edc7a3562..000000000
--- a/shared/extra-utils/users/user-subscriptions.ts
+++ /dev/null
@@ -1,93 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
5 const path = '/api/v1/users/me/subscriptions'
6
7 return makePostBodyRequest({
8 url,
9 path,
10 token,
11 statusCodeExpected,
12 fields: { uri: targetUri }
13 })
14}
15
16function listUserSubscriptions (parameters: {
17 url: string
18 token: string
19 sort?: string
20 search?: string
21 statusCodeExpected?: number
22}) {
23 const { url, token, sort = '-createdAt', search, statusCodeExpected = HttpStatusCode.OK_200 } = parameters
24 const path = '/api/v1/users/me/subscriptions'
25
26 return makeGetRequest({
27 url,
28 path,
29 token,
30 statusCodeExpected,
31 query: {
32 sort,
33 search
34 }
35 })
36}
37
38function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = HttpStatusCode.OK_200) {
39 const path = '/api/v1/users/me/subscriptions/videos'
40
41 return makeGetRequest({
42 url,
43 path,
44 token,
45 statusCodeExpected,
46 query: { sort }
47 })
48}
49
50function getUserSubscription (url: string, token: string, uri: string, statusCodeExpected = HttpStatusCode.OK_200) {
51 const path = '/api/v1/users/me/subscriptions/' + uri
52
53 return makeGetRequest({
54 url,
55 path,
56 token,
57 statusCodeExpected
58 })
59}
60
61function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
62 const path = '/api/v1/users/me/subscriptions/' + uri
63
64 return makeDeleteRequest({
65 url,
66 path,
67 token,
68 statusCodeExpected
69 })
70}
71
72function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = HttpStatusCode.OK_200) {
73 const path = '/api/v1/users/me/subscriptions/exist'
74
75 return makeGetRequest({
76 url,
77 path,
78 query: { 'uris[]': uris },
79 token,
80 statusCodeExpected
81 })
82}
83
84// ---------------------------------------------------------------------------
85
86export {
87 areSubscriptionsExist,
88 addUserSubscription,
89 listUserSubscriptions,
90 getUserSubscription,
91 listUserSubscriptionVideos,
92 removeUserSubscription
93}
diff --git a/shared/extra-utils/users/users-command.ts b/shared/extra-utils/users/users-command.ts
new file mode 100644
index 000000000..d66ad15f2
--- /dev/null
+++ b/shared/extra-utils/users/users-command.ts
@@ -0,0 +1,414 @@
1import { omit, pick } from 'lodash'
2import {
3 HttpStatusCode,
4 MyUser,
5 ResultList,
6 User,
7 UserAdminFlag,
8 UserCreateResult,
9 UserRole,
10 UserUpdate,
11 UserUpdateMe,
12 UserVideoQuota,
13 UserVideoRate
14} from '@shared/models'
15import { ScopedToken } from '@shared/models/users/user-scoped-token'
16import { unwrapBody } from '../requests'
17import { AbstractCommand, OverrideCommandOptions } from '../shared'
18
19export class UsersCommand extends AbstractCommand {
20
21 askResetPassword (options: OverrideCommandOptions & {
22 email: string
23 }) {
24 const { email } = options
25 const path = '/api/v1/users/ask-reset-password'
26
27 return this.postBodyRequest({
28 ...options,
29
30 path,
31 fields: { email },
32 implicitToken: false,
33 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
34 })
35 }
36
37 resetPassword (options: OverrideCommandOptions & {
38 userId: number
39 verificationString: string
40 password: string
41 }) {
42 const { userId, verificationString, password } = options
43 const path = '/api/v1/users/' + userId + '/reset-password'
44
45 return this.postBodyRequest({
46 ...options,
47
48 path,
49 fields: { password, verificationString },
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
52 })
53 }
54
55 // ---------------------------------------------------------------------------
56
57 askSendVerifyEmail (options: OverrideCommandOptions & {
58 email: string
59 }) {
60 const { email } = options
61 const path = '/api/v1/users/ask-send-verify-email'
62
63 return this.postBodyRequest({
64 ...options,
65
66 path,
67 fields: { email },
68 implicitToken: false,
69 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
70 })
71 }
72
73 verifyEmail (options: OverrideCommandOptions & {
74 userId: number
75 verificationString: string
76 isPendingEmail?: boolean // default false
77 }) {
78 const { userId, verificationString, isPendingEmail = false } = options
79 const path = '/api/v1/users/' + userId + '/verify-email'
80
81 return this.postBodyRequest({
82 ...options,
83
84 path,
85 fields: {
86 verificationString,
87 isPendingEmail
88 },
89 implicitToken: false,
90 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
91 })
92 }
93
94 // ---------------------------------------------------------------------------
95
96 banUser (options: OverrideCommandOptions & {
97 userId: number
98 reason?: string
99 }) {
100 const { userId, reason } = options
101 const path = '/api/v1/users' + '/' + userId + '/block'
102
103 return this.postBodyRequest({
104 ...options,
105
106 path,
107 fields: { reason },
108 implicitToken: true,
109 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
110 })
111 }
112
113 unbanUser (options: OverrideCommandOptions & {
114 userId: number
115 }) {
116 const { userId } = options
117 const path = '/api/v1/users' + '/' + userId + '/unblock'
118
119 return this.postBodyRequest({
120 ...options,
121
122 path,
123 implicitToken: true,
124 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
125 })
126 }
127
128 // ---------------------------------------------------------------------------
129
130 getMyScopedTokens (options: OverrideCommandOptions = {}) {
131 const path = '/api/v1/users/scoped-tokens'
132
133 return this.getRequestBody<ScopedToken>({
134 ...options,
135
136 path,
137 implicitToken: true,
138 defaultExpectedStatus: HttpStatusCode.OK_200
139 })
140 }
141
142 renewMyScopedTokens (options: OverrideCommandOptions = {}) {
143 const path = '/api/v1/users/scoped-tokens'
144
145 return this.postBodyRequest({
146 ...options,
147
148 path,
149 implicitToken: true,
150 defaultExpectedStatus: HttpStatusCode.OK_200
151 })
152 }
153
154 // ---------------------------------------------------------------------------
155
156 create (options: OverrideCommandOptions & {
157 username: string
158 password?: string
159 videoQuota?: number
160 videoQuotaDaily?: number
161 role?: UserRole
162 adminFlags?: UserAdminFlag
163 }) {
164 const {
165 username,
166 adminFlags,
167 password = 'password',
168 videoQuota = 42000000,
169 videoQuotaDaily = -1,
170 role = UserRole.USER
171 } = options
172
173 const path = '/api/v1/users'
174
175 return unwrapBody<{ user: UserCreateResult }>(this.postBodyRequest({
176 ...options,
177
178 path,
179 fields: {
180 username,
181 password,
182 role,
183 adminFlags,
184 email: username + '@example.com',
185 videoQuota,
186 videoQuotaDaily
187 },
188 implicitToken: true,
189 defaultExpectedStatus: HttpStatusCode.OK_200
190 })).then(res => res.user)
191 }
192
193 async generate (username: string) {
194 const password = 'password'
195 const user = await this.create({ username, password })
196
197 const token = await this.server.login.getAccessToken({ username, password })
198
199 const me = await this.getMyInfo({ token })
200
201 return {
202 token,
203 userId: user.id,
204 userChannelId: me.videoChannels[0].id
205 }
206 }
207
208 async generateUserAndToken (username: string) {
209 const password = 'password'
210 await this.create({ username, password })
211
212 return this.server.login.getAccessToken({ username, password })
213 }
214
215 register (options: OverrideCommandOptions & {
216 username: string
217 password?: string
218 displayName?: string
219 channel?: {
220 name: string
221 displayName: string
222 }
223 }) {
224 const { username, password = 'password', displayName, channel } = options
225 const path = '/api/v1/users/register'
226
227 return this.postBodyRequest({
228 ...options,
229
230 path,
231 fields: {
232 username,
233 password,
234 email: username + '@example.com',
235 displayName,
236 channel
237 },
238 implicitToken: false,
239 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
240 })
241 }
242
243 // ---------------------------------------------------------------------------
244
245 getMyInfo (options: OverrideCommandOptions = {}) {
246 const path = '/api/v1/users/me'
247
248 return this.getRequestBody<MyUser>({
249 ...options,
250
251 path,
252 implicitToken: true,
253 defaultExpectedStatus: HttpStatusCode.OK_200
254 })
255 }
256
257 getMyQuotaUsed (options: OverrideCommandOptions = {}) {
258 const path = '/api/v1/users/me/video-quota-used'
259
260 return this.getRequestBody<UserVideoQuota>({
261 ...options,
262
263 path,
264 implicitToken: true,
265 defaultExpectedStatus: HttpStatusCode.OK_200
266 })
267 }
268
269 getMyRating (options: OverrideCommandOptions & {
270 videoId: number | string
271 }) {
272 const { videoId } = options
273 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
274
275 return this.getRequestBody<UserVideoRate>({
276 ...options,
277
278 path,
279 implicitToken: true,
280 defaultExpectedStatus: HttpStatusCode.OK_200
281 })
282 }
283
284 deleteMe (options: OverrideCommandOptions = {}) {
285 const path = '/api/v1/users/me'
286
287 return this.deleteRequest({
288 ...options,
289
290 path,
291 implicitToken: true,
292 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
293 })
294 }
295
296 updateMe (options: OverrideCommandOptions & UserUpdateMe) {
297 const path = '/api/v1/users/me'
298
299 const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
300
301 return this.putBodyRequest({
302 ...options,
303
304 path,
305 fields: toSend,
306 implicitToken: true,
307 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
308 })
309 }
310
311 updateMyAvatar (options: OverrideCommandOptions & {
312 fixture: string
313 }) {
314 const { fixture } = options
315 const path = '/api/v1/users/me/avatar/pick'
316
317 return this.updateImageRequest({
318 ...options,
319
320 path,
321 fixture,
322 fieldname: 'avatarfile',
323
324 implicitToken: true,
325 defaultExpectedStatus: HttpStatusCode.OK_200
326 })
327 }
328
329 // ---------------------------------------------------------------------------
330
331 get (options: OverrideCommandOptions & {
332 userId: number
333 withStats?: boolean // default false
334 }) {
335 const { userId, withStats } = options
336 const path = '/api/v1/users/' + userId
337
338 return this.getRequestBody<User>({
339 ...options,
340
341 path,
342 query: { withStats },
343 implicitToken: true,
344 defaultExpectedStatus: HttpStatusCode.OK_200
345 })
346 }
347
348 list (options: OverrideCommandOptions & {
349 start?: number
350 count?: number
351 sort?: string
352 search?: string
353 blocked?: boolean
354 } = {}) {
355 const path = '/api/v1/users'
356
357 return this.getRequestBody<ResultList<User>>({
358 ...options,
359
360 path,
361 query: pick(options, [ 'start', 'count', 'sort', 'search', 'blocked' ]),
362 implicitToken: true,
363 defaultExpectedStatus: HttpStatusCode.OK_200
364 })
365 }
366
367 remove (options: OverrideCommandOptions & {
368 userId: number
369 }) {
370 const { userId } = options
371 const path = '/api/v1/users/' + userId
372
373 return this.deleteRequest({
374 ...options,
375
376 path,
377 implicitToken: true,
378 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
379 })
380 }
381
382 update (options: OverrideCommandOptions & {
383 userId: number
384 email?: string
385 emailVerified?: boolean
386 videoQuota?: number
387 videoQuotaDaily?: number
388 password?: string
389 adminFlags?: UserAdminFlag
390 pluginAuth?: string
391 role?: UserRole
392 }) {
393 const path = '/api/v1/users/' + options.userId
394
395 const toSend: UserUpdate = {}
396 if (options.password !== undefined && options.password !== null) toSend.password = options.password
397 if (options.email !== undefined && options.email !== null) toSend.email = options.email
398 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend.emailVerified = options.emailVerified
399 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend.videoQuota = options.videoQuota
400 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend.videoQuotaDaily = options.videoQuotaDaily
401 if (options.role !== undefined && options.role !== null) toSend.role = options.role
402 if (options.adminFlags !== undefined && options.adminFlags !== null) toSend.adminFlags = options.adminFlags
403 if (options.pluginAuth !== undefined) toSend.pluginAuth = options.pluginAuth
404
405 return this.putBodyRequest({
406 ...options,
407
408 path,
409 fields: toSend,
410 implicitToken: true,
411 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
412 })
413 }
414}
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
deleted file mode 100644
index 0f15962ad..000000000
--- a/shared/extra-utils/users/users.ts
+++ /dev/null
@@ -1,415 +0,0 @@
1import { omit } from 'lodash'
2import * as request from 'supertest'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { UserUpdateMe } from '../../models/users'
5import { UserAdminFlag } from '../../models/users/user-flag.model'
6import { UserRegister } from '../../models/users/user-register.model'
7import { UserRole } from '../../models/users/user-role'
8import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests'
9import { ServerInfo } from '../server/servers'
10import { userLogin } from './login'
11
12function createUser (parameters: {
13 url: string
14 accessToken: string
15 username: string
16 password: string
17 videoQuota?: number
18 videoQuotaDaily?: number
19 role?: UserRole
20 adminFlags?: UserAdminFlag
21 specialStatus?: number
22}) {
23 const {
24 url,
25 accessToken,
26 username,
27 adminFlags,
28 password = 'password',
29 videoQuota = 1000000,
30 videoQuotaDaily = -1,
31 role = UserRole.USER,
32 specialStatus = HttpStatusCode.OK_200
33 } = parameters
34
35 const path = '/api/v1/users'
36 const body = {
37 username,
38 password,
39 role,
40 adminFlags,
41 email: username + '@example.com',
42 videoQuota,
43 videoQuotaDaily
44 }
45
46 return request(url)
47 .post(path)
48 .set('Accept', 'application/json')
49 .set('Authorization', 'Bearer ' + accessToken)
50 .send(body)
51 .expect(specialStatus)
52}
53
54async function generateUser (server: ServerInfo, username: string) {
55 const password = 'my super password'
56 const resCreate = await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
57
58 const token = await userLogin(server, { username, password })
59
60 const resMe = await getMyUserInformation(server.url, token)
61
62 return {
63 token,
64 userId: resCreate.body.user.id,
65 userChannelId: resMe.body.videoChannels[0].id
66 }
67}
68
69async function generateUserAccessToken (server: ServerInfo, username: string) {
70 const password = 'my super password'
71 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
72
73 return userLogin(server, { username, password })
74}
75
76function registerUser (url: string, username: string, password: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
77 const path = '/api/v1/users/register'
78 const body = {
79 username,
80 password,
81 email: username + '@example.com'
82 }
83
84 return request(url)
85 .post(path)
86 .set('Accept', 'application/json')
87 .send(body)
88 .expect(specialStatus)
89}
90
91function registerUserWithChannel (options: {
92 url: string
93 user: { username: string, password: string, displayName?: string }
94 channel: { name: string, displayName: string }
95}) {
96 const path = '/api/v1/users/register'
97 const body: UserRegister = {
98 username: options.user.username,
99 password: options.user.password,
100 email: options.user.username + '@example.com',
101 channel: options.channel
102 }
103
104 if (options.user.displayName) {
105 Object.assign(body, { displayName: options.user.displayName })
106 }
107
108 return makePostBodyRequest({
109 url: options.url,
110 path,
111 fields: body,
112 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
113 })
114}
115
116function getMyUserInformation (url: string, accessToken: string, specialStatus = HttpStatusCode.OK_200) {
117 const path = '/api/v1/users/me'
118
119 return request(url)
120 .get(path)
121 .set('Accept', 'application/json')
122 .set('Authorization', 'Bearer ' + accessToken)
123 .expect(specialStatus)
124 .expect('Content-Type', /json/)
125}
126
127function getUserScopedTokens (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
128 const path = '/api/v1/users/scoped-tokens'
129
130 return makeGetRequest({
131 url,
132 path,
133 token,
134 statusCodeExpected
135 })
136}
137
138function renewUserScopedTokens (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
139 const path = '/api/v1/users/scoped-tokens'
140
141 return makePostBodyRequest({
142 url,
143 path,
144 token,
145 statusCodeExpected
146 })
147}
148
149function deleteMe (url: string, accessToken: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
150 const path = '/api/v1/users/me'
151
152 return request(url)
153 .delete(path)
154 .set('Accept', 'application/json')
155 .set('Authorization', 'Bearer ' + accessToken)
156 .expect(specialStatus)
157}
158
159function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = HttpStatusCode.OK_200) {
160 const path = '/api/v1/users/me/video-quota-used'
161
162 return request(url)
163 .get(path)
164 .set('Accept', 'application/json')
165 .set('Authorization', 'Bearer ' + accessToken)
166 .expect(specialStatus)
167 .expect('Content-Type', /json/)
168}
169
170function getUserInformation (url: string, accessToken: string, userId: number, withStats = false) {
171 const path = '/api/v1/users/' + userId
172
173 return request(url)
174 .get(path)
175 .query({ withStats })
176 .set('Accept', 'application/json')
177 .set('Authorization', 'Bearer ' + accessToken)
178 .expect(HttpStatusCode.OK_200)
179 .expect('Content-Type', /json/)
180}
181
182function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = HttpStatusCode.OK_200) {
183 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
184
185 return request(url)
186 .get(path)
187 .set('Accept', 'application/json')
188 .set('Authorization', 'Bearer ' + accessToken)
189 .expect(specialStatus)
190 .expect('Content-Type', /json/)
191}
192
193function getUsersList (url: string, accessToken: string) {
194 const path = '/api/v1/users'
195
196 return request(url)
197 .get(path)
198 .set('Accept', 'application/json')
199 .set('Authorization', 'Bearer ' + accessToken)
200 .expect(HttpStatusCode.OK_200)
201 .expect('Content-Type', /json/)
202}
203
204function getUsersListPaginationAndSort (
205 url: string,
206 accessToken: string,
207 start: number,
208 count: number,
209 sort: string,
210 search?: string,
211 blocked?: boolean
212) {
213 const path = '/api/v1/users'
214
215 const query = {
216 start,
217 count,
218 sort,
219 search,
220 blocked
221 }
222
223 return request(url)
224 .get(path)
225 .query(query)
226 .set('Accept', 'application/json')
227 .set('Authorization', 'Bearer ' + accessToken)
228 .expect(HttpStatusCode.OK_200)
229 .expect('Content-Type', /json/)
230}
231
232function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
233 const path = '/api/v1/users'
234
235 return request(url)
236 .delete(path + '/' + userId)
237 .set('Accept', 'application/json')
238 .set('Authorization', 'Bearer ' + accessToken)
239 .expect(expectedStatus)
240}
241
242function blockUser (
243 url: string,
244 userId: number | string,
245 accessToken: string,
246 expectedStatus = HttpStatusCode.NO_CONTENT_204,
247 reason?: string
248) {
249 const path = '/api/v1/users'
250 let body: any
251 if (reason) body = { reason }
252
253 return request(url)
254 .post(path + '/' + userId + '/block')
255 .send(body)
256 .set('Accept', 'application/json')
257 .set('Authorization', 'Bearer ' + accessToken)
258 .expect(expectedStatus)
259}
260
261function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
262 const path = '/api/v1/users'
263
264 return request(url)
265 .post(path + '/' + userId + '/unblock')
266 .set('Accept', 'application/json')
267 .set('Authorization', 'Bearer ' + accessToken)
268 .expect(expectedStatus)
269}
270
271function updateMyUser (options: { url: string, accessToken: string, statusCodeExpected?: HttpStatusCode } & UserUpdateMe) {
272 const path = '/api/v1/users/me'
273
274 const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
275
276 return makePutBodyRequest({
277 url: options.url,
278 path,
279 token: options.accessToken,
280 fields: toSend,
281 statusCodeExpected: options.statusCodeExpected || HttpStatusCode.NO_CONTENT_204
282 })
283}
284
285function updateMyAvatar (options: {
286 url: string
287 accessToken: string
288 fixture: string
289}) {
290 const path = '/api/v1/users/me/avatar/pick'
291
292 return updateImageRequest({ ...options, path, fieldname: 'avatarfile' })
293}
294
295function updateUser (options: {
296 url: string
297 userId: number
298 accessToken: string
299 email?: string
300 emailVerified?: boolean
301 videoQuota?: number
302 videoQuotaDaily?: number
303 password?: string
304 adminFlags?: UserAdminFlag
305 pluginAuth?: string
306 role?: UserRole
307}) {
308 const path = '/api/v1/users/' + options.userId
309
310 const toSend = {}
311 if (options.password !== undefined && options.password !== null) toSend['password'] = options.password
312 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
313 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
314 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
315 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
316 if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
317 if (options.adminFlags !== undefined && options.adminFlags !== null) toSend['adminFlags'] = options.adminFlags
318 if (options.pluginAuth !== undefined) toSend['pluginAuth'] = options.pluginAuth
319
320 return makePutBodyRequest({
321 url: options.url,
322 path,
323 token: options.accessToken,
324 fields: toSend,
325 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
326 })
327}
328
329function askResetPassword (url: string, email: string) {
330 const path = '/api/v1/users/ask-reset-password'
331
332 return makePostBodyRequest({
333 url,
334 path,
335 fields: { email },
336 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
337 })
338}
339
340function resetPassword (
341 url: string,
342 userId: number,
343 verificationString: string,
344 password: string,
345 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
346) {
347 const path = '/api/v1/users/' + userId + '/reset-password'
348
349 return makePostBodyRequest({
350 url,
351 path,
352 fields: { password, verificationString },
353 statusCodeExpected
354 })
355}
356
357function askSendVerifyEmail (url: string, email: string) {
358 const path = '/api/v1/users/ask-send-verify-email'
359
360 return makePostBodyRequest({
361 url,
362 path,
363 fields: { email },
364 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
365 })
366}
367
368function verifyEmail (
369 url: string,
370 userId: number,
371 verificationString: string,
372 isPendingEmail = false,
373 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
374) {
375 const path = '/api/v1/users/' + userId + '/verify-email'
376
377 return makePostBodyRequest({
378 url,
379 path,
380 fields: {
381 verificationString,
382 isPendingEmail
383 },
384 statusCodeExpected
385 })
386}
387
388// ---------------------------------------------------------------------------
389
390export {
391 createUser,
392 registerUser,
393 getMyUserInformation,
394 getMyUserVideoRating,
395 deleteMe,
396 registerUserWithChannel,
397 getMyUserVideoQuotaUsed,
398 getUsersList,
399 getUsersListPaginationAndSort,
400 removeUser,
401 updateUser,
402 updateMyUser,
403 getUserInformation,
404 blockUser,
405 unblockUser,
406 askResetPassword,
407 resetPassword,
408 renewUserScopedTokens,
409 updateMyAvatar,
410 generateUser,
411 askSendVerifyEmail,
412 generateUserAccessToken,
413 verifyEmail,
414 getUserScopedTokens
415}
diff --git a/shared/extra-utils/videos/blacklist-command.ts b/shared/extra-utils/videos/blacklist-command.ts
new file mode 100644
index 000000000..3a2ef89ba
--- /dev/null
+++ b/shared/extra-utils/videos/blacklist-command.ts
@@ -0,0 +1,76 @@
1
2import { HttpStatusCode, ResultList } from '@shared/models'
3import { VideoBlacklist, VideoBlacklistType } from '../../models/videos'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class BlacklistCommand extends AbstractCommand {
7
8 add (options: OverrideCommandOptions & {
9 videoId: number | string
10 reason?: string
11 unfederate?: boolean
12 }) {
13 const { videoId, reason, unfederate } = options
14 const path = '/api/v1/videos/' + videoId + '/blacklist'
15
16 return this.postBodyRequest({
17 ...options,
18
19 path,
20 fields: { reason, unfederate },
21 implicitToken: true,
22 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
23 })
24 }
25
26 update (options: OverrideCommandOptions & {
27 videoId: number | string
28 reason?: string
29 }) {
30 const { videoId, reason } = options
31 const path = '/api/v1/videos/' + videoId + '/blacklist'
32
33 return this.putBodyRequest({
34 ...options,
35
36 path,
37 fields: { reason },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
40 })
41 }
42
43 remove (options: OverrideCommandOptions & {
44 videoId: number | string
45 }) {
46 const { videoId } = options
47 const path = '/api/v1/videos/' + videoId + '/blacklist'
48
49 return this.deleteRequest({
50 ...options,
51
52 path,
53 implicitToken: true,
54 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
55 })
56 }
57
58 list (options: OverrideCommandOptions & {
59 sort?: string
60 type?: VideoBlacklistType
61 } = {}) {
62 const { sort, type } = options
63 const path = '/api/v1/videos/blacklist/'
64
65 const query = { sort, type }
66
67 return this.getRequestBody<ResultList<VideoBlacklist>>({
68 ...options,
69
70 path,
71 query,
72 implicitToken: true,
73 defaultExpectedStatus: HttpStatusCode.OK_200
74 })
75 }
76}
diff --git a/shared/extra-utils/videos/captions-command.ts b/shared/extra-utils/videos/captions-command.ts
new file mode 100644
index 000000000..a65ea99e3
--- /dev/null
+++ b/shared/extra-utils/videos/captions-command.ts
@@ -0,0 +1,65 @@
1import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models'
2import { buildAbsoluteFixturePath } from '../miscs'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class CaptionsCommand extends AbstractCommand {
6
7 add (options: OverrideCommandOptions & {
8 videoId: string | number
9 language: string
10 fixture: string
11 mimeType?: string
12 }) {
13 const { videoId, language, fixture, mimeType } = options
14
15 const path = '/api/v1/videos/' + videoId + '/captions/' + language
16
17 const captionfile = buildAbsoluteFixturePath(fixture)
18 const captionfileAttach = mimeType
19 ? [ captionfile, { contentType: mimeType } ]
20 : captionfile
21
22 return this.putUploadRequest({
23 ...options,
24
25 path,
26 fields: {},
27 attaches: {
28 captionfile: captionfileAttach
29 },
30 implicitToken: true,
31 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
32 })
33 }
34
35 list (options: OverrideCommandOptions & {
36 videoId: string | number
37 }) {
38 const { videoId } = options
39 const path = '/api/v1/videos/' + videoId + '/captions'
40
41 return this.getRequestBody<ResultList<VideoCaption>>({
42 ...options,
43
44 path,
45 implicitToken: false,
46 defaultExpectedStatus: HttpStatusCode.OK_200
47 })
48 }
49
50 delete (options: OverrideCommandOptions & {
51 videoId: string | number
52 language: string
53 }) {
54 const { videoId, language } = options
55 const path = '/api/v1/videos/' + videoId + '/captions/' + language
56
57 return this.deleteRequest({
58 ...options,
59
60 path,
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
63 })
64 }
65}
diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts
new file mode 100644
index 000000000..ff8a43366
--- /dev/null
+++ b/shared/extra-utils/videos/captions.ts
@@ -0,0 +1,17 @@
1import { expect } from 'chai'
2import * as request from 'supertest'
3import { HttpStatusCode } from '@shared/models'
4
5async function testCaptionFile (url: string, captionPath: string, containsString: string) {
6 const res = await request(url)
7 .get(captionPath)
8 .expect(HttpStatusCode.OK_200)
9
10 expect(res.text).to.contain(containsString)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 testCaptionFile
17}
diff --git a/shared/extra-utils/videos/change-ownership-command.ts b/shared/extra-utils/videos/change-ownership-command.ts
new file mode 100644
index 000000000..ad4c726ef
--- /dev/null
+++ b/shared/extra-utils/videos/change-ownership-command.ts
@@ -0,0 +1,68 @@
1
2import { HttpStatusCode, ResultList, VideoChangeOwnership } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class ChangeOwnershipCommand extends AbstractCommand {
6
7 create (options: OverrideCommandOptions & {
8 videoId: number | string
9 username: string
10 }) {
11 const { videoId, username } = options
12 const path = '/api/v1/videos/' + videoId + '/give-ownership'
13
14 return this.postBodyRequest({
15 ...options,
16
17 path,
18 fields: { username },
19 implicitToken: true,
20 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
21 })
22 }
23
24 list (options: OverrideCommandOptions = {}) {
25 const path = '/api/v1/videos/ownership'
26
27 return this.getRequestBody<ResultList<VideoChangeOwnership>>({
28 ...options,
29
30 path,
31 query: { sort: '-createdAt' },
32 implicitToken: true,
33 defaultExpectedStatus: HttpStatusCode.OK_200
34 })
35 }
36
37 accept (options: OverrideCommandOptions & {
38 ownershipId: number
39 channelId: number
40 }) {
41 const { ownershipId, channelId } = options
42 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
43
44 return this.postBodyRequest({
45 ...options,
46
47 path,
48 fields: { channelId },
49 implicitToken: true,
50 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
51 })
52 }
53
54 refuse (options: OverrideCommandOptions & {
55 ownershipId: number
56 }) {
57 const { ownershipId } = options
58 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
59
60 return this.postBodyRequest({
61 ...options,
62
63 path,
64 implicitToken: true,
65 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
66 })
67 }
68}
diff --git a/shared/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts
new file mode 100644
index 000000000..f8eb3f885
--- /dev/null
+++ b/shared/extra-utils/videos/channels-command.ts
@@ -0,0 +1,156 @@
1import { pick } from 'lodash'
2import { HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models'
3import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { unwrapBody } from '../requests'
6import { AbstractCommand, OverrideCommandOptions } from '../shared'
7
8export class ChannelsCommand extends AbstractCommand {
9
10 list (options: OverrideCommandOptions & {
11 start?: number
12 count?: number
13 sort?: string
14 withStats?: boolean
15 } = {}) {
16 const path = '/api/v1/video-channels'
17
18 return this.getRequestBody<ResultList<VideoChannel>>({
19 ...options,
20
21 path,
22 query: pick(options, [ 'start', 'count', 'sort', 'withStats' ]),
23 implicitToken: false,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 listByAccount (options: OverrideCommandOptions & {
29 accountName: string
30 start?: number
31 count?: number
32 sort?: string
33 withStats?: boolean
34 search?: string
35 }) {
36 const { accountName, sort = 'createdAt' } = options
37 const path = '/api/v1/accounts/' + accountName + '/video-channels'
38
39 return this.getRequestBody<ResultList<VideoChannel>>({
40 ...options,
41
42 path,
43 query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
44 implicitToken: false,
45 defaultExpectedStatus: HttpStatusCode.OK_200
46 })
47 }
48
49 async create (options: OverrideCommandOptions & {
50 attributes: VideoChannelCreate
51 }) {
52 const path = '/api/v1/video-channels/'
53
54 // Default attributes
55 const defaultAttributes = {
56 displayName: 'my super video channel',
57 description: 'my super channel description',
58 support: 'my super channel support'
59 }
60 const attributes = { ...defaultAttributes, ...options.attributes }
61
62 const body = await unwrapBody<{ videoChannel: VideoChannelCreateResult }>(this.postBodyRequest({
63 ...options,
64
65 path,
66 fields: attributes,
67 implicitToken: true,
68 defaultExpectedStatus: HttpStatusCode.OK_200
69 }))
70
71 return body.videoChannel
72 }
73
74 update (options: OverrideCommandOptions & {
75 channelName: string
76 attributes: VideoChannelUpdate
77 }) {
78 const { channelName, attributes } = options
79 const path = '/api/v1/video-channels/' + channelName
80
81 return this.putBodyRequest({
82 ...options,
83
84 path,
85 fields: attributes,
86 implicitToken: true,
87 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
88 })
89 }
90
91 delete (options: OverrideCommandOptions & {
92 channelName: string
93 }) {
94 const path = '/api/v1/video-channels/' + options.channelName
95
96 return this.deleteRequest({
97 ...options,
98
99 path,
100 implicitToken: true,
101 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
102 })
103 }
104
105 get (options: OverrideCommandOptions & {
106 channelName: string
107 }) {
108 const path = '/api/v1/video-channels/' + options.channelName
109
110 return this.getRequestBody<VideoChannel>({
111 ...options,
112
113 path,
114 implicitToken: false,
115 defaultExpectedStatus: HttpStatusCode.OK_200
116 })
117 }
118
119 updateImage (options: OverrideCommandOptions & {
120 fixture: string
121 channelName: string | number
122 type: 'avatar' | 'banner'
123 }) {
124 const { channelName, fixture, type } = options
125
126 const path = `/api/v1/video-channels/${channelName}/${type}/pick`
127
128 return this.updateImageRequest({
129 ...options,
130
131 path,
132 fixture,
133 fieldname: type + 'file',
134
135 implicitToken: true,
136 defaultExpectedStatus: HttpStatusCode.OK_200
137 })
138 }
139
140 deleteImage (options: OverrideCommandOptions & {
141 channelName: string | number
142 type: 'avatar' | 'banner'
143 }) {
144 const { channelName, type } = options
145
146 const path = `/api/v1/video-channels/${channelName}/${type}`
147
148 return this.deleteRequest({
149 ...options,
150
151 path,
152 implicitToken: true,
153 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
154 })
155 }
156}
diff --git a/shared/extra-utils/videos/channels.ts b/shared/extra-utils/videos/channels.ts
new file mode 100644
index 000000000..756c47453
--- /dev/null
+++ b/shared/extra-utils/videos/channels.ts
@@ -0,0 +1,18 @@
1import { PeerTubeServer } from '../server/server'
2
3function setDefaultVideoChannel (servers: PeerTubeServer[]) {
4 const tasks: Promise<any>[] = []
5
6 for (const server of servers) {
7 const p = server.users.getMyInfo()
8 .then(user => { server.store.channel = user.videoChannels[0] })
9
10 tasks.push(p)
11 }
12
13 return Promise.all(tasks)
14}
15
16export {
17 setDefaultVideoChannel
18}
diff --git a/shared/extra-utils/videos/comments-command.ts b/shared/extra-utils/videos/comments-command.ts
new file mode 100644
index 000000000..f0d163a07
--- /dev/null
+++ b/shared/extra-utils/videos/comments-command.ts
@@ -0,0 +1,152 @@
1import { pick } from 'lodash'
2import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@shared/models'
3import { unwrapBody } from '../requests'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class CommentsCommand extends AbstractCommand {
7
8 private lastVideoId: number | string
9 private lastThreadId: number
10 private lastReplyId: number
11
12 listForAdmin (options: OverrideCommandOptions & {
13 start?: number
14 count?: number
15 sort?: string
16 isLocal?: boolean
17 search?: string
18 searchAccount?: string
19 searchVideo?: string
20 } = {}) {
21 const { sort = '-createdAt' } = options
22 const path = '/api/v1/videos/comments'
23
24 const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'search', 'searchAccount', 'searchVideo' ]) }
25
26 return this.getRequestBody<ResultList<VideoComment>>({
27 ...options,
28
29 path,
30 query,
31 implicitToken: true,
32 defaultExpectedStatus: HttpStatusCode.OK_200
33 })
34 }
35
36 listThreads (options: OverrideCommandOptions & {
37 videoId: number | string
38 start?: number
39 count?: number
40 sort?: string
41 }) {
42 const { start, count, sort, videoId } = options
43 const path = '/api/v1/videos/' + videoId + '/comment-threads'
44
45 return this.getRequestBody<VideoCommentThreads>({
46 ...options,
47
48 path,
49 query: { start, count, sort },
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.OK_200
52 })
53 }
54
55 getThread (options: OverrideCommandOptions & {
56 videoId: number | string
57 threadId: number
58 }) {
59 const { videoId, threadId } = options
60 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
61
62 return this.getRequestBody<VideoCommentThreadTree>({
63 ...options,
64
65 path,
66 implicitToken: false,
67 defaultExpectedStatus: HttpStatusCode.OK_200
68 })
69 }
70
71 async createThread (options: OverrideCommandOptions & {
72 videoId: number | string
73 text: string
74 }) {
75 const { videoId, text } = options
76 const path = '/api/v1/videos/' + videoId + '/comment-threads'
77
78 const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
79 ...options,
80
81 path,
82 fields: { text },
83 implicitToken: true,
84 defaultExpectedStatus: HttpStatusCode.OK_200
85 }))
86
87 this.lastThreadId = body.comment?.id
88 this.lastVideoId = videoId
89
90 return body.comment
91 }
92
93 async addReply (options: OverrideCommandOptions & {
94 videoId: number | string
95 toCommentId: number
96 text: string
97 }) {
98 const { videoId, toCommentId, text } = options
99 const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
100
101 const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
102 ...options,
103
104 path,
105 fields: { text },
106 implicitToken: true,
107 defaultExpectedStatus: HttpStatusCode.OK_200
108 }))
109
110 this.lastReplyId = body.comment?.id
111
112 return body.comment
113 }
114
115 async addReplyToLastReply (options: OverrideCommandOptions & {
116 text: string
117 }) {
118 return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
119 }
120
121 async addReplyToLastThread (options: OverrideCommandOptions & {
122 text: string
123 }) {
124 return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
125 }
126
127 async findCommentId (options: OverrideCommandOptions & {
128 videoId: number | string
129 text: string
130 }) {
131 const { videoId, text } = options
132 const { data } = await this.listThreads({ videoId, count: 25, sort: '-createdAt' })
133
134 return data.find(c => c.text === text).id
135 }
136
137 delete (options: OverrideCommandOptions & {
138 videoId: number | string
139 commentId: number
140 }) {
141 const { videoId, commentId } = options
142 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
143
144 return this.deleteRequest({
145 ...options,
146
147 path,
148 implicitToken: true,
149 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
150 })
151 }
152}
diff --git a/shared/extra-utils/videos/history-command.ts b/shared/extra-utils/videos/history-command.ts
new file mode 100644
index 000000000..13b7150c1
--- /dev/null
+++ b/shared/extra-utils/videos/history-command.ts
@@ -0,0 +1,58 @@
1import { HttpStatusCode, ResultList, Video } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class HistoryCommand extends AbstractCommand {
5
6 wathVideo (options: OverrideCommandOptions & {
7 videoId: number | string
8 currentTime: number
9 }) {
10 const { videoId, currentTime } = options
11
12 const path = '/api/v1/videos/' + videoId + '/watching'
13 const fields = { currentTime }
14
15 return this.putBodyRequest({
16 ...options,
17
18 path,
19 fields,
20 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
22 })
23 }
24
25 list (options: OverrideCommandOptions & {
26 search?: string
27 } = {}) {
28 const { search } = options
29 const path = '/api/v1/users/me/history/videos'
30
31 return this.getRequestBody<ResultList<Video>>({
32 ...options,
33
34 path,
35 query: {
36 search
37 },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.OK_200
40 })
41 }
42
43 remove (options: OverrideCommandOptions & {
44 beforeDate?: string
45 } = {}) {
46 const { beforeDate } = options
47 const path = '/api/v1/users/me/history/videos/remove'
48
49 return this.postBodyRequest({
50 ...options,
51
52 path,
53 fields: { beforeDate },
54 implicitToken: true,
55 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
56 })
57 }
58}
diff --git a/shared/extra-utils/videos/imports-command.ts b/shared/extra-utils/videos/imports-command.ts
new file mode 100644
index 000000000..e4944694d
--- /dev/null
+++ b/shared/extra-utils/videos/imports-command.ts
@@ -0,0 +1,47 @@
1
2import { HttpStatusCode, ResultList } from '@shared/models'
3import { VideoImport, VideoImportCreate } from '../../models/videos'
4import { unwrapBody } from '../requests'
5import { AbstractCommand, OverrideCommandOptions } from '../shared'
6
7export class ImportsCommand extends AbstractCommand {
8
9 importVideo (options: OverrideCommandOptions & {
10 attributes: VideoImportCreate & { torrentfile?: string }
11 }) {
12 const { attributes } = options
13 const path = '/api/v1/videos/imports'
14
15 let attaches: any = {}
16 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
17
18 return unwrapBody<VideoImport>(this.postUploadRequest({
19 ...options,
20
21 path,
22 attaches,
23 fields: options.attributes,
24 implicitToken: true,
25 defaultExpectedStatus: HttpStatusCode.OK_200
26 }))
27 }
28
29 getMyVideoImports (options: OverrideCommandOptions & {
30 sort?: string
31 } = {}) {
32 const { sort } = options
33 const path = '/api/v1/users/me/videos/imports'
34
35 const query = {}
36 if (sort) query['sort'] = sort
37
38 return this.getRequestBody<ResultList<VideoImport>>({
39 ...options,
40
41 path,
42 query: { sort },
43 implicitToken: true,
44 defaultExpectedStatus: HttpStatusCode.OK_200
45 })
46 }
47}
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
new file mode 100644
index 000000000..26e663f46
--- /dev/null
+++ b/shared/extra-utils/videos/index.ts
@@ -0,0 +1,19 @@
1export * from './blacklist-command'
2export * from './captions-command'
3export * from './captions'
4export * from './change-ownership-command'
5export * from './channels'
6export * from './channels-command'
7export * from './comments-command'
8export * from './history-command'
9export * from './imports-command'
10export * from './live-command'
11export * from './live'
12export * from './playlists-command'
13export * from './playlists'
14export * from './services-command'
15export * from './streaming-playlists-command'
16export * from './streaming-playlists'
17export * from './comments-command'
18export * from './videos-command'
19export * from './videos'
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts
new file mode 100644
index 000000000..bf9486a05
--- /dev/null
+++ b/shared/extra-utils/videos/live-command.ts
@@ -0,0 +1,154 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readdir } from 'fs-extra'
4import { omit } from 'lodash'
5import { join } from 'path'
6import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models'
7import { wait } from '../miscs'
8import { unwrapBody } from '../requests'
9import { AbstractCommand, OverrideCommandOptions } from '../shared'
10import { sendRTMPStream, testFfmpegStreamError } from './live'
11
12export class LiveCommand extends AbstractCommand {
13
14 get (options: OverrideCommandOptions & {
15 videoId: number | string
16 }) {
17 const path = '/api/v1/videos/live'
18
19 return this.getRequestBody<LiveVideo>({
20 ...options,
21
22 path: path + '/' + options.videoId,
23 implicitToken: true,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 update (options: OverrideCommandOptions & {
29 videoId: number | string
30 fields: LiveVideoUpdate
31 }) {
32 const { videoId, fields } = options
33 const path = '/api/v1/videos/live'
34
35 return this.putBodyRequest({
36 ...options,
37
38 path: path + '/' + videoId,
39 fields,
40 implicitToken: true,
41 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
42 })
43 }
44
45 async create (options: OverrideCommandOptions & {
46 fields: LiveVideoCreate
47 }) {
48 const { fields } = options
49 const path = '/api/v1/videos/live'
50
51 const attaches: any = {}
52 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
53 if (fields.previewfile) attaches.previewfile = fields.previewfile
54
55 const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
56 ...options,
57
58 path,
59 attaches,
60 fields: omit(fields, 'thumbnailfile', 'previewfile'),
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.OK_200
63 }))
64
65 return body.video
66 }
67
68 async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
69 videoId: number | string
70 fixtureName?: string
71 }) {
72 const { videoId, fixtureName } = options
73 const videoLive = await this.get({ videoId })
74
75 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
76 }
77
78 async runAndTestStreamError (options: OverrideCommandOptions & {
79 videoId: number | string
80 shouldHaveError: boolean
81 }) {
82 const command = await this.sendRTMPStreamInVideo(options)
83
84 return testFfmpegStreamError(command, options.shouldHaveError)
85 }
86
87 waitUntilPublished (options: OverrideCommandOptions & {
88 videoId: number | string
89 }) {
90 const { videoId } = options
91 return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
92 }
93
94 waitUntilWaiting (options: OverrideCommandOptions & {
95 videoId: number | string
96 }) {
97 const { videoId } = options
98 return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
99 }
100
101 waitUntilEnded (options: OverrideCommandOptions & {
102 videoId: number | string
103 }) {
104 const { videoId } = options
105 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
106 }
107
108 waitUntilSegmentGeneration (options: OverrideCommandOptions & {
109 videoUUID: string
110 resolution: number
111 segment: number
112 }) {
113 const { resolution, segment, videoUUID } = options
114 const segmentName = `${resolution}-00000${segment}.ts`
115
116 return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false)
117 }
118
119 async waitUntilSaved (options: OverrideCommandOptions & {
120 videoId: number | string
121 }) {
122 let video: VideoDetails
123
124 do {
125 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
126
127 await wait(500)
128 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
129 }
130
131 async countPlaylists (options: OverrideCommandOptions & {
132 videoUUID: string
133 }) {
134 const basePath = this.server.servers.buildDirectory('streaming-playlists')
135 const hlsPath = join(basePath, 'hls', options.videoUUID)
136
137 const files = await readdir(hlsPath)
138
139 return files.filter(f => f.endsWith('.m3u8')).length
140 }
141
142 private async waitUntilState (options: OverrideCommandOptions & {
143 videoId: number | string
144 state: VideoState
145 }) {
146 let video: VideoDetails
147
148 do {
149 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
150
151 await wait(500)
152 } while (video.state.id !== options.state)
153 }
154}
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts
index c0384769b..502964b1a 100644
--- a/shared/extra-utils/videos/live.ts
+++ b/shared/extra-utils/videos/live.ts
@@ -3,69 +3,9 @@
3import { expect } from 'chai' 3import { expect } from 'chai'
4import * as ffmpeg from 'fluent-ffmpeg' 4import * as ffmpeg from 'fluent-ffmpeg'
5import { pathExists, readdir } from 'fs-extra' 5import { pathExists, readdir } from 'fs-extra'
6import { omit } from 'lodash'
7import { join } from 'path' 6import { join } from 'path'
8import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' 7import { buildAbsoluteFixturePath, wait } from '../miscs'
9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 8import { PeerTubeServer } from '../server/server'
10import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs'
11import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
12import { ServerInfo, waitUntilLog } from '../server/servers'
13import { getVideoWithToken } from './videos'
14
15function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
16 const path = '/api/v1/videos/live'
17
18 return makeGetRequest({
19 url,
20 token,
21 path: path + '/' + videoId,
22 statusCodeExpected
23 })
24}
25
26function updateLive (
27 url: string,
28 token: string,
29 videoId: number | string,
30 fields: LiveVideoUpdate,
31 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
32) {
33 const path = '/api/v1/videos/live'
34
35 return makePutBodyRequest({
36 url,
37 token,
38 path: path + '/' + videoId,
39 fields,
40 statusCodeExpected
41 })
42}
43
44function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = HttpStatusCode.OK_200) {
45 const path = '/api/v1/videos/live'
46
47 const attaches: any = {}
48 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
49 if (fields.previewfile) attaches.previewfile = fields.previewfile
50
51 const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
52
53 return makeUploadRequest({
54 url,
55 path,
56 token,
57 attaches,
58 fields: updatedFields,
59 statusCodeExpected
60 })
61}
62
63async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, fixtureName?: string) {
64 const res = await getLive(url, token, videoId)
65 const videoLive = res.body as LiveVideo
66
67 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
68}
69 9
70function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') { 10function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') {
71 const fixture = buildAbsoluteFixturePath(fixtureName) 11 const fixture = buildAbsoluteFixturePath(fixtureName)
@@ -109,12 +49,6 @@ function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 1
109 }) 49 })
110} 50}
111 51
112async function runAndTestFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) {
113 const command = await sendRTMPStreamInVideo(url, token, videoId)
114
115 return testFfmpegStreamError(command, shouldHaveError)
116}
117
118async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) { 52async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) {
119 let error: Error 53 let error: Error
120 54
@@ -136,53 +70,14 @@ async function stopFfmpeg (command: ffmpeg.FfmpegCommand) {
136 await wait(500) 70 await wait(500)
137} 71}
138 72
139function waitUntilLivePublished (url: string, token: string, videoId: number | string) { 73async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
140 return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED)
141}
142
143function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) {
144 return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE)
145}
146
147function waitUntilLiveEnded (url: string, token: string, videoId: number | string) {
148 return waitUntilLiveState(url, token, videoId, VideoState.LIVE_ENDED)
149}
150
151function waitUntilLiveSegmentGeneration (server: ServerInfo, videoUUID: string, resolutionNum: number, segmentNum: number) {
152 const segmentName = `${resolutionNum}-00000${segmentNum}.ts`
153 return waitUntilLog(server, `${videoUUID}/${segmentName}`, 2, false)
154}
155
156async function waitUntilLiveState (url: string, token: string, videoId: number | string, state: VideoState) {
157 let video: VideoDetails
158
159 do {
160 const res = await getVideoWithToken(url, token, videoId)
161 video = res.body
162
163 await wait(500)
164 } while (video.state.id !== state)
165}
166
167async function waitUntilLiveSaved (url: string, token: string, videoId: number | string) {
168 let video: VideoDetails
169
170 do {
171 const res = await getVideoWithToken(url, token, videoId)
172 video = res.body
173
174 await wait(500)
175 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
176}
177
178async function waitUntilLivePublishedOnAllServers (servers: ServerInfo[], videoId: string) {
179 for (const server of servers) { 74 for (const server of servers) {
180 await waitUntilLivePublished(server.url, server.accessToken, videoId) 75 await server.live.waitUntilPublished({ videoId })
181 } 76 }
182} 77}
183 78
184async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) { 79async function checkLiveCleanup (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) {
185 const basePath = buildServerDirectory(server, 'streaming-playlists') 80 const basePath = server.servers.buildDirectory('streaming-playlists')
186 const hlsPath = join(basePath, 'hls', videoUUID) 81 const hlsPath = join(basePath, 'hls', videoUUID)
187 82
188 if (resolutions.length === 0) { 83 if (resolutions.length === 0) {
@@ -206,33 +101,11 @@ async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resoluti
206 expect(files).to.contain('segments-sha256.json') 101 expect(files).to.contain('segments-sha256.json')
207} 102}
208 103
209async function getPlaylistsCount (server: ServerInfo, videoUUID: string) {
210 const basePath = buildServerDirectory(server, 'streaming-playlists')
211 const hlsPath = join(basePath, 'hls', videoUUID)
212
213 const files = await readdir(hlsPath)
214
215 return files.filter(f => f.endsWith('.m3u8')).length
216}
217
218// ---------------------------------------------------------------------------
219
220export { 104export {
221 getLive, 105 sendRTMPStream,
222 getPlaylistsCount,
223 waitUntilLiveSaved,
224 waitUntilLivePublished,
225 updateLive,
226 createLive,
227 runAndTestFfmpegStreamError,
228 checkLiveCleanup,
229 waitUntilLiveSegmentGeneration,
230 stopFfmpeg,
231 waitUntilLiveWaiting,
232 sendRTMPStreamInVideo,
233 waitUntilLiveEnded,
234 waitFfmpegUntilError, 106 waitFfmpegUntilError,
107 testFfmpegStreamError,
108 stopFfmpeg,
235 waitUntilLivePublishedOnAllServers, 109 waitUntilLivePublishedOnAllServers,
236 sendRTMPStream, 110 checkLiveCleanup
237 testFfmpegStreamError
238} 111}
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
new file mode 100644
index 000000000..6f329800e
--- /dev/null
+++ b/shared/extra-utils/videos/playlists-command.ts
@@ -0,0 +1,279 @@
1import { omit, pick } from 'lodash'
2import {
3 BooleanBothQuery,
4 HttpStatusCode,
5 ResultList,
6 VideoExistInPlaylist,
7 VideoPlaylist,
8 VideoPlaylistCreate,
9 VideoPlaylistCreateResult,
10 VideoPlaylistElement,
11 VideoPlaylistElementCreate,
12 VideoPlaylistElementCreateResult,
13 VideoPlaylistElementUpdate,
14 VideoPlaylistReorder,
15 VideoPlaylistType,
16 VideoPlaylistUpdate
17} from '@shared/models'
18import { unwrapBody } from '../requests'
19import { AbstractCommand, OverrideCommandOptions } from '../shared'
20
21export class PlaylistsCommand extends AbstractCommand {
22
23 list (options: OverrideCommandOptions & {
24 start?: number
25 count?: number
26 sort?: string
27 }) {
28 const path = '/api/v1/video-playlists'
29 const query = pick(options, [ 'start', 'count', 'sort' ])
30
31 return this.getRequestBody<ResultList<VideoPlaylist>>({
32 ...options,
33
34 path,
35 query,
36 implicitToken: false,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 listByChannel (options: OverrideCommandOptions & {
42 handle: string
43 start?: number
44 count?: number
45 sort?: string
46 }) {
47 const path = '/api/v1/video-channels/' + options.handle + '/video-playlists'
48 const query = pick(options, [ 'start', 'count', 'sort' ])
49
50 return this.getRequestBody<ResultList<VideoPlaylist>>({
51 ...options,
52
53 path,
54 query,
55 implicitToken: false,
56 defaultExpectedStatus: HttpStatusCode.OK_200
57 })
58 }
59
60 listByAccount (options: OverrideCommandOptions & {
61 handle: string
62 start?: number
63 count?: number
64 sort?: string
65 search?: string
66 playlistType?: VideoPlaylistType
67 }) {
68 const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
69 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ])
70
71 return this.getRequestBody<ResultList<VideoPlaylist>>({
72 ...options,
73
74 path,
75 query,
76 implicitToken: false,
77 defaultExpectedStatus: HttpStatusCode.OK_200
78 })
79 }
80
81 get (options: OverrideCommandOptions & {
82 playlistId: number | string
83 }) {
84 const { playlistId } = options
85 const path = '/api/v1/video-playlists/' + playlistId
86
87 return this.getRequestBody<VideoPlaylist>({
88 ...options,
89
90 path,
91 implicitToken: false,
92 defaultExpectedStatus: HttpStatusCode.OK_200
93 })
94 }
95
96 listVideos (options: OverrideCommandOptions & {
97 playlistId: number | string
98 start?: number
99 count?: number
100 query?: { nsfw?: BooleanBothQuery }
101 }) {
102 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
103 const query = options.query ?? {}
104
105 return this.getRequestBody<ResultList<VideoPlaylistElement>>({
106 ...options,
107
108 path,
109 query: {
110 ...query,
111 start: options.start,
112 count: options.count
113 },
114 implicitToken: true,
115 defaultExpectedStatus: HttpStatusCode.OK_200
116 })
117 }
118
119 delete (options: OverrideCommandOptions & {
120 playlistId: number | string
121 }) {
122 const path = '/api/v1/video-playlists/' + options.playlistId
123
124 return this.deleteRequest({
125 ...options,
126
127 path,
128 implicitToken: true,
129 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
130 })
131 }
132
133 async create (options: OverrideCommandOptions & {
134 attributes: VideoPlaylistCreate
135 }) {
136 const path = '/api/v1/video-playlists'
137
138 const fields = omit(options.attributes, 'thumbnailfile')
139
140 const attaches = options.attributes.thumbnailfile
141 ? { thumbnailfile: options.attributes.thumbnailfile }
142 : {}
143
144 const body = await unwrapBody<{ videoPlaylist: VideoPlaylistCreateResult }>(this.postUploadRequest({
145 ...options,
146
147 path,
148 fields,
149 attaches,
150 implicitToken: true,
151 defaultExpectedStatus: HttpStatusCode.OK_200
152 }))
153
154 return body.videoPlaylist
155 }
156
157 update (options: OverrideCommandOptions & {
158 attributes: VideoPlaylistUpdate
159 playlistId: number | string
160 }) {
161 const path = '/api/v1/video-playlists/' + options.playlistId
162
163 const fields = omit(options.attributes, 'thumbnailfile')
164
165 const attaches = options.attributes.thumbnailfile
166 ? { thumbnailfile: options.attributes.thumbnailfile }
167 : {}
168
169 return this.putUploadRequest({
170 ...options,
171
172 path,
173 fields,
174 attaches,
175 implicitToken: true,
176 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
177 })
178 }
179
180 async addElement (options: OverrideCommandOptions & {
181 playlistId: number | string
182 attributes: VideoPlaylistElementCreate | { videoId: string }
183 }) {
184 const attributes = {
185 ...options.attributes,
186
187 videoId: await this.server.videos.getId({ ...options, uuid: options.attributes.videoId })
188 }
189
190 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
191
192 const body = await unwrapBody<{ videoPlaylistElement: VideoPlaylistElementCreateResult }>(this.postBodyRequest({
193 ...options,
194
195 path,
196 fields: attributes,
197 implicitToken: true,
198 defaultExpectedStatus: HttpStatusCode.OK_200
199 }))
200
201 return body.videoPlaylistElement
202 }
203
204 updateElement (options: OverrideCommandOptions & {
205 playlistId: number | string
206 elementId: number | string
207 attributes: VideoPlaylistElementUpdate
208 }) {
209 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
210
211 return this.putBodyRequest({
212 ...options,
213
214 path,
215 fields: options.attributes,
216 implicitToken: true,
217 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
218 })
219 }
220
221 removeElement (options: OverrideCommandOptions & {
222 playlistId: number | string
223 elementId: number
224 }) {
225 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
226
227 return this.deleteRequest({
228 ...options,
229
230 path,
231 implicitToken: true,
232 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
233 })
234 }
235
236 reorderElements (options: OverrideCommandOptions & {
237 playlistId: number | string
238 attributes: VideoPlaylistReorder
239 }) {
240 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
241
242 return this.postBodyRequest({
243 ...options,
244
245 path,
246 fields: options.attributes,
247 implicitToken: true,
248 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
249 })
250 }
251
252 getPrivacies (options: OverrideCommandOptions = {}) {
253 const path = '/api/v1/video-playlists/privacies'
254
255 return this.getRequestBody<{ [ id: number ]: string }>({
256 ...options,
257
258 path,
259 implicitToken: false,
260 defaultExpectedStatus: HttpStatusCode.OK_200
261 })
262 }
263
264 videosExist (options: OverrideCommandOptions & {
265 videoIds: number[]
266 }) {
267 const { videoIds } = options
268 const path = '/api/v1/users/me/video-playlists/videos-exist'
269
270 return this.getRequestBody<VideoExistInPlaylist>({
271 ...options,
272
273 path,
274 query: { videoIds },
275 implicitToken: true,
276 defaultExpectedStatus: HttpStatusCode.OK_200
277 })
278 }
279}
diff --git a/shared/extra-utils/videos/playlists.ts b/shared/extra-utils/videos/playlists.ts
new file mode 100644
index 000000000..3dde52bb9
--- /dev/null
+++ b/shared/extra-utils/videos/playlists.ts
@@ -0,0 +1,25 @@
1import { expect } from 'chai'
2import { readdir } from 'fs-extra'
3import { join } from 'path'
4import { root } from '../miscs'
5
6async function checkPlaylistFilesWereRemoved (
7 playlistUUID: string,
8 internalServerNumber: number,
9 directories = [ 'thumbnails' ]
10) {
11 const testDirectory = 'test' + internalServerNumber
12
13 for (const directory of directories) {
14 const directoryPath = join(root(), testDirectory, directory)
15
16 const files = await readdir(directoryPath)
17 for (const file of files) {
18 expect(file).to.not.contain(playlistUUID)
19 }
20 }
21}
22
23export {
24 checkPlaylistFilesWereRemoved
25}
diff --git a/shared/extra-utils/videos/services-command.ts b/shared/extra-utils/videos/services-command.ts
new file mode 100644
index 000000000..06760df42
--- /dev/null
+++ b/shared/extra-utils/videos/services-command.ts
@@ -0,0 +1,29 @@
1import { HttpStatusCode } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class ServicesCommand extends AbstractCommand {
5
6 getOEmbed (options: OverrideCommandOptions & {
7 oembedUrl: string
8 format?: string
9 maxHeight?: number
10 maxWidth?: number
11 }) {
12 const path = '/services/oembed'
13 const query = {
14 url: options.oembedUrl,
15 format: options.format,
16 maxheight: options.maxHeight,
17 maxwidth: options.maxWidth
18 }
19
20 return this.getRequest({
21 ...options,
22
23 path,
24 query,
25 implicitToken: false,
26 defaultExpectedStatus: HttpStatusCode.OK_200
27 })
28 }
29}
diff --git a/shared/extra-utils/videos/services.ts b/shared/extra-utils/videos/services.ts
deleted file mode 100644
index e13a788bd..000000000
--- a/shared/extra-utils/videos/services.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
5 const path = '/services/oembed'
6 const query = {
7 url: oembedUrl,
8 format,
9 maxheight: maxHeight,
10 maxwidth: maxWidth
11 }
12
13 return request(url)
14 .get(path)
15 .query(query)
16 .set('Accept', 'application/json')
17 .expect(HttpStatusCode.OK_200)
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 getOEmbed
24}
diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts
new file mode 100644
index 000000000..9662685da
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists-command.ts
@@ -0,0 +1,44 @@
1import { HttpStatusCode } from '@shared/models'
2import { unwrapBody, unwrapText } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class StreamingPlaylistsCommand extends AbstractCommand {
6
7 get (options: OverrideCommandOptions & {
8 url: string
9 }) {
10 return unwrapText(this.getRawRequest({
11 ...options,
12
13 url: options.url,
14 implicitToken: false,
15 defaultExpectedStatus: HttpStatusCode.OK_200
16 }))
17 }
18
19 getSegment (options: OverrideCommandOptions & {
20 url: string
21 range?: string
22 }) {
23 return unwrapBody<Buffer>(this.getRawRequest({
24 ...options,
25
26 url: options.url,
27 range: options.range,
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 }))
31 }
32
33 getSegmentSha256 (options: OverrideCommandOptions & {
34 url: string
35 }) {
36 return unwrapBody<{ [ id: string ]: string }>(this.getRawRequest({
37 ...options,
38
39 url: options.url,
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 }))
43 }
44}
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts
new file mode 100644
index 000000000..1ae3fefc1
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists.ts
@@ -0,0 +1,75 @@
1import { expect } from 'chai'
2import { sha256 } from '@server/helpers/core-utils'
3import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models'
4import { PeerTubeServer } from '../server'
5
6async function checkSegmentHash (options: {
7 server: PeerTubeServer
8 baseUrlPlaylist: string
9 baseUrlSegment: string
10 videoUUID: string
11 resolution: number
12 hlsPlaylist: VideoStreamingPlaylist
13}) {
14 const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options
15 const command = server.streamingPlaylists
16
17 const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8` })
18
19 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
20
21 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
22
23 const length = parseInt(matches[1], 10)
24 const offset = parseInt(matches[2], 10)
25 const range = `${offset}-${offset + length - 1}`
26
27 const segmentBody = await command.getSegment({
28 url: `${baseUrlSegment}/${videoUUID}/${videoName}`,
29 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
30 range: `bytes=${range}`
31 })
32
33 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
34 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
35}
36
37async function checkLiveSegmentHash (options: {
38 server: PeerTubeServer
39 baseUrlSegment: string
40 videoUUID: string
41 segmentName: string
42 hlsPlaylist: VideoStreamingPlaylist
43}) {
44 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
45 const command = server.streamingPlaylists
46
47 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
48 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
49
50 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
51}
52
53async function checkResolutionsInMasterPlaylist (options: {
54 server: PeerTubeServer
55 playlistUrl: string
56 resolutions: number[]
57}) {
58 const { server, playlistUrl, resolutions } = options
59
60 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
61
62 for (const resolution of resolutions) {
63 const reg = new RegExp(
64 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
65 )
66
67 expect(masterPlaylist).to.match(reg)
68 }
69}
70
71export {
72 checkSegmentHash,
73 checkLiveSegmentHash,
74 checkResolutionsInMasterPlaylist
75}
diff --git a/shared/extra-utils/videos/video-blacklist.ts b/shared/extra-utils/videos/video-blacklist.ts
deleted file mode 100644
index aa1548537..000000000
--- a/shared/extra-utils/videos/video-blacklist.ts
+++ /dev/null
@@ -1,79 +0,0 @@
1import * as request from 'supertest'
2import { VideoBlacklistType } from '../../models/videos'
3import { makeGetRequest } from '..'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function addVideoToBlacklist (
7 url: string,
8 token: string,
9 videoId: number | string,
10 reason?: string,
11 unfederate?: boolean,
12 specialStatus = HttpStatusCode.NO_CONTENT_204
13) {
14 const path = '/api/v1/videos/' + videoId + '/blacklist'
15
16 return request(url)
17 .post(path)
18 .send({ reason, unfederate })
19 .set('Accept', 'application/json')
20 .set('Authorization', 'Bearer ' + token)
21 .expect(specialStatus)
22}
23
24function updateVideoBlacklist (
25 url: string,
26 token: string,
27 videoId: number,
28 reason?: string,
29 specialStatus = HttpStatusCode.NO_CONTENT_204
30) {
31 const path = '/api/v1/videos/' + videoId + '/blacklist'
32
33 return request(url)
34 .put(path)
35 .send({ reason })
36 .set('Accept', 'application/json')
37 .set('Authorization', 'Bearer ' + token)
38 .expect(specialStatus)
39}
40
41function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
42 const path = '/api/v1/videos/' + videoId + '/blacklist'
43
44 return request(url)
45 .delete(path)
46 .set('Accept', 'application/json')
47 .set('Authorization', 'Bearer ' + token)
48 .expect(specialStatus)
49}
50
51function getBlacklistedVideosList (parameters: {
52 url: string
53 token: string
54 sort?: string
55 type?: VideoBlacklistType
56 specialStatus?: HttpStatusCode
57}) {
58 const { url, token, sort, type, specialStatus = HttpStatusCode.OK_200 } = parameters
59 const path = '/api/v1/videos/blacklist/'
60
61 const query = { sort, type }
62
63 return makeGetRequest({
64 url,
65 path,
66 query,
67 token,
68 statusCodeExpected: specialStatus
69 })
70}
71
72// ---------------------------------------------------------------------------
73
74export {
75 addVideoToBlacklist,
76 removeVideoFromBlacklist,
77 getBlacklistedVideosList,
78 updateVideoBlacklist
79}
diff --git a/shared/extra-utils/videos/video-captions.ts b/shared/extra-utils/videos/video-captions.ts
deleted file mode 100644
index 62eec7b90..000000000
--- a/shared/extra-utils/videos/video-captions.ts
+++ /dev/null
@@ -1,72 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
2import * as request from 'supertest'
3import * as chai from 'chai'
4import { buildAbsoluteFixturePath } from '../miscs/miscs'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7const expect = chai.expect
8
9function createVideoCaption (args: {
10 url: string
11 accessToken: string
12 videoId: string | number
13 language: string
14 fixture: string
15 mimeType?: string
16 statusCodeExpected?: number
17}) {
18 const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language
19
20 const captionfile = buildAbsoluteFixturePath(args.fixture)
21 const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile
22
23 return makeUploadRequest({
24 method: 'PUT',
25 url: args.url,
26 path,
27 token: args.accessToken,
28 fields: {},
29 attaches: {
30 captionfile: captionfileAttach
31 },
32 statusCodeExpected: args.statusCodeExpected || HttpStatusCode.NO_CONTENT_204
33 })
34}
35
36function listVideoCaptions (url: string, videoId: string | number) {
37 const path = '/api/v1/videos/' + videoId + '/captions'
38
39 return makeGetRequest({
40 url,
41 path,
42 statusCodeExpected: HttpStatusCode.OK_200
43 })
44}
45
46function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) {
47 const path = '/api/v1/videos/' + videoId + '/captions/' + language
48
49 return makeDeleteRequest({
50 url,
51 token,
52 path,
53 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
54 })
55}
56
57async function testCaptionFile (url: string, captionPath: string, containsString: string) {
58 const res = await request(url)
59 .get(captionPath)
60 .expect(HttpStatusCode.OK_200)
61
62 expect(res.text).to.contain(containsString)
63}
64
65// ---------------------------------------------------------------------------
66
67export {
68 createVideoCaption,
69 listVideoCaptions,
70 testCaptionFile,
71 deleteVideoCaption
72}
diff --git a/shared/extra-utils/videos/video-change-ownership.ts b/shared/extra-utils/videos/video-change-ownership.ts
deleted file mode 100644
index ef82a7636..000000000
--- a/shared/extra-utils/videos/video-change-ownership.ts
+++ /dev/null
@@ -1,72 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function changeVideoOwnership (
5 url: string,
6 token: string,
7 videoId: number | string,
8 username,
9 expectedStatus = HttpStatusCode.NO_CONTENT_204
10) {
11 const path = '/api/v1/videos/' + videoId + '/give-ownership'
12
13 return request(url)
14 .post(path)
15 .set('Accept', 'application/json')
16 .set('Authorization', 'Bearer ' + token)
17 .send({ username })
18 .expect(expectedStatus)
19}
20
21function getVideoChangeOwnershipList (url: string, token: string) {
22 const path = '/api/v1/videos/ownership'
23
24 return request(url)
25 .get(path)
26 .query({ sort: '-createdAt' })
27 .set('Accept', 'application/json')
28 .set('Authorization', 'Bearer ' + token)
29 .expect(HttpStatusCode.OK_200)
30 .expect('Content-Type', /json/)
31}
32
33function acceptChangeOwnership (
34 url: string,
35 token: string,
36 ownershipId: string,
37 channelId: number,
38 expectedStatus = HttpStatusCode.NO_CONTENT_204
39) {
40 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
41
42 return request(url)
43 .post(path)
44 .set('Accept', 'application/json')
45 .set('Authorization', 'Bearer ' + token)
46 .send({ channelId })
47 .expect(expectedStatus)
48}
49
50function refuseChangeOwnership (
51 url: string,
52 token: string,
53 ownershipId: string,
54 expectedStatus = HttpStatusCode.NO_CONTENT_204
55) {
56 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
57
58 return request(url)
59 .post(path)
60 .set('Accept', 'application/json')
61 .set('Authorization', 'Bearer ' + token)
62 .expect(expectedStatus)
63}
64
65// ---------------------------------------------------------------------------
66
67export {
68 changeVideoOwnership,
69 getVideoChangeOwnershipList,
70 acceptChangeOwnership,
71 refuseChangeOwnership
72}
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts
deleted file mode 100644
index 0aab93e52..000000000
--- a/shared/extra-utils/videos/video-channels.ts
+++ /dev/null
@@ -1,192 +0,0 @@
1/* eslint-disable @typescript-eslint/no-floating-promises */
2
3import * as request from 'supertest'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
6import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests'
7import { ServerInfo } from '../server/servers'
8import { MyUser, User } from '../../models/users/user.model'
9import { getMyUserInformation } from '../users/users'
10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
11
12function getVideoChannelsList (url: string, start: number, count: number, sort?: string, withStats?: boolean) {
13 const path = '/api/v1/video-channels'
14
15 const req = request(url)
16 .get(path)
17 .query({ start: start })
18 .query({ count: count })
19
20 if (sort) req.query({ sort })
21 if (withStats) req.query({ withStats })
22
23 return req.set('Accept', 'application/json')
24 .expect(HttpStatusCode.OK_200)
25 .expect('Content-Type', /json/)
26}
27
28function getAccountVideoChannelsList (parameters: {
29 url: string
30 accountName: string
31 start?: number
32 count?: number
33 sort?: string
34 specialStatus?: HttpStatusCode
35 withStats?: boolean
36 search?: string
37}) {
38 const {
39 url,
40 accountName,
41 start,
42 count,
43 sort = 'createdAt',
44 specialStatus = HttpStatusCode.OK_200,
45 withStats = false,
46 search
47 } = parameters
48
49 const path = '/api/v1/accounts/' + accountName + '/video-channels'
50
51 return makeGetRequest({
52 url,
53 path,
54 query: {
55 start,
56 count,
57 sort,
58 withStats,
59 search
60 },
61 statusCodeExpected: specialStatus
62 })
63}
64
65function addVideoChannel (
66 url: string,
67 token: string,
68 videoChannelAttributesArg: VideoChannelCreate,
69 expectedStatus = HttpStatusCode.OK_200
70) {
71 const path = '/api/v1/video-channels/'
72
73 // Default attributes
74 let attributes = {
75 displayName: 'my super video channel',
76 description: 'my super channel description',
77 support: 'my super channel support'
78 }
79 attributes = Object.assign(attributes, videoChannelAttributesArg)
80
81 return request(url)
82 .post(path)
83 .send(attributes)
84 .set('Accept', 'application/json')
85 .set('Authorization', 'Bearer ' + token)
86 .expect(expectedStatus)
87}
88
89function updateVideoChannel (
90 url: string,
91 token: string,
92 channelName: string,
93 attributes: VideoChannelUpdate,
94 expectedStatus = HttpStatusCode.NO_CONTENT_204
95) {
96 const body: any = {}
97 const path = '/api/v1/video-channels/' + channelName
98
99 if (attributes.displayName) body.displayName = attributes.displayName
100 if (attributes.description) body.description = attributes.description
101 if (attributes.support) body.support = attributes.support
102 if (attributes.bulkVideosSupportUpdate) body.bulkVideosSupportUpdate = attributes.bulkVideosSupportUpdate
103
104 return request(url)
105 .put(path)
106 .send(body)
107 .set('Accept', 'application/json')
108 .set('Authorization', 'Bearer ' + token)
109 .expect(expectedStatus)
110}
111
112function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
113 const path = '/api/v1/video-channels/' + channelName
114
115 return request(url)
116 .delete(path)
117 .set('Accept', 'application/json')
118 .set('Authorization', 'Bearer ' + token)
119 .expect(expectedStatus)
120}
121
122function getVideoChannel (url: string, channelName: string) {
123 const path = '/api/v1/video-channels/' + channelName
124
125 return request(url)
126 .get(path)
127 .set('Accept', 'application/json')
128 .expect(HttpStatusCode.OK_200)
129 .expect('Content-Type', /json/)
130}
131
132function updateVideoChannelImage (options: {
133 url: string
134 accessToken: string
135 fixture: string
136 videoChannelName: string | number
137 type: 'avatar' | 'banner'
138}) {
139 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}/pick`
140
141 return updateImageRequest({ ...options, path, fieldname: options.type + 'file' })
142}
143
144function deleteVideoChannelImage (options: {
145 url: string
146 accessToken: string
147 videoChannelName: string | number
148 type: 'avatar' | 'banner'
149}) {
150 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}`
151
152 return makeDeleteRequest({
153 url: options.url,
154 token: options.accessToken,
155 path,
156 statusCodeExpected: 204
157 })
158}
159
160function setDefaultVideoChannel (servers: ServerInfo[]) {
161 const tasks: Promise<any>[] = []
162
163 for (const server of servers) {
164 const p = getMyUserInformation(server.url, server.accessToken)
165 .then(res => { server.videoChannel = (res.body as User).videoChannels[0] })
166
167 tasks.push(p)
168 }
169
170 return Promise.all(tasks)
171}
172
173async function getDefaultVideoChannel (url: string, token: string) {
174 const res = await getMyUserInformation(url, token)
175
176 return (res.body as MyUser).videoChannels[0].id
177}
178
179// ---------------------------------------------------------------------------
180
181export {
182 updateVideoChannelImage,
183 getVideoChannelsList,
184 getAccountVideoChannelsList,
185 addVideoChannel,
186 updateVideoChannel,
187 deleteVideoChannel,
188 getVideoChannel,
189 setDefaultVideoChannel,
190 deleteVideoChannelImage,
191 getDefaultVideoChannel
192}
diff --git a/shared/extra-utils/videos/video-comments.ts b/shared/extra-utils/videos/video-comments.ts
deleted file mode 100644
index 71b9f875a..000000000
--- a/shared/extra-utils/videos/video-comments.ts
+++ /dev/null
@@ -1,138 +0,0 @@
1/* eslint-disable @typescript-eslint/no-floating-promises */
2
3import * as request from 'supertest'
4import { makeDeleteRequest, makeGetRequest } from '../requests/requests'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7function getAdminVideoComments (options: {
8 url: string
9 token: string
10 start: number
11 count: number
12 sort?: string
13 isLocal?: boolean
14 search?: string
15 searchAccount?: string
16 searchVideo?: string
17}) {
18 const { url, token, start, count, sort, isLocal, search, searchAccount, searchVideo } = options
19 const path = '/api/v1/videos/comments'
20
21 const query = {
22 start,
23 count,
24 sort: sort || '-createdAt'
25 }
26
27 if (isLocal !== undefined) Object.assign(query, { isLocal })
28 if (search !== undefined) Object.assign(query, { search })
29 if (searchAccount !== undefined) Object.assign(query, { searchAccount })
30 if (searchVideo !== undefined) Object.assign(query, { searchVideo })
31
32 return makeGetRequest({
33 url,
34 path,
35 token,
36 query,
37 statusCodeExpected: HttpStatusCode.OK_200
38 })
39}
40
41function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
42 const path = '/api/v1/videos/' + videoId + '/comment-threads'
43
44 const req = request(url)
45 .get(path)
46 .query({ start: start })
47 .query({ count: count })
48
49 if (sort) req.query({ sort })
50 if (token) req.set('Authorization', 'Bearer ' + token)
51
52 return req.set('Accept', 'application/json')
53 .expect(HttpStatusCode.OK_200)
54 .expect('Content-Type', /json/)
55}
56
57function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
58 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
59
60 const req = request(url)
61 .get(path)
62 .set('Accept', 'application/json')
63
64 if (token) req.set('Authorization', 'Bearer ' + token)
65
66 return req.expect(HttpStatusCode.OK_200)
67 .expect('Content-Type', /json/)
68}
69
70function addVideoCommentThread (
71 url: string,
72 token: string,
73 videoId: number | string,
74 text: string,
75 expectedStatus = HttpStatusCode.OK_200
76) {
77 const path = '/api/v1/videos/' + videoId + '/comment-threads'
78
79 return request(url)
80 .post(path)
81 .send({ text })
82 .set('Accept', 'application/json')
83 .set('Authorization', 'Bearer ' + token)
84 .expect(expectedStatus)
85}
86
87function addVideoCommentReply (
88 url: string,
89 token: string,
90 videoId: number | string,
91 inReplyToCommentId: number,
92 text: string,
93 expectedStatus = HttpStatusCode.OK_200
94) {
95 const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
96
97 return request(url)
98 .post(path)
99 .send({ text })
100 .set('Accept', 'application/json')
101 .set('Authorization', 'Bearer ' + token)
102 .expect(expectedStatus)
103}
104
105async function findCommentId (url: string, videoId: number | string, text: string) {
106 const res = await getVideoCommentThreads(url, videoId, 0, 25, '-createdAt')
107
108 return res.body.data.find(c => c.text === text).id as number
109}
110
111function deleteVideoComment (
112 url: string,
113 token: string,
114 videoId: number | string,
115 commentId: number,
116 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
117) {
118 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
119
120 return makeDeleteRequest({
121 url,
122 path,
123 token,
124 statusCodeExpected
125 })
126}
127
128// ---------------------------------------------------------------------------
129
130export {
131 getVideoCommentThreads,
132 getAdminVideoComments,
133 getVideoThreadComments,
134 addVideoCommentThread,
135 addVideoCommentReply,
136 findCommentId,
137 deleteVideoComment
138}
diff --git a/shared/extra-utils/videos/video-history.ts b/shared/extra-utils/videos/video-history.ts
deleted file mode 100644
index b989e14dc..000000000
--- a/shared/extra-utils/videos/video-history.ts
+++ /dev/null
@@ -1,49 +0,0 @@
1import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function userWatchVideo (
5 url: string,
6 token: string,
7 videoId: number | string,
8 currentTime: number,
9 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
10) {
11 const path = '/api/v1/videos/' + videoId + '/watching'
12 const fields = { currentTime }
13
14 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
15}
16
17function listMyVideosHistory (url: string, token: string, search?: string) {
18 const path = '/api/v1/users/me/history/videos'
19
20 return makeGetRequest({
21 url,
22 path,
23 token,
24 query: {
25 search
26 },
27 statusCodeExpected: HttpStatusCode.OK_200
28 })
29}
30
31function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
32 const path = '/api/v1/users/me/history/videos/remove'
33
34 return makePostBodyRequest({
35 url,
36 path,
37 token,
38 fields: beforeDate ? { beforeDate } : {},
39 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
40 })
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 userWatchVideo,
47 listMyVideosHistory,
48 removeMyVideosHistory
49}
diff --git a/shared/extra-utils/videos/video-imports.ts b/shared/extra-utils/videos/video-imports.ts
deleted file mode 100644
index 81c0163cb..000000000
--- a/shared/extra-utils/videos/video-imports.ts
+++ /dev/null
@@ -1,90 +0,0 @@
1
2import { VideoImportCreate } from '../../models/videos'
3import { makeGetRequest, makeUploadRequest } from '../requests/requests'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function getYoutubeVideoUrl () {
7 return 'https://www.youtube.com/watch?v=msX3jv1XdvM'
8}
9
10function getYoutubeHDRVideoUrl () {
11 /**
12 * The video is used to check format-selection correctness wrt. HDR,
13 * which brings its own set of oddities outside of a MediaSource.
14 * FIXME: refactor once HDR is supported at playback
15 *
16 * The video needs to have the following format_ids:
17 * (which you can check by using `youtube-dl <url> -F`):
18 * - 303 (1080p webm vp9)
19 * - 299 (1080p mp4 avc1)
20 * - 335 (1080p webm vp9.2 HDR)
21 *
22 * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING
23 * - 400 (1080p mp4 av01)
24 * - 315 (2160p webm vp9 HDR)
25 * - 337 (2160p webm vp9.2 HDR)
26 * - 401 (2160p mp4 av01 HDR)
27 */
28 return 'https://www.youtube.com/watch?v=qR5vOXbZsI4'
29}
30
31function getMagnetURI () {
32 // eslint-disable-next-line max-len
33 return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4'
34}
35
36function getBadVideoUrl () {
37 return 'https://download.cpy.re/peertube/bad_video.mp4'
38}
39
40function getGoodVideoUrl () {
41 return 'https://download.cpy.re/peertube/good_video.mp4'
42}
43
44function importVideo (
45 url: string,
46 token: string,
47 attributes: VideoImportCreate & { torrentfile?: string },
48 statusCodeExpected = HttpStatusCode.OK_200
49) {
50 const path = '/api/v1/videos/imports'
51
52 let attaches: any = {}
53 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
54
55 return makeUploadRequest({
56 url,
57 path,
58 token,
59 attaches,
60 fields: attributes,
61 statusCodeExpected
62 })
63}
64
65function getMyVideoImports (url: string, token: string, sort?: string) {
66 const path = '/api/v1/users/me/videos/imports'
67
68 const query = {}
69 if (sort) query['sort'] = sort
70
71 return makeGetRequest({
72 url,
73 query,
74 path,
75 token,
76 statusCodeExpected: HttpStatusCode.OK_200
77 })
78}
79
80// ---------------------------------------------------------------------------
81
82export {
83 getBadVideoUrl,
84 getYoutubeVideoUrl,
85 getYoutubeHDRVideoUrl,
86 importVideo,
87 getMagnetURI,
88 getMyVideoImports,
89 getGoodVideoUrl
90}
diff --git a/shared/extra-utils/videos/video-playlists.ts b/shared/extra-utils/videos/video-playlists.ts
deleted file mode 100644
index c6f799e5d..000000000
--- a/shared/extra-utils/videos/video-playlists.ts
+++ /dev/null
@@ -1,320 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
2import { VideoPlaylistCreate } from '../../models/videos/playlist/video-playlist-create.model'
3import { omit } from 'lodash'
4import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
5import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
6import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
7import { videoUUIDToId } from './videos'
8import { join } from 'path'
9import { root } from '..'
10import { readdir } from 'fs-extra'
11import { expect } from 'chai'
12import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
13import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
14
15function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
16 const path = '/api/v1/video-playlists'
17
18 const query = {
19 start,
20 count,
21 sort
22 }
23
24 return makeGetRequest({
25 url,
26 path,
27 query,
28 statusCodeExpected: HttpStatusCode.OK_200
29 })
30}
31
32function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
33 const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
34
35 const query = {
36 start,
37 count,
38 sort
39 }
40
41 return makeGetRequest({
42 url,
43 path,
44 query,
45 statusCodeExpected: HttpStatusCode.OK_200
46 })
47}
48
49function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string, search?: string) {
50 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
51
52 const query = {
53 start,
54 count,
55 sort,
56 search
57 }
58
59 return makeGetRequest({
60 url,
61 path,
62 query,
63 statusCodeExpected: HttpStatusCode.OK_200
64 })
65}
66
67function getAccountPlaylistsListWithToken (
68 url: string,
69 token: string,
70 accountName: string,
71 start: number,
72 count: number,
73 playlistType?: VideoPlaylistType,
74 sort?: string
75) {
76 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
77
78 const query = {
79 start,
80 count,
81 playlistType,
82 sort
83 }
84
85 return makeGetRequest({
86 url,
87 token,
88 path,
89 query,
90 statusCodeExpected: HttpStatusCode.OK_200
91 })
92}
93
94function getVideoPlaylist (url: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
95 const path = '/api/v1/video-playlists/' + playlistId
96
97 return makeGetRequest({
98 url,
99 path,
100 statusCodeExpected
101 })
102}
103
104function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
105 const path = '/api/v1/video-playlists/' + playlistId
106
107 return makeGetRequest({
108 url,
109 token,
110 path,
111 statusCodeExpected
112 })
113}
114
115function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
116 const path = '/api/v1/video-playlists/' + playlistId
117
118 return makeDeleteRequest({
119 url,
120 path,
121 token,
122 statusCodeExpected
123 })
124}
125
126function createVideoPlaylist (options: {
127 url: string
128 token: string
129 playlistAttrs: VideoPlaylistCreate
130 expectedStatus?: number
131}) {
132 const path = '/api/v1/video-playlists'
133
134 const fields = omit(options.playlistAttrs, 'thumbnailfile')
135
136 const attaches = options.playlistAttrs.thumbnailfile
137 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
138 : {}
139
140 return makeUploadRequest({
141 method: 'POST',
142 url: options.url,
143 path,
144 token: options.token,
145 fields,
146 attaches,
147 statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
148 })
149}
150
151function updateVideoPlaylist (options: {
152 url: string
153 token: string
154 playlistAttrs: VideoPlaylistUpdate
155 playlistId: number | string
156 expectedStatus?: number
157}) {
158 const path = '/api/v1/video-playlists/' + options.playlistId
159
160 const fields = omit(options.playlistAttrs, 'thumbnailfile')
161
162 const attaches = options.playlistAttrs.thumbnailfile
163 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
164 : {}
165
166 return makeUploadRequest({
167 method: 'PUT',
168 url: options.url,
169 path,
170 token: options.token,
171 fields,
172 attaches,
173 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
174 })
175}
176
177async function addVideoInPlaylist (options: {
178 url: string
179 token: string
180 playlistId: number | string
181 elementAttrs: VideoPlaylistElementCreate | { videoId: string }
182 expectedStatus?: number
183}) {
184 options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
185
186 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
187
188 return makePostBodyRequest({
189 url: options.url,
190 path,
191 token: options.token,
192 fields: options.elementAttrs,
193 statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
194 })
195}
196
197function updateVideoPlaylistElement (options: {
198 url: string
199 token: string
200 playlistId: number | string
201 playlistElementId: number | string
202 elementAttrs: VideoPlaylistElementUpdate
203 expectedStatus?: number
204}) {
205 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
206
207 return makePutBodyRequest({
208 url: options.url,
209 path,
210 token: options.token,
211 fields: options.elementAttrs,
212 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
213 })
214}
215
216function removeVideoFromPlaylist (options: {
217 url: string
218 token: string
219 playlistId: number | string
220 playlistElementId: number
221 expectedStatus?: number
222}) {
223 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
224
225 return makeDeleteRequest({
226 url: options.url,
227 path,
228 token: options.token,
229 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
230 })
231}
232
233function reorderVideosPlaylist (options: {
234 url: string
235 token: string
236 playlistId: number | string
237 elementAttrs: {
238 startPosition: number
239 insertAfterPosition: number
240 reorderLength?: number
241 }
242 expectedStatus?: number
243}) {
244 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
245
246 return makePostBodyRequest({
247 url: options.url,
248 path,
249 token: options.token,
250 fields: options.elementAttrs,
251 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
252 })
253}
254
255async function checkPlaylistFilesWereRemoved (
256 playlistUUID: string,
257 internalServerNumber: number,
258 directories = [ 'thumbnails' ]
259) {
260 const testDirectory = 'test' + internalServerNumber
261
262 for (const directory of directories) {
263 const directoryPath = join(root(), testDirectory, directory)
264
265 const files = await readdir(directoryPath)
266 for (const file of files) {
267 expect(file).to.not.contain(playlistUUID)
268 }
269 }
270}
271
272function getVideoPlaylistPrivacies (url: string) {
273 const path = '/api/v1/video-playlists/privacies'
274
275 return makeGetRequest({
276 url,
277 path,
278 statusCodeExpected: HttpStatusCode.OK_200
279 })
280}
281
282function doVideosExistInMyPlaylist (url: string, token: string, videoIds: number[]) {
283 const path = '/api/v1/users/me/video-playlists/videos-exist'
284
285 return makeGetRequest({
286 url,
287 token,
288 path,
289 query: { videoIds },
290 statusCodeExpected: HttpStatusCode.OK_200
291 })
292}
293
294// ---------------------------------------------------------------------------
295
296export {
297 getVideoPlaylistPrivacies,
298
299 getVideoPlaylistsList,
300 getVideoChannelPlaylistsList,
301 getAccountPlaylistsList,
302 getAccountPlaylistsListWithToken,
303
304 getVideoPlaylist,
305 getVideoPlaylistWithToken,
306
307 createVideoPlaylist,
308 updateVideoPlaylist,
309 deleteVideoPlaylist,
310
311 addVideoInPlaylist,
312 updateVideoPlaylistElement,
313 removeVideoFromPlaylist,
314
315 reorderVideosPlaylist,
316
317 checkPlaylistFilesWereRemoved,
318
319 doVideosExistInMyPlaylist
320}
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
deleted file mode 100644
index 99c2e1880..000000000
--- a/shared/extra-utils/videos/video-streaming-playlists.ts
+++ /dev/null
@@ -1,82 +0,0 @@
1import { makeRawRequest } from '../requests/requests'
2import { sha256 } from '../../../server/helpers/core-utils'
3import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
4import { expect } from 'chai'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7function getPlaylist (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
8 return makeRawRequest(url, statusCodeExpected)
9}
10
11function getSegment (url: string, statusCodeExpected = HttpStatusCode.OK_200, range?: string) {
12 return makeRawRequest(url, statusCodeExpected, range)
13}
14
15function getSegmentSha256 (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
16 return makeRawRequest(url, statusCodeExpected)
17}
18
19async function checkSegmentHash (
20 baseUrlPlaylist: string,
21 baseUrlSegment: string,
22 videoUUID: string,
23 resolution: number,
24 hlsPlaylist: VideoStreamingPlaylist
25) {
26 const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
27 const playlist = res.text
28
29 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
30
31 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
32
33 const length = parseInt(matches[1], 10)
34 const offset = parseInt(matches[2], 10)
35 const range = `${offset}-${offset + length - 1}`
36
37 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, HttpStatusCode.PARTIAL_CONTENT_206, `bytes=${range}`)
38
39 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
40
41 const sha256Server = resSha.body[videoName][range]
42 expect(sha256(res2.body)).to.equal(sha256Server)
43}
44
45async function checkLiveSegmentHash (
46 baseUrlSegment: string,
47 videoUUID: string,
48 segmentName: string,
49 hlsPlaylist: VideoStreamingPlaylist
50) {
51 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${segmentName}`)
52
53 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
54
55 const sha256Server = resSha.body[segmentName]
56 expect(sha256(res2.body)).to.equal(sha256Server)
57}
58
59async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
60 const res = await getPlaylist(playlistUrl)
61
62 const masterPlaylist = res.text
63
64 for (const resolution of resolutions) {
65 const reg = new RegExp(
66 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
67 )
68
69 expect(masterPlaylist).to.match(reg)
70 }
71}
72
73// ---------------------------------------------------------------------------
74
75export {
76 getPlaylist,
77 getSegment,
78 checkResolutionsInMasterPlaylist,
79 getSegmentSha256,
80 checkLiveSegmentHash,
81 checkSegmentHash
82}
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
new file mode 100644
index 000000000..98465e8f6
--- /dev/null
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -0,0 +1,598 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { createReadStream, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got'
6import { omit, pick } from 'lodash'
7import validator from 'validator'
8import { buildUUID } from '@server/helpers/uuid'
9import { loadLanguages } from '@server/initializers/constants'
10import {
11 HttpStatusCode,
12 ResultList,
13 UserVideoRateType,
14 Video,
15 VideoCreate,
16 VideoCreateResult,
17 VideoDetails,
18 VideoFileMetadata,
19 VideoPrivacy,
20 VideosCommonQuery,
21 VideosWithSearchCommonQuery
22} from '@shared/models'
23import { buildAbsoluteFixturePath, wait } from '../miscs'
24import { unwrapBody } from '../requests'
25import { PeerTubeServer, waitJobs } from '../server'
26import { AbstractCommand, OverrideCommandOptions } from '../shared'
27
28export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
29 fixture?: string
30 thumbnailfile?: string
31 previewfile?: string
32}
33
34export class VideosCommand extends AbstractCommand {
35
36 constructor (server: PeerTubeServer) {
37 super(server)
38
39 loadLanguages()
40 }
41
42 getCategories (options: OverrideCommandOptions = {}) {
43 const path = '/api/v1/videos/categories'
44
45 return this.getRequestBody<{ [id: number]: string }>({
46 ...options,
47 path,
48
49 implicitToken: false,
50 defaultExpectedStatus: HttpStatusCode.OK_200
51 })
52 }
53
54 getLicences (options: OverrideCommandOptions = {}) {
55 const path = '/api/v1/videos/licences'
56
57 return this.getRequestBody<{ [id: number]: string }>({
58 ...options,
59 path,
60
61 implicitToken: false,
62 defaultExpectedStatus: HttpStatusCode.OK_200
63 })
64 }
65
66 getLanguages (options: OverrideCommandOptions = {}) {
67 const path = '/api/v1/videos/languages'
68
69 return this.getRequestBody<{ [id: string]: string }>({
70 ...options,
71 path,
72
73 implicitToken: false,
74 defaultExpectedStatus: HttpStatusCode.OK_200
75 })
76 }
77
78 getPrivacies (options: OverrideCommandOptions = {}) {
79 const path = '/api/v1/videos/privacies'
80
81 return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
82 ...options,
83 path,
84
85 implicitToken: false,
86 defaultExpectedStatus: HttpStatusCode.OK_200
87 })
88 }
89
90 // ---------------------------------------------------------------------------
91
92 getDescription (options: OverrideCommandOptions & {
93 descriptionPath: string
94 }) {
95 return this.getRequestBody<{ description: string }>({
96 ...options,
97 path: options.descriptionPath,
98
99 implicitToken: false,
100 defaultExpectedStatus: HttpStatusCode.OK_200
101 })
102 }
103
104 getFileMetadata (options: OverrideCommandOptions & {
105 url: string
106 }) {
107 return unwrapBody<VideoFileMetadata>(this.getRawRequest({
108 ...options,
109
110 url: options.url,
111 implicitToken: false,
112 defaultExpectedStatus: HttpStatusCode.OK_200
113 }))
114 }
115
116 // ---------------------------------------------------------------------------
117
118 view (options: OverrideCommandOptions & {
119 id: number | string
120 xForwardedFor?: string
121 }) {
122 const { id, xForwardedFor } = options
123 const path = '/api/v1/videos/' + id + '/views'
124
125 return this.postBodyRequest({
126 ...options,
127
128 path,
129 xForwardedFor,
130 implicitToken: false,
131 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
132 })
133 }
134
135 rate (options: OverrideCommandOptions & {
136 id: number | string
137 rating: UserVideoRateType
138 }) {
139 const { id, rating } = options
140 const path = '/api/v1/videos/' + id + '/rate'
141
142 return this.putBodyRequest({
143 ...options,
144
145 path,
146 fields: { rating },
147 implicitToken: true,
148 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
149 })
150 }
151
152 // ---------------------------------------------------------------------------
153
154 get (options: OverrideCommandOptions & {
155 id: number | string
156 }) {
157 const path = '/api/v1/videos/' + options.id
158
159 return this.getRequestBody<VideoDetails>({
160 ...options,
161
162 path,
163 implicitToken: false,
164 defaultExpectedStatus: HttpStatusCode.OK_200
165 })
166 }
167
168 getWithToken (options: OverrideCommandOptions & {
169 id: number | string
170 }) {
171 return this.get({
172 ...options,
173
174 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
175 })
176 }
177
178 async getId (options: OverrideCommandOptions & {
179 uuid: number | string
180 }) {
181 const { uuid } = options
182
183 if (validator.isUUID('' + uuid) === false) return uuid as number
184
185 const { id } = await this.get({ ...options, id: uuid })
186
187 return id
188 }
189
190 // ---------------------------------------------------------------------------
191
192 listMyVideos (options: OverrideCommandOptions & {
193 start?: number
194 count?: number
195 sort?: string
196 search?: string
197 isLive?: boolean
198 } = {}) {
199 const path = '/api/v1/users/me/videos'
200
201 return this.getRequestBody<ResultList<Video>>({
202 ...options,
203
204 path,
205 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
206 implicitToken: true,
207 defaultExpectedStatus: HttpStatusCode.OK_200
208 })
209 }
210
211 // ---------------------------------------------------------------------------
212
213 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
214 const path = '/api/v1/videos'
215
216 const query = this.buildListQuery(options)
217
218 return this.getRequestBody<ResultList<Video>>({
219 ...options,
220
221 path,
222 query: { sort: 'name', ...query },
223 implicitToken: false,
224 defaultExpectedStatus: HttpStatusCode.OK_200
225 })
226 }
227
228 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
229 return this.list({
230 ...options,
231
232 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
233 })
234 }
235
236 listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
237 handle: string
238 }) {
239 const { handle, search } = options
240 const path = '/api/v1/accounts/' + handle + '/videos'
241
242 return this.getRequestBody<ResultList<Video>>({
243 ...options,
244
245 path,
246 query: { search, ...this.buildListQuery(options) },
247 implicitToken: true,
248 defaultExpectedStatus: HttpStatusCode.OK_200
249 })
250 }
251
252 listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
253 handle: string
254 }) {
255 const { handle } = options
256 const path = '/api/v1/video-channels/' + handle + '/videos'
257
258 return this.getRequestBody<ResultList<Video>>({
259 ...options,
260
261 path,
262 query: this.buildListQuery(options),
263 implicitToken: true,
264 defaultExpectedStatus: HttpStatusCode.OK_200
265 })
266 }
267
268 // ---------------------------------------------------------------------------
269
270 async find (options: OverrideCommandOptions & {
271 name: string
272 }) {
273 const { data } = await this.list(options)
274
275 return data.find(v => v.name === options.name)
276 }
277
278 // ---------------------------------------------------------------------------
279
280 update (options: OverrideCommandOptions & {
281 id: number | string
282 attributes?: VideoEdit
283 }) {
284 const { id, attributes = {} } = options
285 const path = '/api/v1/videos/' + id
286
287 // Upload request
288 if (attributes.thumbnailfile || attributes.previewfile) {
289 const attaches: any = {}
290 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
291 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
292
293 return this.putUploadRequest({
294 ...options,
295
296 path,
297 fields: options.attributes,
298 attaches: {
299 thumbnailfile: attributes.thumbnailfile,
300 previewfile: attributes.previewfile
301 },
302 implicitToken: true,
303 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
304 })
305 }
306
307 return this.putBodyRequest({
308 ...options,
309
310 path,
311 fields: options.attributes,
312 implicitToken: true,
313 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
314 })
315 }
316
317 remove (options: OverrideCommandOptions & {
318 id: number | string
319 }) {
320 const path = '/api/v1/videos/' + options.id
321
322 return unwrapBody(this.deleteRequest({
323 ...options,
324
325 path,
326 implicitToken: true,
327 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
328 }))
329 }
330
331 async removeAll () {
332 const { data } = await this.list()
333
334 for (const v of data) {
335 await this.remove({ id: v.id })
336 }
337 }
338
339 // ---------------------------------------------------------------------------
340
341 async upload (options: OverrideCommandOptions & {
342 attributes?: VideoEdit
343 mode?: 'legacy' | 'resumable' // default legacy
344 } = {}) {
345 const { mode = 'legacy' } = options
346 let defaultChannelId = 1
347
348 try {
349 const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
350 defaultChannelId = videoChannels[0].id
351 } catch (e) { /* empty */ }
352
353 // Override default attributes
354 const attributes = {
355 name: 'my super video',
356 category: 5,
357 licence: 4,
358 language: 'zh',
359 channelId: defaultChannelId,
360 nsfw: true,
361 waitTranscoding: false,
362 description: 'my super description',
363 support: 'my super support text',
364 tags: [ 'tag' ],
365 privacy: VideoPrivacy.PUBLIC,
366 commentsEnabled: true,
367 downloadEnabled: true,
368 fixture: 'video_short.webm',
369
370 ...options.attributes
371 }
372
373 const created = mode === 'legacy'
374 ? await this.buildLegacyUpload({ ...options, attributes })
375 : await this.buildResumeUpload({ ...options, attributes })
376
377 // Wait torrent generation
378 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
379 if (expectedStatus === HttpStatusCode.OK_200) {
380 let video: VideoDetails
381
382 do {
383 video = await this.getWithToken({ ...options, id: created.uuid })
384
385 await wait(50)
386 } while (!video.files[0].torrentUrl)
387 }
388
389 return created
390 }
391
392 async buildLegacyUpload (options: OverrideCommandOptions & {
393 attributes: VideoEdit
394 }): Promise<VideoCreateResult> {
395 const path = '/api/v1/videos/upload'
396
397 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
398 ...options,
399
400 path,
401 fields: this.buildUploadFields(options.attributes),
402 attaches: this.buildUploadAttaches(options.attributes),
403 implicitToken: true,
404 defaultExpectedStatus: HttpStatusCode.OK_200
405 })).then(body => body.video || body as any)
406 }
407
408 async buildResumeUpload (options: OverrideCommandOptions & {
409 attributes: VideoEdit
410 }): Promise<VideoCreateResult> {
411 const { attributes, expectedStatus } = options
412
413 let size = 0
414 let videoFilePath: string
415 let mimetype = 'video/mp4'
416
417 if (attributes.fixture) {
418 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
419 size = (await stat(videoFilePath)).size
420
421 if (videoFilePath.endsWith('.mkv')) {
422 mimetype = 'video/x-matroska'
423 } else if (videoFilePath.endsWith('.webm')) {
424 mimetype = 'video/webm'
425 }
426 }
427
428 // Do not check status automatically, we'll check it manually
429 const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
430 const initStatus = initializeSessionRes.status
431
432 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
433 const locationHeader = initializeSessionRes.header['location']
434 expect(locationHeader).to.not.be.undefined
435
436 const pathUploadId = locationHeader.split('?')[1]
437
438 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
439
440 return result.body?.video || result.body as any
441 }
442
443 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
444 ? HttpStatusCode.CREATED_201
445 : expectedStatus
446
447 expect(initStatus).to.equal(expectedInitStatus)
448
449 return initializeSessionRes.body.video || initializeSessionRes.body
450 }
451
452 async prepareResumableUpload (options: OverrideCommandOptions & {
453 attributes: VideoEdit
454 size: number
455 mimetype: string
456 }) {
457 const { attributes, size, mimetype } = options
458
459 const path = '/api/v1/videos/upload-resumable'
460
461 return this.postUploadRequest({
462 ...options,
463
464 path,
465 headers: {
466 'X-Upload-Content-Type': mimetype,
467 'X-Upload-Content-Length': size.toString()
468 },
469 fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
470 // Fixture will be sent later
471 attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
472 implicitToken: true,
473
474 defaultExpectedStatus: null
475 })
476 }
477
478 sendResumableChunks (options: OverrideCommandOptions & {
479 pathUploadId: string
480 videoFilePath: string
481 size: number
482 contentLength?: number
483 contentRangeBuilder?: (start: number, chunk: any) => string
484 }) {
485 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
486
487 const path = '/api/v1/videos/upload-resumable'
488 let start = 0
489
490 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
491 const url = this.server.url
492
493 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
494 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
495 readable.on('data', async function onData (chunk) {
496 readable.pause()
497
498 const headers = {
499 'Authorization': 'Bearer ' + token,
500 'Content-Type': 'application/octet-stream',
501 'Content-Range': contentRangeBuilder
502 ? contentRangeBuilder(start, chunk)
503 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
504 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
505 }
506
507 const res = await got<{ video: VideoCreateResult }>({
508 url,
509 method: 'put',
510 headers,
511 path: path + '?' + pathUploadId,
512 body: chunk,
513 responseType: 'json',
514 throwHttpErrors: false
515 })
516
517 start += chunk.length
518
519 if (res.statusCode === expectedStatus) {
520 return resolve(res)
521 }
522
523 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
524 readable.off('data', onData)
525 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
526 }
527
528 readable.resume()
529 })
530 })
531 }
532
533 quickUpload (options: OverrideCommandOptions & {
534 name: string
535 nsfw?: boolean
536 privacy?: VideoPrivacy
537 fixture?: string
538 }) {
539 const attributes: VideoEdit = { name: options.name }
540 if (options.nsfw) attributes.nsfw = options.nsfw
541 if (options.privacy) attributes.privacy = options.privacy
542 if (options.fixture) attributes.fixture = options.fixture
543
544 return this.upload({ ...options, attributes })
545 }
546
547 async randomUpload (options: OverrideCommandOptions & {
548 wait?: boolean // default true
549 additionalParams?: VideoEdit & { prefixName?: string }
550 } = {}) {
551 const { wait = true, additionalParams } = options
552 const prefixName = additionalParams?.prefixName || ''
553 const name = prefixName + buildUUID()
554
555 const attributes = { name, ...additionalParams }
556
557 const result = await this.upload({ ...options, attributes })
558
559 if (wait) await waitJobs([ this.server ])
560
561 return { ...result, name }
562 }
563
564 // ---------------------------------------------------------------------------
565
566 private buildListQuery (options: VideosCommonQuery) {
567 return pick(options, [
568 'start',
569 'count',
570 'sort',
571 'nsfw',
572 'isLive',
573 'categoryOneOf',
574 'licenceOneOf',
575 'languageOneOf',
576 'tagsOneOf',
577 'tagsAllOf',
578 'filter',
579 'skipCount'
580 ])
581 }
582
583 private buildUploadFields (attributes: VideoEdit) {
584 return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
585 }
586
587 private buildUploadAttaches (attributes: VideoEdit) {
588 const attaches: { [ name: string ]: string } = {}
589
590 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
591 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
592 }
593
594 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
595
596 return attaches
597 }
598}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 469ea4d63..9a9bfb3cf 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -1,348 +1,20 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra' 4import { pathExists, readdir } from 'fs-extra'
5import got, { Response as GotResponse } from 'got/dist/source'
6import * as parseTorrent from 'parse-torrent'
7import { join } from 'path' 5import { join } from 'path'
8import * as request from 'supertest'
9import validator from 'validator'
10import { getLowercaseExtension } from '@server/helpers/core-utils' 6import { getLowercaseExtension } from '@server/helpers/core-utils'
11import { buildUUID } from '@server/helpers/uuid' 7import { HttpStatusCode } from '@shared/models'
12import { HttpStatusCode } from '@shared/core-utils' 8import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
13import { VideosCommonQuery } from '@shared/models' 9import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
14import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' 10import { makeRawRequest } from '../requests/requests'
15import { VideoDetails, VideoPrivacy } from '../../models/videos' 11import { waitJobs } from '../server'
16import { 12import { PeerTubeServer } from '../server/server'
17 buildAbsoluteFixturePath, 13import { VideoEdit } from './videos-command'
18 buildServerDirectory,
19 dateIsValid,
20 immutableAssign,
21 testImage,
22 wait,
23 webtorrentAdd
24} from '../miscs/miscs'
25import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests'
26import { waitJobs } from '../server/jobs'
27import { ServerInfo } from '../server/servers'
28import { getMyUserInformation } from '../users/users'
29
30loadLanguages()
31
32type VideoAttributes = {
33 name?: string
34 category?: number
35 licence?: number
36 language?: string
37 nsfw?: boolean
38 commentsEnabled?: boolean
39 downloadEnabled?: boolean
40 waitTranscoding?: boolean
41 description?: string
42 originallyPublishedAt?: string
43 tags?: string[]
44 channelId?: number
45 privacy?: VideoPrivacy
46 fixture?: string
47 support?: string
48 thumbnailfile?: string
49 previewfile?: string
50 scheduleUpdate?: {
51 updateAt: string
52 privacy?: VideoPrivacy
53 }
54}
55
56function getVideoCategories (url: string) {
57 const path = '/api/v1/videos/categories'
58
59 return makeGetRequest({
60 url,
61 path,
62 statusCodeExpected: HttpStatusCode.OK_200
63 })
64}
65
66function getVideoLicences (url: string) {
67 const path = '/api/v1/videos/licences'
68
69 return makeGetRequest({
70 url,
71 path,
72 statusCodeExpected: HttpStatusCode.OK_200
73 })
74}
75
76function getVideoLanguages (url: string) {
77 const path = '/api/v1/videos/languages'
78
79 return makeGetRequest({
80 url,
81 path,
82 statusCodeExpected: HttpStatusCode.OK_200
83 })
84}
85
86function getVideoPrivacies (url: string) {
87 const path = '/api/v1/videos/privacies'
88
89 return makeGetRequest({
90 url,
91 path,
92 statusCodeExpected: HttpStatusCode.OK_200
93 })
94}
95
96function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
97 const path = '/api/v1/videos/' + id
98
99 return request(url)
100 .get(path)
101 .set('Accept', 'application/json')
102 .expect(expectedStatus)
103}
104
105async function getVideoIdFromUUID (url: string, uuid: string) {
106 const res = await getVideo(url, uuid)
107
108 return res.body.id
109}
110
111function getVideoFileMetadataUrl (url: string) {
112 return request(url)
113 .get('/')
114 .set('Accept', 'application/json')
115 .expect(HttpStatusCode.OK_200)
116 .expect('Content-Type', /json/)
117}
118
119function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) {
120 const path = '/api/v1/videos/' + id + '/views'
121
122 const req = request(url)
123 .post(path)
124 .set('Accept', 'application/json')
125
126 if (xForwardedFor) {
127 req.set('X-Forwarded-For', xForwardedFor)
128 }
129
130 return req.expect(expectedStatus)
131}
132
133function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
134 const path = '/api/v1/videos/' + id
135
136 return request(url)
137 .get(path)
138 .set('Authorization', 'Bearer ' + token)
139 .set('Accept', 'application/json')
140 .expect(expectedStatus)
141}
142
143function getVideoDescription (url: string, descriptionPath: string) {
144 return request(url)
145 .get(descriptionPath)
146 .set('Accept', 'application/json')
147 .expect(HttpStatusCode.OK_200)
148 .expect('Content-Type', /json/)
149}
150
151function getVideosList (url: string) {
152 const path = '/api/v1/videos'
153
154 return request(url)
155 .get(path)
156 .query({ sort: 'name' })
157 .set('Accept', 'application/json')
158 .expect(HttpStatusCode.OK_200)
159 .expect('Content-Type', /json/)
160}
161
162function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
163 const path = '/api/v1/videos'
164
165 return request(url)
166 .get(path)
167 .set('Authorization', 'Bearer ' + token)
168 .query(immutableAssign(query, { sort: 'name' }))
169 .set('Accept', 'application/json')
170 .expect(HttpStatusCode.OK_200)
171 .expect('Content-Type', /json/)
172}
173
174function getLocalVideos (url: string) {
175 const path = '/api/v1/videos'
176
177 return request(url)
178 .get(path)
179 .query({ sort: 'name', filter: 'local' })
180 .set('Accept', 'application/json')
181 .expect(HttpStatusCode.OK_200)
182 .expect('Content-Type', /json/)
183}
184
185function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
186 const path = '/api/v1/users/me/videos'
187
188 const req = request(url)
189 .get(path)
190 .query({ start: start })
191 .query({ count: count })
192 .query({ search: search })
193
194 if (sort) req.query({ sort })
195
196 return req.set('Accept', 'application/json')
197 .set('Authorization', 'Bearer ' + accessToken)
198 .expect(HttpStatusCode.OK_200)
199 .expect('Content-Type', /json/)
200}
201
202function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
203 const path = '/api/v1/users/me/videos'
204
205 return makeGetRequest({
206 url,
207 path,
208 token: accessToken,
209 query,
210 statusCodeExpected: HttpStatusCode.OK_200
211 })
212}
213
214function getAccountVideos (
215 url: string,
216 accessToken: string,
217 accountName: string,
218 start: number,
219 count: number,
220 sort?: string,
221 query: {
222 nsfw?: boolean
223 search?: string
224 } = {}
225) {
226 const path = '/api/v1/accounts/' + accountName + '/videos'
227
228 return makeGetRequest({
229 url,
230 path,
231 query: immutableAssign(query, {
232 start,
233 count,
234 sort
235 }),
236 token: accessToken,
237 statusCodeExpected: HttpStatusCode.OK_200
238 })
239}
240
241function getVideoChannelVideos (
242 url: string,
243 accessToken: string,
244 videoChannelName: string,
245 start: number,
246 count: number,
247 sort?: string,
248 query: { nsfw?: boolean } = {}
249) {
250 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
251
252 return makeGetRequest({
253 url,
254 path,
255 query: immutableAssign(query, {
256 start,
257 count,
258 sort
259 }),
260 token: accessToken,
261 statusCodeExpected: HttpStatusCode.OK_200
262 })
263}
264
265function getPlaylistVideos (
266 url: string,
267 accessToken: string,
268 playlistId: number | string,
269 start: number,
270 count: number,
271 query: { nsfw?: boolean } = {}
272) {
273 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
274
275 return makeGetRequest({
276 url,
277 path,
278 query: immutableAssign(query, {
279 start,
280 count
281 }),
282 token: accessToken,
283 statusCodeExpected: HttpStatusCode.OK_200
284 })
285}
286
287function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
288 const path = '/api/v1/videos'
289
290 const req = request(url)
291 .get(path)
292 .query({ start: start })
293 .query({ count: count })
294
295 if (sort) req.query({ sort })
296 if (skipCount) req.query({ skipCount })
297
298 return req.set('Accept', 'application/json')
299 .expect(HttpStatusCode.OK_200)
300 .expect('Content-Type', /json/)
301}
302
303function getVideosListSort (url: string, sort: string) {
304 const path = '/api/v1/videos'
305
306 return request(url)
307 .get(path)
308 .query({ sort: sort })
309 .set('Accept', 'application/json')
310 .expect(HttpStatusCode.OK_200)
311 .expect('Content-Type', /json/)
312}
313
314function getVideosWithFilters (url: string, query: VideosCommonQuery) {
315 const path = '/api/v1/videos'
316
317 return request(url)
318 .get(path)
319 .query(query)
320 .set('Accept', 'application/json')
321 .expect(HttpStatusCode.OK_200)
322 .expect('Content-Type', /json/)
323}
324
325function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
326 const path = '/api/v1/videos'
327
328 return request(url)
329 .delete(path + '/' + id)
330 .set('Accept', 'application/json')
331 .set('Authorization', 'Bearer ' + token)
332 .expect(expectedStatus)
333}
334
335async function removeAllVideos (server: ServerInfo) {
336 const resVideos = await getVideosList(server.url)
337
338 for (const v of resVideos.body.data) {
339 await removeVideo(server.url, server.accessToken, v.id)
340 }
341}
342 14
343async function checkVideoFilesWereRemoved ( 15async function checkVideoFilesWereRemoved (
344 videoUUID: string, 16 videoUUID: string,
345 serverNumber: number, 17 server: PeerTubeServer,
346 directories = [ 18 directories = [
347 'redundancy', 19 'redundancy',
348 'videos', 20 'videos',
@@ -355,7 +27,7 @@ async function checkVideoFilesWereRemoved (
355 ] 27 ]
356) { 28) {
357 for (const directory of directories) { 29 for (const directory of directories) {
358 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory) 30 const directoryPath = server.servers.buildDirectory(directory)
359 31
360 const directoryExists = await pathExists(directoryPath) 32 const directoryExists = await pathExists(directoryPath)
361 if (directoryExists === false) continue 33 if (directoryExists === false) continue
@@ -367,280 +39,20 @@ async function checkVideoFilesWereRemoved (
367 } 39 }
368} 40}
369 41
370async function uploadVideo (
371 url: string,
372 accessToken: string,
373 videoAttributesArg: VideoAttributes,
374 specialStatus = HttpStatusCode.OK_200,
375 mode: 'legacy' | 'resumable' = 'legacy'
376) {
377 let defaultChannelId = '1'
378
379 try {
380 const res = await getMyUserInformation(url, accessToken)
381 defaultChannelId = res.body.videoChannels[0].id
382 } catch (e) { /* empty */ }
383
384 // Override default attributes
385 const attributes = Object.assign({
386 name: 'my super video',
387 category: 5,
388 licence: 4,
389 language: 'zh',
390 channelId: defaultChannelId,
391 nsfw: true,
392 waitTranscoding: false,
393 description: 'my super description',
394 support: 'my super support text',
395 tags: [ 'tag' ],
396 privacy: VideoPrivacy.PUBLIC,
397 commentsEnabled: true,
398 downloadEnabled: true,
399 fixture: 'video_short.webm'
400 }, videoAttributesArg)
401
402 const res = mode === 'legacy'
403 ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
404 : await buildResumeUpload(url, accessToken, attributes, specialStatus)
405
406 // Wait torrent generation
407 if (specialStatus === HttpStatusCode.OK_200) {
408 let video: VideoDetails
409 do {
410 const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
411 video = resVideo.body
412
413 await wait(50)
414 } while (!video.files[0].torrentUrl)
415 }
416
417 return res
418}
419
420function checkUploadVideoParam ( 42function checkUploadVideoParam (
421 url: string, 43 server: PeerTubeServer,
422 token: string, 44 token: string,
423 attributes: Partial<VideoAttributes>, 45 attributes: Partial<VideoEdit>,
424 specialStatus = HttpStatusCode.OK_200, 46 expectedStatus = HttpStatusCode.OK_200,
425 mode: 'legacy' | 'resumable' = 'legacy' 47 mode: 'legacy' | 'resumable' = 'legacy'
426) { 48) {
427 return mode === 'legacy' 49 return mode === 'legacy'
428 ? buildLegacyUpload(url, token, attributes, specialStatus) 50 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
429 : buildResumeUpload(url, token, attributes, specialStatus) 51 : server.videos.buildResumeUpload({ token, attributes, expectedStatus })
430}
431
432async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
433 const path = '/api/v1/videos/upload'
434 const req = request(url)
435 .post(path)
436 .set('Accept', 'application/json')
437 .set('Authorization', 'Bearer ' + token)
438
439 buildUploadReq(req, attributes)
440
441 if (attributes.fixture !== undefined) {
442 req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
443 }
444
445 return req.expect(specialStatus)
446}
447
448async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
449 let size = 0
450 let videoFilePath: string
451 let mimetype = 'video/mp4'
452
453 if (attributes.fixture) {
454 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
455 size = (await stat(videoFilePath)).size
456
457 if (videoFilePath.endsWith('.mkv')) {
458 mimetype = 'video/x-matroska'
459 } else if (videoFilePath.endsWith('.webm')) {
460 mimetype = 'video/webm'
461 }
462 }
463
464 const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
465 const initStatus = initializeSessionRes.status
466
467 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
468 const locationHeader = initializeSessionRes.header['location']
469 expect(locationHeader).to.not.be.undefined
470
471 const pathUploadId = locationHeader.split('?')[1]
472
473 return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
474 }
475
476 const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
477 ? HttpStatusCode.CREATED_201
478 : specialStatus
479
480 expect(initStatus).to.equal(expectedInitStatus)
481
482 return initializeSessionRes
483}
484
485async function prepareResumableUpload (options: {
486 url: string
487 token: string
488 attributes: VideoAttributes
489 size: number
490 mimetype: string
491}) {
492 const { url, token, attributes, size, mimetype } = options
493
494 const path = '/api/v1/videos/upload-resumable'
495
496 const req = request(url)
497 .post(path)
498 .set('Authorization', 'Bearer ' + token)
499 .set('X-Upload-Content-Type', mimetype)
500 .set('X-Upload-Content-Length', size.toString())
501
502 buildUploadReq(req, attributes)
503
504 if (attributes.fixture) {
505 req.field('filename', attributes.fixture)
506 }
507
508 return req
509}
510
511function sendResumableChunks (options: {
512 url: string
513 token: string
514 pathUploadId: string
515 videoFilePath: string
516 size: number
517 specialStatus?: HttpStatusCode
518 contentLength?: number
519 contentRangeBuilder?: (start: number, chunk: any) => string
520}) {
521 const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
522
523 const expectedStatus = specialStatus || HttpStatusCode.OK_200
524
525 const path = '/api/v1/videos/upload-resumable'
526 let start = 0
527
528 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
529 return new Promise<GotResponse>((resolve, reject) => {
530 readable.on('data', async function onData (chunk) {
531 readable.pause()
532
533 const headers = {
534 'Authorization': 'Bearer ' + token,
535 'Content-Type': 'application/octet-stream',
536 'Content-Range': contentRangeBuilder
537 ? contentRangeBuilder(start, chunk)
538 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
539 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
540 }
541
542 const res = await got({
543 url,
544 method: 'put',
545 headers,
546 path: path + '?' + pathUploadId,
547 body: chunk,
548 responseType: 'json',
549 throwHttpErrors: false
550 })
551
552 start += chunk.length
553
554 if (res.statusCode === expectedStatus) {
555 return resolve(res)
556 }
557
558 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
559 readable.off('data', onData)
560 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
561 }
562
563 readable.resume()
564 })
565 })
566}
567
568function updateVideo (
569 url: string,
570 accessToken: string,
571 id: number | string,
572 attributes: VideoAttributes,
573 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
574) {
575 const path = '/api/v1/videos/' + id
576 const body = {}
577
578 if (attributes.name) body['name'] = attributes.name
579 if (attributes.category) body['category'] = attributes.category
580 if (attributes.licence) body['licence'] = attributes.licence
581 if (attributes.language) body['language'] = attributes.language
582 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
583 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
584 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
585 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
586 if (attributes.description) body['description'] = attributes.description
587 if (attributes.tags) body['tags'] = attributes.tags
588 if (attributes.privacy) body['privacy'] = attributes.privacy
589 if (attributes.channelId) body['channelId'] = attributes.channelId
590 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
591
592 // Upload request
593 if (attributes.thumbnailfile || attributes.previewfile) {
594 const attaches: any = {}
595 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
596 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
597
598 return makeUploadRequest({
599 url,
600 method: 'PUT',
601 path,
602 token: accessToken,
603 fields: body,
604 attaches,
605 statusCodeExpected
606 })
607 }
608
609 return makePutBodyRequest({
610 url,
611 path,
612 fields: body,
613 token: accessToken,
614 statusCodeExpected
615 })
616}
617
618function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
619 const path = '/api/v1/videos/' + id + '/rate'
620
621 return request(url)
622 .put(path)
623 .set('Accept', 'application/json')
624 .set('Authorization', 'Bearer ' + accessToken)
625 .send({ rating })
626 .expect(specialStatus)
627}
628
629function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
630 return new Promise<any>((res, rej) => {
631 const torrentName = videoUUID + '-' + resolution + '.torrent'
632 const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
633
634 readFile(torrentPath, (err, data) => {
635 if (err) return rej(err)
636
637 return res(parseTorrent(data))
638 })
639 })
640} 52}
641 53
642async function completeVideoCheck ( 54async function completeVideoCheck (
643 url: string, 55 server: PeerTubeServer,
644 video: any, 56 video: any,
645 attributes: { 57 attributes: {
646 name: string 58 name: string
@@ -682,7 +94,7 @@ async function completeVideoCheck (
682 if (!attributes.likes) attributes.likes = 0 94 if (!attributes.likes) attributes.likes = 0
683 if (!attributes.dislikes) attributes.dislikes = 0 95 if (!attributes.dislikes) attributes.dislikes = 0
684 96
685 const host = new URL(url).host 97 const host = new URL(server.url).host
686 const originHost = attributes.account.host 98 const originHost = attributes.account.host
687 99
688 expect(video.name).to.equal(attributes.name) 100 expect(video.name).to.equal(attributes.name)
@@ -719,8 +131,7 @@ async function completeVideoCheck (
719 expect(video.originallyPublishedAt).to.be.null 131 expect(video.originallyPublishedAt).to.be.null
720 } 132 }
721 133
722 const res = await getVideo(url, video.uuid) 134 const videoDetails = await server.videos.get({ id: video.uuid })
723 const videoDetails: VideoDetails = res.body
724 135
725 expect(videoDetails.files).to.have.lengthOf(attributes.files.length) 136 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
726 expect(videoDetails.tags).to.deep.equal(attributes.tags) 137 expect(videoDetails.tags).to.deep.equal(attributes.tags)
@@ -776,149 +187,33 @@ async function completeVideoCheck (
776 } 187 }
777 188
778 expect(videoDetails.thumbnailPath).to.exist 189 expect(videoDetails.thumbnailPath).to.exist
779 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) 190 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
780 191
781 if (attributes.previewfile) { 192 if (attributes.previewfile) {
782 expect(videoDetails.previewPath).to.exist 193 expect(videoDetails.previewPath).to.exist
783 await testImage(url, attributes.previewfile, videoDetails.previewPath) 194 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
784 } 195 }
785} 196}
786 197
787async function videoUUIDToId (url: string, id: number | string) {
788 if (validator.isUUID('' + id) === false) return id
789
790 const res = await getVideo(url, id)
791 return res.body.id
792}
793
794async function uploadVideoAndGetId (options: {
795 server: ServerInfo
796 videoName: string
797 nsfw?: boolean
798 privacy?: VideoPrivacy
799 token?: string
800 fixture?: string
801}) {
802 const videoAttrs: any = { name: options.videoName }
803 if (options.nsfw) videoAttrs.nsfw = options.nsfw
804 if (options.privacy) videoAttrs.privacy = options.privacy
805 if (options.fixture) videoAttrs.fixture = options.fixture
806
807 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
808
809 return res.body.video as { id: number, uuid: string, shortUUID: string }
810}
811
812async function getLocalIdByUUID (url: string, uuid: string) {
813 const res = await getVideo(url, uuid)
814
815 return res.body.id
816}
817
818// serverNumber starts from 1 198// serverNumber starts from 1
819async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) { 199async function uploadRandomVideoOnServers (
200 servers: PeerTubeServer[],
201 serverNumber: number,
202 additionalParams?: VideoEdit & { prefixName?: string }
203) {
820 const server = servers.find(s => s.serverNumber === serverNumber) 204 const server = servers.find(s => s.serverNumber === serverNumber)
821 const res = await uploadRandomVideo(server, false, additionalParams) 205 const res = await server.videos.randomUpload({ wait: false, additionalParams })
822 206
823 await waitJobs(servers) 207 await waitJobs(servers)
824 208
825 return res 209 return res
826} 210}
827 211
828async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
829 const prefixName = additionalParams.prefixName || ''
830 const name = prefixName + buildUUID()
831
832 const data = Object.assign({ name }, additionalParams)
833 const res = await uploadVideo(server.url, server.accessToken, data)
834
835 if (wait) await waitJobs([ server ])
836
837 return { uuid: res.body.video.uuid, name }
838}
839
840// --------------------------------------------------------------------------- 212// ---------------------------------------------------------------------------
841 213
842export { 214export {
843 getVideoDescription,
844 getVideoCategories,
845 uploadRandomVideo,
846 getVideoLicences,
847 videoUUIDToId,
848 getVideoPrivacies,
849 getVideoLanguages,
850 getMyVideos,
851 getAccountVideos,
852 getVideoChannelVideos,
853 getVideo,
854 getVideoFileMetadataUrl,
855 getVideoWithToken,
856 getVideosList,
857 removeAllVideos,
858 checkUploadVideoParam, 215 checkUploadVideoParam,
859 getVideosListPagination,
860 getVideosListSort,
861 removeVideo,
862 getVideosListWithToken,
863 uploadVideo,
864 sendResumableChunks,
865 getVideosWithFilters,
866 uploadRandomVideoOnServers,
867 updateVideo,
868 rateVideo,
869 viewVideo,
870 parseTorrentVideo,
871 getLocalVideos,
872 completeVideoCheck, 216 completeVideoCheck,
873 checkVideoFilesWereRemoved, 217 uploadRandomVideoOnServers,
874 getPlaylistVideos, 218 checkVideoFilesWereRemoved
875 getMyVideosWithFilter,
876 uploadVideoAndGetId,
877 getLocalIdByUUID,
878 getVideoIdFromUUID,
879 prepareResumableUpload
880}
881
882// ---------------------------------------------------------------------------
883
884function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
885
886 for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
887 if (attributes[key] !== undefined) {
888 req.field(key, attributes[key])
889 }
890 }
891
892 for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
893 if (attributes[key] !== undefined) {
894 req.field(key, JSON.stringify(attributes[key]))
895 }
896 }
897
898 for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
899 if (attributes[key] !== undefined) {
900 req.field(key, attributes[key].toString())
901 }
902 }
903
904 const tags = attributes.tags || []
905 for (let i = 0; i < tags.length; i++) {
906 req.field('tags[' + i + ']', attributes.tags[i])
907 }
908
909 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
910 if (attributes[key] !== undefined) {
911 req.attach(key, buildAbsoluteFixturePath(attributes[key]))
912 }
913 }
914
915 if (attributes.scheduleUpdate) {
916 if (attributes.scheduleUpdate.updateAt) {
917 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
918 }
919
920 if (attributes.scheduleUpdate.privacy) {
921 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
922 }
923 }
924} 219}