b686af4e4eb4f79310edfc591f33c23af7a0d0b4
[oweals/peertube.git] / server / tests / api / server / follows.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import * as chai from 'chai'
4 import 'mocha'
5 import { Video, VideoPrivacy } from '../../../../shared/models/videos'
6 import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
7 import { cleanupTests, completeVideoCheck, deleteVideoComment } from '../../../../shared/extra-utils'
8 import {
9   flushAndRunMultipleServers,
10   getVideosList,
11   ServerInfo,
12   setAccessTokensToServers,
13   uploadVideo
14 } from '../../../../shared/extra-utils/index'
15 import { dateIsValid } from '../../../../shared/extra-utils/miscs/miscs'
16 import {
17   follow,
18   getFollowersListPaginationAndSort,
19   getFollowingListPaginationAndSort,
20   unfollow
21 } from '../../../../shared/extra-utils/server/follows'
22 import { expectAccountFollows } from '../../../../shared/extra-utils/users/accounts'
23 import { userLogin } from '../../../../shared/extra-utils/users/login'
24 import { createUser } from '../../../../shared/extra-utils/users/users'
25 import {
26   addVideoCommentReply,
27   addVideoCommentThread,
28   getVideoCommentThreads,
29   getVideoThreadComments
30 } from '../../../../shared/extra-utils/videos/video-comments'
31 import { rateVideo } from '../../../../shared/extra-utils/videos/videos'
32 import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
33 import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/extra-utils/videos/video-captions'
34 import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
35
36 const expect = chai.expect
37
38 describe('Test follows', function () {
39   let servers: ServerInfo[] = []
40
41   before(async function () {
42     this.timeout(30000)
43
44     servers = await flushAndRunMultipleServers(3)
45
46     // Get the access tokens
47     await setAccessTokensToServers(servers)
48   })
49
50   it('Should not have followers', async function () {
51     for (const server of servers) {
52       const res = await getFollowersListPaginationAndSort({ url: server.url, start: 0, count: 5, sort: 'createdAt' })
53       const follows = res.body.data
54
55       expect(res.body.total).to.equal(0)
56       expect(follows).to.be.an('array')
57       expect(follows.length).to.equal(0)
58     }
59   })
60
61   it('Should not have following', async function () {
62     for (const server of servers) {
63       const res = await getFollowingListPaginationAndSort({ url: server.url, start: 0, count: 5, sort: 'createdAt' })
64       const follows = res.body.data
65
66       expect(res.body.total).to.equal(0)
67       expect(follows).to.be.an('array')
68       expect(follows.length).to.equal(0)
69     }
70   })
71
72   it('Should have server 1 following server 2 and 3', async function () {
73     this.timeout(30000)
74
75     await follow(servers[0].url, [ servers[1].url, servers[2].url ], servers[0].accessToken)
76
77     await waitJobs(servers)
78   })
79
80   it('Should have 2 followings on server 1', async function () {
81     let res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 1, sort: 'createdAt' })
82     let follows = res.body.data
83
84     expect(res.body.total).to.equal(2)
85     expect(follows).to.be.an('array')
86     expect(follows.length).to.equal(1)
87
88     res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 1, count: 1, sort: 'createdAt' })
89     follows = follows.concat(res.body.data)
90
91     const server2Follow = follows.find(f => f.following.host === 'localhost:' + servers[1].port)
92     const server3Follow = follows.find(f => f.following.host === 'localhost:' + servers[2].port)
93
94     expect(server2Follow).to.not.be.undefined
95     expect(server3Follow).to.not.be.undefined
96     expect(server2Follow.state).to.equal('accepted')
97     expect(server3Follow.state).to.equal('accepted')
98   })
99
100   it('Should search/filter followings on server 1', async function () {
101     const sort = 'createdAt'
102     const start = 0
103     const count = 1
104     const url = servers[0].url
105
106     {
107       const search = ':' + servers[1].port
108
109       {
110         const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search })
111         const follows = res.body.data
112
113         expect(res.body.total).to.equal(1)
114         expect(follows.length).to.equal(1)
115         expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
116       }
117
118       {
119         const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search, state: 'accepted' })
120         expect(res.body.total).to.equal(1)
121         expect(res.body.data).to.have.lengthOf(1)
122       }
123
124       {
125         const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search, state: 'accepted', actorType: 'Person' })
126         expect(res.body.total).to.equal(0)
127         expect(res.body.data).to.have.lengthOf(0)
128       }
129
130       {
131         const res = await getFollowingListPaginationAndSort({
132           url,
133           start,
134           count,
135           sort,
136           search,
137           state: 'accepted',
138           actorType: 'Application'
139         })
140         expect(res.body.total).to.equal(1)
141         expect(res.body.data).to.have.lengthOf(1)
142       }
143
144       {
145         const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search, state: 'pending' })
146         expect(res.body.total).to.equal(0)
147         expect(res.body.data).to.have.lengthOf(0)
148       }
149     }
150
151     {
152       const res = await getFollowingListPaginationAndSort({ url, start, count, sort, search: 'bla' })
153       const follows = res.body.data
154
155       expect(res.body.total).to.equal(0)
156       expect(follows.length).to.equal(0)
157     }
158   })
159
160   it('Should have 0 followings on server 2 and 3', async function () {
161     for (const server of [ servers[1], servers[2] ]) {
162       const res = await getFollowingListPaginationAndSort({ url: server.url, start: 0, count: 5, sort: 'createdAt' })
163       const follows = res.body.data
164
165       expect(res.body.total).to.equal(0)
166       expect(follows).to.be.an('array')
167       expect(follows.length).to.equal(0)
168     }
169   })
170
171   it('Should have 1 followers on server 2 and 3', async function () {
172     for (const server of [ servers[1], servers[2] ]) {
173       const res = await getFollowersListPaginationAndSort({ url: server.url, start: 0, count: 1, sort: 'createdAt' })
174
175       const follows = res.body.data
176       expect(res.body.total).to.equal(1)
177       expect(follows).to.be.an('array')
178       expect(follows.length).to.equal(1)
179       expect(follows[0].follower.host).to.equal('localhost:' + servers[0].port)
180     }
181   })
182
183   it('Should search/filter followers on server 2', async function () {
184     const url = servers[2].url
185     const start = 0
186     const count = 5
187     const sort = 'createdAt'
188
189     {
190       const search = servers[0].port + ''
191
192       {
193         const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search })
194         const follows = res.body.data
195
196         expect(res.body.total).to.equal(1)
197         expect(follows.length).to.equal(1)
198         expect(follows[0].following.host).to.equal('localhost:' + servers[2].port)
199       }
200
201       {
202         const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search, state: 'accepted' })
203         expect(res.body.total).to.equal(1)
204         expect(res.body.data).to.have.lengthOf(1)
205       }
206
207       {
208         const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search, state: 'accepted', actorType: 'Person' })
209         expect(res.body.total).to.equal(0)
210         expect(res.body.data).to.have.lengthOf(0)
211       }
212
213       {
214         const res = await getFollowersListPaginationAndSort({
215           url,
216           start,
217           count,
218           sort,
219           search,
220           state: 'accepted',
221           actorType: 'Application'
222         })
223         expect(res.body.total).to.equal(1)
224         expect(res.body.data).to.have.lengthOf(1)
225       }
226
227       {
228         const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search, state: 'pending' })
229         expect(res.body.total).to.equal(0)
230         expect(res.body.data).to.have.lengthOf(0)
231       }
232     }
233
234     {
235       const res = await getFollowersListPaginationAndSort({ url, start, count, sort, search: 'bla' })
236       const follows = res.body.data
237
238       expect(res.body.total).to.equal(0)
239       expect(follows.length).to.equal(0)
240     }
241   })
242
243   it('Should have 0 followers on server 1', async function () {
244     const res = await getFollowersListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
245     const follows = res.body.data
246
247     expect(res.body.total).to.equal(0)
248     expect(follows).to.be.an('array')
249     expect(follows.length).to.equal(0)
250   })
251
252   it('Should have the correct follows counts', async function () {
253     await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 2)
254     await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
255     await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[2].port, 1, 0)
256
257     // Server 2 and 3 does not know server 1 follow another server (there was not a refresh)
258     await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
259     await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
260
261     await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 1)
262     await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 1, 0)
263   })
264
265   it('Should unfollow server 3 on server 1', async function () {
266     this.timeout(5000)
267
268     await unfollow(servers[0].url, servers[0].accessToken, servers[2])
269
270     await waitJobs(servers)
271   })
272
273   it('Should not follow server 3 on server 1 anymore', async function () {
274     const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 2, sort: 'createdAt' })
275     const follows = res.body.data
276
277     expect(res.body.total).to.equal(1)
278     expect(follows).to.be.an('array')
279     expect(follows.length).to.equal(1)
280
281     expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
282   })
283
284   it('Should not have server 1 as follower on server 3 anymore', async function () {
285     const res = await getFollowersListPaginationAndSort({ url: servers[2].url, start: 0, count: 1, sort: 'createdAt' })
286
287     const follows = res.body.data
288     expect(res.body.total).to.equal(0)
289     expect(follows).to.be.an('array')
290     expect(follows.length).to.equal(0)
291   })
292
293   it('Should have the correct follows counts 2', async function () {
294     await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 1)
295     await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
296
297     await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
298     await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
299
300     await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 0)
301     await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 0, 0)
302   })
303
304   it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () {
305     this.timeout(35000)
306
307     await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'server2' })
308     await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3' })
309
310     await waitJobs(servers)
311
312     let res = await getVideosList(servers[0].url)
313     expect(res.body.total).to.equal(1)
314     expect(res.body.data[0].name).to.equal('server2')
315
316     res = await getVideosList(servers[1].url)
317     expect(res.body.total).to.equal(1)
318     expect(res.body.data[0].name).to.equal('server2')
319
320     res = await getVideosList(servers[2].url)
321     expect(res.body.total).to.equal(1)
322     expect(res.body.data[0].name).to.equal('server3')
323   })
324
325   describe('Should propagate data on a new following', function () {
326     let video4: Video
327
328     before(async function () {
329       this.timeout(20000)
330
331       const video4Attributes = {
332         name: 'server3-4',
333         category: 2,
334         nsfw: true,
335         licence: 6,
336         tags: [ 'tag1', 'tag2', 'tag3' ]
337       }
338
339       await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-2' })
340       await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-3' })
341       await uploadVideo(servers[2].url, servers[2].accessToken, video4Attributes)
342       await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-5' })
343       await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-6' })
344
345       {
346         const user = { username: 'captain', password: 'password' }
347         await createUser({ url: servers[2].url, accessToken: servers[2].accessToken, username: user.username, password: user.password })
348         const userAccessToken = await userLogin(servers[2], user)
349
350         const resVideos = await getVideosList(servers[2].url)
351         video4 = resVideos.body.data.find(v => v.name === 'server3-4')
352
353         {
354           await rateVideo(servers[2].url, servers[2].accessToken, video4.id, 'like')
355           await rateVideo(servers[2].url, userAccessToken, video4.id, 'dislike')
356         }
357
358         {
359           {
360             const text = 'my super first comment'
361             const res = await addVideoCommentThread(servers[2].url, servers[2].accessToken, video4.id, text)
362             const threadId = res.body.comment.id
363
364             const text1 = 'my super answer to thread 1'
365             const childCommentRes = await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text1)
366             const childCommentId = childCommentRes.body.comment.id
367
368             const text2 = 'my super answer to answer of thread 1'
369             await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, childCommentId, text2)
370
371             const text3 = 'my second answer to thread 1'
372             await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text3)
373           }
374
375           {
376             const text = 'will be deleted'
377             const res = await addVideoCommentThread(servers[2].url, servers[2].accessToken, video4.id, text)
378             const threadId = res.body.comment.id
379
380             const text1 = 'answer to deleted'
381             await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text1)
382
383             const text2 = 'will also be deleted'
384             const childCommentRes = await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text2)
385             const childCommentId = childCommentRes.body.comment.id
386
387             const text3 = 'my second answer to deleted'
388             await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, childCommentId, text3)
389
390             await deleteVideoComment(servers[2].url, servers[2].accessToken, video4.id, threadId)
391             await deleteVideoComment(servers[2].url, servers[2].accessToken, video4.id, childCommentId)
392           }
393         }
394
395         {
396           await createVideoCaption({
397             url: servers[2].url,
398             accessToken: servers[2].accessToken,
399             language: 'ar',
400             videoId: video4.id,
401             fixture: 'subtitle-good2.vtt'
402           })
403         }
404       }
405
406       await waitJobs(servers)
407
408       // Server 1 follows server 3
409       await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
410
411       await waitJobs(servers)
412     })
413
414     it('Should have the correct follows counts 3', async function () {
415       await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[0].port, 0, 2)
416       await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[1].port, 1, 0)
417       await expectAccountFollows(servers[0].url, 'peertube@localhost:' + servers[2].port, 1, 0)
418
419       await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[0].port, 0, 1)
420       await expectAccountFollows(servers[1].url, 'peertube@localhost:' + servers[1].port, 1, 0)
421
422       await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[0].port, 0, 1)
423       await expectAccountFollows(servers[2].url, 'peertube@localhost:' + servers[2].port, 1, 0)
424     })
425
426     it('Should have propagated videos', async function () {
427       const res = await getVideosList(servers[0].url)
428       expect(res.body.total).to.equal(7)
429
430       const video2 = res.body.data.find(v => v.name === 'server3-2')
431       video4 = res.body.data.find(v => v.name === 'server3-4')
432       const video6 = res.body.data.find(v => v.name === 'server3-6')
433
434       expect(video2).to.not.be.undefined
435       expect(video4).to.not.be.undefined
436       expect(video6).to.not.be.undefined
437
438       const isLocal = false
439       const checkAttributes = {
440         name: 'server3-4',
441         category: 2,
442         licence: 6,
443         language: 'zh',
444         nsfw: true,
445         description: 'my super description',
446         support: 'my super support text',
447         account: {
448           name: 'root',
449           host: 'localhost:' + servers[2].port
450         },
451         isLocal,
452         commentsEnabled: true,
453         downloadEnabled: true,
454         duration: 5,
455         tags: [ 'tag1', 'tag2', 'tag3' ],
456         privacy: VideoPrivacy.PUBLIC,
457         likes: 1,
458         dislikes: 1,
459         channel: {
460           displayName: 'Main root channel',
461           name: 'root_channel',
462           description: '',
463           isLocal
464         },
465         fixture: 'video_short.webm',
466         files: [
467           {
468             resolution: 720,
469             size: 218910
470           }
471         ]
472       }
473       await completeVideoCheck(servers[0].url, video4, checkAttributes)
474     })
475
476     it('Should have propagated comments', async function () {
477       const res1 = await getVideoCommentThreads(servers[0].url, video4.id, 0, 5, 'createdAt')
478
479       expect(res1.body.total).to.equal(2)
480       expect(res1.body.data).to.be.an('array')
481       expect(res1.body.data).to.have.lengthOf(2)
482
483       {
484         const comment: VideoComment = res1.body.data[0]
485         expect(comment.inReplyToCommentId).to.be.null
486         expect(comment.text).equal('my super first comment')
487         expect(comment.videoId).to.equal(video4.id)
488         expect(comment.id).to.equal(comment.threadId)
489         expect(comment.account.name).to.equal('root')
490         expect(comment.account.host).to.equal('localhost:' + servers[2].port)
491         expect(comment.totalReplies).to.equal(3)
492         expect(dateIsValid(comment.createdAt as string)).to.be.true
493         expect(dateIsValid(comment.updatedAt as string)).to.be.true
494
495         const threadId = comment.threadId
496
497         const res2 = await getVideoThreadComments(servers[0].url, video4.id, threadId)
498
499         const tree: VideoCommentThreadTree = res2.body
500         expect(tree.comment.text).equal('my super first comment')
501         expect(tree.children).to.have.lengthOf(2)
502
503         const firstChild = tree.children[0]
504         expect(firstChild.comment.text).to.equal('my super answer to thread 1')
505         expect(firstChild.children).to.have.lengthOf(1)
506
507         const childOfFirstChild = firstChild.children[0]
508         expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
509         expect(childOfFirstChild.children).to.have.lengthOf(0)
510
511         const secondChild = tree.children[1]
512         expect(secondChild.comment.text).to.equal('my second answer to thread 1')
513         expect(secondChild.children).to.have.lengthOf(0)
514       }
515
516       {
517         const deletedComment: VideoComment = res1.body.data[1]
518         expect(deletedComment).to.not.be.undefined
519         expect(deletedComment.isDeleted).to.be.true
520         expect(deletedComment.deletedAt).to.not.be.null
521         expect(deletedComment.text).to.equal('')
522         expect(deletedComment.inReplyToCommentId).to.be.null
523         expect(deletedComment.account).to.be.null
524         expect(deletedComment.totalReplies).to.equal(3)
525         expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
526
527         const res2 = await getVideoThreadComments(servers[0].url, video4.id, deletedComment.threadId)
528
529         const tree: VideoCommentThreadTree = res2.body
530         const [ commentRoot, deletedChildRoot ] = tree.children
531
532         expect(deletedChildRoot).to.not.be.undefined
533         expect(deletedChildRoot.comment.isDeleted).to.be.true
534         expect(deletedChildRoot.comment.deletedAt).to.not.be.null
535         expect(deletedChildRoot.comment.text).to.equal('')
536         expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
537         expect(deletedChildRoot.comment.account).to.be.null
538         expect(deletedChildRoot.children).to.have.lengthOf(1)
539
540         const answerToDeletedChild = deletedChildRoot.children[0]
541         expect(answerToDeletedChild.comment).to.not.be.undefined
542         expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id)
543         expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted')
544         expect(answerToDeletedChild.comment.account.name).to.equal('root')
545
546         expect(commentRoot.comment).to.not.be.undefined
547         expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id)
548         expect(commentRoot.comment.text).to.equal('answer to deleted')
549         expect(commentRoot.comment.account.name).to.equal('root')
550       }
551     })
552
553     it('Should have propagated captions', async function () {
554       const res = await listVideoCaptions(servers[0].url, video4.id)
555       expect(res.body.total).to.equal(1)
556       expect(res.body.data).to.have.lengthOf(1)
557
558       const caption1: VideoCaption = res.body.data[0]
559       expect(caption1.language.id).to.equal('ar')
560       expect(caption1.language.label).to.equal('Arabic')
561       expect(caption1.captionPath).to.equal('/static/video-captions/' + video4.uuid + '-ar.vtt')
562       await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.')
563     })
564
565     it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () {
566       this.timeout(5000)
567
568       await unfollow(servers[0].url, servers[0].accessToken, servers[2])
569
570       await waitJobs(servers)
571
572       const res = await getVideosList(servers[0].url)
573       expect(res.body.total).to.equal(1)
574     })
575
576   })
577
578   after(async function () {
579     await cleanupTests(servers)
580   })
581 })