Skip to content
Snippets Groups Projects
Commit 2bea2a40 authored by richard.petersen's avatar richard.petersen :sailboat:
Browse files

Introduce testdouble as module loader

parent fdb4b2c1
No related branches found
No related tags found
No related merge requests found
This diff is collapsed.
......@@ -9,7 +9,7 @@
"start": "node src/index.js",
"dev": "nodemon index.js",
"prepare": "husky install",
"test": "LOG_LEVEL=error mocha --parallel",
"test": "LOG_LEVEL=error mocha --loader=testdouble",
"release-chart": "cd helm/core-ui-middleware/ && npx --package=@open-xchange/release-it -- release-it",
"release-app": "npx --package=@open-xchange/release-it -- release-it",
"release": "yarn release-chart && yarn release-app"
......
import request from 'supertest'
import { createApp } from '../src/createApp.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { expect } from 'chai'
const port = getRandomPort()
import { Response } from 'node-fetch'
import * as td from 'testdouble'
describe('JS files with dependencies contain events', function () {
let app
let mockserver
let restoreConfig
before(function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] }))
app = createApp()
})
after(function () {
restoreConfig()
})
beforeEach(async function () {
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({
'example.js': {},
'main.css': {},
'index.html': {
file: 'index.html.js',
isEntry: true,
imports: ['example.js'],
css: ['main.css']
}
}),
'/example.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is example'),
'/index.html.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('console.log("this is index.html.js")'),
'/main.css': (req, res) => res.setHeader('content-type', 'text/css').status(200).send('.foo { color: #000; }'),
'/index.html': (req, res) => res.setHeader('content-type', 'text/html').status(200).send('<html><head></head><body>it\'s me</body></html>')
before(async function () {
mockConfig({ urls: ['http://ui-server/'] })
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({
'example.js': {},
'main.css': {},
'index.html': {
file: 'index.html.js',
isEntry: true,
imports: ['example.js'],
css: ['main.css']
}
}),
'/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
'/index.html.js': () => new Response('console.log("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' } })
}
})
await request(app).get('/ready')
app = await mockApp()
})
afterEach(function () {
mockserver.close()
process.env.CACHE_TTL = 30000
after(function () {
td.reset()
})
it('javascript file contains dispatcher for dependencies', async function () {
......
import request from 'supertest'
import { createApp } from '../src/createApp.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import fs from 'fs'
import { expect } from 'chai'
import * as td from 'testdouble'
import { Response } from 'node-fetch'
const image = fs.readFileSync('./spec/media/image.png')
const imageStat = fs.statSync('./spec/media/image.png')
const port = getRandomPort()
describe('File caching service', function () {
let app
let mockserver
let restoreConfig
before(function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] }))
app = createApp()
})
after(function () {
restoreConfig()
})
beforeEach(async function () {
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/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': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is example'),
'/test.txt': (req, res) => res.setHeader('content-type', 'text/plain').status(200).send('this is test'),
'/index.html.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is index.html.js'),
'/index.html': (req, res) => res.setHeader('content-type', 'text/html').status(200).send('<html><head></head><body>it\'s me</body></html>'),
'/main.css': (req, res) => res.setHeader('content-type', 'text/css').status(200).send('.foo { color: #000; }'),
'/favicon.ico': 'not really a favicon, though',
'/image.png': (req, res) => {
// need to do this like this, because jest messes up file system within tests
res.set({
'Content-Type': 'image/png',
'Content-Length': imageStat.size
})
res.end(image)
before(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',
'/image.png': () => {
return new Response(image, {
headers: {
'Content-Type': 'image/png',
'Content-Length': imageStat.size
}
})
}
}
})
await request(app).get('/ready')
app = await mockApp()
})
afterEach(function () {
mockserver.close()
process.env.CACHE_TTL = 30000
after(function () {
td.reset()
})
it('serves files defined in manifest.json file', async function () {
const response = await request(app).get('/example.js')
expect(response.statusCode).to.equal(200)
expect(response.headers['content-type']).to.equal('application/javascript; charset=utf-8')
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).get('/test.txt')
expect(response2.statusCode).to.equal(200)
expect(response2.headers['content-type']).to.equal('text/plain; charset=utf-8')
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).get('/main.css')
expect(response.statusCode).to.equal(200)
expect(response.headers['content-type']).to.equal('text/css; charset=utf-8')
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).get('/')
expect(response.statusCode).to.equal(200)
expect(response.headers['content-type']).to.equal('text/html; charset=utf-8')
expect(response.headers['content-type']).to.equal('text/html')
expect(response.text).to.equal('<html><head></head><body>it\'s me</body></html>')
})
......@@ -96,7 +86,7 @@ describe('File caching service', function () {
it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () {
const response = await request(app).get('/favicon.ico')
expect(response.statusCode).to.equal(200)
expect(response.body).to.equal('not really a favicon, though')
expect(response.text).to.equal('not really a favicon, though')
})
it('returns 404 if file can not be resolved', async function () {
......
import request from 'supertest'
import { createApp } from '../src/createApp.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { expect } from 'chai'
const port = getRandomPort()
import { Response } from 'node-fetch'
import * as td from 'testdouble'
describe('Responses contain custom headers', function () {
let app
let mockserver
let restoreConfig
before(function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] }))
app = createApp()
})
after(function () {
restoreConfig()
})
beforeEach(async function () {
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({
'example.js': {},
'main.css': {},
'index.html': {
file: 'index.html.js',
isEntry: true,
imports: ['example.js'],
css: ['main.css']
}
}),
'/example.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is example'),
'/index.html.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is index.html.js'),
'/main.css': (req, res) => res.setHeader('content-type', 'text/css').status(200).send('.foo { color: #000; }'),
'/index.html': (req, res) => res.setHeader('content-type', 'text/html').status(200).send('<html><head></head><body>it\'s me</body></html>')
before(async function () {
mockConfig({ urls: ['http://ui-server/'] })
mockFetch({
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({
'example.js': {},
'main.css': {},
'index.html': {
file: 'index.html.js',
isEntry: true,
imports: ['example.js'],
css: ['main.css']
}
}),
'/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
'/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' } })
}
})
await request(app).get('/ready')
app = await mockApp()
})
afterEach(function () {
mockserver.close()
process.env.CACHE_TTL = 30000
after(function () {
td.reset()
})
it('index.html has version', async function () {
......
......@@ -2,7 +2,7 @@ import { viteToOxManifest } from '../src/manifests.js'
import { expect } from 'chai'
describe('Vite manifest parsing', function () {
it('should', function () {
it('should work', function () {
const manifests = viteToOxManifest({
'../io.ox/guidedtours/i18n.de_DE.js': {
file: 'io.ox/guidedtours/i18n.de_DE.js',
......
import request from 'supertest'
import { createApp } from '../src/createApp.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { expect } from 'chai'
const port = getRandomPort()
import { Response } from 'node-fetch'
import * as td from 'testdouble'
describe('Responses contain custom headers', function () {
let fetchConfig
let app
let mockserver
let restoreConfig
before(function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] }))
app = createApp()
before(async function () {
mockConfig({ urls: ['http://ui-server/'] })
mockFetch(fetchConfig = {
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({
'example.js': {}
}),
'/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
'/meta.json': { name: 'sample-service', version: '1.0' }
}
})
app = await mockApp()
})
after(function () {
restoreConfig()
})
beforeEach(async function () {
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({
'example.js': {}
}),
'/example.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is example'),
'/meta.json': { name: 'sample-service', version: '1.0' }
})
await request(app).get('/ready')
td.reset()
})
afterEach(function () {
mockserver.close()
delete process.env.APP_VERSION
delete process.env.BUILD_TIMESTAMP
delete process.env.CI_COMMIT_SHA
......@@ -63,14 +57,25 @@ describe('Responses contain custom headers', function () {
})
})
it('does not have metadata from ui service when unavailable', async function () {
await mockserver.close()
describe('without service avaible', function () {
let prevConfig
const response = await request(app).get('/meta')
expect(response.statusCode).to.equal(200)
expect(response.body).to.not.deep.contain({
name: 'sample-service',
version: '1.0'
beforeEach(function () {
prevConfig = fetchConfig['http://ui-server']
delete fetchConfig['http://ui-server']
})
afterEach(function () {
fetchConfig['http://ui-server'] = prevConfig
})
it('does not have metadata from ui service when unavailable', async function () {
const response = await request(app).get('/meta')
expect(response.statusCode).to.equal(200)
expect(response.body).to.not.deep.contain({
name: 'sample-service',
version: '1.0'
})
})
})
})
import request from 'supertest'
import { createApp } from '../src/createApp.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { expect } from 'chai'
const port = getRandomPort()
const port2 = getRandomPort()
import * as td from 'testdouble'
describe('UI Middleware', function () {
let app
let mockserver, mockserver2
let restoreConfig
before(function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] }))
app = createApp()
let fetchConfig
before(async function () {
mockConfig({ urls: ['http://ui-server/'] })
mockFetch(fetchConfig = {
'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'test' }),
'/example.js': ''
}
})
app = await mockApp()
})
after(function () {
restoreConfig()
})
beforeEach(async function () {
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'test' }),
'/example.js': ''
})
td.reset()
})
afterEach(async function () {
await mockserver?.close()
await mockserver2?.close()
process.env.CACHE_TTL = 30000
})
......@@ -48,74 +40,82 @@ describe('UI Middleware', function () {
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
})
it('caches manifest data', async function () {
const response = await request(app).get('/manifests')
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
mockserver.close()
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
'/example.js': ''
describe('when configuration changes', function () {
let prevConfig
beforeEach(async function () {
prevConfig = fetchConfig['http://ui-server']
})
await new Promise(resolve => setTimeout(resolve, 150))
afterEach(function () {
fetchConfig['http://ui-server'] = prevConfig
})
const response2 = await request(app).get('/manifests')
expect(response2.statusCode).to.equal(200)
expect(response2.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
})
it('caches manifest data', async function () {
const response = await request(app).get('/manifests')
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
it('refreshes manifest data after caching timeout', async function () {
process.env.CACHE_TTL = 1
app = createApp()
fetchConfig['http://ui-server'] = {
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
'/example.js': ''
}
const response = await request(app).get('/manifests')
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
await new Promise(resolve => setTimeout(resolve, 150))
mockserver.close()
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
'/example.js': ''
const response2 = await request(app).get('/manifests')
expect(response2.statusCode).to.equal(200)
expect(response2.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
})
// wait some time
await new Promise(resolve => setTimeout(resolve, 10))
it('refreshes manifest data after caching timeout', async function () {
process.env.CACHE_TTL = 1
app = await mockApp()
const response2 = await request(app).get('/manifests')
expect(response2.statusCode).to.equal(200)
expect(response2.body).to.deep.equal([{ namespace: 'other', path: 'example' }])
})
const response = await request(app).get('/manifests')
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
fetchConfig['http://ui-server'] = {
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
'/example.js': ''
}
it('can load multiple configurations', async function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`, `http://localhost:${port2}`] }))
// wait some time
await new Promise(resolve => setTimeout(resolve, 10))
mockserver.close()
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example1.js': 'other' }),
'/example1.js': ''
const response2 = await request(app).get('/manifests')
expect(response2.statusCode).to.equal(200)
expect(response2.body).to.deep.equal([{ namespace: 'other', path: 'example' }])
})
mockserver2 = await createMockServer({ port: port2 })
mockserver2.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example2.js': 'thing' }),
'/example2.js': ''
})
describe('multiple configurations', function () {
let prevApp
beforeEach(async function () {
mockConfig({ urls: ['http://ui-server/', 'http://ui-server2/'] })
fetchConfig['http://ui-server2'] = {
'/manifest.json': generateSimpleViteManifest({ 'example2.js': 'thing' }),
'/example2.js': ''
}
prevApp = app
app = await mockApp()
})
process.env.CACHE_TTL = 1
const app = createApp()
await request(app)
.get('/manifests')
.then(response => {
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal([
{ namespace: 'other', path: 'example1' },
{ namespace: 'thing', path: 'example2' }
])
})
afterEach(function () {
delete fetchConfig['http://ui-server2']
app = prevApp
})
it('can load multiple configurations', async function () {
await request(app)
.get('/manifests')
.then(response => {
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal([
{ namespace: 'test', path: 'example' },
{ namespace: 'thing', path: 'example2' }
])
})
})
})
})
import express from 'express'
import config from '../src/config.js'
import * as td from 'testdouble'
import { register } from 'prom-client'
import request from 'supertest'
import { Response } from 'node-fetch'
// TODO remove this
export function getRandomPort () {
return 1000 + (Math.random() * 39000) >> 0
}
// TODO remove this
export async function createMockServer ({ port }) {
const app = express()
const server = await new Promise(resolve => {
......@@ -35,14 +40,35 @@ export function generateSimpleViteManifest (mapping) {
}
export function mockConfig ({ urls = [] } = {}) {
const prevLoad = config.load
config.load = async () => {}
config._urls = urls
// return a restore
return {
restore () {
config.load = prevLoad
delete config._urls
td.replaceEsm('fs/promises', {}, {
readFile () {
return `baseUrls:\n${urls.map(u => ` - ${u}`).join('\n')}`
}
}
})
}
export function mockFetch (servers = {}) {
td.replaceEsm('node-fetch', {}, async function ({ origin, pathname }) {
const response = servers[origin]?.[pathname]
if (response === undefined) return new Response('', { status: 404 })
if (response instanceof Function) return response.apply(this, arguments)
if (typeof response === 'object') {
return new Response(JSON.stringify(response), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
})
}
return new Response(response, { status: 200 })
})
}
export async function mockApp () {
register.clear()
const { createApp } = await import('../src/createApp.js')
const app = createApp()
await request(app).get('/ready')
return app
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment