Plan Checker, Wochenbericht-Fokus-Modus und Wandtafel-Dashboard-Feinschliff
Plan Checker (neues Studenten-Werkzeug, Seitenleiste + Admin → Plan-Checker- Merkmale): lädt einen Bauplan als PDF hoch und lässt ChatGPT oder Claude ihn gegen eine gepflegte Merkmalsliste prüfen. PDF-Rendering (pdf.js + @napi-rs/canvas) berücksichtigt jetzt Ebenen (Optional Content Groups) und WASM-Decoder korrekt, sendet eine Übersicht plus mehrere hochaufgelöste Kachel-Ausschnitte statt eines einzelnen, zu groben Bilds, und beide Anbieter bekommen dasselbe Bildmaterial. Jedes Prüfergebnis kann auf eines der gesendeten Bilder als Beleg verweisen (bildIndex) — als Screenshot im Ergebnis anklickbar, öffnet vergrößert in einem Modal statt eines von Chrome blockierten target=_blank-Links auf eine data:-URL. KI-Modellname ist jetzt pro Konto überschreibbar (Einstellungen → Mein Konto) für Drittanbieter mit abweichendem Modell-Katalog. Wochenbericht: dichte Kartenansicht bekommt einen Fokus-Modus — nur der Kommentar liegt offen, Leistungsphasen und Beteiligte stecken in aufklappbaren Abschnitten statt dauerhaft nebeneinander zu stehen. Projektbereich: Terminplan-Auslastung rechnet jetzt exakt wie der Auslastungs-Tab/die Wandtafel (statt einer unabhängigen, tageweisen Schätzung); die LPH-Fortschrittsliste im Dashboard zeigt nur noch die beauftragten Leistungsphasen. Wandtafel: Dashboard-Kopf mit Kennzahlen-Reihe und Panel-Reihe neu strukturiert, in den Blautönen des Firmenlogos; dieselbe Palette wurde anschließend auf den gesamten App-weiten Dark Mode übernommen. Dateibereinigung: nicht mehr benötigte Test-Uploads/Backups entfernt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db63a29365
commit
6ba93b2a92
308 files changed
+64441
-743
No files matched your search
+720
@@ -0,0 +1,720 @@
|
||||
'use strict'
|
||||
|
||||
const { MockNotMatchedError } = require('./mock-errors')
|
||||
const {
|
||||
kDispatches,
|
||||
kMockAgent,
|
||||
kOriginalDispatch,
|
||||
kOrigin,
|
||||
kGetNetConnect,
|
||||
kTotalDispatchCount
|
||||
} = require('./mock-symbols')
|
||||
const { serializePathWithQuery, parseHeaders } = require('../core/util')
|
||||
const { STATUS_CODES } = require('node:http')
|
||||
const {
|
||||
types: {
|
||||
isPromise
|
||||
}
|
||||
} = require('node:util')
|
||||
const { InvalidArgumentError } = require('../core/errors')
|
||||
const requestAborted = Symbol('request aborted')
|
||||
|
||||
function matchValue (match, value) {
|
||||
if (typeof match === 'string') {
|
||||
return match === value
|
||||
}
|
||||
if (match instanceof RegExp) {
|
||||
return match.test(value)
|
||||
}
|
||||
if (typeof match === 'function') {
|
||||
return match(value) === true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function lowerCaseEntries (headers) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(headers).map(([headerName, headerValue]) => {
|
||||
return [headerName.toLocaleLowerCase(), headerValue]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../../index').Headers|string[]|Record<string, string>} headers
|
||||
* @param {string} key
|
||||
*/
|
||||
function getHeaderByName (headers, key) {
|
||||
if (Array.isArray(headers)) {
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) {
|
||||
return headers[i + 1]
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
} else if (typeof headers.get === 'function') {
|
||||
return headers.get(key)
|
||||
} else {
|
||||
return lowerCaseEntries(headers)[key.toLocaleLowerCase()]
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string[]} headers */
|
||||
function buildHeadersFromArray (headers) { // fetch HeadersList
|
||||
const clone = headers.slice()
|
||||
const entries = []
|
||||
for (let index = 0; index < clone.length; index += 2) {
|
||||
entries.push([clone[index], clone[index + 1]])
|
||||
}
|
||||
return Object.fromEntries(entries)
|
||||
}
|
||||
|
||||
function matchHeaders (mockDispatch, headers) {
|
||||
if (typeof mockDispatch.headers === 'function') {
|
||||
if (Array.isArray(headers)) { // fetch HeadersList
|
||||
headers = buildHeadersFromArray(headers)
|
||||
}
|
||||
return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {})
|
||||
}
|
||||
if (typeof mockDispatch.headers === 'undefined') {
|
||||
return true
|
||||
}
|
||||
if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) {
|
||||
const headerValue = getHeaderByName(headers, matchHeaderName)
|
||||
|
||||
if (!matchValue(matchHeaderValue, headerValue)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function normalizeSearchParams (query) {
|
||||
if (typeof query !== 'string') {
|
||||
return query
|
||||
}
|
||||
|
||||
const originalQp = new URLSearchParams(query)
|
||||
const normalizedQp = new URLSearchParams()
|
||||
|
||||
for (let [key, value] of originalQp.entries()) {
|
||||
key = key.replace('[]', '')
|
||||
|
||||
const valueRepresentsString = /^(['"]).*\1$/.test(value)
|
||||
if (valueRepresentsString) {
|
||||
normalizedQp.append(key, value)
|
||||
continue
|
||||
}
|
||||
|
||||
if (value.includes(',')) {
|
||||
const values = value.split(',')
|
||||
for (const v of values) {
|
||||
normalizedQp.append(key, v)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
normalizedQp.append(key, value)
|
||||
}
|
||||
|
||||
return normalizedQp
|
||||
}
|
||||
|
||||
function safeUrl (path) {
|
||||
if (typeof path !== 'string') {
|
||||
return path
|
||||
}
|
||||
const pathSegments = path.split('?', 3)
|
||||
if (pathSegments.length !== 2) {
|
||||
return path
|
||||
}
|
||||
|
||||
const qp = new URLSearchParams(pathSegments.pop())
|
||||
qp.sort()
|
||||
return [...pathSegments, qp.toString()].join('?')
|
||||
}
|
||||
|
||||
function matchKey (mockDispatch, { path, method, body, headers }) {
|
||||
const pathMatch = matchValue(mockDispatch.path, path)
|
||||
const methodMatch = matchValue(mockDispatch.method, method)
|
||||
const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true
|
||||
const headersMatch = matchHeaders(mockDispatch, headers)
|
||||
return pathMatch && methodMatch && bodyMatch && headersMatch
|
||||
}
|
||||
|
||||
function getResponseData (data) {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
return data
|
||||
} else if (data instanceof Uint8Array) {
|
||||
return data
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
return data
|
||||
} else if (ArrayBuffer.isView(data)) {
|
||||
// A DataView, or any non-Uint8Array typed array, is a byte container
|
||||
// rather than a plain object. Buffer.from() cannot read one directly, so
|
||||
// expose the bytes it covers instead of letting it reach JSON.stringify.
|
||||
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
||||
} else if (typeof data === 'object') {
|
||||
return JSON.stringify(data)
|
||||
} else if (data) {
|
||||
return data.toString()
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getMockDispatch (mockDispatches, key) {
|
||||
const basePath = key.query ? serializePathWithQuery(key.path, key.query) : key.path
|
||||
const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath
|
||||
|
||||
const resolvedPathWithoutTrailingSlash = removeTrailingSlash(resolvedPath)
|
||||
|
||||
// Match path
|
||||
let matchedMockDispatches = mockDispatches
|
||||
.filter(({ consumed }) => !consumed)
|
||||
.filter(({ path, ignoreTrailingSlash }) => {
|
||||
return ignoreTrailingSlash
|
||||
? matchValue(removeTrailingSlash(safeUrl(path)), resolvedPathWithoutTrailingSlash)
|
||||
: matchValue(safeUrl(path), resolvedPath)
|
||||
})
|
||||
if (matchedMockDispatches.length === 0) {
|
||||
throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
|
||||
}
|
||||
|
||||
// Match method
|
||||
matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method))
|
||||
if (matchedMockDispatches.length === 0) {
|
||||
throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}' on path '${resolvedPath}'`)
|
||||
}
|
||||
|
||||
// Match body
|
||||
matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true)
|
||||
if (matchedMockDispatches.length === 0) {
|
||||
throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}' on path '${resolvedPath}'`)
|
||||
}
|
||||
|
||||
// Match headers
|
||||
matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers))
|
||||
if (matchedMockDispatches.length === 0) {
|
||||
const headers = typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers
|
||||
throw new MockNotMatchedError(`Mock dispatch not matched for headers '${headers}' on path '${resolvedPath}'`)
|
||||
}
|
||||
|
||||
return matchedMockDispatches[0]
|
||||
}
|
||||
|
||||
function addMockDispatch (mockDispatches, key, data, opts) {
|
||||
const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false, ...opts }
|
||||
const replyData = typeof data === 'function' ? { callback: data } : { ...data }
|
||||
const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }
|
||||
mockDispatches.push(newMockDispatch)
|
||||
// Track total number of intercepts ever registered for better error messages
|
||||
mockDispatches[kTotalDispatchCount] = (mockDispatches[kTotalDispatchCount] || 0) + 1
|
||||
return newMockDispatch
|
||||
}
|
||||
|
||||
function deleteMockDispatch (mockDispatches, key) {
|
||||
const index = mockDispatches.findIndex(dispatch => {
|
||||
if (!dispatch.consumed) {
|
||||
return false
|
||||
}
|
||||
return matchKey(dispatch, key)
|
||||
})
|
||||
if (index !== -1) {
|
||||
mockDispatches.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|RegExp|Function} path Path, or path matcher, to remove trailing slash from
|
||||
*/
|
||||
function removeTrailingSlash (path) {
|
||||
// Registered path matchers may be a RegExp or a function, which have no
|
||||
// trailing slash to strip; hand those back for matchValue to apply.
|
||||
if (typeof path !== 'string') {
|
||||
return path
|
||||
}
|
||||
|
||||
while (path.endsWith('/')) {
|
||||
path = path.slice(0, -1)
|
||||
}
|
||||
|
||||
if (path.length === 0) {
|
||||
path = '/'
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
function buildKey (opts) {
|
||||
const { path, method, body, headers, query } = opts
|
||||
|
||||
return {
|
||||
path,
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
function generateKeyValues (data) {
|
||||
const keys = Object.keys(data)
|
||||
const result = []
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i]
|
||||
const value = data[key]
|
||||
const name = Buffer.from(`${key}`)
|
||||
if (Array.isArray(value)) {
|
||||
for (let j = 0; j < value.length; ++j) {
|
||||
result.push(name, Buffer.from(`${value[j]}`))
|
||||
}
|
||||
} else {
|
||||
result.push(name, Buffer.from(`${value}`))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
||||
* @param {number} statusCode
|
||||
*/
|
||||
function getStatusText (statusCode) {
|
||||
return STATUS_CODES[statusCode] || 'unknown'
|
||||
}
|
||||
|
||||
async function getResponse (body) {
|
||||
const buffers = []
|
||||
for await (const data of body) {
|
||||
buffers.push(data)
|
||||
}
|
||||
return Buffer.concat(buffers).toString('utf8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock dispatch function used to simulate undici dispatches
|
||||
*/
|
||||
function mockDispatch (opts, handler) {
|
||||
// Get mock dispatch from built key
|
||||
const key = buildKey(opts)
|
||||
const mockDispatch = getMockDispatch(this[kDispatches], key)
|
||||
const mockDispatches = this[kDispatches]
|
||||
|
||||
mockDispatch.timesInvoked++
|
||||
|
||||
const { timesInvoked, times } = mockDispatch
|
||||
|
||||
// If it's used up and not persistent, mark as consumed
|
||||
mockDispatch.consumed = !mockDispatch.persist && timesInvoked >= times
|
||||
mockDispatch.pending = timesInvoked < times
|
||||
|
||||
const hasBodyHooks = typeof handler.onBodySent === 'function' ||
|
||||
typeof handler.onRequestSent === 'function'
|
||||
|
||||
// Here's where we resolve a callback if a callback is present for the dispatch data.
|
||||
if (mockDispatch.data.callback && (!hasBodyHooks || opts.body == null)) {
|
||||
const { callback, ...responseDefaults } = mockDispatch.data
|
||||
const callbackResult = callback(opts)
|
||||
|
||||
// An asynchronous reply options callback resolves to the reply data, so
|
||||
// the dispatch can only continue once the returned promise settles.
|
||||
// A rejection cannot be thrown synchronously from the dispatch at that
|
||||
// point, so it is surfaced as a response error instead.
|
||||
if (isPromise(callbackResult)) {
|
||||
callbackResult.then(
|
||||
(resolvedData) => {
|
||||
if (resolvedData == null || typeof resolvedData !== 'object') {
|
||||
handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object'))
|
||||
return
|
||||
}
|
||||
dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler, { ...responseDefaults, ...resolvedData })
|
||||
},
|
||||
(error) => {
|
||||
handler.onResponseError(null, error)
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
if (callbackResult == null || typeof callbackResult !== 'object') {
|
||||
throw new InvalidArgumentError('reply options callback must return an object')
|
||||
}
|
||||
|
||||
return dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler, { ...responseDefaults, ...callbackResult })
|
||||
}
|
||||
|
||||
return dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replies to a request once the mock dispatch data is fully resolved
|
||||
*/
|
||||
function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler, resolvedResponse) {
|
||||
// Parse mockDispatch data. When a reply callback has already been resolved
|
||||
// in mockDispatch() (i.e. no body lifecycle hooks are involved), the resolved
|
||||
// response is passed in here, leaving mockDispatch.data untouched so the
|
||||
// callback can be re-invoked for persistent / times() replies.
|
||||
const { data: responseData, delay } = mockDispatch
|
||||
const response = resolvedResponse ?? responseData
|
||||
|
||||
// If specified, trigger dispatch error
|
||||
if (response.error !== null) {
|
||||
deleteMockDispatch(mockDispatches, key)
|
||||
handler.onResponseError(null, response.error)
|
||||
return true
|
||||
}
|
||||
|
||||
// Track whether the request has been aborted
|
||||
let aborted = false
|
||||
let timer = null
|
||||
|
||||
// Create the controller early so abort can use it
|
||||
const controller = {
|
||||
paused: false,
|
||||
rawHeaders: null,
|
||||
rawTrailers: null,
|
||||
pause () {
|
||||
this.paused = true
|
||||
},
|
||||
resume () {
|
||||
this.paused = false
|
||||
},
|
||||
abort: (reason) => {
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
aborted = true
|
||||
|
||||
// Clear the pending delayed response if any
|
||||
if (timer !== null) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
handler.onResponseError?.(controller, reason)
|
||||
}
|
||||
}
|
||||
|
||||
let replyOpts = opts
|
||||
const dispatches = mockDispatches
|
||||
|
||||
// Call onRequestStart to allow the handler to receive the controller
|
||||
handler.onRequestStart?.(controller, null)
|
||||
|
||||
if (aborted) {
|
||||
return true
|
||||
}
|
||||
|
||||
const requestBody = dispatchRequestBody(opts.body, handler, controller, () => aborted)
|
||||
|
||||
if (isPromise(requestBody)) {
|
||||
requestBody.then((body) => {
|
||||
if (body === requestAborted) {
|
||||
return
|
||||
}
|
||||
|
||||
if (body !== opts.body) {
|
||||
replyOpts = { ...opts, body }
|
||||
}
|
||||
|
||||
sendReply()
|
||||
}, (error) => controller.abort(error))
|
||||
return true
|
||||
}
|
||||
|
||||
if (requestBody === requestAborted) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (requestBody !== opts.body) {
|
||||
replyOpts = { ...opts, body: requestBody }
|
||||
}
|
||||
|
||||
sendReply()
|
||||
|
||||
function sendReply () {
|
||||
if (response.callback) {
|
||||
const { callback, ...responseDefaults } = response
|
||||
let callbackResult
|
||||
try {
|
||||
callbackResult = callback(replyOpts)
|
||||
} catch (err) {
|
||||
deleteMockDispatch(mockDispatches, key)
|
||||
handler.onResponseError(null, err)
|
||||
return
|
||||
}
|
||||
|
||||
if (isPromise(callbackResult)) {
|
||||
callbackResult.then(
|
||||
(resolvedData) => {
|
||||
if (resolvedData == null || typeof resolvedData !== 'object') {
|
||||
handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object'))
|
||||
return
|
||||
}
|
||||
handleReply(dispatches, { ...responseDefaults, ...resolvedData })
|
||||
},
|
||||
(err) => {
|
||||
handler.onResponseError(null, err)
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (callbackResult == null || typeof callbackResult !== 'object') {
|
||||
throw new InvalidArgumentError('reply options callback must return an object')
|
||||
}
|
||||
|
||||
handleReply(dispatches, { ...responseDefaults, ...callbackResult })
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the request with a delay if necessary
|
||||
if (typeof delay === 'number' && delay > 0) {
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
handleReply(dispatches)
|
||||
}, delay)
|
||||
} else {
|
||||
handleReply(dispatches)
|
||||
}
|
||||
}
|
||||
|
||||
function handleReply (mockDispatches, _response = response) {
|
||||
// Don't send response if the request was aborted
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
const { statusCode, data, headers, trailers } = _response
|
||||
|
||||
// fetch's HeadersList is a 1D string array
|
||||
const optsHeaders = Array.isArray(opts.headers)
|
||||
? buildHeadersFromArray(opts.headers)
|
||||
: opts.headers
|
||||
const body = typeof data === 'function'
|
||||
? data({ ...replyOpts, headers: optsHeaders })
|
||||
: data
|
||||
|
||||
// util.types.isPromise is likely needed for jest.
|
||||
if (isPromise(body)) {
|
||||
// If handleReply is asynchronous, throwing an error
|
||||
// in the callback will reject the promise, rather than
|
||||
// synchronously throw the error, which breaks some tests.
|
||||
// Rather, we wait for the callback to resolve if it is a
|
||||
// promise, and then re-run handleReply with the new body.
|
||||
return body.then((newData) => handleReply(mockDispatches, { ..._response, data: newData }))
|
||||
}
|
||||
|
||||
// Check again if aborted after async body resolution
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
const responseData = getResponseData(body)
|
||||
const responseHeaders = generateKeyValues(headers ?? {})
|
||||
const responseTrailers = generateKeyValues(trailers ?? {})
|
||||
|
||||
// Update the controller with response data
|
||||
controller.rawHeaders = responseHeaders
|
||||
controller.rawTrailers = responseTrailers
|
||||
|
||||
handler.onResponseStart?.(controller, statusCode, parseHeaders(responseHeaders), getStatusText(statusCode))
|
||||
handler.onResponseData?.(controller, Buffer.from(responseData))
|
||||
handler.onResponseEnd?.(controller, parseHeaders(responseTrailers))
|
||||
deleteMockDispatch(mockDispatches, key)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function dispatchRequestBody (body, handler, controller, isAborted) {
|
||||
if (typeof handler.onBodySent !== 'function' && typeof handler.onRequestSent !== 'function') {
|
||||
return body
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted
|
||||
}
|
||||
|
||||
if (body && typeof body[Symbol.asyncIterator] === 'function') {
|
||||
return dispatchAsyncIterableBody(body, handler, controller, isAborted)
|
||||
}
|
||||
|
||||
if (isIterableBody(body)) {
|
||||
const chunks = []
|
||||
|
||||
for (const chunk of body) {
|
||||
if (isAborted()) {
|
||||
return requestAborted
|
||||
}
|
||||
chunks.push(chunk)
|
||||
if (!callOnBodySent(handler, controller, chunk) || isAborted()) {
|
||||
return requestAborted
|
||||
}
|
||||
}
|
||||
|
||||
return callOnRequestSent(handler, controller, isAborted) ? chunks : requestAborted
|
||||
}
|
||||
|
||||
if (isAborted()) {
|
||||
return requestAborted
|
||||
}
|
||||
|
||||
if (!callOnBodySent(handler, controller, body)) {
|
||||
return requestAborted
|
||||
}
|
||||
|
||||
return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted
|
||||
}
|
||||
|
||||
async function dispatchAsyncIterableBody (body, handler, controller, isAborted) {
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of body) {
|
||||
if (isAborted()) {
|
||||
return requestAborted
|
||||
}
|
||||
chunks.push(chunk)
|
||||
if (!callOnBodySent(handler, controller, chunk) || isAborted()) {
|
||||
return requestAborted
|
||||
}
|
||||
}
|
||||
|
||||
if (!callOnRequestSent(handler, controller, isAborted)) {
|
||||
return requestAborted
|
||||
}
|
||||
|
||||
return {
|
||||
async * [Symbol.asyncIterator] () {
|
||||
yield * chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function callOnBodySent (handler, controller, chunk) {
|
||||
try {
|
||||
handler.onBodySent?.(chunk)
|
||||
return true
|
||||
} catch (error) {
|
||||
controller.abort(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function callOnRequestSent (handler, controller, isAborted) {
|
||||
try {
|
||||
handler.onRequestSent?.()
|
||||
return !isAborted()
|
||||
} catch (error) {
|
||||
controller.abort(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isIterableBody (body) {
|
||||
return typeof body !== 'string' &&
|
||||
!Buffer.isBuffer(body) &&
|
||||
!ArrayBuffer.isView(body) &&
|
||||
typeof body[Symbol.iterator] === 'function'
|
||||
}
|
||||
|
||||
function buildMockDispatch () {
|
||||
const agent = this[kMockAgent]
|
||||
const origin = this[kOrigin]
|
||||
const originalDispatch = this[kOriginalDispatch]
|
||||
|
||||
return function dispatch (opts, handler) {
|
||||
if (agent.isMockActive) {
|
||||
try {
|
||||
mockDispatch.call(this, opts, handler)
|
||||
} catch (error) {
|
||||
if (error.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
|
||||
const netConnect = agent[kGetNetConnect]()
|
||||
const totalInterceptsCount = this[kDispatches][kTotalDispatchCount] || this[kDispatches].length
|
||||
const pendingInterceptsCount = this[kDispatches].filter(({ consumed }) => !consumed).length
|
||||
const interceptsMessage = `, ${pendingInterceptsCount} interceptor(s) remaining out of ${totalInterceptsCount} defined`
|
||||
if (netConnect === false) {
|
||||
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)${interceptsMessage}`)
|
||||
}
|
||||
if (checkNetConnect(netConnect, origin)) {
|
||||
originalDispatch.call(this, '__mockAgentBodyForDispatch' in opts
|
||||
? { ...opts, body: opts.__mockAgentBodyForDispatch }
|
||||
: opts, handler)
|
||||
} else {
|
||||
throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)${interceptsMessage}`)
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} else {
|
||||
originalDispatch.call(this, opts, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkNetConnect (netConnect, origin) {
|
||||
const url = new URL(origin)
|
||||
if (netConnect === true) {
|
||||
return true
|
||||
} else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function normalizeOrigin (origin) {
|
||||
if (typeof origin !== 'string' && !(origin instanceof URL)) {
|
||||
return origin
|
||||
}
|
||||
|
||||
if (origin instanceof URL) {
|
||||
return origin.origin
|
||||
}
|
||||
|
||||
return origin.toLowerCase()
|
||||
}
|
||||
|
||||
function buildAndValidateMockOptions (opts) {
|
||||
const { agent, ...mockOptions } = opts
|
||||
|
||||
if ('enableCallHistory' in mockOptions && typeof mockOptions.enableCallHistory !== 'boolean') {
|
||||
throw new InvalidArgumentError('options.enableCallHistory must to be a boolean')
|
||||
}
|
||||
|
||||
if ('acceptNonStandardSearchParameters' in mockOptions && typeof mockOptions.acceptNonStandardSearchParameters !== 'boolean') {
|
||||
throw new InvalidArgumentError('options.acceptNonStandardSearchParameters must to be a boolean')
|
||||
}
|
||||
|
||||
if ('ignoreTrailingSlash' in mockOptions && typeof mockOptions.ignoreTrailingSlash !== 'boolean') {
|
||||
throw new InvalidArgumentError('options.ignoreTrailingSlash must to be a boolean')
|
||||
}
|
||||
|
||||
return mockOptions
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getResponseData,
|
||||
getMockDispatch,
|
||||
addMockDispatch,
|
||||
deleteMockDispatch,
|
||||
buildKey,
|
||||
generateKeyValues,
|
||||
matchValue,
|
||||
getResponse,
|
||||
getStatusText,
|
||||
mockDispatch,
|
||||
buildMockDispatch,
|
||||
checkNetConnect,
|
||||
buildAndValidateMockOptions,
|
||||
getHeaderByName,
|
||||
buildHeadersFromArray,
|
||||
normalizeSearchParams,
|
||||
normalizeOrigin
|
||||
}
|
||||
Reference in new issue
Block a user