@digitraffic/common 2024.4.25-1 → 2024.6.14-1
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/README.md +4 -3
- package/dist/__test__/imports.test.mjs +0 -4
- package/dist/__test__/infra/acl-builder.test.mjs +24 -0
- package/dist/aws/infra/acl-builder.d.mts +10 -0
- package/dist/aws/infra/acl-builder.mjs +155 -1
- package/dist/aws/infra/stack/monitoredfunction.mjs +8 -2
- package/package.json +1 -1
- package/dist/__test__/test/httpserver.test.d.mts +0 -1
- package/dist/__test__/test/httpserver.test.mjs +0 -154
- package/dist/test/httpserver.d.mts +0 -17
- package/dist/test/httpserver.mjs +0 -85
package/README.md
CHANGED
@@ -4,10 +4,11 @@ This is a place for common utilities and classes that can be used in other cdk-p
|
|
4
4
|
|
5
5
|
## How to build
|
6
6
|
|
7
|
-
Use `
|
7
|
+
Use `pnpm` to build the code i.e.
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
pnpm install
|
10
|
+
pnpm build
|
11
|
+
pnpm test
|
11
12
|
|
12
13
|
## How to use
|
13
14
|
|
@@ -71,10 +71,6 @@ test("dbTestutils import ok?", () => {
|
|
71
71
|
const dbTestutils = import("../test/db-testutils.mjs");
|
72
72
|
return expect(dbTestutils).resolves.toBeDefined();
|
73
73
|
});
|
74
|
-
test("httpserver import ok?", () => {
|
75
|
-
const httpserver = import("../test/httpserver.mjs");
|
76
|
-
return expect(httpserver).resolves.toBeDefined();
|
77
|
-
});
|
78
74
|
test("asserter import ok?", () => {
|
79
75
|
const asserter = import("../test/asserter.mjs");
|
80
76
|
return expect(asserter).resolves.toBeDefined();
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { AclBuilder } from "../../aws/infra/acl-builder.mjs";
|
2
2
|
import { App, Stack } from "aws-cdk-lib";
|
3
|
+
import { expect } from "@jest/globals";
|
3
4
|
describe("acl-builder tests", () => {
|
4
5
|
function createBuilder() {
|
5
6
|
const app = new App();
|
@@ -21,5 +22,28 @@ describe("acl-builder tests", () => {
|
|
21
22
|
const acl = createBuilder().withIpRestrictionRule(["1.2.3.4", "1.2.6.6"]).build();
|
22
23
|
expect(acl.rules).toHaveLength(1);
|
23
24
|
});
|
25
|
+
test("throttle rules", () => {
|
26
|
+
for (const aclBuilder of [
|
27
|
+
createBuilder().withThrottleDigitrafficUserIp(100),
|
28
|
+
createBuilder().withThrottleDigitrafficUserIpAndUriPath(100),
|
29
|
+
createBuilder().withThrottleAnonymousUserIp(100),
|
30
|
+
createBuilder().withThrottleAnonymousUserIpAndUriPath(100)
|
31
|
+
]) {
|
32
|
+
const acl = aclBuilder.build();
|
33
|
+
// Check that the rule exists and a custom response is defined
|
34
|
+
expect(acl.rules).toHaveLength(1);
|
35
|
+
expect(Object.keys(acl.customResponseBodies)).toHaveLength(1);
|
36
|
+
// Check that the rule does throttle
|
37
|
+
const throttleRule = acl.rules[0];
|
38
|
+
expect(throttleRule.statement.rateBasedStatement).toBeDefined();
|
39
|
+
expect(throttleRule.action.block).toBeDefined();
|
40
|
+
}
|
41
|
+
});
|
42
|
+
test("Cannot define two rules with the same name", () => {
|
43
|
+
expect(() => createBuilder()
|
44
|
+
.withThrottleAnonymousUserIp(10)
|
45
|
+
.withThrottleAnonymousUserIp(200)
|
46
|
+
.build()).toThrow();
|
47
|
+
});
|
24
48
|
});
|
25
49
|
//# sourceMappingURL=acl-builder.test.mjs.map
|
@@ -13,9 +13,19 @@ export declare class AclBuilder {
|
|
13
13
|
readonly _rules: CfnWebACL.RuleProperty[];
|
14
14
|
_scope: string;
|
15
15
|
_name: string;
|
16
|
+
_customResponseBodies: Record<string, CfnWebACL.CustomResponseBodyProperty>;
|
16
17
|
constructor(construct: Construct);
|
17
18
|
isRuleDefined(rules: AWSManagedWafRule[] | "all", rule: AWSManagedWafRule): boolean;
|
18
19
|
withAWSManagedRules(rules?: AWSManagedWafRule[] | "all"): AclBuilder;
|
19
20
|
withIpRestrictionRule(addresses: string[]): AclBuilder;
|
21
|
+
withThrottleRule(name: string, priority: number, limit: number, customResponseBodyKey: string, isHeaderRequired: boolean, isBasedOnIpAndUriPath: boolean): AclBuilder;
|
22
|
+
withCustomResponseBody(key: string, customResponseBody: CfnWebACL.CustomResponseBodyProperty): this;
|
23
|
+
withThrottleDigitrafficUserIp(limit: number): AclBuilder;
|
24
|
+
withThrottleDigitrafficUserIpAndUriPath(limit: number): AclBuilder;
|
25
|
+
withThrottleAnonymousUserIp(limit: number): AclBuilder;
|
26
|
+
withThrottleAnonymousUserIpAndUriPath(limit: number): AclBuilder;
|
27
|
+
_isCustomResponseBodyKeySet(key: string): boolean;
|
28
|
+
_addThrottleResponseBody(customResponseBodyKey: string, limit: number): void;
|
29
|
+
_logMissingLimit(method: string): void;
|
20
30
|
build(): CfnWebACL;
|
21
31
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { CfnIPSet, CfnWebACL } from "aws-cdk-lib/aws-wafv2";
|
2
|
+
import { logger } from "../runtime/dt-logger-default.mjs";
|
2
3
|
/**
|
3
4
|
* Builder class for building CfnWebACL.
|
4
5
|
*
|
@@ -11,6 +12,7 @@ export class AclBuilder {
|
|
11
12
|
_rules = [];
|
12
13
|
_scope = "CLOUDFRONT";
|
13
14
|
_name = "WebACL";
|
15
|
+
_customResponseBodies = {};
|
14
16
|
constructor(construct) {
|
15
17
|
this._construct = construct;
|
16
18
|
}
|
@@ -55,10 +57,98 @@ export class AclBuilder {
|
|
55
57
|
});
|
56
58
|
return this;
|
57
59
|
}
|
60
|
+
withThrottleRule(name, priority, limit, customResponseBodyKey, isHeaderRequired, isBasedOnIpAndUriPath) {
|
61
|
+
this._rules.push({
|
62
|
+
name,
|
63
|
+
priority,
|
64
|
+
visibilityConfig: {
|
65
|
+
sampledRequestsEnabled: true,
|
66
|
+
cloudWatchMetricsEnabled: true,
|
67
|
+
metricName: name
|
68
|
+
},
|
69
|
+
action: {
|
70
|
+
block: {
|
71
|
+
customResponse: {
|
72
|
+
responseCode: 429,
|
73
|
+
customResponseBodyKey
|
74
|
+
}
|
75
|
+
}
|
76
|
+
},
|
77
|
+
statement: createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath)
|
78
|
+
});
|
79
|
+
return this;
|
80
|
+
}
|
81
|
+
withCustomResponseBody(key, customResponseBody) {
|
82
|
+
if (key in this._customResponseBodies) {
|
83
|
+
logger.warn({
|
84
|
+
method: "acl-builder.withCustomResponseBody",
|
85
|
+
message: `Overriding custom response body with key ${key}`
|
86
|
+
});
|
87
|
+
}
|
88
|
+
this._customResponseBodies[key] = customResponseBody;
|
89
|
+
return this;
|
90
|
+
}
|
91
|
+
withThrottleDigitrafficUserIp(limit) {
|
92
|
+
if (limit == null) {
|
93
|
+
this._logMissingLimit("withThrottleDigitrafficUserIp");
|
94
|
+
return this;
|
95
|
+
}
|
96
|
+
const customResponseBodyKey = `IP_THROTTLE_DIGITRAFFIC_USER_${limit}`;
|
97
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
98
|
+
return this.withThrottleRule("ThrottleRuleWithDigitrafficUser", 1, limit, customResponseBodyKey, true, false);
|
99
|
+
}
|
100
|
+
withThrottleDigitrafficUserIpAndUriPath(limit) {
|
101
|
+
if (limit == null) {
|
102
|
+
this._logMissingLimit("withThrottleDigitrafficUserIpAndUriPath");
|
103
|
+
return this;
|
104
|
+
}
|
105
|
+
const customResponseBodyKey = `IP_PATH_THROTTLE_DIGITRAFFIC_USER_${limit}`;
|
106
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
107
|
+
return this.withThrottleRule("ThrottleRuleIPQueryWithDigitrafficUser", 2, limit, customResponseBodyKey, true, true);
|
108
|
+
}
|
109
|
+
withThrottleAnonymousUserIp(limit) {
|
110
|
+
if (limit == null) {
|
111
|
+
this._logMissingLimit("withThrottleAnonymousUserIp");
|
112
|
+
return this;
|
113
|
+
}
|
114
|
+
const customResponseBodyKey = `IP_THROTTLE_ANONYMOUS_USER_${limit}`;
|
115
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
116
|
+
return this.withThrottleRule("ThrottleRuleWithAnonymousUser", 3, limit, customResponseBodyKey, false, false);
|
117
|
+
}
|
118
|
+
withThrottleAnonymousUserIpAndUriPath(limit) {
|
119
|
+
if (limit == null) {
|
120
|
+
this._logMissingLimit("withThrottleAnonymousUserIpAndUriPath");
|
121
|
+
return this;
|
122
|
+
}
|
123
|
+
const customResponseBodyKey = `IP_PATH_THROTTLE_ANONYMOUS_USER_${limit}`;
|
124
|
+
this._addThrottleResponseBody(customResponseBodyKey, limit);
|
125
|
+
return this.withThrottleRule("ThrottleRuleIPQueryWithAnonymousUser", 4, limit, customResponseBodyKey, false, true);
|
126
|
+
}
|
127
|
+
_isCustomResponseBodyKeySet(key) {
|
128
|
+
return key in this._customResponseBodies;
|
129
|
+
}
|
130
|
+
_addThrottleResponseBody(customResponseBodyKey, limit) {
|
131
|
+
if (!this._isCustomResponseBodyKeySet(customResponseBodyKey)) {
|
132
|
+
this.withCustomResponseBody(customResponseBodyKey, {
|
133
|
+
content: `Request rate is limited to ${limit} requests in a 5 minute window.`,
|
134
|
+
contentType: "TEXT_PLAIN"
|
135
|
+
});
|
136
|
+
}
|
137
|
+
}
|
138
|
+
_logMissingLimit(method) {
|
139
|
+
logger.warn({
|
140
|
+
method: `acl-builder.${method}`,
|
141
|
+
message: `'limit' was not defined. Not setting a throttle rule`
|
142
|
+
});
|
143
|
+
}
|
58
144
|
build() {
|
59
145
|
if (this._rules.length === 0) {
|
60
146
|
throw new Error("No rules defined for WebACL");
|
61
147
|
}
|
148
|
+
const uniqueRuleNames = new Set(this._rules.map(rule => rule.name));
|
149
|
+
if (uniqueRuleNames.size != this._rules.length) {
|
150
|
+
throw new Error("Tried to create an Access Control List with multiple rules having the same name");
|
151
|
+
}
|
62
152
|
const acl = new CfnWebACL(this._construct, this._name, {
|
63
153
|
defaultAction: { allow: {} },
|
64
154
|
scope: this._scope,
|
@@ -68,11 +158,75 @@ export class AclBuilder {
|
|
68
158
|
sampledRequestsEnabled: false
|
69
159
|
},
|
70
160
|
rules: this._rules,
|
71
|
-
|
161
|
+
customResponseBodies: this._customResponseBodies,
|
72
162
|
});
|
73
163
|
return acl;
|
74
164
|
}
|
75
165
|
}
|
166
|
+
const CUSTOM_KEYS_IP_AND_URI_PATH = [
|
167
|
+
{
|
168
|
+
uriPath: {
|
169
|
+
textTransformations: [
|
170
|
+
{
|
171
|
+
priority: 1,
|
172
|
+
type: "LOWERCASE"
|
173
|
+
},
|
174
|
+
{
|
175
|
+
priority: 2,
|
176
|
+
type: "NORMALIZE_PATH"
|
177
|
+
},
|
178
|
+
{
|
179
|
+
priority: 3,
|
180
|
+
type: "MD5"
|
181
|
+
}
|
182
|
+
]
|
183
|
+
}
|
184
|
+
},
|
185
|
+
{
|
186
|
+
ip: {}
|
187
|
+
}
|
188
|
+
];
|
189
|
+
function notStatement(statement) {
|
190
|
+
return {
|
191
|
+
notStatement: {
|
192
|
+
statement
|
193
|
+
}
|
194
|
+
};
|
195
|
+
}
|
196
|
+
function createThrottleStatement(limit, isHeaderRequired, isBasedOnIpAndUriPath) {
|
197
|
+
// this statement matches empty digitraffic-user -header
|
198
|
+
const matchStatement = {
|
199
|
+
sizeConstraintStatement: {
|
200
|
+
comparisonOperator: isHeaderRequired ? "GT" : "GE",
|
201
|
+
fieldToMatch: {
|
202
|
+
singleHeader: {
|
203
|
+
Name: "digitraffic-user"
|
204
|
+
}
|
205
|
+
},
|
206
|
+
textTransformations: [{ priority: 0, type: "NONE" }],
|
207
|
+
size: 0
|
208
|
+
}
|
209
|
+
};
|
210
|
+
// header present -> size > 0
|
211
|
+
// header not present -> NOT(size >= 0)
|
212
|
+
if (isBasedOnIpAndUriPath) {
|
213
|
+
return {
|
214
|
+
rateBasedStatement: {
|
215
|
+
aggregateKeyType: "CUSTOM_KEYS",
|
216
|
+
customKeys: CUSTOM_KEYS_IP_AND_URI_PATH,
|
217
|
+
limit: limit,
|
218
|
+
scopeDownStatement: isHeaderRequired ? matchStatement : notStatement(matchStatement)
|
219
|
+
}
|
220
|
+
};
|
221
|
+
}
|
222
|
+
return {
|
223
|
+
rateBasedStatement: {
|
224
|
+
aggregateKeyType: "IP",
|
225
|
+
limit: limit,
|
226
|
+
scopeDownStatement: isHeaderRequired ? matchStatement : notStatement(matchStatement)
|
227
|
+
}
|
228
|
+
};
|
229
|
+
}
|
76
230
|
function createAWSCommonRuleSet() {
|
77
231
|
return createRuleProperty("AWS-AWSManagedRulesCommonRuleSet", 70, {
|
78
232
|
statement: {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Function, LoggingFormat } from "aws-cdk-lib/aws-lambda";
|
1
|
+
import { ApplicationLogLevel, Function, LoggingFormat, SystemLogLevel } from "aws-cdk-lib/aws-lambda";
|
2
2
|
import { Stack } from "aws-cdk-lib";
|
3
3
|
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
|
4
4
|
import { ComparisonOperator, Metric } from "aws-cdk-lib/aws-cloudwatch";
|
@@ -75,7 +75,13 @@ export class MonitoredFunction extends Function {
|
|
75
75
|
*/
|
76
76
|
constructor(scope, id, functionProps, alarmSnsTopic, warningSnsTopic, production, trafficType, props) {
|
77
77
|
// Set default loggingFormat to JSON if not explicitly set to TEXT
|
78
|
-
super(scope, id, {
|
78
|
+
super(scope, id, {
|
79
|
+
...{
|
80
|
+
loggingFormat: LoggingFormat.JSON,
|
81
|
+
applicationLogLevel: ApplicationLogLevel.DEBUG,
|
82
|
+
systemLogLevel: SystemLogLevel.INFO
|
83
|
+
}, ...functionProps
|
84
|
+
});
|
79
85
|
if (functionProps.functionName === undefined) {
|
80
86
|
throw new Error("Function name not provided");
|
81
87
|
}
|
package/package.json
CHANGED
@@ -1 +0,0 @@
|
|
1
|
-
export {};
|
@@ -1,154 +0,0 @@
|
|
1
|
-
import { TestHttpServer, ERROR_NO_MATCH, ERRORCODE_NOT_FOUND, } from "../../test/httpserver.mjs";
|
2
|
-
import { IncomingMessage } from "http";
|
3
|
-
import { Socket } from "net";
|
4
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
5
|
-
import * as http from "http";
|
6
|
-
import { expect } from "@jest/globals";
|
7
|
-
const threadLocalPort = new AsyncLocalStorage();
|
8
|
-
const DEFAULT_PATH = "/";
|
9
|
-
const DEFAULT_PROPS = {
|
10
|
-
"/": () => "",
|
11
|
-
};
|
12
|
-
const findOpenPort = async (excludedPorts) => {
|
13
|
-
const ephemeralPorts = Array.from({ length: 65535 - 1024 + 1 }, (__, i) => 1024 + i);
|
14
|
-
const allSocketEvents = [
|
15
|
-
"close",
|
16
|
-
"connect",
|
17
|
-
"data",
|
18
|
-
"drain",
|
19
|
-
"end",
|
20
|
-
"error",
|
21
|
-
"lookup",
|
22
|
-
"ready",
|
23
|
-
"timeout",
|
24
|
-
];
|
25
|
-
let openPort = null;
|
26
|
-
for (const testPort of ephemeralPorts) {
|
27
|
-
if (openPort !== null) {
|
28
|
-
break;
|
29
|
-
}
|
30
|
-
if (excludedPorts.has(testPort)) {
|
31
|
-
continue;
|
32
|
-
}
|
33
|
-
const portConnected = new Promise((resolve) => {
|
34
|
-
const socket = new Socket();
|
35
|
-
socket.setTimeout(500);
|
36
|
-
for (const socketEvent of allSocketEvents) {
|
37
|
-
if (socketEvent === "error") {
|
38
|
-
socket.on(socketEvent, (error) => {
|
39
|
-
socket.destroy();
|
40
|
-
if (error.code === "ECONNREFUSED") {
|
41
|
-
resolve(testPort);
|
42
|
-
}
|
43
|
-
else {
|
44
|
-
resolve(null);
|
45
|
-
}
|
46
|
-
});
|
47
|
-
}
|
48
|
-
else {
|
49
|
-
socket.on(socketEvent, () => {
|
50
|
-
socket.destroy();
|
51
|
-
resolve(null);
|
52
|
-
});
|
53
|
-
}
|
54
|
-
}
|
55
|
-
// connect method is asynchronous. That is why we wrap this thing inside of a promise.
|
56
|
-
socket.connect({ port: testPort, host: "127.0.0.1" });
|
57
|
-
});
|
58
|
-
openPort = await portConnected;
|
59
|
-
}
|
60
|
-
if (openPort === null) {
|
61
|
-
throw Error("All ephemeral ports in use!");
|
62
|
-
}
|
63
|
-
return openPort;
|
64
|
-
};
|
65
|
-
const usedPorts = new Set();
|
66
|
-
async function withServer(fn, props = DEFAULT_PROPS, statusCode = 200) {
|
67
|
-
const server = new TestHttpServer();
|
68
|
-
let openPort;
|
69
|
-
while (!openPort) {
|
70
|
-
const foundPort = await findOpenPort(usedPorts);
|
71
|
-
console.info(`foundPort ${foundPort}`);
|
72
|
-
if (!usedPorts.has(foundPort)) {
|
73
|
-
usedPorts.add(foundPort);
|
74
|
-
openPort = foundPort;
|
75
|
-
}
|
76
|
-
}
|
77
|
-
console.info(`Using port ${openPort} to run the test`);
|
78
|
-
server.listen(openPort, props, false, statusCode);
|
79
|
-
threadLocalPort.enterWith(openPort);
|
80
|
-
try {
|
81
|
-
await fn(server);
|
82
|
-
}
|
83
|
-
finally {
|
84
|
-
await server.close();
|
85
|
-
console.info("Server closed");
|
86
|
-
}
|
87
|
-
}
|
88
|
-
function sendGetRequest(path = DEFAULT_PATH) {
|
89
|
-
return sendRequest("GET", path);
|
90
|
-
}
|
91
|
-
function sendPostRequest(path = DEFAULT_PATH, body) {
|
92
|
-
return sendRequest("POST", path, body);
|
93
|
-
}
|
94
|
-
function sendRequest(method, path, body) {
|
95
|
-
return new Promise((resolve, reject) => {
|
96
|
-
const port = threadLocalPort.getStore();
|
97
|
-
const request = http.request({
|
98
|
-
path,
|
99
|
-
port,
|
100
|
-
method,
|
101
|
-
}, (response) => {
|
102
|
-
response.on("data", () => {
|
103
|
-
// do nothing
|
104
|
-
});
|
105
|
-
//the whole response has been received, so we just print it out here
|
106
|
-
response.on("end", () => {
|
107
|
-
resolve(response);
|
108
|
-
});
|
109
|
-
response.on("error", (error) => {
|
110
|
-
reject(error);
|
111
|
-
});
|
112
|
-
});
|
113
|
-
if (method === "POST") {
|
114
|
-
request.write(body);
|
115
|
-
}
|
116
|
-
request.end();
|
117
|
-
});
|
118
|
-
}
|
119
|
-
test("no calls", () => {
|
120
|
-
return withServer((server) => {
|
121
|
-
expect(server.getCallCount()).toEqual(0);
|
122
|
-
});
|
123
|
-
});
|
124
|
-
test("one get", async () => {
|
125
|
-
await withServer(async (server) => {
|
126
|
-
await sendGetRequest();
|
127
|
-
expect(server.getCallCount()).toEqual(1);
|
128
|
-
});
|
129
|
-
});
|
130
|
-
test("one get - no MATCH", async () => {
|
131
|
-
await withServer(async (server) => {
|
132
|
-
const response = await sendGetRequest("/no-match");
|
133
|
-
expect(server.getCallCount()).toEqual(1);
|
134
|
-
expect(server.getRequestBody(0)).toEqual(ERROR_NO_MATCH);
|
135
|
-
expect(response.statusCode).toEqual(ERRORCODE_NOT_FOUND);
|
136
|
-
});
|
137
|
-
});
|
138
|
-
test("get - error 405", async () => {
|
139
|
-
const ERROR_CODE = 405;
|
140
|
-
await withServer(async (server) => {
|
141
|
-
const response = await sendGetRequest();
|
142
|
-
expect(server.getCallCount()).toEqual(1);
|
143
|
-
expect(response.statusCode).toEqual(ERROR_CODE);
|
144
|
-
}, DEFAULT_PROPS, ERROR_CODE);
|
145
|
-
});
|
146
|
-
test("one post", async () => {
|
147
|
-
await withServer(async (server) => {
|
148
|
-
const testBody = "Testing123!";
|
149
|
-
await sendPostRequest(DEFAULT_PATH, testBody);
|
150
|
-
expect(server.getCallCount()).toEqual(1);
|
151
|
-
expect(server.getRequestBody(0)).toEqual(testBody);
|
152
|
-
});
|
153
|
-
});
|
154
|
-
//# sourceMappingURL=httpserver.test.mjs.map
|
@@ -1,17 +0,0 @@
|
|
1
|
-
export declare const ERROR_NO_MATCH = "NO MATCH";
|
2
|
-
export declare const ERRORCODE_NOT_FOUND = 404;
|
3
|
-
/**
|
4
|
-
* A mock HTTP server created for testing connections from a Lambda to an outside integration
|
5
|
-
*/
|
6
|
-
export declare class TestHttpServer {
|
7
|
-
private server?;
|
8
|
-
private debug;
|
9
|
-
private messageStack;
|
10
|
-
constructor();
|
11
|
-
getCallCount(): number;
|
12
|
-
getRequestBody(callNumber: number): string;
|
13
|
-
listen(port: number, props: ListenProperties, debug?: boolean, statusCode?: number): void;
|
14
|
-
close(): Promise<boolean>;
|
15
|
-
private debuglog;
|
16
|
-
}
|
17
|
-
export type ListenProperties = Record<string, (url?: string, data?: string) => string>;
|
package/dist/test/httpserver.mjs
DELETED
@@ -1,85 +0,0 @@
|
|
1
|
-
import { Server, createServer } from "http";
|
2
|
-
import { parse } from "url";
|
3
|
-
export const ERROR_NO_MATCH = "NO MATCH";
|
4
|
-
export const ERRORCODE_NOT_FOUND = 404;
|
5
|
-
/**
|
6
|
-
* A mock HTTP server created for testing connections from a Lambda to an outside integration
|
7
|
-
*/
|
8
|
-
export class TestHttpServer {
|
9
|
-
server;
|
10
|
-
debug;
|
11
|
-
messageStack;
|
12
|
-
constructor() {
|
13
|
-
this.debug = false;
|
14
|
-
this.messageStack = [];
|
15
|
-
}
|
16
|
-
getCallCount() {
|
17
|
-
return this.messageStack.length;
|
18
|
-
}
|
19
|
-
getRequestBody(callNumber) {
|
20
|
-
return this.messageStack[callNumber] ?? '';
|
21
|
-
}
|
22
|
-
listen(port, props, debug = false, statusCode = 200) {
|
23
|
-
this.debug = debug;
|
24
|
-
this.messageStack = [];
|
25
|
-
this.debuglog(`Starting test server on port ${port}`);
|
26
|
-
this.server = createServer((req, res) => {
|
27
|
-
this.debuglog("Mapped urls: ");
|
28
|
-
Object.keys(props).forEach((k) => this.debuglog(k));
|
29
|
-
if (!req.url) {
|
30
|
-
throw new Error("Missing request url!");
|
31
|
-
}
|
32
|
-
this.debuglog(`Received request to url ${req.url} ..`);
|
33
|
-
const path = parse(req.url).pathname;
|
34
|
-
if (!path) {
|
35
|
-
throw new Error("Missing path from request!");
|
36
|
-
}
|
37
|
-
let dataStr = "";
|
38
|
-
req.on("data", (chunk) => {
|
39
|
-
if (chunk) {
|
40
|
-
dataStr += chunk;
|
41
|
-
}
|
42
|
-
});
|
43
|
-
if (path in props) {
|
44
|
-
this.debuglog("..url matched");
|
45
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
46
|
-
res.setHeader("Access-Control-Allow-Headers", "Authorization,X-User-Id,X-Auth-Token");
|
47
|
-
res.writeHead(statusCode);
|
48
|
-
req.on("end", () => {
|
49
|
-
// assume sent data is in JSON format
|
50
|
-
this.messageStack[this.messageStack.length] = dataStr;
|
51
|
-
const invokable = props[path];
|
52
|
-
res.end(invokable(req.url, dataStr));
|
53
|
-
});
|
54
|
-
}
|
55
|
-
else {
|
56
|
-
this.debuglog(`..no match for ${path}`);
|
57
|
-
req.on("end", () => {
|
58
|
-
// assume sent data is in JSON format
|
59
|
-
this.messageStack[this.messageStack.length] =
|
60
|
-
ERROR_NO_MATCH;
|
61
|
-
res.writeHead(ERRORCODE_NOT_FOUND);
|
62
|
-
res.end(ERROR_NO_MATCH);
|
63
|
-
});
|
64
|
-
}
|
65
|
-
});
|
66
|
-
this.server.listen(port);
|
67
|
-
}
|
68
|
-
close() {
|
69
|
-
return new Promise((resolve, reject) => {
|
70
|
-
this.debuglog("Closing test server");
|
71
|
-
if (this.server !== undefined) {
|
72
|
-
this.server.close((error) => error != null ? reject(false) : resolve(true));
|
73
|
-
}
|
74
|
-
else {
|
75
|
-
resolve(true);
|
76
|
-
}
|
77
|
-
});
|
78
|
-
}
|
79
|
-
debuglog(str) {
|
80
|
-
if (this.debug) {
|
81
|
-
console.debug(str);
|
82
|
-
}
|
83
|
-
}
|
84
|
-
}
|
85
|
-
//# sourceMappingURL=httpserver.mjs.map
|