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 @@ ...@@ -9,7 +9,7 @@
"start": "node src/index.js", "start": "node src/index.js",
"dev": "nodemon index.js", "dev": "nodemon index.js",
"prepare": "husky install", "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-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-app": "npx --package=@open-xchange/release-it -- release-it",
"release": "yarn release-chart && yarn release-app" "release": "yarn release-chart && yarn release-app"
......
import request from 'supertest' import request from 'supertest'
import { createApp } from '../src/createApp.js' import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { expect } from 'chai' import { expect } from 'chai'
import { Response } from 'node-fetch'
const port = getRandomPort() import * as td from 'testdouble'
describe('JS files with dependencies contain events', function () { describe('JS files with dependencies contain events', function () {
let app let app
let mockserver
let restoreConfig
before(function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] }))
app = createApp()
})
after(function () { before(async function () {
restoreConfig() mockConfig({ urls: ['http://ui-server/'] })
}) mockFetch({
'http://ui-server': {
beforeEach(async function () { '/manifest.json': generateSimpleViteManifest({
mockserver = await createMockServer({ port }) 'example.js': {},
mockserver.respondWith({ 'main.css': {},
'/manifest.json': generateSimpleViteManifest({ 'index.html': {
'example.js': {}, file: 'index.html.js',
'main.css': {}, isEntry: true,
'index.html': { imports: ['example.js'],
file: 'index.html.js', css: ['main.css']
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' } }),
'/example.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is example'), '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } })
'/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>')
}) })
await request(app).get('/ready') app = await mockApp()
}) })
afterEach(function () { after(function () {
mockserver.close() td.reset()
process.env.CACHE_TTL = 30000
}) })
it('javascript file contains dispatcher for dependencies', async function () { it('javascript file contains dispatcher for dependencies', async function () {
......
import request from 'supertest' import request from 'supertest'
import { createApp } from '../src/createApp.js' import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import fs from 'fs' import fs from 'fs'
import { expect } from 'chai' import { expect } from 'chai'
import * as td from 'testdouble'
import { Response } from 'node-fetch'
const image = fs.readFileSync('./spec/media/image.png') const image = fs.readFileSync('./spec/media/image.png')
const imageStat = fs.statSync('./spec/media/image.png') const imageStat = fs.statSync('./spec/media/image.png')
const port = getRandomPort()
describe('File caching service', function () { describe('File caching service', function () {
let app let app
let mockserver
let restoreConfig
before(function () { before(async function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] })) mockConfig({ urls: ['http://ui-server/'] })
app = createApp() mockFetch({
}) 'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({
after(function () { 'example.js': { imports: ['test.txt'] },
restoreConfig() 'test.txt': { },
}) 'main.css': {},
'index.html': {
beforeEach(async function () { file: 'index.html.js',
mockserver = await createMockServer({ port }) isEntry: true,
mockserver.respondWith({ imports: ['example.js'],
'/manifest.json': generateSimpleViteManifest({ css: ['main.css']
'example.js': { imports: ['test.txt'] }, },
'test.txt': { }, 'image.png': {}
'main.css': {}, }),
'index.html': { '/example.js': () => new Response('this is example', { headers: { 'content-type': 'application/javascript' } }),
file: 'index.html.js', '/test.txt': () => new Response('this is test', { headers: { 'content-type': 'text/plain' } }),
isEntry: true, '/index.html.js': () => new Response('this is index.html.js', { headers: { 'content-type': 'application/javascript' } }),
imports: ['example.js'], '/index.html': () => new Response('<html><head></head><body>it\'s me</body></html>', { headers: { 'content-type': 'text/html' } }),
css: ['main.css'] '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } }),
}, '/favicon.ico': 'not really a favicon, though',
'image.png': {} '/image.png': () => {
}), return new Response(image, {
'/example.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is example'), headers: {
'/test.txt': (req, res) => res.setHeader('content-type', 'text/plain').status(200).send('this is test'), 'Content-Type': 'image/png',
'/index.html.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is index.html.js'), 'Content-Length': imageStat.size
'/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)
} }
}) })
await request(app).get('/ready') app = await mockApp()
}) })
afterEach(function () { after(function () {
mockserver.close() td.reset()
process.env.CACHE_TTL = 30000
}) })
it('serves files defined in manifest.json file', async function () { it('serves files defined in manifest.json file', async function () {
const response = await request(app).get('/example.js') const response = await request(app).get('/example.js')
expect(response.statusCode).to.equal(200) 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.text).to.equal('this is example')
// expect(response.headers['content-security-policy']).to.contain('sha256-NzZhMTE2Njc2YTgyNTZmZTdlZGVjZDU3YTNmYzRjNmM1OWZkMTI2NjRkYzZmMWM3YTkwMGU3ZTdhNDlhZmVlMwo=') // expect(response.headers['content-security-policy']).to.contain('sha256-NzZhMTE2Njc2YTgyNTZmZTdlZGVjZDU3YTNmYzRjNmM1OWZkMTI2NjRkYzZmMWM3YTkwMGU3ZTdhNDlhZmVlMwo=')
const response2 = await request(app).get('/test.txt') const response2 = await request(app).get('/test.txt')
expect(response2.statusCode).to.equal(200) 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') expect(response2.text).to.equal('this is test')
}) })
it('serves css files', async function () { it('serves css files', async function () {
const response = await request(app).get('/main.css') const response = await request(app).get('/main.css')
expect(response.statusCode).to.equal(200) 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=') // expect(response.headers['content-security-policy']).to.contain('sha256-YjRiYWRlYTVhYmM5ZTZkNjE2ZGM4YjcwZWRlNzUxMmU0YjgxY2UxMWExOTI2ZjM1NzM1M2Y2MWJjNmUwMmZjMwo=')
}) })
it('serves / as index.html', async function () { it('serves / as index.html', async function () {
const response = await request(app).get('/') const response = await request(app).get('/')
expect(response.statusCode).to.equal(200) 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>') expect(response.text).to.equal('<html><head></head><body>it\'s me</body></html>')
}) })
...@@ -96,7 +86,7 @@ describe('File caching service', function () { ...@@ -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 () { it('directly fetches files not referenced in manifest.json files from the upstream servers', async function () {
const response = await request(app).get('/favicon.ico') const response = await request(app).get('/favicon.ico')
expect(response.statusCode).to.equal(200) 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 () { it('returns 404 if file can not be resolved', async function () {
......
import request from 'supertest' import request from 'supertest'
import { createApp } from '../src/createApp.js' import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { expect } from 'chai' import { expect } from 'chai'
import { Response } from 'node-fetch'
const port = getRandomPort() import * as td from 'testdouble'
describe('Responses contain custom headers', function () { describe('Responses contain custom headers', function () {
let app let app
let mockserver
let restoreConfig
before(function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] }))
app = createApp()
})
after(function () { before(async function () {
restoreConfig() mockConfig({ urls: ['http://ui-server/'] })
}) mockFetch({
'http://ui-server': {
beforeEach(async function () { '/manifest.json': generateSimpleViteManifest({
mockserver = await createMockServer({ port }) 'example.js': {},
mockserver.respondWith({ 'main.css': {},
'/manifest.json': generateSimpleViteManifest({ 'index.html': {
'example.js': {}, file: 'index.html.js',
'main.css': {}, isEntry: true,
'index.html': { imports: ['example.js'],
file: 'index.html.js', css: ['main.css']
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' } }),
'/example.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is example'), '/main.css': () => new Response('.foo { color: #000; }', { headers: { 'content-type': 'text/css' } })
'/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>')
}) })
await request(app).get('/ready') app = await mockApp()
}) })
afterEach(function () { after(function () {
mockserver.close() td.reset()
process.env.CACHE_TTL = 30000
}) })
it('index.html has version', async function () { it('index.html has version', async function () {
......
...@@ -2,7 +2,7 @@ import { viteToOxManifest } from '../src/manifests.js' ...@@ -2,7 +2,7 @@ import { viteToOxManifest } from '../src/manifests.js'
import { expect } from 'chai' import { expect } from 'chai'
describe('Vite manifest parsing', function () { describe('Vite manifest parsing', function () {
it('should', function () { it('should work', function () {
const manifests = viteToOxManifest({ const manifests = viteToOxManifest({
'../io.ox/guidedtours/i18n.de_DE.js': { '../io.ox/guidedtours/i18n.de_DE.js': {
file: 'io.ox/guidedtours/i18n.de_DE.js', file: 'io.ox/guidedtours/i18n.de_DE.js',
......
import request from 'supertest' import request from 'supertest'
import { createApp } from '../src/createApp.js' import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { expect } from 'chai' import { expect } from 'chai'
import { Response } from 'node-fetch'
const port = getRandomPort() import * as td from 'testdouble'
describe('Responses contain custom headers', function () { describe('Responses contain custom headers', function () {
let fetchConfig
let app let app
let mockserver
let restoreConfig
before(function () { before(async function () {
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] })) mockConfig({ urls: ['http://ui-server/'] })
app = createApp() 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 () { after(function () {
restoreConfig() td.reset()
})
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')
}) })
afterEach(function () { afterEach(function () {
mockserver.close()
delete process.env.APP_VERSION delete process.env.APP_VERSION
delete process.env.BUILD_TIMESTAMP delete process.env.BUILD_TIMESTAMP
delete process.env.CI_COMMIT_SHA delete process.env.CI_COMMIT_SHA
...@@ -63,14 +57,25 @@ describe('Responses contain custom headers', function () { ...@@ -63,14 +57,25 @@ describe('Responses contain custom headers', function () {
}) })
}) })
it('does not have metadata from ui service when unavailable', async function () { describe('without service avaible', function () {
await mockserver.close() let prevConfig
const response = await request(app).get('/meta') beforeEach(function () {
expect(response.statusCode).to.equal(200) prevConfig = fetchConfig['http://ui-server']
expect(response.body).to.not.deep.contain({ delete fetchConfig['http://ui-server']
name: 'sample-service', })
version: '1.0'
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 request from 'supertest'
import { createApp } from '../src/createApp.js' import { generateSimpleViteManifest, mockApp, mockConfig, mockFetch } from './util.js'
import { createMockServer, generateSimpleViteManifest, getRandomPort, mockConfig } from './util.js'
import { expect } from 'chai' import { expect } from 'chai'
import * as td from 'testdouble'
const port = getRandomPort()
const port2 = getRandomPort()
describe('UI Middleware', function () { describe('UI Middleware', function () {
let app let app
let mockserver, mockserver2 let fetchConfig
let restoreConfig
before(async function () {
before(function () { mockConfig({ urls: ['http://ui-server/'] })
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`] })) mockFetch(fetchConfig = {
app = createApp() 'http://ui-server': {
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'test' }),
'/example.js': ''
}
})
app = await mockApp()
}) })
after(function () { after(function () {
restoreConfig() td.reset()
})
beforeEach(async function () {
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'test' }),
'/example.js': ''
})
}) })
afterEach(async function () { afterEach(async function () {
await mockserver?.close()
await mockserver2?.close()
process.env.CACHE_TTL = 30000 process.env.CACHE_TTL = 30000
}) })
...@@ -48,74 +40,82 @@ describe('UI Middleware', function () { ...@@ -48,74 +40,82 @@ describe('UI Middleware', function () {
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }]) expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
}) })
it('caches manifest data', async function () { describe('when configuration changes', function () {
const response = await request(app).get('/manifests') let prevConfig
expect(response.statusCode).to.equal(200) beforeEach(async function () {
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }]) prevConfig = fetchConfig['http://ui-server']
mockserver.close()
mockserver = await createMockServer({ port })
mockserver.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
'/example.js': ''
}) })
await new Promise(resolve => setTimeout(resolve, 150)) afterEach(function () {
fetchConfig['http://ui-server'] = prevConfig
})
const response2 = await request(app).get('/manifests') it('caches manifest data', async function () {
expect(response2.statusCode).to.equal(200) const response = await request(app).get('/manifests')
expect(response2.body).to.deep.equal([{ namespace: 'test', path: 'example' }]) 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 () { fetchConfig['http://ui-server'] = {
process.env.CACHE_TTL = 1 '/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
app = createApp() '/example.js': ''
}
const response = await request(app).get('/manifests') await new Promise(resolve => setTimeout(resolve, 150))
expect(response.statusCode).to.equal(200)
expect(response.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
mockserver.close() const response2 = await request(app).get('/manifests')
mockserver = await createMockServer({ port }) expect(response2.statusCode).to.equal(200)
mockserver.respondWith({ expect(response2.body).to.deep.equal([{ namespace: 'test', path: 'example' }])
'/manifest.json': generateSimpleViteManifest({ 'example.js': 'other' }),
'/example.js': ''
}) })
// wait some time it('refreshes manifest data after caching timeout', async function () {
await new Promise(resolve => setTimeout(resolve, 10)) process.env.CACHE_TTL = 1
app = await mockApp()
const response2 = await request(app).get('/manifests') const response = await request(app).get('/manifests')
expect(response2.statusCode).to.equal(200) expect(response.statusCode).to.equal(200)
expect(response2.body).to.deep.equal([{ namespace: 'other', path: 'example' }]) 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 () { // wait some time
;({ restore: restoreConfig } = mockConfig({ urls: [`http://localhost:${port}/`, `http://localhost:${port2}`] })) await new Promise(resolve => setTimeout(resolve, 10))
mockserver.close() const response2 = await request(app).get('/manifests')
mockserver = await createMockServer({ port }) expect(response2.statusCode).to.equal(200)
mockserver.respondWith({ expect(response2.body).to.deep.equal([{ namespace: 'other', path: 'example' }])
'/manifest.json': generateSimpleViteManifest({ 'example1.js': 'other' }),
'/example1.js': ''
}) })
mockserver2 = await createMockServer({ port: port2 }) })
mockserver2.respondWith({
'/manifest.json': generateSimpleViteManifest({ 'example2.js': 'thing' }), describe('multiple configurations', function () {
'/example2.js': '' 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 afterEach(function () {
const app = createApp() delete fetchConfig['http://ui-server2']
app = prevApp
await request(app) })
.get('/manifests')
.then(response => { it('can load multiple configurations', async function () {
expect(response.statusCode).to.equal(200) await request(app)
expect(response.body).to.deep.equal([ .get('/manifests')
{ namespace: 'other', path: 'example1' }, .then(response => {
{ namespace: 'thing', path: 'example2' } 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 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 () { export function getRandomPort () {
return 1000 + (Math.random() * 39000) >> 0 return 1000 + (Math.random() * 39000) >> 0
} }
// TODO remove this
export async function createMockServer ({ port }) { export async function createMockServer ({ port }) {
const app = express() const app = express()
const server = await new Promise(resolve => { const server = await new Promise(resolve => {
...@@ -35,14 +40,35 @@ export function generateSimpleViteManifest (mapping) { ...@@ -35,14 +40,35 @@ export function generateSimpleViteManifest (mapping) {
} }
export function mockConfig ({ urls = [] } = {}) { export function mockConfig ({ urls = [] } = {}) {
const prevLoad = config.load td.replaceEsm('fs/promises', {}, {
config.load = async () => {} readFile () {
config._urls = urls return `baseUrls:\n${urls.map(u => ` - ${u}`).join('\n')}`
// return a restore
return {
restore () {
config.load = prevLoad
delete config._urls
} }
} })
}
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