HTTP
HTTP module of Camouflage lets you mock your backends based on http, https and http2 protocols. You can create a Camouflage http object from CamouflageHttp
class and configure it to serve mocks for your incoming requests.
Start by installing required dependencies
npx jsr add @camouflage/http
- You can create the Camouflage object without any parameters, and load the required options as needed
import CamouflageHttp from "@camouflage/http";
const camouflageHttp = new CamouflageHttp();
camouflageHttp.loadConfigFromJson("./config.json");
camouflageHttp.setServerOptions(options);
camouflageHttp.setSecureServerOptions(options);
camouflageHttp.setupCacheWithOptions(options);
camouflageHttp.setupCorsWithOptions(options);
camouflageHttp.start();
- Or you can create the Camouflage object with the options
import CamouflageHttp, { CamouflageHttpConfig } from "@camouflage/http";
import type http from "http";
import type https from "https";
import type apicache from "apicache";
import type cors from "cors";
const config = {};
const httpOptions = {};
const httpsOptions = {};
const cacheOptions = {};
const corsOptions = {};
const camouflageHttp = new CamouflageHttp(config, httpOptions, httpsOptions, cacheOptions, corsOptions);
camouflageHttp.start();
Available methods
getHelpers = (): Helpers
When you create a CamouflageHttp object, it automatically creates an instance of helpers. You can use getHelpers()
to get a reference to this helpers object. This is useful when you want add custom helpers that are specific to your requirements.
import Helpers from "@camouflage/helpers";
const helpers = camouflageHttp.getHelpers();
helpers.addHelper("ping", (context) => {
return "pong";
});
camouflageHttp.start();
You can take a look at how inbuilt helpers have been created, in case you want to understand how custom helpers can be created. Refer to the helper source code.
loadConfigFromJson(configFilePath: string): void
While you can include your config as part of your code and ensure the types yourself, you may at times want to maintain the configuration for your Camouflage server separate from the application code. This is usually a good practice from maintainability point of view, or even practical if you want to maintain multiple config files for different usecases.
loadConfigFromJson
lets you load a config via a .json file. You don't need to worry about validating your config file, Camouflage takes care of validating your config and prints relevant errors which help you fix your config files, if you miss something.
setServerOptionsHttp(options: http.ServerOptions): void
Depending on your use case, you might want to set additional options. Use setServerOptionsHttp2
to pass those options to Camouflage server. Read more about the available options in the official documentation
setServerOptionsHttps(options: https.ServerOptions): void
In case you are creating an https server, you would need to use setServerOptionsHttps
to provide the necessary credentials. You can use it to add other available options as well to your https servers.
setServerOptionsHttp2(options: spdy.server.ServerOptions): void
In case you are creating an http2 server, you would need to use setServerOptionsHttps
to provide the necessary credentials. You can use it to add other available options as well to your http2 servers.
setupCacheWithOptions(options: apicache.Options): void
Camouflage HTTP uses, apicache
to configure a cache middleware for your mocks. By default, the cache is saved in memory and you can provide a ttl in seconds via config. However in case you want more control over the options, you can fine tune the settings using setupCacheWithOptions
.
Following example shows how you can cache with redis instead of in memory.
npm i redis
import redis from "redis";
import type apicache from "apicache";
let cacheOptions = {
redisClient: redis.createClient(),
respectCacheControl: true,
statusCodes: {
exclude: [401, 403, 404],
include: [200],
},
// more options
};
camouflageHttp.setupCacheWithOptions(cacheOptions);
camouflageHttp.start();
You can refer to apicache documentation for more details on the available options and how to configure them.
setupCorsWithOptions(corsOptions: cors.CorsOptions): void
Camouflage uses cors
middleware to configure cors for your mocks. By default, cors is enabled for all origins and methods, however you can control this by providing Camouflage a corsOptions before you start the server.
import type cors from "cors";
const corsOptions = {
origin: ["http://localhost:3000", "http://instrukti.com/"],
methods: "GET,POST",
};
camouflageHttp.setupCorsWithOptions(corsOptions);
camouflageHttp.start();
Read more about available options on cors documentation.
setupValidationWithOptions = (validationOpts: OpenApiValidatorOpts): void
Allows you to setup validations using an Open API 3 specification. Read more on the usage in the OpenAPI Validation section.
setupCompressionWithOptions = (compressionOpts: CompressionOptions): void
Allows you to setup compression options provided by compression middleware. Read more on usage in the compression section.
addHook = (route: string, event: "onRequest" | "beforeResponse" | "afterResponse", fn: CamouflageHook): void
Apart from the Camouflage helpers, ability to create custom helpers, Camouflage allows you to add hooks to specific routes. You can add hooks to listen to certain events and manipulate the request/response as you wish.
Available hooks are:
- onRequest:
onRequest
hooks are executed as soon as Camouflage recieves the request. You can useonRequest
hooks to intercept the incoming request. You can either make some changes to your incoming request object and then let Camouflage run the response builder on the modified request, or bypass the Camouflage response builder entirely and send the response from within the hook itself without refering to any mock files. - beforeResponse:
beforeResponse
hooks are executed right before Camouflage is about to send the response.beforeResponse
hooks are useful when you want Camouflage response builder to use the provided mock file to build a response, however you want to modify the response before it's sent. - afterResponse:
afterResponse
hooks are executed once Camouflage has sent the response.afterResponse
hooks is useful for logging or other such activities.
In the following section, you'll see how we can configure and use these hooks.
start(): void
Self explanatory. Starts the Camouflage http server.
stop(): void
Self explanatory. Stops the Camouflage http server.
restart(): void
Self explanatory. Restarts the Camouflage http server.
Hooks
onRequest
In the following example, we'll see how can we use onRequest
hook to intercept the incoming request:
camouflageHttp.addHook("/user/:userId/wallet/:walletId", "onRequest", (req, res) => {
console.log("Hello from hook", req.route.path); // You can do some logging
res.set("Sent-From", "onRequestHook"); // You can set some headers
// You can check some conditions
if (req.param.userId === 1) {
// If the condition passes, you can choose to bypass Camouflage entirely by sending the response from within the hook
res.set("Content-Type", "application/json");
const body = {
message: "Sent from onRequestHook",
};
res.status(200).send(JSON.stringify(body));
}
// Or you can do nothing and let Camouflage take over after you are done modifying the request/response objects
});
beforeResponse
Similarily you can use beforeResponse
, to intercept and manipulate the responses generated by Camouflage response builder
camouflageHttp.addHook("/user/:userId/wallet/:walletId", "beforeResponse", (req, res, camouflageResponse) => {
if (camouflageResponse) console.log(camouflageResponse);
res.set("Added-In-Hook", "SomeHeader");
});
afterResponse
Finally, you can use afterResponse
hooks to measure time or log some messages as shown below
let time = 0;
let startTime = 0;
camouflageHttp.addHook("/user/:userId/wallet/:walletId", "onRequest", (req, res) => {
startTime = Date.now();
console.log("Hello from hook", req.route.path);
});
camouflageHttp.addHook("/user/:userId/wallet/:walletId", "afterResponse", (req, res) => {
time = Date.now() - startTime;
console.log(time);
time = 0;
startTime = 0;
});
Camouflage Http Configuration
You can provide following configuration options in your config.json
file and load it to Camouflage before you start the server
{
"log": {
"enable": true, // enables or disables the logs entirely
"level": "trace", // if enable=true, sets the log level. Available options are "fatal", "error", "warn", "info", "debug", "trace"
"disableRequestLogs": true // if not disabled, each incoming request will be logged.
},
"http": {
"enable": true, // enables or disables http server
"port": 8080 // port on which http server would be available
},
"https": {
"enable": false, // enables or disables https server
"port": 8443 // port on which https server would be available
},
"http2": {
"enable": false, // enables or disables http2 server
"port": 9443 // port on which http2 server would be available
},
"monitoring": true, // if enabled, provides a /monitoring endpoint with some dashboards for monitoring
"cache": {
"enable": true, // enables or disables cache
"timeInSeconds": 5 // if enabled, sets cache ttl to 5 seconds
},
"enableCors": true, // enables or disables cors
"mocksDir": "./mocks" // location of the directory where your mocks live.
}
Folder structure
The way you organize your directories inside the mocksDir
, determine how your endpoints will be available. Following examples will help you understand the folder structure you need to maintain. We assume that you have configured your mocksDir
as ./mocks
in following examples
GET Request to /hello/world
- Create a directory
./mocks/hello/world
. - Create a
GET.mock
file inside it with your required response.
HTTP/1.1 200 OK
X-Provided-By: CamouflageHttp
Content-Type: application/json
{
"hello": "world"
}
GET Request to /user/:userId/wallet/:walletId
- Create a directory
/user/:userId/wallet/:walletId
- Create a
GET.mock
file inside it with your required response
HTTP/1.1 200 OK
Content-Type: application/json
{
"userId": {{capture from='path' key='userId'}},
"walletId": {{capture from='path' key='walletId'}},
"createdAt": "{{now format='epoch'}}",
}
Note
Notice the use of helpers
in the above mock. Alongwith the inbuilt now
helper, Camouflage http module provides some additional helpers, capture
being one of them. In the following section, you can read more about additional helpers that specific to http module.
Similarly you can create PUT.mock, DELETE.mock etc in your intended path as required by your mocked endpoint.
Warning
If you are coming from the previous version of Camouflage, note that we have dropped support for wildcards i.e. __ / double underscores. They were primarily intended to be used for dynamic path params which are now handled the way express handles them, which makes it more robust and easy to understand.
Camouflage HTTP Helpers
In addition to the available helpers that come with @camouflage/helpers
module, Camouflage http module provides some protocol specific helpers.
capture
Helper
Usage:
- {{capture from='query' key='firstName'}} - Pretty self-explanatory, but if your endpoint looks like
/hello/world?firstName=John&lastName=Wick
. And your response is{"message": "Hello Wick, John"}
, you can make the response dynamic by formatting your response as
{
"message": "Hello {{capture from='query' key='lastName'}}, {{capture from='query' key='firstName'}}"
}
- {{capture from='cookies' key='mycookie'}} - For cookies, you'd need to specify a key to capture a value.
- {{capture from='path' key='walletId'}} - For path, you'd need to specify a key to capture a value.
- {{capture from='headers' key='Authorization'}} - For headers, you'd need to specify a key to capture a value.
- {{capture from='body' using='jsonpath' selector='$.lastName'}} - To capture values from the request body, your options are either
using='regex'
orusing='jsonpath'
. Selector will change accordingly.
file
Helper
Usage:
{{file path='/location/of/the/image/or/text/or/any/file'}}: If you want to serve a file as a response, maybe an image, or text file, a pdf document, or any type of supported files, use file helper to do so. Content-Type header is decided automatically based on the file type. An example is shown below:
HTTP/1.1 200 OK
{{file path="./docs/camouflage.png"}}
state
Helper
Usage: State helper gets the mocked state value using a key send within the cookie header. If no value is found it will use the default context within the block.
For example:
{
"has_pending_order": {{#state key='has-pending-order'}}false{{/state}},
"cart": {{#state key='cart'}}[
{"id": 999, "name": "default prod"}
]{{/state}}
}
To set a value just send cookie with a specific prefix.
const prefix = "mocked-state";
const key = "has-pending-order";
setCookie(`${prefix}-has-pending-order`, "true");
setCookie(`${prefix}-cart`, '[{id: 1, name: "prod1"}, {id: 2, name: "prod2"}]');
Usage in Cypress
If you use Camouflage with Cypress you could add the following custom command to make life easier.
/**
* Custom cypress command to set a mocked state
*/
Cypress.Commands.add("setState", (key, value) => {
cy.setCookie(`mocked-state-${key}`, typeof value === "string" ? value : JSON.stringify(value));
});
Then in your tests
cy.setState("has_pending_order", true);
cy.setState("cart", [
{ id: 1, name: "prod1" },
{ id: 2, name: "prod2" },
]);
What data to put in .mock files
Camouflage expects a raw HTTP Response to be placed in the .mock files. Please refer to this Wikipedia page, if you are not sure what the response looks like.
Each mock file can have the HTTP Responses in following manner:
- One response per .mock file.
- Multiple responses in one .mock file with conditions defined to help Camouflage decide which response should be sent under what conditions. (Read Handlebars section for more)
- Multiple responses separated by Camouflage's delimiter i.e. "====" (four equals). Camouflage will pick one response at random and send it to the client. An example of this can be found here
The data you want in your mock file can be easily fetched using a curl command with -i -X flags as shown in the example below.
curl -i -X GET https://jsonplaceholder.typicode.com/users/1 > GET.mock
Running this command, gives you a GET.mock
file with following content. Modify it according to your requirement and place it in the location ./mocks/users/:userId
, and you have successfully mocked jsonplaceholder API.
HTTP/1.1 200 OK
date: Sat, 17 Apr 2021 05:21:51 GMT
content-type: application/json; charset=utf-8
content-length: 509
set-cookie: __cfduid=ddf6b687a745fea6ab343400b5dfe9f141618636911; expires=Mon, 17-May-21 05:21:51 GMT; path=/; domain=.typicode.com; HttpOnly; SameSite=Lax
x-powered-by: Express
x-ratelimit-limit: 1000
x-ratelimit-remaining: 998
x-ratelimit-reset: 1612952731
vary: Origin, Accept-Encoding
access-control-allow-credentials: true
cache-control: max-age=43200
pragma: no-cache
expires: -1
x-content-type-options: nosniff
etag: W/"1fd-+2Y3G3w049iSZtw5t1mzSnunngE"
via: 1.1 vegur
cf-cache-status: HIT
age: 14578
accept-ranges: bytes
cf-request-id: 097fe04d2c000019d97db7d000000001
expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report?s=%2FkpNonG0wnuykR5xxlGXKBUxm5DN%2BI1PpQ0ytmiw931XaIVBNqZMJLEr0%2F3kDTrOhbX%2FCCPZtI4iuU3V%2F07wO5uwqov0d4c12%2Fcdpiz7TIFqzGkr7DwUrzt40CLH"}],"max_age":604800,"group":"cf-nel"}
nel: {"max_age":604800,"report_to":"cf-nel"}
server: cloudflare
cf-ray: 6413365b7e9919d9-SIN
alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}
Another, easier, approach to create mocks is by installing the REST Client VS Code Extension and using it to fetch the required data for mocks.
- Launch VS Code and install "REST Client" Extension by Huachao Mao or simply open the link above.
- Create a .http file in your project to document your actual http endpoints and make the requests.
- Visit REST Client github repository for more details on usage
Line breaks
Note
Camouflage by default looks for the OS specific line breaks. For example, if you are on MacOS or Unix based systems, the default line break/new line is \n
, whereas on windows it's \r\n
. This is known to cause issues if your development environment and testing environment are different for Camouflage. For example, if you have created your mock file on a windows machine and uploaded it to a Camouflage server running on linux, your mocks might not work as expected. Or in case your text editor's line break settings do not match your OS default line break, you might not get an expected response.
Though Camouflage detects new lines used in the file irrespective of the OS default, you should not face any issues. However, if you face any issues where you are getting a blank response or any unexpected response, please create an issue attaching your log files. REMEMBER TO REMOVE SENSITIVE DATA, IF ANY, FROM YOUR LOGS.
Request Matching
There are scenarios when you would need to change your response based on some conditions met by fields on request objects. For example, if the end user passes an Authorization header, you'd want to send a 200 OK response if not you'd want to send a 401 Unauthorized response.
To do so you can utilize the beauty of handlebars. Simply provide an if else condition and you are good to go. Let's understand how to do this in the following example:
You expect the user to call the endpoint /hello
in two ways.
- By simple making a GET request to
/hello
; Or - By adding a query parameter name in the GET request to
/hello
. i.e./hello?name=John
Based on how the user calls the API, you'd want to send different responses. Let's see how we can achieve the desired result.
Start by creating a GET.mock
file at the location ./mocks/hello
. Paste the following content in the mock file.
{{#if request.query.name}}
HTTP/1.1 200 OK
X-Provided-By: CamouflageHttp
Content-Type: application/json
{
"greeting": "Hello {{capture from='query' key='name'}}",
"phone": {{randomValue length=10 type='NUMERIC'}},
"dateOfBirth": "{{now format='MM/DD/YYYY'}}",
"test": "{{randomValue}}"
}
{{else}}
HTTP/1.1 200 OK
X-Provided-By: CamouflageHttp
Content-Type: application/json
{
"greeting": "Hello World",
"phone": {{randomValue length=10 type='NUMERIC'}},
"dateOfBirth": "{{now format='MM/DD/YYYY'}}",
"test": "{{randomValue}}"
}
{{/if}}
In the example above, we have provided two responses that Camouflage can pick from, one with greeting: "Hello World"
, and another with greeting: "Hello John"
. In the first line of the mock {{#if request.query.name}}
, we are checking for the condition, if there exists a query parameter called name
. And that's it. If your request is made with the query param name
, Camouflage will respond with first response, if not then the 2nd second response is what you get. And the value of name
can be anything. We are using capture
helper to help us make our response dynamic.
Note
if
and unless
helpers are provided by handlebarjs, which don't have comparison capabilities. These helpers only check if the provided value is truthy or falsy. i.e. you can not do something like this: {{#if something = something}}
. For comparisons, you'd need to use is
helper. See Helpers page for example.
Request Matching using headers
You can use the approach shown above to perform request matching with query, path params and cookies. Use request.query.name
or request.params.name
or request.cookies.something
as required.
However for headers and body, we need to follow a slightly different approach. In the following example, we are using capture
helper to capture a specific header value which then can be passed to other helpers like is
or if
.
{{#if (capture from='headers' key='Authorization') }}
HTTP/1.1 200 OK
Content-Type: application/json
{
"response": "response if auth header is present."
}
{{else}}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"response": "response if no auth header present."
}
{{/if}}
If you want to validate a given header against a specific value, the mock file would be as shown below:
{{#is (capture from='headers' key='Authorization') 'Basic c2h1YmhlbmR1Om1hZGh1a2Fy' }}
HTTP/1.1 200 OK
Content-Type: application/json
{
"response": "response if auth header is present."
}
{{else}}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"response": "response if no auth header present."
}
{{/is}}
Request model
Request object made available by Camouflage is simply an instance of express request object for a given incoming request. Following are the properties/objects available on the request object which can be used in request matching or to extract information out of the request.
- request.baseUrl
- request.body
- request.cookies
- request.method
- request.originalUrl
- request.path
- request.protocol
- request.query
- request.body
Refer to Express Documentation for more information on each of these properties.
Response Delays
You can add a Response-Delay header in raw response placed in your .mock file.
For example, if you'd like to simulate a delay of 2 seconds for GET /hello
endpoint, contents of your ./mocks/hello/GET.mock
file would be as follows:
HTTP/1.1 200 OK
X-Requested-By: Shubhendu Madhukar
Content-Type: application/json
Response-Delay: 2000
{
"greeting": "Hello World",
"phone": {{randomValue length=10 type='NUMERIC'}},
"dateOfBirth": "{{now format='MM/DD/YYYY'}}",
"test": "{{randomValue}}"
}
Additionally you can also simulate a dynamic delay using the {{num_between}} handlebar as follows
Response-Delay: {{num_between lower=500 upper=600}}
OpenAPI Conversion
If you have access to the OpenAPI specification for the APIs/Endpoints you want to mock, Camouflage supports the conversion via the camoswag
utility.
- To use
camoswag
, you would need your OpenAPI specification file in either .json or .yaml format. - You don't need to install
camoswag
locally on your machine, you can simply run the script using npx. - Run the command:
npx camoswag --spec ./swagger.yaml
ornpx camoswag --spec ./swagger.json
. (Replace file location with your spec file location) - If you would like to install camoswag locally, you can do so by running the command:
npm i -g camoswag
. For conversion use,camoswag --spec ./swagger.yaml
- This would create a new folder with the name
camouflage-${current_timestamp}
containing the required folder structure and mock files corresponding to each endpoint defined in your spec file. - You can either delete or modify the dummy responses placed in the mockfiles as per your expectations. Once you are satisfied with the modifications, you can move the contents of the folder to your original
mocksDir
of your running Camouflage server. - Note that if your spec file doesn't contain a response defined for a given endpoint,
camoswag
would put the following default response in the mock file.
{
"message": "More Configuration Needed"
}
Caution
camoswag
currently supports JSON responses only.
OpenAPI Validation
Camouflage uses express-openapi-validator
to enable you to validate your requests/responses against a provided OpenAPI 3 schema.
You can configure validation in two ways.
- Basic Usage: You can enable it via config.json. Add following options to you config:
{
// Other Camouflage options
"validation": {
"enable": true,
"apiSpec": "./apiSpec.yaml",
"validateRequests": true,
"validateResponses": true
}
}
Modify the above config as per your requirements, and you are good to go.
- Advanced Usage: If you want more control over how to configure validation, you can set the supported validation options via the Camouflage method
setupValidationWithOptions
Enable it via config
{
// Other Camouflage options
"validation": {
"enable": true
}
}
And then configure rest of the options as you wish
camouflageHttp.setupValidationWithOptions({
apiSpec: "./openapi.yaml",
validateRequests: true,
validateResponses: true,
// Other options
});
Compression
You can optionally enable the compression options via config.json. Once enabled, compression is applied on all the routes, however you can restrict this behaviour using the setupCompressionWithOptions
method and available options
Enable compression in config.json
{
// Other Camouflage options
"compression": true
}
Then provide required options
function shouldCompress(req, res) {
if (req.headers["x-no-compression"]) {
// don't compress responses with this request header
return false;
}
// fallback to standard filter function
return compression.filter(req, res);
}
camouflageHttp.setupCompressionWithOptions({ filter: shouldCompress });