Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions lib/headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* This file implements the Web API Headers interface
* (https://developer.mozilla.org/en-US/docs/Web/API/Headers)
* while maintaining compatibility with Express.js style header access.
*/

/**
* Creates a Headers object that implements both Express.js style access
* and the Web API Headers interface
*
* @param {Object} headers - Initial headers object
* @returns {HeaderWebAPI} - A proxy that implements the HeaderWebAPI interface
*/
function createHeaders(headers = {}) {
return new Proxy(headers, {
get(target, prop) {
// Direct property access for Express.js style
if (typeof prop === 'string' && prop in target) {
return target[prop];
}

// Handle Headers interface methods
switch (prop) {
case 'get':
return (name) => {
const lowerName = name.toLowerCase();
// Special case for referer/referrer
if (lowerName === 'referer' || lowerName === 'referrer') {
return target.referrer || target.referer;
}
return target[lowerName];
};
case 'getAll':
return (name) => {
const value = target[name.toLowerCase()];
if (!value) {
return [];
}
return Array.isArray(value) ? value : [value];
};
case 'has':
return (name) => name.toLowerCase() in target;
case 'set':
return (name, value) => {
// eslint-disable-next-line no-param-reassign
target[name.toLowerCase()] = value;
};
case 'append':
return (name, value) => {
const lowerName = name.toLowerCase();
if (lowerName in target) {
const existingValue = target[lowerName];
if (Array.isArray(existingValue)) {
existingValue.push(value);
} else {
// eslint-disable-next-line no-param-reassign
target[lowerName] = [existingValue, value];
}
} else {
// eslint-disable-next-line no-param-reassign
target[lowerName] = value;
}
};
case 'delete':
return (name) => {
// eslint-disable-next-line no-param-reassign
delete target[name.toLowerCase()];
};
case 'forEach':
return (callback, thisArg) => {
Object.entries(target).forEach(([key, value]) => {
callback.call(thisArg, value, key, target);
});
};
case 'entries':
return () => Object.entries(target)[Symbol.iterator]();
case 'keys':
return () => Object.keys(target)[Symbol.iterator]();
case 'values':
return () => Object.values(target)[Symbol.iterator]();
case Symbol.iterator:
return (
target[Symbol.iterator] ||
function* iterator() {
yield* Object.entries(target);
}
);
default:
return target[prop];
}
},
set(target, prop, value) {
if (typeof prop === 'string') {
// eslint-disable-next-line no-param-reassign
target[prop.toLowerCase()] = value;
return true;
}
return false;
},
has(target, prop) {
if (typeof prop === 'string') {
return prop.toLowerCase() in target;
}
return prop in target;
},
deleteProperty(target, prop) {
if (typeof prop === 'string') {
// eslint-disable-next-line no-param-reassign
delete target[prop.toLowerCase()];
return true;
}
// eslint-disable-next-line no-param-reassign
return delete target[prop];
},
ownKeys(target) {
return Object.keys(target);
},
getOwnPropertyDescriptor(target, prop) {
return Object.getOwnPropertyDescriptor(target, prop);
}
});
}

module.exports = { createHeaders };
31 changes: 30 additions & 1 deletion lib/http-mock.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Cookies {
}

export interface Headers {
// Standard HTTP headers
accept?: string;
'accept-language'?: string;
'accept-patch'?: string;
Expand Down Expand Up @@ -73,9 +74,35 @@ export interface Headers {
via?: string;
warning?: string;
'www-authenticate'?: string;

// Support for arbitrary headers
[header: string]: string | string[] | undefined;
}

/**
* HeaderWebAPI interface combines the existing Headers type with
* standard Web API Headers interface methods for better compatibility
* with browser environments.
*/
export interface HeaderWebAPI {
// Include all the header properties
[header: string]: any; // 'any' to accommodate both header values and methods

// Web API Headers methods
append(name: string, value: string): void;
delete(name: string): void;
get(name: string): string | null;
has(name: string): boolean;
set(name: string, value: string): void;
forEach(callbackfn: (value: string, key: string, parent: HeaderWebAPI) => void, thisArg?: any): void;

// Iterator methods
entries(): IterableIterator<[string, string]>;
keys(): IterableIterator<string>;
values(): IterableIterator<string>;
[Symbol.iterator](): IterableIterator<[string, string]>;
}

export interface Query {
[key: string]: any;
}
Expand Down Expand Up @@ -121,6 +148,8 @@ export type MockRequest<T extends RequestType> = T & {
_setBody: (body?: Body) => void;
_addBody: (key: string, value?: any) => void;

headers: HeaderWebAPI;

// Support custom properties appended on Request objects.
[key: string]: any;
};
Expand All @@ -139,7 +168,7 @@ export type ResponseCookie = {

export type MockResponse<T extends ResponseType> = T & {
_isEndCalled: () => boolean;
_getHeaders: () => Headers;
_getHeaders: () => HeaderWebAPI;
_getData: () => any;
_getJSONData: () => any;
_getBuffer: () => Buffer;
Expand Down
7 changes: 6 additions & 1 deletion lib/mockRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const parseRange = require('range-parser');
let { EventEmitter } = require('events');
const querystring = require('querystring');
const utils = require('./utils');
const { createHeaders } = require('./headers');

const standardRequestOptions = [
'method',
Expand Down Expand Up @@ -73,7 +74,11 @@ function createRequest(options = {}) {
if (options.signedCookies) {
mockRequest.signedCookies = options.signedCookies;
}
mockRequest.headers = options.headers ? utils.convertKeysToLowerCase(options.headers) : {};

// Create headers using the Headers.js module
const originalHeaders = options.headers ? utils.convertKeysToLowerCase(options.headers) : {};
mockRequest.headers = createHeaders(originalHeaders);

mockRequest.body = options.body ? options.body : {};
mockRequest.query = options.query ? options.query : {};
mockRequest.files = options.files ? options.files : {};
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading