aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared
diff options
context:
space:
mode:
Diffstat (limited to 'shared')
-rw-r--r--shared/models/activitypub/activity.ts31
-rw-r--r--shared/models/activitypub/objects/object.model.ts1
-rw-r--r--shared/models/actors/actor.model.ts2
-rw-r--r--shared/models/i18n/i18n.ts9
-rw-r--r--shared/models/server/contact-form.model.ts5
-rw-r--r--shared/models/server/custom-config.model.ts5
-rw-r--r--shared/models/server/index.ts6
-rw-r--r--shared/models/server/server-config.model.ts14
-rw-r--r--shared/models/server/server-stats.model.ts1
-rw-r--r--shared/models/users/index.ts2
-rw-r--r--shared/models/users/user-notification-setting.model.ts17
-rw-r--r--shared/models/users/user-notification.model.ts83
-rw-r--r--shared/models/users/user-right.enum.ts5
-rw-r--r--shared/models/users/user-role.ts3
-rw-r--r--shared/models/users/user-update-me.model.ts7
-rw-r--r--shared/models/users/user.model.ts6
-rw-r--r--shared/models/videos/blacklist/video-blacklist-create.model.ts1
-rw-r--r--shared/models/videos/blacklist/video-blacklist.model.ts1
-rw-r--r--shared/models/videos/video.model.ts4
-rw-r--r--shared/utils/cli/cli.ts24
-rw-r--r--shared/utils/feeds/feeds.ts32
-rw-r--r--shared/utils/index.ts23
-rw-r--r--shared/utils/miscs/email-child-process.js27
-rw-r--r--shared/utils/miscs/email.ts64
-rw-r--r--shared/utils/miscs/miscs.ts101
-rw-r--r--shared/utils/miscs/sql.ts38
-rw-r--r--shared/utils/miscs/stubs.ts14
-rw-r--r--shared/utils/overviews/overviews.ts18
-rw-r--r--shared/utils/requests/activitypub.ts43
-rw-r--r--shared/utils/requests/check-api-params.ts40
-rw-r--r--shared/utils/requests/requests.ts168
-rw-r--r--shared/utils/search/video-channels.ts22
-rw-r--r--shared/utils/search/videos.ts77
-rw-r--r--shared/utils/server/activitypub.ts14
-rw-r--r--shared/utils/server/clients.ts19
-rw-r--r--shared/utils/server/config.ts139
-rw-r--r--shared/utils/server/contact-form.ts28
-rw-r--r--shared/utils/server/follows.ts79
-rw-r--r--shared/utils/server/jobs.ts82
-rw-r--r--shared/utils/server/redundancy.ts17
-rw-r--r--shared/utils/server/servers.ts210
-rw-r--r--shared/utils/server/stats.ts22
-rw-r--r--shared/utils/socket/socket-io.ts13
-rw-r--r--shared/utils/users/accounts.ts63
-rw-r--r--shared/utils/users/blocklist.ts197
-rw-r--r--shared/utils/users/login.ts62
-rw-r--r--shared/utils/users/user-notifications.ts437
-rw-r--r--shared/utils/users/user-subscriptions.ts82
-rw-r--r--shared/utils/users/users.ts302
-rw-r--r--shared/utils/videos/services.ts23
-rw-r--r--shared/utils/videos/video-abuses.ts65
-rw-r--r--shared/utils/videos/video-blacklist.ts74
-rw-r--r--shared/utils/videos/video-captions.ts71
-rw-r--r--shared/utils/videos/video-change-ownership.ts54
-rw-r--r--shared/utils/videos/video-channels.ts118
-rw-r--r--shared/utils/videos/video-comments.ts87
-rw-r--r--shared/utils/videos/video-history.ts39
-rw-r--r--shared/utils/videos/video-imports.ts57
-rw-r--r--shared/utils/videos/videos.ts577
59 files changed, 3811 insertions, 14 deletions
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
index 44cb99efb..89994f665 100644
--- a/shared/models/activitypub/activity.ts
+++ b/shared/models/activitypub/activity.ts
@@ -5,12 +5,14 @@ import { DislikeObject } from './objects/dislike-object'
5import { VideoAbuseObject } from './objects/video-abuse-object' 5import { VideoAbuseObject } from './objects/video-abuse-object'
6import { VideoCommentObject } from './objects/video-comment-object' 6import { VideoCommentObject } from './objects/video-comment-object'
7import { ViewObject } from './objects/view-object' 7import { ViewObject } from './objects/view-object'
8import { APObject } from './objects/object.model'
8 9
9export type Activity = ActivityCreate | ActivityUpdate | 10export type Activity = ActivityCreate | ActivityUpdate |
10 ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce | 11 ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce |
11 ActivityUndo | ActivityLike | ActivityReject 12 ActivityUndo | ActivityLike | ActivityReject | ActivityView | ActivityDislike | ActivityFlag
12 13
13export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' | 'Reject' 14export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' | 'Like' | 'Reject' |
15 'View' | 'Dislike' | 'Flag'
14 16
15export interface ActivityAudience { 17export interface ActivityAudience {
16 to: string[] 18 to: string[]
@@ -59,15 +61,34 @@ export interface ActivityReject extends BaseActivity {
59 61
60export interface ActivityAnnounce extends BaseActivity { 62export interface ActivityAnnounce extends BaseActivity {
61 type: 'Announce' 63 type: 'Announce'
62 object: string | { id: string } 64 object: APObject
63} 65}
64 66
65export interface ActivityUndo extends BaseActivity { 67export interface ActivityUndo extends BaseActivity {
66 type: 'Undo', 68 type: 'Undo',
67 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce 69 object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce
68} 70}
69 71
70export interface ActivityLike extends BaseActivity { 72export interface ActivityLike extends BaseActivity {
71 type: 'Like', 73 type: 'Like',
72 object: string 74 object: APObject
75}
76
77export interface ActivityView extends BaseActivity {
78 type: 'View',
79 actor: string
80 object: APObject
81}
82
83export interface ActivityDislike extends BaseActivity {
84 id: string
85 type: 'Dislike'
86 actor: string
87 object: APObject
88}
89
90export interface ActivityFlag extends BaseActivity {
91 type: 'Flag',
92 content: string,
93 object: APObject
73} 94}
diff --git a/shared/models/activitypub/objects/object.model.ts b/shared/models/activitypub/objects/object.model.ts
new file mode 100644
index 000000000..3fd33800a
--- /dev/null
+++ b/shared/models/activitypub/objects/object.model.ts
@@ -0,0 +1 @@
export type APObject = string | { id: string }
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts
index 6b3b1b47c..a3953874d 100644
--- a/shared/models/actors/actor.model.ts
+++ b/shared/models/actors/actor.model.ts
@@ -10,5 +10,5 @@ export interface Actor {
10 followersCount: number 10 followersCount: number
11 createdAt: Date | string 11 createdAt: Date | string
12 updatedAt: Date | string 12 updatedAt: Date | string
13 avatar: Avatar 13 avatar?: Avatar
14} 14}
diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts
index 5c3249452..d7164b73f 100644
--- a/shared/models/i18n/i18n.ts
+++ b/shared/models/i18n/i18n.ts
@@ -8,12 +8,14 @@ export const I18N_LOCALES = {
8 'cs-CZ': 'Čeština', 8 'cs-CZ': 'Čeština',
9 'eo': 'Esperanto', 9 'eo': 'Esperanto',
10 'de-DE': 'Deutsch', 10 'de-DE': 'Deutsch',
11 'it-IT': 'Italiano',
11 'es-ES': 'Español', 12 'es-ES': 'Español',
12 'oc': 'Occitan', 13 'oc': 'Occitan',
13 'zh-Hant-TW': '繁體中文(台灣)', 14 'zh-Hant-TW': '繁體中文(台灣)',
14 'pt-BR': 'Português (Brasil)', 15 'pt-BR': 'Português (Brasil)',
15 'sv-SE': 'svenska', 16 'sv-SE': 'svenska',
16 // 'pl-PL': 'Polski' 17 'pl-PL': 'Polski',
18 'ru-RU': 'русский',
17 'zh-Hans-CN': '简体中文(中国)' 19 'zh-Hans-CN': '简体中文(中国)'
18} 20}
19 21
@@ -26,8 +28,9 @@ const I18N_LOCALE_ALIAS = {
26 'de': 'de-DE', 28 'de': 'de-DE',
27 'es': 'es-ES', 29 'es': 'es-ES',
28 'pt': 'pt-BR', 30 'pt': 'pt-BR',
29 'sv': 'sv-SE' 31 'sv': 'sv-SE',
30 // 'pl': 'pl-PL' 32 'pl': 'pl-PL',
33 'ru': 'ru-RU'
31} 34}
32 35
33export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES) 36export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
diff --git a/shared/models/server/contact-form.model.ts b/shared/models/server/contact-form.model.ts
new file mode 100644
index 000000000..0696be8b4
--- /dev/null
+++ b/shared/models/server/contact-form.model.ts
@@ -0,0 +1,5 @@
1export interface ContactForm {
2 fromEmail: string
3 fromName: string
4 body: string
5}
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 3afd36fcd..7a3eaa33f 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -41,6 +41,10 @@ export interface CustomConfig {
41 email: string 41 email: string
42 } 42 }
43 43
44 contactForm: {
45 enabled: boolean
46 }
47
44 user: { 48 user: {
45 videoQuota: number 49 videoQuota: number
46 videoQuotaDaily: number 50 videoQuotaDaily: number
@@ -48,6 +52,7 @@ export interface CustomConfig {
48 52
49 transcoding: { 53 transcoding: {
50 enabled: boolean 54 enabled: boolean
55 allowAdditionalExtensions: boolean
51 threads: number 56 threads: number
52 resolutions: { 57 resolutions: {
53 '240p': boolean 58 '240p': boolean
diff --git a/shared/models/server/index.ts b/shared/models/server/index.ts
new file mode 100644
index 000000000..c42f6f67f
--- /dev/null
+++ b/shared/models/server/index.ts
@@ -0,0 +1,6 @@
1export * from './about.model'
2export * from './contact-form.model'
3export * from './custom-config.model'
4export * from './job.model'
5export * from './server-config.model'
6export * from './server-stats.model'
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 91196c1eb..f4245ed4d 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -15,6 +15,14 @@ export interface ServerConfig {
15 } 15 }
16 } 16 }
17 17
18 email: {
19 enabled: boolean
20 }
21
22 contactForm: {
23 enabled: boolean
24 }
25
18 signup: { 26 signup: {
19 allowed: boolean, 27 allowed: boolean,
20 allowedForCurrentIP: boolean, 28 allowedForCurrentIP: boolean,
@@ -70,4 +78,10 @@ export interface ServerConfig {
70 videoQuota: number 78 videoQuota: number
71 videoQuotaDaily: number 79 videoQuotaDaily: number
72 } 80 }
81
82 trending: {
83 videos: {
84 intervalDays: number
85 }
86 }
73} 87}
diff --git a/shared/models/server/server-stats.model.ts b/shared/models/server/server-stats.model.ts
index a6bd2d4d3..74f3de5d3 100644
--- a/shared/models/server/server-stats.model.ts
+++ b/shared/models/server/server-stats.model.ts
@@ -5,6 +5,7 @@ export interface ServerStats {
5 totalLocalVideos: number 5 totalLocalVideos: number
6 totalLocalVideoViews: number 6 totalLocalVideoViews: number
7 totalLocalVideoComments: number 7 totalLocalVideoComments: number
8 totalLocalVideoFilesSize: number
8 9
9 totalVideos: number 10 totalVideos: number
10 totalVideoComments: number 11 totalVideoComments: number
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts
index 7114741e0..cd07cf320 100644
--- a/shared/models/users/index.ts
+++ b/shared/models/users/index.ts
@@ -1,6 +1,8 @@
1export * from './user.model' 1export * from './user.model'
2export * from './user-create.model' 2export * from './user-create.model'
3export * from './user-login.model' 3export * from './user-login.model'
4export * from './user-notification.model'
5export * from './user-notification-setting.model'
4export * from './user-refresh-token.model' 6export * from './user-refresh-token.model'
5export * from './user-update.model' 7export * from './user-update.model'
6export * from './user-update-me.model' 8export * from './user-update-me.model'
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
new file mode 100644
index 000000000..531e12bba
--- /dev/null
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -0,0 +1,17 @@
1export enum UserNotificationSettingValue {
2 NONE = 0,
3 WEB = 1 << 0,
4 EMAIL = 1 << 1
5}
6
7export interface UserNotificationSetting {
8 newVideoFromSubscription: UserNotificationSettingValue
9 newCommentOnMyVideo: UserNotificationSettingValue
10 videoAbuseAsModerator: UserNotificationSettingValue
11 blacklistOnMyVideo: UserNotificationSettingValue
12 myVideoPublished: UserNotificationSettingValue
13 myVideoImportFinished: UserNotificationSettingValue
14 newUserRegistration: UserNotificationSettingValue
15 newFollow: UserNotificationSettingValue
16 commentMention: UserNotificationSettingValue
17}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
new file mode 100644
index 000000000..186b62612
--- /dev/null
+++ b/shared/models/users/user-notification.model.ts
@@ -0,0 +1,83 @@
1export enum UserNotificationType {
2 NEW_VIDEO_FROM_SUBSCRIPTION = 1,
3 NEW_COMMENT_ON_MY_VIDEO = 2,
4 NEW_VIDEO_ABUSE_FOR_MODERATORS = 3,
5
6 BLACKLIST_ON_MY_VIDEO = 4,
7 UNBLACKLIST_ON_MY_VIDEO = 5,
8
9 MY_VIDEO_PUBLISHED = 6,
10
11 MY_VIDEO_IMPORT_SUCCESS = 7,
12 MY_VIDEO_IMPORT_ERROR = 8,
13
14 NEW_USER_REGISTRATION = 9,
15 NEW_FOLLOW = 10,
16 COMMENT_MENTION = 11
17}
18
19export interface VideoInfo {
20 id: number
21 uuid: string
22 name: string
23}
24
25export interface ActorInfo {
26 id: number
27 displayName: string
28 name: string
29 host: string
30 avatar?: {
31 path: string
32 }
33}
34
35export interface UserNotification {
36 id: number
37 type: UserNotificationType
38 read: boolean
39
40 video?: VideoInfo & {
41 channel: ActorInfo
42 }
43
44 videoImport?: {
45 id: number
46 video?: VideoInfo
47 torrentName?: string
48 magnetUri?: string
49 targetUrl?: string
50 }
51
52 comment?: {
53 id: number
54 threadId: number
55 account: ActorInfo
56 video: VideoInfo
57 }
58
59 videoAbuse?: {
60 id: number
61 video: VideoInfo
62 }
63
64 videoBlacklist?: {
65 id: number
66 video: VideoInfo
67 }
68
69 account?: ActorInfo
70
71 actorFollow?: {
72 id: number
73 follower: ActorInfo
74 following: {
75 type: 'account' | 'channel'
76 name: string
77 displayName: string
78 }
79 }
80
81 createdAt: string
82 updatedAt: string
83}
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index 51c59d20a..090256bca 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -2,10 +2,15 @@ export enum UserRight {
2 ALL, 2 ALL,
3 3
4 MANAGE_USERS, 4 MANAGE_USERS,
5
5 MANAGE_SERVER_FOLLOW, 6 MANAGE_SERVER_FOLLOW,
7
6 MANAGE_SERVER_REDUNDANCY, 8 MANAGE_SERVER_REDUNDANCY,
9
7 MANAGE_VIDEO_ABUSES, 10 MANAGE_VIDEO_ABUSES,
11
8 MANAGE_JOBS, 12 MANAGE_JOBS,
13
9 MANAGE_CONFIGURATION, 14 MANAGE_CONFIGURATION,
10 15
11 MANAGE_ACCOUNTS_BLOCKLIST, 16 MANAGE_ACCOUNTS_BLOCKLIST,
diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts
index adef8fd95..59c2ba106 100644
--- a/shared/models/users/user-role.ts
+++ b/shared/models/users/user-role.ts
@@ -29,7 +29,8 @@ const userRoleRights: { [ id: number ]: UserRight[] } = {
29 UserRight.UPDATE_ANY_VIDEO, 29 UserRight.UPDATE_ANY_VIDEO,
30 UserRight.SEE_ALL_VIDEOS, 30 UserRight.SEE_ALL_VIDEOS,
31 UserRight.MANAGE_ACCOUNTS_BLOCKLIST, 31 UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
32 UserRight.MANAGE_SERVERS_BLOCKLIST 32 UserRight.MANAGE_SERVERS_BLOCKLIST,
33 UserRight.MANAGE_USERS
33 ], 34 ],
34 35
35 [UserRole.USER]: [] 36 [UserRole.USER]: []
diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts
index 10edeee2e..e24afab94 100644
--- a/shared/models/users/user-update-me.model.ts
+++ b/shared/models/users/user-update-me.model.ts
@@ -3,9 +3,12 @@ import { NSFWPolicyType } from '../videos/nsfw-policy.type'
3export interface UserUpdateMe { 3export interface UserUpdateMe {
4 displayName?: string 4 displayName?: string
5 description?: string 5 description?: string
6 nsfwPolicy?: NSFWPolicyType, 6 nsfwPolicy?: NSFWPolicyType
7 webTorrentEnabled?: boolean, 7
8 webTorrentEnabled?: boolean
8 autoPlayVideo?: boolean 9 autoPlayVideo?: boolean
10 videosHistoryEnabled?: boolean
11
9 email?: string 12 email?: string
10 currentPassword?: string 13 currentPassword?: string
11 password?: string 14 password?: string
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index 82af17516..af783d389 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -2,6 +2,7 @@ import { Account } from '../actors'
2import { VideoChannel } from '../videos/channel/video-channel.model' 2import { VideoChannel } from '../videos/channel/video-channel.model'
3import { UserRole } from './user-role' 3import { UserRole } from './user-role'
4import { NSFWPolicyType } from '../videos/nsfw-policy.type' 4import { NSFWPolicyType } from '../videos/nsfw-policy.type'
5import { UserNotificationSetting } from './user-notification-setting.model'
5 6
6export interface User { 7export interface User {
7 id: number 8 id: number
@@ -9,12 +10,17 @@ export interface User {
9 email: string 10 email: string
10 emailVerified: boolean 11 emailVerified: boolean
11 nsfwPolicy: NSFWPolicyType 12 nsfwPolicy: NSFWPolicyType
13
12 autoPlayVideo: boolean 14 autoPlayVideo: boolean
15 webTorrentEnabled: boolean
16 videosHistoryEnabled: boolean
17
13 role: UserRole 18 role: UserRole
14 videoQuota: number 19 videoQuota: number
15 videoQuotaDaily: number 20 videoQuotaDaily: number
16 createdAt: Date 21 createdAt: Date
17 account: Account 22 account: Account
23 notificationSettings?: UserNotificationSetting
18 videoChannels?: VideoChannel[] 24 videoChannels?: VideoChannel[]
19 25
20 blocked: boolean 26 blocked: boolean
diff --git a/shared/models/videos/blacklist/video-blacklist-create.model.ts b/shared/models/videos/blacklist/video-blacklist-create.model.ts
index 89c69cb56..6e7d36421 100644
--- a/shared/models/videos/blacklist/video-blacklist-create.model.ts
+++ b/shared/models/videos/blacklist/video-blacklist-create.model.ts
@@ -1,3 +1,4 @@
1export interface VideoBlacklistCreate { 1export interface VideoBlacklistCreate {
2 reason?: string 2 reason?: string
3 unfederate?: boolean
3} 4}
diff --git a/shared/models/videos/blacklist/video-blacklist.model.ts b/shared/models/videos/blacklist/video-blacklist.model.ts
index ef4e5e3a2..4bd976190 100644
--- a/shared/models/videos/blacklist/video-blacklist.model.ts
+++ b/shared/models/videos/blacklist/video-blacklist.model.ts
@@ -2,6 +2,7 @@ export interface VideoBlacklist {
2 id: number 2 id: number
3 createdAt: Date 3 createdAt: Date
4 updatedAt: Date 4 updatedAt: Date
5 unfederated: boolean
5 reason?: string 6 reason?: string
6 7
7 video: { 8 video: {
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 4a9fa58b1..022876a0b 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -24,7 +24,7 @@ export interface VideoChannelAttribute {
24 displayName: string 24 displayName: string
25 url: string 25 url: string
26 host: string 26 host: string
27 avatar: Avatar 27 avatar?: Avatar
28} 28}
29 29
30export interface AccountAttribute { 30export interface AccountAttribute {
@@ -34,7 +34,7 @@ export interface AccountAttribute {
34 displayName: string 34 displayName: string
35 url: string 35 url: string
36 host: string 36 host: string
37 avatar: Avatar 37 avatar?: Avatar
38} 38}
39 39
40export interface Video { 40export interface Video {
diff --git a/shared/utils/cli/cli.ts b/shared/utils/cli/cli.ts
new file mode 100644
index 000000000..54d05e9c6
--- /dev/null
+++ b/shared/utils/cli/cli.ts
@@ -0,0 +1,24 @@
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.serverNumber}`
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/utils/feeds/feeds.ts b/shared/utils/feeds/feeds.ts
new file mode 100644
index 000000000..af6df2b20
--- /dev/null
+++ b/shared/utils/feeds/feeds.ts
@@ -0,0 +1,32 @@
1import * as request from 'supertest'
2
3type FeedType = 'videos' | 'video-comments'
4
5function getXMLfeed (url: string, feed: FeedType, format?: string) {
6 const path = '/feeds/' + feed + '.xml'
7
8 return request(url)
9 .get(path)
10 .query((format) ? { format: format } : {})
11 .set('Accept', 'application/xml')
12 .expect(200)
13 .expect('Content-Type', /xml/)
14}
15
16function getJSONfeed (url: string, feed: FeedType, query: any = {}) {
17 const path = '/feeds/' + feed + '.json'
18
19 return request(url)
20 .get(path)
21 .query(query)
22 .set('Accept', 'application/json')
23 .expect(200)
24 .expect('Content-Type', /json/)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 getXMLfeed,
31 getJSONfeed
32}
diff --git a/shared/utils/index.ts b/shared/utils/index.ts
new file mode 100644
index 000000000..e08bbfd2a
--- /dev/null
+++ b/shared/utils/index.ts
@@ -0,0 +1,23 @@
1export * from './server/activitypub'
2export * from './cli/cli'
3export * from './server/clients'
4export * from './server/config'
5export * from './server/jobs'
6export * from './users/login'
7export * from './miscs/miscs'
8export * from './miscs/stubs'
9export * from './miscs/sql'
10export * from './server/follows'
11export * from './requests/activitypub'
12export * from './requests/requests'
13export * from './requests/check-api-params'
14export * from './server/servers'
15export * from './videos/services'
16export * from './users/users'
17export * from './videos/video-abuses'
18export * from './videos/video-blacklist'
19export * from './videos/video-channels'
20export * from './videos/videos'
21export * from './videos/video-change-ownership'
22export * from './feeds/feeds'
23export * from './search/videos'
diff --git a/shared/utils/miscs/email-child-process.js b/shared/utils/miscs/email-child-process.js
new file mode 100644
index 000000000..40ae37d70
--- /dev/null
+++ b/shared/utils/miscs/email-child-process.js
@@ -0,0 +1,27 @@
1const MailDev = require('maildev')
2
3// must run maildev as forked ChildProcess
4// failed instantiation stops main process with exit code 0
5process.on('message', (msg) => {
6 if (msg.start) {
7 const maildev = new MailDev({
8 ip: '127.0.0.1',
9 smtp: 1025,
10 disableWeb: true,
11 silent: true
12 })
13
14 maildev.on('new', email => {
15 process.send({ email })
16 })
17
18 maildev.listen(err => {
19 if (err) {
20 // cannot send as Error object
21 return process.send({ err: err.message })
22 }
23
24 return process.send({ err: null })
25 })
26 }
27})
diff --git a/shared/utils/miscs/email.ts b/shared/utils/miscs/email.ts
new file mode 100644
index 000000000..f9f1bd95b
--- /dev/null
+++ b/shared/utils/miscs/email.ts
@@ -0,0 +1,64 @@
1import { fork, ChildProcess } from 'child_process'
2
3class MockSmtpServer {
4
5 private static instance: MockSmtpServer
6 private started = false
7 private emailChildProcess: ChildProcess
8 private emails: object[]
9
10 private constructor () {
11 this.emailChildProcess = fork(`${__dirname}/email-child-process`, [])
12
13 this.emailChildProcess.on('message', (msg) => {
14 if (msg.email) {
15 return this.emails.push(msg.email)
16 }
17 })
18
19 process.on('exit', () => this.kill())
20 }
21
22 collectEmails (emailsCollection: object[]) {
23 return new Promise((res, rej) => {
24 if (this.started) {
25 this.emails = emailsCollection
26 return res()
27 }
28
29 // ensure maildev isn't started until
30 // unexpected exit can be reported to test runner
31 this.emailChildProcess.send({ start: true })
32 this.emailChildProcess.on('exit', () => {
33 return rej(new Error('maildev exited unexpectedly, confirm port not in use'))
34 })
35 this.emailChildProcess.on('message', (msg) => {
36 if (msg.err) {
37 return rej(new Error(msg.err))
38 }
39 this.started = true
40 this.emails = emailsCollection
41 return res()
42 })
43 })
44 }
45
46 kill () {
47 if (!this.emailChildProcess) return
48
49 process.kill(this.emailChildProcess.pid)
50
51 this.emailChildProcess = null
52 MockSmtpServer.instance = null
53 }
54
55 static get Instance () {
56 return this.instance || (this.instance = new this())
57 }
58}
59
60// ---------------------------------------------------------------------------
61
62export {
63 MockSmtpServer
64}
diff --git a/shared/utils/miscs/miscs.ts b/shared/utils/miscs/miscs.ts
new file mode 100644
index 000000000..91a93b631
--- /dev/null
+++ b/shared/utils/miscs/miscs.ts
@@ -0,0 +1,101 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import { isAbsolute, join } from 'path'
5import * as request from 'supertest'
6import * as WebTorrent from 'webtorrent'
7import { pathExists, readFile } from 'fs-extra'
8import * as ffmpeg from 'fluent-ffmpeg'
9
10const expect = chai.expect
11let webtorrent = new WebTorrent()
12
13function immutableAssign <T, U> (target: T, source: U) {
14 return Object.assign<{}, T, U>({}, target, source)
15}
16
17 // Default interval -> 5 minutes
18function dateIsValid (dateString: string, interval = 300000) {
19 const dateToCheck = new Date(dateString)
20 const now = new Date()
21
22 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
23}
24
25function wait (milliseconds: number) {
26 return new Promise(resolve => setTimeout(resolve, milliseconds))
27}
28
29function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
30 if (refreshWebTorrent === true) webtorrent = new WebTorrent()
31
32 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
33}
34
35function root () {
36 // We are in /shared/utils/miscs
37 return join(__dirname, '..', '..', '..')
38}
39
40async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
41 const res = await request(url)
42 .get(imagePath)
43 .expect(200)
44
45 const body = res.body
46
47 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
48 const minLength = body.length - ((20 * body.length) / 100)
49 const maxLength = body.length + ((20 * body.length) / 100)
50
51 expect(data.length).to.be.above(minLength)
52 expect(data.length).to.be.below(maxLength)
53}
54
55function buildAbsoluteFixturePath (path: string, customTravisPath = false) {
56 if (isAbsolute(path)) {
57 return path
58 }
59
60 if (customTravisPath && process.env.TRAVIS) return join(process.env.HOME, 'fixtures', path)
61
62 return join(root(), 'server', 'tests', 'fixtures', path)
63}
64
65async function generateHighBitrateVideo () {
66 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
67
68 const exists = await pathExists(tempFixturePath)
69 if (!exists) {
70
71 // Generate a random, high bitrate video on the fly, so we don't have to include
72 // a large file in the repo. The video needs to have a certain minimum length so
73 // that FFmpeg properly applies bitrate limits.
74 // https://stackoverflow.com/a/15795112
75 return new Promise<string>(async (res, rej) => {
76 ffmpeg()
77 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
78 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
79 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
80 .output(tempFixturePath)
81 .on('error', rej)
82 .on('end', () => res(tempFixturePath))
83 .run()
84 })
85 }
86
87 return tempFixturePath
88}
89
90// ---------------------------------------------------------------------------
91
92export {
93 dateIsValid,
94 wait,
95 webtorrentAdd,
96 immutableAssign,
97 testImage,
98 buildAbsoluteFixturePath,
99 root,
100 generateHighBitrateVideo
101}
diff --git a/shared/utils/miscs/sql.ts b/shared/utils/miscs/sql.ts
new file mode 100644
index 000000000..027f78131
--- /dev/null
+++ b/shared/utils/miscs/sql.ts
@@ -0,0 +1,38 @@
1import * as Sequelize from 'sequelize'
2
3function getSequelize (serverNumber: number) {
4 const dbname = 'peertube_test' + serverNumber
5 const username = 'peertube'
6 const password = 'peertube'
7 const host = 'localhost'
8 const port = 5432
9
10 return new Sequelize(dbname, username, password, {
11 dialect: 'postgres',
12 host,
13 port,
14 operatorsAliases: false,
15 logging: false
16 })
17}
18
19function setActorField (serverNumber: number, to: string, field: string, value: string) {
20 const seq = getSequelize(serverNumber)
21
22 const options = { type: Sequelize.QueryTypes.UPDATE }
23
24 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
25}
26
27function setVideoField (serverNumber: number, uuid: string, field: string, value: string) {
28 const seq = getSequelize(serverNumber)
29
30 const options = { type: Sequelize.QueryTypes.UPDATE }
31
32 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
33}
34
35export {
36 setVideoField,
37 setActorField
38}
diff --git a/shared/utils/miscs/stubs.ts b/shared/utils/miscs/stubs.ts
new file mode 100644
index 000000000..d1eb0e3b2
--- /dev/null
+++ b/shared/utils/miscs/stubs.ts
@@ -0,0 +1,14 @@
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/utils/overviews/overviews.ts b/shared/utils/overviews/overviews.ts
new file mode 100644
index 000000000..23e3ceb1e
--- /dev/null
+++ b/shared/utils/overviews/overviews.ts
@@ -0,0 +1,18 @@
1import { makeGetRequest } from '../requests/requests'
2
3function getVideosOverview (url: string, useCache = false) {
4 const path = '/api/v1/overviews/videos'
5
6 const query = {
7 t: useCache ? undefined : new Date().getTime()
8 }
9
10 return makeGetRequest({
11 url,
12 path,
13 query,
14 statusCodeExpected: 200
15 })
16}
17
18export { getVideosOverview }
diff --git a/shared/utils/requests/activitypub.ts b/shared/utils/requests/activitypub.ts
new file mode 100644
index 000000000..e2348ace0
--- /dev/null
+++ b/shared/utils/requests/activitypub.ts
@@ -0,0 +1,43 @@
1import { doRequest } from '../../../server/helpers/requests'
2import { HTTP_SIGNATURE } from '../../../server/initializers'
3import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../server/helpers/activitypub'
5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = {
8 method: 'POST',
9 uri: url,
10 json: body,
11 httpSignature,
12 headers
13 }
14
15 return doRequest(options)
16}
17
18async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
19 const follow = {
20 type: 'Follow',
21 id: by.url + '/toto',
22 actor: by.url,
23 object: to.url
24 }
25
26 const body = activityPubContextify(follow)
27
28 const httpSignature = {
29 algorithm: HTTP_SIGNATURE.ALGORITHM,
30 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
31 keyId: by.url,
32 key: by.privateKey,
33 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
34 }
35 const headers = buildGlobalHeaders(body)
36
37 return makePOSTAPRequest(to.url, body, httpSignature, headers)
38}
39
40export {
41 makePOSTAPRequest,
42 makeFollowRequest
43}
diff --git a/shared/utils/requests/check-api-params.ts b/shared/utils/requests/check-api-params.ts
new file mode 100644
index 000000000..a2a549682
--- /dev/null
+++ b/shared/utils/requests/check-api-params.ts
@@ -0,0 +1,40 @@
1import { makeGetRequest } from './requests'
2import { immutableAssign } from '../miscs/miscs'
3
4function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
5 return makeGetRequest({
6 url,
7 path,
8 token,
9 query: immutableAssign(query, { start: 'hello' }),
10 statusCodeExpected: 400
11 })
12}
13
14function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
15 return makeGetRequest({
16 url,
17 path,
18 token,
19 query: immutableAssign(query, { count: 'hello' }),
20 statusCodeExpected: 400
21 })
22}
23
24function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
25 return makeGetRequest({
26 url,
27 path,
28 token,
29 query: immutableAssign(query, { sort: 'hello' }),
30 statusCodeExpected: 400
31 })
32}
33
34// ---------------------------------------------------------------------------
35
36export {
37 checkBadStartPagination,
38 checkBadCountPagination,
39 checkBadSortPagination
40}
diff --git a/shared/utils/requests/requests.ts b/shared/utils/requests/requests.ts
new file mode 100644
index 000000000..77e9f6164
--- /dev/null
+++ b/shared/utils/requests/requests.ts
@@ -0,0 +1,168 @@
1import * as request from 'supertest'
2import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
3import { isAbsolute, join } from 'path'
4
5function makeGetRequest (options: {
6 url: string,
7 path: string,
8 query?: any,
9 token?: string,
10 statusCodeExpected?: number,
11 contentType?: string
12}) {
13 if (!options.statusCodeExpected) options.statusCodeExpected = 400
14 if (options.contentType === undefined) options.contentType = 'application/json'
15
16 const req = request(options.url)
17 .get(options.path)
18
19 if (options.contentType) req.set('Accept', options.contentType)
20 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
21 if (options.query) req.query(options.query)
22
23 return req.expect(options.statusCodeExpected)
24}
25
26function makeDeleteRequest (options: {
27 url: string,
28 path: string,
29 token?: string,
30 statusCodeExpected?: number
31}) {
32 if (!options.statusCodeExpected) options.statusCodeExpected = 400
33
34 const req = request(options.url)
35 .delete(options.path)
36 .set('Accept', 'application/json')
37
38 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
39
40 return req.expect(options.statusCodeExpected)
41}
42
43function makeUploadRequest (options: {
44 url: string,
45 method?: 'POST' | 'PUT',
46 path: string,
47 token?: string,
48 fields: { [ fieldName: string ]: any },
49 attaches: { [ attachName: string ]: any | any[] },
50 statusCodeExpected?: number
51}) {
52 if (!options.statusCodeExpected) options.statusCodeExpected = 400
53
54 let req: request.Test
55 if (options.method === 'PUT') {
56 req = request(options.url).put(options.path)
57 } else {
58 req = request(options.url).post(options.path)
59 }
60
61 req.set('Accept', 'application/json')
62
63 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
64
65 Object.keys(options.fields).forEach(field => {
66 const value = options.fields[field]
67
68 if (Array.isArray(value)) {
69 for (let i = 0; i < value.length; i++) {
70 req.field(field + '[' + i + ']', value[i])
71 }
72 } else {
73 req.field(field, value)
74 }
75 })
76
77 Object.keys(options.attaches).forEach(attach => {
78 const value = options.attaches[attach]
79 if (Array.isArray(value)) {
80 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
81 } else {
82 req.attach(attach, buildAbsoluteFixturePath(value))
83 }
84 })
85
86 return req.expect(options.statusCodeExpected)
87}
88
89function makePostBodyRequest (options: {
90 url: string,
91 path: string,
92 token?: string,
93 fields?: { [ fieldName: string ]: any },
94 statusCodeExpected?: number
95}) {
96 if (!options.fields) options.fields = {}
97 if (!options.statusCodeExpected) options.statusCodeExpected = 400
98
99 const req = request(options.url)
100 .post(options.path)
101 .set('Accept', 'application/json')
102
103 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
104
105 return req.send(options.fields)
106 .expect(options.statusCodeExpected)
107}
108
109function makePutBodyRequest (options: {
110 url: string,
111 path: string,
112 token?: string,
113 fields: { [ fieldName: string ]: any },
114 statusCodeExpected?: number
115}) {
116 if (!options.statusCodeExpected) options.statusCodeExpected = 400
117
118 const req = request(options.url)
119 .put(options.path)
120 .set('Accept', 'application/json')
121
122 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
123
124 return req.send(options.fields)
125 .expect(options.statusCodeExpected)
126}
127
128function makeHTMLRequest (url: string, path: string) {
129 return request(url)
130 .get(path)
131 .set('Accept', 'text/html')
132 .expect(200)
133}
134
135function updateAvatarRequest (options: {
136 url: string,
137 path: string,
138 accessToken: string,
139 fixture: string
140}) {
141 let filePath = ''
142 if (isAbsolute(options.fixture)) {
143 filePath = options.fixture
144 } else {
145 filePath = join(root(), 'server', 'tests', 'fixtures', options.fixture)
146 }
147
148 return makeUploadRequest({
149 url: options.url,
150 path: options.path,
151 token: options.accessToken,
152 fields: {},
153 attaches: { avatarfile: filePath },
154 statusCodeExpected: 200
155 })
156}
157
158// ---------------------------------------------------------------------------
159
160export {
161 makeHTMLRequest,
162 makeGetRequest,
163 makeUploadRequest,
164 makePostBodyRequest,
165 makePutBodyRequest,
166 makeDeleteRequest,
167 updateAvatarRequest
168}
diff --git a/shared/utils/search/video-channels.ts b/shared/utils/search/video-channels.ts
new file mode 100644
index 000000000..0532134ae
--- /dev/null
+++ b/shared/utils/search/video-channels.ts
@@ -0,0 +1,22 @@
1import { makeGetRequest } from '../requests/requests'
2
3function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = 200) {
4 const path = '/api/v1/search/video-channels'
5
6 return makeGetRequest({
7 url,
8 path,
9 query: {
10 sort: '-createdAt',
11 search
12 },
13 token,
14 statusCodeExpected
15 })
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 searchVideoChannel
22}
diff --git a/shared/utils/search/videos.ts b/shared/utils/search/videos.ts
new file mode 100644
index 000000000..ba4627017
--- /dev/null
+++ b/shared/utils/search/videos.ts
@@ -0,0 +1,77 @@
1/* tslint:disable:no-unused-expression */
2
3import * as request from 'supertest'
4import { VideosSearchQuery } from '../../models/search'
5import { immutableAssign } from '../miscs/miscs'
6
7function searchVideo (url: string, search: string) {
8 const path = '/api/v1/search/videos'
9 const req = request(url)
10 .get(path)
11 .query({ sort: '-publishedAt', search })
12 .set('Accept', 'application/json')
13
14 return req.expect(200)
15 .expect('Content-Type', /json/)
16}
17
18function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
19 const path = '/api/v1/search/videos'
20 const req = request(url)
21 .get(path)
22 .set('Authorization', 'Bearer ' + token)
23 .query(immutableAssign(query, { sort: '-publishedAt', search }))
24 .set('Accept', 'application/json')
25
26 return req.expect(200)
27 .expect('Content-Type', /json/)
28}
29
30function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
31 const path = '/api/v1/search/videos'
32
33 const req = request(url)
34 .get(path)
35 .query({ start })
36 .query({ search })
37 .query({ count })
38
39 if (sort) req.query({ sort })
40
41 return req.set('Accept', 'application/json')
42 .expect(200)
43 .expect('Content-Type', /json/)
44}
45
46function searchVideoWithSort (url: string, search: string, sort: string) {
47 const path = '/api/v1/search/videos'
48
49 return request(url)
50 .get(path)
51 .query({ search })
52 .query({ sort })
53 .set('Accept', 'application/json')
54 .expect(200)
55 .expect('Content-Type', /json/)
56}
57
58function advancedVideosSearch (url: string, options: VideosSearchQuery) {
59 const path = '/api/v1/search/videos'
60
61 return request(url)
62 .get(path)
63 .query(options)
64 .set('Accept', 'application/json')
65 .expect(200)
66 .expect('Content-Type', /json/)
67}
68
69// ---------------------------------------------------------------------------
70
71export {
72 searchVideo,
73 advancedVideosSearch,
74 searchVideoWithToken,
75 searchVideoWithPagination,
76 searchVideoWithSort
77}
diff --git a/shared/utils/server/activitypub.ts b/shared/utils/server/activitypub.ts
new file mode 100644
index 000000000..eccb198ca
--- /dev/null
+++ b/shared/utils/server/activitypub.ts
@@ -0,0 +1,14 @@
1import * as request from 'supertest'
2
3function makeActivityPubGetRequest (url: string, path: string, expectedStatus = 200) {
4 return request(url)
5 .get(path)
6 .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8')
7 .expect(expectedStatus)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 makeActivityPubGetRequest
14}
diff --git a/shared/utils/server/clients.ts b/shared/utils/server/clients.ts
new file mode 100644
index 000000000..273aac747
--- /dev/null
+++ b/shared/utils/server/clients.ts
@@ -0,0 +1,19 @@
1import * as request from 'supertest'
2import * as urlUtil from 'url'
3
4function getClient (url: string) {
5 const path = '/api/v1/oauth-clients/local'
6
7 return request(url)
8 .get(path)
9 .set('Host', urlUtil.parse(url).host)
10 .set('Accept', 'application/json')
11 .expect(200)
12 .expect('Content-Type', /json/)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 getClient
19}
diff --git a/shared/utils/server/config.ts b/shared/utils/server/config.ts
new file mode 100644
index 000000000..0c5512bab
--- /dev/null
+++ b/shared/utils/server/config.ts
@@ -0,0 +1,139 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../models/server/custom-config.model'
3
4function getConfig (url: string) {
5 const path = '/api/v1/config'
6
7 return makeGetRequest({
8 url,
9 path,
10 statusCodeExpected: 200
11 })
12}
13
14function getAbout (url: string) {
15 const path = '/api/v1/config/about'
16
17 return makeGetRequest({
18 url,
19 path,
20 statusCodeExpected: 200
21 })
22}
23
24function getCustomConfig (url: string, token: string, statusCodeExpected = 200) {
25 const path = '/api/v1/config/custom'
26
27 return makeGetRequest({
28 url,
29 token,
30 path,
31 statusCodeExpected
32 })
33}
34
35function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = 200) {
36 const path = '/api/v1/config/custom'
37
38 return makePutBodyRequest({
39 url,
40 token,
41 path,
42 fields: newCustomConfig,
43 statusCodeExpected
44 })
45}
46
47function updateCustomSubConfig (url: string, token: string, newConfig: any) {
48 const updateParams: CustomConfig = {
49 instance: {
50 name: 'PeerTube updated',
51 shortDescription: 'my short description',
52 description: 'my super description',
53 terms: 'my super terms',
54 defaultClientRoute: '/videos/recently-added',
55 defaultNSFWPolicy: 'blur',
56 customizations: {
57 javascript: 'alert("coucou")',
58 css: 'body { background-color: red; }'
59 }
60 },
61 services: {
62 twitter: {
63 username: '@MySuperUsername',
64 whitelisted: true
65 }
66 },
67 cache: {
68 previews: {
69 size: 2
70 },
71 captions: {
72 size: 3
73 }
74 },
75 signup: {
76 enabled: false,
77 limit: 5,
78 requiresEmailVerification: false
79 },
80 admin: {
81 email: 'superadmin1@example.com'
82 },
83 contactForm: {
84 enabled: true
85 },
86 user: {
87 videoQuota: 5242881,
88 videoQuotaDaily: 318742
89 },
90 transcoding: {
91 enabled: true,
92 allowAdditionalExtensions: true,
93 threads: 1,
94 resolutions: {
95 '240p': false,
96 '360p': true,
97 '480p': true,
98 '720p': false,
99 '1080p': false
100 }
101 },
102 import: {
103 videos: {
104 http: {
105 enabled: false
106 },
107 torrent: {
108 enabled: false
109 }
110 }
111 }
112 }
113
114 Object.assign(updateParams, newConfig)
115
116 return updateCustomConfig(url, token, updateParams)
117}
118
119function deleteCustomConfig (url: string, token: string, statusCodeExpected = 200) {
120 const path = '/api/v1/config/custom'
121
122 return makeDeleteRequest({
123 url,
124 token,
125 path,
126 statusCodeExpected
127 })
128}
129
130// ---------------------------------------------------------------------------
131
132export {
133 getConfig,
134 getCustomConfig,
135 updateCustomConfig,
136 getAbout,
137 deleteCustomConfig,
138 updateCustomSubConfig
139}
diff --git a/shared/utils/server/contact-form.ts b/shared/utils/server/contact-form.ts
new file mode 100644
index 000000000..80394cf99
--- /dev/null
+++ b/shared/utils/server/contact-form.ts
@@ -0,0 +1,28 @@
1import * as request from 'supertest'
2import { ContactForm } from '../../models/server'
3
4function sendContactForm (options: {
5 url: string,
6 fromEmail: string,
7 fromName: string,
8 body: string,
9 expectedStatus?: number
10}) {
11 const path = '/api/v1/server/contact'
12
13 const body: ContactForm = {
14 fromEmail: options.fromEmail,
15 fromName: options.fromName,
16 body: options.body
17 }
18 return request(options.url)
19 .post(path)
20 .send(body)
21 .expect(options.expectedStatus || 204)
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 sendContactForm
28}
diff --git a/shared/utils/server/follows.ts b/shared/utils/server/follows.ts
new file mode 100644
index 000000000..7741757a6
--- /dev/null
+++ b/shared/utils/server/follows.ts
@@ -0,0 +1,79 @@
1import * as request from 'supertest'
2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs'
4
5function getFollowersListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
6 const path = '/api/v1/server/followers'
7
8 return request(url)
9 .get(path)
10 .query({ start })
11 .query({ count })
12 .query({ sort })
13 .query({ search })
14 .set('Accept', 'application/json')
15 .expect(200)
16 .expect('Content-Type', /json/)
17}
18
19function getFollowingListPaginationAndSort (url: string, start: number, count: number, sort: string, search?: string) {
20 const path = '/api/v1/server/following'
21
22 return request(url)
23 .get(path)
24 .query({ start })
25 .query({ count })
26 .query({ sort })
27 .query({ search })
28 .set('Accept', 'application/json')
29 .expect(200)
30 .expect('Content-Type', /json/)
31}
32
33async function follow (follower: string, following: string[], accessToken: string, expectedStatus = 204) {
34 const path = '/api/v1/server/following'
35
36 const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
37 const res = await request(follower)
38 .post(path)
39 .set('Accept', 'application/json')
40 .set('Authorization', 'Bearer ' + accessToken)
41 .send({ 'hosts': followingHosts })
42 .expect(expectedStatus)
43
44 return res
45}
46
47async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = 204) {
48 const path = '/api/v1/server/following/' + target.host
49
50 const res = await request(url)
51 .delete(path)
52 .set('Accept', 'application/json')
53 .set('Authorization', 'Bearer ' + accessToken)
54 .expect(expectedStatus)
55
56 return res
57}
58
59async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
60 await Promise.all([
61 follow(server1.url, [ server2.url ], server1.accessToken),
62 follow(server2.url, [ server1.url ], server2.accessToken)
63 ])
64
65 // Wait request propagation
66 await waitJobs([ server1, server2 ])
67
68 return true
69}
70
71// ---------------------------------------------------------------------------
72
73export {
74 getFollowersListPaginationAndSort,
75 getFollowingListPaginationAndSort,
76 unfollow,
77 follow,
78 doubleFollow
79}
diff --git a/shared/utils/server/jobs.ts b/shared/utils/server/jobs.ts
new file mode 100644
index 000000000..692b5e24d
--- /dev/null
+++ b/shared/utils/server/jobs.ts
@@ -0,0 +1,82 @@
1import * as request from 'supertest'
2import { Job, JobState } from '../../models'
3import { wait } from '../miscs/miscs'
4import { ServerInfo } from './servers'
5
6function getJobsList (url: string, accessToken: string, state: JobState) {
7 const path = '/api/v1/jobs/' + state
8
9 return request(url)
10 .get(path)
11 .set('Accept', 'application/json')
12 .set('Authorization', 'Bearer ' + accessToken)
13 .expect(200)
14 .expect('Content-Type', /json/)
15}
16
17function getJobsListPaginationAndSort (url: string, accessToken: string, state: JobState, start: number, count: number, sort: string) {
18 const path = '/api/v1/jobs/' + state
19
20 return request(url)
21 .get(path)
22 .query({ start })
23 .query({ count })
24 .query({ sort })
25 .set('Accept', 'application/json')
26 .set('Authorization', 'Bearer ' + accessToken)
27 .expect(200)
28 .expect('Content-Type', /json/)
29}
30
31async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
32 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) : 2000
33 let servers: ServerInfo[]
34
35 if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ]
36 else servers = serversArg as ServerInfo[]
37
38 const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
39 let pendingRequests = false
40
41 function tasksBuilder () {
42 const tasks: Promise<any>[] = []
43 pendingRequests = false
44
45 // Check if each server has pending request
46 for (const server of servers) {
47 for (const state of states) {
48 const p = getJobsListPaginationAndSort(server.url, server.accessToken, state, 0, 10, '-createdAt')
49 .then(res => res.body.data)
50 .then((jobs: Job[]) => jobs.filter(j => j.type !== 'videos-views'))
51 .then(jobs => {
52 if (jobs.length !== 0) pendingRequests = true
53 })
54 tasks.push(p)
55 }
56 }
57
58 return tasks
59 }
60
61 do {
62 await Promise.all(tasksBuilder())
63
64 // Retry, in case of new jobs were created
65 if (pendingRequests === false) {
66 await wait(pendingJobWait)
67 await Promise.all(tasksBuilder())
68 }
69
70 if (pendingRequests) {
71 await wait(1000)
72 }
73 } while (pendingRequests)
74}
75
76// ---------------------------------------------------------------------------
77
78export {
79 getJobsList,
80 waitJobs,
81 getJobsListPaginationAndSort
82}
diff --git a/shared/utils/server/redundancy.ts b/shared/utils/server/redundancy.ts
new file mode 100644
index 000000000..c39ff2c8b
--- /dev/null
+++ b/shared/utils/server/redundancy.ts
@@ -0,0 +1,17 @@
1import { makePutBodyRequest } from '../requests/requests'
2
3async function updateRedundancy (url: string, accessToken: string, host: string, redundancyAllowed: boolean, expectedStatus = 204) {
4 const path = '/api/v1/server/redundancy/' + host
5
6 return makePutBodyRequest({
7 url,
8 path,
9 token: accessToken,
10 fields: { redundancyAllowed },
11 statusCodeExpected: expectedStatus
12 })
13}
14
15export {
16 updateRedundancy
17}
diff --git a/shared/utils/server/servers.ts b/shared/utils/server/servers.ts
new file mode 100644
index 000000000..cb57e0a69
--- /dev/null
+++ b/shared/utils/server/servers.ts
@@ -0,0 +1,210 @@
1/* tslint:disable:no-unused-expression */
2
3import { ChildProcess, exec, fork } from 'child_process'
4import { join } from 'path'
5import { root, wait } from '../miscs/miscs'
6import { readdir, readFile } from 'fs-extra'
7import { existsSync } from 'fs'
8import { expect } from 'chai'
9
10interface ServerInfo {
11 app: ChildProcess,
12 url: string
13 host: string
14 serverNumber: number
15
16 client: {
17 id: string,
18 secret: string
19 }
20
21 user: {
22 username: string,
23 password: string,
24 email?: string
25 }
26
27 accessToken?: string
28
29 video?: {
30 id: number
31 uuid: string
32 name: string
33 account: {
34 name: string
35 }
36 }
37
38 remoteVideo?: {
39 id: number
40 uuid: string
41 }
42}
43
44function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
45 let apps = []
46 let i = 0
47
48 return new Promise<ServerInfo[]>(res => {
49 function anotherServerDone (serverNumber, app) {
50 apps[serverNumber - 1] = app
51 i++
52 if (i === totalServers) {
53 return res(apps)
54 }
55 }
56
57 flushTests()
58 .then(() => {
59 for (let j = 1; j <= totalServers; j++) {
60 runServer(j, configOverride).then(app => anotherServerDone(j, app))
61 }
62 })
63 })
64}
65
66function flushTests () {
67 return new Promise<void>((res, rej) => {
68 return exec('npm run clean:server:test', err => {
69 if (err) return rej(err)
70
71 return res()
72 })
73 })
74}
75
76function runServer (serverNumber: number, configOverride?: Object, args = []) {
77 const server: ServerInfo = {
78 app: null,
79 serverNumber: serverNumber,
80 url: `http://localhost:${9000 + serverNumber}`,
81 host: `localhost:${9000 + serverNumber}`,
82 client: {
83 id: null,
84 secret: null
85 },
86 user: {
87 username: null,
88 password: null
89 }
90 }
91
92 // These actions are async so we need to be sure that they have both been done
93 const serverRunString = {
94 'Server listening': false
95 }
96 const key = 'Database peertube_test' + serverNumber + ' is ready'
97 serverRunString[key] = false
98
99 const regexps = {
100 client_id: 'Client id: (.+)',
101 client_secret: 'Client secret: (.+)',
102 user_username: 'Username: (.+)',
103 user_password: 'User password: (.+)'
104 }
105
106 // Share the environment
107 const env = Object.create(process.env)
108 env['NODE_ENV'] = 'test'
109 env['NODE_APP_INSTANCE'] = serverNumber.toString()
110
111 if (configOverride !== undefined) {
112 env['NODE_CONFIG'] = JSON.stringify(configOverride)
113 }
114
115 const options = {
116 silent: true,
117 env: env,
118 detached: true
119 }
120
121 return new Promise<ServerInfo>(res => {
122 server.app = fork(join(root(), 'dist', 'server.js'), args, options)
123 server.app.stdout.on('data', function onStdout (data) {
124 let dontContinue = false
125
126 // Capture things if we want to
127 for (const key of Object.keys(regexps)) {
128 const regexp = regexps[key]
129 const matches = data.toString().match(regexp)
130 if (matches !== null) {
131 if (key === 'client_id') server.client.id = matches[1]
132 else if (key === 'client_secret') server.client.secret = matches[1]
133 else if (key === 'user_username') server.user.username = matches[1]
134 else if (key === 'user_password') server.user.password = matches[1]
135 }
136 }
137
138 // Check if all required sentences are here
139 for (const key of Object.keys(serverRunString)) {
140 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
141 if (serverRunString[key] === false) dontContinue = true
142 }
143
144 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
145 if (dontContinue === true) return
146
147 server.app.stdout.removeListener('data', onStdout)
148
149 process.on('exit', () => {
150 try {
151 process.kill(server.app.pid)
152 } catch { /* empty */ }
153 })
154
155 res(server)
156 })
157
158 })
159}
160
161async function reRunServer (server: ServerInfo, configOverride?: any) {
162 const newServer = await runServer(server.serverNumber, configOverride)
163 server.app = newServer.app
164
165 return server
166}
167
168async function checkTmpIsEmpty (server: ServerInfo) {
169 const testDirectory = 'test' + server.serverNumber
170
171 const directoryPath = join(root(), testDirectory, 'tmp')
172
173 const directoryExists = existsSync(directoryPath)
174 expect(directoryExists).to.be.true
175
176 const files = await readdir(directoryPath)
177 expect(files).to.have.lengthOf(0)
178}
179
180function killallServers (servers: ServerInfo[]) {
181 for (const server of servers) {
182 process.kill(-server.app.pid)
183 }
184}
185
186async function waitUntilLog (server: ServerInfo, str: string, count = 1) {
187 const logfile = join(root(), 'test' + server.serverNumber, 'logs/peertube.log')
188
189 while (true) {
190 const buf = await readFile(logfile)
191
192 const matches = buf.toString().match(new RegExp(str, 'g'))
193 if (matches && matches.length === count) return
194
195 await wait(1000)
196 }
197}
198
199// ---------------------------------------------------------------------------
200
201export {
202 checkTmpIsEmpty,
203 ServerInfo,
204 flushAndRunMultipleServers,
205 flushTests,
206 runServer,
207 killallServers,
208 reRunServer,
209 waitUntilLog
210}
diff --git a/shared/utils/server/stats.ts b/shared/utils/server/stats.ts
new file mode 100644
index 000000000..6f079ad18
--- /dev/null
+++ b/shared/utils/server/stats.ts
@@ -0,0 +1,22 @@
1import { makeGetRequest } from '../requests/requests'
2
3function getStats (url: string, useCache = false) {
4 const path = '/api/v1/server/stats'
5
6 const query = {
7 t: useCache ? undefined : new Date().getTime()
8 }
9
10 return makeGetRequest({
11 url,
12 path,
13 query,
14 statusCodeExpected: 200
15 })
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 getStats
22}
diff --git a/shared/utils/socket/socket-io.ts b/shared/utils/socket/socket-io.ts
new file mode 100644
index 000000000..854ab71af
--- /dev/null
+++ b/shared/utils/socket/socket-io.ts
@@ -0,0 +1,13 @@
1import * as io from 'socket.io-client'
2
3function getUserNotificationSocket (serverUrl: string, accessToken: string) {
4 return io(serverUrl + '/user-notifications', {
5 query: { accessToken }
6 })
7}
8
9// ---------------------------------------------------------------------------
10
11export {
12 getUserNotificationSocket
13}
diff --git a/shared/utils/users/accounts.ts b/shared/utils/users/accounts.ts
new file mode 100644
index 000000000..388eb6973
--- /dev/null
+++ b/shared/utils/users/accounts.ts
@@ -0,0 +1,63 @@
1/* tslint:disable:no-unused-expression */
2
3import { expect } from 'chai'
4import { existsSync, readdir } from 'fs-extra'
5import { join } from 'path'
6import { Account } from '../../models/actors'
7import { root } from '../miscs/miscs'
8import { makeGetRequest } from '../requests/requests'
9
10function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = 200) {
11 const path = '/api/v1/accounts'
12
13 return makeGetRequest({
14 url,
15 query: { sort },
16 path,
17 statusCodeExpected
18 })
19}
20
21function getAccount (url: string, accountName: string, statusCodeExpected = 200) {
22 const path = '/api/v1/accounts/' + accountName
23
24 return makeGetRequest({
25 url,
26 path,
27 statusCodeExpected
28 })
29}
30
31async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) {
32 const res = await getAccountsList(url)
33 const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain)
34
35 const message = `${nameWithDomain} on ${url}`
36 expect(account.followersCount).to.equal(followersCount, message)
37 expect(account.followingCount).to.equal(followingCount, message)
38}
39
40async function checkActorFilesWereRemoved (actorUUID: string, serverNumber: number) {
41 const testDirectory = 'test' + serverNumber
42
43 for (const directory of [ 'avatars' ]) {
44 const directoryPath = join(root(), testDirectory, directory)
45
46 const directoryExists = existsSync(directoryPath)
47 expect(directoryExists).to.be.true
48
49 const files = await readdir(directoryPath)
50 for (const file of files) {
51 expect(file).to.not.contain(actorUUID)
52 }
53 }
54}
55
56// ---------------------------------------------------------------------------
57
58export {
59 getAccount,
60 expectAccountFollows,
61 getAccountsList,
62 checkActorFilesWereRemoved
63}
diff --git a/shared/utils/users/blocklist.ts b/shared/utils/users/blocklist.ts
new file mode 100644
index 000000000..5feb84179
--- /dev/null
+++ b/shared/utils/users/blocklist.ts
@@ -0,0 +1,197 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests'
4
5function getAccountBlocklistByAccount (
6 url: string,
7 token: string,
8 start: number,
9 count: number,
10 sort = '-createdAt',
11 statusCodeExpected = 200
12) {
13 const path = '/api/v1/users/me/blocklist/accounts'
14
15 return makeGetRequest({
16 url,
17 token,
18 query: { start, count, sort },
19 path,
20 statusCodeExpected
21 })
22}
23
24function addAccountToAccountBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
25 const path = '/api/v1/users/me/blocklist/accounts'
26
27 return makePostBodyRequest({
28 url,
29 path,
30 token,
31 fields: {
32 accountName: accountToBlock
33 },
34 statusCodeExpected
35 })
36}
37
38function removeAccountFromAccountBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
39 const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
40
41 return makeDeleteRequest({
42 url,
43 path,
44 token,
45 statusCodeExpected
46 })
47}
48
49function getServerBlocklistByAccount (
50 url: string,
51 token: string,
52 start: number,
53 count: number,
54 sort = '-createdAt',
55 statusCodeExpected = 200
56) {
57 const path = '/api/v1/users/me/blocklist/servers'
58
59 return makeGetRequest({
60 url,
61 token,
62 query: { start, count, sort },
63 path,
64 statusCodeExpected
65 })
66}
67
68function addServerToAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
69 const path = '/api/v1/users/me/blocklist/servers'
70
71 return makePostBodyRequest({
72 url,
73 path,
74 token,
75 fields: {
76 host: serverToBlock
77 },
78 statusCodeExpected
79 })
80}
81
82function removeServerFromAccountBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
83 const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
84
85 return makeDeleteRequest({
86 url,
87 path,
88 token,
89 statusCodeExpected
90 })
91}
92
93function getAccountBlocklistByServer (
94 url: string,
95 token: string,
96 start: number,
97 count: number,
98 sort = '-createdAt',
99 statusCodeExpected = 200
100) {
101 const path = '/api/v1/server/blocklist/accounts'
102
103 return makeGetRequest({
104 url,
105 token,
106 query: { start, count, sort },
107 path,
108 statusCodeExpected
109 })
110}
111
112function addAccountToServerBlocklist (url: string, token: string, accountToBlock: string, statusCodeExpected = 204) {
113 const path = '/api/v1/server/blocklist/accounts'
114
115 return makePostBodyRequest({
116 url,
117 path,
118 token,
119 fields: {
120 accountName: accountToBlock
121 },
122 statusCodeExpected
123 })
124}
125
126function removeAccountFromServerBlocklist (url: string, token: string, accountToUnblock: string, statusCodeExpected = 204) {
127 const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock
128
129 return makeDeleteRequest({
130 url,
131 path,
132 token,
133 statusCodeExpected
134 })
135}
136
137function getServerBlocklistByServer (
138 url: string,
139 token: string,
140 start: number,
141 count: number,
142 sort = '-createdAt',
143 statusCodeExpected = 200
144) {
145 const path = '/api/v1/server/blocklist/servers'
146
147 return makeGetRequest({
148 url,
149 token,
150 query: { start, count, sort },
151 path,
152 statusCodeExpected
153 })
154}
155
156function addServerToServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
157 const path = '/api/v1/server/blocklist/servers'
158
159 return makePostBodyRequest({
160 url,
161 path,
162 token,
163 fields: {
164 host: serverToBlock
165 },
166 statusCodeExpected
167 })
168}
169
170function removeServerFromServerBlocklist (url: string, token: string, serverToBlock: string, statusCodeExpected = 204) {
171 const path = '/api/v1/server/blocklist/servers/' + serverToBlock
172
173 return makeDeleteRequest({
174 url,
175 path,
176 token,
177 statusCodeExpected
178 })
179}
180
181// ---------------------------------------------------------------------------
182
183export {
184 getAccountBlocklistByAccount,
185 addAccountToAccountBlocklist,
186 removeAccountFromAccountBlocklist,
187 getServerBlocklistByAccount,
188 addServerToAccountBlocklist,
189 removeServerFromAccountBlocklist,
190
191 getAccountBlocklistByServer,
192 addAccountToServerBlocklist,
193 removeAccountFromServerBlocklist,
194 getServerBlocklistByServer,
195 addServerToServerBlocklist,
196 removeServerFromServerBlocklist
197}
diff --git a/shared/utils/users/login.ts b/shared/utils/users/login.ts
new file mode 100644
index 000000000..ddeb9df2a
--- /dev/null
+++ b/shared/utils/users/login.ts
@@ -0,0 +1,62 @@
1import * as request from 'supertest'
2
3import { ServerInfo } from '../server/servers'
4
5type Client = { id: string, secret: string }
6type User = { username: string, password: string }
7type Server = { url: string, client: Client, user: User }
8
9function login (url: string, client: Client, user: User, expectedStatus = 200) {
10 const path = '/api/v1/users/token'
11
12 const body = {
13 client_id: client.id,
14 client_secret: client.secret,
15 username: user.username,
16 password: user.password,
17 response_type: 'code',
18 grant_type: 'password',
19 scope: 'upload'
20 }
21
22 return request(url)
23 .post(path)
24 .type('form')
25 .send(body)
26 .expect(expectedStatus)
27}
28
29async function serverLogin (server: Server) {
30 const res = await login(server.url, server.client, server.user, 200)
31
32 return res.body.access_token as string
33}
34
35async function userLogin (server: Server, user: User, expectedStatus = 200) {
36 const res = await login(server.url, server.client, user, expectedStatus)
37
38 return res.body.access_token as string
39}
40
41function setAccessTokensToServers (servers: ServerInfo[]) {
42 const tasks: Promise<any>[] = []
43
44 for (const server of servers) {
45 const p = serverLogin(server).then(t => server.accessToken = t)
46 tasks.push(p)
47 }
48
49 return Promise.all(tasks)
50}
51
52// ---------------------------------------------------------------------------
53
54export {
55 login,
56 serverLogin,
57 userLogin,
58 setAccessTokensToServers,
59 Server,
60 Client,
61 User
62}
diff --git a/shared/utils/users/user-notifications.ts b/shared/utils/users/user-notifications.ts
new file mode 100644
index 000000000..c8ed7df30
--- /dev/null
+++ b/shared/utils/users/user-notifications.ts
@@ -0,0 +1,437 @@
1/* tslint:disable:no-unused-expression */
2
3import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
4import { UserNotification, UserNotificationSetting, UserNotificationType } from '../../models/users'
5import { ServerInfo } from '..'
6import { expect } from 'chai'
7import { inspect } from 'util'
8
9function updateMyNotificationSettings (url: string, token: string, settings: UserNotificationSetting, statusCodeExpected = 204) {
10 const path = '/api/v1/users/me/notification-settings'
11
12 return makePutBodyRequest({
13 url,
14 path,
15 token,
16 fields: settings,
17 statusCodeExpected
18 })
19}
20
21function getUserNotifications (
22 url: string,
23 token: string,
24 start: number,
25 count: number,
26 unread?: boolean,
27 sort = '-createdAt',
28 statusCodeExpected = 200
29) {
30 const path = '/api/v1/users/me/notifications'
31
32 return makeGetRequest({
33 url,
34 path,
35 token,
36 query: {
37 start,
38 count,
39 sort,
40 unread
41 },
42 statusCodeExpected
43 })
44}
45
46function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = 204) {
47 const path = '/api/v1/users/me/notifications/read'
48
49 return makePostBodyRequest({
50 url,
51 path,
52 token,
53 fields: { ids },
54 statusCodeExpected
55 })
56}
57function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = 204) {
58 const path = '/api/v1/users/me/notifications/read-all'
59
60 return makePostBodyRequest({
61 url,
62 path,
63 token,
64 statusCodeExpected
65 })
66}
67
68async function getLastNotification (serverUrl: string, accessToken: string) {
69 const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
70
71 if (res.body.total === 0) return undefined
72
73 return res.body.data[0] as UserNotification
74}
75
76type CheckerBaseParams = {
77 server: ServerInfo
78 emails: object[]
79 socketNotifications: UserNotification[]
80 token: string,
81 check?: { web: boolean, mail: boolean }
82}
83
84type CheckerType = 'presence' | 'absence'
85
86async function checkNotification (
87 base: CheckerBaseParams,
88 notificationChecker: (notification: UserNotification, type: CheckerType) => void,
89 emailNotificationFinder: (email: object) => boolean,
90 checkType: CheckerType
91) {
92 const check = base.check || { web: true, mail: true }
93
94 if (check.web) {
95 const notification = await getLastNotification(base.server.url, base.token)
96
97 if (notification || checkType !== 'absence') {
98 notificationChecker(notification, checkType)
99 }
100
101 const socketNotification = base.socketNotifications.find(n => {
102 try {
103 notificationChecker(n, 'presence')
104 return true
105 } catch {
106 return false
107 }
108 })
109
110 if (checkType === 'presence') {
111 const obj = inspect(base.socketNotifications, { depth: 5 })
112 expect(socketNotification, 'The socket notification is absent. ' + obj).to.not.be.undefined
113 } else {
114 const obj = inspect(socketNotification, { depth: 5 })
115 expect(socketNotification, 'The socket notification is present. ' + obj).to.be.undefined
116 }
117 }
118
119 if (check.mail) {
120 // Last email
121 const email = base.emails
122 .slice()
123 .reverse()
124 .find(e => emailNotificationFinder(e))
125
126 if (checkType === 'presence') {
127 expect(email, 'The email is absent. ' + inspect(base.emails)).to.not.be.undefined
128 } else {
129 expect(email, 'The email is present. ' + inspect(email)).to.be.undefined
130 }
131 }
132}
133
134function checkVideo (video: any, videoName?: string, videoUUID?: string) {
135 expect(video.name).to.be.a('string')
136 expect(video.name).to.not.be.empty
137 if (videoName) expect(video.name).to.equal(videoName)
138
139 expect(video.uuid).to.be.a('string')
140 expect(video.uuid).to.not.be.empty
141 if (videoUUID) expect(video.uuid).to.equal(videoUUID)
142
143 expect(video.id).to.be.a('number')
144}
145
146function checkActor (actor: any) {
147 expect(actor.displayName).to.be.a('string')
148 expect(actor.displayName).to.not.be.empty
149 expect(actor.host).to.not.be.undefined
150}
151
152function checkComment (comment: any, commentId: number, threadId: number) {
153 expect(comment.id).to.equal(commentId)
154 expect(comment.threadId).to.equal(threadId)
155}
156
157async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
158 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
159
160 function notificationChecker (notification: UserNotification, type: CheckerType) {
161 if (type === 'presence') {
162 expect(notification).to.not.be.undefined
163 expect(notification.type).to.equal(notificationType)
164
165 checkVideo(notification.video, videoName, videoUUID)
166 checkActor(notification.video.channel)
167 } else {
168 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
169 }
170 }
171
172 function emailFinder (email: object) {
173 return email[ 'text' ].indexOf(videoUUID) !== -1
174 }
175
176 await checkNotification(base, notificationChecker, emailFinder, type)
177}
178
179async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
180 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
181
182 function notificationChecker (notification: UserNotification, type: CheckerType) {
183 if (type === 'presence') {
184 expect(notification).to.not.be.undefined
185 expect(notification.type).to.equal(notificationType)
186
187 checkVideo(notification.video, videoName, videoUUID)
188 checkActor(notification.video.channel)
189 } else {
190 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
191 }
192 }
193
194 function emailFinder (email: object) {
195 const text: string = email[ 'text' ]
196 return text.includes(videoUUID) && text.includes('Your video')
197 }
198
199 await checkNotification(base, notificationChecker, emailFinder, type)
200}
201
202async function checkMyVideoImportIsFinished (
203 base: CheckerBaseParams,
204 videoName: string,
205 videoUUID: string,
206 url: string,
207 success: boolean,
208 type: CheckerType
209) {
210 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
211
212 function notificationChecker (notification: UserNotification, type: CheckerType) {
213 if (type === 'presence') {
214 expect(notification).to.not.be.undefined
215 expect(notification.type).to.equal(notificationType)
216
217 expect(notification.videoImport.targetUrl).to.equal(url)
218
219 if (success) checkVideo(notification.videoImport.video, videoName, videoUUID)
220 } else {
221 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
222 }
223 }
224
225 function emailFinder (email: object) {
226 const text: string = email[ 'text' ]
227 const toFind = success ? ' finished' : ' error'
228
229 return text.includes(url) && text.includes(toFind)
230 }
231
232 await checkNotification(base, notificationChecker, emailFinder, type)
233}
234
235async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) {
236 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
237
238 function notificationChecker (notification: UserNotification, type: CheckerType) {
239 if (type === 'presence') {
240 expect(notification).to.not.be.undefined
241 expect(notification.type).to.equal(notificationType)
242
243 checkActor(notification.account)
244 expect(notification.account.name).to.equal(username)
245 } else {
246 expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
247 }
248 }
249
250 function emailFinder (email: object) {
251 const text: string = email[ 'text' ]
252
253 return text.includes(' registered ') && text.includes(username)
254 }
255
256 await checkNotification(base, notificationChecker, emailFinder, type)
257}
258
259async function checkNewActorFollow (
260 base: CheckerBaseParams,
261 followType: 'channel' | 'account',
262 followerName: string,
263 followerDisplayName: string,
264 followingDisplayName: string,
265 type: CheckerType
266) {
267 const notificationType = UserNotificationType.NEW_FOLLOW
268
269 function notificationChecker (notification: UserNotification, type: CheckerType) {
270 if (type === 'presence') {
271 expect(notification).to.not.be.undefined
272 expect(notification.type).to.equal(notificationType)
273
274 checkActor(notification.actorFollow.follower)
275 expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
276 expect(notification.actorFollow.follower.name).to.equal(followerName)
277 expect(notification.actorFollow.follower.host).to.not.be.undefined
278
279 expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName)
280 expect(notification.actorFollow.following.type).to.equal(followType)
281 } else {
282 expect(notification).to.satisfy(n => {
283 return n.type !== notificationType ||
284 (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
285 })
286 }
287 }
288
289 function emailFinder (email: object) {
290 const text: string = email[ 'text' ]
291
292 return text.includes('Your ' + followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
293 }
294
295 await checkNotification(base, notificationChecker, emailFinder, type)
296}
297
298async function checkCommentMention (
299 base: CheckerBaseParams,
300 uuid: string,
301 commentId: number,
302 threadId: number,
303 byAccountDisplayName: string,
304 type: CheckerType
305) {
306 const notificationType = UserNotificationType.COMMENT_MENTION
307
308 function notificationChecker (notification: UserNotification, type: CheckerType) {
309 if (type === 'presence') {
310 expect(notification).to.not.be.undefined
311 expect(notification.type).to.equal(notificationType)
312
313 checkComment(notification.comment, commentId, threadId)
314 checkActor(notification.comment.account)
315 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
316
317 checkVideo(notification.comment.video, undefined, uuid)
318 } else {
319 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
320 }
321 }
322
323 function emailFinder (email: object) {
324 const text: string = email[ 'text' ]
325
326 return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName)
327 }
328
329 await checkNotification(base, notificationChecker, emailFinder, type)
330}
331
332let lastEmailCount = 0
333async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
334 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
335
336 function notificationChecker (notification: UserNotification, type: CheckerType) {
337 if (type === 'presence') {
338 expect(notification).to.not.be.undefined
339 expect(notification.type).to.equal(notificationType)
340
341 checkComment(notification.comment, commentId, threadId)
342 checkActor(notification.comment.account)
343 checkVideo(notification.comment.video, undefined, uuid)
344 } else {
345 expect(notification).to.satisfy((n: UserNotification) => {
346 return n === undefined || n.comment === undefined || n.comment.id !== commentId
347 })
348 }
349 }
350
351 const commentUrl = `http://localhost:9001/videos/watch/${uuid};threadId=${threadId}`
352 function emailFinder (email: object) {
353 return email[ 'text' ].indexOf(commentUrl) !== -1
354 }
355
356 await checkNotification(base, notificationChecker, emailFinder, type)
357
358 if (type === 'presence') {
359 // We cannot detect email duplicates, so check we received another email
360 expect(base.emails).to.have.length.above(lastEmailCount)
361 lastEmailCount = base.emails.length
362 }
363}
364
365async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
366 const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS
367
368 function notificationChecker (notification: UserNotification, type: CheckerType) {
369 if (type === 'presence') {
370 expect(notification).to.not.be.undefined
371 expect(notification.type).to.equal(notificationType)
372
373 expect(notification.videoAbuse.id).to.be.a('number')
374 checkVideo(notification.videoAbuse.video, videoName, videoUUID)
375 } else {
376 expect(notification).to.satisfy((n: UserNotification) => {
377 return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID
378 })
379 }
380 }
381
382 function emailFinder (email: object) {
383 const text = email[ 'text' ]
384 return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
385 }
386
387 await checkNotification(base, notificationChecker, emailFinder, type)
388}
389
390async function checkNewBlacklistOnMyVideo (
391 base: CheckerBaseParams,
392 videoUUID: string,
393 videoName: string,
394 blacklistType: 'blacklist' | 'unblacklist'
395) {
396 const notificationType = blacklistType === 'blacklist'
397 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
398 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
399
400 function notificationChecker (notification: UserNotification) {
401 expect(notification).to.not.be.undefined
402 expect(notification.type).to.equal(notificationType)
403
404 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
405
406 checkVideo(video, videoName, videoUUID)
407 }
408
409 function emailFinder (email: object) {
410 const text = email[ 'text' ]
411 return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1
412 }
413
414 await checkNotification(base, notificationChecker, emailFinder, 'presence')
415}
416
417// ---------------------------------------------------------------------------
418
419export {
420 CheckerBaseParams,
421 CheckerType,
422 checkNotification,
423 markAsReadAllNotifications,
424 checkMyVideoImportIsFinished,
425 checkUserRegistered,
426 checkVideoIsPublished,
427 checkNewVideoFromSubscription,
428 checkNewActorFollow,
429 checkNewCommentOnMyVideo,
430 checkNewBlacklistOnMyVideo,
431 checkCommentMention,
432 updateMyNotificationSettings,
433 checkNewVideoAbuseForModerators,
434 getUserNotifications,
435 markAsReadNotifications,
436 getLastNotification
437}
diff --git a/shared/utils/users/user-subscriptions.ts b/shared/utils/users/user-subscriptions.ts
new file mode 100644
index 000000000..7148fbfca
--- /dev/null
+++ b/shared/utils/users/user-subscriptions.ts
@@ -0,0 +1,82 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests'
2
3function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = 204) {
4 const path = '/api/v1/users/me/subscriptions'
5
6 return makePostBodyRequest({
7 url,
8 path,
9 token,
10 statusCodeExpected,
11 fields: { uri: targetUri }
12 })
13}
14
15function listUserSubscriptions (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
16 const path = '/api/v1/users/me/subscriptions'
17
18 return makeGetRequest({
19 url,
20 path,
21 token,
22 statusCodeExpected,
23 query: { sort }
24 })
25}
26
27function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = 200) {
28 const path = '/api/v1/users/me/subscriptions/videos'
29
30 return makeGetRequest({
31 url,
32 path,
33 token,
34 statusCodeExpected,
35 query: { sort }
36 })
37}
38
39function getUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 200) {
40 const path = '/api/v1/users/me/subscriptions/' + uri
41
42 return makeGetRequest({
43 url,
44 path,
45 token,
46 statusCodeExpected
47 })
48}
49
50function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = 204) {
51 const path = '/api/v1/users/me/subscriptions/' + uri
52
53 return makeDeleteRequest({
54 url,
55 path,
56 token,
57 statusCodeExpected
58 })
59}
60
61function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = 200) {
62 const path = '/api/v1/users/me/subscriptions/exist'
63
64 return makeGetRequest({
65 url,
66 path,
67 query: { 'uris[]': uris },
68 token,
69 statusCodeExpected
70 })
71}
72
73// ---------------------------------------------------------------------------
74
75export {
76 areSubscriptionsExist,
77 addUserSubscription,
78 listUserSubscriptions,
79 getUserSubscription,
80 listUserSubscriptionVideos,
81 removeUserSubscription
82}
diff --git a/shared/utils/users/users.ts b/shared/utils/users/users.ts
new file mode 100644
index 000000000..61a7e3757
--- /dev/null
+++ b/shared/utils/users/users.ts
@@ -0,0 +1,302 @@
1import * as request from 'supertest'
2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
3
4import { UserRole } from '../../index'
5import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
6
7function createUser (
8 url: string,
9 accessToken: string,
10 username: string,
11 password: string,
12 videoQuota = 1000000,
13 videoQuotaDaily = -1,
14 role: UserRole = UserRole.USER,
15 specialStatus = 200
16) {
17 const path = '/api/v1/users'
18 const body = {
19 username,
20 password,
21 role,
22 email: username + '@example.com',
23 videoQuota,
24 videoQuotaDaily
25 }
26
27 return request(url)
28 .post(path)
29 .set('Accept', 'application/json')
30 .set('Authorization', 'Bearer ' + accessToken)
31 .send(body)
32 .expect(specialStatus)
33}
34
35function registerUser (url: string, username: string, password: string, specialStatus = 204) {
36 const path = '/api/v1/users/register'
37 const body = {
38 username,
39 password,
40 email: username + '@example.com'
41 }
42
43 return request(url)
44 .post(path)
45 .set('Accept', 'application/json')
46 .send(body)
47 .expect(specialStatus)
48}
49
50function getMyUserInformation (url: string, accessToken: string, specialStatus = 200) {
51 const path = '/api/v1/users/me'
52
53 return request(url)
54 .get(path)
55 .set('Accept', 'application/json')
56 .set('Authorization', 'Bearer ' + accessToken)
57 .expect(specialStatus)
58 .expect('Content-Type', /json/)
59}
60
61function deleteMe (url: string, accessToken: string, specialStatus = 204) {
62 const path = '/api/v1/users/me'
63
64 return request(url)
65 .delete(path)
66 .set('Accept', 'application/json')
67 .set('Authorization', 'Bearer ' + accessToken)
68 .expect(specialStatus)
69}
70
71function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) {
72 const path = '/api/v1/users/me/video-quota-used'
73
74 return request(url)
75 .get(path)
76 .set('Accept', 'application/json')
77 .set('Authorization', 'Bearer ' + accessToken)
78 .expect(specialStatus)
79 .expect('Content-Type', /json/)
80}
81
82function getUserInformation (url: string, accessToken: string, userId: number) {
83 const path = '/api/v1/users/' + userId
84
85 return request(url)
86 .get(path)
87 .set('Accept', 'application/json')
88 .set('Authorization', 'Bearer ' + accessToken)
89 .expect(200)
90 .expect('Content-Type', /json/)
91}
92
93function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = 200) {
94 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
95
96 return request(url)
97 .get(path)
98 .set('Accept', 'application/json')
99 .set('Authorization', 'Bearer ' + accessToken)
100 .expect(specialStatus)
101 .expect('Content-Type', /json/)
102}
103
104function getUsersList (url: string, accessToken: string) {
105 const path = '/api/v1/users'
106
107 return request(url)
108 .get(path)
109 .set('Accept', 'application/json')
110 .set('Authorization', 'Bearer ' + accessToken)
111 .expect(200)
112 .expect('Content-Type', /json/)
113}
114
115function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) {
116 const path = '/api/v1/users'
117
118 return request(url)
119 .get(path)
120 .query({ start })
121 .query({ count })
122 .query({ sort })
123 .query({ search })
124 .set('Accept', 'application/json')
125 .set('Authorization', 'Bearer ' + accessToken)
126 .expect(200)
127 .expect('Content-Type', /json/)
128}
129
130function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
131 const path = '/api/v1/users'
132
133 return request(url)
134 .delete(path + '/' + userId)
135 .set('Accept', 'application/json')
136 .set('Authorization', 'Bearer ' + accessToken)
137 .expect(expectedStatus)
138}
139
140function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) {
141 const path = '/api/v1/users'
142 let body: any
143 if (reason) body = { reason }
144
145 return request(url)
146 .post(path + '/' + userId + '/block')
147 .send(body)
148 .set('Accept', 'application/json')
149 .set('Authorization', 'Bearer ' + accessToken)
150 .expect(expectedStatus)
151}
152
153function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
154 const path = '/api/v1/users'
155
156 return request(url)
157 .post(path + '/' + userId + '/unblock')
158 .set('Accept', 'application/json')
159 .set('Authorization', 'Bearer ' + accessToken)
160 .expect(expectedStatus)
161}
162
163function updateMyUser (options: {
164 url: string
165 accessToken: string
166 currentPassword?: string
167 newPassword?: string
168 nsfwPolicy?: NSFWPolicyType
169 email?: string
170 autoPlayVideo?: boolean
171 displayName?: string
172 description?: string
173 videosHistoryEnabled?: boolean
174}) {
175 const path = '/api/v1/users/me'
176
177 const toSend = {}
178 if (options.currentPassword !== undefined && options.currentPassword !== null) toSend['currentPassword'] = options.currentPassword
179 if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword
180 if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy
181 if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo
182 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
183 if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
184 if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
185 if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
186 toSend['videosHistoryEnabled'] = options.videosHistoryEnabled
187 }
188
189 return makePutBodyRequest({
190 url: options.url,
191 path,
192 token: options.accessToken,
193 fields: toSend,
194 statusCodeExpected: 204
195 })
196}
197
198function updateMyAvatar (options: {
199 url: string,
200 accessToken: string,
201 fixture: string
202}) {
203 const path = '/api/v1/users/me/avatar/pick'
204
205 return updateAvatarRequest(Object.assign(options, { path }))
206}
207
208function updateUser (options: {
209 url: string
210 userId: number,
211 accessToken: string,
212 email?: string,
213 emailVerified?: boolean,
214 videoQuota?: number,
215 videoQuotaDaily?: number,
216 role?: UserRole
217}) {
218 const path = '/api/v1/users/' + options.userId
219
220 const toSend = {}
221 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
222 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
223 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
224 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
225 if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
226
227 return makePutBodyRequest({
228 url: options.url,
229 path,
230 token: options.accessToken,
231 fields: toSend,
232 statusCodeExpected: 204
233 })
234}
235
236function askResetPassword (url: string, email: string) {
237 const path = '/api/v1/users/ask-reset-password'
238
239 return makePostBodyRequest({
240 url,
241 path,
242 fields: { email },
243 statusCodeExpected: 204
244 })
245}
246
247function resetPassword (url: string, userId: number, verificationString: string, password: string, statusCodeExpected = 204) {
248 const path = '/api/v1/users/' + userId + '/reset-password'
249
250 return makePostBodyRequest({
251 url,
252 path,
253 fields: { password, verificationString },
254 statusCodeExpected
255 })
256}
257
258function askSendVerifyEmail (url: string, email: string) {
259 const path = '/api/v1/users/ask-send-verify-email'
260
261 return makePostBodyRequest({
262 url,
263 path,
264 fields: { email },
265 statusCodeExpected: 204
266 })
267}
268
269function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) {
270 const path = '/api/v1/users/' + userId + '/verify-email'
271
272 return makePostBodyRequest({
273 url,
274 path,
275 fields: { verificationString },
276 statusCodeExpected
277 })
278}
279
280// ---------------------------------------------------------------------------
281
282export {
283 createUser,
284 registerUser,
285 getMyUserInformation,
286 getMyUserVideoRating,
287 deleteMe,
288 getMyUserVideoQuotaUsed,
289 getUsersList,
290 getUsersListPaginationAndSort,
291 removeUser,
292 updateUser,
293 updateMyUser,
294 getUserInformation,
295 blockUser,
296 unblockUser,
297 askResetPassword,
298 resetPassword,
299 updateMyAvatar,
300 askSendVerifyEmail,
301 verifyEmail
302}
diff --git a/shared/utils/videos/services.ts b/shared/utils/videos/services.ts
new file mode 100644
index 000000000..1a53dd4cf
--- /dev/null
+++ b/shared/utils/videos/services.ts
@@ -0,0 +1,23 @@
1import * as request from 'supertest'
2
3function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
4 const path = '/services/oembed'
5 const query = {
6 url: oembedUrl,
7 format,
8 maxheight: maxHeight,
9 maxwidth: maxWidth
10 }
11
12 return request(url)
13 .get(path)
14 .query(query)
15 .set('Accept', 'application/json')
16 .expect(200)
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 getOEmbed
23}
diff --git a/shared/utils/videos/video-abuses.ts b/shared/utils/videos/video-abuses.ts
new file mode 100644
index 000000000..7f011ec0f
--- /dev/null
+++ b/shared/utils/videos/video-abuses.ts
@@ -0,0 +1,65 @@
1import * as request from 'supertest'
2import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
3import { makeDeleteRequest, makePutBodyRequest } from '../requests/requests'
4
5function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) {
6 const path = '/api/v1/videos/' + videoId + '/abuse'
7
8 return request(url)
9 .post(path)
10 .set('Accept', 'application/json')
11 .set('Authorization', 'Bearer ' + token)
12 .send({ reason })
13 .expect(specialStatus)
14}
15
16function getVideoAbusesList (url: string, token: string) {
17 const path = '/api/v1/videos/abuse'
18
19 return request(url)
20 .get(path)
21 .query({ sort: 'createdAt' })
22 .set('Accept', 'application/json')
23 .set('Authorization', 'Bearer ' + token)
24 .expect(200)
25 .expect('Content-Type', /json/)
26}
27
28function updateVideoAbuse (
29 url: string,
30 token: string,
31 videoId: string | number,
32 videoAbuseId: number,
33 body: VideoAbuseUpdate,
34 statusCodeExpected = 204
35) {
36 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
37
38 return makePutBodyRequest({
39 url,
40 token,
41 path,
42 fields: body,
43 statusCodeExpected
44 })
45}
46
47function deleteVideoAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
48 const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
49
50 return makeDeleteRequest({
51 url,
52 token,
53 path,
54 statusCodeExpected
55 })
56}
57
58// ---------------------------------------------------------------------------
59
60export {
61 reportVideoAbuse,
62 getVideoAbusesList,
63 updateVideoAbuse,
64 deleteVideoAbuse
65}
diff --git a/shared/utils/videos/video-blacklist.ts b/shared/utils/videos/video-blacklist.ts
new file mode 100644
index 000000000..f2ae0ed26
--- /dev/null
+++ b/shared/utils/videos/video-blacklist.ts
@@ -0,0 +1,74 @@
1import * as request from 'supertest'
2
3function addVideoToBlacklist (
4 url: string,
5 token: string,
6 videoId: number | string,
7 reason?: string,
8 unfederate?: boolean,
9 specialStatus = 204
10) {
11 const path = '/api/v1/videos/' + videoId + '/blacklist'
12
13 return request(url)
14 .post(path)
15 .send({ reason, unfederate })
16 .set('Accept', 'application/json')
17 .set('Authorization', 'Bearer ' + token)
18 .expect(specialStatus)
19}
20
21function updateVideoBlacklist (url: string, token: string, videoId: number, reason?: string, specialStatus = 204) {
22 const path = '/api/v1/videos/' + videoId + '/blacklist'
23
24 return request(url)
25 .put(path)
26 .send({ reason })
27 .set('Accept', 'application/json')
28 .set('Authorization', 'Bearer ' + token)
29 .expect(specialStatus)
30}
31
32function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
33 const path = '/api/v1/videos/' + videoId + '/blacklist'
34
35 return request(url)
36 .delete(path)
37 .set('Accept', 'application/json')
38 .set('Authorization', 'Bearer ' + token)
39 .expect(specialStatus)
40}
41
42function getBlacklistedVideosList (url: string, token: string, specialStatus = 200) {
43 const path = '/api/v1/videos/blacklist/'
44
45 return request(url)
46 .get(path)
47 .query({ sort: 'createdAt' })
48 .set('Accept', 'application/json')
49 .set('Authorization', 'Bearer ' + token)
50 .expect(specialStatus)
51 .expect('Content-Type', /json/)
52}
53
54function getSortedBlacklistedVideosList (url: string, token: string, sort: string, specialStatus = 200) {
55 const path = '/api/v1/videos/blacklist/'
56
57 return request(url)
58 .get(path)
59 .query({ sort: sort })
60 .set('Accept', 'application/json')
61 .set('Authorization', 'Bearer ' + token)
62 .expect(specialStatus)
63 .expect('Content-Type', /json/)
64}
65
66// ---------------------------------------------------------------------------
67
68export {
69 addVideoToBlacklist,
70 removeVideoFromBlacklist,
71 getBlacklistedVideosList,
72 getSortedBlacklistedVideosList,
73 updateVideoBlacklist
74}
diff --git a/shared/utils/videos/video-captions.ts b/shared/utils/videos/video-captions.ts
new file mode 100644
index 000000000..8d67f617b
--- /dev/null
+++ b/shared/utils/videos/video-captions.ts
@@ -0,0 +1,71 @@
1import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
2import * as request from 'supertest'
3import * as chai from 'chai'
4import { buildAbsoluteFixturePath } from '../miscs/miscs'
5
6const expect = chai.expect
7
8function createVideoCaption (args: {
9 url: string,
10 accessToken: string
11 videoId: string | number
12 language: string
13 fixture: string,
14 mimeType?: string,
15 statusCodeExpected?: number
16}) {
17 const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language
18
19 const captionfile = buildAbsoluteFixturePath(args.fixture)
20 const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile
21
22 return makeUploadRequest({
23 method: 'PUT',
24 url: args.url,
25 path,
26 token: args.accessToken,
27 fields: {},
28 attaches: {
29 captionfile: captionfileAttach
30 },
31 statusCodeExpected: args.statusCodeExpected || 204
32 })
33}
34
35function listVideoCaptions (url: string, videoId: string | number) {
36 const path = '/api/v1/videos/' + videoId + '/captions'
37
38 return makeGetRequest({
39 url,
40 path,
41 statusCodeExpected: 200
42 })
43}
44
45function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) {
46 const path = '/api/v1/videos/' + videoId + '/captions/' + language
47
48 return makeDeleteRequest({
49 url,
50 token,
51 path,
52 statusCodeExpected: 204
53 })
54}
55
56async function testCaptionFile (url: string, captionPath: string, containsString: string) {
57 const res = await request(url)
58 .get(captionPath)
59 .expect(200)
60
61 expect(res.text).to.contain(containsString)
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 createVideoCaption,
68 listVideoCaptions,
69 testCaptionFile,
70 deleteVideoCaption
71}
diff --git a/shared/utils/videos/video-change-ownership.ts b/shared/utils/videos/video-change-ownership.ts
new file mode 100644
index 000000000..f288692ea
--- /dev/null
+++ b/shared/utils/videos/video-change-ownership.ts
@@ -0,0 +1,54 @@
1import * as request from 'supertest'
2
3function changeVideoOwnership (url: string, token: string, videoId: number | string, username) {
4 const path = '/api/v1/videos/' + videoId + '/give-ownership'
5
6 return request(url)
7 .post(path)
8 .set('Accept', 'application/json')
9 .set('Authorization', 'Bearer ' + token)
10 .send({ username })
11 .expect(204)
12}
13
14function getVideoChangeOwnershipList (url: string, token: string) {
15 const path = '/api/v1/videos/ownership'
16
17 return request(url)
18 .get(path)
19 .query({ sort: '-createdAt' })
20 .set('Accept', 'application/json')
21 .set('Authorization', 'Bearer ' + token)
22 .expect(200)
23 .expect('Content-Type', /json/)
24}
25
26function acceptChangeOwnership (url: string, token: string, ownershipId: string, channelId: number, expectedStatus = 204) {
27 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
28
29 return request(url)
30 .post(path)
31 .set('Accept', 'application/json')
32 .set('Authorization', 'Bearer ' + token)
33 .send({ channelId })
34 .expect(expectedStatus)
35}
36
37function refuseChangeOwnership (url: string, token: string, ownershipId: string, expectedStatus = 204) {
38 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
39
40 return request(url)
41 .post(path)
42 .set('Accept', 'application/json')
43 .set('Authorization', 'Bearer ' + token)
44 .expect(expectedStatus)
45}
46
47// ---------------------------------------------------------------------------
48
49export {
50 changeVideoOwnership,
51 getVideoChangeOwnershipList,
52 acceptChangeOwnership,
53 refuseChangeOwnership
54}
diff --git a/shared/utils/videos/video-channels.ts b/shared/utils/videos/video-channels.ts
new file mode 100644
index 000000000..3935c261e
--- /dev/null
+++ b/shared/utils/videos/video-channels.ts
@@ -0,0 +1,118 @@
1import * as request from 'supertest'
2import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos'
3import { updateAvatarRequest } from '../requests/requests'
4
5function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
6 const path = '/api/v1/video-channels'
7
8 const req = request(url)
9 .get(path)
10 .query({ start: start })
11 .query({ count: count })
12
13 if (sort) req.query({ sort })
14
15 return req.set('Accept', 'application/json')
16 .expect(200)
17 .expect('Content-Type', /json/)
18}
19
20function getAccountVideoChannelsList (url: string, accountName: string, specialStatus = 200) {
21 const path = '/api/v1/accounts/' + accountName + '/video-channels'
22
23 return request(url)
24 .get(path)
25 .set('Accept', 'application/json')
26 .expect(specialStatus)
27 .expect('Content-Type', /json/)
28}
29
30function addVideoChannel (
31 url: string,
32 token: string,
33 videoChannelAttributesArg: VideoChannelCreate,
34 expectedStatus = 200
35) {
36 const path = '/api/v1/video-channels/'
37
38 // Default attributes
39 let attributes = {
40 displayName: 'my super video channel',
41 description: 'my super channel description',
42 support: 'my super channel support'
43 }
44 attributes = Object.assign(attributes, videoChannelAttributesArg)
45
46 return request(url)
47 .post(path)
48 .send(attributes)
49 .set('Accept', 'application/json')
50 .set('Authorization', 'Bearer ' + token)
51 .expect(expectedStatus)
52}
53
54function updateVideoChannel (
55 url: string,
56 token: string,
57 channelName: string,
58 attributes: VideoChannelUpdate,
59 expectedStatus = 204
60) {
61 const body = {}
62 const path = '/api/v1/video-channels/' + channelName
63
64 if (attributes.displayName) body['displayName'] = attributes.displayName
65 if (attributes.description) body['description'] = attributes.description
66 if (attributes.support) body['support'] = attributes.support
67
68 return request(url)
69 .put(path)
70 .send(body)
71 .set('Accept', 'application/json')
72 .set('Authorization', 'Bearer ' + token)
73 .expect(expectedStatus)
74}
75
76function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = 204) {
77 const path = '/api/v1/video-channels/' + channelName
78
79 return request(url)
80 .delete(path)
81 .set('Accept', 'application/json')
82 .set('Authorization', 'Bearer ' + token)
83 .expect(expectedStatus)
84}
85
86function getVideoChannel (url: string, channelName: string) {
87 const path = '/api/v1/video-channels/' + channelName
88
89 return request(url)
90 .get(path)
91 .set('Accept', 'application/json')
92 .expect(200)
93 .expect('Content-Type', /json/)
94}
95
96function updateVideoChannelAvatar (options: {
97 url: string,
98 accessToken: string,
99 fixture: string,
100 videoChannelName: string | number
101}) {
102
103 const path = '/api/v1/video-channels/' + options.videoChannelName + '/avatar/pick'
104
105 return updateAvatarRequest(Object.assign(options, { path }))
106}
107
108// ---------------------------------------------------------------------------
109
110export {
111 updateVideoChannelAvatar,
112 getVideoChannelsList,
113 getAccountVideoChannelsList,
114 addVideoChannel,
115 updateVideoChannel,
116 deleteVideoChannel,
117 getVideoChannel
118}
diff --git a/shared/utils/videos/video-comments.ts b/shared/utils/videos/video-comments.ts
new file mode 100644
index 000000000..0ebf69ced
--- /dev/null
+++ b/shared/utils/videos/video-comments.ts
@@ -0,0 +1,87 @@
1import * as request from 'supertest'
2import { makeDeleteRequest } from '../requests/requests'
3
4function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
5 const path = '/api/v1/videos/' + videoId + '/comment-threads'
6
7 const req = request(url)
8 .get(path)
9 .query({ start: start })
10 .query({ count: count })
11
12 if (sort) req.query({ sort })
13 if (token) req.set('Authorization', 'Bearer ' + token)
14
15 return req.set('Accept', 'application/json')
16 .expect(200)
17 .expect('Content-Type', /json/)
18}
19
20function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
21 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
22
23 const req = request(url)
24 .get(path)
25 .set('Accept', 'application/json')
26
27 if (token) req.set('Authorization', 'Bearer ' + token)
28
29 return req.expect(200)
30 .expect('Content-Type', /json/)
31}
32
33function addVideoCommentThread (url: string, token: string, videoId: number | string, text: string, expectedStatus = 200) {
34 const path = '/api/v1/videos/' + videoId + '/comment-threads'
35
36 return request(url)
37 .post(path)
38 .send({ text })
39 .set('Accept', 'application/json')
40 .set('Authorization', 'Bearer ' + token)
41 .expect(expectedStatus)
42}
43
44function addVideoCommentReply (
45 url: string,
46 token: string,
47 videoId: number | string,
48 inReplyToCommentId: number,
49 text: string,
50 expectedStatus = 200
51) {
52 const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
53
54 return request(url)
55 .post(path)
56 .send({ text })
57 .set('Accept', 'application/json')
58 .set('Authorization', 'Bearer ' + token)
59 .expect(expectedStatus)
60}
61
62function deleteVideoComment (
63 url: string,
64 token: string,
65 videoId: number | string,
66 commentId: number,
67 statusCodeExpected = 204
68) {
69 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
70
71 return makeDeleteRequest({
72 url,
73 path,
74 token,
75 statusCodeExpected
76 })
77}
78
79// ---------------------------------------------------------------------------
80
81export {
82 getVideoCommentThreads,
83 getVideoThreadComments,
84 addVideoCommentThread,
85 addVideoCommentReply,
86 deleteVideoComment
87}
diff --git a/shared/utils/videos/video-history.ts b/shared/utils/videos/video-history.ts
new file mode 100644
index 000000000..dc7095b4d
--- /dev/null
+++ b/shared/utils/videos/video-history.ts
@@ -0,0 +1,39 @@
1import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2
3function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number, statusCodeExpected = 204) {
4 const path = '/api/v1/videos/' + videoId + '/watching'
5 const fields = { currentTime }
6
7 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
8}
9
10function listMyVideosHistory (url: string, token: string) {
11 const path = '/api/v1/users/me/history/videos'
12
13 return makeGetRequest({
14 url,
15 path,
16 token,
17 statusCodeExpected: 200
18 })
19}
20
21function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
22 const path = '/api/v1/users/me/history/videos/remove'
23
24 return makePostBodyRequest({
25 url,
26 path,
27 token,
28 fields: beforeDate ? { beforeDate } : {},
29 statusCodeExpected: 204
30 })
31}
32
33// ---------------------------------------------------------------------------
34
35export {
36 userWatchVideo,
37 listMyVideosHistory,
38 removeMyVideosHistory
39}
diff --git a/shared/utils/videos/video-imports.ts b/shared/utils/videos/video-imports.ts
new file mode 100644
index 000000000..ec77cdcda
--- /dev/null
+++ b/shared/utils/videos/video-imports.ts
@@ -0,0 +1,57 @@
1
2import { VideoImportCreate } from '../../models/videos'
3import { makeGetRequest, makeUploadRequest } from '../requests/requests'
4
5function getYoutubeVideoUrl () {
6 return 'https://youtu.be/msX3jv1XdvM'
7}
8
9function getMagnetURI () {
10 // tslint:disable:max-line-length
11 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'
12}
13
14function getBadVideoUrl () {
15 return 'https://download.cpy.re/peertube/bad_video.mp4'
16}
17
18function importVideo (url: string, token: string, attributes: VideoImportCreate) {
19 const path = '/api/v1/videos/imports'
20
21 let attaches: any = {}
22 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
23
24 return makeUploadRequest({
25 url,
26 path,
27 token,
28 attaches,
29 fields: attributes,
30 statusCodeExpected: 200
31 })
32}
33
34function getMyVideoImports (url: string, token: string, sort?: string) {
35 const path = '/api/v1/users/me/videos/imports'
36
37 const query = {}
38 if (sort) query['sort'] = sort
39
40 return makeGetRequest({
41 url,
42 query,
43 path,
44 token,
45 statusCodeExpected: 200
46 })
47}
48
49// ---------------------------------------------------------------------------
50
51export {
52 getBadVideoUrl,
53 getYoutubeVideoUrl,
54 importVideo,
55 getMagnetURI,
56 getMyVideoImports
57}
diff --git a/shared/utils/videos/videos.ts b/shared/utils/videos/videos.ts
new file mode 100644
index 000000000..0cf6e7c4f
--- /dev/null
+++ b/shared/utils/videos/videos.ts
@@ -0,0 +1,577 @@
1/* tslint:disable:no-unused-expression */
2
3import { expect } from 'chai'
4import { existsSync, readdir, readFile } from 'fs-extra'
5import * as parseTorrent from 'parse-torrent'
6import { extname, join } from 'path'
7import * as request from 'supertest'
8import {
9 buildAbsoluteFixturePath,
10 getMyUserInformation,
11 immutableAssign,
12 makeGetRequest,
13 makePutBodyRequest,
14 makeUploadRequest,
15 root,
16 ServerInfo,
17 testImage
18} from '../'
19
20import { VideoDetails, VideoPrivacy } from '../../models/videos'
21import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
22import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
23
24type VideoAttributes = {
25 name?: string
26 category?: number
27 licence?: number
28 language?: string
29 nsfw?: boolean
30 commentsEnabled?: boolean
31 waitTranscoding?: boolean
32 description?: string
33 tags?: string[]
34 channelId?: number
35 privacy?: VideoPrivacy
36 fixture?: string
37 thumbnailfile?: string
38 previewfile?: string
39 scheduleUpdate?: {
40 updateAt: string
41 privacy?: VideoPrivacy
42 }
43}
44
45function getVideoCategories (url: string) {
46 const path = '/api/v1/videos/categories'
47
48 return makeGetRequest({
49 url,
50 path,
51 statusCodeExpected: 200
52 })
53}
54
55function getVideoLicences (url: string) {
56 const path = '/api/v1/videos/licences'
57
58 return makeGetRequest({
59 url,
60 path,
61 statusCodeExpected: 200
62 })
63}
64
65function getVideoLanguages (url: string) {
66 const path = '/api/v1/videos/languages'
67
68 return makeGetRequest({
69 url,
70 path,
71 statusCodeExpected: 200
72 })
73}
74
75function getVideoPrivacies (url: string) {
76 const path = '/api/v1/videos/privacies'
77
78 return makeGetRequest({
79 url,
80 path,
81 statusCodeExpected: 200
82 })
83}
84
85function getVideo (url: string, id: number | string, expectedStatus = 200) {
86 const path = '/api/v1/videos/' + id
87
88 return request(url)
89 .get(path)
90 .set('Accept', 'application/json')
91 .expect(expectedStatus)
92}
93
94function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) {
95 const path = '/api/v1/videos/' + id + '/views'
96
97 const req = request(url)
98 .post(path)
99 .set('Accept', 'application/json')
100
101 if (xForwardedFor) {
102 req.set('X-Forwarded-For', xForwardedFor)
103 }
104
105 return req.expect(expectedStatus)
106}
107
108function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) {
109 const path = '/api/v1/videos/' + id
110
111 return request(url)
112 .get(path)
113 .set('Authorization', 'Bearer ' + token)
114 .set('Accept', 'application/json')
115 .expect(expectedStatus)
116}
117
118function getVideoDescription (url: string, descriptionPath: string) {
119 return request(url)
120 .get(descriptionPath)
121 .set('Accept', 'application/json')
122 .expect(200)
123 .expect('Content-Type', /json/)
124}
125
126function getVideosList (url: string) {
127 const path = '/api/v1/videos'
128
129 return request(url)
130 .get(path)
131 .query({ sort: 'name' })
132 .set('Accept', 'application/json')
133 .expect(200)
134 .expect('Content-Type', /json/)
135}
136
137function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
138 const path = '/api/v1/videos'
139
140 return request(url)
141 .get(path)
142 .set('Authorization', 'Bearer ' + token)
143 .query(immutableAssign(query, { sort: 'name' }))
144 .set('Accept', 'application/json')
145 .expect(200)
146 .expect('Content-Type', /json/)
147}
148
149function getLocalVideos (url: string) {
150 const path = '/api/v1/videos'
151
152 return request(url)
153 .get(path)
154 .query({ sort: 'name', filter: 'local' })
155 .set('Accept', 'application/json')
156 .expect(200)
157 .expect('Content-Type', /json/)
158}
159
160function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) {
161 const path = '/api/v1/users/me/videos'
162
163 const req = request(url)
164 .get(path)
165 .query({ start: start })
166 .query({ count: count })
167
168 if (sort) req.query({ sort })
169
170 return req.set('Accept', 'application/json')
171 .set('Authorization', 'Bearer ' + accessToken)
172 .expect(200)
173 .expect('Content-Type', /json/)
174}
175
176function getAccountVideos (
177 url: string,
178 accessToken: string,
179 accountName: string,
180 start: number,
181 count: number,
182 sort?: string,
183 query: { nsfw?: boolean } = {}
184) {
185 const path = '/api/v1/accounts/' + accountName + '/videos'
186
187 return makeGetRequest({
188 url,
189 path,
190 query: immutableAssign(query, {
191 start,
192 count,
193 sort
194 }),
195 token: accessToken,
196 statusCodeExpected: 200
197 })
198}
199
200function getVideoChannelVideos (
201 url: string,
202 accessToken: string,
203 videoChannelName: string,
204 start: number,
205 count: number,
206 sort?: string,
207 query: { nsfw?: boolean } = {}
208) {
209 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
210
211 return makeGetRequest({
212 url,
213 path,
214 query: immutableAssign(query, {
215 start,
216 count,
217 sort
218 }),
219 token: accessToken,
220 statusCodeExpected: 200
221 })
222}
223
224function getVideosListPagination (url: string, start: number, count: number, sort?: string) {
225 const path = '/api/v1/videos'
226
227 const req = request(url)
228 .get(path)
229 .query({ start: start })
230 .query({ count: count })
231
232 if (sort) req.query({ sort })
233
234 return req.set('Accept', 'application/json')
235 .expect(200)
236 .expect('Content-Type', /json/)
237}
238
239function getVideosListSort (url: string, sort: string) {
240 const path = '/api/v1/videos'
241
242 return request(url)
243 .get(path)
244 .query({ sort: sort })
245 .set('Accept', 'application/json')
246 .expect(200)
247 .expect('Content-Type', /json/)
248}
249
250function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
251 const path = '/api/v1/videos'
252
253 return request(url)
254 .get(path)
255 .query(query)
256 .set('Accept', 'application/json')
257 .expect(200)
258 .expect('Content-Type', /json/)
259}
260
261function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
262 const path = '/api/v1/videos'
263
264 return request(url)
265 .delete(path + '/' + id)
266 .set('Accept', 'application/json')
267 .set('Authorization', 'Bearer ' + token)
268 .expect(expectedStatus)
269}
270
271async function checkVideoFilesWereRemoved (
272 videoUUID: string,
273 serverNumber: number,
274 directories = [ 'redundancy', 'videos', 'thumbnails', 'torrents', 'previews', 'captions' ]
275) {
276 const testDirectory = 'test' + serverNumber
277
278 for (const directory of directories) {
279 const directoryPath = join(root(), testDirectory, directory)
280
281 const directoryExists = existsSync(directoryPath)
282 expect(directoryExists).to.be.true
283
284 const files = await readdir(directoryPath)
285 for (const file of files) {
286 expect(file).to.not.contain(videoUUID)
287 }
288 }
289}
290
291async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
292 const path = '/api/v1/videos/upload'
293 let defaultChannelId = '1'
294
295 try {
296 const res = await getMyUserInformation(url, accessToken)
297 defaultChannelId = res.body.videoChannels[0].id
298 } catch (e) { /* empty */ }
299
300 // Override default attributes
301 const attributes = Object.assign({
302 name: 'my super video',
303 category: 5,
304 licence: 4,
305 language: 'zh',
306 channelId: defaultChannelId,
307 nsfw: true,
308 waitTranscoding: false,
309 description: 'my super description',
310 support: 'my super support text',
311 tags: [ 'tag' ],
312 privacy: VideoPrivacy.PUBLIC,
313 commentsEnabled: true,
314 fixture: 'video_short.webm'
315 }, videoAttributesArg)
316
317 const req = request(url)
318 .post(path)
319 .set('Accept', 'application/json')
320 .set('Authorization', 'Bearer ' + accessToken)
321 .field('name', attributes.name)
322 .field('nsfw', JSON.stringify(attributes.nsfw))
323 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
324 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
325 .field('privacy', attributes.privacy.toString())
326 .field('channelId', attributes.channelId)
327
328 if (attributes.description !== undefined) {
329 req.field('description', attributes.description)
330 }
331 if (attributes.language !== undefined) {
332 req.field('language', attributes.language.toString())
333 }
334 if (attributes.category !== undefined) {
335 req.field('category', attributes.category.toString())
336 }
337 if (attributes.licence !== undefined) {
338 req.field('licence', attributes.licence.toString())
339 }
340
341 for (let i = 0; i < attributes.tags.length; i++) {
342 req.field('tags[' + i + ']', attributes.tags[i])
343 }
344
345 if (attributes.thumbnailfile !== undefined) {
346 req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
347 }
348 if (attributes.previewfile !== undefined) {
349 req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
350 }
351
352 if (attributes.scheduleUpdate) {
353 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
354
355 if (attributes.scheduleUpdate.privacy) {
356 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
357 }
358 }
359
360 return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
361 .expect(specialStatus)
362}
363
364function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
365 const path = '/api/v1/videos/' + id
366 const body = {}
367
368 if (attributes.name) body['name'] = attributes.name
369 if (attributes.category) body['category'] = attributes.category
370 if (attributes.licence) body['licence'] = attributes.licence
371 if (attributes.language) body['language'] = attributes.language
372 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
373 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
374 if (attributes.description) body['description'] = attributes.description
375 if (attributes.tags) body['tags'] = attributes.tags
376 if (attributes.privacy) body['privacy'] = attributes.privacy
377 if (attributes.channelId) body['channelId'] = attributes.channelId
378 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
379
380 // Upload request
381 if (attributes.thumbnailfile || attributes.previewfile) {
382 const attaches: any = {}
383 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
384 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
385
386 return makeUploadRequest({
387 url,
388 method: 'PUT',
389 path,
390 token: accessToken,
391 fields: body,
392 attaches,
393 statusCodeExpected
394 })
395 }
396
397 return makePutBodyRequest({
398 url,
399 path,
400 fields: body,
401 token: accessToken,
402 statusCodeExpected
403 })
404}
405
406function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
407 const path = '/api/v1/videos/' + id + '/rate'
408
409 return request(url)
410 .put(path)
411 .set('Accept', 'application/json')
412 .set('Authorization', 'Bearer ' + accessToken)
413 .send({ rating })
414 .expect(specialStatus)
415}
416
417function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
418 return new Promise<any>((res, rej) => {
419 const torrentName = videoUUID + '-' + resolution + '.torrent'
420 const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
421 readFile(torrentPath, (err, data) => {
422 if (err) return rej(err)
423
424 return res(parseTorrent(data))
425 })
426 })
427}
428
429async function completeVideoCheck (
430 url: string,
431 video: any,
432 attributes: {
433 name: string
434 category: number
435 licence: number
436 language: string
437 nsfw: boolean
438 commentsEnabled: boolean
439 description: string
440 publishedAt?: string
441 support: string
442 account: {
443 name: string
444 host: string
445 }
446 isLocal: boolean
447 tags: string[]
448 privacy: number
449 likes?: number
450 dislikes?: number
451 duration: number
452 channel: {
453 displayName: string
454 name: string
455 description
456 isLocal: boolean
457 }
458 fixture: string
459 files: {
460 resolution: number
461 size: number
462 }[],
463 thumbnailfile?: string
464 previewfile?: string
465 }
466) {
467 if (!attributes.likes) attributes.likes = 0
468 if (!attributes.dislikes) attributes.dislikes = 0
469
470 expect(video.name).to.equal(attributes.name)
471 expect(video.category.id).to.equal(attributes.category)
472 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
473 expect(video.licence.id).to.equal(attributes.licence)
474 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
475 expect(video.language.id).to.equal(attributes.language)
476 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
477 expect(video.privacy.id).to.deep.equal(attributes.privacy)
478 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
479 expect(video.nsfw).to.equal(attributes.nsfw)
480 expect(video.description).to.equal(attributes.description)
481 expect(video.account.id).to.be.a('number')
482 expect(video.account.uuid).to.be.a('string')
483 expect(video.account.host).to.equal(attributes.account.host)
484 expect(video.account.name).to.equal(attributes.account.name)
485 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
486 expect(video.channel.name).to.equal(attributes.channel.name)
487 expect(video.likes).to.equal(attributes.likes)
488 expect(video.dislikes).to.equal(attributes.dislikes)
489 expect(video.isLocal).to.equal(attributes.isLocal)
490 expect(video.duration).to.equal(attributes.duration)
491 expect(dateIsValid(video.createdAt)).to.be.true
492 expect(dateIsValid(video.publishedAt)).to.be.true
493 expect(dateIsValid(video.updatedAt)).to.be.true
494
495 if (attributes.publishedAt) {
496 expect(video.publishedAt).to.equal(attributes.publishedAt)
497 }
498
499 const res = await getVideo(url, video.uuid)
500 const videoDetails: VideoDetails = res.body
501
502 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
503 expect(videoDetails.tags).to.deep.equal(attributes.tags)
504 expect(videoDetails.account.name).to.equal(attributes.account.name)
505 expect(videoDetails.account.host).to.equal(attributes.account.host)
506 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
507 expect(video.channel.name).to.equal(attributes.channel.name)
508 expect(videoDetails.channel.host).to.equal(attributes.account.host)
509 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
510 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
511 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
512 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
513
514 for (const attributeFile of attributes.files) {
515 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
516 expect(file).not.to.be.undefined
517
518 let extension = extname(attributes.fixture)
519 // Transcoding enabled on server 2, extension will always be .mp4
520 if (attributes.account.host === 'localhost:9002') extension = '.mp4'
521
522 const magnetUri = file.magnetUri
523 expect(file.magnetUri).to.have.lengthOf.above(2)
524 expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
525 expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
526 expect(file.resolution.id).to.equal(attributeFile.resolution)
527 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
528
529 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
530 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
531 expect(file.size,
532 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
533 .to.be.above(minSize).and.below(maxSize)
534
535 {
536 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
537 }
538
539 if (attributes.previewfile) {
540 await testImage(url, attributes.previewfile, videoDetails.previewPath)
541 }
542
543 const torrent = await webtorrentAdd(magnetUri, true)
544 expect(torrent.files).to.be.an('array')
545 expect(torrent.files.length).to.equal(1)
546 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
547 }
548}
549
550// ---------------------------------------------------------------------------
551
552export {
553 getVideoDescription,
554 getVideoCategories,
555 getVideoLicences,
556 getVideoPrivacies,
557 getVideoLanguages,
558 getMyVideos,
559 getAccountVideos,
560 getVideoChannelVideos,
561 getVideo,
562 getVideoWithToken,
563 getVideosList,
564 getVideosListPagination,
565 getVideosListSort,
566 removeVideo,
567 getVideosListWithToken,
568 uploadVideo,
569 getVideosWithFilters,
570 updateVideo,
571 rateVideo,
572 viewVideo,
573 parseTorrentVideo,
574 getLocalVideos,
575 completeVideoCheck,
576 checkVideoFilesWereRemoved
577}