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