mockaton
Version:
HTTP Mock Server
136 lines (113 loc) • 3.26 kB
JavaScript
import { DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
import { includesComment, extractComments, parseFilename } from './Filename.js'
/**
* MockBroker is a state for a particular route. It knows the available mock
* files that can be served for the route, the currently selected file, etc.
*/
export class MockBroker {
constructor(file) {
this.file = '' // selected mock filename
this.mocks = [] // filenames
this.status = -1
this.delayed = false
this.proxied = false
this.auto500 = false
this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
this.register(file)
}
#is500 = file => parseFilename(file).status === 500
#sortMocks() {
this.mocks.sort()
const defaults = this.mocks.filter(f => includesComment(f, DEFAULT_MOCK_COMMENT))
this.mocks = Array.from(new Set(defaults).union(new Set(this.mocks)))
}
register(file) {
if (this.auto500 && this.#is500(file))
this.selectFile(file)
this.mocks.push(file)
this.#sortMocks()
}
unregister(file) {
this.mocks = this.mocks.filter(f => f !== file)
const isEmpty = !this.mocks.length
if (!isEmpty && this.file === file)
this.selectDefaultFile()
return isEmpty
}
hasMock = file => this.mocks.includes(file)
selectFile(filename) {
this.file = filename
this.proxied = false
this.auto500 = false
this.status = parseFilename(filename).status
}
selectDefaultFile() {
this.selectFile(this.mocks[0])
}
toggle500() {
const shouldUnset = this.auto500 || this.status === 500
if (shouldUnset)
this.selectDefaultFile()
else {
const f500 = this.mocks.find(this.#is500)
if (f500)
this.selectFile(f500)
else {
this.auto500 = true
this.status = 500
}
}
this.proxied = false
}
setDelayed(delayed) {
this.delayed = delayed
}
setProxied(proxied) {
this.auto500 = false
this.proxied = proxied
}
setByMatchingComment(comment) {
for (const file of this.mocks)
if (includesComment(file, comment)) {
this.selectFile(file)
break
}
}
extractComments() {
const comments = []
for (const file of this.mocks)
comments.push(...extractComments(file))
return comments
}
}
class UrlMatcher {
#urlRegex
constructor(file) {
this.#urlRegex = this.#buildUrlRegex(file)
}
#buildUrlRegex(file) {
let { urlMask } = parseFilename(file)
urlMask = this.#removeQueryStringAndFragment(urlMask)
urlMask = this.#disregardVariables(urlMask)
return new RegExp('^' + urlMask + '/*$')
}
#removeQueryStringAndFragment(str) {
return str.replace(/[?#].*/, '')
}
#disregardVariables(str) { // Stars out all parts that are in square brackets
return str.replace(/\[.*?]/g, '[^/]+')
}
// Appending a '/' so URLs ending with variables don't match
// URLs that have a path after that variable. For example,
// without it, the following regex would match both of these URLs:
// api/foo/[route_id] => api/foo/.* (wrong match because it’s too greedy)
// api/foo/[route_id]/suffix => api/foo/.*/suffix
// By the same token, the regex handles many trailing
// slashes. For instance, for routing api/foo/[id]?qs…
urlMaskMatches = (url) => {
let u = decodeURIComponent(url)
u = this.#removeQueryStringAndFragment(u)
u += '/'
return this.#urlRegex.test(u)
}
}