Skip to content
Snippets Groups Projects
file_caching_test.js 14.7 KiB
Newer Older
import request from 'supertest'
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch, mockRedis } from './util.js'
import fs from 'fs'
import { expect } from 'chai'
import * as td from 'testdouble'
import RedisMock from 'ioredis-mock'
import sinon from 'sinon'

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 () {
  let app
  beforeEach(async function () {
    mockConfig({ urls: ['http://ui-server/'] })
    redis = mockRedis()
    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()
    app = await mockApp()
  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.statusCode).to.equal(200)
    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.statusCode).to.equal(200)
    expect(response2.headers['content-type']).to.equal('text/plain')
    expect(response2.text).to.equal('this is test')
  it('serves css files', async function () {
    const response = await request(app.server).get('/main.css')
    expect(response.statusCode).to.equal(200)
    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.statusCode).to.equal(200)
    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.statusCode).to.equal(200)
    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')
    expect(response.statusCode).to.equal(404)
  it('serves binary files', async function () {
    const response = await request(app.server).get('/image.png')
    expect(response.statusCode).to.equal(200)
    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')
    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)
  })

  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)
    })
  })

  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')
    expect(response.statusCode).to.equal(200)
    expect(response.headers['content-encoding']).to.equal('gzip')

    // check for files in redis
    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)