feeds.ts 16 KB

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