aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/server-commands
diff options
context:
space:
mode:
Diffstat (limited to 'shared/server-commands')
-rw-r--r--shared/server-commands/miscs/sql-command.ts5
-rw-r--r--shared/server-commands/requests/requests.ts26
-rw-r--r--shared/server-commands/server/jobs.ts9
-rw-r--r--shared/server-commands/server/object-storage-command.ts105
-rw-r--r--shared/server-commands/server/server.ts48
-rw-r--r--shared/server-commands/users/index.ts1
-rw-r--r--shared/server-commands/users/login-command.ts73
-rw-r--r--shared/server-commands/users/two-factor-command.ts92
-rw-r--r--shared/server-commands/users/users-command.ts8
-rw-r--r--shared/server-commands/videos/index.ts1
-rw-r--r--shared/server-commands/videos/live-command.ts125
-rw-r--r--shared/server-commands/videos/live.ts7
-rw-r--r--shared/server-commands/videos/streaming-playlists-command.ts38
-rw-r--r--shared/server-commands/videos/video-token-command.ts31
-rw-r--r--shared/server-commands/videos/videos-command.ts25
15 files changed, 498 insertions, 96 deletions
diff --git a/shared/server-commands/miscs/sql-command.ts b/shared/server-commands/miscs/sql-command.ts
index 09a99f834..b0d9ce56d 100644
--- a/shared/server-commands/miscs/sql-command.ts
+++ b/shared/server-commands/miscs/sql-command.ts
@@ -23,6 +23,11 @@ export class SQLCommand extends AbstractCommand {
23 return parseInt(total, 10) 23 return parseInt(total, 10)
24 } 24 }
25 25
26 async getInternalFileUrl (fileId: number) {
27 return this.selectQuery(`SELECT "fileUrl" FROM "videoFile" WHERE id = ${fileId}`)
28 .then(rows => rows[0].fileUrl as string)
29 }
30
26 setActorField (to: string, field: string, value: string) { 31 setActorField (to: string, field: string, value: string) {
27 const seq = this.getSequelize() 32 const seq = this.getSequelize()
28 33
diff --git a/shared/server-commands/requests/requests.ts b/shared/server-commands/requests/requests.ts
index 85cbc9be9..b247017fd 100644
--- a/shared/server-commands/requests/requests.ts
+++ b/shared/server-commands/requests/requests.ts
@@ -3,7 +3,7 @@
3import { decode } from 'querystring' 3import { decode } from 'querystring'
4import request from 'supertest' 4import request from 'supertest'
5import { URL } from 'url' 5import { URL } from 'url'
6import { buildAbsoluteFixturePath } from '@shared/core-utils' 6import { buildAbsoluteFixturePath, pick } from '@shared/core-utils'
7import { HttpStatusCode } from '@shared/models' 7import { HttpStatusCode } from '@shared/models'
8 8
9export type CommonRequestParams = { 9export type CommonRequestParams = {
@@ -21,10 +21,21 @@ export type CommonRequestParams = {
21 expectedStatus?: HttpStatusCode 21 expectedStatus?: HttpStatusCode
22} 22}
23 23
24function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) { 24function makeRawRequest (options: {
25 const { host, protocol, pathname } = new URL(url) 25 url: string
26 token?: string
27 expectedStatus?: HttpStatusCode
28 range?: string
29 query?: { [ id: string ]: string }
30}) {
31 const { host, protocol, pathname } = new URL(options.url)
32
33 return makeGetRequest({
34 url: `${protocol}//${host}`,
35 path: pathname,
26 36
27 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range }) 37 ...pick(options, [ 'expectedStatus', 'range', 'token', 'query' ])
38 })
28} 39}
29 40
30function makeGetRequest (options: CommonRequestParams & { 41function makeGetRequest (options: CommonRequestParams & {
@@ -134,7 +145,12 @@ function unwrapText (test: request.Test): Promise<string> {
134function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> { 145function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
135 return test.then(res => { 146 return test.then(res => {
136 if (res.body instanceof Buffer) { 147 if (res.body instanceof Buffer) {
137 return JSON.parse(new TextDecoder().decode(res.body)) 148 try {
149 return JSON.parse(new TextDecoder().decode(res.body))
150 } catch (err) {
151 console.error('Cannot decode JSON.', res.body)
152 throw err
153 }
138 } 154 }
139 155
140 return res.body 156 return res.body
diff --git a/shared/server-commands/server/jobs.ts b/shared/server-commands/server/jobs.ts
index fc65a873b..e1d6cdff4 100644
--- a/shared/server-commands/server/jobs.ts
+++ b/shared/server-commands/server/jobs.ts
@@ -4,7 +4,14 @@ import { wait } from '@shared/core-utils'
4import { JobState, JobType } from '../../models' 4import { JobState, JobType } from '../../models'
5import { PeerTubeServer } from './server' 5import { PeerTubeServer } from './server'
6 6
7async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) { 7async function waitJobs (
8 serversArg: PeerTubeServer[] | PeerTubeServer,
9 options: {
10 skipDelayed?: boolean // default false
11 } = {}
12) {
13 const { skipDelayed = false } = options
14
8 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT 15 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
9 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) 16 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
10 : 250 17 : 250
diff --git a/shared/server-commands/server/object-storage-command.ts b/shared/server-commands/server/object-storage-command.ts
index b4de8f4cb..a1fe4f0f7 100644
--- a/shared/server-commands/server/object-storage-command.ts
+++ b/shared/server-commands/server/object-storage-command.ts
@@ -4,74 +4,135 @@ import { makePostBodyRequest } from '../requests'
4import { AbstractCommand } from '../shared' 4import { AbstractCommand } from '../shared'
5 5
6export class ObjectStorageCommand extends AbstractCommand { 6export class ObjectStorageCommand extends AbstractCommand {
7 static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists' 7 static readonly DEFAULT_PLAYLIST_MOCK_BUCKET = 'streaming-playlists'
8 static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos' 8 static readonly DEFAULT_WEBTORRENT_MOCK_BUCKET = 'videos'
9 9
10 static getDefaultConfig () { 10 static readonly DEFAULT_SCALEWAY_BUCKET = 'peertube-ci-test'
11
12 // ---------------------------------------------------------------------------
13
14 static getDefaultMockConfig () {
11 return { 15 return {
12 object_storage: { 16 object_storage: {
13 enabled: true, 17 enabled: true,
14 endpoint: 'http://' + this.getEndpointHost(), 18 endpoint: 'http://' + this.getMockEndpointHost(),
15 region: this.getRegion(), 19 region: this.getMockRegion(),
16 20
17 credentials: this.getCredentialsConfig(), 21 credentials: this.getMockCredentialsConfig(),
18 22
19 streaming_playlists: { 23 streaming_playlists: {
20 bucket_name: this.DEFAULT_PLAYLIST_BUCKET 24 bucket_name: this.DEFAULT_PLAYLIST_MOCK_BUCKET
21 }, 25 },
22 26
23 videos: { 27 videos: {
24 bucket_name: this.DEFAULT_WEBTORRENT_BUCKET 28 bucket_name: this.DEFAULT_WEBTORRENT_MOCK_BUCKET
25 } 29 }
26 } 30 }
27 } 31 }
28 } 32 }
29 33
30 static getCredentialsConfig () { 34 static getMockCredentialsConfig () {
31 return { 35 return {
32 access_key_id: 'AKIAIOSFODNN7EXAMPLE', 36 access_key_id: 'AKIAIOSFODNN7EXAMPLE',
33 secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' 37 secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
34 } 38 }
35 } 39 }
36 40
37 static getEndpointHost () { 41 static getMockEndpointHost () {
38 return 'localhost:9444' 42 return 'localhost:9444'
39 } 43 }
40 44
41 static getRegion () { 45 static getMockRegion () {
42 return 'us-east-1' 46 return 'us-east-1'
43 } 47 }
44 48
45 static getWebTorrentBaseUrl () { 49 static getMockWebTorrentBaseUrl () {
46 return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/` 50 return `http://${this.DEFAULT_WEBTORRENT_MOCK_BUCKET}.${this.getMockEndpointHost()}/`
47 } 51 }
48 52
49 static getPlaylistBaseUrl () { 53 static getMockPlaylistBaseUrl () {
50 return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/` 54 return `http://${this.DEFAULT_PLAYLIST_MOCK_BUCKET}.${this.getMockEndpointHost()}/`
51 } 55 }
52 56
53 static async prepareDefaultBuckets () { 57 static async prepareDefaultMockBuckets () {
54 await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET) 58 await this.createMockBucket(this.DEFAULT_PLAYLIST_MOCK_BUCKET)
55 await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET) 59 await this.createMockBucket(this.DEFAULT_WEBTORRENT_MOCK_BUCKET)
56 } 60 }
57 61
58 static async createBucket (name: string) { 62 static async createMockBucket (name: string) {
59 await makePostBodyRequest({ 63 await makePostBodyRequest({
60 url: this.getEndpointHost(), 64 url: this.getMockEndpointHost(),
61 path: '/ui/' + name + '?delete', 65 path: '/ui/' + name + '?delete',
62 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 66 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
63 }) 67 })
64 68
65 await makePostBodyRequest({ 69 await makePostBodyRequest({
66 url: this.getEndpointHost(), 70 url: this.getMockEndpointHost(),
67 path: '/ui/' + name + '?create', 71 path: '/ui/' + name + '?create',
68 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 72 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
69 }) 73 })
70 74
71 await makePostBodyRequest({ 75 await makePostBodyRequest({
72 url: this.getEndpointHost(), 76 url: this.getMockEndpointHost(),
73 path: '/ui/' + name + '?make-public', 77 path: '/ui/' + name + '?make-public',
74 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 78 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
75 }) 79 })
76 } 80 }
81
82 // ---------------------------------------------------------------------------
83
84 static getDefaultScalewayConfig (options: {
85 serverNumber: number
86 enablePrivateProxy?: boolean // default true
87 privateACL?: 'private' | 'public-read' // default 'private'
88 }) {
89 const { serverNumber, enablePrivateProxy = true, privateACL = 'private' } = options
90
91 return {
92 object_storage: {
93 enabled: true,
94 endpoint: this.getScalewayEndpointHost(),
95 region: this.getScalewayRegion(),
96
97 credentials: this.getScalewayCredentialsConfig(),
98
99 upload_acl: {
100 private: privateACL
101 },
102
103 proxy: {
104 proxify_private_files: enablePrivateProxy
105 },
106
107 streaming_playlists: {
108 bucket_name: this.DEFAULT_SCALEWAY_BUCKET,
109 prefix: `test:server-${serverNumber}-streaming-playlists:`
110 },
111
112 videos: {
113 bucket_name: this.DEFAULT_SCALEWAY_BUCKET,
114 prefix: `test:server-${serverNumber}-videos:`
115 }
116 }
117 }
118 }
119
120 static getScalewayCredentialsConfig () {
121 return {
122 access_key_id: process.env.OBJECT_STORAGE_SCALEWAY_KEY_ID,
123 secret_access_key: process.env.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY
124 }
125 }
126
127 static getScalewayEndpointHost () {
128 return 's3.fr-par.scw.cloud'
129 }
130
131 static getScalewayRegion () {
132 return 'fr-par'
133 }
134
135 static getScalewayBaseUrl () {
136 return `https://${this.DEFAULT_SCALEWAY_BUCKET}.${this.getScalewayEndpointHost()}/`
137 }
77} 138}
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts
index 2b4c9c9f8..c062e6986 100644
--- a/shared/server-commands/server/server.ts
+++ b/shared/server-commands/server/server.ts
@@ -13,7 +13,15 @@ import { AbusesCommand } from '../moderation'
13import { OverviewsCommand } from '../overviews' 13import { OverviewsCommand } from '../overviews'
14import { SearchCommand } from '../search' 14import { SearchCommand } from '../search'
15import { SocketIOCommand } from '../socket' 15import { SocketIOCommand } from '../socket'
16import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users' 16import {
17 AccountsCommand,
18 BlocklistCommand,
19 LoginCommand,
20 NotificationsCommand,
21 SubscriptionsCommand,
22 TwoFactorCommand,
23 UsersCommand
24} from '../users'
17import { 25import {
18 BlacklistCommand, 26 BlacklistCommand,
19 CaptionsCommand, 27 CaptionsCommand,
@@ -28,6 +36,7 @@ import {
28 StreamingPlaylistsCommand, 36 StreamingPlaylistsCommand,
29 VideosCommand, 37 VideosCommand,
30 VideoStudioCommand, 38 VideoStudioCommand,
39 VideoTokenCommand,
31 ViewsCommand 40 ViewsCommand
32} from '../videos' 41} from '../videos'
33import { CommentsCommand } from '../videos/comments-command' 42import { CommentsCommand } from '../videos/comments-command'
@@ -136,6 +145,8 @@ export class PeerTubeServer {
136 videos?: VideosCommand 145 videos?: VideosCommand
137 videoStats?: VideoStatsCommand 146 videoStats?: VideoStatsCommand
138 views?: ViewsCommand 147 views?: ViewsCommand
148 twoFactor?: TwoFactorCommand
149 videoToken?: VideoTokenCommand
139 150
140 constructor (options: { serverNumber: number } | { url: string }) { 151 constructor (options: { serverNumber: number } | { url: string }) {
141 if ((options as any).url) { 152 if ((options as any).url) {
@@ -182,6 +193,12 @@ export class PeerTubeServer {
182 this.port = parseInt(parsed.port) 193 this.port = parseInt(parsed.port)
183 } 194 }
184 195
196 getDirectoryPath (directoryName: string) {
197 const testDirectory = 'test' + this.internalServerNumber
198
199 return join(root(), testDirectory, directoryName)
200 }
201
185 async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) { 202 async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
186 await ServersCommand.flushTests(this.internalServerNumber) 203 await ServersCommand.flushTests(this.internalServerNumber)
187 204
@@ -341,19 +358,20 @@ export class PeerTubeServer {
341 suffix: '_test' + this.internalServerNumber 358 suffix: '_test' + this.internalServerNumber
342 }, 359 },
343 storage: { 360 storage: {
344 tmp: `test${this.internalServerNumber}/tmp/`, 361 tmp: this.getDirectoryPath('tmp') + '/',
345 bin: `test${this.internalServerNumber}/bin/`, 362 bin: this.getDirectoryPath('bin') + '/',
346 avatars: `test${this.internalServerNumber}/avatars/`, 363 avatars: this.getDirectoryPath('avatars') + '/',
347 videos: `test${this.internalServerNumber}/videos/`, 364 videos: this.getDirectoryPath('videos') + '/',
348 streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`, 365 streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/',
349 redundancy: `test${this.internalServerNumber}/redundancy/`, 366 redundancy: this.getDirectoryPath('redundancy') + '/',
350 logs: `test${this.internalServerNumber}/logs/`, 367 logs: this.getDirectoryPath('logs') + '/',
351 previews: `test${this.internalServerNumber}/previews/`, 368 previews: this.getDirectoryPath('previews') + '/',
352 thumbnails: `test${this.internalServerNumber}/thumbnails/`, 369 thumbnails: this.getDirectoryPath('thumbnails') + '/',
353 torrents: `test${this.internalServerNumber}/torrents/`, 370 torrents: this.getDirectoryPath('torrents') + '/',
354 captions: `test${this.internalServerNumber}/captions/`, 371 captions: this.getDirectoryPath('captions') + '/',
355 cache: `test${this.internalServerNumber}/cache/`, 372 cache: this.getDirectoryPath('cache') + '/',
356 plugins: `test${this.internalServerNumber}/plugins/` 373 plugins: this.getDirectoryPath('plugins') + '/',
374 well_known: this.getDirectoryPath('well-known') + '/'
357 }, 375 },
358 admin: { 376 admin: {
359 email: `admin${this.internalServerNumber}@example.com` 377 email: `admin${this.internalServerNumber}@example.com`
@@ -410,5 +428,7 @@ export class PeerTubeServer {
410 this.videoStudio = new VideoStudioCommand(this) 428 this.videoStudio = new VideoStudioCommand(this)
411 this.videoStats = new VideoStatsCommand(this) 429 this.videoStats = new VideoStatsCommand(this)
412 this.views = new ViewsCommand(this) 430 this.views = new ViewsCommand(this)
431 this.twoFactor = new TwoFactorCommand(this)
432 this.videoToken = new VideoTokenCommand(this)
413 } 433 }
414} 434}
diff --git a/shared/server-commands/users/index.ts b/shared/server-commands/users/index.ts
index f6f93b4d2..1afc02dc1 100644
--- a/shared/server-commands/users/index.ts
+++ b/shared/server-commands/users/index.ts
@@ -5,4 +5,5 @@ export * from './login'
5export * from './login-command' 5export * from './login-command'
6export * from './notifications-command' 6export * from './notifications-command'
7export * from './subscriptions-command' 7export * from './subscriptions-command'
8export * from './two-factor-command'
8export * from './users-command' 9export * from './users-command'
diff --git a/shared/server-commands/users/login-command.ts b/shared/server-commands/users/login-command.ts
index 54070e426..f2fc6d1c5 100644
--- a/shared/server-commands/users/login-command.ts
+++ b/shared/server-commands/users/login-command.ts
@@ -2,34 +2,27 @@ import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models'
2import { unwrapBody } from '../requests' 2import { unwrapBody } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared' 3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4 4
5type LoginOptions = OverrideCommandOptions & {
6 client?: { id?: string, secret?: string }
7 user?: { username: string, password?: string }
8 otpToken?: string
9}
10
5export class LoginCommand extends AbstractCommand { 11export class LoginCommand extends AbstractCommand {
6 12
7 login (options: OverrideCommandOptions & { 13 async login (options: LoginOptions = {}) {
8 client?: { id?: string, secret?: string } 14 const res = await this._login(options)
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 15
14 const body = { 16 return this.unwrapLoginBody(res.body)
15 client_id: client.id, 17 }
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 18
24 return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({ 19 async loginAndGetResponse (options: LoginOptions = {}) {
25 ...options, 20 const res = await this._login(options)
26 21
27 path, 22 return {
28 requestType: 'form', 23 res,
29 fields: body, 24 body: this.unwrapLoginBody(res.body)
30 implicitToken: false, 25 }
31 defaultExpectedStatus: HttpStatusCode.OK_200
32 }))
33 } 26 }
34 27
35 getAccessToken (arg1?: { username: string, password?: string }): Promise<string> 28 getAccessToken (arg1?: { username: string, password?: string }): Promise<string>
@@ -129,4 +122,38 @@ export class LoginCommand extends AbstractCommand {
129 defaultExpectedStatus: HttpStatusCode.OK_200 122 defaultExpectedStatus: HttpStatusCode.OK_200
130 }) 123 })
131 } 124 }
125
126 private _login (options: LoginOptions) {
127 const { client = this.server.store.client, user = this.server.store.user, otpToken } = options
128 const path = '/api/v1/users/token'
129
130 const body = {
131 client_id: client.id,
132 client_secret: client.secret,
133 username: user.username,
134 password: user.password ?? 'password',
135 response_type: 'code',
136 grant_type: 'password',
137 scope: 'upload'
138 }
139
140 const headers = otpToken
141 ? { 'x-peertube-otp': otpToken }
142 : {}
143
144 return this.postBodyRequest({
145 ...options,
146
147 path,
148 headers,
149 requestType: 'form',
150 fields: body,
151 implicitToken: false,
152 defaultExpectedStatus: HttpStatusCode.OK_200
153 })
154 }
155
156 private unwrapLoginBody (body: any) {
157 return body as { access_token: string, refresh_token: string } & PeerTubeProblemDocument
158 }
132} 159}
diff --git a/shared/server-commands/users/two-factor-command.ts b/shared/server-commands/users/two-factor-command.ts
new file mode 100644
index 000000000..5542acfda
--- /dev/null
+++ b/shared/server-commands/users/two-factor-command.ts
@@ -0,0 +1,92 @@
1import { TOTP } from 'otpauth'
2import { HttpStatusCode, TwoFactorEnableResult } from '@shared/models'
3import { unwrapBody } from '../requests'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class TwoFactorCommand extends AbstractCommand {
7
8 static buildOTP (options: {
9 secret: string
10 }) {
11 const { secret } = options
12
13 return new TOTP({
14 issuer: 'PeerTube',
15 algorithm: 'SHA1',
16 digits: 6,
17 period: 30,
18 secret
19 })
20 }
21
22 request (options: OverrideCommandOptions & {
23 userId: number
24 currentPassword?: string
25 }) {
26 const { currentPassword, userId } = options
27
28 const path = '/api/v1/users/' + userId + '/two-factor/request'
29
30 return unwrapBody<TwoFactorEnableResult>(this.postBodyRequest({
31 ...options,
32
33 path,
34 fields: { currentPassword },
35 implicitToken: true,
36 defaultExpectedStatus: HttpStatusCode.OK_200
37 }))
38 }
39
40 confirmRequest (options: OverrideCommandOptions & {
41 userId: number
42 requestToken: string
43 otpToken: string
44 }) {
45 const { userId, requestToken, otpToken } = options
46
47 const path = '/api/v1/users/' + userId + '/two-factor/confirm-request'
48
49 return this.postBodyRequest({
50 ...options,
51
52 path,
53 fields: { requestToken, otpToken },
54 implicitToken: true,
55 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
56 })
57 }
58
59 disable (options: OverrideCommandOptions & {
60 userId: number
61 currentPassword?: string
62 }) {
63 const { userId, currentPassword } = options
64 const path = '/api/v1/users/' + userId + '/two-factor/disable'
65
66 return this.postBodyRequest({
67 ...options,
68
69 path,
70 fields: { currentPassword },
71 implicitToken: true,
72 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
73 })
74 }
75
76 async requestAndConfirm (options: OverrideCommandOptions & {
77 userId: number
78 currentPassword?: string
79 }) {
80 const { userId, currentPassword } = options
81
82 const { otpRequest } = await this.request({ userId, currentPassword })
83
84 await this.confirmRequest({
85 userId,
86 requestToken: otpRequest.requestToken,
87 otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
88 })
89
90 return otpRequest
91 }
92}
diff --git a/shared/server-commands/users/users-command.ts b/shared/server-commands/users/users-command.ts
index d8303848d..811b9685b 100644
--- a/shared/server-commands/users/users-command.ts
+++ b/shared/server-commands/users/users-command.ts
@@ -202,7 +202,8 @@ export class UsersCommand extends AbstractCommand {
202 token, 202 token,
203 userId: user.id, 203 userId: user.id,
204 userChannelId: me.videoChannels[0].id, 204 userChannelId: me.videoChannels[0].id,
205 userChannelName: me.videoChannels[0].name 205 userChannelName: me.videoChannels[0].name,
206 password
206 } 207 }
207 } 208 }
208 209
@@ -217,12 +218,13 @@ export class UsersCommand extends AbstractCommand {
217 username: string 218 username: string
218 password?: string 219 password?: string
219 displayName?: string 220 displayName?: string
221 email?: string
220 channel?: { 222 channel?: {
221 name: string 223 name: string
222 displayName: string 224 displayName: string
223 } 225 }
224 }) { 226 }) {
225 const { username, password = 'password', displayName, channel } = options 227 const { username, password = 'password', displayName, channel, email = username + '@example.com' } = options
226 const path = '/api/v1/users/register' 228 const path = '/api/v1/users/register'
227 229
228 return this.postBodyRequest({ 230 return this.postBodyRequest({
@@ -232,7 +234,7 @@ export class UsersCommand extends AbstractCommand {
232 fields: { 234 fields: {
233 username, 235 username,
234 password, 236 password,
235 email: username + '@example.com', 237 email,
236 displayName, 238 displayName,
237 channel 239 channel
238 }, 240 },
diff --git a/shared/server-commands/videos/index.ts b/shared/server-commands/videos/index.ts
index b4d6fa37b..c17f6ef20 100644
--- a/shared/server-commands/videos/index.ts
+++ b/shared/server-commands/videos/index.ts
@@ -14,5 +14,6 @@ export * from './services-command'
14export * from './streaming-playlists-command' 14export * from './streaming-playlists-command'
15export * from './comments-command' 15export * from './comments-command'
16export * from './video-studio-command' 16export * from './video-studio-command'
17export * from './video-token-command'
17export * from './views-command' 18export * from './views-command'
18export * from './videos-command' 19export * from './videos-command'
diff --git a/shared/server-commands/videos/live-command.ts b/shared/server-commands/videos/live-command.ts
index d804fd883..cc9502c6f 100644
--- a/shared/server-commands/videos/live-command.ts
+++ b/shared/server-commands/videos/live-command.ts
@@ -12,9 +12,11 @@ import {
12 ResultList, 12 ResultList,
13 VideoCreateResult, 13 VideoCreateResult,
14 VideoDetails, 14 VideoDetails,
15 VideoPrivacy,
15 VideoState 16 VideoState
16} from '@shared/models' 17} from '@shared/models'
17import { unwrapBody } from '../requests' 18import { unwrapBody } from '../requests'
19import { ObjectStorageCommand, PeerTubeServer } from '../server'
18import { AbstractCommand, OverrideCommandOptions } from '../shared' 20import { AbstractCommand, OverrideCommandOptions } from '../shared'
19import { sendRTMPStream, testFfmpegStreamError } from './live' 21import { sendRTMPStream, testFfmpegStreamError } from './live'
20 22
@@ -34,6 +36,8 @@ export class LiveCommand extends AbstractCommand {
34 }) 36 })
35 } 37 }
36 38
39 // ---------------------------------------------------------------------------
40
37 listSessions (options: OverrideCommandOptions & { 41 listSessions (options: OverrideCommandOptions & {
38 videoId: number | string 42 videoId: number | string
39 }) { 43 }) {
@@ -70,6 +74,8 @@ export class LiveCommand extends AbstractCommand {
70 }) 74 })
71 } 75 }
72 76
77 // ---------------------------------------------------------------------------
78
73 update (options: OverrideCommandOptions & { 79 update (options: OverrideCommandOptions & {
74 videoId: number | string 80 videoId: number | string
75 fields: LiveVideoUpdate 81 fields: LiveVideoUpdate
@@ -110,6 +116,33 @@ export class LiveCommand extends AbstractCommand {
110 return body.video 116 return body.video
111 } 117 }
112 118
119 async quickCreate (options: OverrideCommandOptions & {
120 saveReplay: boolean
121 permanentLive: boolean
122 privacy?: VideoPrivacy
123 }) {
124 const { saveReplay, permanentLive, privacy } = options
125
126 const { uuid } = await this.create({
127 ...options,
128
129 fields: {
130 name: 'live',
131 permanentLive,
132 saveReplay,
133 channelId: this.server.store.channel.id,
134 privacy
135 }
136 })
137
138 const video = await this.server.videos.getWithToken({ id: uuid })
139 const live = await this.get({ videoId: uuid })
140
141 return { video, live }
142 }
143
144 // ---------------------------------------------------------------------------
145
113 async sendRTMPStreamInVideo (options: OverrideCommandOptions & { 146 async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
114 videoId: number | string 147 videoId: number | string
115 fixtureName?: string 148 fixtureName?: string
@@ -130,6 +163,8 @@ export class LiveCommand extends AbstractCommand {
130 return testFfmpegStreamError(command, options.shouldHaveError) 163 return testFfmpegStreamError(command, options.shouldHaveError)
131 } 164 }
132 165
166 // ---------------------------------------------------------------------------
167
133 waitUntilPublished (options: OverrideCommandOptions & { 168 waitUntilPublished (options: OverrideCommandOptions & {
134 videoId: number | string 169 videoId: number | string
135 }) { 170 }) {
@@ -151,27 +186,77 @@ export class LiveCommand extends AbstractCommand {
151 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED }) 186 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
152 } 187 }
153 188
154 waitUntilSegmentGeneration (options: OverrideCommandOptions & { 189 async waitUntilSegmentGeneration (options: OverrideCommandOptions & {
190 server: PeerTubeServer
155 videoUUID: string 191 videoUUID: string
156 playlistNumber: number 192 playlistNumber: number
157 segment: number 193 segment: number
158 totalSessions?: number 194 objectStorage: boolean
159 }) { 195 }) {
160 const { playlistNumber, segment, videoUUID, totalSessions = 1 } = options 196 const { server, objectStorage, playlistNumber, segment, videoUUID } = options
197
161 const segmentName = `${playlistNumber}-00000${segment}.ts` 198 const segmentName = `${playlistNumber}-00000${segment}.ts`
199 const baseUrl = objectStorage
200 ? ObjectStorageCommand.getMockPlaylistBaseUrl() + 'hls'
201 : server.url + '/static/streaming-playlists/hls'
202
203 let error = true
204
205 while (error) {
206 try {
207 await this.getRawRequest({
208 ...options,
209
210 url: `${baseUrl}/${videoUUID}/${segmentName}`,
211 implicitToken: false,
212 defaultExpectedStatus: HttpStatusCode.OK_200
213 })
214
215 const video = await server.videos.get({ id: videoUUID })
216 const hlsPlaylist = video.streamingPlaylists[0]
217
218 const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
219
220 if (!shaBody[segmentName]) {
221 throw new Error('Segment SHA does not exist')
222 }
162 223
163 return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, totalSessions * 2, false) 224 error = false
225 } catch {
226 error = true
227 await wait(100)
228 }
229 }
164 } 230 }
165 231
166 getSegment (options: OverrideCommandOptions & { 232 async waitUntilReplacedByReplay (options: OverrideCommandOptions & {
233 videoId: number | string
234 }) {
235 let video: VideoDetails
236
237 do {
238 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
239
240 await wait(500)
241 } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
242 }
243
244 // ---------------------------------------------------------------------------
245
246 getSegmentFile (options: OverrideCommandOptions & {
167 videoUUID: string 247 videoUUID: string
168 playlistNumber: number 248 playlistNumber: number
169 segment: number 249 segment: number
250 objectStorage?: boolean // default false
170 }) { 251 }) {
171 const { playlistNumber, segment, videoUUID } = options 252 const { playlistNumber, segment, videoUUID, objectStorage = false } = options
172 253
173 const segmentName = `${playlistNumber}-00000${segment}.ts` 254 const segmentName = `${playlistNumber}-00000${segment}.ts`
174 const url = `${this.server.url}/static/streaming-playlists/hls/${videoUUID}/${segmentName}` 255 const baseUrl = objectStorage
256 ? ObjectStorageCommand.getMockPlaylistBaseUrl()
257 : `${this.server.url}/static/streaming-playlists/hls`
258
259 const url = `${baseUrl}/${videoUUID}/${segmentName}`
175 260
176 return this.getRawRequest({ 261 return this.getRawRequest({
177 ...options, 262 ...options,
@@ -182,18 +267,30 @@ export class LiveCommand extends AbstractCommand {
182 }) 267 })
183 } 268 }
184 269
185 async waitUntilReplacedByReplay (options: OverrideCommandOptions & { 270 getPlaylistFile (options: OverrideCommandOptions & {
186 videoId: number | string 271 videoUUID: string
272 playlistName: string
273 objectStorage?: boolean // default false
187 }) { 274 }) {
188 let video: VideoDetails 275 const { playlistName, videoUUID, objectStorage = false } = options
189 276
190 do { 277 const baseUrl = objectStorage
191 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId }) 278 ? ObjectStorageCommand.getMockPlaylistBaseUrl()
279 : `${this.server.url}/static/streaming-playlists/hls`
192 280
193 await wait(500) 281 const url = `${baseUrl}/${videoUUID}/${playlistName}`
194 } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED) 282
283 return this.getRawRequest({
284 ...options,
285
286 url,
287 implicitToken: false,
288 defaultExpectedStatus: HttpStatusCode.OK_200
289 })
195 } 290 }
196 291
292 // ---------------------------------------------------------------------------
293
197 async countPlaylists (options: OverrideCommandOptions & { 294 async countPlaylists (options: OverrideCommandOptions & {
198 videoUUID: string 295 videoUUID: string
199 }) { 296 }) {
diff --git a/shared/server-commands/videos/live.ts b/shared/server-commands/videos/live.ts
index 6f180b05f..ee7444b64 100644
--- a/shared/server-commands/videos/live.ts
+++ b/shared/server-commands/videos/live.ts
@@ -1,7 +1,7 @@
1import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg' 1import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
2import { buildAbsoluteFixturePath, wait } from '@shared/core-utils' 2import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
3import { VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models'
3import { PeerTubeServer } from '../server/server' 4import { PeerTubeServer } from '../server/server'
4import { VideoDetails, VideoInclude } from '@shared/models'
5 5
6function sendRTMPStream (options: { 6function sendRTMPStream (options: {
7 rtmpBaseUrl: string 7 rtmpBaseUrl: string
@@ -98,7 +98,10 @@ async function waitUntilLiveReplacedByReplayOnAllServers (servers: PeerTubeServe
98} 98}
99 99
100async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: VideoDetails) { 100async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: VideoDetails) {
101 const { data } = await server.videos.list({ token: server.accessToken, sort: '-publishedAt', include: VideoInclude.BLACKLISTED }) 101 const include = VideoInclude.BLACKLISTED
102 const privacyOneOf = [ VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.PUBLIC, VideoPrivacy.UNLISTED ]
103
104 const { data } = await server.videos.list({ token: server.accessToken, sort: '-publishedAt', include, privacyOneOf })
102 105
103 return data.find(v => v.name === liveDetails.name + ' - ' + new Date(liveDetails.publishedAt).toLocaleString()) 106 return data.find(v => v.name === liveDetails.name + ' - ' + new Date(liveDetails.publishedAt).toLocaleString())
104} 107}
diff --git a/shared/server-commands/videos/streaming-playlists-command.ts b/shared/server-commands/videos/streaming-playlists-command.ts
index 5d40d35cb..25e446e72 100644
--- a/shared/server-commands/videos/streaming-playlists-command.ts
+++ b/shared/server-commands/videos/streaming-playlists-command.ts
@@ -1,22 +1,42 @@
1import { wait } from '@shared/core-utils'
1import { HttpStatusCode } from '@shared/models' 2import { HttpStatusCode } from '@shared/models'
2import { unwrapBody, unwrapTextOrDecode, unwrapBodyOrDecodeToJSON } from '../requests' 3import { unwrapBody, unwrapBodyOrDecodeToJSON, unwrapTextOrDecode } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared' 4import { AbstractCommand, OverrideCommandOptions } from '../shared'
4 5
5export class StreamingPlaylistsCommand extends AbstractCommand { 6export class StreamingPlaylistsCommand extends AbstractCommand {
6 7
7 get (options: OverrideCommandOptions & { 8 async get (options: OverrideCommandOptions & {
8 url: string 9 url: string
10 withRetry?: boolean // default false
11 currentRetry?: number
9 }) { 12 }) {
10 return unwrapTextOrDecode(this.getRawRequest({ 13 const { withRetry, currentRetry = 1 } = options
11 ...options,
12 14
13 url: options.url, 15 try {
14 implicitToken: false, 16 const result = await unwrapTextOrDecode(this.getRawRequest({
15 defaultExpectedStatus: HttpStatusCode.OK_200 17 ...options,
16 })) 18
19 url: options.url,
20 implicitToken: false,
21 defaultExpectedStatus: HttpStatusCode.OK_200
22 }))
23
24 return result
25 } catch (err) {
26 if (!withRetry || currentRetry > 5) throw err
27
28 await wait(100)
29
30 return this.get({
31 ...options,
32
33 withRetry,
34 currentRetry: currentRetry + 1
35 })
36 }
17 } 37 }
18 38
19 getSegment (options: OverrideCommandOptions & { 39 getFragmentedSegment (options: OverrideCommandOptions & {
20 url: string 40 url: string
21 range?: string 41 range?: string
22 }) { 42 }) {
diff --git a/shared/server-commands/videos/video-token-command.ts b/shared/server-commands/videos/video-token-command.ts
new file mode 100644
index 000000000..0531bee65
--- /dev/null
+++ b/shared/server-commands/videos/video-token-command.ts
@@ -0,0 +1,31 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { HttpStatusCode, VideoToken } from '@shared/models'
4import { unwrapBody } from '../requests'
5import { AbstractCommand, OverrideCommandOptions } from '../shared'
6
7export class VideoTokenCommand extends AbstractCommand {
8
9 create (options: OverrideCommandOptions & {
10 videoId: number | string
11 }) {
12 const { videoId } = options
13 const path = '/api/v1/videos/' + videoId + '/token'
14
15 return unwrapBody<VideoToken>(this.postBodyRequest({
16 ...options,
17
18 path,
19 implicitToken: true,
20 defaultExpectedStatus: HttpStatusCode.OK_200
21 }))
22 }
23
24 async getVideoFileToken (options: OverrideCommandOptions & {
25 videoId: number | string
26 }) {
27 const { files } = await this.create(options)
28
29 return files.token
30 }
31}
diff --git a/shared/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts
index 168391523..590244484 100644
--- a/shared/server-commands/videos/videos-command.ts
+++ b/shared/server-commands/videos/videos-command.ts
@@ -4,7 +4,7 @@ import { expect } from 'chai'
4import { createReadStream, stat } from 'fs-extra' 4import { createReadStream, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got' 5import got, { Response as GotResponse } from 'got'
6import validator from 'validator' 6import validator from 'validator'
7import { buildAbsoluteFixturePath, omit, pick, wait } from '@shared/core-utils' 7import { buildAbsoluteFixturePath, getAllPrivacies, omit, pick, wait } from '@shared/core-utils'
8import { buildUUID } from '@shared/extra-utils' 8import { buildUUID } from '@shared/extra-utils'
9import { 9import {
10 HttpStatusCode, 10 HttpStatusCode,
@@ -15,6 +15,7 @@ import {
15 VideoCreateResult, 15 VideoCreateResult,
16 VideoDetails, 16 VideoDetails,
17 VideoFileMetadata, 17 VideoFileMetadata,
18 VideoInclude,
18 VideoPrivacy, 19 VideoPrivacy,
19 VideosCommonQuery, 20 VideosCommonQuery,
20 VideoTranscodingCreate 21 VideoTranscodingCreate
@@ -234,6 +235,22 @@ export class VideosCommand extends AbstractCommand {
234 }) 235 })
235 } 236 }
236 237
238 listAllForAdmin (options: OverrideCommandOptions & VideosCommonQuery = {}) {
239 const include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER
240 const nsfw = 'both'
241 const privacyOneOf = getAllPrivacies()
242
243 return this.list({
244 ...options,
245
246 include,
247 nsfw,
248 privacyOneOf,
249
250 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
251 })
252 }
253
237 listByAccount (options: OverrideCommandOptions & VideosCommonQuery & { 254 listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
238 handle: string 255 handle: string
239 }) { 256 }) {
@@ -342,8 +359,9 @@ export class VideosCommand extends AbstractCommand {
342 async upload (options: OverrideCommandOptions & { 359 async upload (options: OverrideCommandOptions & {
343 attributes?: VideoEdit 360 attributes?: VideoEdit
344 mode?: 'legacy' | 'resumable' // default legacy 361 mode?: 'legacy' | 'resumable' // default legacy
362 waitTorrentGeneration?: boolean // default true
345 } = {}) { 363 } = {}) {
346 const { mode = 'legacy' } = options 364 const { mode = 'legacy', waitTorrentGeneration = true } = options
347 let defaultChannelId = 1 365 let defaultChannelId = 1
348 366
349 try { 367 try {
@@ -377,7 +395,7 @@ export class VideosCommand extends AbstractCommand {
377 395
378 // Wait torrent generation 396 // Wait torrent generation
379 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 }) 397 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
380 if (expectedStatus === HttpStatusCode.OK_200) { 398 if (expectedStatus === HttpStatusCode.OK_200 && waitTorrentGeneration) {
381 let video: VideoDetails 399 let video: VideoDetails
382 400
383 do { 401 do {
@@ -692,6 +710,7 @@ export class VideosCommand extends AbstractCommand {
692 'categoryOneOf', 710 'categoryOneOf',
693 'licenceOneOf', 711 'licenceOneOf',
694 'languageOneOf', 712 'languageOneOf',
713 'privacyOneOf',
695 'tagsOneOf', 714 'tagsOneOf',
696 'tagsAllOf', 715 'tagsAllOf',
697 'isLocal', 716 'isLocal',