@bifravst/http-api-mock 3.0.7 → 3.0.8
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 +3 -3
- package/{npm → dist}/cdk/http-api-mock.js +1 -1
- package/dist/package.json +103 -0
- package/package.json +3 -5
- package/cdk/App.ts +0 -26
- package/cdk/Stack.ts +0 -66
- package/cdk/http-api-mock.ts +0 -109
- package/cdk/resources/HttpApiMock.ts +0 -117
- 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/npm/cdk/App.ts +0 -26
- package/npm/cdk/Stack.ts +0 -66
- package/npm/cdk/http-api-mock.ts +0 -109
- package/npm/cdk/resources/HttpApiMock.ts +0 -117
- package/npm/cdk/resources/checkMatchingQueryParams.spec.ts +0 -67
- package/npm/cdk/resources/checkMatchingQueryParams.ts +0 -40
- package/npm/cdk/resources/http-api-mock-lambda.ts +0 -147
- package/npm/cdk/resources/splitMockResponse.spec.ts +0 -17
- package/npm/cdk/resources/splitMockResponse.ts +0 -25
- package/npm/package.json +0 -105
- 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/{npm → dist}/cdk/App.d.ts +0 -0
- /package/{npm → dist}/cdk/App.js +0 -0
- /package/{npm → dist}/cdk/Stack.d.ts +0 -0
- /package/{npm → dist}/cdk/Stack.js +0 -0
- /package/{npm → dist}/cdk/http-api-mock.d.ts +0 -0
- /package/{npm → dist}/cdk/resources/HttpApiMock.d.ts +0 -0
- /package/{npm → dist}/cdk/resources/HttpApiMock.js +0 -0
- /package/{npm → dist}/cdk/resources/checkMatchingQueryParams.d.ts +0 -0
- /package/{npm → dist}/cdk/resources/checkMatchingQueryParams.js +0 -0
- /package/{npm → dist}/cdk/resources/checkMatchingQueryParams.spec.d.ts +0 -0
- /package/{npm → dist}/cdk/resources/checkMatchingQueryParams.spec.js +0 -0
- /package/{npm → dist}/cdk/resources/http-api-mock-lambda.d.ts +0 -0
- /package/{npm → dist}/cdk/resources/http-api-mock-lambda.js +0 -0
- /package/{npm → dist}/cdk/resources/splitMockResponse.d.ts +0 -0
- /package/{npm → dist}/cdk/resources/splitMockResponse.js +0 -0
- /package/{npm → dist}/cdk/resources/splitMockResponse.spec.d.ts +0 -0
- /package/{npm → dist}/cdk/resources/splitMockResponse.spec.js +0 -0
- /package/{npm → dist}/e2e.spec.d.ts +0 -0
- /package/{npm → dist}/e2e.spec.js +0 -0
- /package/{npm → dist}/src/mock.d.ts +0 -0
- /package/{npm → dist}/src/mock.js +0 -0
- /package/{npm → dist}/src/mock.spec.d.ts +0 -0
- /package/{npm → dist}/src/mock.spec.js +0 -0
- /package/{npm → dist}/src/parseMockRequest.d.ts +0 -0
- /package/{npm → dist}/src/parseMockRequest.js +0 -0
- /package/{npm → dist}/src/parseMockRequest.spec.d.ts +0 -0
- /package/{npm → dist}/src/parseMockRequest.spec.js +0 -0
- /package/{npm → dist}/src/parseMockResponse.d.ts +0 -0
- /package/{npm → dist}/src/parseMockResponse.js +0 -0
- /package/{npm → dist}/src/parseMockResponse.spec.d.ts +0 -0
- /package/{npm → dist}/src/parseMockResponse.spec.js +0 -0
- /package/{npm → dist}/src/randomString.d.ts +0 -0
- /package/{npm → dist}/src/randomString.js +0 -0
- /package/{npm → dist}/src/requests.d.ts +0 -0
- /package/{npm → dist}/src/requests.js +0 -0
- /package/{npm → dist}/src/responses.d.ts +0 -0
- /package/{npm → dist}/src/responses.js +0 -0
- /package/{npm → dist}/src/sortQueryString.d.ts +0 -0
- /package/{npm → dist}/src/sortQueryString.js +0 -0
- /package/{npm → dist}/src/sortQueryString.spec.d.ts +0 -0
- /package/{npm → dist}/src/sortQueryString.spec.js +0 -0
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DeleteItemCommand,
|
|
3
|
-
DynamoDBClient,
|
|
4
|
-
PutItemCommand,
|
|
5
|
-
ScanCommand,
|
|
6
|
-
} from '@aws-sdk/client-dynamodb'
|
|
7
|
-
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
|
|
8
|
-
import type {
|
|
9
|
-
APIGatewayEvent,
|
|
10
|
-
APIGatewayProxyResult,
|
|
11
|
-
Context,
|
|
12
|
-
} from 'aws-lambda'
|
|
13
|
-
import { URLSearchParams } from 'url'
|
|
14
|
-
import { sortQueryString } from '../../src/sortQueryString.ts'
|
|
15
|
-
import { checkMatchingQueryParams } from './checkMatchingQueryParams.ts'
|
|
16
|
-
import { splitMockResponse } from './splitMockResponse.ts'
|
|
17
|
-
|
|
18
|
-
const db = new DynamoDBClient({})
|
|
19
|
-
|
|
20
|
-
export const handler = async (
|
|
21
|
-
event: APIGatewayEvent,
|
|
22
|
-
context: Context,
|
|
23
|
-
): Promise<APIGatewayProxyResult> => {
|
|
24
|
-
console.log(JSON.stringify({ event }))
|
|
25
|
-
const query =
|
|
26
|
-
event.queryStringParameters !== null &&
|
|
27
|
-
event.queryStringParameters !== undefined
|
|
28
|
-
? new URLSearchParams(
|
|
29
|
-
event.queryStringParameters as Record<string, string>,
|
|
30
|
-
)
|
|
31
|
-
: undefined
|
|
32
|
-
const path = event.path.replace(/^\//, '')
|
|
33
|
-
const pathWithQuery = sortQueryString(
|
|
34
|
-
`${path}${query !== undefined ? `?${query.toString()}` : ''}`,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
const request = {
|
|
38
|
-
methodPathQuery: `${event.httpMethod} ${pathWithQuery}`,
|
|
39
|
-
timestamp: new Date().toISOString(),
|
|
40
|
-
requestId: context.awsRequestId,
|
|
41
|
-
method: event.httpMethod,
|
|
42
|
-
path,
|
|
43
|
-
query: query === undefined ? null : Object.fromEntries(query),
|
|
44
|
-
body: event.body ?? '{}',
|
|
45
|
-
headers: JSON.stringify(event.headers),
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Check if response exists
|
|
49
|
-
console.debug(
|
|
50
|
-
`Checking if response exists for ${event.httpMethod} ${pathWithQuery}...`,
|
|
51
|
-
)
|
|
52
|
-
// Scan using httpMethod and path only so query strings can be partially matched
|
|
53
|
-
const { Items } = await db.send(
|
|
54
|
-
new ScanCommand({
|
|
55
|
-
TableName: process.env.RESPONSES_TABLE_NAME,
|
|
56
|
-
FilterExpression: 'begins_with(methodPathQuery, :methodPath)',
|
|
57
|
-
ExpressionAttributeValues: {
|
|
58
|
-
[':methodPath']: {
|
|
59
|
-
S: `${event.httpMethod} ${path}`,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
}),
|
|
63
|
-
)
|
|
64
|
-
console.debug(
|
|
65
|
-
`Found response items beginning with same path: ${Items?.length}`,
|
|
66
|
-
)
|
|
67
|
-
// use newest response first
|
|
68
|
-
const itemsByTimestampDesc = (Items ?? [])
|
|
69
|
-
.map((Item) => unmarshall(Item))
|
|
70
|
-
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
71
|
-
|
|
72
|
-
let res: APIGatewayProxyResult = {
|
|
73
|
-
statusCode: 404,
|
|
74
|
-
body: 'No responses found',
|
|
75
|
-
}
|
|
76
|
-
for (const objItem of itemsByTimestampDesc) {
|
|
77
|
-
const hasExpectedQueryParams =
|
|
78
|
-
'queryParams' in objItem || query !== undefined
|
|
79
|
-
const matchedQueryParams = hasExpectedQueryParams
|
|
80
|
-
? checkMatchingQueryParams(
|
|
81
|
-
event.queryStringParameters,
|
|
82
|
-
objItem.queryParams,
|
|
83
|
-
)
|
|
84
|
-
: true
|
|
85
|
-
if (matchedQueryParams === false) continue
|
|
86
|
-
|
|
87
|
-
console.debug(`Matched response`, JSON.stringify({ response: objItem }))
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
objItem?.requestId !== undefined &&
|
|
91
|
-
objItem?.timestamp !== undefined &&
|
|
92
|
-
objItem?.keep !== true
|
|
93
|
-
) {
|
|
94
|
-
await db.send(
|
|
95
|
-
new DeleteItemCommand({
|
|
96
|
-
TableName: process.env.RESPONSES_TABLE_NAME,
|
|
97
|
-
Key: marshall({
|
|
98
|
-
requestId: objItem.requestId,
|
|
99
|
-
timestamp: objItem.timestamp,
|
|
100
|
-
}),
|
|
101
|
-
}),
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const { body, headers } = splitMockResponse(objItem.body ?? '')
|
|
106
|
-
|
|
107
|
-
// Send as binary, if mock response is HEX encoded. See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html
|
|
108
|
-
const isBinary = /^[0-9a-f]+$/.test(body)
|
|
109
|
-
res = {
|
|
110
|
-
statusCode: objItem.statusCode ?? 200,
|
|
111
|
-
headers: isBinary
|
|
112
|
-
? {
|
|
113
|
-
...headers,
|
|
114
|
-
'Content-Type': 'application/octet-stream',
|
|
115
|
-
}
|
|
116
|
-
: headers,
|
|
117
|
-
body: isBinary
|
|
118
|
-
? /* body is HEX encoded */ Buffer.from(body, 'hex').toString('base64')
|
|
119
|
-
: body,
|
|
120
|
-
isBase64Encoded: isBinary,
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
break
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
console.debug(`Return response`, JSON.stringify({ response: res }))
|
|
127
|
-
|
|
128
|
-
await db.send(
|
|
129
|
-
new PutItemCommand({
|
|
130
|
-
TableName: process.env.REQUESTS_TABLE_NAME,
|
|
131
|
-
Item: marshall(
|
|
132
|
-
{
|
|
133
|
-
...request,
|
|
134
|
-
responseStatusCode: res.statusCode,
|
|
135
|
-
responseHeaders: res.headers,
|
|
136
|
-
responseBody: res.body,
|
|
137
|
-
responseIsBase64Encoded: res.isBase64Encoded,
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
removeUndefinedValues: true,
|
|
141
|
-
},
|
|
142
|
-
),
|
|
143
|
-
}),
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
return res
|
|
147
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
|
-
import { describe, it } from 'node:test'
|
|
3
|
-
import { splitMockResponse } from './splitMockResponse.ts'
|
|
4
|
-
void describe('split mock response', () => {
|
|
5
|
-
void it('should parse headers and body', () =>
|
|
6
|
-
assert.deepEqual(
|
|
7
|
-
splitMockResponse(`Content-Type: application/octet-stream
|
|
8
|
-
|
|
9
|
-
(binary A-GNSS data) other types`),
|
|
10
|
-
{
|
|
11
|
-
headers: {
|
|
12
|
-
'Content-Type': 'application/octet-stream',
|
|
13
|
-
},
|
|
14
|
-
body: '(binary A-GNSS data) other types',
|
|
15
|
-
},
|
|
16
|
-
))
|
|
17
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export const splitMockResponse = (
|
|
2
|
-
r: string,
|
|
3
|
-
): { headers: Record<string, string>; body: string } => {
|
|
4
|
-
const trimmedLines = r
|
|
5
|
-
.split('\n')
|
|
6
|
-
.map((s) => s.trim())
|
|
7
|
-
.join('\n')
|
|
8
|
-
const blankLineLocation = trimmedLines.indexOf('\n\n')
|
|
9
|
-
if (blankLineLocation === -1)
|
|
10
|
-
return {
|
|
11
|
-
headers: {},
|
|
12
|
-
body: trimmedLines,
|
|
13
|
-
}
|
|
14
|
-
return {
|
|
15
|
-
headers: trimmedLines
|
|
16
|
-
.slice(0, blankLineLocation)
|
|
17
|
-
.split('\n')
|
|
18
|
-
.map((s) => s.split(':', 2))
|
|
19
|
-
.reduce(
|
|
20
|
-
(headers, [k, v]) => ({ ...headers, [k as string]: v?.trim() }),
|
|
21
|
-
{},
|
|
22
|
-
),
|
|
23
|
-
body: trimmedLines.slice(blankLineLocation + 2),
|
|
24
|
-
}
|
|
25
|
-
}
|
package/npm/package.json
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bifravst/http-api-mock",
|
|
3
|
-
"version": "3.0.7",
|
|
4
|
-
"description": "Helper functions for AWS lambdas written in TypeScript.",
|
|
5
|
-
"exports": {
|
|
6
|
-
"./*": {
|
|
7
|
-
"import": {
|
|
8
|
-
"default": "./npm/src/*.js",
|
|
9
|
-
"types": "./npm/src/*.d.ts"
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
"type": "module",
|
|
14
|
-
"scripts": {
|
|
15
|
-
"prepare": "husky && case \"$npm_command\" in install|ci) check-node-version --package ;; esac",
|
|
16
|
-
"prepublishOnly": "npx tsc --noEmit false --outDir ./npm -d && cp -rv cdk/* npm/cdk && cp package.json npm/package.json",
|
|
17
|
-
"test": "node --no-warnings --experimental-transform-types --test \"!(node_modules|e2e-tests|integration-tests)/**/*.spec.ts\""
|
|
18
|
-
},
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/bifravst/http-api-mock.git"
|
|
22
|
-
},
|
|
23
|
-
"bugs": {
|
|
24
|
-
"url": "https://github.com/bifravst/http-api-mock/issues"
|
|
25
|
-
},
|
|
26
|
-
"homepage": "https://github.com/bifravst/http-api-mock",
|
|
27
|
-
"keywords": [
|
|
28
|
-
"aws",
|
|
29
|
-
"lambda",
|
|
30
|
-
"typescript"
|
|
31
|
-
],
|
|
32
|
-
"author": "Nordic Semiconductor ASA | nordicsemi.no",
|
|
33
|
-
"license": "BSD-3-Clause",
|
|
34
|
-
"lint-staged": {
|
|
35
|
-
"*.ts": [
|
|
36
|
-
"prettier --write",
|
|
37
|
-
"eslint"
|
|
38
|
-
],
|
|
39
|
-
"*.{md,json,yaml,yml}": [
|
|
40
|
-
"prettier --write"
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
"engines": {
|
|
44
|
-
"node": ">=24.19.0 <25",
|
|
45
|
-
"npm": ">=12.0.2 <13"
|
|
46
|
-
},
|
|
47
|
-
"release": {
|
|
48
|
-
"branches": [
|
|
49
|
-
"saga"
|
|
50
|
-
],
|
|
51
|
-
"remoteTags": true,
|
|
52
|
-
"plugins": [
|
|
53
|
-
"@semantic-release/commit-analyzer",
|
|
54
|
-
"@semantic-release/release-notes-generator",
|
|
55
|
-
"@semantic-release/npm",
|
|
56
|
-
[
|
|
57
|
-
"@semantic-release/github",
|
|
58
|
-
{
|
|
59
|
-
"successCommentCondition": false,
|
|
60
|
-
"failTitle": false
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
]
|
|
64
|
-
},
|
|
65
|
-
"publishConfig": {
|
|
66
|
-
"access": "public"
|
|
67
|
-
},
|
|
68
|
-
"files": [
|
|
69
|
-
"npm",
|
|
70
|
-
"cdk",
|
|
71
|
-
"src",
|
|
72
|
-
"LICENSE",
|
|
73
|
-
"README.md"
|
|
74
|
-
],
|
|
75
|
-
"prettier": "@bifravst/prettier-config",
|
|
76
|
-
"dependencies": {
|
|
77
|
-
"@aws-sdk/client-cloudformation": "3.1111.0",
|
|
78
|
-
"@aws-sdk/client-dynamodb": "3.1111.0",
|
|
79
|
-
"@aws-sdk/client-sts": "3.1111.0",
|
|
80
|
-
"@aws-cdk/toolkit-lib": "1.38.2",
|
|
81
|
-
"@aws-sdk/util-dynamodb": "3.996.9",
|
|
82
|
-
"@bifravst/aws-cdk-lambda-helpers": "5.0.4",
|
|
83
|
-
"@bifravst/cloudformation-helpers": "10.0.1",
|
|
84
|
-
"@bifravst/from-env": "4.0.0",
|
|
85
|
-
"@bifravst/run": "2.0.1",
|
|
86
|
-
"aws-cdk-lib": "2.265.0",
|
|
87
|
-
"cdk": "2.1136.0",
|
|
88
|
-
"chalk": "6.0.0",
|
|
89
|
-
"command-line-args": "6.0.2"
|
|
90
|
-
},
|
|
91
|
-
"devDependencies": {
|
|
92
|
-
"@bifravst/eslint-config-typescript": "8.0.0",
|
|
93
|
-
"@bifravst/prettier-config": "2.0.0",
|
|
94
|
-
"@commitlint/config-conventional": "21.2.2",
|
|
95
|
-
"@types/aws-lambda": "8.10.162",
|
|
96
|
-
"@types/command-line-args": "5.2.3",
|
|
97
|
-
"@types/node": "26.2.0",
|
|
98
|
-
"@typescript/native": "npm:typescript@7.0.2",
|
|
99
|
-
"check-node-version": "4.2.1",
|
|
100
|
-
"commitlint": "21.2.2",
|
|
101
|
-
"husky": "9.1.7",
|
|
102
|
-
"lint-staged": "17.3.0",
|
|
103
|
-
"typescript": "npm:@typescript/typescript6@6.0.2"
|
|
104
|
-
}
|
|
105
|
-
}
|
package/src/mock.spec.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { AttributeValue, DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
-
import { unmarshall } from '@aws-sdk/util-dynamodb'
|
|
3
|
-
import assert from 'node:assert/strict'
|
|
4
|
-
import { describe, it, mock as testMock } from 'node:test'
|
|
5
|
-
import { mock } from './mock.ts'
|
|
6
|
-
|
|
7
|
-
void describe('mock()', () => {
|
|
8
|
-
void it('should register a response', async () => {
|
|
9
|
-
const db = {
|
|
10
|
-
send: testMock.fn(async () => Promise.resolve(undefined)),
|
|
11
|
-
}
|
|
12
|
-
const httpApiMock = mock({
|
|
13
|
-
db: db as unknown as DynamoDBClient,
|
|
14
|
-
responsesTable: 'response-table',
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
await httpApiMock.response(`GET foo/bar?k=v`, {
|
|
18
|
-
status: 200,
|
|
19
|
-
headers: new Headers({
|
|
20
|
-
'content-type': 'application/json; charset=utf-8',
|
|
21
|
-
}),
|
|
22
|
-
body: JSON.stringify({
|
|
23
|
-
result: 'some-value',
|
|
24
|
-
}),
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
assert.equal(db.send.mock.callCount(), 1)
|
|
28
|
-
const [{ input: args }] = db.send.mock.calls[0]?.arguments as unknown as [
|
|
29
|
-
{
|
|
30
|
-
input: {
|
|
31
|
-
TableName: string
|
|
32
|
-
Item: Record<string, AttributeValue>
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
]
|
|
36
|
-
assert.equal(args.TableName, 'response-table')
|
|
37
|
-
const { methodPathQuery, statusCode, body, queryParams } = unmarshall(
|
|
38
|
-
args.Item,
|
|
39
|
-
)
|
|
40
|
-
assert.equal(statusCode, 200)
|
|
41
|
-
assert.equal(methodPathQuery, 'GET foo/bar?k=v')
|
|
42
|
-
assert.equal(
|
|
43
|
-
body,
|
|
44
|
-
[
|
|
45
|
-
`content-type: application/json; charset=utf-8`,
|
|
46
|
-
``,
|
|
47
|
-
JSON.stringify({ result: 'some-value' }),
|
|
48
|
-
].join('\n'),
|
|
49
|
-
)
|
|
50
|
-
assert.deepEqual(queryParams, { k: 'v' })
|
|
51
|
-
})
|
|
52
|
-
})
|
package/src/mock.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import type { DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
-
import { registerResponse } from './responses.ts'
|
|
3
|
-
|
|
4
|
-
type MockResponseFn = (
|
|
5
|
-
// The expected request in the form 'GET resource/subresource?query=value
|
|
6
|
-
methodPathQuery: string,
|
|
7
|
-
// The response
|
|
8
|
-
response: Partial<{
|
|
9
|
-
headers: Headers
|
|
10
|
-
status: number
|
|
11
|
-
body: string
|
|
12
|
-
}>,
|
|
13
|
-
keep?: boolean,
|
|
14
|
-
) => Promise<void>
|
|
15
|
-
|
|
16
|
-
export const mockResponse =
|
|
17
|
-
(db: DynamoDBClient, responsesTable: string): MockResponseFn =>
|
|
18
|
-
async (methodPathQuery, response, keep) => {
|
|
19
|
-
const [method, pathWithQuery] = methodPathQuery.split(' ', 2)
|
|
20
|
-
if (!/^[A-Z]+$/.test(method ?? ''))
|
|
21
|
-
throw new Error(`Invalid method ${method} in ${methodPathQuery}!`)
|
|
22
|
-
if (pathWithQuery === undefined)
|
|
23
|
-
throw new Error(`Missing path in ${methodPathQuery}!`)
|
|
24
|
-
const [path, query] = pathWithQuery.split('?', 2) as [
|
|
25
|
-
string,
|
|
26
|
-
string | undefined,
|
|
27
|
-
]
|
|
28
|
-
if (path.startsWith('/'))
|
|
29
|
-
throw new Error(`Path ${path} must not start with /!`)
|
|
30
|
-
|
|
31
|
-
const bodyParts = []
|
|
32
|
-
if (response.headers !== undefined) {
|
|
33
|
-
for (const [k, v] of response.headers.entries()) {
|
|
34
|
-
bodyParts.push(`${k}: ${v}`)
|
|
35
|
-
}
|
|
36
|
-
bodyParts.push('')
|
|
37
|
-
}
|
|
38
|
-
if (response.body !== undefined) bodyParts.push(response.body)
|
|
39
|
-
await registerResponse(db, responsesTable, {
|
|
40
|
-
path,
|
|
41
|
-
method: method ?? 'GET',
|
|
42
|
-
queryParams: new URLSearchParams(query),
|
|
43
|
-
body: bodyParts.length > 0 ? bodyParts.join('\n') : undefined,
|
|
44
|
-
statusCode: response.status,
|
|
45
|
-
keep,
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type HttpAPIMock = {
|
|
50
|
-
response: MockResponseFn
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export const mock = ({
|
|
54
|
-
db,
|
|
55
|
-
responsesTable,
|
|
56
|
-
}: {
|
|
57
|
-
db: DynamoDBClient
|
|
58
|
-
responsesTable: string
|
|
59
|
-
}): HttpAPIMock => ({
|
|
60
|
-
response: mockResponse(db, responsesTable),
|
|
61
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { describe, it } from 'node:test'
|
|
3
|
-
import { parseMockRequest } from './parseMockRequest.ts'
|
|
4
|
-
|
|
5
|
-
void describe('parseMockRequest()', () => {
|
|
6
|
-
void it('should parse method, resource, protocol, headers and body', () =>
|
|
7
|
-
assert.deepEqual(
|
|
8
|
-
parseMockRequest(
|
|
9
|
-
[
|
|
10
|
-
`PATCH /v1/devices/foo/state HTTP/1.1`,
|
|
11
|
-
`Content-Length: 36`,
|
|
12
|
-
`Content-Type: application/json`,
|
|
13
|
-
`If-Match: 8835`,
|
|
14
|
-
``,
|
|
15
|
-
`{"desired":{"config":{"nod":null}}}`,
|
|
16
|
-
].join('\n'),
|
|
17
|
-
),
|
|
18
|
-
{
|
|
19
|
-
method: 'PATCH',
|
|
20
|
-
resource: '/v1/devices/foo/state',
|
|
21
|
-
protocol: 'HTTP/1.1',
|
|
22
|
-
headers: {
|
|
23
|
-
'Content-Length': '36',
|
|
24
|
-
'Content-Type': 'application/json',
|
|
25
|
-
'If-Match': '8835',
|
|
26
|
-
},
|
|
27
|
-
body: '{"desired":{"config":{"nod":null}}}',
|
|
28
|
-
},
|
|
29
|
-
))
|
|
30
|
-
})
|
package/src/parseMockRequest.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export const parseMockRequest = (
|
|
2
|
-
r: string,
|
|
3
|
-
): {
|
|
4
|
-
method: string
|
|
5
|
-
resource: string
|
|
6
|
-
protocol: string // 'HTTP/1.0' | 'HTTP/1.1'
|
|
7
|
-
headers: Record<string, string>
|
|
8
|
-
body: string
|
|
9
|
-
} => {
|
|
10
|
-
const lines = r.split('\n')
|
|
11
|
-
const methodResourceProtol = lines.shift()
|
|
12
|
-
const blankLineLocation = lines.indexOf('')
|
|
13
|
-
const headerLines =
|
|
14
|
-
blankLineLocation === -1 ? lines : lines.slice(0, blankLineLocation)
|
|
15
|
-
const body =
|
|
16
|
-
blankLineLocation === -1
|
|
17
|
-
? ''
|
|
18
|
-
: lines.slice(blankLineLocation + 1).join('\n')
|
|
19
|
-
|
|
20
|
-
const requestInfo =
|
|
21
|
-
/^(?<method>[A-Z]+) (?<resource>[^ ]+) (?<protocol>HTTP\/[0-9.]+)/.exec(
|
|
22
|
-
methodResourceProtol ?? '',
|
|
23
|
-
)?.groups as { method: string; resource: string; protocol: string }
|
|
24
|
-
|
|
25
|
-
if (requestInfo === null)
|
|
26
|
-
throw new Error(`Invalid request info: ${methodResourceProtol}`)
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
...requestInfo,
|
|
30
|
-
headers: headerLines
|
|
31
|
-
.map((s) => s.split(':', 2))
|
|
32
|
-
.reduce((headers, [k, v]) => ({ ...headers, [k ?? '']: v?.trim() }), {}),
|
|
33
|
-
body,
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { describe, it } from 'node:test'
|
|
3
|
-
import { parseMockResponse } from './parseMockResponse.ts'
|
|
4
|
-
|
|
5
|
-
void describe('parseMockResponse()', () => {
|
|
6
|
-
void it('should parse protocol, statusCode, headers and body', () =>
|
|
7
|
-
assert.deepEqual(
|
|
8
|
-
parseMockResponse(
|
|
9
|
-
[
|
|
10
|
-
`HTTP/1.1 202 Accepted`,
|
|
11
|
-
`Content-Length: 36`,
|
|
12
|
-
`Content-Type: application/json`,
|
|
13
|
-
``,
|
|
14
|
-
`{"desired":{"config":{"nod":null}}}`,
|
|
15
|
-
].join('\n'),
|
|
16
|
-
),
|
|
17
|
-
{
|
|
18
|
-
statusCode: 202,
|
|
19
|
-
protocol: 'HTTP/1.1',
|
|
20
|
-
headers: {
|
|
21
|
-
'Content-Length': '36',
|
|
22
|
-
'Content-Type': 'application/json',
|
|
23
|
-
},
|
|
24
|
-
body: '{"desired":{"config":{"nod":null}}}',
|
|
25
|
-
},
|
|
26
|
-
))
|
|
27
|
-
})
|
package/src/parseMockResponse.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export const parseMockResponse = (
|
|
2
|
-
r: string,
|
|
3
|
-
): {
|
|
4
|
-
statusCode: number
|
|
5
|
-
protocol: string // 'HTTP/1.0' | 'HTTP/1.1'
|
|
6
|
-
headers: Record<string, string>
|
|
7
|
-
body: string
|
|
8
|
-
} => {
|
|
9
|
-
const lines = r.split('\n')
|
|
10
|
-
const protocolStatusCode = lines.shift()
|
|
11
|
-
const blankLineLocation = lines.indexOf('')
|
|
12
|
-
const headerLines =
|
|
13
|
-
blankLineLocation === -1 ? lines : lines.slice(0, blankLineLocation)
|
|
14
|
-
const body =
|
|
15
|
-
blankLineLocation === -1
|
|
16
|
-
? ''
|
|
17
|
-
: lines.slice(blankLineLocation + 1).join('\n')
|
|
18
|
-
|
|
19
|
-
const responseInfo =
|
|
20
|
-
/^(?<protocol>HTTP\/[0-9.]+) (?<statusCode>[0-9]+) /.exec(
|
|
21
|
-
protocolStatusCode ?? '',
|
|
22
|
-
)?.groups as { statusCode: string; protocol: string }
|
|
23
|
-
|
|
24
|
-
if (responseInfo === null)
|
|
25
|
-
throw new Error(`Invalid request info: ${protocolStatusCode}`)
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
statusCode: parseInt(responseInfo.statusCode, 10),
|
|
29
|
-
protocol: responseInfo.protocol,
|
|
30
|
-
headers: headerLines
|
|
31
|
-
.map((s) => s.split(':', 2))
|
|
32
|
-
.reduce((headers, [k, v]) => ({ ...headers, [k ?? '']: v?.trim() }), {}),
|
|
33
|
-
body,
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/randomString.ts
DELETED
package/src/requests.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { ScanCommand, type DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
-
import { unmarshall } from '@aws-sdk/util-dynamodb'
|
|
3
|
-
|
|
4
|
-
export type Request = {
|
|
5
|
-
path: string //e.g.'555c3960-2092-438b-b2b0-f28eebd1f5bb'
|
|
6
|
-
query: null
|
|
7
|
-
timestamp: string //e.g.'2024-04-05T13:01:14.434Z'
|
|
8
|
-
ttl: string //e.g. 1712322374
|
|
9
|
-
headers: Record<string, string> //e.g. '{"Accept":"*/*","Accept-Encoding":"br, gzip, deflate","Accept-Language":"*","CloudFront-Forwarded-Proto":"https","CloudFront-Is-Desktop-Viewer":"true","CloudFront-Is-Mobile-Viewer":"false","CloudFront-Is-SmartTV-Viewer":"false","CloudFront-Is-Tablet-Viewer":"false","CloudFront-Viewer-ASN":"2116","CloudFront-Viewer-Country":"NO","Host":"idj1fffo0k.execute-api.eu-west-1.amazonaws.com","sec-fetch-mode":"cors","User-Agent":"node","Via":"1.1 b053873243f91b1bb6dc406ce0c67db4.cloudfront.net (CloudFront)","X-Amz-Cf-Id":"_vJIGo6Z89QxDzoqOZL4G0PQqPFWGesVXVan4ND934_Urqn2ifSOsQ==","X-Amzn-Trace-Id":"Root=1-660ff61a-25e1219a7f153e1b0c768358","X-Forwarded-For":"194.19.86.146, 130.176.182.18","X-Forwarded-Port":"443","X-Forwarded-Proto":"https"}'
|
|
10
|
-
method: string //e.g.'GET'
|
|
11
|
-
requestId: string //e.g.'f34b042b-e9a2-4089-97a2-241516d40d64'
|
|
12
|
-
body: string //e.g.'{}'
|
|
13
|
-
methodPathQuery: string //e.g.'GET 555c3960-2092-438b-b2b0-f28eebd1f5bb'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const listRequests = async (
|
|
17
|
-
db: DynamoDBClient,
|
|
18
|
-
requestsTable: string,
|
|
19
|
-
): Promise<Array<Request>> =>
|
|
20
|
-
((await db.send(new ScanCommand({ TableName: requestsTable }))).Items ?? [])
|
|
21
|
-
.map((item) => {
|
|
22
|
-
const i = unmarshall(item)
|
|
23
|
-
return {
|
|
24
|
-
...i,
|
|
25
|
-
headers: JSON.parse(i.headers),
|
|
26
|
-
} as Request
|
|
27
|
-
})
|
|
28
|
-
.sort((i1, i2) => i1.timestamp.localeCompare(i2.timestamp))
|
package/src/responses.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { PutItemCommand, type DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
-
import { marshall } from '@aws-sdk/util-dynamodb'
|
|
3
|
-
import { randomUUID } from 'node:crypto'
|
|
4
|
-
import { sortQuery } from './sortQueryString.ts'
|
|
5
|
-
|
|
6
|
-
export type Response = {
|
|
7
|
-
// e.g. 'GET'
|
|
8
|
-
method: string
|
|
9
|
-
// without leading slash
|
|
10
|
-
path: string
|
|
11
|
-
queryParams?: URLSearchParams
|
|
12
|
-
statusCode?: number
|
|
13
|
-
/**
|
|
14
|
-
* Header + Body
|
|
15
|
-
*
|
|
16
|
-
* @see splitMockResponse
|
|
17
|
-
*/
|
|
18
|
-
body?: string
|
|
19
|
-
ttl?: number
|
|
20
|
-
// Whether to delete the message after sending it
|
|
21
|
-
keep?: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const registerResponse = async (
|
|
25
|
-
db: DynamoDBClient,
|
|
26
|
-
responsesTable: string,
|
|
27
|
-
response: Response,
|
|
28
|
-
): Promise<void> => {
|
|
29
|
-
await db.send(
|
|
30
|
-
new PutItemCommand({
|
|
31
|
-
TableName: responsesTable,
|
|
32
|
-
Item: marshall(
|
|
33
|
-
{
|
|
34
|
-
responseId: randomUUID(),
|
|
35
|
-
methodPathQuery: `${response.method} ${response.path}${response.queryParams !== undefined ? `?${sortQuery(response.queryParams)}` : ``}`,
|
|
36
|
-
timestamp: new Date().toISOString(),
|
|
37
|
-
statusCode: response.statusCode,
|
|
38
|
-
body: response.body,
|
|
39
|
-
queryParams:
|
|
40
|
-
response.queryParams !== undefined
|
|
41
|
-
? Object.fromEntries(response.queryParams)
|
|
42
|
-
: undefined,
|
|
43
|
-
ttl: response.ttl,
|
|
44
|
-
keep: response.keep,
|
|
45
|
-
},
|
|
46
|
-
{ removeUndefinedValues: true },
|
|
47
|
-
),
|
|
48
|
-
}),
|
|
49
|
-
)
|
|
50
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert'
|
|
2
|
-
import { describe, test as it } from 'node:test'
|
|
3
|
-
import { URLSearchParams } from 'node:url'
|
|
4
|
-
import { sortQuery, sortQueryString } from './sortQueryString.ts'
|
|
5
|
-
|
|
6
|
-
void describe('sortQueryString', () => {
|
|
7
|
-
void it('should sort the query part of a mock URL', () =>
|
|
8
|
-
assert.deepStrictEqual(
|
|
9
|
-
sortQueryString(
|
|
10
|
-
'api.nrfcloud.com/v1/location/agps?eci=73393515&tac=132&requestType=custom&mcc=397&mnc=73&customTypes=2',
|
|
11
|
-
),
|
|
12
|
-
'api.nrfcloud.com/v1/location/agps?customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132',
|
|
13
|
-
))
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
void describe('sortQuery', () => {
|
|
17
|
-
void it('should sort URLSearchParams', () =>
|
|
18
|
-
assert.equal(
|
|
19
|
-
sortQuery(
|
|
20
|
-
new URLSearchParams(
|
|
21
|
-
'eci=73393515&tac=132&requestType=custom&mcc=397&mnc=73&customTypes=2',
|
|
22
|
-
),
|
|
23
|
-
),
|
|
24
|
-
'customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132',
|
|
25
|
-
))
|
|
26
|
-
void it('should sort a Record', () =>
|
|
27
|
-
assert.equal(
|
|
28
|
-
sortQuery({
|
|
29
|
-
eci: '73393515',
|
|
30
|
-
tac: '132',
|
|
31
|
-
requestType: 'custom',
|
|
32
|
-
mcc: '397',
|
|
33
|
-
mnc: '73',
|
|
34
|
-
customTypes: '2',
|
|
35
|
-
}),
|
|
36
|
-
'customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132',
|
|
37
|
-
))
|
|
38
|
-
})
|