@stuntman/server 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/config/default.json +23 -0
  3. package/config/test.json +26 -0
  4. package/dist/api/api.d.ts +19 -0
  5. package/dist/api/api.js +162 -0
  6. package/dist/api/utils.d.ts +4 -0
  7. package/dist/api/utils.js +69 -0
  8. package/dist/api/validatiors.d.ts +3 -0
  9. package/dist/api/validatiors.js +118 -0
  10. package/dist/api/webgui/rules.pug +145 -0
  11. package/dist/api/webgui/style.css +28 -0
  12. package/dist/api/webgui/traffic.pug +37 -0
  13. package/dist/bin/stuntman.d.ts +2 -0
  14. package/dist/bin/stuntman.js +7 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +5 -0
  17. package/dist/ipUtils.d.ts +17 -0
  18. package/dist/ipUtils.js +101 -0
  19. package/dist/mock.d.ts +27 -0
  20. package/dist/mock.js +308 -0
  21. package/dist/requestContext.d.ts +9 -0
  22. package/dist/requestContext.js +18 -0
  23. package/dist/ruleExecutor.d.ts +21 -0
  24. package/dist/ruleExecutor.js +172 -0
  25. package/dist/rules/catchAll.d.ts +2 -0
  26. package/dist/rules/catchAll.js +15 -0
  27. package/dist/rules/echo.d.ts +2 -0
  28. package/dist/rules/echo.js +15 -0
  29. package/dist/rules/index.d.ts +2 -0
  30. package/dist/rules/index.js +7 -0
  31. package/dist/storage.d.ts +4 -0
  32. package/dist/storage.js +42 -0
  33. package/package.json +58 -0
  34. package/src/api/api.ts +194 -0
  35. package/src/api/utils.ts +69 -0
  36. package/src/api/validatiors.ts +123 -0
  37. package/src/api/webgui/rules.pug +145 -0
  38. package/src/api/webgui/style.css +28 -0
  39. package/src/api/webgui/traffic.pug +37 -0
  40. package/src/bin/stuntman.ts +8 -0
  41. package/src/index.ts +1 -0
  42. package/src/ipUtils.ts +83 -0
  43. package/src/mock.ts +349 -0
  44. package/src/requestContext.ts +23 -0
  45. package/src/ruleExecutor.ts +193 -0
  46. package/src/rules/catchAll.ts +14 -0
  47. package/src/rules/echo.ts +14 -0
  48. package/src/rules/index.ts +7 -0
  49. package/src/storage.ts +39 -0
  50. package/tsconfig.json +16 -0
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ruleExecutor = void 0;
7
+ const await_lock_1 = __importDefault(require("await-lock"));
8
+ const shared_1 = require("@stuntman/shared");
9
+ const rules_1 = require("./rules");
10
+ const rulesLock = new await_lock_1.default();
11
+ const transformMockRuleToLive = (rule) => {
12
+ var _a;
13
+ return {
14
+ ...rule,
15
+ counter: 0,
16
+ isEnabled: (_a = rule.isEnabled) !== null && _a !== void 0 ? _a : true,
17
+ createdTimestamp: Date.now(),
18
+ };
19
+ };
20
+ class RuleExecutor {
21
+ get enabledRules() {
22
+ if (!this._rules) {
23
+ return new Array();
24
+ }
25
+ const now = Date.now();
26
+ return this._rules
27
+ .filter((r) => (r.isEnabled && !Number.isFinite(r.ttlSeconds)) || r.createdTimestamp + r.ttlSeconds * 1000 > now)
28
+ .sort((a, b) => { var _a, _b; return ((_a = a.priority) !== null && _a !== void 0 ? _a : shared_1.DEFAULT_RULE_PRIORITY) - ((_b = b.priority) !== null && _b !== void 0 ? _b : shared_1.DEFAULT_RULE_PRIORITY); });
29
+ }
30
+ constructor(rules) {
31
+ this._rules = (rules || []).map(transformMockRuleToLive);
32
+ }
33
+ hasExpired() {
34
+ const now = Date.now();
35
+ return this._rules.some((r) => Number.isFinite(r.ttlSeconds) && r.createdTimestamp + r.ttlSeconds * 1000 < now);
36
+ }
37
+ async cleanUpExpired() {
38
+ if (!this.hasExpired()) {
39
+ return;
40
+ }
41
+ await rulesLock.acquireAsync();
42
+ const now = Date.now();
43
+ try {
44
+ this._rules = this._rules.filter((r) => {
45
+ const shouldKeep = !Number.isFinite(r.ttlSeconds) || r.createdTimestamp + r.ttlSeconds * 1000 > now;
46
+ if (!shouldKeep) {
47
+ shared_1.logger.debug({ ruleId: r.id }, 'removing expired rule');
48
+ }
49
+ return shouldKeep;
50
+ });
51
+ }
52
+ finally {
53
+ await rulesLock.release();
54
+ }
55
+ }
56
+ async addRule(rule, overwrite) {
57
+ await this.cleanUpExpired();
58
+ await rulesLock.acquireAsync();
59
+ try {
60
+ if (this._rules.some((r) => r.id === rule.id)) {
61
+ if (!overwrite) {
62
+ throw new shared_1.AppError({ httpCode: shared_1.HttpCode.CONFLICT, message: 'rule with given ID already exists' });
63
+ }
64
+ this._removeRule(rule.id);
65
+ }
66
+ const liveRule = transformMockRuleToLive(rule);
67
+ this._rules.push(liveRule);
68
+ shared_1.logger.debug(liveRule, 'rule added');
69
+ return liveRule;
70
+ }
71
+ finally {
72
+ await rulesLock.release();
73
+ }
74
+ }
75
+ _removeRule(ruleOrId) {
76
+ this._rules = this._rules.filter((r) => {
77
+ const notFound = r.id !== (typeof ruleOrId === 'string' ? ruleOrId : ruleOrId.id);
78
+ if (!notFound) {
79
+ shared_1.logger.debug({ ruleId: r.id }, 'rule removed');
80
+ }
81
+ return notFound;
82
+ });
83
+ }
84
+ async removeRule(ruleOrId) {
85
+ await this.cleanUpExpired();
86
+ await rulesLock.acquireAsync();
87
+ try {
88
+ this._removeRule(ruleOrId);
89
+ }
90
+ finally {
91
+ await rulesLock.release();
92
+ }
93
+ }
94
+ enableRule(ruleOrId) {
95
+ this._rules.forEach((r) => {
96
+ if (r.id === (typeof ruleOrId === 'string' ? ruleOrId : ruleOrId.id)) {
97
+ r.isEnabled = true;
98
+ shared_1.logger.debug({ ruleId: r.id }, 'rule enabled');
99
+ }
100
+ });
101
+ }
102
+ disableRule(ruleOrId) {
103
+ this._rules.forEach((r) => {
104
+ if (r.id === (typeof ruleOrId === 'string' ? ruleOrId : ruleOrId.id)) {
105
+ r.isEnabled = false;
106
+ shared_1.logger.debug({ ruleId: r.id }, 'rule disabled');
107
+ }
108
+ });
109
+ }
110
+ async findMatchingRule(request) {
111
+ const logContext = {
112
+ requestId: request.id,
113
+ };
114
+ const matchingRule = this.enabledRules.find((rule) => {
115
+ const matchResult = rule.matches(request);
116
+ if (typeof matchResult === 'boolean') {
117
+ return matchResult;
118
+ }
119
+ return matchResult.result;
120
+ });
121
+ if (!matchingRule) {
122
+ shared_1.logger.debug(logContext, 'no matching rule found');
123
+ return null;
124
+ }
125
+ const matchResult = matchingRule.matches(request);
126
+ logContext.ruleId = matchingRule.id;
127
+ shared_1.logger.debug(logContext, 'matching rule found');
128
+ const matchingRuleClone = Object.freeze(Object.assign({}, matchingRule));
129
+ ++matchingRule.counter;
130
+ logContext.ruleCounter = matchingRule.counter;
131
+ if (Number.isNaN(matchingRule.counter) || !Number.isFinite(matchingRule.counter)) {
132
+ matchingRule.counter = 0;
133
+ shared_1.logger.warn(logContext, "it's over 9000!!!");
134
+ }
135
+ if (matchingRule.disableAfterUse) {
136
+ if (typeof matchingRule.disableAfterUse === 'boolean' || matchingRule.disableAfterUse <= matchingRule.counter) {
137
+ shared_1.logger.debug(logContext, 'disabling rule for future requests');
138
+ this.disableRule(matchingRule);
139
+ }
140
+ }
141
+ if (matchingRule.removeAfterUse) {
142
+ if (typeof matchingRule.removeAfterUse === 'boolean' || matchingRule.removeAfterUse <= matchingRule.counter) {
143
+ shared_1.logger.debug(logContext, 'removing rule for future requests');
144
+ this.removeRule(matchingRule);
145
+ }
146
+ }
147
+ if (typeof matchResult !== 'boolean') {
148
+ if (matchResult.disableRuleIds && matchResult.disableRuleIds.length > 0) {
149
+ shared_1.logger.debug({ ...logContext, disableRuleIds: matchResult.disableRuleIds }, 'disabling rules based on matchResult');
150
+ for (const ruleId of matchResult.disableRuleIds) {
151
+ this.disableRule(ruleId);
152
+ }
153
+ }
154
+ if (matchResult.enableRuleIds && matchResult.enableRuleIds.length > 0) {
155
+ shared_1.logger.debug({ ...logContext, disableRuleIds: matchResult.disableRuleIds }, 'enabling rules based on matchResult');
156
+ for (const ruleId of matchResult.enableRuleIds) {
157
+ this.enableRule(ruleId);
158
+ }
159
+ }
160
+ }
161
+ return matchingRuleClone;
162
+ }
163
+ async getRules() {
164
+ await this.cleanUpExpired();
165
+ return this._rules;
166
+ }
167
+ async getRule(id) {
168
+ await this.cleanUpExpired();
169
+ return this._rules.find((r) => r.id === id);
170
+ }
171
+ }
172
+ exports.ruleExecutor = new RuleExecutor(rules_1.DEFAULT_RULES.map((r) => ({ ...r, ttlSeconds: Infinity })));
@@ -0,0 +1,2 @@
1
+ import type * as Stuntman from '@stuntman/shared';
2
+ export declare const catchAllRule: Stuntman.DeployedRule;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.catchAllRule = void 0;
4
+ const shared_1 = require("@stuntman/shared");
5
+ exports.catchAllRule = {
6
+ id: shared_1.CATCH_RULE_NAME,
7
+ matches: () => true,
8
+ priority: shared_1.CATCH_ALL_RULE_PRIORITY,
9
+ actions: {
10
+ mockResponse: (req) => ({
11
+ body: `Request received by Stuntman mock <pre>${JSON.stringify(req, null, 4)}</pre>`,
12
+ status: 200,
13
+ }),
14
+ },
15
+ };
@@ -0,0 +1,2 @@
1
+ import type * as Stuntman from '@stuntman/shared';
2
+ export declare const echoRule: Stuntman.DeployedRule;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.echoRule = void 0;
4
+ const shared_1 = require("@stuntman/shared");
5
+ exports.echoRule = {
6
+ id: 'internal/echo',
7
+ priority: shared_1.DEFAULT_RULE_PRIORITY + 1,
8
+ matches: (req) => /https?:\/\/echo\/.*/.test(req.url),
9
+ actions: {
10
+ mockResponse: (req) => ({
11
+ body: req,
12
+ status: 200,
13
+ }),
14
+ },
15
+ };
@@ -0,0 +1,2 @@
1
+ import type * as Stuntman from '@stuntman/shared';
2
+ export declare const DEFAULT_RULES: Stuntman.DeployedRule[];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_RULES = void 0;
4
+ const catchAll_1 = require("./catchAll");
5
+ const echo_1 = require("./echo");
6
+ // TODO add option to load rules additional default rules from some nice configurable folder
7
+ exports.DEFAULT_RULES = [catchAll_1.catchAllRule, echo_1.echoRule];
@@ -0,0 +1,4 @@
1
+ import LRUCache from 'lru-cache';
2
+ import type * as Stuntman from '@stuntman/shared';
3
+ export declare const getTrafficStore: (key: string, options?: Stuntman.StorageConfig) => LRUCache<string, Stuntman.LogEntry>;
4
+ export declare const getDnsResolutionCache: (key: string) => LRUCache<string, string>;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getDnsResolutionCache = exports.getTrafficStore = void 0;
7
+ const lru_cache_1 = __importDefault(require("lru-cache"));
8
+ const object_sizeof_1 = __importDefault(require("object-sizeof"));
9
+ const DNS_CACHE_OPTIONS = {
10
+ max: 1000,
11
+ ttl: 1000 * 60 * 15,
12
+ allowStale: false,
13
+ updateAgeOnGet: false,
14
+ updateAgeOnHas: false,
15
+ };
16
+ const trafficStoreInstances = {};
17
+ const dnsResolutionCacheInstances = {};
18
+ const getTrafficStore = (key, options) => {
19
+ if (!(key in trafficStoreInstances)) {
20
+ if (!options) {
21
+ throw new Error('initialize with options first');
22
+ }
23
+ trafficStoreInstances[key] = new lru_cache_1.default({
24
+ max: options.limitCount,
25
+ maxSize: options.limitSize,
26
+ ttl: options.ttl,
27
+ allowStale: false,
28
+ updateAgeOnGet: false,
29
+ updateAgeOnHas: false,
30
+ sizeCalculation: (value) => (0, object_sizeof_1.default)(value),
31
+ });
32
+ }
33
+ return trafficStoreInstances[key];
34
+ };
35
+ exports.getTrafficStore = getTrafficStore;
36
+ const getDnsResolutionCache = (key) => {
37
+ if (!(key in dnsResolutionCacheInstances)) {
38
+ dnsResolutionCacheInstances[key] = new lru_cache_1.default(DNS_CACHE_OPTIONS);
39
+ }
40
+ return dnsResolutionCacheInstances[key];
41
+ };
42
+ exports.getDnsResolutionCache = getDnsResolutionCache;
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@stuntman/server",
3
+ "version": "0.1.0",
4
+ "description": "Stuntman - HTTP proxy / mock server with API",
5
+ "main": "dist/index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/andrzej-woof/stuntman.git"
9
+ },
10
+ "keywords": [
11
+ "proxy",
12
+ "mock",
13
+ "http",
14
+ "https",
15
+ "server",
16
+ "api",
17
+ "e2e",
18
+ "development",
19
+ "rest",
20
+ "gql",
21
+ "end-to-end",
22
+ "testing",
23
+ "qa",
24
+ "automated-testing",
25
+ "stub",
26
+ "functional"
27
+ ],
28
+ "author": "Andrzej Pasterczyk",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@stuntman/shared": "^0.1.0",
32
+ "await-lock": "2.2.2",
33
+ "express": "5.0.0-beta.1",
34
+ "lru-cache": "7.16.0",
35
+ "object-sizeof": "2.6.1",
36
+ "pug": "^3.0.2",
37
+ "serialize-javascript": "6.0.1",
38
+ "uuid": "9.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/express": "4.17.17",
42
+ "@types/serialize-javascript": "5.0.2",
43
+ "@types/uuid": "9.0.0"
44
+ },
45
+ "bin": {
46
+ "stuntman": "./dist/bin/stuntman.js"
47
+ },
48
+ "scripts": {
49
+ "test": "echo \"Error: no test specified\" && exit 1",
50
+ "clean": "rm -fr dist",
51
+ "build": "tsc && cp -rv src/api/webgui dist/api",
52
+ "lint": "prettier --check . && eslint . --ext ts",
53
+ "lint:fix": "prettier --write ./src && eslint ./src --ext ts --fix",
54
+ "start": "node ./dist/bin/stuntman.js",
55
+ "start:dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' ./src/bin/stuntman.ts",
56
+ "start:debug": "node --inspect-brk=0.0.0.0 ./node_modules/.bin/ts-node --transpile-only ./src/bin/stuntman.ts"
57
+ }
58
+ }
package/src/api/api.ts ADDED
@@ -0,0 +1,194 @@
1
+ import http from 'http';
2
+ import express, { NextFunction, Request, Response, Express as ExpressServer } from 'express';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { getTrafficStore } from '../storage';
5
+ import { ruleExecutor } from '../ruleExecutor';
6
+ import { logger, AppError, HttpCode, MAX_RULE_TTL_SECONDS, stringify, INDEX_DTS } from '@stuntman/shared';
7
+ import type * as Stuntman from '@stuntman/shared';
8
+ import RequestContext from '../requestContext';
9
+ import serializeJavascript from 'serialize-javascript';
10
+ import LRUCache from 'lru-cache';
11
+ import { validateDeserializedRule } from './validatiors';
12
+ import { deserializeRule, escapedSerialize, liveRuleToRule } from './utils';
13
+
14
+ type ApiOptions = Stuntman.ApiConfig & {
15
+ mockUuid: string;
16
+ };
17
+
18
+ export class API {
19
+ protected options: Required<ApiOptions>;
20
+ protected apiApp: ExpressServer;
21
+ trafficStore: LRUCache<string, Stuntman.LogEntry>;
22
+ server: http.Server | null = null;
23
+
24
+ constructor(options: ApiOptions, webGuiOptions?: Stuntman.WebGuiConfig) {
25
+ this.options = options;
26
+
27
+ this.trafficStore = getTrafficStore(this.options.mockUuid);
28
+ this.apiApp = express();
29
+
30
+ this.apiApp.use(express.json());
31
+ this.apiApp.use(express.text());
32
+
33
+ this.apiApp.use((req: Request, res: Response, next: NextFunction) => {
34
+ RequestContext.bind(req, this.options.mockUuid);
35
+ next();
36
+ });
37
+
38
+ this.apiApp.get('/rule', async (req, res) => {
39
+ res.send(stringify(await ruleExecutor.getRules()));
40
+ });
41
+
42
+ this.apiApp.get('/rule/:ruleId', async (req, res) => {
43
+ res.send(stringify(await ruleExecutor.getRule(req.params.ruleId)));
44
+ });
45
+
46
+ this.apiApp.get('/rule/:ruleId/disable', (req, res) => {
47
+ ruleExecutor.disableRule(req.params.ruleId);
48
+ res.send();
49
+ });
50
+
51
+ this.apiApp.get('/rule/:ruleId/enable', (req, res) => {
52
+ ruleExecutor.enableRule(req.params.ruleId);
53
+ res.send();
54
+ });
55
+
56
+ this.apiApp.post('/rule', async (req: Request<object, string, Stuntman.SerializedRule>, res) => {
57
+ const deserializedRule = deserializeRule(req.body);
58
+ validateDeserializedRule(deserializedRule);
59
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
60
+ // @ts-ignore
61
+ const rule = await ruleExecutor.addRule(deserializedRule);
62
+ res.send(stringify(rule));
63
+ });
64
+
65
+ this.apiApp.get('/rule/:ruleId/remove', async (req, res) => {
66
+ await ruleExecutor.removeRule(req.params.ruleId);
67
+ res.send();
68
+ });
69
+
70
+ this.apiApp.get('/traffic', (req, res) => {
71
+ const serializedTraffic: Record<string, Stuntman.LogEntry> = {};
72
+ for (const [key, value] of this.trafficStore.entries()) {
73
+ serializedTraffic[key] = value;
74
+ }
75
+ res.json(serializedTraffic);
76
+ });
77
+
78
+ this.apiApp.get('/traffic/:ruleIdOrLabel', (req, res) => {
79
+ const serializedTraffic: Record<string, Stuntman.LogEntry> = {};
80
+ for (const [key, value] of this.trafficStore.entries()) {
81
+ if (value.mockRuleId === req.params.ruleIdOrLabel || (value.labels || []).includes(req.params.ruleIdOrLabel)) {
82
+ serializedTraffic[key] = value;
83
+ }
84
+ }
85
+ res.json(serializedTraffic);
86
+ });
87
+
88
+ this.apiApp.use((error: Error | AppError, req: Request, res: Response, _next: NextFunction) => {
89
+ const ctx: RequestContext | null = RequestContext.get(req);
90
+ const uuid = ctx?.uuid || uuidv4();
91
+ if (error instanceof AppError && error.isOperational && res) {
92
+ logger.error(error);
93
+ res.status(error.httpCode).json({
94
+ error: { message: error.message, httpCode: error.httpCode, stack: error.stack },
95
+ });
96
+ return;
97
+ }
98
+ logger.error({ ...error, uuid }, 'Unexpected error');
99
+ if (res) {
100
+ res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
101
+ error: { message: error.message, httpCode: HttpCode.INTERNAL_SERVER_ERROR, uuid },
102
+ });
103
+ return;
104
+ }
105
+ console.log('Application encountered a critical error. Exiting');
106
+ process.exit(1);
107
+ });
108
+
109
+ this.apiApp.set('views', __dirname + '/webgui');
110
+ this.apiApp.set('view engine', 'pug');
111
+
112
+ if (!webGuiOptions?.disabled) {
113
+ this.initWebGui();
114
+ }
115
+ }
116
+
117
+ private initWebGui() {
118
+ this.apiApp.get('/webgui/rules', async (req, res) => {
119
+ const rules: Record<string, string> = {};
120
+ for (const rule of await ruleExecutor.getRules()) {
121
+ rules[rule.id] = serializeJavascript(liveRuleToRule(rule), { unsafe: true });
122
+ }
123
+ res.render('rules', { rules: escapedSerialize(rules), INDEX_DTS, ruleKeys: Object.keys(rules) });
124
+ });
125
+
126
+ this.apiApp.get('/webgui/traffic', async (req, res) => {
127
+ const serializedTraffic: Stuntman.LogEntry[] = [];
128
+ for (const value of this.trafficStore.values()) {
129
+ serializedTraffic.push(value);
130
+ }
131
+ res.render('traffic', {
132
+ traffic: JSON.stringify(
133
+ serializedTraffic.sort((a, b) => b.originalRequest.timestamp - a.originalRequest.timestamp)
134
+ ),
135
+ });
136
+ });
137
+
138
+ // TODO make webui way better and safer, nicer formatting, eslint/prettier, blackjack and hookers
139
+
140
+ this.apiApp.post('/webgui/rules/unsafesave', async (req, res) => {
141
+ const rule: Stuntman.Rule = new Function(req.body)();
142
+ if (
143
+ !rule ||
144
+ !rule.id ||
145
+ typeof rule.matches !== 'function' ||
146
+ typeof rule.ttlSeconds !== 'number' ||
147
+ rule.ttlSeconds > MAX_RULE_TTL_SECONDS
148
+ ) {
149
+ throw new AppError({ httpCode: HttpCode.BAD_REQUEST, message: 'Invalid rule' });
150
+ }
151
+ await ruleExecutor.addRule(
152
+ {
153
+ id: rule.id,
154
+ matches: rule.matches,
155
+ ttlSeconds: rule.ttlSeconds,
156
+ ...(rule.actions && {
157
+ actions: {
158
+ ...(rule.actions.mockResponse
159
+ ? { mockResponse: rule.actions.mockResponse }
160
+ : { modifyRequest: rule.actions.modifyRequest, modifyResponse: rule.actions.modifyResponse }),
161
+ },
162
+ }),
163
+ ...(rule.disableAfterUse !== undefined && { disableAfterUse: rule.disableAfterUse }),
164
+ ...(rule.isEnabled !== undefined && { isEnabled: rule.isEnabled }),
165
+ ...(rule.labels !== undefined && { labels: rule.labels }),
166
+ ...(rule.priority !== undefined && { priority: rule.priority }),
167
+ ...(rule.removeAfterUse !== undefined && { removeAfterUse: rule.removeAfterUse }),
168
+ ...(rule.storeTraffic !== undefined && { storeTraffic: rule.storeTraffic }),
169
+ },
170
+ true
171
+ );
172
+ res.send();
173
+ });
174
+ }
175
+
176
+ public start() {
177
+ if (this.server) {
178
+ throw new Error('mock server already started');
179
+ }
180
+ this.server = this.apiApp.listen(this.options.port, () => {
181
+ logger.info(`API listening on ${this.options.port}`);
182
+ });
183
+ }
184
+
185
+ public stop() {
186
+ if (!this.server) {
187
+ throw new Error('mock server not started');
188
+ }
189
+ this.server.close((error) => {
190
+ logger.warn(error, 'problem closing server');
191
+ this.server = null;
192
+ });
193
+ }
194
+ }
@@ -0,0 +1,69 @@
1
+ import serializeJavascript from 'serialize-javascript';
2
+ import type * as Stuntman from '@stuntman/shared';
3
+ import { logger } from '@stuntman/shared';
4
+ import { validateSerializedRuleProperties } from './validatiors';
5
+
6
+ // TODO
7
+ export const deserializeRule = (serializedRule: Stuntman.SerializedRule): Stuntman.Rule => {
8
+ logger.debug(serializedRule, 'attempt to deserialize rule');
9
+ validateSerializedRuleProperties(serializedRule);
10
+ const rule: Stuntman.Rule = {
11
+ id: serializedRule.id,
12
+ matches: (req: Stuntman.Request) => new Function('____arg0', serializedRule.matches.remoteFn)(req),
13
+ ttlSeconds: serializedRule.ttlSeconds,
14
+ ...(serializedRule.disableAfterUse !== undefined && { disableAfterUse: serializedRule.disableAfterUse }),
15
+ ...(serializedRule.removeAfterUse !== undefined && { removeAfterUse: serializedRule.removeAfterUse }),
16
+ ...(serializedRule.labels !== undefined && { labels: serializedRule.labels }),
17
+ ...(serializedRule.priority !== undefined && { priority: serializedRule.priority }),
18
+ ...(serializedRule.isEnabled !== undefined && { isEnabled: serializedRule.isEnabled }),
19
+ ...(serializedRule.storeTraffic !== undefined && { storeTraffic: serializedRule.storeTraffic }),
20
+ };
21
+ if (serializedRule.actions) {
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
+ if (serializedRule.actions.modifyRequest) {
37
+ rule.actions.modifyRequest = (req: Stuntman.Request) =>
38
+ new Function(
39
+ '____arg0',
40
+ (serializedRule.actions?.modifyRequest as Stuntman.SerializedRemotableFunction).remoteFn
41
+ )(req);
42
+ }
43
+ if (serializedRule.actions.modifyResponse) {
44
+ rule.actions.modifyResponse = (req: Stuntman.Request, res: Stuntman.Response) =>
45
+ new Function(
46
+ '____arg0',
47
+ '____arg1',
48
+ (serializedRule.actions?.modifyResponse as Stuntman.SerializedRemotableFunction).remoteFn
49
+ )(req, res);
50
+ }
51
+ }
52
+ }
53
+ logger.debug(rule, 'deserialized rule');
54
+ return rule;
55
+ };
56
+
57
+ export const escapedSerialize = (obj: any) =>
58
+ serializeJavascript(obj).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, "\\n'\n+ '");
59
+
60
+ export const liveRuleToRule = (liveRule: Stuntman.LiveRule) => {
61
+ const ruleClone: Stuntman.Rule = { ...liveRule };
62
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
63
+ // @ts-ignore
64
+ delete ruleClone.counter;
65
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
66
+ // @ts-ignore
67
+ delete ruleClone.createdTimestamp;
68
+ return ruleClone;
69
+ };