Split notification tests
[oweals/peertube.git] / shared / extra-utils / users / user-notifications.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import { expect } from 'chai'
4 import { inspect } from 'util'
5 import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
6 import { MockSmtpServer } from '../miscs/email'
7 import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
8 import { doubleFollow } from '../server/follows'
9 import { flushAndRunMultipleServers, ServerInfo } from '../server/servers'
10 import { getUserNotificationSocket } from '../socket/socket-io'
11 import { setAccessTokensToServers, userLogin } from './login'
12 import { createUser, getMyUserInformation } from './users'
13
14 function updateMyNotificationSettings (url: string, token: string, settings: UserNotificationSetting, statusCodeExpected = 204) {
15   const path = '/api/v1/users/me/notification-settings'
16
17   return makePutBodyRequest({
18     url,
19     path,
20     token,
21     fields: settings,
22     statusCodeExpected
23   })
24 }
25
26 async function getUserNotifications (
27   url: string,
28   token: string,
29   start: number,
30   count: number,
31   unread?: boolean,
32   sort = '-createdAt',
33   statusCodeExpected = 200
34 ) {
35   const path = '/api/v1/users/me/notifications'
36
37   return makeGetRequest({
38     url,
39     path,
40     token,
41     query: {
42       start,
43       count,
44       sort,
45       unread
46     },
47     statusCodeExpected
48   })
49 }
50
51 function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = 204) {
52   const path = '/api/v1/users/me/notifications/read'
53
54   return makePostBodyRequest({
55     url,
56     path,
57     token,
58     fields: { ids },
59     statusCodeExpected
60   })
61 }
62
63 function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = 204) {
64   const path = '/api/v1/users/me/notifications/read-all'
65
66   return makePostBodyRequest({
67     url,
68     path,
69     token,
70     statusCodeExpected
71   })
72 }
73
74 async function getLastNotification (serverUrl: string, accessToken: string) {
75   const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
76
77   if (res.body.total === 0) return undefined
78
79   return res.body.data[0] as UserNotification
80 }
81
82 type CheckerBaseParams = {
83   server: ServerInfo
84   emails: any[]
85   socketNotifications: UserNotification[]
86   token: string
87   check?: { web: boolean, mail: boolean }
88 }
89
90 type CheckerType = 'presence' | 'absence'
91
92 async function checkNotification (
93   base: CheckerBaseParams,
94   notificationChecker: (notification: UserNotification, type: CheckerType) => void,
95   emailNotificationFinder: (email: object) => boolean,
96   checkType: CheckerType
97 ) {
98   const check = base.check || { web: true, mail: true }
99
100   if (check.web) {
101     const notification = await getLastNotification(base.server.url, base.token)
102
103     if (notification || checkType !== 'absence') {
104       notificationChecker(notification, checkType)
105     }
106
107     const socketNotification = base.socketNotifications.find(n => {
108       try {
109         notificationChecker(n, 'presence')
110         return true
111       } catch {
112         return false
113       }
114     })
115
116     if (checkType === 'presence') {
117       const obj = inspect(base.socketNotifications, { depth: 5 })
118       expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
119     } else {
120       const obj = inspect(socketNotification, { depth: 5 })
121       expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
122     }
123   }
124
125   if (check.mail) {
126     // Last email
127     const email = base.emails
128                       .slice()
129                       .reverse()
130                       .find(e => emailNotificationFinder(e))
131
132     if (checkType === 'presence') {
133       const emails = base.emails.map(e => e.text)
134       expect(email, 'The email is absent when is should be present. ' + inspect(emails)).to.not.be.undefined
135     } else {
136       expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
137     }
138   }
139 }
140
141 function checkVideo (video: any, videoName?: string, videoUUID?: string) {
142   expect(video.name).to.be.a('string')
143   expect(video.name).to.not.be.empty
144   if (videoName) expect(video.name).to.equal(videoName)
145
146   expect(video.uuid).to.be.a('string')
147   expect(video.uuid).to.not.be.empty
148   if (videoUUID) expect(video.uuid).to.equal(videoUUID)
149
150   expect(video.id).to.be.a('number')
151 }
152
153 function checkActor (actor: any) {
154   expect(actor.displayName).to.be.a('string')
155   expect(actor.displayName).to.not.be.empty
156   expect(actor.host).to.not.be.undefined
157 }
158
159 function checkComment (comment: any, commentId: number, threadId: number) {
160   expect(comment.id).to.equal(commentId)
161   expect(comment.threadId).to.equal(threadId)
162 }
163
164 async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
165   const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
166
167   function notificationChecker (notification: UserNotification, type: CheckerType) {
168     if (type === 'presence') {
169       expect(notification).to.not.be.undefined
170       expect(notification.type).to.equal(notificationType)
171
172       checkVideo(notification.video, videoName, videoUUID)
173       checkActor(notification.video.channel)
174     } else {
175       expect(notification).to.satisfy((n: UserNotification) => {
176         return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
177       })
178     }
179   }
180
181   function emailNotificationFinder (email: object) {
182     const text = email['text']
183     return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1
184   }
185
186   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
187 }
188
189 async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
190   const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
191
192   function notificationChecker (notification: UserNotification, type: CheckerType) {
193     if (type === 'presence') {
194       expect(notification).to.not.be.undefined
195       expect(notification.type).to.equal(notificationType)
196
197       checkVideo(notification.video, videoName, videoUUID)
198       checkActor(notification.video.channel)
199     } else {
200       expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
201     }
202   }
203
204   function emailNotificationFinder (email: object) {
205     const text: string = email['text']
206     return text.includes(videoUUID) && text.includes('Your video')
207   }
208
209   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
210 }
211
212 async function checkMyVideoImportIsFinished (
213   base: CheckerBaseParams,
214   videoName: string,
215   videoUUID: string,
216   url: string,
217   success: boolean,
218   type: CheckerType
219 ) {
220   const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
221
222   function notificationChecker (notification: UserNotification, type: CheckerType) {
223     if (type === 'presence') {
224       expect(notification).to.not.be.undefined
225       expect(notification.type).to.equal(notificationType)
226
227       expect(notification.videoImport.targetUrl).to.equal(url)
228
229       if (success) checkVideo(notification.videoImport.video, videoName, videoUUID)
230     } else {
231       expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
232     }
233   }
234
235   function emailNotificationFinder (email: object) {
236     const text: string = email['text']
237     const toFind = success ? ' finished' : ' error'
238
239     return text.includes(url) && text.includes(toFind)
240   }
241
242   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
243 }
244
245 async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) {
246   const notificationType = UserNotificationType.NEW_USER_REGISTRATION
247
248   function notificationChecker (notification: UserNotification, type: CheckerType) {
249     if (type === 'presence') {
250       expect(notification).to.not.be.undefined
251       expect(notification.type).to.equal(notificationType)
252
253       checkActor(notification.account)
254       expect(notification.account.name).to.equal(username)
255     } else {
256       expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
257     }
258   }
259
260   function emailNotificationFinder (email: object) {
261     const text: string = email['text']
262
263     return text.includes(' registered.') && text.includes(username)
264   }
265
266   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
267 }
268
269 async function checkNewActorFollow (
270   base: CheckerBaseParams,
271   followType: 'channel' | 'account',
272   followerName: string,
273   followerDisplayName: string,
274   followingDisplayName: string,
275   type: CheckerType
276 ) {
277   const notificationType = UserNotificationType.NEW_FOLLOW
278
279   function notificationChecker (notification: UserNotification, type: CheckerType) {
280     if (type === 'presence') {
281       expect(notification).to.not.be.undefined
282       expect(notification.type).to.equal(notificationType)
283
284       checkActor(notification.actorFollow.follower)
285       expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
286       expect(notification.actorFollow.follower.name).to.equal(followerName)
287       expect(notification.actorFollow.follower.host).to.not.be.undefined
288
289       const following = notification.actorFollow.following
290       expect(following.displayName).to.equal(followingDisplayName)
291       expect(following.type).to.equal(followType)
292     } else {
293       expect(notification).to.satisfy(n => {
294         return n.type !== notificationType ||
295           (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
296       })
297     }
298   }
299
300   function emailNotificationFinder (email: object) {
301     const text: string = email['text']
302
303     return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
304   }
305
306   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
307 }
308
309 async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost: string, type: CheckerType) {
310   const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
311
312   function notificationChecker (notification: UserNotification, type: CheckerType) {
313     if (type === 'presence') {
314       expect(notification).to.not.be.undefined
315       expect(notification.type).to.equal(notificationType)
316
317       checkActor(notification.actorFollow.follower)
318       expect(notification.actorFollow.follower.name).to.equal('peertube')
319       expect(notification.actorFollow.follower.host).to.equal(followerHost)
320
321       expect(notification.actorFollow.following.name).to.equal('peertube')
322     } else {
323       expect(notification).to.satisfy(n => {
324         return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
325       })
326     }
327   }
328
329   function emailNotificationFinder (email: object) {
330     const text: string = email['text']
331
332     return text.includes('instance has a new follower') && text.includes(followerHost)
333   }
334
335   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
336 }
337
338 async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) {
339   const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
340
341   function notificationChecker (notification: UserNotification, type: CheckerType) {
342     if (type === 'presence') {
343       expect(notification).to.not.be.undefined
344       expect(notification.type).to.equal(notificationType)
345
346       const following = notification.actorFollow.following
347       checkActor(following)
348       expect(following.name).to.equal('peertube')
349       expect(following.host).to.equal(followingHost)
350
351       expect(notification.actorFollow.follower.name).to.equal('peertube')
352       expect(notification.actorFollow.follower.host).to.equal(followerHost)
353     } else {
354       expect(notification).to.satisfy(n => {
355         return n.type !== notificationType || n.actorFollow.following.host !== followingHost
356       })
357     }
358   }
359
360   function emailNotificationFinder (email: object) {
361     const text: string = email['text']
362
363     return text.includes(' automatically followed a new instance') && text.includes(followingHost)
364   }
365
366   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
367 }
368
369 async function checkCommentMention (
370   base: CheckerBaseParams,
371   uuid: string,
372   commentId: number,
373   threadId: number,
374   byAccountDisplayName: string,
375   type: CheckerType
376 ) {
377   const notificationType = UserNotificationType.COMMENT_MENTION
378
379   function notificationChecker (notification: UserNotification, type: CheckerType) {
380     if (type === 'presence') {
381       expect(notification).to.not.be.undefined
382       expect(notification.type).to.equal(notificationType)
383
384       checkComment(notification.comment, commentId, threadId)
385       checkActor(notification.comment.account)
386       expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
387
388       checkVideo(notification.comment.video, undefined, uuid)
389     } else {
390       expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
391     }
392   }
393
394   function emailNotificationFinder (email: object) {
395     const text: string = email['text']
396
397     return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName)
398   }
399
400   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
401 }
402
403 let lastEmailCount = 0
404
405 async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) {
406   const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
407
408   function notificationChecker (notification: UserNotification, type: CheckerType) {
409     if (type === 'presence') {
410       expect(notification).to.not.be.undefined
411       expect(notification.type).to.equal(notificationType)
412
413       checkComment(notification.comment, commentId, threadId)
414       checkActor(notification.comment.account)
415       checkVideo(notification.comment.video, undefined, uuid)
416     } else {
417       expect(notification).to.satisfy((n: UserNotification) => {
418         return n === undefined || n.comment === undefined || n.comment.id !== commentId
419       })
420     }
421   }
422
423   const commentUrl = `http://localhost:${base.server.port}/videos/watch/${uuid};threadId=${threadId}`
424
425   function emailNotificationFinder (email: object) {
426     return email['text'].indexOf(commentUrl) !== -1
427   }
428
429   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
430
431   if (type === 'presence') {
432     // We cannot detect email duplicates, so check we received another email
433     expect(base.emails).to.have.length.above(lastEmailCount)
434     lastEmailCount = base.emails.length
435   }
436 }
437
438 async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
439   const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS
440
441   function notificationChecker (notification: UserNotification, type: CheckerType) {
442     if (type === 'presence') {
443       expect(notification).to.not.be.undefined
444       expect(notification.type).to.equal(notificationType)
445
446       expect(notification.videoAbuse.id).to.be.a('number')
447       checkVideo(notification.videoAbuse.video, videoName, videoUUID)
448     } else {
449       expect(notification).to.satisfy((n: UserNotification) => {
450         return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID
451       })
452     }
453   }
454
455   function emailNotificationFinder (email: object) {
456     const text = email['text']
457     return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1
458   }
459
460   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
461 }
462
463 async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) {
464   const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
465
466   function notificationChecker (notification: UserNotification, type: CheckerType) {
467     if (type === 'presence') {
468       expect(notification).to.not.be.undefined
469       expect(notification.type).to.equal(notificationType)
470
471       expect(notification.videoBlacklist.video.id).to.be.a('number')
472       checkVideo(notification.videoBlacklist.video, videoName, videoUUID)
473     } else {
474       expect(notification).to.satisfy((n: UserNotification) => {
475         return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
476       })
477     }
478   }
479
480   function emailNotificationFinder (email: object) {
481     const text = email['text']
482     return text.indexOf(videoUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
483   }
484
485   await checkNotification(base, notificationChecker, emailNotificationFinder, type)
486 }
487
488 async function checkNewBlacklistOnMyVideo (
489   base: CheckerBaseParams,
490   videoUUID: string,
491   videoName: string,
492   blacklistType: 'blacklist' | 'unblacklist'
493 ) {
494   const notificationType = blacklistType === 'blacklist'
495     ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
496     : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
497
498   function notificationChecker (notification: UserNotification) {
499     expect(notification).to.not.be.undefined
500     expect(notification.type).to.equal(notificationType)
501
502     const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
503
504     checkVideo(video, videoName, videoUUID)
505   }
506
507   function emailNotificationFinder (email: object) {
508     const text = email['text']
509     return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1
510   }
511
512   await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence')
513 }
514
515 function getAllNotificationsSettings () {
516   return {
517     newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
518     newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
519     videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
520     videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
521     blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
522     myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
523     myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
524     commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
525     newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
526     newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
527     newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
528     autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
529   } as UserNotificationSetting
530 }
531
532 async function prepareNotificationsTest (serversCount = 3) {
533   const userNotifications: UserNotification[] = []
534   const adminNotifications: UserNotification[] = []
535   const adminNotificationsServer2: UserNotification[] = []
536   const emails: object[] = []
537
538   const port = await MockSmtpServer.Instance.collectEmails(emails)
539
540   const overrideConfig = {
541     smtp: {
542       hostname: 'localhost',
543       port
544     }
545   }
546   const servers = await flushAndRunMultipleServers(serversCount, overrideConfig)
547
548   await setAccessTokensToServers(servers)
549   await doubleFollow(servers[0], servers[1])
550
551   const user = {
552     username: 'user_1',
553     password: 'super password'
554   }
555   await createUser({
556     url: servers[0].url,
557     accessToken: servers[0].accessToken,
558     username: user.username,
559     password: user.password,
560     videoQuota: 10 * 1000 * 1000
561   })
562   const userAccessToken = await userLogin(servers[0], user)
563
564   await updateMyNotificationSettings(servers[0].url, userAccessToken, getAllNotificationsSettings())
565   await updateMyNotificationSettings(servers[0].url, servers[0].accessToken, getAllNotificationsSettings())
566
567   if (serversCount > 1) {
568     await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, getAllNotificationsSettings())
569   }
570
571   {
572     const socket = getUserNotificationSocket(servers[0].url, userAccessToken)
573     socket.on('new-notification', n => userNotifications.push(n))
574   }
575   {
576     const socket = getUserNotificationSocket(servers[0].url, servers[0].accessToken)
577     socket.on('new-notification', n => adminNotifications.push(n))
578   }
579
580   if (serversCount > 1) {
581     const socket = getUserNotificationSocket(servers[1].url, servers[1].accessToken)
582     socket.on('new-notification', n => adminNotificationsServer2.push(n))
583   }
584
585   const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken)
586   const channelId = resChannel.body.videoChannels[0].id
587
588   return {
589     userNotifications,
590     adminNotifications,
591     adminNotificationsServer2,
592     userAccessToken,
593     emails,
594     servers,
595     channelId
596   }
597 }
598
599 // ---------------------------------------------------------------------------
600
601 export {
602   CheckerBaseParams,
603   CheckerType,
604   getAllNotificationsSettings,
605   checkNotification,
606   markAsReadAllNotifications,
607   checkMyVideoImportIsFinished,
608   checkUserRegistered,
609   checkAutoInstanceFollowing,
610   checkVideoIsPublished,
611   checkNewVideoFromSubscription,
612   checkNewActorFollow,
613   checkNewCommentOnMyVideo,
614   checkNewBlacklistOnMyVideo,
615   checkCommentMention,
616   updateMyNotificationSettings,
617   checkNewVideoAbuseForModerators,
618   checkVideoAutoBlacklistForModerators,
619   getUserNotifications,
620   markAsReadNotifications,
621   getLastNotification,
622   checkNewInstanceFollower,
623   prepareNotificationsTest
624 }