Newer
Older
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
import RedisMock from 'ioredis-mock'
import sinon from 'sinon'

richard.petersen
committed
import zlib from 'node:zlib'
const image = fs.readFileSync('./spec/media/image.png')
const imageStat = fs.statSync('./spec/media/image.png')
const sandbox = sinon.createSandbox()
describe('File caching service', function () {
beforeEach(async function () {
mockConfig({ urls: ['http://ui-server/'] })
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({
'example.js': { imports: ['test.txt'] },
'test.txt': { },
'main.css': {},
'index.html': {
file: 'index.html.js',
isEntry: true,
imports: ['example.js'],
css: ['main.css']
},
'image.png': {}
}),
'/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
'/test.txt': () => new Response('this is test', { headers: { 'content-type': 'text/plain' } }),
'/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }),
'/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
'/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }),
'/favicon.ico': 'not really a favicon, though',
'/test.svg': () => {
if (count > 0) {
return new Response(null, { status: 404 })
}
count++
return new Response('<svg></svg>', { headers: { 'content-type': 'image/svg' } })
},
'/image.png': () => {
return new Response(image, {
headers: {
'Content-Type': 'image/png',
'Content-Length': imageStat.size.toString()
afterEach(async function () {
await new RedisMock().flushdb()
it('serves files defined in manifest.json file', async function () {
const response = await request(app.server).get('/example.js')
expect(response.headers['content-type']).to.equal('application/javascript')
expect(response.text).to.equal('this is example')
// expect(response.headers['content-security-policy']).to.contain('sha256-NzZhMTE2Njc2YTgyNTZmZTdlZGVjZDU3YTNmYzRjNmM1OWZkMTI2NjRkYzZmMWM3YTkwMGU3ZTdhNDlhZmVlMwo=')
const response2 = await request(app.server).get('/test.txt')
expect(response2.headers['content-type']).to.equal('text/plain')
expect(response2.text).to.equal('this is test')
const response = await request(app.server).get('/main.css')
expect(response.headers['content-type']).to.equal('text/css')
// expect(response.headers['content-security-policy']).to.contain('sha256-YjRiYWRlYTVhYmM5ZTZkNjE2ZGM4YjcwZWRlNzUxMmU0YjgxY2UxMWExOTI2ZjM1NzM1M2Y2MWJjNmUwMmZjMwo=')
it('serves / as index.html', async function () {
const response = await request(app.server).get('/')
expect(response.headers['content-type']).to.equal('text/html')
expect(response.text).to.equal('<html><head></head><body>it\'s me</body></html>')
it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () {
const response = await request(app.server).get('/favicon.ico')
expect(response.text).to.equal('not really a favicon, though')
})
it('returns 404 if file can not be resolved', async function () {
const response = await request(app.server).get('/unknown-file.txt')
})
it('serves binary files', async function () {
const response = await request(app.server).get('/image.png')
expect(response.body.length === imageStat.size)
expect(response.body).to.deep.equal(image)
})
it('only fetches files once', async function () {
let spy
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/example.js': spy = sandbox.spy(() => {
return new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
})
}
})
app = await mockApp()
expect(spy.callCount).to.equal(0)
let response = await request(app.server).get('/example.js')
expect(response.statusCode).to.equal(200)
expect(spy.callCount).to.equal(1)
response = await request(app.server).get('/example.js')
expect(response.statusCode).to.equal(200)
expect(spy.callCount).to.equal(1)
})
it('only fetches files once, but deliver from local cache', async function () {
let spy
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/example.js': spy = sandbox.spy(() => {
return new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
})
}
})
app = await mockApp()
expect(spy.callCount).to.equal(0)
let response = await request(app.server).get('/example.js')
expect(response.statusCode).to.equal(200)
expect(spy.callCount).to.equal(1)
// delete file from redis
await redis.client.del(`${response.headers.version}:/example.js:body`)
await redis.client.del(`${response.headers.version}:/example.js:meta`)
// and fetch once more
response = await request(app.server).get('/example.js')
expect(response.statusCode).to.equal(200)
expect(spy.callCount).to.equal(1)
})
it('delivers binary files from cache', async function () {
let spy
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/image.png': spy = sandbox.spy(() => {
return new Response(image, {
headers: {
'Content-Type': 'image/png',
'Content-Length': imageStat.size.toString()
}
})
})
}
})
app = await mockApp()
expect(spy.callCount).to.equal(0)
let response = await request(app.server).get('/image.png')
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal(image)
expect(spy.callCount).to.equal(1)
response = await request(app.server).get('/image.png')
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal(image)
expect(spy.callCount).to.equal(1)
})
it('a file is not cached again, if loaded from cache', async function () {
const spy = sandbox.spy(redis.client, 'set')
let response = await request(app.server).get('/example.js')
expect(response.statusCode).to.equal(200)
// called 4 times.
// once for manifests
// once for dependencies
// two times for for example.js (meta and body)
expect(spy.callCount).to.equal(4)
response = await request(app.server).get('/example.js')
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
expect(response.statusCode).to.equal(200)
// should still be called 4 times, because everything is in cache
expect(spy.callCount).to.equal(4)
})
it('requests known file only from one server', async function () {
let spy1, spy2
mockConfig({ urls: ['http://ui-server1/', 'http://ui-server2/'] })
// we have example.js in both files. the first one will be overwritten and therefore not be called
mockFetch({
'http://ui-server1': {
'/manifest.json': generateSimpleViteManifest({
'example.js': { }
}),
'/example.js': spy1 = sandbox.spy(() => {
return new Response('example', { headers: { 'content-type': 'text/plain' } })
})
},
'http://ui-server2': {
'/manifest.json': generateSimpleViteManifest({
'example.js': { }
}),
'/example.js': spy2 = sandbox.spy(() => {
return new Response('example', { headers: { 'content-type': 'text/plain' } })
})
}
})
app = await mockApp()
const response = await request(app.server).get('/example.js')
expect(response.statusCode).to.equal(200)
expect(spy1.callCount).to.equal(0)
expect(spy2.callCount).to.equal(1)
})
it('serves cached files with version', async function () {
// we have example.js in both files. the first one will be overwritten and therefore not be called
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({
'example.js': { }
}),
'/example.js': td.when(td.func()(td.matchers.anything())).thenReturn(
new Response('first', { headers: { 'content-type': 'text/plain' } }),
new Response('second', { headers: { 'content-type': 'text/plain' } })
)
}
})
app = await mockApp()
const response1 = await request(app.server).get('/example.js').set('version', '1234')
expect(response1.statusCode).to.equal(200)
expect(response1.text).to.equal('first')
const response2 = await request(app.server).get('/example.js')
expect(response2.statusCode).to.equal(200)
expect(response2.text).to.equal('second')
const latestVersion = response2.headers['latest-version']
const response3 = await request(app.server).get('/example.js').set('version', '1234')
expect(response3.statusCode).to.equal(200)
expect(response3.text).to.equal('first')
const response4 = await request(app.server).get('/example.js')
expect(response4.statusCode).to.equal(200)
expect(response4.text).to.equal('second')
const response5 = await request(app.server).get('/example.js').set('version', latestVersion)
expect(response5.statusCode).to.equal(200)
expect(response5.text).to.equal('second')
it('only fetches files once, even when requested simultanously', async function () {
let spy
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/example.js': spy = sandbox.spy(() => {
return new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
})
}
})
app = await mockApp()
expect(spy.callCount).to.equal(0)
const [res1, res2] = await Promise.all([
request(app.server).get('/example.js'),
request(app.server).get('/example.js')
])
expect(res1.statusCode).to.equal(200)
expect(res2.statusCode).to.equal(200)
expect(spy.callCount).to.equal(1)
})
it('only fetches manifests once, even when requested simultanously', async function () {
let spy
mockFetch({
'http://ui-server': {
'/manifest.json': spy = sandbox.spy(async () => {
await new Promise(resolve => setTimeout(resolve, 10))
return new Response(JSON.stringify(generateSimpleViteManifest({})), { headers: { 'content-type': 'application/json' } })
}),
'/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
}
})
app = await mockApp()
expect(spy.callCount).to.equal(0)
const [res1, res2] = await Promise.all([
request(app.server).get('/manifests'),
request(app.server).get('/example.js')
])
expect(res1.statusCode).to.equal(200)
expect(res2.statusCode).to.equal(200)
expect(spy.callCount).to.equal(1)
})
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
describe('redis request latency', function () {
beforeEach(function () {
// overwrite redis.get to simulate network latency
const get = redis.client.get
redis.client.get = function () {
return new Promise(resolve => {
setTimeout(async () => {
resolve(await get.apply(redis.client, arguments))
}, 20)
})
}
})
it('only requests files once, even though the response takes some time', async function () {
let spy
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/example.js': spy = sandbox.spy(() => {
return new Response('this is example', { headers: { 'content-type': 'application/javascript' } })
})
}
})
app = await mockApp()
expect(spy.callCount).to.equal(0)
const [res1, res2] = await Promise.all([
request(app.server).get('/example.js'),
request(app.server).get('/example.js')
])
expect(res1.statusCode).to.equal(200)
expect(res2.statusCode).to.equal(200)
expect(spy.callCount).to.equal(1)
})
})

