Add mock API service tool for frontend development (#4814)

This commit is contained in:
gillespi314 2022-03-25 17:02:58 -05:00 committed by GitHub
parent 723b0e2dc8
commit 489ec700c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 510 additions and 0 deletions

View file

@ -0,0 +1,92 @@
# How to use mock service
The mock service implements the `sendRequest` interface and enables you to more easily develop
frontend features that make use of spec API endpoints without waiting for backend development to be
completed.
You configure the mock service by mapping the API request paths and expected JSON responses
based on the API specification for your feature. Then, you simply import the mock service
in place of `sendRequest` when you build out the `/services/entities` methods for your feature.
The mock service simulates async network requests and responses for the API. The mock service tries
to match incoming request URLs against the paths declared in `REQUEST_RESPONSE_MAPPINGS`.
If a match is found, the mock service returns the expected JSON response with `Promise.resolve`.
If no match is found, the mock service returns an error with `Promise.reject`.
## Importing the mock service
To use the mock service in development, import the following in the `/services/entities` file
for your API service.
```js
import { sendRequest } from "services/mock_service/service/service";
```
When the real API is ready, swap out the mock service import with the normal `sendRequest` import.
```js
import { sendRequest } from "services";
```
## Configuring the mock service
Configuration consists of two files: `config` and `responses`.
### Responses file
Declare your static JSON responses as constants in the `responses` file inside the `./mocks` folder.
Each JSON response should be given its own unique name and added to the default export for the file.
These responses will be imported as `STATIC` in the `config` file where you will map the responses
to the the request paths for your API service.
### Config file
Declare your endpoint and each of your API request paths to its expected JSON response in the
`config` file inside the `./mocks` folder.
Set the `DELAY` constant (in milliseconds) if you want to simulate a delayed API response.
Set the `ENDPOINT` constant to the base route for your API endpoint (for example, `/v1/fleet`).
Use the `REQUEST_RESPONSE_MAPPINGS` dictionary to declare your request-responses mappings. For example,
here's how you might configure the `GET hosts/manage` and `GET hosts/count`endpoints:
```js
const REQUEST_RESPONSE_MAPPINGS = {
GET: {
"/hosts/manage": STATIC.MANAGE,
"/hosts/count": STATIC.COUNT
}
};
```
You can declare different responses for specific route parameters or query parameters. Alteratively,
you can use wildcards if you don't particularly care what parameter value is contained in the request.
The example below shows how you might use wildcards or specific values as the `id` route parameter
to get a host's device mapping by host id and how you might use wildcards or specific values
for the `team_id` query parameter to get hosts filtered by team id.
```js
const REQUEST_RESPONSE_MAPPINGS = {
GET: {
"/hosts/1/device_mapping": STATIC.HOST_1_DEVICE_MAPPING, // specific route param value
"/hosts/:id/device_mapping": STATIC.HOST_ID_DEVICE_MAPPING, // wildcard route param value
"/hosts/manage?team_id=1": STATIC.HOSTS_TEAM_1, // specific query param value
"/hosts/manage?team_id={id}": STATIC.HOSTS_TEAM_ID // wildcard query param value
}
};
```
For purposes of illustration, the example above uses ":" as well as "{" and "}" for wildcard
characters. You can set the `WILDCARDS` constant in the `config` file to define any number
of wildcards to suit the conventions of the API spec for your feature.
The mock service evaluates URLs part-by-part. Each URL is split at the "?" character. If more than
one "?" is present, the mock service throws an error. If this happens in the context of an API request, the
mock service returns with `Promise.reject`. Assuming only one "?" is present, the first half is
split into parts at each "/" and the second half is split into parts at each "&". The substring parts are
evaluated for matching purposes in the order they appeared in the URL string. If no "?" is present,
the URL will only be split by "/".
The presence of one or more wildcard characters anywhere in a substring part of the URL path
declared in `REQUEST_RESPONSE_MAPPINGS` triggers a match against the corresponding part of the
request URL. More precise handling of wildcard parameters is something that may be added in the
future. In the meantime, you should take care that the path you declare in `REQUEST_RESPONSE_MAPPINGS`
follows the order of the params in the request made by your `/services/entities` methods.
# Examples
Example `config` and `response` files are included in `./examples`. Please be sure to copy these
files into `./mocks` before making any changes.

View file

@ -0,0 +1,54 @@
/*
* NOTE: This is an example of how to configure your mock service.
* Be sure to copy this file into `../mocks` and only edit that copy!
* Also please check the README for how to use the mock service :)
*/
import RESPONSES from "./responses";
type IResponses = Record<string, Record<string, Record<string, unknown>>>;
const DELAY = 1000; // modify the DELAY value (in milliseconds) to simulate a delayed async response
const ENDPOINT = "/v1/fleet"; // modify the ENDPOINT string to correspond to your API spec
// WILDCARDS can be used to represent URL parameters in any combination as illustrated below
// modify the WILDCARDS array if you prefer to use different characters
const WILDCARDS: string[] = [":", "*", "{", "}"];
// REQUEST_RESPONSE_MAPPINGS dictionary maps your static responses to the specified API request path
const REQUEST_RESPONSE_MAPPINGS: IResponses = {
GET: {
// this is a basic path with no wildcards
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc":
RESPONSES.ALL_HOSTS,
// this basic path only matches with '1337' as the value for the team id query param
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id=1337":
RESPONSES.HOSTS_TEAM_1337,
// this wildcard path matches with any other value for the team id query param
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id={team_id}":
RESPONSES.HOSTS_TEAM_ID,
// this basic path only matches with '1337' as the value for the host id route param
"/hosts/1337": RESPONSES.HOST_1337,
// this wildcard path matches with any other value for the host id route param
"/hosts/*id": RESPONSES.HOST_ID,
// this wildcard path matches with any value for the host id route param
"/hosts/:id/device_mapping": RESPONSES.DEVICE_MAPPING,
// this wildcard path matches with any value for the host id route param
"hosts/{*}/macadmins": RESPONSES.MACADMINS,
// this is a basic path with no wildcards
"hosts/count": {
count: 1,
},
// this wildcard path matches with any value for the team id route param
"hosts/count?team_id={*}": {
count: 1,
},
},
// additional mappings can be specified for other HTTP request types (POST, PATCH, DELETE, etc.)
POST: {
"/:id/refetch": {}, // this wildcard route returns empty JSON
},
} as IResponses;
export default { DELAY, ENDPOINT, WILDCARDS, REQUEST_RESPONSE_MAPPINGS };

View file

@ -0,0 +1,96 @@
/*
* NOTE: This is an example of how to define data for your mock responses.
* Be sure to copy this file into `../mocks` and only edit that copy!
* Also please check the README for how to use the mock service :)
*/
const HOST_ID = {
host: {
created_at: "2021-03-31T00:00:00Z",
updated_at: "2021-03-31T00:00:00Z",
software: [],
id: 1337,
detail_updated_at: "2021-03-31T00:00:00Z",
label_updated_at: "2021-03-31T00:00:00Z",
policy_updated_at: "2021-03-31T00:00:00Z",
last_enrolled_at: "2021-03-31T00:00:00Z",
seen_time: "2021-03-31T00:00:00ZZ",
refetch_requested: false,
hostname: "myf1337d3v1c3",
uuid: "13371337-0000-0000-1337-133713371337",
platform: "rhel",
osquery_version: "5.1.0",
os_version: "Ubuntu 20.4.0",
build: "",
platform_like: "deb",
code_name: "",
uptime: 13371337133713371337,
memory: 143593800000,
cpu_type: "1337",
cpu_subtype: "1337",
cpu_brand: "Intel(R) Core(TM) i3-37k CPU @ 13.37GHz",
cpu_physical_cores: 8,
cpu_logical_cores: 8,
hardware_vendor: "",
hardware_model: "",
hardware_version: "",
hardware_serial: "",
computer_name: "myf1337d3v1c3",
primary_ip: "133.7.133.7",
primary_mac: "13:37:13:37:13:37",
distributed_interval: 1337,
config_tls_refresh: 1337,
logger_tls_period: 1337,
team_id: null,
pack_stats: [],
team_name: null,
users: [
{
uid: 1337,
username: "root",
type: "",
groupname: "root",
shell: "/bin/bash",
},
],
gigs_disk_space_available: 13.37,
percent_disk_space_available: 13.37,
issues: {
total_issues_count: 1337,
failing_policies_count: 1337,
},
labels: [],
packs: [],
policies: [],
status: "online",
display_text: "myf1337d3v1c3",
},
};
const HOST_1337 = {
...HOST_ID,
team_id: 1337,
team_name: "h4x0r",
};
export default {
ALL_HOSTS: {
hosts: [HOST_ID.host],
},
HOSTS_TEAM_ID: {
hosts: [{ ...HOST_ID.host, team_id: 2, team_name: "n00bz" }],
},
HOSTS_TEAM_1337: {
hosts: [HOST_1337.host],
},
HOST_ID,
HOST_1337,
DEVICE_MAPPING: {
host_id: 1337,
device_mapping: null,
foo: "bar",
},
MACADMINS: {
macadmins: null,
foo: "bar",
},
};

View file

@ -0,0 +1 @@
export { default } from "./service/service";

View file

@ -0,0 +1,54 @@
/*
* NOTE: This is an example of how to configure your mock service.
* Be sure to copy this file into `../mocks` and only edit that copy!
* Also please check the README for how to use the mock service :)
*/
import RESPONSES from "./responses";
type IResponses = Record<string, Record<string, Record<string, unknown>>>;
const DELAY = 1000; // modify the DELAY value (in milliseconds) to simulate a delayed async response
const ENDPOINT = "/v1/fleet"; // modify the ENDPOINT string to correspond to your API spec
// WILDCARDS can be used to represent URL parameters in any combination as illustrated below
// modify the WILDCARDS array if you prefer to use different characters
const WILDCARDS: string[] = [":", "*", "{", "}"];
// REQUEST_RESPONSE_MAPPINGS dictionary maps your static responses to the specified API request path
const REQUEST_RESPONSE_MAPPINGS: IResponses = {
GET: {
// this is a basic path with no wildcards
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc":
RESPONSES.ALL_HOSTS,
// this basic path only matches with '1337' as the value for the team id query param
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id=1337":
RESPONSES.HOSTS_TEAM_1337,
// this wildcard path matches with any other value for the team id query param
"/hosts?page=0&per_page=100&order_key=hostname&order_direction=asc&team_id={team_id}":
RESPONSES.HOSTS_TEAM_ID,
// this basic path only matches with '1337' as the value for the host id route param
"/hosts/1337": RESPONSES.HOST_1337,
// this wildcard path matches with any other value for the host id route param
"/hosts/*id": RESPONSES.HOST_ID,
// this wildcard path matches with any value for the host id route param
"/hosts/:id/device_mapping": RESPONSES.DEVICE_MAPPING,
// this wildcard path matches with any value for the host id route param
"hosts/{*}/macadmins": RESPONSES.MACADMINS,
// this is a basic path with no wildcards
"hosts/count": {
count: 1,
},
// this wildcard path matches with any value for the team id route param
"hosts/count?team_id={*}": {
count: 1,
},
},
// additional mappings can be specified for other HTTP request types (POST, PATCH, DELETE, etc.)
POST: {
"/:id/refetch": {}, // this wildcard route returns empty JSON
},
} as IResponses;
export default { DELAY, ENDPOINT, WILDCARDS, REQUEST_RESPONSE_MAPPINGS };

View file

@ -0,0 +1,96 @@
/*
* NOTE: This is an example of how to define data for your mock responses.
* Be sure to copy this file into `../mocks` and only edit that copy!
* Also please check the README for how to use the mock service :)
*/
const HOST_ID = {
host: {
created_at: "2021-03-31T00:00:00Z",
updated_at: "2021-03-31T00:00:00Z",
software: [],
id: 1337,
detail_updated_at: "2021-03-31T00:00:00Z",
label_updated_at: "2021-03-31T00:00:00Z",
policy_updated_at: "2021-03-31T00:00:00Z",
last_enrolled_at: "2021-03-31T00:00:00Z",
seen_time: "2021-03-31T00:00:00ZZ",
refetch_requested: false,
hostname: "myf1337d3v1c3",
uuid: "13371337-0000-0000-1337-133713371337",
platform: "rhel",
osquery_version: "5.1.0",
os_version: "Ubuntu 20.4.0",
build: "",
platform_like: "deb",
code_name: "",
uptime: 13371337133713371337,
memory: 143593800000,
cpu_type: "1337",
cpu_subtype: "1337",
cpu_brand: "Intel(R) Core(TM) i3-37k CPU @ 13.37GHz",
cpu_physical_cores: 8,
cpu_logical_cores: 8,
hardware_vendor: "",
hardware_model: "",
hardware_version: "",
hardware_serial: "",
computer_name: "myf1337d3v1c3",
primary_ip: "133.7.133.7",
primary_mac: "13:37:13:37:13:37",
distributed_interval: 1337,
config_tls_refresh: 1337,
logger_tls_period: 1337,
team_id: null,
pack_stats: [],
team_name: null,
users: [
{
uid: 1337,
username: "root",
type: "",
groupname: "root",
shell: "/bin/bash",
},
],
gigs_disk_space_available: 13.37,
percent_disk_space_available: 13.37,
issues: {
total_issues_count: 1337,
failing_policies_count: 1337,
},
labels: [],
packs: [],
policies: [],
status: "online",
display_text: "myf1337d3v1c3",
},
};
const HOST_1337 = {
...HOST_ID,
team_id: 1337,
team_name: "h4x0r",
};
export default {
ALL_HOSTS: {
hosts: [HOST_ID.host],
},
HOSTS_TEAM_ID: {
hosts: [{ ...HOST_ID.host, team_id: 2, team_name: "n00bz" }],
},
HOSTS_TEAM_1337: {
hosts: [HOST_1337.host],
},
HOST_ID,
HOST_1337,
DEVICE_MAPPING: {
host_id: 1337,
device_mapping: null,
foo: "bar",
},
MACADMINS: {
macadmins: null,
foo: "bar",
},
};

View file

@ -0,0 +1,117 @@
/*
* NOTE: Do not make changes to this file!
* Also please check the README for how to use the mock service :)
*/
import { trim } from "lodash";
import CONFIG from "../mocks/config";
const {
DELAY,
ENDPOINT,
REQUEST_RESPONSE_MAPPINGS: RESPONSES,
WILDCARDS,
} = CONFIG;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const splitRouteAndQueryString = (path: string) => {
path = trim(path, "/").replace(trim(ENDPOINT, "/"), "");
const strings = trim(path, "/").split("?");
if (strings.length > 2) {
throw new Error(
"Invalid usage: URL cannot contain more than one `?` and query string must follow format `?key=value&another_key=another_value`"
);
}
if (strings.length === 1) {
return path.includes("?")
? [undefined, strings[0]]
: [strings[0], undefined];
}
return strings;
};
const getParts = (pathString: string) => {
const [routeString, queryString] = splitRouteAndQueryString(pathString);
const routeParts = routeString ? routeString.split("/") : [];
const queryParts = queryString ? queryString.split("&") : [];
return routeParts.concat(queryParts);
};
const partsByPathByMethod = {} as Record<string, Record<string, string[]>>;
Object.entries(RESPONSES).forEach(([method, paths]) => {
Object.keys(paths).forEach((pathString) => {
if (!partsByPathByMethod[method]) {
partsByPathByMethod[method] = {} as Record<string, string[]>;
}
partsByPathByMethod[method][pathString] = getParts(pathString);
});
});
const isPartMatch = (
partToMatch: string,
configPart: string,
wildcards: string[] = []
) => {
return (
partToMatch === configPart || wildcards.some((w) => configPart.includes(w)) // if a config part includes any wildcards, it matches with any value
);
};
const matchPathToResponse = (method: string, requestPath: string) => {
const results = Object.entries(partsByPathByMethod[method]).filter(
([, configParts]) => {
const requestParts = getParts(requestPath);
return (
requestParts.length === configParts.length &&
requestParts.every((p, i) => isPartMatch(p, configParts[i], WILDCARDS))
);
}
);
return results;
};
export const sendRequest = async (
method = "GET",
requestPath: string,
data?: Record<string, unknown>
): Promise<any> => {
console.log("Mock service request URL: ", requestPath);
console.log("Mock service request body: ", data);
requestPath = trim(requestPath, "/").replace(ENDPOINT, "");
let response: Record<string, unknown> | undefined;
let responseKey: string | undefined;
try {
const matches = matchPathToResponse(method, requestPath) || [];
if (matches.length > 1) {
[responseKey] =
matches.find(([key]) => !WILDCARDS.some((w) => key.includes(w))) || [];
} else {
responseKey = matches?.[0]?.[0];
}
} catch (err) {
return Promise.reject(err);
}
if (responseKey) {
response = RESPONSES?.[method]?.[responseKey];
}
if (!responseKey || !response) {
return Promise.reject(`Mock service error: 404 ${requestPath} not found`);
}
await sleep(DELAY);
console.log("Mock service response: ", response);
return Promise.resolve(response);
};
export default sendRequest;