counterfact 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +10 -1
- package/CHANGELOG.md +15 -0
- package/README.md +90 -39
- package/bin/counterfact.js +67 -24
- package/docs/quick-start.md +19 -0
- package/docs/usage.md +47 -7
- package/package.json +4 -2
- package/src/client/index.html.hbs +27 -21
- package/src/client/rapi-doc.html.hbs +36 -0
- package/src/server/dispatcher.js +31 -0
- package/src/server/koa-middleware.js +12 -3
- package/src/server/module-loader.js +2 -2
- package/src/server/registry.js +2 -1
- package/src/server/start.js +50 -32
- package/src/typescript-generator/coder.js +4 -0
- package/src/typescript-generator/context-coder.js +22 -5
- package/src/typescript-generator/operation-type-coder.js +4 -3
- package/src/typescript-generator/script.js +4 -3
- package/test/server/dispatcher.proxy.test.js +40 -0
- package/test/server/dispatcher.test.js +1 -3
- package/test/server/koa-middleware.test.js +30 -0
- package/test/server/module-loader.test.js +2 -2
- package/test/typescript-generator/__snapshots__/end-to-end.test.js.snap +1311 -396
- package/test/typescript-generator/__snapshots__/operation-type-coder.test.js.snap +8 -0
- package/test/server/start.test.js +0 -11
package/.vscode/settings.json
CHANGED
|
@@ -15,5 +15,14 @@
|
|
|
15
15
|
"[typescript]": {
|
|
16
16
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
|
17
17
|
},
|
|
18
|
-
"cSpell.words": [
|
|
18
|
+
"cSpell.words": [
|
|
19
|
+
"bodyparser",
|
|
20
|
+
"counterfact",
|
|
21
|
+
"Deno",
|
|
22
|
+
"openapi",
|
|
23
|
+
"Petstore",
|
|
24
|
+
"counterfactuals",
|
|
25
|
+
"openapi",
|
|
26
|
+
"transpiles"
|
|
27
|
+
]
|
|
19
28
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# counterfact
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 78c0c31: return $.proxy(url) to proxy to another server
|
|
8
|
+
- 6bf3d42: $context.ts -> $.context.ts
|
|
9
|
+
- 7c2893a: change rapidoc configuration to show paths in the nav bar
|
|
10
|
+
- 033c067: new landing page with links to Rapidoc and Swagger UI
|
|
11
|
+
- 6bf3d42: improve and document the REPL
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- a2d6bf0: always start the server regardless of which command line options are present
|
|
16
|
+
- 4a96dfa: fix crash when a path is not found
|
|
17
|
+
|
|
3
18
|
## 0.14.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,75 +1,126 @@
|
|
|
1
|
+
<div align="center" markdown="1">
|
|
1
2
|
|
|
3
|
+
# Counterfact is the mock server that front end engineers need to be productive
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
[Quick Start](./docs/quick-start.md) | [Documentation](./docs/usage.md) | [Contributing](CONTRIBUTING.md)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
</div>
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
<br>
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
<div align="center" markdown="1">
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
It enables you to write front end code and test UX flows without a complete back end.
|
|
13
|
+
[](https://coveralls.io/github/pmcelhaney/counterfact) [](https://dashboard.stryker-mutator.io/reports/github.com/pmcelhaney/counterfact/main) 
|
|
13
14
|
|
|
14
15
|
</div>
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
## Features
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
- Supports JavaScript and TypeScript
|
|
20
|
+
- Runs on Node, Deno, and Bun
|
|
21
|
+
- Uses Swagger / OpenAPI if you have it
|
|
22
|
+
- File system based routing
|
|
23
|
+
- Hot reload server-side code
|
|
24
|
+
- Manipulate test data with a REPL
|
|
25
|
+
- Toggle between the mock and real backend at runtime
|
|
26
|
+
- Works with any “front end” that calls RESTful services (a React app, a native iOS app, a web service, etc.)
|
|
27
|
+
- Emulates the real back end, but 100X faster
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
<td>
|
|
29
|
+
## Use Cases
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
- Rapidly iterate on the front end without waiting for back end features or changes
|
|
32
|
+
- Reproduce bugs when the conditions on the server side are hard to replicate
|
|
33
|
+
- Test against a private, local stack that you fully control
|
|
34
|
+
- Write contract tests for APIs
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
Unlike similar tools, Counterfact is not limited to canned or randomly generated responses. It can mimic as much or as little of the business logic in your n-tier / cloud-native back end as you want. Test end-to-end scenarios in your front end code as if a lighting-fast implementation of the real backend is running on your machine. Because it is.
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
As a front end dev, when you have **complete and granular control over the back end**, and you can **modify anything on a whim**, it **enhances your developer experience** in ways that are hard to describe.
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
🧑🔬 test UI code against hard-to-recreate edge cases<br>
|
|
33
|
-
🔌 plug into your existing toolchain
|
|
40
|
+
## What does the code look like?
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
<details markdown="1">
|
|
36
43
|
|
|
37
|
-
</
|
|
44
|
+
<summary><code>GET /hello/world</code></summary>
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
```js
|
|
47
|
+
// ./paths/hello/world.js
|
|
48
|
+
export const GET = () => "Hello World!";
|
|
49
|
+
```
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
</details>
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
<details>
|
|
44
54
|
|
|
55
|
+
<summary markdown="1">Using the <a href="https://petstore3.swagger.io">Swagger Petstore</a> as an example, here’s one way to implement <code>GET /pet/{petId}`</code> and <code>POST `POST /pets`</code>.</summary>
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
```js
|
|
58
|
+
// ./paths/pet/{petId}.js
|
|
59
|
+
export const GET = ({ context, response, path }) => {
|
|
60
|
+
const pet = context.getPetById(path.petId);
|
|
47
61
|
|
|
48
|
-
|
|
62
|
+
if (!pet) {
|
|
63
|
+
return response[404].text(`Pet with ID ${path.petID} not found.`);
|
|
64
|
+
}
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
return response[200].json(pet);
|
|
67
|
+
};
|
|
68
|
+
```
|
|
51
69
|
|
|
52
|
-
```
|
|
53
|
-
|
|
70
|
+
```js
|
|
71
|
+
// ./paths/pets.js
|
|
72
|
+
export const POST = ({ context, response, body }) => {
|
|
73
|
+
const pet = context.addPet(body);
|
|
74
|
+
|
|
75
|
+
return response[200].json(pet);
|
|
76
|
+
};
|
|
54
77
|
```
|
|
55
78
|
|
|
56
|
-
|
|
57
|
-
|
|
79
|
+
```js
|
|
80
|
+
// ./paths/$.context.js
|
|
81
|
+
class PetStore () {
|
|
82
|
+
pets = {};
|
|
83
|
+
|
|
84
|
+
getPetById(petId) {
|
|
85
|
+
return pets[id];
|
|
86
|
+
}
|
|
58
87
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
5. opens your browser to [Swagger UI](https://swagger.io/tools/swagger-ui/) (`--open`)
|
|
88
|
+
addPet(pet) {
|
|
89
|
+
this.pets[pet.id] = pet;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
64
92
|
|
|
65
|
-
|
|
93
|
+
export default new PetStore();
|
|
94
|
+
```
|
|
66
95
|
|
|
67
96
|
</details>
|
|
68
97
|
|
|
69
|
-
|
|
98
|
+
## If you have a Swagger / OpenAPI spec, you'll be up and running in seconds
|
|
99
|
+
|
|
100
|
+
For example, run the following command to generate code for the Swagger Petstore.
|
|
101
|
+
|
|
102
|
+
```sh copy
|
|
103
|
+
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api --open
|
|
104
|
+
```
|
|
70
105
|
|
|
71
|
-
|
|
106
|
+
That command generates and starts a TypeScript implementation of the Swagger Petstore which returns random, valid responses. Not too shabby for _a few seconds of work_!
|
|
107
|
+
|
|
108
|
+
## It's your server. Do whatever you want with it.
|
|
109
|
+
|
|
110
|
+
Edit the code under `./api/paths` to **implement real behavior**, as we did in the JavaScript examples above. Store and retrieve data, perform calculations, filter / sort / paginate, whatever it takes the make the API real enough to support the development and testing of your front end.
|
|
111
|
+
|
|
112
|
+
- Use autocomplete as documentation. For example, when you're working on an operation that takes query parameters, type "`$.query.`" to list all of the available parameters.
|
|
113
|
+
- Stay in sync with changes in your OpenAPI / Swagger document. Counterfact will keep the type definitions up to date without overwriting your work.
|
|
114
|
+
- Get immediate feedback. Modules are hot reloaded so changes apply immediately without restarting the server or resetting its state.
|
|
115
|
+
- Interact with the running server using a REPL. For example, type `context.addPet({id: 2, name: "Fido"})` to add a pet to the store. Then view the pet you just added at `http://localhost:3100/pet/2`.
|
|
116
|
+
- Much more!
|
|
117
|
+
|
|
118
|
+
See the [Usage Guide](./docs/usage.md) for details.
|
|
72
119
|
|
|
73
120
|
---
|
|
74
121
|
|
|
75
|
-
Counterfact is brand new as of
|
|
122
|
+
Counterfact is brand new as of November 30, 2022. Please send feedback / questions to pmcelhaney@gmail.com or [create a new issue](https://github.com/pmcelhaney/counterfact/issues/new). If you like what you see, please give this project a star!
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```
|
package/bin/counterfact.js
CHANGED
|
@@ -20,32 +20,79 @@ async function main(source, destination) {
|
|
|
20
20
|
const basePath = nodePath.resolve(nodePath.join(process.cwd(), destination));
|
|
21
21
|
|
|
22
22
|
const openBrowser = options.open;
|
|
23
|
-
const includeSwagger = options.swagger || openBrowser;
|
|
24
|
-
const startServer = options.server || includeSwagger;
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
const url = `http://localhost:${options.port}`;
|
|
25
|
+
|
|
26
|
+
const guiUrl = `${url}/counterfact/`;
|
|
27
|
+
|
|
28
|
+
const { contextRegistry } = await start({
|
|
29
|
+
basePath,
|
|
30
|
+
port: options.port,
|
|
31
|
+
openApiPath: source,
|
|
32
|
+
includeSwaggerUi: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const waysToInteract = [
|
|
36
|
+
`Call the REST APIs at ${url} (with your front end app, curl, Postman, etc.)`,
|
|
37
|
+
`Change the implementation of the APIs by editing files under ${nodePath.join(
|
|
28
38
|
basePath,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
"paths"
|
|
40
|
+
)} (no need to restart)`,
|
|
41
|
+
`Use the GUI at ${guiUrl}`,
|
|
42
|
+
"Use the REPL below (type .counterfact for more information)",
|
|
43
|
+
];
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
const introduction = [
|
|
46
|
+
"",
|
|
47
|
+
"Welcome to Counterfact!",
|
|
48
|
+
"",
|
|
49
|
+
"Counterfact is a mock server used to develop and test your front end app.",
|
|
50
|
+
"There are several ways to poke and prod the server in order to make it behave the way you need for testing.",
|
|
51
|
+
"",
|
|
52
|
+
];
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
'\n\nStarting REPL... (start with `const context = loadContext("/")`)\n'
|
|
40
|
-
);
|
|
54
|
+
process.stdout.write(`${introduction.join("\n")}\n`);
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
waysToInteract.map((text, index) => `${index + 1}. ${text}`).join("\n")
|
|
58
|
+
);
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
process.stdout.write("\n\n");
|
|
61
|
+
|
|
62
|
+
process.stdout.write("Starting REPL...\n");
|
|
63
|
+
|
|
64
|
+
const replServer = repl.start("> ");
|
|
65
|
+
|
|
66
|
+
replServer.defineCommand("counterfact", {
|
|
67
|
+
help: "Get help with Counterfact",
|
|
68
|
+
|
|
69
|
+
action() {
|
|
70
|
+
process.stdout.write(
|
|
71
|
+
"This is a read-eval-print loop (REPL), the same as the one you get when you run node with no arguments.\n"
|
|
72
|
+
);
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
"Except that it's connected to the running server, which you can access with the following globals:\n\n"
|
|
75
|
+
);
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
"- loadContext('/some/path'): to access the context object for a given path\n"
|
|
78
|
+
);
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
"- context: the root context ( same as loadContext('/') )\n"
|
|
81
|
+
);
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
"\nFor more information, see https://counterfact.dev/docs/usage.html\n\n"
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
this.clearBufferedCommand();
|
|
87
|
+
this.displayPrompt();
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
replServer.context.loadContext = (path) => contextRegistry.find(path);
|
|
92
|
+
replServer.context.context = replServer.context.loadContext("/");
|
|
46
93
|
|
|
47
94
|
if (openBrowser) {
|
|
48
|
-
await open(
|
|
95
|
+
await open(guiUrl);
|
|
49
96
|
}
|
|
50
97
|
}
|
|
51
98
|
|
|
@@ -56,12 +103,8 @@ program
|
|
|
56
103
|
)
|
|
57
104
|
.argument("<openapi.yaml>", "path or URL to OpenAPI document")
|
|
58
105
|
.argument("[destination]", "path to generated code", ".")
|
|
59
|
-
.option("--serve", "start the server after generating code")
|
|
60
106
|
.option("--port <number>", "server port number", DEFAULT_PORT)
|
|
61
|
-
.option("--swagger", "include swagger-ui
|
|
62
|
-
.option(
|
|
63
|
-
"--open",
|
|
64
|
-
"open a browser to swagger-ui (implies --swagger and --serve)"
|
|
65
|
-
)
|
|
107
|
+
.option("--swagger", "include swagger-ui")
|
|
108
|
+
.option("--open", "open a browser")
|
|
66
109
|
.action(main)
|
|
67
110
|
.parse(process.argv);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Got 60 Seconds? Try me!
|
|
2
|
+
|
|
3
|
+
Copy the following command into your terminal. The only prerequisite is Node 16+.
|
|
4
|
+
|
|
5
|
+
```sh copy
|
|
6
|
+
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api --open
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What does that command do?
|
|
10
|
+
|
|
11
|
+
1. installs the `@latest` version of `counterfact`
|
|
12
|
+
2. reads an [OpenAPI 3](https://oai.github.io/Documentation/) document (`https://petstore3.swagger.io/api/v3/openapi.json`)
|
|
13
|
+
3. generates TypeScript files in the `api` directory
|
|
14
|
+
4. starts a server which implements the API
|
|
15
|
+
5. opens your browser to [Swagger UI](https://swagger.io/tools/swagger-ui/) (`--open`)
|
|
16
|
+
|
|
17
|
+
You can use Swagger to try out the auto-generated API. Out of the box, it returns random responses using metadata from the OpenAPI document. Edit the files under `./api/paths` to add more realistic behavior. There's no need to restart the server.
|
|
18
|
+
|
|
19
|
+
To learn more, see the [Usage Guide](./usage.md).
|
package/docs/usage.md
CHANGED
|
@@ -117,7 +117,9 @@ Note that each of these objects is typed so you can use autocomplete to identify
|
|
|
117
117
|
|
|
118
118
|
The `$.path` parameters are identified by dynamic sections of the path, i.e. `/groups/{groupName}/user/{userId}.ts`.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
<a id="context-object"></a>
|
|
121
|
+
|
|
122
|
+
### Working with state: the `$.context` object and `$.context.ts`
|
|
121
123
|
|
|
122
124
|
There's one more parameter we need to explore, the `$.context` object. It stands in for microservices, databases, and other entities with which the real API interacts. It looks something like this:
|
|
123
125
|
|
|
@@ -137,7 +139,7 @@ export const POST: HTTP_POST = ($) => {
|
|
|
137
139
|
};
|
|
138
140
|
```
|
|
139
141
|
|
|
140
|
-
The `context` object is defined in
|
|
142
|
+
The `context` object is defined in `$.context.js` in the same directory as the file that uses it. It's up to you to define the API for a context object. For example, your `$.context.js` file might look like this.
|
|
141
143
|
|
|
142
144
|
```ts
|
|
143
145
|
class PetStore {
|
|
@@ -157,7 +159,7 @@ class PetStore {
|
|
|
157
159
|
export default new PetStore();
|
|
158
160
|
```
|
|
159
161
|
|
|
160
|
-
By default, each
|
|
162
|
+
By default, each `$.context.ts` delegates to its parent directory, so you can define one context object in the root `$.context.ts` and use it everywhere.
|
|
161
163
|
|
|
162
164
|
> You can make the context objects do whatever you want, including things like writing to databases. But remember that Counterfact is meant for testing, so holding on to data between sessions is an anti-pattern. Keeping everything in memory also makes it fast.
|
|
163
165
|
|
|
@@ -174,23 +176,61 @@ Hot reloading supports one of Counterfact's key design goals. While developing a
|
|
|
174
176
|
|
|
175
177
|
In such cases, we want to be sure the front end code responds appropriately. Getting a real server to do what we need to test front end code is usually difficult if not impossible. Counterfact is optimized to make bending the server's behavior to suit a test case as painless as possible, in both manual and automated tests.
|
|
176
178
|
|
|
179
|
+
## REPL without a Pause ⏯
|
|
180
|
+
|
|
181
|
+
Another way to explore counterfactuals in real time is to interact with the running server via the read-eval-print loop (REPL), in the same way that you interact with running UI code in your browser's developer tools console. If you look in the terminal after starting Counterfact you should see a prompt like this:
|
|
182
|
+
|
|
183
|
+
```txt
|
|
184
|
+
Welcome to Counterfact!
|
|
185
|
+
|
|
186
|
+
Counterfact is a mock server used to develop and test your front end app.
|
|
187
|
+
There are several ways to poke and prod the server in order to make it behave the way you need for testing.
|
|
188
|
+
|
|
189
|
+
1. Call the REST APIs at http://localhost:3100 (with your front end app, curl, Postman, etc.)
|
|
190
|
+
2. Change the implementation of the APIs by editing files under /Users/pmcelhaney/code/counterfact/out/paths (no need to restart)
|
|
191
|
+
3. Use the GUI at http://localhost:3100
|
|
192
|
+
4. Use the REPL below (type .counterfact for more information)
|
|
193
|
+
|
|
194
|
+
>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
At the `> ` prompt, you can enter JavaScript code to interact with the live [context object](#context-object). For example, here's a quick way to add a pet to the store.
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
context.addPet({ name: "Fluffy", photoUrls: [] });
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Or add 100 pets:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
for (i = 0; i < 100; i++) context.addPet({ name: `Pet ${i}`, photoUrls: [] });
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Or get a list of pets whose names start with "F"
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
context.pets.find((pet) => pet.name.startsWith("F"));
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Using the REPL is a lot faster (and more fun) than wrangling config files and SQL and whatever else it takes to a real back end into the states you need to test your UI flows.
|
|
216
|
+
|
|
177
217
|
## No Cap Recap 🧢
|
|
178
218
|
|
|
179
|
-
|
|
219
|
+
Using convention over configuration and automatically generated types, Counterfact allows front-end developers to quickly build fake REST APIs for prototype and testing purposes.
|
|
180
220
|
|
|
181
221
|
- Given an OpenAPI document, you can generate working TypeScript code and start up a server in seconds.
|
|
182
222
|
- By default, the server returns random responses based on metadata in the OpenAPI document (e.g. it uses examples where provided).
|
|
183
223
|
- Each endpoint is represented by a TypeScript file where the path to the file corresponds to the path of the endpoint.
|
|
184
224
|
- You can change the implementation at any time by changing these files.
|
|
185
225
|
- You can and should commit the generated code to source control. Files you change will not be overwritten when you start the server again. (The _types_ will be updated if the OpenAPI document changes, but you shouldn't need to edit the type definitions by hand.)
|
|
186
|
-
- Put behavior in
|
|
226
|
+
- Put behavior in `$.context.ts` files. These are created for you, but you should rewrite them to suit your needs. (At least update the root `$.context.ts` file.)
|
|
227
|
+
- Use the REPL to manipulate the server's state at runtime
|
|
187
228
|
|
|
188
229
|
## We're Just Getting Started 🐣
|
|
189
230
|
|
|
190
|
-
|
|
231
|
+
More features are coming soon:
|
|
191
232
|
|
|
192
233
|
- Integrate Counterfact into your workflow (Express, Koa, Webpack Dev Server, etc.)
|
|
193
|
-
- Use a REPL to interact with the context objects while the server is running
|
|
194
234
|
- Use Counterfact in your automated tests
|
|
195
235
|
- Record API calls while testing the front end manually and reuse those calls in automated tests (ala [Playwright](https://playwright.dev/))
|
|
196
236
|
- Use [HAR](https://toolbox.googleapps.com/apps/har_analyzer/) files to recreate scenarios / bugs encountered by real users
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counterfact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "a library for building a fake REST API for testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/counterfact.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@stryker-mutator/jest-runner": "6.3.0",
|
|
42
42
|
"@types/koa": "2.13.5",
|
|
43
43
|
"@types/koa-static": "^4.0.2",
|
|
44
|
-
"eslint": "8.
|
|
44
|
+
"eslint": "8.28.0",
|
|
45
45
|
"eslint-config-hardcore": "25.1.0",
|
|
46
46
|
"eslint-formatter-github-annotations": "0.1.0",
|
|
47
47
|
"eslint-import-resolver-typescript": "^3.2.5",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"@types/json-schema": "^7.0.11",
|
|
62
62
|
"chokidar": "^3.5.3",
|
|
63
63
|
"commander": "^9.4.0",
|
|
64
|
+
"fetch": "^1.1.0",
|
|
64
65
|
"fs-extra": "^10.1.0",
|
|
65
66
|
"handlebars": "^4.7.7",
|
|
66
67
|
"js-yaml": "^4.1.0",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"jsonwebtoken": "^8.5.1",
|
|
70
71
|
"koa": "^2.13.4",
|
|
71
72
|
"koa-bodyparser": "^4.3.0",
|
|
73
|
+
"koa-proxy": "^1.0.0-alpha.3",
|
|
72
74
|
"koa2-swagger-ui": "^5.6.0",
|
|
73
75
|
"node-fetch": "^3.2.10",
|
|
74
76
|
"open": "^8.4.0",
|
|
@@ -3,37 +3,43 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<title>Counterfact</title>
|
|
5
5
|
<meta charset="utf-8">
|
|
6
|
-
<meta name="description" content="Counterfact
|
|
6
|
+
<meta name="description" content="Counterfact Dashboard" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
-
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
<style type="text/css">
|
|
10
|
+
body {
|
|
11
|
+
font-family: Helvetica, Arial, sans-serif;
|
|
12
|
+
max-width: 800px;
|
|
13
|
+
margin: 0 auto;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
13
16
|
<body>
|
|
14
17
|
|
|
18
|
+
<h1>Counterfact</h1>
|
|
15
19
|
|
|
20
|
+
<p>
|
|
21
|
+
This server implements the OpenAPI spec at <a href="{{openApiHref}}">{{openApiPath}}</a>.
|
|
22
|
+
You can try it out using your favorite REST client (curl, Postman, etc.) or browse the interactive API documentation in <a href="./swagger">Swagger UI</a> or <a href="./rapidoc">Rapidoc</a>.
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
</p>
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<a
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<a slot="delete-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
|
|
31
|
-
<a slot="patch-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
|
|
26
|
+
<p>
|
|
27
|
+
The TypeScript code for each path can be found under <code>{{basePath}}</code>.
|
|
28
|
+
Click on a path below to edit its corresponding TypeScript file in VSCode.
|
|
29
|
+
For more information, see the <a href="https://counterfact.dev/docs/usage.html">usage guide</a>.
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<ul>
|
|
33
|
+
{{#each routes as |route|}}
|
|
34
|
+
<li><a href="vscode://file/{{../basePath}}/paths/{{route}}.ts">{{route}}</a></li>
|
|
35
|
+
{{/each}}
|
|
36
|
+
</ul>
|
|
32
37
|
|
|
33
|
-
{{/each}}
|
|
34
|
-
</rapi-doc>
|
|
35
38
|
|
|
39
|
+
<hr>
|
|
36
40
|
|
|
41
|
+
<p>👋 Thanks for trying <a href="https://github.com/pmcelhaney/counterfact/">Counterfact</a>! Let me know how it goes at <a href="mailto:pmcelhaney@gmail.com">pmcelhaney@gmail.com</a> or <a href="https://github.com/pmcelhaney/counterfact/issues/new">Github</a> - <a href="https://patrickmcelhaney.com">Patrick</a></p>
|
|
37
42
|
|
|
43
|
+
|
|
38
44
|
</body>
|
|
39
45
|
</html>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Counterfact</title>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="description" content="Rapidoc" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
<rapi-doc spec-url="/counterfact/openapi" show-method-in-nav-bar="as-colored-block" nav-active-item-marker="left-bar" use-path-in-nav-bar="true">
|
|
15
|
+
|
|
16
|
+
<div slot="overview" style="background-color: #eee">
|
|
17
|
+
<p>Put an explanation of Counterfact here.</p>
|
|
18
|
+
<a
|
|
19
|
+
href="https://github.com/pmcelhaney/counterfact/blob/main/docs/usage.md#generated-code-"
|
|
20
|
+
>How does this work?</a
|
|
21
|
+
>
|
|
22
|
+
</div>
|
|
23
|
+
{{#each routes as |route|}}
|
|
24
|
+
<a slot="get-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
|
|
25
|
+
<a slot="put-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
|
|
26
|
+
<a slot="post-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
|
|
27
|
+
<a slot="delete-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
|
|
28
|
+
<a slot="patch-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
|
|
29
|
+
|
|
30
|
+
{{/each}}
|
|
31
|
+
</rapi-doc>
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
package/src/server/dispatcher.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import Accept from "@hapi/accept";
|
|
2
|
+
// eslint-disable-next-line no-shadow
|
|
3
|
+
import fetch, { Headers } from "node-fetch";
|
|
2
4
|
|
|
3
5
|
import { createResponseBuilder } from "./response-builder.js";
|
|
4
6
|
import { Tools } from "./tools.js";
|
|
@@ -30,6 +32,9 @@ function parameterTypes(parameters) {
|
|
|
30
32
|
export class Dispatcher {
|
|
31
33
|
registry;
|
|
32
34
|
|
|
35
|
+
// alias the fetch function so we can mock it in tests
|
|
36
|
+
fetch = fetch;
|
|
37
|
+
|
|
33
38
|
constructor(registry, contextRegistry, openApiDocument) {
|
|
34
39
|
this.registry = registry;
|
|
35
40
|
this.contextRegistry = contextRegistry;
|
|
@@ -61,6 +66,32 @@ export class Dispatcher {
|
|
|
61
66
|
method
|
|
62
67
|
)
|
|
63
68
|
),
|
|
69
|
+
|
|
70
|
+
proxy: async (hostOrOptions) => {
|
|
71
|
+
const options =
|
|
72
|
+
typeof hostOrOptions === "string"
|
|
73
|
+
? { host: hostOrOptions }
|
|
74
|
+
: hostOrOptions;
|
|
75
|
+
|
|
76
|
+
const fetchResponse = await this.fetch(options.host, {
|
|
77
|
+
method,
|
|
78
|
+
path,
|
|
79
|
+
headers: new Headers(headers),
|
|
80
|
+
query,
|
|
81
|
+
...options,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const responseHeaders = Object.fromEntries(
|
|
85
|
+
fetchResponse.headers.entries()
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
status: fetchResponse.status,
|
|
90
|
+
contentType: responseHeaders["content-type"] ?? "unknown/unknown",
|
|
91
|
+
headers: responseHeaders,
|
|
92
|
+
body: await fetchResponse.text(),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
64
95
|
});
|
|
65
96
|
|
|
66
97
|
const normalizedResponse = this.normalizeResponse(
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import koaProxy from "koa-proxy";
|
|
2
|
+
|
|
1
3
|
const HTTP_STATUS_CODE_OK = 200;
|
|
2
4
|
|
|
3
|
-
export function koaMiddleware(dispatcher) {
|
|
4
|
-
return async function middleware(ctx) {
|
|
5
|
-
const { method, path, body, query } = ctx.request;
|
|
5
|
+
export function koaMiddleware(dispatcher, options = {}, proxy = koaProxy) {
|
|
6
|
+
return async function middleware(ctx, next) {
|
|
7
|
+
const { method, path, headers, body, query } = ctx.request;
|
|
8
|
+
|
|
9
|
+
if (options.proxyUrl) {
|
|
10
|
+
return proxy({ host: options.proxyUrl })(ctx, next);
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
const response = await dispatcher.request({
|
|
8
14
|
method,
|
|
9
15
|
path,
|
|
16
|
+
headers,
|
|
10
17
|
body,
|
|
11
18
|
query,
|
|
12
19
|
});
|
|
@@ -15,5 +22,7 @@ export function koaMiddleware(dispatcher) {
|
|
|
15
22
|
ctx.body = response.body;
|
|
16
23
|
ctx.status = response.status ?? HTTP_STATUS_CODE_OK;
|
|
17
24
|
/* eslint-enable require-atomic-updates */
|
|
25
|
+
|
|
26
|
+
return undefined;
|
|
18
27
|
};
|
|
19
28
|
}
|