@amadeus-it-group/kassette
Version:
Development server, used mainly for testing, which proxies requests and is able to easily manage local mocks.
552 lines (551 loc) • 24.8 kB
TypeScript
import { IncomingHttpHeaders } from 'http';
import { RecursiveArray } from '../../lib/array';
import { IMergedConfiguration, Mode, Delay, MocksFormat } from '../configuration';
import { Status, IFetchedRequest, IResponse, RequestPayload } from '../server/model';
import { ConsoleSpec } from '../logger/model';
import { ChecksumArgs } from './checksum/model';
import { RequestTimings } from '../../lib/har/harTypes';
import { HarKeyManager } from '../../lib/har/harFile';
/**
* Parameter of the {@link ConfigurationSpec.hook|hook} callback, that is called for every
* HTTP request that kassette receives.
* @public
*/
export interface HookAPI {
/**
* Provides the API to specify how to handle the HTTP request.
*/
mock: IMock;
/**
* The console object as specified in the {@link ConfigurationSpec.console|configuration},
* otherwise it is the global console object (usually the one of the platform).
*/
console: ConsoleSpec;
}
/**
* The public interface exposed to the end user to handle a given request and the associated mock and response.
*
* An object implementing this interface is passed to the {@link ConfigurationSpec.hook|hook} function, under property `mock` of the single argument object.
*
* @public
*/
export interface IMock {
/**
* The wrapper around the input request
*/
readonly request: IFetchedRequest;
/**
* The wrapper around the output response
*/
readonly response: IResponse;
/**
* Link to global configuration options
*/
readonly options: MockingOptions;
/**
* The current mode, configured either by a call to {@link IMock.setMode|setMode},
* or by {@link CLIConfigurationSpec.mode|the global setting}.
*/
readonly mode: Mode;
/**
* Sets the {@link IMock.mode|mode} for the current request.
* @param mode - mode to set, or null to use the default value from {@link CLIConfigurationSpec.mode|the global setting}
*/
setMode(mode: Mode | null): void;
/**
* The current remote URL, configured either by a call to {@link IMock.setRemoteURL|setRemoteURL},
* or by {@link CLIConfigurationSpec.remoteURL|the global setting}.
*/
readonly remoteURL: string | null;
/**
* Sets the {@link IMock.remoteURL|remote URL} for the current request.
* @param url - the URL to set, or null to use the default value from {@link CLIConfigurationSpec.remoteURL|the global setting}
*/
setRemoteURL(url: string | null): void;
/**
* The currently computed delay that will be applied, configured either by a call to {@link IMock.setDelay|setDelay},
* or by {@link CLIConfigurationSpec.delay|the global setting}. Note that if the delay is set to `recorded` and the local mock
* to use is not yet loaded, the value returned by this getter will be the default delay and not the recorded delay.
*/
readonly delay: number;
/**
* Sets the {@link IMock.delay|delay} that will be used to send the response to the client
* when the data is taken from the local mock.
* @param delay - can be either a number (to directly specify the delay in milliseconds),
* `'recorded'` (to read the delay from the {@link MockData.time|time} property of an already recorded mock if any)
* or `null` (to remove the effect of any previous call of `setDelay` and use the default value from {@link CLIConfigurationSpec.delay|the global setting} instead).
*/
setDelay(delay: Delay | null): void;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'folder', specifies the root folder of all mocks,
* from which specific mocks paths will be resolved
* (resolved against {@link MockingOptions.root|options.root}).
*/
readonly mocksFolder: string;
/**
* Sets the {@link IMock.mocksFolder|mocksFolder} value.
*
* @param value - any combination of arrays of path parts,
* (as it is also possible for {@link IMock.setLocalPath|setLocalPath}).
* You can pass an absolute path, or a relative one which will be resolved against {@link MockingOptions.root|options.root}.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.mocksFolder|the global setting}.
*/
setMocksFolder(value: RecursiveArray<string | null | undefined> | null): void;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har', contains the full path of the the har file to use.
* If the file name has the `.yml` or `.yaml` extension, the YAML format is used instead of the standard JSON format to read and write the file.
*/
readonly mocksHarFile: string;
/**
* Sets the {@link IMock.mocksHarFile|mocksHarFile} value.
*
* @param value - any combination of arrays of path parts.
* You can pass an absolute path, or a relative one which will be resolved against {@link MockingOptions.root|options.root}.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.mocksHarFile|the global setting}.
*/
setMocksHarFile(value: RecursiveArray<string | null | undefined> | null): void;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har', specifies the default mock har key to use in case
* {@link IMock.setMockHarKey | setMockHarKey} is not called with a non-null value.
* It is computed by calling the {@link IMock.mocksHarKeyManager|mocks har key manager} with the current request.
*
* @remarks
*
* Note that it is lazily computed, but once computed, it is not re-computed even if
* {@link IMock.mocksHarKeyManager | mocksHarKeyManager} changes.
*/
readonly defaultMockHarKey?: string;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har', specifies the key to use inside the har file to either
* read or write a mock.
*
* @remarks
*
* The way the key is stored inside the har file depends on the value of {@link IMock.mocksHarKeyManager|mocksHarKeyManager}.
*/
readonly mockHarKey?: string;
/**
* Sets the {@link IMock.mockHarKey|mockHarKey} value.
*
* @param value - specifies the key to use inside the har file to either read or write a mock. If an array is set (which can be nested),
* it is flattened with null items removed, and joined with forward slashes to produce a string.
* Passing null resets the value to {@link IMock.defaultMockHarKey|the default value}.
*/
setMockHarKey(value: RecursiveArray<string | null | undefined> | null): void;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har', specifies the {@link HarKeyManager|har key manager} to use.
*/
readonly mocksHarKeyManager: HarKeyManager;
/**
* Sets the {@link IMock.mocksHarKeyManager|mocksHarKeyManager} value.
*
* @param value - the {@link HarKeyManager|har key manager} to use for this request.
* Passing null resets the value to the default coming from {@link ConfigurationSpec.mocksHarKeyManager|the global setting}.
*/
setMocksHarKeyManager(value: HarKeyManager | null): void;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har',
* specifies a list of mime types that will attempt to parse the request/response body as JSON.
* This will only be applicable to request bodies if {@link IMock.saveInputRequestBody|saveInputRequestBody} is set to true
* Default value will be [] and will only be overridden by {@link IMock.setHarMimeTypesParseJson|setHarMimeTypesParseJson}
*/
readonly harMimeTypesParseJson: string[];
/**
* Sets the {@link IMock.harMimeTypesParseJson|harMimeTypesParseJson} value.
*
* @param value - The mime types that should attempt to parse the body as json
*/
setHarMimeTypesParseJson(value: string[]): void;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'folder', specifies the local path of the mock, relative to {@link IMock.mocksFolder|mocksFolder}.
* It is either the one set by the user through {@link IMock.setLocalPath|setLocalPath} or {@link IMock.defaultLocalPath|defaultLocalPath}.
*/
readonly localPath: string;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'folder', specifies the default local path of the mock, relative to {@link IMock.mocksFolder|mocksFolder}, .
* It uses the URL pathname to build an equivalent folders hierarchy,
* and appends the HTTP method as a leaf folder.
*/
readonly defaultLocalPath: string;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'folder', specifies the full, absolute path of the mock, built from {@link IMock.localPath|localPath}/{@link IMock.defaultLocalPath|defaultLocalPath}, {@link IMock.mocksFolder|mocksFolder}
* and possibly {@link MockingOptions.root|options.root} if {@link IMock.mocksFolder|mocksFolder} is not absolute.
*/
readonly mockFolderFullPath: string;
/**
* Content produced by the last call to the {@link IMock.checksum|checksum} method,
* as it was passed to the hash algorithm.
*/
readonly checksumContent: string | null;
/**
* Compute a checksum using content from the request.
*
* @remarks
* The computed checksum is intended to be added to the path of the mock
* so that semantically different requests use different mocks.
*
* It is difficult to predict what will actually be relevant to include in
* the checksum or not for your use case, and that's why we provide many options
* to include/exclude/transform data (cf {@link ChecksumArgs}).
*
* Note that we designed the API so that it is usually not needed to
* call the checksum method more than once for a given request/mock.
*
* The method stores the computed content (which is passed to the hash
* algorithm) in property {@link IMock.checksumContent|checksumContent}
* (in the `mock` object), as a string. It is built according to your options
* and the request's data. It is also persisted, so that you can debug more easily,
* especially by committing it into your SCM to analyze changes across versions of
* your code. File is along with the other files of the mock under file name `checksum`.
*
* @param spec - specifies which data from the request to include in the checksum
* @returns The actual checksum value, that you can then
* use for instance to add to the mock's path.
*/
checksum(spec: ChecksumArgs): Promise<string>;
/**
* Sets the {@link IMock.localPath|localPath} value.
*
* @param pathParts - Any combination of values and array of values,
* which will eventually all be flattened, converted to strings and
* joined to build a path.
*
* @example
* The following example will use the HTTP method followed by the URL pathname:
* ```
* mock.setLocalPath([mock.request.method, mock.request.pathname])
* ```
* @example
* The following example will concatenate `prefix`, all portions of the
* URL pathname except the first one (also excluding the very first one which is empty since {@link IFetchedRequest.pathname} has a leading slash)
* and optionally a suffix sequence depending on a boolean.
*
* ```
* mock.setLocalPath([prefix, mock.request.pathname.split('/').slice(2), addSuffix ? [suffix, '-static-suffix'] : null])
* ```
*/
setLocalPath(pathParts: RecursiveArray<string | null | undefined>): void;
/**
* Returns true if the mock exists locally.
*
* @deprecated Use {@link IMock.hasLocalMock} instead.
*
* @remarks
*
* {@link IMock.hasNoLocalFiles|hasNoLocalFiles} returns the opposite boolean value.
*/
hasLocalFiles(): Promise<boolean>;
/**
* Returns true if the mock does not exist locally.
*
* @deprecated Use {@link IMock.hasNoLocalMock} instead.
*
* @remarks
*
* {@link IMock.hasLocalFiles|hasLocalFiles} returns the opposite boolean value.
*/
hasNoLocalFiles(): Promise<boolean>;
/**
* Returns true if the mock exists locally.
*
* @remarks
*
* {@link IMock.hasNoLocalMock|hasNoLocalMock} returns the opposite boolean value.
*/
hasLocalMock(): Promise<boolean>;
/**
* Returns true if the mock does not exist locally.
*
* @remarks
*
* {@link IMock.hasLocalMock|hasLocalMock} returns the opposite boolean value.
*/
hasNoLocalMock(): Promise<boolean>;
/**
* The mocks format used for this request.
*/
readonly mocksFormat: MocksFormat;
/**
* Sets the {@link IMock.mocksFormat|mocksFormat} value.
*
* @param value - the mocks format to use for this request.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.mocksFormat|the global setting}.
*/
setMocksFormat(value: MocksFormat | null): void;
/**
* Whether to save the content used to create a checksum when creating a new mock with a checksum for this request.
*/
readonly saveChecksumContent: boolean;
/**
* Sets the {@link IMock.saveChecksumContent|saveChecksumContent} value.
*
* @param value - whether to save the content used to create a checksum when creating a new mock with a checksum for this request.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.saveChecksumContent|the global setting}.
*/
setSaveChecksumContent(value: boolean | null): void;
/**
* Whether to save {@link RequestTimings | detailed timings} when creating a new mock for this request.
*/
readonly saveDetailedTimings: boolean;
/**
* Sets the {@link IMock.saveDetailedTimings|saveDetailedTimings} value.
*
* @param value - whether to save {@link RequestTimings | detailed timings} when creating a new mock with a checksum for this request.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.saveDetailedTimings|the global setting}.
*/
setSaveDetailedTimings(value: boolean | null): void;
/**
* Whether to save the input request data (headers, method, URL) when creating a new mock for this request.
*/
readonly saveInputRequestData: boolean;
/**
* Sets the {@link IMock.saveInputRequestData|saveInputRequestData} value.
*
* @param value - whether to save the input request data (headers, method, URL) when creating a new mock for this request.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.saveInputRequestData|the global setting}.
*/
setSaveInputRequestData(value: boolean | null): void;
/**
* Whether to save the content of the input request body when creating a new mock for this request.
*/
readonly saveInputRequestBody: boolean;
/**
* Sets the {@link IMock.saveInputRequestBody|saveInputRequestBody} value.
*
* @param value - whether to save the content of the input request body when creating a new mock for this request.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.saveInputRequestBody|the global setting}.
*/
setSaveInputRequestBody(value: boolean | null): void;
/**
* Whether to save the forwarded request data (headers, method, URL) when creating a new mock for this request.
*/
readonly saveForwardedRequestData: boolean;
/**
* Sets the {@link IMock.saveForwardedRequestData|saveForwardedRequestData} value.
*
* @param value - whether to save the forwarded request data (headers, method, URL) when creating a new mock for this request.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.saveForwardedRequestData|the global setting}.
*/
setSaveForwardedRequestData(value: boolean | null): void;
/**
* Whether to save the forwarded request body when creating a new mock for this request.
*/
readonly saveForwardedRequestBody: boolean;
/**
* Sets the {@link IMock.saveForwardedRequestBody|saveForwardedRequestBody} value.
*
* @param value - whether to save the forwarded request body when creating a new mock for this request.
* Passing null resets the value to the default coming from {@link CLIConfigurationSpec.saveForwardedRequestBody|the global setting}.
*/
setSaveForwardedRequestBody(value: boolean | null): void;
/**
* Returns a wrapped payload built from data persisted in local files.
* If no local file is present, returns `undefined`.
*/
readLocalPayload(): Promise<PayloadWithOrigin<'local' | 'user'> | undefined>;
/**
* Take the given wrapped payload and persist it in local files.
* @param payload - payload to persist in local files
*/
persistPayload(payload: PayloadWithOrigin): Promise<void>;
/**
* Forward the client request to the remote backend and get a wrapped payload from the response in output.
*/
fetchPayload(): Promise<RemotePayload>;
/**
* Create a wrapped payload (with `user` origin) from the given payload data.
* @param payload - payload data
*/
createPayload(payload: Payload): PayloadWithOrigin<'user'>;
/**
* Sets the current local payload, with a custom one you would have created.
* @param payload - payload to set
*/
setPayload(payload: PayloadWithOrigin<'local' | 'user'>): void;
/**
* Combines {@link IMock.fetchPayload|fetchPayload} and {@link IMock.persistPayload|persistPayload}
* and returns the wrapped payload.
*/
downloadPayload(): Promise<RemotePayload>;
/**
* Returns the wrapped local payload using {@link IMock.readLocalPayload|readLocalPayload} if it exists,
* otherwise use {@link IMock.downloadPayload|downloadPayload} and returns this wrapped payload.
*/
readOrDownloadPayload(): Promise<PayloadWithOrigin<'local' | 'user'> | RemotePayload>;
/**
* Returns the wrapped local payload using {@link IMock.readLocalPayload|readLocalPayload} if it exists,
* otherwise use {@link IMock.fetchPayload|fetchPayload} and returns this wrapped payload.
*/
readOrFetchPayload(): Promise<PayloadWithOrigin<'local' | 'user'> | RemotePayload>;
/**
* As soon as response is filled with a payload, this property holds the reference to that
* payload's wrapper. The wrapper is useful here to know where the payload comes from.
* Before that, this property is `undefined`.
*/
sourcePayload: PayloadWithOrigin | undefined;
/**
* Use data present in given wrapped payload to fill in the response.
* @param payload - payload to use to fill the response.
*
* @remarks
*
* This method changes {@link IMock.sourcePayload|sourcePayload}.
* It does nothing if {@link IMock.sourcePayload|sourcePayload} is already set.
*/
fillResponseFromPayload(payload: PayloadWithOrigin): void;
/**
* Depending on the {@link IMock.mode|mode}, gets the payload (remote / local / default)
* and uses {@link IMock.fillResponseFromPayload|fillResponseFromPayload} with that payload.
* If {@link IMock.mode|mode} is `manual`, does nothing.
*/
getPayloadAndFillResponse(): Promise<void>;
/**
* Combines {@link IMock.readLocalPayload|readLocalPayload} and {@link IMock.fillResponseFromPayload|fillResponseFromPayload} if there is a
* local payload then returns `true`, otherwise does nothing and return `false`.
*/
readLocalPayloadAndFillResponse(): Promise<boolean>;
/**
* Sends the response back to the client, with the previously specified delay
* if the payload origin is not `remote`.
*/
sendResponse(): Promise<void>;
/**
* Combines {@link IMock.getPayloadAndFillResponse|getPayloadAndFillResponse} and {@link IMock.sendResponse|sendResponse}.
*
* @remarks
*
* If {@link IMock.mode|mode} is `manual`, this method does nothing.
* It also uses a private guard to make sure it is executed only once.
* Therefore, if you call it from the {@link ConfigurationSpec.hook|hook} method, the automatic call made for you after the
* hook execution will actually not do anything (and the same if you call it yourself multiple times).
*/
process(): Promise<void>;
}
/**
* Configuration options, including the root folder used to resolve
* relative paths and the current global configuration.
* @public
*/
export interface MockingOptions {
/** The root folder from which to resolve given relative paths */
readonly root: string;
/** The current global user configuration */
readonly userConfiguration: IMergedConfiguration;
}
export interface MockSpec {
/** See `MockingOptions` */
readonly options: MockingOptions;
/** A wrapper around the input request (see `Request`) */
readonly request: IFetchedRequest;
/** A wrapper around the output response (see `Response`) */
readonly response: IResponse;
}
/**
* The data representing the mock, which is persisted and used for serving the mock
*
* @public
*/
export interface MockData {
/**
* Http version of the server when the mock was recorded. Will most likely be '1.1' or perhaps '1.0'.
* It is not used when replaying responses.
*/
readonly httpVersion?: string;
/**
* Recorded headers to be served back, without the ignored ones.
*/
readonly headers?: Readonly<IncomingHttpHeaders>;
/**
* Ignored headers, which are recorded headers that should not be served back.
* In practice, this is mainly the `content-length` header (because the `content-length` header that is
* actually served back is computed based on the actual data to send).
*/
readonly ignoredHeaders?: Readonly<IncomingHttpHeaders>;
/**
* The name of the local file containing the body content (needed since the name is dynamic)
*/
readonly bodyFileName: string;
/**
* HTTP status.
*/
readonly status: Readonly<Status>;
/**
* Time used by the server to process the request.
* It is used to simulate the real processing time when mocking the server if the {@link CLIConfigurationSpec.delay|delay} is set to `recorded`.
*/
readonly time: number;
/**
* Detailed timings recorded during the request.
*/
readonly timings?: RequestTimings;
/**
* Timestamp when the payload was created.
*/
readonly creationDateTime?: Date;
}
/**
* The payload represents the content of an HTTP response from the backend, no matter if it actually comes from it or if it was created manually.
*
* @remarks
*
* A payload is often wrapped in {@link PayloadWithOrigin}. To create the wrapped payload, you can use {@link IMock.createPayload|createPayload}.
*
* From the wrapped payload, response to the client can be filled with {@link IMock.fillResponseFromPayload|fillResponseFromPayload}.
*
* The payload can also be persisted and read, to avoid contacting the backend later on.
*
* @public
*/
export interface Payload {
/**
* Data such as http status and headers, response delay.
*/
data: MockData;
/**
* Body of the HTTP response.
*/
body: Buffer | string | null;
}
/**
* Origin of the payload.
*
* @remarks
*
* Here are the possible values:
*
* - `local`: if the payload was read from local mock
*
* - `remote`: if the payload was fetched from the remote backend by forwarding the request
*
* - `user`: if the payload has been created from the user, manually using {@link IMock.createPayload|createPayload}
*
* - `proxy`: if the payload has been created from kassette itself, especially for `404 Not found` errors (in `local` mode)
* and `502 Bad Gateway` errors (when kassette cannot reach the remote server)
*
* @public
*/
export type PayloadOrigin = 'local' | 'proxy' | 'remote' | 'user';
/**
* Contains the payload along with its origin.
* @public
*/
export interface PayloadWithOrigin<Origin extends PayloadOrigin = PayloadOrigin> {
/**
* Origin of the payload.
*/
origin: Origin;
/**
* Content of the payload.
*/
payload: Payload;
}
/**
* Remote payload and the request that was made to get it.
* @public
*/
export interface RemotePayload extends PayloadWithOrigin<'remote'> {
/**
* Request used to get this remote payload.
*/
requestOptions: RequestPayload;
}