@govuk-pay/run-amock
Version:
A drop-in replacement for Mountebank in our govuk-pay codebases.
165 lines (148 loc) • 6.21 kB
JavaScript
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { getConfiguredHandlersSharedState, getDebuggerSharedState } from './sharedState.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const projectRoot = path.join(__dirname, '..')
const pageTemplateLocation = path.join(projectRoot, 'debugger-assets', 'page-template.html')
const pageTemplateSnapshotNotFoundLocation = path.join(projectRoot, 'debugger-assets', 'page-template-snapshot-not-found.html')
const faviconLocation = path.join(projectRoot, 'debugger-assets', 'favicon.ico')
const boilerplateStylesheetLocation = path.join(projectRoot, 'debugger-assets', 'css-boilerplate.css')
const stylesheetLocation = path.join(projectRoot, 'debugger-assets', 'style.css')
const scriptLocation = path.join(projectRoot, 'debugger-assets', 'client-side-script.js')
const runAmockVersion = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')).version
const debuggerSharedState = getDebuggerSharedState()
const configuredHandlers = getConfiguredHandlersSharedState()
const jsonReplacer = (key, value) => {
if (typeof value === 'bigint') {
return Number(value)
}
return value
}
function setupHandlerForFile (filePath, mimeType) {
return () => {
return {
statusCode: 200,
headers: {
'Content-Type': mimeType
},
body: fs.createReadStream(filePath)
}
}
}
const globalConstants = { runAmockVersion }
function replaceVariables (source, ...variableSources) {
const allSourcesMixedTogether = Object.assign({}, ...variableSources)
return Object.keys(allSourcesMixedTogether).reduce((accumulator, key) => {
const find = `{{${key}}}`
const rawValue = allSourcesMixedTogether[key]
const processedValue = typeof rawValue === 'object' ? `<code><pre>${JSON.stringify(rawValue, jsonReplacer, 2)}</pre></code>` : rawValue
return accumulator.replaceAll(find, processedValue)
}, source)
}
function filterConfiguredHandlers (handlers, filterFn) {
const output = {}
Object.keys(handlers).forEach(methodOrDefault => {
if (methodOrDefault === '__default__') {
if (filterFn(handlers[methodOrDefault])) {
output.__default__ = handlers[methodOrDefault]
}
return
}
Object.keys(handlers[methodOrDefault]).forEach(key => {
const handlersArray = handlers[methodOrDefault][key]
if (!Array.isArray(handlersArray)) {
console.error('Expected array, got', handlersArray)
console.error(`Lookup was [${key}] in`, handlers[methodOrDefault])
}
handlersArray.filter(filterFn).forEach(handlerConfig => {
output[methodOrDefault] = output[methodOrDefault] || {}
output[methodOrDefault][key] = output[methodOrDefault][key] || []
output[methodOrDefault][key].push(handlerConfig)
})
})
})
return output
}
function getSnapshotListHtml () {
const defaultLi = '<li><a href="/__debugger__">Current state (not a snapshot)</a></li>'
return [defaultLi].concat(Object.keys(debuggerSharedState.snapshots)
.map(key => `<li><a href="/__debugger__?snapshot=${encodeURIComponent(key)}">${key}</a></li>`))
.join('\n')
}
export function clearSnapshotsEndpoint () {
debuggerSharedState.snapshots = []
const url = '/__debugger__#snapshots'
return {
statusCode: 302,
headers: {
Location: url
},
body: `Redirecting to ${url}`
}
}
function parseSnapshotJson (json) {
return JSON.parse(json)
}
export function debuggerSnapshotEndpoint (req) {
const snapshotName = [
new Date().toISOString(),
req.body?.name
]
.filter(x => !!x)
.join(': ')
debuggerSharedState.snapshots[snapshotName] = {
latestMockRequest: JSON.stringify(debuggerSharedState.latestMockRequest || 'Not called since the snapshot was made', jsonReplacer),
configuredHandlers: JSON.stringify(configuredHandlers, jsonReplacer),
unmatchedRequests: JSON.stringify(debuggerSharedState.unmatchedRequests, jsonReplacer)
}
return {
statusCode: 200,
body: {
name: snapshotName
}
}
}
export function debuggerGETEndpoints () {
return {
'/__debugger__/favicon.ico': setupHandlerForFile(faviconLocation, 'image/x-icon'),
'/__debugger__/css-boilerplate.css': setupHandlerForFile(boilerplateStylesheetLocation, 'text/css'),
'/__debugger__/style.css': setupHandlerForFile(stylesheetLocation, 'text/css'),
'/__debugger__/client-side-script.js': setupHandlerForFile(scriptLocation, 'application/javascript'),
'/__debugger__': (req) => {
const requestedSnapshotName = req.queryObj?.snapshot
const snapshot = debuggerSharedState.snapshots[requestedSnapshotName]
if (requestedSnapshotName && !snapshot) {
const body = fs.readFileSync(pageTemplateSnapshotNotFoundLocation, 'utf8')
return {
statusCode: 404,
headers: {
'Content-Type': 'text/html; charset=UTF-8'
},
body
}
}
const fileContents = fs.readFileSync(pageTemplateLocation, 'utf8')
const handlers = snapshot ? parseSnapshotJson(snapshot.configuredHandlers) : configuredHandlers
const latestMockRequest = snapshot ? parseSnapshotJson(snapshot.latestMockRequest) : debuggerSharedState.latestMockRequest
const unmatchedRequests = snapshot ? parseSnapshotJson(snapshot.unmatchedRequests) : debuggerSharedState.unmatchedRequests
const body = replaceVariables(fileContents, globalConstants, {
viewingSnapshotHtml: snapshot ? `<h3>You are viewing snapshot: ${requestedSnapshotName}</h3>` : '',
latestMockRequest,
internalState: handlers,
unvisitedMocks: filterConfiguredHandlers(handlers, ({ callCount }) => !(callCount > 0)),
visitedMocks: filterConfiguredHandlers(handlers, ({ callCount }) => callCount > 0),
unmatchedRequests: unmatchedRequests || 'No unmatched requests since last reset',
snapshotListHtml: getSnapshotListHtml()
})
return {
statusCode: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8'
},
body
}
}
}
}