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