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