@stuntman/server 0.1.5 → 0.1.6
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/dist/api/api.js +7 -13
- package/dist/api/utils.js +18 -27
- package/dist/api/validators.js +40 -34
- package/dist/api/webgui/rules.pug +3 -1
- package/dist/mock.js +14 -8
- package/package.json +6 -4
- package/src/api/api.ts +7 -13
- package/src/api/utils.ts +26 -26
- package/src/api/validators.ts +44 -35
- package/src/api/webgui/rules.pug +3 -1
- package/src/mock.ts +13 -6
package/dist/api/api.js
CHANGED
|
@@ -76,17 +76,17 @@ class API {
|
|
|
76
76
|
res.send();
|
|
77
77
|
});
|
|
78
78
|
this.apiApp.get('/traffic', this.authReadOnly, (req, res) => {
|
|
79
|
-
const serializedTraffic =
|
|
80
|
-
for (const
|
|
81
|
-
serializedTraffic
|
|
79
|
+
const serializedTraffic = [];
|
|
80
|
+
for (const value of this.trafficStore.values()) {
|
|
81
|
+
serializedTraffic.push(value);
|
|
82
82
|
}
|
|
83
83
|
res.json(serializedTraffic);
|
|
84
84
|
});
|
|
85
85
|
this.apiApp.get('/traffic/:ruleIdOrLabel', this.authReadOnly, (req, res) => {
|
|
86
|
-
const serializedTraffic =
|
|
87
|
-
for (const
|
|
86
|
+
const serializedTraffic = [];
|
|
87
|
+
for (const value of this.trafficStore.values()) {
|
|
88
88
|
if (value.mockRuleId === req.params.ruleIdOrLabel || (value.labels || []).includes(req.params.ruleIdOrLabel)) {
|
|
89
|
-
serializedTraffic
|
|
89
|
+
serializedTraffic.push(value);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
res.json(serializedTraffic);
|
|
@@ -150,13 +150,7 @@ class API {
|
|
|
150
150
|
id: rule.id,
|
|
151
151
|
matches: rule.matches,
|
|
152
152
|
ttlSeconds: rule.ttlSeconds,
|
|
153
|
-
|
|
154
|
-
actions: {
|
|
155
|
-
...(rule.actions.mockResponse
|
|
156
|
-
? { mockResponse: rule.actions.mockResponse }
|
|
157
|
-
: { modifyRequest: rule.actions.modifyRequest, modifyResponse: rule.actions.modifyResponse }),
|
|
158
|
-
},
|
|
159
|
-
}),
|
|
153
|
+
actions: rule.actions,
|
|
160
154
|
...(rule.disableAfterUse !== undefined && { disableAfterUse: rule.disableAfterUse }),
|
|
161
155
|
...(rule.isEnabled !== undefined && { isEnabled: rule.isEnabled }),
|
|
162
156
|
...(rule.labels !== undefined && { labels: rule.labels }),
|
package/dist/api/utils.js
CHANGED
|
@@ -15,6 +15,7 @@ const deserializeRule = (serializedRule) => {
|
|
|
15
15
|
id: serializedRule.id,
|
|
16
16
|
matches: (req) => new Function('____arg0', serializedRule.matches.remoteFn)(req),
|
|
17
17
|
ttlSeconds: serializedRule.ttlSeconds,
|
|
18
|
+
actions: { mockResponse: { status: 200 } },
|
|
18
19
|
...(serializedRule.disableAfterUse !== undefined && { disableAfterUse: serializedRule.disableAfterUse }),
|
|
19
20
|
...(serializedRule.removeAfterUse !== undefined && { removeAfterUse: serializedRule.removeAfterUse }),
|
|
20
21
|
...(serializedRule.labels !== undefined && { labels: serializedRule.labels }),
|
|
@@ -22,33 +23,23 @@ const deserializeRule = (serializedRule) => {
|
|
|
22
23
|
...(serializedRule.isEnabled !== undefined && { isEnabled: serializedRule.isEnabled }),
|
|
23
24
|
...(serializedRule.storeTraffic !== undefined && { storeTraffic: serializedRule.storeTraffic }),
|
|
24
25
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return new Function('____arg0', ((_a = serializedRule.actions) === null || _a === void 0 ? void 0 : _a.modifyRequest).remoteFn)(req);
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (serializedRule.actions.modifyResponse) {
|
|
46
|
-
rule.actions.modifyResponse = (req, res) => {
|
|
47
|
-
var _a;
|
|
48
|
-
return new Function('____arg0', '____arg1', ((_a = serializedRule.actions) === null || _a === void 0 ? void 0 : _a.modifyResponse).remoteFn)(req, res);
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
26
|
+
// TODO store the original localFn and variables sent from client for web UI editing maybe
|
|
27
|
+
if (serializedRule.actions.mockResponse) {
|
|
28
|
+
rule.actions = {
|
|
29
|
+
mockResponse: 'remoteFn' in serializedRule.actions.mockResponse
|
|
30
|
+
? (req) => new Function('____arg0', serializedRule.actions.mockResponse.remoteFn)(req)
|
|
31
|
+
: serializedRule.actions.mockResponse,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
rule.actions = {
|
|
36
|
+
...(serializedRule.actions.modifyRequest && {
|
|
37
|
+
modifyRequest: ((req) => new Function('____arg0', serializedRule.actions.modifyRequest.remoteFn)(req)),
|
|
38
|
+
}),
|
|
39
|
+
...(serializedRule.actions.modifyResponse && {
|
|
40
|
+
modifyResponse: ((req, res) => new Function('____arg0', '____arg1', serializedRule.actions.modifyResponse.remoteFn)(req, res)),
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
52
43
|
}
|
|
53
44
|
shared_1.logger.debug(rule, 'deserialized rule');
|
|
54
45
|
return rule;
|
package/dist/api/validators.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.validateDeserializedRule = exports.validateSerializedRuleProperties = void 0;
|
|
4
4
|
const shared_1 = require("@stuntman/shared");
|
|
5
5
|
const validateSerializedRuleProperties = (rule) => {
|
|
6
|
-
var _a;
|
|
7
6
|
if (!rule) {
|
|
8
7
|
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid serialized rule' });
|
|
9
8
|
}
|
|
@@ -16,43 +15,50 @@ const validateSerializedRuleProperties = (rule) => {
|
|
|
16
15
|
if (rule.priority && typeof rule.priority !== 'number') {
|
|
17
16
|
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.priority' });
|
|
18
17
|
}
|
|
19
|
-
if (typeof rule.actions !== '
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (typeof rule.actions !== 'object') {
|
|
19
|
+
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions - not an object' });
|
|
20
|
+
}
|
|
21
|
+
if (rule.actions.proxyPass !== true &&
|
|
22
|
+
typeof rule.actions.mockResponse !== 'object' &&
|
|
23
|
+
typeof rule.actions.modifyRequest !== 'object' &&
|
|
24
|
+
typeof rule.actions.modifyResponse !== 'object') {
|
|
25
|
+
throw new shared_1.AppError({
|
|
26
|
+
httpCode: shared_1.HttpCode.BAD_REQUEST,
|
|
27
|
+
message: 'invalid rule.actions - missing one of: proxyPass, mockResponse, modifyRequest, modifyResponse',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (typeof rule.actions.mockResponse !== 'undefined') {
|
|
31
|
+
if (typeof rule.actions.mockResponse !== 'object') {
|
|
32
|
+
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
22
33
|
}
|
|
23
|
-
if (typeof rule.actions.mockResponse !== '
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
if ('remoteFn' in rule.actions.mockResponse && typeof rule.actions.mockResponse.remoteFn !== 'string') {
|
|
35
|
+
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
36
|
+
}
|
|
37
|
+
else if ('status' in rule.actions.mockResponse) {
|
|
38
|
+
if (typeof rule.actions.mockResponse.status !== 'number') {
|
|
39
|
+
throw new shared_1.AppError({
|
|
40
|
+
httpCode: shared_1.HttpCode.BAD_REQUEST,
|
|
41
|
+
message: 'invalid rule.actions.mockResponse.status',
|
|
42
|
+
});
|
|
26
43
|
}
|
|
27
|
-
if (
|
|
28
|
-
|
|
44
|
+
if (typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
|
|
45
|
+
(!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
|
|
46
|
+
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))) {
|
|
47
|
+
throw new shared_1.AppError({
|
|
48
|
+
httpCode: shared_1.HttpCode.BAD_REQUEST,
|
|
49
|
+
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
50
|
+
});
|
|
29
51
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
throw new shared_1.AppError({
|
|
33
|
-
httpCode: shared_1.HttpCode.BAD_REQUEST,
|
|
34
|
-
message: 'invalid rule.actions.mockResponse.status',
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
if (typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
|
|
38
|
-
(!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
|
|
39
|
-
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))) {
|
|
40
|
-
throw new shared_1.AppError({
|
|
41
|
-
httpCode: shared_1.HttpCode.BAD_REQUEST,
|
|
42
|
-
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
46
|
-
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
47
|
-
}
|
|
52
|
+
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
53
|
+
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
+
}
|
|
57
|
+
if (typeof rule.actions.modifyRequest !== 'undefined' && typeof rule.actions.modifyRequest.remoteFn !== 'string') {
|
|
58
|
+
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyRequest' });
|
|
59
|
+
}
|
|
60
|
+
if (typeof rule.actions.modifyResponse !== 'undefined' && typeof rule.actions.modifyResponse.remoteFn !== 'string') {
|
|
61
|
+
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyResponse' });
|
|
56
62
|
}
|
|
57
63
|
if (typeof rule.disableAfterUse !== 'undefined' &&
|
|
58
64
|
typeof rule.disableAfterUse !== 'boolean' &&
|
|
@@ -68,7 +74,7 @@ const validateSerializedRuleProperties = (rule) => {
|
|
|
68
74
|
(!Array.isArray(rule.labels) || rule.labels.some((label) => typeof label !== 'string'))) {
|
|
69
75
|
throw new shared_1.AppError({ httpCode: shared_1.HttpCode.BAD_REQUEST, message: 'invalid rule.labels' });
|
|
70
76
|
}
|
|
71
|
-
if (
|
|
77
|
+
if (rule.actions.mockResponse && rule.actions.modifyResponse) {
|
|
72
78
|
throw new shared_1.AppError({
|
|
73
79
|
httpCode: shared_1.HttpCode.BAD_REQUEST,
|
|
74
80
|
message: 'rule.actions.mockResponse and rule.actions.modifyResponse are mutually exclusive',
|
|
@@ -120,6 +120,8 @@ html
|
|
|
120
120
|
body: ruleFunctionText + '\n return STUNTMAN_RULE;',
|
|
121
121
|
}).then((response) => {
|
|
122
122
|
if (response.ok) {
|
|
123
|
+
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?ruleId=${encodeURIComponent(newId)}`;
|
|
124
|
+
window.history.pushState({ path: newUrl }, '', newUrl);
|
|
123
125
|
window.location.reload();
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
@@ -133,7 +135,7 @@ html
|
|
|
133
135
|
|
|
134
136
|
window.newRule = () => {
|
|
135
137
|
const ruleId = uuidv4();
|
|
136
|
-
const emptyRule = `import type * as Stuntman from \'stuntman\';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600 };`;
|
|
138
|
+
const emptyRule = `import type * as Stuntman from \'stuntman\';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600, actions: { mockResponse: { status: '200', body: '${ruleId}' }} };`;
|
|
137
139
|
models[ruleId] = monaco.editor.createModel(emptyRule, 'typescript', `file:///${ruleId}.ts`);
|
|
138
140
|
const ruleKeyNode = document.getElementById('ruleKeys').firstChild;
|
|
139
141
|
const ruleKeyNodeClone = ruleKeyNode.cloneNode(true);
|
package/dist/mock.js
CHANGED
|
@@ -79,7 +79,6 @@ class Mock {
|
|
|
79
79
|
? null
|
|
80
80
|
: new ipUtils_1.IPUtils({ mockUuid: this.mockUuid, externalDns: this.options.mock.externalDns });
|
|
81
81
|
this.requestHandler = async (req, res) => {
|
|
82
|
-
var _a, _b, _c, _d, _e;
|
|
83
82
|
const ctx = requestContext_1.default.get(req);
|
|
84
83
|
const requestUuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)();
|
|
85
84
|
const timestamp = Date.now();
|
|
@@ -115,7 +114,7 @@ class Mock {
|
|
|
115
114
|
if (matchingRule) {
|
|
116
115
|
mockEntry.mockRuleId = matchingRule.id;
|
|
117
116
|
mockEntry.labels = matchingRule.labels;
|
|
118
|
-
if (
|
|
117
|
+
if (matchingRule.actions.mockResponse) {
|
|
119
118
|
const staticResponse = typeof matchingRule.actions.mockResponse === 'function'
|
|
120
119
|
? matchingRule.actions.mockResponse(mockEntry.modifiedRequest)
|
|
121
120
|
: matchingRule.actions.mockResponse;
|
|
@@ -134,8 +133,8 @@ class Mock {
|
|
|
134
133
|
// static response blocks any further processing
|
|
135
134
|
return;
|
|
136
135
|
}
|
|
137
|
-
if (
|
|
138
|
-
mockEntry.modifiedRequest =
|
|
136
|
+
if (matchingRule.actions.modifyRequest) {
|
|
137
|
+
mockEntry.modifiedRequest = matchingRule.actions.modifyRequest(mockEntry.modifiedRequest);
|
|
139
138
|
shared_1.logger.debug({ ...logContext, modifiedRequest: mockEntry.modifiedRequest }, 'modified original request');
|
|
140
139
|
}
|
|
141
140
|
}
|
|
@@ -154,7 +153,14 @@ class Mock {
|
|
|
154
153
|
shared_1.logger.warn({ ...logContext, error }, `error trying to resolve IP for "${hostname}"`);
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
|
-
const originalResponse =
|
|
156
|
+
const originalResponse = this.options.mock.disableProxy
|
|
157
|
+
? {
|
|
158
|
+
timestamp: Date.now(),
|
|
159
|
+
body: undefined,
|
|
160
|
+
rawHeaders: new shared_1.RawHeaders(),
|
|
161
|
+
status: 404,
|
|
162
|
+
}
|
|
163
|
+
: await this.proxyRequest(req, mockEntry, logContext);
|
|
158
164
|
shared_1.logger.debug({ ...logContext, originalResponse }, 'received response');
|
|
159
165
|
mockEntry.originalResponse = originalResponse;
|
|
160
166
|
let modifedResponse = {
|
|
@@ -169,8 +175,8 @@ class Mock {
|
|
|
169
175
|
];
|
|
170
176
|
})),
|
|
171
177
|
};
|
|
172
|
-
if (
|
|
173
|
-
modifedResponse =
|
|
178
|
+
if (matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions.modifyResponse) {
|
|
179
|
+
modifedResponse = matchingRule === null || matchingRule === void 0 ? void 0 : matchingRule.actions.modifyResponse(mockEntry.modifiedRequest, originalResponse);
|
|
174
180
|
shared_1.logger.debug({ ...logContext, modifedResponse }, 'modified response');
|
|
175
181
|
}
|
|
176
182
|
mockEntry.modifiedResponse = modifedResponse;
|
|
@@ -200,7 +206,7 @@ class Mock {
|
|
|
200
206
|
next();
|
|
201
207
|
});
|
|
202
208
|
this.mockApp.all(/.*/, this.requestHandler);
|
|
203
|
-
this.mockApp.use((error, req, res
|
|
209
|
+
this.mockApp.use((error, req, res) => {
|
|
204
210
|
const ctx = requestContext_1.default.get(req);
|
|
205
211
|
const uuid = (ctx === null || ctx === void 0 ? void 0 : ctx.uuid) || (0, uuid_1.v4)();
|
|
206
212
|
shared_1.logger.error({ message: error.message, stack: error.stack, name: error.name, uuid }, 'unexpected error');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stuntman/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Stuntman - HTTP proxy / mock server with API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -41,17 +41,19 @@
|
|
|
41
41
|
"pug": "3.0.2",
|
|
42
42
|
"serialize-javascript": "6.0.1",
|
|
43
43
|
"ts-import": "4.0.0-beta.10",
|
|
44
|
-
"typescript": "4.9.5",
|
|
45
44
|
"undici": "5.20.0",
|
|
46
45
|
"uuid": "9.0.0"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
48
|
+
"@jest/globals": "29.4.3",
|
|
49
49
|
"@prettier/plugin-pug": "2.4.1",
|
|
50
50
|
"@types/express": "4.17.17",
|
|
51
51
|
"@types/glob": "8.1.0",
|
|
52
52
|
"@types/serialize-javascript": "5.0.2",
|
|
53
53
|
"@types/uuid": "9.0.0",
|
|
54
|
-
"
|
|
54
|
+
"jest": "29.4.3",
|
|
55
|
+
"prettier": "2.8.4",
|
|
56
|
+
"typescript": "4.9.5"
|
|
55
57
|
},
|
|
56
58
|
"bin": {
|
|
57
59
|
"stuntman": "./dist/bin/stuntman.js"
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
"CHANGELOG.md"
|
|
65
67
|
],
|
|
66
68
|
"scripts": {
|
|
67
|
-
"test": "
|
|
69
|
+
"test": "SUPPRESS_NO_CONFIG_WARNING=1 jest",
|
|
68
70
|
"clean": "rm -fr dist",
|
|
69
71
|
"build": "tsc && cp -rv src/api/webgui dist/api",
|
|
70
72
|
"lint": "prettier --check . && eslint . --ext ts",
|
package/src/api/api.ts
CHANGED
|
@@ -103,18 +103,18 @@ export class API {
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
this.apiApp.get('/traffic', this.authReadOnly, (req, res) => {
|
|
106
|
-
const serializedTraffic:
|
|
107
|
-
for (const
|
|
108
|
-
serializedTraffic
|
|
106
|
+
const serializedTraffic: Stuntman.LogEntry[] = [];
|
|
107
|
+
for (const value of this.trafficStore.values()) {
|
|
108
|
+
serializedTraffic.push(value);
|
|
109
109
|
}
|
|
110
110
|
res.json(serializedTraffic);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
this.apiApp.get('/traffic/:ruleIdOrLabel', this.authReadOnly, (req, res) => {
|
|
114
|
-
const serializedTraffic:
|
|
115
|
-
for (const
|
|
114
|
+
const serializedTraffic: Stuntman.LogEntry[] = [];
|
|
115
|
+
for (const value of this.trafficStore.values()) {
|
|
116
116
|
if (value.mockRuleId === req.params.ruleIdOrLabel || (value.labels || []).includes(req.params.ruleIdOrLabel)) {
|
|
117
|
-
serializedTraffic
|
|
117
|
+
serializedTraffic.push(value);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
res.json(serializedTraffic);
|
|
@@ -190,13 +190,7 @@ export class API {
|
|
|
190
190
|
id: rule.id,
|
|
191
191
|
matches: rule.matches,
|
|
192
192
|
ttlSeconds: rule.ttlSeconds,
|
|
193
|
-
|
|
194
|
-
actions: {
|
|
195
|
-
...(rule.actions.mockResponse
|
|
196
|
-
? { mockResponse: rule.actions.mockResponse }
|
|
197
|
-
: { modifyRequest: rule.actions.modifyRequest, modifyResponse: rule.actions.modifyResponse }),
|
|
198
|
-
},
|
|
199
|
-
}),
|
|
193
|
+
actions: rule.actions,
|
|
200
194
|
...(rule.disableAfterUse !== undefined && { disableAfterUse: rule.disableAfterUse }),
|
|
201
195
|
...(rule.isEnabled !== undefined && { isEnabled: rule.isEnabled }),
|
|
202
196
|
...(rule.labels !== undefined && { labels: rule.labels }),
|
package/src/api/utils.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const deserializeRule = (serializedRule: Stuntman.SerializedRule): Stuntm
|
|
|
11
11
|
id: serializedRule.id,
|
|
12
12
|
matches: (req: Stuntman.Request) => new Function('____arg0', serializedRule.matches.remoteFn)(req),
|
|
13
13
|
ttlSeconds: serializedRule.ttlSeconds,
|
|
14
|
+
actions: { mockResponse: { status: 200 } },
|
|
14
15
|
...(serializedRule.disableAfterUse !== undefined && { disableAfterUse: serializedRule.disableAfterUse }),
|
|
15
16
|
...(serializedRule.removeAfterUse !== undefined && { removeAfterUse: serializedRule.removeAfterUse }),
|
|
16
17
|
...(serializedRule.labels !== undefined && { labels: serializedRule.labels }),
|
|
@@ -18,37 +19,36 @@ export const deserializeRule = (serializedRule: Stuntman.SerializedRule): Stuntm
|
|
|
18
19
|
...(serializedRule.isEnabled !== undefined && { isEnabled: serializedRule.isEnabled }),
|
|
19
20
|
...(serializedRule.storeTraffic !== undefined && { storeTraffic: serializedRule.storeTraffic }),
|
|
20
21
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
mockResponse
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
rule.actions.modifyRequest = (req: Stuntman.Request) =>
|
|
22
|
+
// TODO store the original localFn and variables sent from client for web UI editing maybe
|
|
23
|
+
if (serializedRule.actions.mockResponse) {
|
|
24
|
+
rule.actions = {
|
|
25
|
+
mockResponse:
|
|
26
|
+
'remoteFn' in serializedRule.actions.mockResponse
|
|
27
|
+
? (req: Stuntman.Request) =>
|
|
28
|
+
new Function(
|
|
29
|
+
'____arg0',
|
|
30
|
+
(serializedRule.actions.mockResponse as Stuntman.SerializedRemotableFunction).remoteFn
|
|
31
|
+
)(req)
|
|
32
|
+
: serializedRule.actions.mockResponse,
|
|
33
|
+
};
|
|
34
|
+
} else {
|
|
35
|
+
rule.actions = {
|
|
36
|
+
...(serializedRule.actions.modifyRequest && {
|
|
37
|
+
modifyRequest: ((req: Stuntman.Request) =>
|
|
38
38
|
new Function(
|
|
39
39
|
'____arg0',
|
|
40
|
-
(serializedRule.actions
|
|
41
|
-
)(req)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
(serializedRule.actions.modifyRequest as Stuntman.SerializedRemotableFunction).remoteFn
|
|
41
|
+
)(req)) as Stuntman.RequestManipulationFn,
|
|
42
|
+
}),
|
|
43
|
+
...(serializedRule.actions.modifyResponse && {
|
|
44
|
+
modifyResponse: ((req: Stuntman.Request, res: Stuntman.Response) =>
|
|
45
45
|
new Function(
|
|
46
46
|
'____arg0',
|
|
47
47
|
'____arg1',
|
|
48
|
-
(serializedRule.actions
|
|
49
|
-
)(req, res)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
48
|
+
(serializedRule.actions.modifyResponse as Stuntman.SerializedRemotableFunction).remoteFn
|
|
49
|
+
)(req, res)) as Stuntman.ResponseManipulationFn,
|
|
50
|
+
}),
|
|
51
|
+
} as Stuntman.Actions;
|
|
52
52
|
}
|
|
53
53
|
logger.debug(rule, 'deserialized rule');
|
|
54
54
|
return rule;
|
package/src/api/validators.ts
CHANGED
|
@@ -14,44 +14,53 @@ export const validateSerializedRuleProperties = (rule: Stuntman.SerializedRule):
|
|
|
14
14
|
if (rule.priority && typeof rule.priority !== 'number') {
|
|
15
15
|
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.priority' });
|
|
16
16
|
}
|
|
17
|
-
if (typeof rule.actions !== '
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
if (typeof rule.actions !== 'object') {
|
|
18
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions - not an object' });
|
|
19
|
+
}
|
|
20
|
+
if (
|
|
21
|
+
rule.actions.proxyPass !== true &&
|
|
22
|
+
typeof rule.actions.mockResponse !== 'object' &&
|
|
23
|
+
typeof rule.actions.modifyRequest !== 'object' &&
|
|
24
|
+
typeof rule.actions.modifyResponse !== 'object'
|
|
25
|
+
) {
|
|
26
|
+
throw new AppError({
|
|
27
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
28
|
+
message: 'invalid rule.actions - missing one of: proxyPass, mockResponse, modifyRequest, modifyResponse',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (typeof rule.actions.mockResponse !== 'undefined') {
|
|
32
|
+
if (typeof rule.actions.mockResponse !== 'object') {
|
|
33
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
20
34
|
}
|
|
21
|
-
if (typeof rule.actions.mockResponse !== '
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
if ('remoteFn' in rule.actions.mockResponse && typeof rule.actions.mockResponse.remoteFn !== 'string') {
|
|
36
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse' });
|
|
37
|
+
} else if ('status' in rule.actions.mockResponse) {
|
|
38
|
+
if (typeof rule.actions.mockResponse.status !== 'number') {
|
|
39
|
+
throw new AppError({
|
|
40
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
41
|
+
message: 'invalid rule.actions.mockResponse.status',
|
|
42
|
+
});
|
|
24
43
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))
|
|
38
|
-
) {
|
|
39
|
-
throw new AppError({
|
|
40
|
-
httpCode: HttpCode.BAD_REQUEST,
|
|
41
|
-
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
45
|
-
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
46
|
-
}
|
|
44
|
+
if (
|
|
45
|
+
typeof rule.actions.mockResponse.rawHeaders !== 'undefined' &&
|
|
46
|
+
(!Array.isArray(rule.actions.mockResponse.rawHeaders) ||
|
|
47
|
+
rule.actions.mockResponse.rawHeaders.some((header) => typeof header !== 'string'))
|
|
48
|
+
) {
|
|
49
|
+
throw new AppError({
|
|
50
|
+
httpCode: HttpCode.BAD_REQUEST,
|
|
51
|
+
message: 'invalid rule.actions.mockResponse.rawHeaders',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (typeof rule.actions.mockResponse.body !== 'undefined' && typeof rule.actions.mockResponse.body !== 'string') {
|
|
55
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.mockResponse.body' });
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
58
|
+
}
|
|
59
|
+
if (typeof rule.actions.modifyRequest !== 'undefined' && typeof rule.actions.modifyRequest.remoteFn !== 'string') {
|
|
60
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyRequest' });
|
|
61
|
+
}
|
|
62
|
+
if (typeof rule.actions.modifyResponse !== 'undefined' && typeof rule.actions.modifyResponse.remoteFn !== 'string') {
|
|
63
|
+
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.actions.modifyResponse' });
|
|
55
64
|
}
|
|
56
65
|
if (
|
|
57
66
|
typeof rule.disableAfterUse !== 'undefined' &&
|
|
@@ -73,7 +82,7 @@ export const validateSerializedRuleProperties = (rule: Stuntman.SerializedRule):
|
|
|
73
82
|
) {
|
|
74
83
|
throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'invalid rule.labels' });
|
|
75
84
|
}
|
|
76
|
-
if (rule.actions
|
|
85
|
+
if (rule.actions.mockResponse && rule.actions.modifyResponse) {
|
|
77
86
|
throw new AppError({
|
|
78
87
|
httpCode: HttpCode.BAD_REQUEST,
|
|
79
88
|
message: 'rule.actions.mockResponse and rule.actions.modifyResponse are mutually exclusive',
|
package/src/api/webgui/rules.pug
CHANGED
|
@@ -120,6 +120,8 @@ html
|
|
|
120
120
|
body: ruleFunctionText + '\n return STUNTMAN_RULE;',
|
|
121
121
|
}).then((response) => {
|
|
122
122
|
if (response.ok) {
|
|
123
|
+
const newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?ruleId=${encodeURIComponent(newId)}`;
|
|
124
|
+
window.history.pushState({ path: newUrl }, '', newUrl);
|
|
123
125
|
window.location.reload();
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
@@ -133,7 +135,7 @@ html
|
|
|
133
135
|
|
|
134
136
|
window.newRule = () => {
|
|
135
137
|
const ruleId = uuidv4();
|
|
136
|
-
const emptyRule = `import type * as Stuntman from \'stuntman\';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600 };`;
|
|
138
|
+
const emptyRule = `import type * as Stuntman from \'stuntman\';\n\nvar STUNTMAN_RULE: Stuntman.Rule = { id: '${ruleId}', matches: (req: Stuntman.Request) => true, ttlSeconds: 600, actions: { mockResponse: { status: '200', body: '${ruleId}' }} };`;
|
|
137
139
|
models[ruleId] = monaco.editor.createModel(emptyRule, 'typescript', `file:///${ruleId}.ts`);
|
|
138
140
|
const ruleKeyNode = document.getElementById('ruleKeys').firstChild;
|
|
139
141
|
const ruleKeyNodeClone = ruleKeyNode.cloneNode(true);
|
package/src/mock.ts
CHANGED
|
@@ -137,7 +137,7 @@ export class Mock {
|
|
|
137
137
|
if (matchingRule) {
|
|
138
138
|
mockEntry.mockRuleId = matchingRule.id;
|
|
139
139
|
mockEntry.labels = matchingRule.labels;
|
|
140
|
-
if (matchingRule.actions
|
|
140
|
+
if (matchingRule.actions.mockResponse) {
|
|
141
141
|
const staticResponse =
|
|
142
142
|
typeof matchingRule.actions.mockResponse === 'function'
|
|
143
143
|
? matchingRule.actions.mockResponse(mockEntry.modifiedRequest)
|
|
@@ -157,8 +157,8 @@ export class Mock {
|
|
|
157
157
|
// static response blocks any further processing
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
|
-
if (matchingRule.actions
|
|
161
|
-
mockEntry.modifiedRequest = matchingRule.actions
|
|
160
|
+
if (matchingRule.actions.modifyRequest) {
|
|
161
|
+
mockEntry.modifiedRequest = matchingRule.actions.modifyRequest(mockEntry.modifiedRequest);
|
|
162
162
|
logger.debug({ ...logContext, modifiedRequest: mockEntry.modifiedRequest }, 'modified original request');
|
|
163
163
|
}
|
|
164
164
|
}
|
|
@@ -180,7 +180,14 @@ export class Mock {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
const originalResponse =
|
|
183
|
+
const originalResponse: Required<Stuntman.Response> = this.options.mock.disableProxy
|
|
184
|
+
? {
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
body: undefined,
|
|
187
|
+
rawHeaders: new RawHeaders(),
|
|
188
|
+
status: 404,
|
|
189
|
+
}
|
|
190
|
+
: await this.proxyRequest(req, mockEntry, logContext);
|
|
184
191
|
|
|
185
192
|
logger.debug({ ...logContext, originalResponse }, 'received response');
|
|
186
193
|
mockEntry.originalResponse = originalResponse;
|
|
@@ -201,8 +208,8 @@ export class Mock {
|
|
|
201
208
|
})
|
|
202
209
|
),
|
|
203
210
|
};
|
|
204
|
-
if (matchingRule?.actions
|
|
205
|
-
modifedResponse = matchingRule?.actions
|
|
211
|
+
if (matchingRule?.actions.modifyResponse) {
|
|
212
|
+
modifedResponse = matchingRule?.actions.modifyResponse(mockEntry.modifiedRequest, originalResponse);
|
|
206
213
|
logger.debug({ ...logContext, modifedResponse }, 'modified response');
|
|
207
214
|
}
|
|
208
215
|
|