@bifravst/http-api-mock 1.1.0 → 1.1.2
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/cdk/http-api-mock.ts +3 -1
- package/dist/cdk/http-api-mock.js +1 -1
- package/dist/lambdas/httpApiMock.zip +0 -0
- package/dist/layers/testResources.zip +0 -0
- package/dist/src/cli.js +2 -2
- package/package.json +2 -1
- package/src/cli.ts +109 -0
- package/src/randomString.ts +7 -0
- package/src/requests.ts +29 -0
- package/src/responses.ts +40 -0
- package/src/sortQueryString.spec.ts +13 -0
- package/src/sortQueryString.ts +14 -0
package/cdk/http-api-mock.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { fromEnv } from '@nordicsemiconductor/from-env'
|
|
|
5
5
|
import path, { dirname } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
|
|
8
|
-
const { stackName } = fromEnv({ stackName: '
|
|
8
|
+
const { stackName } = fromEnv({ stackName: 'HTTP_API_MOCK_STACK_NAME' })(
|
|
9
|
+
process.env,
|
|
10
|
+
)
|
|
9
11
|
|
|
10
12
|
new HTTPAPIMockApp(stackName, {
|
|
11
13
|
lambdaSources: {
|
|
@@ -4,7 +4,7 @@ import { HTTPAPIMockApp } from './App.js';
|
|
|
4
4
|
import { fromEnv } from '@nordicsemiconductor/from-env';
|
|
5
5
|
import path, { dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
const { stackName } = fromEnv({ stackName: '
|
|
7
|
+
const { stackName } = fromEnv({ stackName: 'HTTP_API_MOCK_STACK_NAME' })(process.env);
|
|
8
8
|
new HTTPAPIMockApp(stackName, {
|
|
9
9
|
lambdaSources: {
|
|
10
10
|
httpApiMock: await packLambdaFromPath('httpApiMock', 'cdk/resources/http-api-mock-lambda.ts', undefined, path.join(dirname(fileURLToPath(import.meta.url)), '..')),
|
|
Binary file
|
|
Binary file
|
package/dist/src/cli.js
CHANGED
|
@@ -35,8 +35,8 @@ export const cli = async () => {
|
|
|
35
35
|
command: 'npx',
|
|
36
36
|
args: ['cdk', ...cdkApp(), 'deploy', '--require-approval', 'never'],
|
|
37
37
|
env: {
|
|
38
|
-
STACK_NAME: stackName,
|
|
39
38
|
...process.env,
|
|
39
|
+
HTTP_API_MOCK_STACK_NAME: stackName,
|
|
40
40
|
},
|
|
41
41
|
log: {
|
|
42
42
|
debug: (msg) => console.error(chalk.blueBright('[CDK]'), chalk.blue(msg)),
|
|
@@ -64,8 +64,8 @@ const destroy = async (stackName) => {
|
|
|
64
64
|
command: 'npx',
|
|
65
65
|
args: ['cdk', ...cdkApp(), 'destroy', '-f'],
|
|
66
66
|
env: {
|
|
67
|
-
STACK_NAME: stackName,
|
|
68
67
|
...process.env,
|
|
68
|
+
HTTP_API_MOCK_STACK_NAME: stackName,
|
|
69
69
|
},
|
|
70
70
|
log: {
|
|
71
71
|
debug: (msg) => console.error(chalk.blueBright('[CDK]'), chalk.blue(msg)),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bifravst/http-api-mock",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Helper functions for AWS lambdas written in TypeScript.",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./*": {
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"package-lock.json",
|
|
84
84
|
"dist",
|
|
85
85
|
"cdk",
|
|
86
|
+
"src",
|
|
86
87
|
"cli.js",
|
|
87
88
|
"LICENSE",
|
|
88
89
|
"README.md"
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { randomString } from './randomString.js'
|
|
2
|
+
import run from '@bifravst/run'
|
|
3
|
+
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { stackOutput } from '@nordicsemiconductor/cloudformation-helpers'
|
|
6
|
+
import type { StackOutputs } from '../cdk/Stack.js'
|
|
7
|
+
import { CloudFormationClient } from '@aws-sdk/client-cloudformation'
|
|
8
|
+
import path, { dirname } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const die = (err: Error): void => {
|
|
12
|
+
console.error('')
|
|
13
|
+
console.error(chalk.yellow('⚠️'), chalk.red.bold(err.message))
|
|
14
|
+
console.error('')
|
|
15
|
+
console.error(err)
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.on('uncaughtException', die)
|
|
20
|
+
process.on('unhandledRejection', die)
|
|
21
|
+
|
|
22
|
+
const cdkApp = () => [
|
|
23
|
+
'--app',
|
|
24
|
+
`"npx tsx --no-warnings ${path.join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'cdk', 'http-api-mock.ts')}"`,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export const cli = async (): Promise<void> => {
|
|
28
|
+
await whoAmI()
|
|
29
|
+
if (process.argv.includes('destroy')) {
|
|
30
|
+
await destroy(getStackNameFromArgs('destroy'))
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (process.argv.includes('describe')) {
|
|
34
|
+
await describe(getStackNameFromArgs('describe'))
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const stackName = `http-api-mock-${randomString()}`
|
|
38
|
+
console.error(chalk.yellow(`Stack name`), chalk.green(stackName))
|
|
39
|
+
|
|
40
|
+
await run({
|
|
41
|
+
command: 'npx',
|
|
42
|
+
args: ['cdk', ...cdkApp(), 'deploy', '--require-approval', 'never'],
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
HTTP_API_MOCK_STACK_NAME: stackName,
|
|
46
|
+
},
|
|
47
|
+
log: {
|
|
48
|
+
debug: (msg) => console.error(chalk.blueBright('[CDK]'), chalk.blue(msg)),
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await describe(stackName)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const whoAmI = async (): Promise<{ Account: string }> => {
|
|
56
|
+
try {
|
|
57
|
+
const me = await new STSClient({}).send(new GetCallerIdentityCommand({}))
|
|
58
|
+
|
|
59
|
+
if (me.Account === undefined) throw new Error(`Not authenticated!`)
|
|
60
|
+
|
|
61
|
+
console.error(chalk.yellow('Account'), chalk.green(me.Account))
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
Account: me.Account,
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new Error(`Not authenticated!`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const destroy = async (stackName: string) => {
|
|
72
|
+
console.error(chalk.yellow(`Stack name`), chalk.green(stackName))
|
|
73
|
+
await run({
|
|
74
|
+
command: 'npx',
|
|
75
|
+
args: ['cdk', ...cdkApp(), 'destroy', '-f'],
|
|
76
|
+
env: {
|
|
77
|
+
...process.env,
|
|
78
|
+
HTTP_API_MOCK_STACK_NAME: stackName,
|
|
79
|
+
},
|
|
80
|
+
log: {
|
|
81
|
+
debug: (msg) => console.error(chalk.blueBright('[CDK]'), chalk.blue(msg)),
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
console.error(chalk.green(`Stack destroyed`))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const describe = async (stackName: string) => {
|
|
89
|
+
console.log(
|
|
90
|
+
JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
stackName,
|
|
93
|
+
...(await stackOutput(new CloudFormationClient({}))<StackOutputs>(
|
|
94
|
+
stackName,
|
|
95
|
+
)),
|
|
96
|
+
},
|
|
97
|
+
null,
|
|
98
|
+
2,
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const getStackNameFromArgs = (command: string) => {
|
|
104
|
+
const stackName = process.argv[process.argv.indexOf(command) + 1]
|
|
105
|
+
if (stackName === undefined) {
|
|
106
|
+
throw new Error(`Must provide a stack name!`)
|
|
107
|
+
}
|
|
108
|
+
return stackName
|
|
109
|
+
}
|
package/src/requests.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
resource: string //e.g. '555c3960-2092-438b-b2b0-f28eebd1f5bb'
|
|
12
|
+
requestId: string //e.g.'f34b042b-e9a2-4089-97a2-241516d40d64'
|
|
13
|
+
body: string //e.g.'{}'
|
|
14
|
+
methodPathQuery: string //e.g.'GET 555c3960-2092-438b-b2b0-f28eebd1f5bb'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const listRequests = async (
|
|
18
|
+
db: DynamoDBClient,
|
|
19
|
+
requestsTable: string,
|
|
20
|
+
): Promise<Array<Request>> =>
|
|
21
|
+
((await db.send(new ScanCommand({ TableName: requestsTable }))).Items ?? [])
|
|
22
|
+
.map((item) => {
|
|
23
|
+
const i = unmarshall(item)
|
|
24
|
+
return {
|
|
25
|
+
...i,
|
|
26
|
+
headers: JSON.parse(i.headers),
|
|
27
|
+
} as Request
|
|
28
|
+
})
|
|
29
|
+
.sort((i1, i2) => i1.timestamp.localeCompare(i2.timestamp))
|
package/src/responses.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PutItemCommand, type DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
+
import { marshall } from '@aws-sdk/util-dynamodb'
|
|
3
|
+
import { sortQueryString } from './sortQueryString.js'
|
|
4
|
+
|
|
5
|
+
export type Response = {
|
|
6
|
+
// e.g. 'GET'
|
|
7
|
+
method: string
|
|
8
|
+
// without leading slash
|
|
9
|
+
path: string
|
|
10
|
+
queryParams?: Record<string, string>
|
|
11
|
+
statusCode?: number
|
|
12
|
+
body?: string
|
|
13
|
+
ttl?: number
|
|
14
|
+
// Whether to delete the message after sending it
|
|
15
|
+
keep?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const registerResponse = async (
|
|
19
|
+
db: DynamoDBClient,
|
|
20
|
+
responsesTable: string,
|
|
21
|
+
response: Response,
|
|
22
|
+
): Promise<void> => {
|
|
23
|
+
await db.send(
|
|
24
|
+
new PutItemCommand({
|
|
25
|
+
TableName: responsesTable,
|
|
26
|
+
Item: marshall(
|
|
27
|
+
{
|
|
28
|
+
methodPathQuery: `${response.method} ${sortQueryString(response.path)}`,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
statusCode: response.statusCode,
|
|
31
|
+
body: response.body,
|
|
32
|
+
queryParams: response.queryParams,
|
|
33
|
+
ttl: response.ttl,
|
|
34
|
+
keep: response.ttl,
|
|
35
|
+
},
|
|
36
|
+
{ removeUndefinedValues: true },
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { describe, test as it } from 'node:test'
|
|
3
|
+
import { sortQueryString } from './sortQueryString.js'
|
|
4
|
+
|
|
5
|
+
void describe('sortQueryString', () => {
|
|
6
|
+
void it('should sort the query part of a mock URL', () =>
|
|
7
|
+
assert.deepStrictEqual(
|
|
8
|
+
sortQueryString(
|
|
9
|
+
'api.nrfcloud.com/v1/location/agps?eci=73393515&tac=132&requestType=custom&mcc=397&mnc=73&customTypes=2',
|
|
10
|
+
),
|
|
11
|
+
'api.nrfcloud.com/v1/location/agps?customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132',
|
|
12
|
+
))
|
|
13
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const sortQueryString = (mockUrl: string): string => {
|
|
2
|
+
const [host, query] = mockUrl.split('?', 2) as [string, string | undefined]
|
|
3
|
+
if ((query?.length ?? 0) === 0) return host
|
|
4
|
+
const params: string[][] = []
|
|
5
|
+
new URLSearchParams(query).forEach((v, k) => {
|
|
6
|
+
params.push([k, v])
|
|
7
|
+
})
|
|
8
|
+
params.sort(([k1], [k2]) => (k1 ?? '').localeCompare(k2 ?? ''))
|
|
9
|
+
const sortedParams = new URLSearchParams()
|
|
10
|
+
for (const [k, v] of params) {
|
|
11
|
+
sortedParams.append(k as string, v as string)
|
|
12
|
+
}
|
|
13
|
+
return `${host}?${sortedParams.toString()}`
|
|
14
|
+
}
|