@bifravst/http-api-mock 2.1.354 → 2.1.356
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/npm/mock.js +27 -0
- package/npm/parseMockRequest.js +17 -0
- package/{dist/src → npm}/parseMockResponse.js +8 -10
- package/npm/randomString.js +2 -0
- package/{dist/src → npm}/requests.d.ts +1 -1
- package/npm/requests.js +11 -0
- package/{dist/src → npm}/responses.js +6 -6
- package/{dist/src → npm}/sortQueryString.d.ts +1 -1
- package/{dist/src → npm}/sortQueryString.js +11 -10
- package/package.json +23 -27
- package/cdk/App.ts +0 -26
- package/cdk/Stack.ts +0 -66
- package/cdk/http-api-mock.ts +0 -41
- package/cdk/resources/HttpApiMock.ts +0 -119
- package/cdk/resources/checkMatchingQueryParams.spec.ts +0 -67
- package/cdk/resources/checkMatchingQueryParams.ts +0 -40
- package/cdk/resources/http-api-mock-lambda.ts +0 -147
- package/cdk/resources/splitMockResponse.spec.ts +0 -17
- package/cdk/resources/splitMockResponse.ts +0 -25
- package/cli.js +0 -5
- package/dist/cdk/App.d.ts +0 -11
- package/dist/cdk/App.js +0 -12
- package/dist/cdk/Stack.d.ts +0 -20
- package/dist/cdk/Stack.js +0 -40
- package/dist/cdk/http-api-mock.d.ts +0 -1
- package/dist/cdk/http-api-mock.js +0 -35
- package/dist/cdk/resources/HttpApiMock.d.ts +0 -14
- package/dist/cdk/resources/HttpApiMock.js +0 -87
- package/dist/cdk/resources/checkMatchingQueryParams.d.ts +0 -5
- package/dist/cdk/resources/checkMatchingQueryParams.js +0 -33
- package/dist/cdk/resources/checkMatchingQueryParams.spec.d.ts +0 -1
- package/dist/cdk/resources/checkMatchingQueryParams.spec.js +0 -57
- package/dist/cdk/resources/http-api-mock-lambda.d.ts +0 -2
- package/dist/cdk/resources/http-api-mock-lambda.js +0 -98
- package/dist/cdk/resources/splitMockResponse.d.ts +0 -4
- package/dist/cdk/resources/splitMockResponse.js +0 -20
- package/dist/cdk/resources/splitMockResponse.spec.d.ts +0 -1
- package/dist/cdk/resources/splitMockResponse.spec.js +0 -13
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +0 -88
- package/dist/src/mock.js +0 -31
- package/dist/src/mock.spec.js +0 -36
- package/dist/src/parseMockRequest.js +0 -19
- package/dist/src/parseMockRequest.spec.js +0 -23
- package/dist/src/parseMockResponse.spec.js +0 -20
- package/dist/src/randomString.js +0 -5
- package/dist/src/requests.js +0 -11
- package/dist/src/sortQueryString.spec.js +0 -18
- package/src/cli.ts +0 -109
- package/src/mock.spec.ts +0 -52
- package/src/mock.ts +0 -61
- package/src/parseMockRequest.spec.ts +0 -30
- package/src/parseMockRequest.ts +0 -35
- package/src/parseMockResponse.spec.ts +0 -27
- package/src/parseMockResponse.ts +0 -35
- package/src/randomString.ts +0 -7
- package/src/requests.ts +0 -28
- package/src/responses.ts +0 -50
- package/src/sortQueryString.spec.ts +0 -38
- package/src/sortQueryString.ts +0 -26
- /package/{dist/src → npm}/mock.d.ts +0 -0
- /package/{dist/src → npm}/mock.spec.d.ts +0 -0
- /package/{dist/src → npm}/parseMockRequest.d.ts +0 -0
- /package/{dist/src → npm}/parseMockRequest.spec.d.ts +0 -0
- /package/{dist/src → npm}/parseMockResponse.d.ts +0 -0
- /package/{dist/src → npm}/parseMockResponse.spec.d.ts +0 -0
- /package/{dist/src → npm}/randomString.d.ts +0 -0
- /package/{dist/src → npm}/responses.d.ts +0 -0
- /package/{dist/src → npm}/sortQueryString.spec.d.ts +0 -0
package/npm/mock.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { registerResponse } from './responses.js';
|
|
2
|
+
export const mockResponse = (db, responsesTable)=>async (methodPathQuery, response, keep)=>{
|
|
3
|
+
const [method, pathWithQuery] = methodPathQuery.split(' ', 2);
|
|
4
|
+
if (!/^[A-Z]+$/.test(method ?? '')) throw new Error(`Invalid method ${method} in ${methodPathQuery}!`);
|
|
5
|
+
if (pathWithQuery === undefined) throw new Error(`Missing path in ${methodPathQuery}!`);
|
|
6
|
+
const [path, query] = pathWithQuery.split('?', 2);
|
|
7
|
+
if (path.startsWith('/')) throw new Error(`Path ${path} must not start with /!`);
|
|
8
|
+
const bodyParts = [];
|
|
9
|
+
if (response.headers !== undefined) {
|
|
10
|
+
for (const [k, v] of response.headers.entries()){
|
|
11
|
+
bodyParts.push(`${k}: ${v}`);
|
|
12
|
+
}
|
|
13
|
+
bodyParts.push('');
|
|
14
|
+
}
|
|
15
|
+
if (response.body !== undefined) bodyParts.push(response.body);
|
|
16
|
+
await registerResponse(db, responsesTable, {
|
|
17
|
+
path,
|
|
18
|
+
method: method ?? 'GET',
|
|
19
|
+
queryParams: new URLSearchParams(query),
|
|
20
|
+
body: bodyParts.length > 0 ? bodyParts.join('\n') : undefined,
|
|
21
|
+
statusCode: response.status,
|
|
22
|
+
keep
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
export const mock = ({ db, responsesTable })=>({
|
|
26
|
+
response: mockResponse(db, responsesTable)
|
|
27
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const parseMockRequest = (r)=>{
|
|
2
|
+
const lines = r.split('\n');
|
|
3
|
+
const methodResourceProtol = lines.shift();
|
|
4
|
+
const blankLineLocation = lines.indexOf('');
|
|
5
|
+
const headerLines = blankLineLocation === -1 ? lines : lines.slice(0, blankLineLocation);
|
|
6
|
+
const body = blankLineLocation === -1 ? '' : lines.slice(blankLineLocation + 1).join('\n');
|
|
7
|
+
const requestInfo = /^(?<method>[A-Z]+) (?<resource>[^ ]+) (?<protocol>HTTP\/[0-9.]+)/.exec(methodResourceProtol ?? '')?.groups;
|
|
8
|
+
if (requestInfo === null) throw new Error(`Invalid request info: ${methodResourceProtol}`);
|
|
9
|
+
return {
|
|
10
|
+
...requestInfo,
|
|
11
|
+
headers: headerLines.map((s)=>s.split(':', 2)).reduce((headers, [k, v])=>({
|
|
12
|
+
...headers,
|
|
13
|
+
[k ?? '']: v?.trim()
|
|
14
|
+
}), {}),
|
|
15
|
+
body
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
export const parseMockResponse = (r)
|
|
1
|
+
export const parseMockResponse = (r)=>{
|
|
2
2
|
const lines = r.split('\n');
|
|
3
3
|
const protocolStatusCode = lines.shift();
|
|
4
4
|
const blankLineLocation = lines.indexOf('');
|
|
5
5
|
const headerLines = blankLineLocation === -1 ? lines : lines.slice(0, blankLineLocation);
|
|
6
|
-
const body = blankLineLocation === -1
|
|
7
|
-
? ''
|
|
8
|
-
: lines.slice(blankLineLocation + 1).join('\n');
|
|
6
|
+
const body = blankLineLocation === -1 ? '' : lines.slice(blankLineLocation + 1).join('\n');
|
|
9
7
|
const responseInfo = /^(?<protocol>HTTP\/[0-9.]+) (?<statusCode>[0-9]+) /.exec(protocolStatusCode ?? '')?.groups;
|
|
10
|
-
if (responseInfo === null)
|
|
11
|
-
throw new Error(`Invalid request info: ${protocolStatusCode}`);
|
|
8
|
+
if (responseInfo === null) throw new Error(`Invalid request info: ${protocolStatusCode}`);
|
|
12
9
|
return {
|
|
13
10
|
statusCode: parseInt(responseInfo.statusCode, 10),
|
|
14
11
|
protocol: responseInfo.protocol,
|
|
15
|
-
headers: headerLines
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
headers: headerLines.map((s)=>s.split(':', 2)).reduce((headers, [k, v])=>({
|
|
13
|
+
...headers,
|
|
14
|
+
[k ?? '']: v?.trim()
|
|
15
|
+
}), {}),
|
|
16
|
+
body
|
|
19
17
|
};
|
|
20
18
|
};
|
|
@@ -10,4 +10,4 @@ export type Request = {
|
|
|
10
10
|
body: string;
|
|
11
11
|
methodPathQuery: string;
|
|
12
12
|
};
|
|
13
|
-
export declare const listRequests: (db: DynamoDBClient, requestsTable: string) => Promise<
|
|
13
|
+
export declare const listRequests: (db: DynamoDBClient, requestsTable: string) => Promise<Request[]>;
|
package/npm/requests.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ScanCommand } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { unmarshall } from '@aws-sdk/util-dynamodb';
|
|
3
|
+
export const listRequests = async (db, requestsTable)=>((await db.send(new ScanCommand({
|
|
4
|
+
TableName: requestsTable
|
|
5
|
+
}))).Items ?? []).map((item)=>{
|
|
6
|
+
const i = unmarshall(item);
|
|
7
|
+
return {
|
|
8
|
+
...i,
|
|
9
|
+
headers: JSON.parse(i.headers)
|
|
10
|
+
};
|
|
11
|
+
}).sort((i1, i2)=>i1.timestamp.localeCompare(i2.timestamp));
|
|
@@ -2,7 +2,7 @@ import { PutItemCommand } from '@aws-sdk/client-dynamodb';
|
|
|
2
2
|
import { marshall } from '@aws-sdk/util-dynamodb';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { sortQuery } from './sortQueryString.js';
|
|
5
|
-
export const registerResponse = async (db, responsesTable, response)
|
|
5
|
+
export const registerResponse = async (db, responsesTable, response)=>{
|
|
6
6
|
await db.send(new PutItemCommand({
|
|
7
7
|
TableName: responsesTable,
|
|
8
8
|
Item: marshall({
|
|
@@ -11,11 +11,11 @@ export const registerResponse = async (db, responsesTable, response) => {
|
|
|
11
11
|
timestamp: new Date().toISOString(),
|
|
12
12
|
statusCode: response.statusCode,
|
|
13
13
|
body: response.body,
|
|
14
|
-
queryParams: response.queryParams !== undefined
|
|
15
|
-
? Object.fromEntries(response.queryParams)
|
|
16
|
-
: undefined,
|
|
14
|
+
queryParams: response.queryParams !== undefined ? Object.fromEntries(response.queryParams) : undefined,
|
|
17
15
|
ttl: response.ttl,
|
|
18
|
-
keep: response.keep
|
|
19
|
-
}, {
|
|
16
|
+
keep: response.keep
|
|
17
|
+
}, {
|
|
18
|
+
removeUndefinedValues: true
|
|
19
|
+
})
|
|
20
20
|
}));
|
|
21
21
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { URLSearchParams } from 'node:url';
|
|
2
2
|
export declare const sortQueryString: (mockUrl: string) => string;
|
|
3
|
-
export declare const sortQuery: (query:
|
|
3
|
+
export declare const sortQuery: (query: Record<string, string> | URLSearchParams) => string;
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { URLSearchParams } from 'node:url';
|
|
2
|
-
export const sortQueryString = (mockUrl)
|
|
2
|
+
export const sortQueryString = (mockUrl)=>{
|
|
3
3
|
const [host, query] = mockUrl.split('?', 2);
|
|
4
|
-
if (query === undefined || (query?.length ?? 0) === 0)
|
|
5
|
-
return host;
|
|
4
|
+
if (query === undefined || (query?.length ?? 0) === 0) return host;
|
|
6
5
|
return `${host}?${sortQuery(new URLSearchParams(query))}`;
|
|
7
6
|
};
|
|
8
|
-
export const sortQuery = (query)
|
|
7
|
+
export const sortQuery = (query)=>{
|
|
9
8
|
const params = [];
|
|
10
9
|
if (query instanceof URLSearchParams) {
|
|
11
|
-
query.forEach((v, k)
|
|
12
|
-
params.push([
|
|
10
|
+
query.forEach((v, k)=>{
|
|
11
|
+
params.push([
|
|
12
|
+
k,
|
|
13
|
+
v
|
|
14
|
+
]);
|
|
13
15
|
});
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
+
} else {
|
|
16
17
|
params.push(...Object.entries(query));
|
|
17
18
|
}
|
|
18
|
-
params.sort(([k1], [k2])
|
|
19
|
+
params.sort(([k1], [k2])=>(k1 ?? '').localeCompare(k2 ?? ''));
|
|
19
20
|
const sortedParams = new URLSearchParams();
|
|
20
|
-
for (const [k, v] of params)
|
|
21
|
+
for (const [k, v] of params){
|
|
21
22
|
sortedParams.append(k, v);
|
|
22
23
|
}
|
|
23
24
|
return sortedParams.toString();
|
package/package.json
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bifravst/http-api-mock",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.356",
|
|
4
4
|
"description": "Helper functions for AWS lambdas written in TypeScript.",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./*": {
|
|
7
7
|
"import": {
|
|
8
|
-
"types": "./
|
|
9
|
-
"default": "./
|
|
8
|
+
"types": "./npm/*.d.ts",
|
|
9
|
+
"default": "./npm/*.js"
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"type": "module",
|
|
14
14
|
"scripts": {
|
|
15
15
|
"prepare": "husky",
|
|
16
|
-
"prepublishOnly": "npx
|
|
17
|
-
"test": "
|
|
18
|
-
},
|
|
19
|
-
"bin": {
|
|
20
|
-
"http-api-mock": "cli.js"
|
|
16
|
+
"prepublishOnly": "node --experimental-strip-types ./.npm/compile.ts && npx tsgo src/*.ts --emitDeclarationOnly --declaration --ignoreConfig --allowImportingTsExtensions --target esnext --module nodenext --moduleResolution nodenext --outDir ./npm && mv npm/src/* npm",
|
|
17
|
+
"test": "node --no-warnings --experimental-transform-types --test \"!(node_modules|e2e-tests|integration-tests)/**/*.spec.ts\""
|
|
21
18
|
},
|
|
22
19
|
"repository": {
|
|
23
20
|
"type": "git",
|
|
@@ -44,8 +41,8 @@
|
|
|
44
41
|
]
|
|
45
42
|
},
|
|
46
43
|
"engines": {
|
|
47
|
-
"node": ">=
|
|
48
|
-
"npm": ">=
|
|
44
|
+
"node": ">=24",
|
|
45
|
+
"npm": ">=11"
|
|
49
46
|
},
|
|
50
47
|
"release": {
|
|
51
48
|
"branches": [
|
|
@@ -69,37 +66,36 @@
|
|
|
69
66
|
"access": "public"
|
|
70
67
|
},
|
|
71
68
|
"files": [
|
|
72
|
-
"
|
|
73
|
-
"dist/src",
|
|
74
|
-
"cdk",
|
|
75
|
-
"src",
|
|
76
|
-
"cli.js",
|
|
69
|
+
"npm",
|
|
77
70
|
"LICENSE",
|
|
78
71
|
"README.md"
|
|
79
72
|
],
|
|
80
73
|
"prettier": "@bifravst/prettier-config",
|
|
81
74
|
"dependencies": {
|
|
82
|
-
"@aws-sdk/client-cloudformation": "3.
|
|
83
|
-
"@aws-sdk/client-dynamodb": "3.
|
|
84
|
-
"@aws-sdk/client-sts": "3.
|
|
85
|
-
"@aws-sdk/util-dynamodb": "3.
|
|
86
|
-
"@bifravst/aws-cdk-lambda-helpers": "
|
|
75
|
+
"@aws-sdk/client-cloudformation": "3.972.0",
|
|
76
|
+
"@aws-sdk/client-dynamodb": "3.972.0",
|
|
77
|
+
"@aws-sdk/client-sts": "3.972.0",
|
|
78
|
+
"@aws-sdk/util-dynamodb": "3.972.0",
|
|
79
|
+
"@bifravst/aws-cdk-lambda-helpers": "4.0.12",
|
|
87
80
|
"@bifravst/cloudformation-helpers": "9.1.1",
|
|
88
81
|
"@bifravst/from-env": "3.0.2",
|
|
89
82
|
"@bifravst/run": "1.2.0",
|
|
90
|
-
"aws-cdk-lib": "2.
|
|
91
|
-
"cdk": "2.
|
|
83
|
+
"aws-cdk-lib": "2.235.1",
|
|
84
|
+
"cdk": "2.1102.0",
|
|
92
85
|
"chalk": "5.6.2",
|
|
93
86
|
"tsx": "4.21.0"
|
|
94
87
|
},
|
|
95
88
|
"devDependencies": {
|
|
89
|
+
"@aws-cdk/toolkit-lib": "1.14.0",
|
|
96
90
|
"@bifravst/eslint-config-typescript": "6.4.4",
|
|
97
91
|
"@bifravst/prettier-config": "1.1.17",
|
|
98
|
-
"@commitlint/config-conventional": "
|
|
99
|
-
"@types/aws-lambda": "8.10.
|
|
100
|
-
"@types/
|
|
101
|
-
"
|
|
102
|
-
"
|
|
92
|
+
"@commitlint/config-conventional": "20.3.1",
|
|
93
|
+
"@types/aws-lambda": "8.10.160",
|
|
94
|
+
"@types/command-line-args": "5.2.3",
|
|
95
|
+
"@types/node": "25.0.10",
|
|
96
|
+
"@typescript/native-preview": "7.0.0-dev.20260122.3",
|
|
97
|
+
"command-line-args": "6.0.1",
|
|
98
|
+
"commitlint": "20.3.1",
|
|
103
99
|
"husky": "9.1.7"
|
|
104
100
|
}
|
|
105
101
|
}
|
package/cdk/App.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { PackedLambda } from '@bifravst/aws-cdk-lambda-helpers'
|
|
2
|
-
import type { PackedLayer } from '@bifravst/aws-cdk-lambda-helpers/layer'
|
|
3
|
-
import { App } from 'aws-cdk-lib'
|
|
4
|
-
import { HTTPAPIMockStack } from './Stack.js'
|
|
5
|
-
|
|
6
|
-
export class HTTPAPIMockApp extends App {
|
|
7
|
-
public constructor(
|
|
8
|
-
stackName: string,
|
|
9
|
-
{
|
|
10
|
-
lambdaSources,
|
|
11
|
-
layer,
|
|
12
|
-
}: {
|
|
13
|
-
lambdaSources: {
|
|
14
|
-
httpApiMock: PackedLambda
|
|
15
|
-
}
|
|
16
|
-
layer: PackedLayer
|
|
17
|
-
},
|
|
18
|
-
) {
|
|
19
|
-
super({
|
|
20
|
-
context: {
|
|
21
|
-
isTest: true,
|
|
22
|
-
},
|
|
23
|
-
})
|
|
24
|
-
new HTTPAPIMockStack(this, stackName, { lambdaSources, layer })
|
|
25
|
-
}
|
|
26
|
-
}
|
package/cdk/Stack.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { PackedLambda } from '@bifravst/aws-cdk-lambda-helpers'
|
|
2
|
-
import { LambdaSource } from '@bifravst/aws-cdk-lambda-helpers/cdk'
|
|
3
|
-
import type { PackedLayer } from '@bifravst/aws-cdk-lambda-helpers/layer'
|
|
4
|
-
import type { App } from 'aws-cdk-lib'
|
|
5
|
-
import { CfnOutput, aws_lambda as Lambda, Stack } from 'aws-cdk-lib'
|
|
6
|
-
import { HttpApiMock } from './resources/HttpApiMock.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* This is CloudFormation stack sets up a dummy HTTP API which stores all requests in SQS for inspection
|
|
10
|
-
*/
|
|
11
|
-
export class HTTPAPIMockStack extends Stack {
|
|
12
|
-
public constructor(
|
|
13
|
-
parent: App,
|
|
14
|
-
stackName: string,
|
|
15
|
-
{
|
|
16
|
-
lambdaSources,
|
|
17
|
-
layer,
|
|
18
|
-
}: {
|
|
19
|
-
lambdaSources: {
|
|
20
|
-
httpApiMock: PackedLambda
|
|
21
|
-
}
|
|
22
|
-
layer: PackedLayer
|
|
23
|
-
},
|
|
24
|
-
) {
|
|
25
|
-
super(parent, stackName, {
|
|
26
|
-
description:
|
|
27
|
-
'Provides a mock HTTP API for testing third-party API integrations.',
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
const baseLayer = new Lambda.LayerVersion(this, 'baseLayer', {
|
|
31
|
-
layerVersionName: `${Stack.of(this).stackName}-baseLayer`,
|
|
32
|
-
code: new LambdaSource(this, {
|
|
33
|
-
id: 'baseLayer',
|
|
34
|
-
zipFilePath: layer.layerZipFilePath,
|
|
35
|
-
hash: layer.hash,
|
|
36
|
-
}).code,
|
|
37
|
-
compatibleArchitectures: [Lambda.Architecture.ARM_64],
|
|
38
|
-
compatibleRuntimes: [Lambda.Runtime.NODEJS_22_X],
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const httpMockApi = new HttpApiMock(this, {
|
|
42
|
-
lambdaSources,
|
|
43
|
-
layers: [baseLayer],
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
// Export these so the test runner can use them
|
|
47
|
-
new CfnOutput(this, 'apiURL', {
|
|
48
|
-
value: httpMockApi.api.url,
|
|
49
|
-
exportName: `${this.stackName}:apiURL`,
|
|
50
|
-
})
|
|
51
|
-
new CfnOutput(this, 'responsesTableName', {
|
|
52
|
-
value: httpMockApi.responsesTable.tableName,
|
|
53
|
-
exportName: `${this.stackName}:responsesTableName`,
|
|
54
|
-
})
|
|
55
|
-
new CfnOutput(this, 'requestsTableName', {
|
|
56
|
-
value: httpMockApi.requestsTable.tableName,
|
|
57
|
-
exportName: `${this.stackName}:requestsTableName`,
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export type StackOutputs = {
|
|
63
|
-
apiURL: string
|
|
64
|
-
requestsTableName: string
|
|
65
|
-
responsesTableName: string
|
|
66
|
-
}
|
package/cdk/http-api-mock.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { packLambdaFromPath } from '@bifravst/aws-cdk-lambda-helpers'
|
|
2
|
-
import { packLayer } from '@bifravst/aws-cdk-lambda-helpers/layer'
|
|
3
|
-
import { fromEnv } from '@bifravst/from-env'
|
|
4
|
-
import fs from 'node:fs/promises'
|
|
5
|
-
import os from 'node:os'
|
|
6
|
-
import path from 'node:path'
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import pJSON from '../package.json' assert { type: 'json' }
|
|
9
|
-
import { HTTPAPIMockApp } from './App.js'
|
|
10
|
-
|
|
11
|
-
const { stackName } = fromEnv({ stackName: 'HTTP_API_MOCK_STACK_NAME' })(
|
|
12
|
-
process.env,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
const baseDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
16
|
-
const distDir = await fs.mkdtemp(path.join(os.tmpdir(), 'temp-'))
|
|
17
|
-
const lambdasDir = path.join(distDir, 'lambdas')
|
|
18
|
-
await fs.mkdir(lambdasDir)
|
|
19
|
-
const layersDir = path.join(distDir, 'layers')
|
|
20
|
-
await fs.mkdir(layersDir)
|
|
21
|
-
|
|
22
|
-
const dependencies: Array<keyof (typeof pJSON)['dependencies']> = [
|
|
23
|
-
'@bifravst/from-env',
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
new HTTPAPIMockApp(stackName, {
|
|
27
|
-
lambdaSources: {
|
|
28
|
-
httpApiMock: await packLambdaFromPath({
|
|
29
|
-
id: 'httpApiMock',
|
|
30
|
-
sourceFilePath: 'cdk/resources/http-api-mock-lambda.ts',
|
|
31
|
-
baseDir,
|
|
32
|
-
distDir: lambdasDir,
|
|
33
|
-
}),
|
|
34
|
-
},
|
|
35
|
-
layer: await packLayer({
|
|
36
|
-
id: 'testResources',
|
|
37
|
-
dependencies,
|
|
38
|
-
baseDir,
|
|
39
|
-
distDir: layersDir,
|
|
40
|
-
}),
|
|
41
|
-
})
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { PackedLambda } from '@bifravst/aws-cdk-lambda-helpers'
|
|
2
|
-
import {
|
|
3
|
-
LambdaLogGroup,
|
|
4
|
-
LambdaSource,
|
|
5
|
-
} from '@bifravst/aws-cdk-lambda-helpers/cdk'
|
|
6
|
-
import {
|
|
7
|
-
aws_apigateway as ApiGateway,
|
|
8
|
-
Duration,
|
|
9
|
-
aws_dynamodb as DynamoDB,
|
|
10
|
-
aws_iam as IAM,
|
|
11
|
-
aws_lambda as Lambda,
|
|
12
|
-
aws_logs as Logs,
|
|
13
|
-
RemovalPolicy,
|
|
14
|
-
Resource,
|
|
15
|
-
} from 'aws-cdk-lib'
|
|
16
|
-
import type { Construct } from 'constructs'
|
|
17
|
-
|
|
18
|
-
export class HttpApiMock extends Resource {
|
|
19
|
-
public readonly api: ApiGateway.RestApi
|
|
20
|
-
public readonly requestsTable: DynamoDB.Table
|
|
21
|
-
public readonly responsesTable: DynamoDB.Table
|
|
22
|
-
|
|
23
|
-
public constructor(
|
|
24
|
-
parent: Construct,
|
|
25
|
-
{
|
|
26
|
-
lambdaSources,
|
|
27
|
-
layers,
|
|
28
|
-
}: {
|
|
29
|
-
lambdaSources: {
|
|
30
|
-
httpApiMock: PackedLambda
|
|
31
|
-
}
|
|
32
|
-
layers: Lambda.ILayerVersion[]
|
|
33
|
-
},
|
|
34
|
-
) {
|
|
35
|
-
super(parent, 'http-api-mock')
|
|
36
|
-
|
|
37
|
-
// This table will store all the requests made to the API Gateway
|
|
38
|
-
this.requestsTable = new DynamoDB.Table(this, 'requests', {
|
|
39
|
-
billingMode: DynamoDB.BillingMode.PAY_PER_REQUEST,
|
|
40
|
-
partitionKey: {
|
|
41
|
-
name: 'requestId',
|
|
42
|
-
type: DynamoDB.AttributeType.STRING,
|
|
43
|
-
},
|
|
44
|
-
sortKey: {
|
|
45
|
-
name: 'timestamp',
|
|
46
|
-
type: DynamoDB.AttributeType.STRING,
|
|
47
|
-
},
|
|
48
|
-
pointInTimeRecovery: true,
|
|
49
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
50
|
-
})
|
|
51
|
-
this.requestsTable.addGlobalSecondaryIndex({
|
|
52
|
-
indexName: 'methodPathQuery',
|
|
53
|
-
partitionKey: {
|
|
54
|
-
name: 'methodPathQuery',
|
|
55
|
-
type: DynamoDB.AttributeType.STRING,
|
|
56
|
-
},
|
|
57
|
-
projectionType: DynamoDB.ProjectionType.ALL,
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
// This table will store optional responses to be sent
|
|
61
|
-
this.responsesTable = new DynamoDB.Table(this, 'responses', {
|
|
62
|
-
billingMode: DynamoDB.BillingMode.PAY_PER_REQUEST,
|
|
63
|
-
partitionKey: {
|
|
64
|
-
name: 'responseId',
|
|
65
|
-
type: DynamoDB.AttributeType.STRING,
|
|
66
|
-
},
|
|
67
|
-
sortKey: {
|
|
68
|
-
name: 'timestamp',
|
|
69
|
-
type: DynamoDB.AttributeType.STRING,
|
|
70
|
-
},
|
|
71
|
-
pointInTimeRecovery: true,
|
|
72
|
-
removalPolicy: RemovalPolicy.DESTROY,
|
|
73
|
-
timeToLiveAttribute: 'ttl',
|
|
74
|
-
})
|
|
75
|
-
this.responsesTable.addGlobalSecondaryIndex({
|
|
76
|
-
indexName: 'methodPathQuery',
|
|
77
|
-
partitionKey: {
|
|
78
|
-
name: 'methodPathQuery',
|
|
79
|
-
type: DynamoDB.AttributeType.STRING,
|
|
80
|
-
},
|
|
81
|
-
projectionType: DynamoDB.ProjectionType.ALL,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// This lambda will publish all requests made to the API Gateway in the queue
|
|
85
|
-
const lambda = new Lambda.Function(this, 'Lambda', {
|
|
86
|
-
description:
|
|
87
|
-
'Mocks a HTTP API and stores all requests in SQS for inspection, and optionally replies with enqued responses',
|
|
88
|
-
code: new LambdaSource(this, lambdaSources.httpApiMock).code,
|
|
89
|
-
layers,
|
|
90
|
-
handler: lambdaSources.httpApiMock.handler,
|
|
91
|
-
architecture: Lambda.Architecture.ARM_64,
|
|
92
|
-
runtime: Lambda.Runtime.NODEJS_22_X,
|
|
93
|
-
timeout: Duration.seconds(5),
|
|
94
|
-
environment: {
|
|
95
|
-
REQUESTS_TABLE_NAME: this.requestsTable.tableName,
|
|
96
|
-
RESPONSES_TABLE_NAME: this.responsesTable.tableName,
|
|
97
|
-
LOG_LEVEL: this.node.tryGetContext('logLevel'),
|
|
98
|
-
NODE_NO_WARNINGS: '1',
|
|
99
|
-
},
|
|
100
|
-
...new LambdaLogGroup(this, 'LambdaLogs', Logs.RetentionDays.ONE_DAY),
|
|
101
|
-
})
|
|
102
|
-
this.responsesTable.grantReadWriteData(lambda)
|
|
103
|
-
this.requestsTable.grantReadWriteData(lambda)
|
|
104
|
-
|
|
105
|
-
// This is the API Gateway, AWS CDK automatically creates a prod stage and deployment
|
|
106
|
-
this.api = new ApiGateway.RestApi(this, 'api', {
|
|
107
|
-
restApiName: `HTTP Mock API for testing`,
|
|
108
|
-
description: 'API Gateway to test outgoing requests',
|
|
109
|
-
binaryMediaTypes: ['application/octet-stream'],
|
|
110
|
-
})
|
|
111
|
-
const proxyResource = this.api.root.addResource('{proxy+}')
|
|
112
|
-
proxyResource.addMethod('ANY', new ApiGateway.LambdaIntegration(lambda))
|
|
113
|
-
// API Gateway needs to be able to call the lambda
|
|
114
|
-
lambda.addPermission('InvokeByApiGateway', {
|
|
115
|
-
principal: new IAM.ServicePrincipal('apigateway.amazonaws.com'),
|
|
116
|
-
sourceArn: this.api.arnForExecuteApi(),
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { describe, it } from 'node:test'
|
|
3
|
-
import { checkMatchingQueryParams } from './checkMatchingQueryParams.js'
|
|
4
|
-
|
|
5
|
-
void describe('checkMatchingQueryParams', () => {
|
|
6
|
-
void it('should return true when expected is subset of actual parameters', () => {
|
|
7
|
-
const actual = {
|
|
8
|
-
param1: 'value1',
|
|
9
|
-
param2: 'value2',
|
|
10
|
-
}
|
|
11
|
-
const expected = {
|
|
12
|
-
param1: 'value1',
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const result = checkMatchingQueryParams(actual, expected)
|
|
16
|
-
assert.equal(result, true)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
void it('should return true when expected contains regular expression and it matches', () => {
|
|
20
|
-
const actual = {
|
|
21
|
-
param1: 'value1,value2,value3',
|
|
22
|
-
}
|
|
23
|
-
const expected = {
|
|
24
|
-
param1: '/\\bvalue2\\b/',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const result = checkMatchingQueryParams(actual, expected)
|
|
28
|
-
assert.equal(result, true)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
void it('should return false when expected does not match actual parameters', () => {
|
|
32
|
-
const actual = {
|
|
33
|
-
param1: 'value1',
|
|
34
|
-
param2: 'value2',
|
|
35
|
-
}
|
|
36
|
-
const expected = {
|
|
37
|
-
param1: 'value2',
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const result = checkMatchingQueryParams(actual, expected)
|
|
41
|
-
assert.equal(result, false)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
void it('should return false when actual is null', () => {
|
|
45
|
-
const actual = null
|
|
46
|
-
const expected = {
|
|
47
|
-
param1: 'value1',
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const result = checkMatchingQueryParams(actual, expected)
|
|
51
|
-
assert.equal(result, false)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
void it('should return true when expected parameters having number or boolean', () => {
|
|
55
|
-
const actual = {
|
|
56
|
-
param1: 'true',
|
|
57
|
-
param2: '1',
|
|
58
|
-
}
|
|
59
|
-
const expected = {
|
|
60
|
-
param1: true,
|
|
61
|
-
param2: 1,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const result = checkMatchingQueryParams(actual, expected)
|
|
65
|
-
assert.equal(result, true)
|
|
66
|
-
})
|
|
67
|
-
})
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
type Logger = {
|
|
2
|
-
debug: (...arg: any) => void
|
|
3
|
-
}
|
|
4
|
-
const matchRegex = /^\/(?<re>.+)\/(?<option>[gi])?$/
|
|
5
|
-
|
|
6
|
-
export const checkMatchingQueryParams = (
|
|
7
|
-
actual: Record<string, unknown> | null,
|
|
8
|
-
expected: Record<string, unknown>,
|
|
9
|
-
log?: Logger,
|
|
10
|
-
): boolean => {
|
|
11
|
-
log?.debug('checkMatchingQueryParams', { actual, expected })
|
|
12
|
-
if (actual === null) return false
|
|
13
|
-
|
|
14
|
-
// Check whether expected query parameters is subset of actual query parameters
|
|
15
|
-
for (const prop in expected) {
|
|
16
|
-
const expectedValue = expected[prop]
|
|
17
|
-
const actualValue = actual?.[prop]
|
|
18
|
-
if (actualValue === undefined) return false
|
|
19
|
-
|
|
20
|
-
if (typeof expectedValue === 'string') {
|
|
21
|
-
const match = matchRegex.exec(expectedValue)
|
|
22
|
-
if (match !== null) {
|
|
23
|
-
log?.debug('Compare using regex', { expectedValue })
|
|
24
|
-
// Expect is regex
|
|
25
|
-
const check = new RegExp(
|
|
26
|
-
match?.groups?.re ?? '',
|
|
27
|
-
match?.groups?.option,
|
|
28
|
-
).test(String(actualValue))
|
|
29
|
-
if (check === false) return false
|
|
30
|
-
} else {
|
|
31
|
-
if (actualValue !== expectedValue) return false
|
|
32
|
-
}
|
|
33
|
-
} else {
|
|
34
|
-
// All query parameters are string
|
|
35
|
-
if (actualValue !== String(expectedValue)) return false
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return true
|
|
40
|
-
}
|