mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Add mock API service tool for frontend development (#4814)
This commit is contained in:
parent
723b0e2dc8
commit
489ec700c5
7 changed files with 510 additions and 0 deletions
92
frontend/services/mock_service/README.md
Normal file
92
frontend/services/mock_service/README.md
Normal 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.
|
||||
54
frontend/services/mock_service/examples/config.ts
Normal file
54
frontend/services/mock_service/examples/config.ts
Normal 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 };
|
||||
96
frontend/services/mock_service/examples/responses.ts
Normal file
96
frontend/services/mock_service/examples/responses.ts
Normal 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",
|
||||
},
|
||||
};
|
||||
1
frontend/services/mock_service/index.ts
Normal file
1
frontend/services/mock_service/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./service/service";
|
||||
54
frontend/services/mock_service/mocks/config.ts
Normal file
54
frontend/services/mock_service/mocks/config.ts
Normal 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 };
|
||||
96
frontend/services/mock_service/mocks/responses.ts
Normal file
96
frontend/services/mock_service/mocks/responses.ts
Normal 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",
|
||||
},
|
||||
};
|
||||
117
frontend/services/mock_service/service/service.ts
Normal file
117
frontend/services/mock_service/service/service.ts
Normal 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;
|
||||
Loading…
Reference in a new issue