richard.petersen
committed
it('serves files as gzip', async function () {
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/large.js': () => new Response([...new Array(2500)].join(' '), { headers: { 'content-type': 'application/javascript' } })
}
})
app = await mockApp()
const response = await request(app.server).get('/large.js')

richard.petersen
committed
expect(response.statusCode).to.equal(200)
expect(response.headers['content-encoding']).to.equal('gzip')
// check for files in redis
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
expect(zlib.gunzipSync(await redis.client.getBuffer('ui-middleware:554855300:/large.js:body')).toString()).to.equal(response.text)
})
it('does not serve small files with gzip', async function () {
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/small.js': () => new Response('small', { headers: { 'content-type': 'application/javascript' } })
}
})
app = await mockApp()
const response = await request(app.server).get('/small.js')
expect(response.statusCode).to.equal(200)
expect(response.headers).to.not.have.property('content-encoding')
// check for files in redis
expect((await redis.client.getBuffer('ui-middleware:554855300:/small.js:body')).toString()).to.equal(response.text)
})
it('does not serve other mime types with gzip', async function () {
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({}),
'/file.mp3': () => new Response('123', { headers: { 'content-type': 'audio/mpeg' } })
}
})
app = await mockApp()
const response = await request(app.server).get('/file.mp3')
expect(response.statusCode).to.equal(200)
expect(response.headers).to.not.have.property('content-encoding')
// check for files in redis
expect(await redis.client.getBuffer('ui-middleware:554855300:/file.mp3:body')).to.deep.equal(response.body)

richard.petersen
committed
})