]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/feeds/feeds.ts
f1055ea4492ae40265df1893eb459c47f354ba9f
[github/Chocobozzz/PeerTube.git] / server / tests / feeds / feeds.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import 'mocha'
4 import * as chai from 'chai'
5 import * as libxmljs from 'libxmljs'
6 import {
7 addAccountToAccountBlocklist,
8 addAccountToServerBlocklist,
9 removeAccountFromServerBlocklist
10 } from '@shared/extra-utils/users/blocklist'
11 import { addUserSubscription, listUserSubscriptionVideos } from '@shared/extra-utils/users/user-subscriptions'
12 import { VideoPrivacy } from '@shared/models'
13 import { ScopedToken } from '@shared/models/users/user-scoped-token'
14 import {
15 cleanupTests,
16 createUser,
17 doubleFollow,
18 flushAndRunMultipleServers,
19 flushAndRunServer,
20 getJSONfeed,
21 getMyUserInformation,
22 getUserScopedTokens,
23 getXMLfeed,
24 renewUserScopedTokens,
25 ServerInfo,
26 setAccessTokensToServers,
27 uploadVideo,
28 uploadVideoAndGetId,
29 userLogin
30 } from '../../../shared/extra-utils'
31 import { waitJobs } from '../../../shared/extra-utils/server/jobs'
32 import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
33 import { User } from '../../../shared/models/users'
34 import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
35
36 chai.use(require('chai-xml'))
37 chai.use(require('chai-json-schema'))
38 chai.config.includeStack = true
39 const expect = chai.expect
40
41 describe('Test syndication feeds', () => {
42 let servers: ServerInfo[] = []
43 let serverHLSOnly: ServerInfo
44 let userAccessToken: string
45 let rootAccountId: number
46 let rootChannelId: number
47 let userAccountId: number
48 let userChannelId: number
49 let userFeedToken: string
50
51 before(async function () {
52 this.timeout(120000)
53
54 // Run servers
55 servers = await flushAndRunMultipleServers(2)
56 serverHLSOnly = await flushAndRunServer(3, {
57 transcoding: {
58 enabled: true,
59 webtorrent: { enabled: false },
60 hls: { enabled: true }
61 }
62 })
63
64 await setAccessTokensToServers([ ...servers, serverHLSOnly ])
65 await doubleFollow(servers[0], servers[1])
66
67 {
68 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
69 const user: User = res.body
70 rootAccountId = user.account.id
71 rootChannelId = user.videoChannels[0].id
72 }
73
74 {
75 const attr = { username: 'john', password: 'password' }
76 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: attr.username, password: attr.password })
77 userAccessToken = await userLogin(servers[0], attr)
78
79 const res = await getMyUserInformation(servers[0].url, userAccessToken)
80 const user: User = res.body
81 userAccountId = user.account.id
82 userChannelId = user.videoChannels[0].id
83
84 const res2 = await getUserScopedTokens(servers[0].url, userAccessToken)
85 const token: ScopedToken = res2.body
86 userFeedToken = token.feedToken
87 }
88
89 {
90 await uploadVideo(servers[0].url, userAccessToken, { name: 'user video' })
91 }
92
93 {
94 const videoAttributes = {
95 name: 'my super name for server 1',
96 description: 'my super description for server 1',
97 fixture: 'video_short.webm'
98 }
99 const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
100 const videoId = res.body.video.id
101
102 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'super comment 1')
103 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'super comment 2')
104 }
105
106 {
107 const videoAttributes = { name: 'unlisted video', privacy: VideoPrivacy.UNLISTED }
108 const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
109 const videoId = res.body.video.id
110
111 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'comment on unlisted video')
112 }
113
114 await waitJobs(servers)
115 })
116
117 describe('All feed', function () {
118
119 it('Should be well formed XML (covers RSS 2.0 and ATOM 1.0 endpoints)', async function () {
120 for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) {
121 const rss = await getXMLfeed(servers[0].url, feed)
122 expect(rss.text).xml.to.be.valid()
123
124 const atom = await getXMLfeed(servers[0].url, feed, 'atom')
125 expect(atom.text).xml.to.be.valid()
126 }
127 })
128
129 it('Should be well formed JSON (covers JSON feed 1.0 endpoint)', async function () {
130 for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) {
131 const json = await getJSONfeed(servers[0].url, feed)
132 expect(JSON.parse(json.text)).to.be.jsonSchema({ type: 'object' })
133 }
134 })
135 })
136
137 describe('Videos feed', function () {
138
139 it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () {
140 for (const server of servers) {
141 const rss = await getXMLfeed(server.url, 'videos')
142 const xmlDoc = libxmljs.parseXmlString(rss.text)
143 const xmlEnclosure = xmlDoc.get('/rss/channel/item/enclosure')
144 expect(xmlEnclosure).to.exist
145 expect(xmlEnclosure.attr('type').value()).to.be.equal('application/x-bittorrent')
146 expect(xmlEnclosure.attr('length').value()).to.be.equal('218910')
147 expect(xmlEnclosure.attr('url').value()).to.contain('720.torrent')
148 }
149 })
150
151 it('Should contain a valid \'attachments\' object (covers JSON feed 1.0 endpoint)', async function () {
152 for (const server of servers) {
153 const json = await getJSONfeed(server.url, 'videos')
154 const jsonObj = JSON.parse(json.text)
155 expect(jsonObj.items.length).to.be.equal(2)
156 expect(jsonObj.items[0].attachments).to.exist
157 expect(jsonObj.items[0].attachments.length).to.be.eq(1)
158 expect(jsonObj.items[0].attachments[0].mime_type).to.be.eq('application/x-bittorrent')
159 expect(jsonObj.items[0].attachments[0].size_in_bytes).to.be.eq(218910)
160 expect(jsonObj.items[0].attachments[0].url).to.contain('720.torrent')
161 }
162 })
163
164 it('Should filter by account', async function () {
165 {
166 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: rootAccountId })
167 const jsonObj = JSON.parse(json.text)
168 expect(jsonObj.items.length).to.be.equal(1)
169 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
170 expect(jsonObj.items[0].author.name).to.equal('root')
171 }
172
173 {
174 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId })
175 const jsonObj = JSON.parse(json.text)
176 expect(jsonObj.items.length).to.be.equal(1)
177 expect(jsonObj.items[0].title).to.equal('user video')
178 expect(jsonObj.items[0].author.name).to.equal('john')
179 }
180
181 for (const server of servers) {
182 {
183 const json = await getJSONfeed(server.url, 'videos', { accountName: 'root@localhost:' + servers[0].port })
184 const jsonObj = JSON.parse(json.text)
185 expect(jsonObj.items.length).to.be.equal(1)
186 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
187 }
188
189 {
190 const json = await getJSONfeed(server.url, 'videos', { accountName: 'john@localhost:' + servers[0].port })
191 const jsonObj = JSON.parse(json.text)
192 expect(jsonObj.items.length).to.be.equal(1)
193 expect(jsonObj.items[0].title).to.equal('user video')
194 }
195 }
196 })
197
198 it('Should filter by video channel', async function () {
199 {
200 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: rootChannelId })
201 const jsonObj = JSON.parse(json.text)
202 expect(jsonObj.items.length).to.be.equal(1)
203 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
204 expect(jsonObj.items[0].author.name).to.equal('root')
205 }
206
207 {
208 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: userChannelId })
209 const jsonObj = JSON.parse(json.text)
210 expect(jsonObj.items.length).to.be.equal(1)
211 expect(jsonObj.items[0].title).to.equal('user video')
212 expect(jsonObj.items[0].author.name).to.equal('john')
213 }
214
215 for (const server of servers) {
216 {
217 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'root_channel@localhost:' + servers[0].port })
218 const jsonObj = JSON.parse(json.text)
219 expect(jsonObj.items.length).to.be.equal(1)
220 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
221 }
222
223 {
224 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'john_channel@localhost:' + servers[0].port })
225 const jsonObj = JSON.parse(json.text)
226 expect(jsonObj.items.length).to.be.equal(1)
227 expect(jsonObj.items[0].title).to.equal('user video')
228 }
229 }
230 })
231
232 it('Should correctly have videos feed with HLS only', async function () {
233 this.timeout(120000)
234
235 await uploadVideo(serverHLSOnly.url, serverHLSOnly.accessToken, { name: 'hls only video' })
236
237 await waitJobs([ serverHLSOnly ])
238
239 const json = await getJSONfeed(serverHLSOnly.url, 'videos')
240 const jsonObj = JSON.parse(json.text)
241 expect(jsonObj.items.length).to.be.equal(1)
242 expect(jsonObj.items[0].attachments).to.exist
243 expect(jsonObj.items[0].attachments.length).to.be.eq(4)
244
245 for (let i = 0; i < 4; i++) {
246 expect(jsonObj.items[0].attachments[i].mime_type).to.be.eq('application/x-bittorrent')
247 expect(jsonObj.items[0].attachments[i].size_in_bytes).to.be.greaterThan(0)
248 expect(jsonObj.items[0].attachments[i].url).to.exist
249 }
250 })
251 })
252
253 describe('Video comments feed', function () {
254
255 it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted videos', async function () {
256 for (const server of servers) {
257 const json = await getJSONfeed(server.url, 'video-comments')
258
259 const jsonObj = JSON.parse(json.text)
260 expect(jsonObj.items.length).to.be.equal(2)
261 expect(jsonObj.items[0].html_content).to.equal('super comment 2')
262 expect(jsonObj.items[1].html_content).to.equal('super comment 1')
263 }
264 })
265
266 it('Should not list comments from muted accounts or instances', async function () {
267 this.timeout(30000)
268
269 const remoteHandle = 'root@localhost:' + servers[0].port
270
271 await addAccountToServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
272
273 {
274 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 2 })
275 const jsonObj = JSON.parse(json.text)
276 expect(jsonObj.items.length).to.be.equal(0)
277 }
278
279 await removeAccountFromServerBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
280
281 {
282 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'server 2' })).uuid
283 await waitJobs(servers)
284 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'super comment')
285 await waitJobs(servers)
286
287 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 3 })
288 const jsonObj = JSON.parse(json.text)
289 expect(jsonObj.items.length).to.be.equal(3)
290 }
291
292 await addAccountToAccountBlocklist(servers[1].url, servers[1].accessToken, remoteHandle)
293
294 {
295 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 4 })
296 const jsonObj = JSON.parse(json.text)
297 expect(jsonObj.items.length).to.be.equal(2)
298 }
299 })
300 })
301
302 describe('Video feed from my subscriptions', function () {
303 let feeduserAccountId: number
304 let feeduserFeedToken: string
305
306 it('Should list no videos for a user with no videos and no subscriptions', async function () {
307 const attr = { username: 'feeduser', password: 'password' }
308 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: attr.username, password: attr.password })
309 const feeduserAccessToken = await userLogin(servers[0], attr)
310
311 {
312 const res = await getMyUserInformation(servers[0].url, feeduserAccessToken)
313 const user: User = res.body
314 feeduserAccountId = user.account.id
315 }
316
317 {
318 const res = await getUserScopedTokens(servers[0].url, feeduserAccessToken)
319 const token: ScopedToken = res.body
320 feeduserFeedToken = token.feedToken
321 }
322
323 {
324 const res = await listUserSubscriptionVideos(servers[0].url, feeduserAccessToken)
325 expect(res.body.total).to.equal(0)
326
327 const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: feeduserAccountId, token: feeduserFeedToken })
328 const jsonObj = JSON.parse(json.text)
329 expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos
330 }
331 })
332
333 it('Should fail with an invalid token', async function () {
334 await getJSONfeed(servers[0].url, 'subscriptions', { accountId: feeduserAccountId, token: 'toto' }, HttpStatusCode.FORBIDDEN_403)
335 })
336
337 it('Should fail with a token of another user', async function () {
338 await getJSONfeed(
339 servers[0].url,
340 'subscriptions',
341 { accountId: feeduserAccountId, token: userFeedToken },
342 HttpStatusCode.FORBIDDEN_403
343 )
344 })
345
346 it('Should list no videos for a user with videos but no subscriptions', async function () {
347 const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken)
348 expect(res.body.total).to.equal(0)
349
350 const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken })
351 const jsonObj = JSON.parse(json.text)
352 expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos
353 })
354
355 it('Should list self videos for a user with a subscription to themselves', async function () {
356 this.timeout(30000)
357
358 await addUserSubscription(servers[0].url, userAccessToken, 'john_channel@localhost:' + servers[0].port)
359 await waitJobs(servers)
360
361 {
362 const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken)
363 expect(res.body.total).to.equal(1)
364 expect(res.body.data[0].name).to.equal('user video')
365
366 const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 1 })
367 const jsonObj = JSON.parse(json.text)
368 expect(jsonObj.items.length).to.be.equal(1) // subscribed to self, it should not list the instance's videos but list john's
369 }
370 })
371
372 it('Should list videos of a user\'s subscription', async function () {
373 this.timeout(30000)
374
375 await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:' + servers[0].port)
376 await waitJobs(servers)
377
378 {
379 const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken)
380 expect(res.body.total).to.equal(2, "there should be 2 videos part of the subscription")
381
382 const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 2 })
383 const jsonObj = JSON.parse(json.text)
384 expect(jsonObj.items.length).to.be.equal(2) // subscribed to root, it should not list the instance's videos but list root/john's
385 }
386 })
387
388 it('Should renew the token, and so have an invalid old token', async function () {
389 await renewUserScopedTokens(servers[0].url, userAccessToken)
390
391 await getJSONfeed(
392 servers[0].url,
393 'subscriptions',
394 { accountId: userAccountId, token: userFeedToken, version: 3 },
395 HttpStatusCode.FORBIDDEN_403
396 )
397 })
398
399 it('Should succeed with the new token', async function () {
400 const res2 = await getUserScopedTokens(servers[0].url, userAccessToken)
401 const token: ScopedToken = res2.body
402 userFeedToken = token.feedToken
403
404 await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 4 })
405 })
406
407 })
408
409 after(async function () {
410 await cleanupTests([ ...servers, serverHLSOnly ])
411 })
412 })