Skip to content

Instantly share code, notes, and snippets.

@pachun
Last active May 17, 2025 23:36
Show Gist options
  • Save pachun/8448fd9b52a1ef973616113b824c3658 to your computer and use it in GitHub Desktop.
Save pachun/8448fd9b52a1ef973616113b824c3658 to your computer and use it in GitHub Desktop.
import { http, HttpResponse, HttpHandler, StrictRequest } from "msw"
import server from "./emmaApiMock"
type Headers = Record<string, string>
type QueryParams = Record<string, string | null>
type HttpMethod = "get" | "post" | "put" | "delete"
class MSWNock {
private method: HttpMethod = "get"
private fullUrl = ""
private expectedBody?: Record<string, any>
private expectedHeaders: Headers = {}
private expectedQuery?: QueryParams
private responseDelayMs: number | null = null
constructor(private baseUrl: string) {}
get(path: string) {
this.method = "get"
this.fullUrl = this.baseUrl + path
return this
}
post(path: string, body?: Record<string, any>) {
this.method = "post"
this.fullUrl = this.baseUrl + path
this.expectedBody = body
return this
}
put(path: string, body?: Record<string, any>) {
this.method = "put"
this.fullUrl = this.baseUrl + path
this.expectedBody = body
return this
}
delete(path: string) {
this.method = "delete"
this.fullUrl = this.baseUrl + path
return this
}
matchHeader(key: string, value: string) {
this.expectedHeaders[key.toLowerCase()] = value
return this
}
query(params: QueryParams) {
this.expectedQuery = params
return this
}
delay(ms: number) {
this.responseDelayMs = ms
return this
}
replyWithError() {
this.setupHandler(null, true)
}
reply(status: number, responseBody: any = {}) {
this.setupHandler(() => responseBody, false, status)
}
private setupHandler(
getResponseBody: (() => any) | null,
isError: boolean,
status = 200,
) {
const handler: HttpHandler = http[this.method](
this.fullUrl,
async ({ request }) => {
const url = new URL(request.url)
this.validateHeaders(request)
await this.validateJsonBody(request)
this.validateQuery(url)
if (isError) {
throw new Error("Simulated network error from mswNock.replyWithError")
}
const body = getResponseBody!()
if (this.responseDelayMs) {
await new Promise(resolve =>
setTimeout(resolve, this.responseDelayMs ?? 0),
)
}
return HttpResponse.json(body, { status })
},
)
server.use(handler)
}
private validateHeaders(request: StrictRequest<any>) {
const reqHeaders = Object.fromEntries(request.headers.entries())
for (const [key, value] of Object.entries(this.expectedHeaders)) {
if (reqHeaders[key.toLowerCase()] !== value) {
this.failJestTest(
`Expected header "${key}" to be "${value}", but got "${reqHeaders[key.toLowerCase()]}"`,
)
}
}
}
private async validateJsonBody(request: StrictRequest<any>) {
const contentType = request.headers.get("content-type") ?? ""
if (this.expectedBody && contentType.includes("application/json")) {
const body = await request.json()
for (const [key, expectedValue] of Object.entries(this.expectedBody)) {
if (body[key] !== expectedValue) {
this.failJestTest(
`Expected request body key "${key}" to be "${expectedValue}", but got "${body[key]}"`,
)
}
}
}
}
private validateQuery(url: URL) {
if (!this.expectedQuery) return
for (const [key, expectedValue] of Object.entries(this.expectedQuery)) {
const actual = url.searchParams.get(key)
if (actual !== expectedValue) {
this.failJestTest(
`Expected query param "${key}"="${expectedValue}", but got "${actual}"`,
)
}
}
}
private failJestTest(message: string) {
it(message, () => {})
}
}
export function mswNock(baseUrl: string) {
return new MSWNock(baseUrl)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment