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