@bifravst/http-api-mock 1.1.7 → 1.2.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.
- package/cdk/http-api-mock.ts +6 -5
- package/cdk/resources/http-api-mock-lambda.ts +6 -8
- package/dist/cdk/http-api-mock.js +5 -5
- package/dist/cdk/resources/http-api-mock-lambda.js +7 -8
- package/dist/src/URLSearchParamsToObject.d.ts +1 -0
- package/dist/src/URLSearchParamsToObject.js +7 -0
- package/dist/src/URLSearchParamsToObject.spec.d.ts +1 -0
- package/dist/src/URLSearchParamsToObject.spec.js +13 -0
- package/dist/src/mock.d.ts +15 -0
- package/dist/src/mock.js +30 -0
- package/dist/src/mock.spec.d.ts +1 -0
- package/dist/src/mock.spec.js +36 -0
- package/dist/src/responses.d.ts +6 -1
- package/dist/src/responses.js +6 -3
- package/dist/src/sortQueryString.d.ts +1 -0
- package/dist/src/sortQueryString.js +13 -5
- package/dist/src/sortQueryString.spec.js +13 -1
- package/package.json +16 -19
- package/src/URLSearchParamsToObject.spec.ts +22 -0
- package/src/URLSearchParamsToObject.ts +9 -0
- package/src/mock.spec.ts +52 -0
- package/src/mock.ts +59 -0
- package/src/responses.ts +13 -4
- package/src/sortQueryString.spec.ts +26 -1
- package/src/sortQueryString.ts +15 -5
package/cdk/http-api-mock.ts
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path'
|
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import fs from 'node:fs/promises'
|
|
8
8
|
import os from 'node:os'
|
|
9
|
+
import pJSON from '../package.json'
|
|
9
10
|
|
|
10
11
|
const { stackName } = fromEnv({ stackName: 'HTTP_API_MOCK_STACK_NAME' })(
|
|
11
12
|
process.env,
|
|
@@ -18,6 +19,10 @@ await fs.mkdir(lambdasDir)
|
|
|
18
19
|
const layersDir = path.join(distDir, 'layers')
|
|
19
20
|
await fs.mkdir(layersDir)
|
|
20
21
|
|
|
22
|
+
const dependencies: Array<keyof (typeof pJSON)['dependencies']> = [
|
|
23
|
+
'@nordicsemiconductor/from-env',
|
|
24
|
+
]
|
|
25
|
+
|
|
21
26
|
new HTTPAPIMockApp(stackName, {
|
|
22
27
|
lambdaSources: {
|
|
23
28
|
httpApiMock: await packLambdaFromPath(
|
|
@@ -30,11 +35,7 @@ new HTTPAPIMockApp(stackName, {
|
|
|
30
35
|
},
|
|
31
36
|
layer: await packLayer({
|
|
32
37
|
id: 'testResources',
|
|
33
|
-
dependencies
|
|
34
|
-
'@aws-sdk/client-dynamodb',
|
|
35
|
-
'@nordicsemiconductor/from-env',
|
|
36
|
-
'@hello.nrfcloud.com/lambda-helpers',
|
|
37
|
-
],
|
|
38
|
+
dependencies,
|
|
38
39
|
baseDir,
|
|
39
40
|
distDir: layersDir,
|
|
40
41
|
}),
|
|
@@ -11,18 +11,17 @@ import type {
|
|
|
11
11
|
Context,
|
|
12
12
|
} from 'aws-lambda'
|
|
13
13
|
import { URLSearchParams } from 'url'
|
|
14
|
-
import { logger } from '@hello.nrfcloud.com/lambda-helpers/logger'
|
|
15
14
|
import { checkMatchingQueryParams } from './checkMatchingQueryParams.js'
|
|
16
15
|
import { splitMockResponse } from './splitMockResponse.js'
|
|
17
16
|
import { sortQueryString } from '../../src/sortQueryString.js'
|
|
18
17
|
|
|
19
18
|
const db = new DynamoDBClient({})
|
|
20
|
-
const log = logger('httpApiMock')
|
|
21
19
|
|
|
22
20
|
export const handler = async (
|
|
23
21
|
event: APIGatewayEvent,
|
|
24
22
|
context: Context,
|
|
25
23
|
): Promise<APIGatewayProxyResult> => {
|
|
24
|
+
console.log(JSON.stringify({ event }))
|
|
26
25
|
const query =
|
|
27
26
|
event.queryStringParameters !== null &&
|
|
28
27
|
event.queryStringParameters !== undefined
|
|
@@ -80,7 +79,7 @@ export const handler = async (
|
|
|
80
79
|
)
|
|
81
80
|
|
|
82
81
|
// Check if response exists
|
|
83
|
-
|
|
82
|
+
console.debug(
|
|
84
83
|
`Checking if response exists for ${event.httpMethod} ${pathWithQuery}...`,
|
|
85
84
|
)
|
|
86
85
|
// Query using httpMethod and path only
|
|
@@ -90,13 +89,13 @@ export const handler = async (
|
|
|
90
89
|
KeyConditionExpression: 'methodPathQuery = :methodPathQuery',
|
|
91
90
|
ExpressionAttributeValues: {
|
|
92
91
|
[':methodPathQuery']: {
|
|
93
|
-
S: `${event.httpMethod} ${
|
|
92
|
+
S: `${event.httpMethod} ${pathWithQuery}`,
|
|
94
93
|
},
|
|
95
94
|
},
|
|
96
95
|
ScanIndexForward: false,
|
|
97
96
|
}),
|
|
98
97
|
)
|
|
99
|
-
|
|
98
|
+
console.debug(`Found response items: ${Items?.length}`)
|
|
100
99
|
|
|
101
100
|
let res: APIGatewayProxyResult | undefined
|
|
102
101
|
for (const Item of Items ?? []) {
|
|
@@ -106,7 +105,6 @@ export const handler = async (
|
|
|
106
105
|
? checkMatchingQueryParams(
|
|
107
106
|
event.queryStringParameters,
|
|
108
107
|
objItem.queryParams,
|
|
109
|
-
log,
|
|
110
108
|
)
|
|
111
109
|
: true
|
|
112
110
|
if (matchedQueryParams === false) continue
|
|
@@ -144,7 +142,7 @@ export const handler = async (
|
|
|
144
142
|
: body,
|
|
145
143
|
isBase64Encoded: isBinary,
|
|
146
144
|
}
|
|
147
|
-
|
|
145
|
+
console.debug(`Return response`, JSON.stringify({ response: res }))
|
|
148
146
|
|
|
149
147
|
break
|
|
150
148
|
}
|
|
@@ -153,6 +151,6 @@ export const handler = async (
|
|
|
153
151
|
return res
|
|
154
152
|
}
|
|
155
153
|
|
|
156
|
-
|
|
154
|
+
console.debug('No responses found')
|
|
157
155
|
return { statusCode: 404, body: '' }
|
|
158
156
|
}
|
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import fs from 'node:fs/promises';
|
|
8
8
|
import os from 'node:os';
|
|
9
|
+
import pJSON from '../package.json';
|
|
9
10
|
const { stackName } = fromEnv({ stackName: 'HTTP_API_MOCK_STACK_NAME' })(process.env);
|
|
10
11
|
const baseDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
12
|
const distDir = await fs.mkdtemp(path.join(os.tmpdir(), 'temp-'));
|
|
@@ -13,17 +14,16 @@ const lambdasDir = path.join(distDir, 'lambdas');
|
|
|
13
14
|
await fs.mkdir(lambdasDir);
|
|
14
15
|
const layersDir = path.join(distDir, 'layers');
|
|
15
16
|
await fs.mkdir(layersDir);
|
|
17
|
+
const dependencies = [
|
|
18
|
+
'@nordicsemiconductor/from-env',
|
|
19
|
+
];
|
|
16
20
|
new HTTPAPIMockApp(stackName, {
|
|
17
21
|
lambdaSources: {
|
|
18
22
|
httpApiMock: await packLambdaFromPath('httpApiMock', 'cdk/resources/http-api-mock-lambda.ts', undefined, baseDir, lambdasDir),
|
|
19
23
|
},
|
|
20
24
|
layer: await packLayer({
|
|
21
25
|
id: 'testResources',
|
|
22
|
-
dependencies
|
|
23
|
-
'@aws-sdk/client-dynamodb',
|
|
24
|
-
'@nordicsemiconductor/from-env',
|
|
25
|
-
'@hello.nrfcloud.com/lambda-helpers',
|
|
26
|
-
],
|
|
26
|
+
dependencies,
|
|
27
27
|
baseDir,
|
|
28
28
|
distDir: layersDir,
|
|
29
29
|
}),
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { DeleteItemCommand, DynamoDBClient, PutItemCommand, QueryCommand, } from '@aws-sdk/client-dynamodb';
|
|
2
2
|
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
|
|
3
3
|
import { URLSearchParams } from 'url';
|
|
4
|
-
import { logger } from '@hello.nrfcloud.com/lambda-helpers/logger';
|
|
5
4
|
import { checkMatchingQueryParams } from './checkMatchingQueryParams.js';
|
|
6
5
|
import { splitMockResponse } from './splitMockResponse.js';
|
|
7
6
|
import { sortQueryString } from '../../src/sortQueryString.js';
|
|
8
7
|
const db = new DynamoDBClient({});
|
|
9
|
-
const log = logger('httpApiMock');
|
|
10
8
|
export const handler = async (event, context) => {
|
|
9
|
+
console.log(JSON.stringify({ event }));
|
|
11
10
|
const query = event.queryStringParameters !== null &&
|
|
12
11
|
event.queryStringParameters !== undefined
|
|
13
12
|
? new URLSearchParams(event.queryStringParameters)
|
|
@@ -50,25 +49,25 @@ export const handler = async (event, context) => {
|
|
|
50
49
|
},
|
|
51
50
|
}));
|
|
52
51
|
// Check if response exists
|
|
53
|
-
|
|
52
|
+
console.debug(`Checking if response exists for ${event.httpMethod} ${pathWithQuery}...`);
|
|
54
53
|
// Query using httpMethod and path only
|
|
55
54
|
const { Items } = await db.send(new QueryCommand({
|
|
56
55
|
TableName: process.env.RESPONSES_TABLE_NAME,
|
|
57
56
|
KeyConditionExpression: 'methodPathQuery = :methodPathQuery',
|
|
58
57
|
ExpressionAttributeValues: {
|
|
59
58
|
[':methodPathQuery']: {
|
|
60
|
-
S: `${event.httpMethod} ${
|
|
59
|
+
S: `${event.httpMethod} ${pathWithQuery}`,
|
|
61
60
|
},
|
|
62
61
|
},
|
|
63
62
|
ScanIndexForward: false,
|
|
64
63
|
}));
|
|
65
|
-
|
|
64
|
+
console.debug(`Found response items: ${Items?.length}`);
|
|
66
65
|
let res;
|
|
67
66
|
for (const Item of Items ?? []) {
|
|
68
67
|
const objItem = unmarshall(Item);
|
|
69
68
|
const hasExpectedQueryParams = 'queryParams' in objItem;
|
|
70
69
|
const matchedQueryParams = hasExpectedQueryParams
|
|
71
|
-
? checkMatchingQueryParams(event.queryStringParameters, objItem.queryParams
|
|
70
|
+
? checkMatchingQueryParams(event.queryStringParameters, objItem.queryParams)
|
|
72
71
|
: true;
|
|
73
72
|
if (matchedQueryParams === false)
|
|
74
73
|
continue;
|
|
@@ -99,12 +98,12 @@ export const handler = async (event, context) => {
|
|
|
99
98
|
: body,
|
|
100
99
|
isBase64Encoded: isBinary,
|
|
101
100
|
};
|
|
102
|
-
|
|
101
|
+
console.debug(`Return response`, JSON.stringify({ response: res }));
|
|
103
102
|
break;
|
|
104
103
|
}
|
|
105
104
|
if (res !== undefined) {
|
|
106
105
|
return res;
|
|
107
106
|
}
|
|
108
|
-
|
|
107
|
+
console.debug('No responses found');
|
|
109
108
|
return { statusCode: 404, body: '' };
|
|
110
109
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const URLSearchParamsToObject: (params: URLSearchParams) => Record<string, string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { URLSearchParamsToObject } from './URLSearchParamsToObject.js';
|
|
4
|
+
void describe('URLSearchParamsToObject()', () => {
|
|
5
|
+
void it('should convert URLSearchParams to a plain object', () => assert.deepEqual(URLSearchParamsToObject(new URLSearchParams('eci=73393515&tac=132&requestType=custom&mcc=397&mnc=73&customTypes=2')), {
|
|
6
|
+
eci: '73393515',
|
|
7
|
+
tac: '132',
|
|
8
|
+
requestType: 'custom',
|
|
9
|
+
mcc: '397',
|
|
10
|
+
mnc: '73',
|
|
11
|
+
customTypes: '2',
|
|
12
|
+
}));
|
|
13
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
type MockResponseFn = (methodPathQuery: string, response: Partial<{
|
|
3
|
+
headers: Headers;
|
|
4
|
+
status: number;
|
|
5
|
+
body: string;
|
|
6
|
+
}>) => Promise<void>;
|
|
7
|
+
export declare const mockResponse: (db: DynamoDBClient, responsesTable: string) => MockResponseFn;
|
|
8
|
+
export type HttpAPIMock = {
|
|
9
|
+
response: MockResponseFn;
|
|
10
|
+
};
|
|
11
|
+
export declare const mock: ({ db, responsesTable, }: {
|
|
12
|
+
db: DynamoDBClient;
|
|
13
|
+
responsesTable: string;
|
|
14
|
+
}) => HttpAPIMock;
|
|
15
|
+
export {};
|
package/dist/src/mock.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { registerResponse } from './responses.js';
|
|
2
|
+
export const mockResponse = (db, responsesTable) => async (methodPathQuery, response) => {
|
|
3
|
+
const [method, pathWithQuery] = methodPathQuery.split(' ', 2);
|
|
4
|
+
if (!/^[A-Z]+$/.test(method ?? ''))
|
|
5
|
+
throw new Error(`Invalid method ${method} in ${methodPathQuery}!`);
|
|
6
|
+
if (pathWithQuery === undefined)
|
|
7
|
+
throw new Error(`Missing path in ${methodPathQuery}!`);
|
|
8
|
+
const [path, query] = pathWithQuery.split('?', 2);
|
|
9
|
+
if (path.startsWith('/'))
|
|
10
|
+
throw new Error(`Path ${path} must not start with /!`);
|
|
11
|
+
const bodyParts = [];
|
|
12
|
+
if (response.headers !== undefined) {
|
|
13
|
+
for (const [k, v] of response.headers.entries()) {
|
|
14
|
+
bodyParts.push(`${k}: ${v}`);
|
|
15
|
+
}
|
|
16
|
+
bodyParts.push('');
|
|
17
|
+
}
|
|
18
|
+
if (response.body !== undefined)
|
|
19
|
+
bodyParts.push(response.body);
|
|
20
|
+
await registerResponse(db, responsesTable, {
|
|
21
|
+
path,
|
|
22
|
+
method: method ?? 'GET',
|
|
23
|
+
queryParams: new URLSearchParams(query),
|
|
24
|
+
body: bodyParts.length > 0 ? bodyParts.join('\n') : undefined,
|
|
25
|
+
statusCode: response.status,
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
export const mock = ({ db, responsesTable, }) => ({
|
|
29
|
+
response: mockResponse(db, responsesTable),
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, mock as testMock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mock } from './mock.js';
|
|
4
|
+
import { unmarshall } from '@aws-sdk/util-dynamodb';
|
|
5
|
+
void describe('mock()', () => {
|
|
6
|
+
void it('should register a response', async () => {
|
|
7
|
+
const db = {
|
|
8
|
+
send: testMock.fn(async () => Promise.resolve(undefined)),
|
|
9
|
+
};
|
|
10
|
+
const httpApiMock = mock({
|
|
11
|
+
db: db,
|
|
12
|
+
responsesTable: 'response-table',
|
|
13
|
+
});
|
|
14
|
+
await httpApiMock.response(`GET foo/bar?k=v`, {
|
|
15
|
+
status: 200,
|
|
16
|
+
headers: new Headers({
|
|
17
|
+
'content-type': 'application/json; charset=utf-8',
|
|
18
|
+
}),
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
result: 'some-value',
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
assert.equal(db.send.mock.callCount(), 1);
|
|
24
|
+
const [{ input: args }] = db.send.mock.calls[0]?.arguments;
|
|
25
|
+
assert.equal(args.TableName, 'response-table');
|
|
26
|
+
const { methodPathQuery, statusCode, body, queryParams } = unmarshall(args.Item);
|
|
27
|
+
assert.equal(statusCode, 200);
|
|
28
|
+
assert.equal(methodPathQuery, 'GET foo/bar?k=v');
|
|
29
|
+
assert.equal(body, [
|
|
30
|
+
`content-type: application/json; charset=utf-8`,
|
|
31
|
+
``,
|
|
32
|
+
JSON.stringify({ result: 'some-value' }),
|
|
33
|
+
].join('\n'));
|
|
34
|
+
assert.deepEqual(queryParams, { k: 'v' });
|
|
35
|
+
});
|
|
36
|
+
});
|
package/dist/src/responses.d.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { type DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
|
2
2
|
export type Response = {
|
|
3
3
|
method: string;
|
|
4
4
|
path: string;
|
|
5
|
-
queryParams?:
|
|
5
|
+
queryParams?: URLSearchParams;
|
|
6
6
|
statusCode?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Header + Body
|
|
9
|
+
*
|
|
10
|
+
* @see splitMockResponse
|
|
11
|
+
*/
|
|
7
12
|
body?: string;
|
|
8
13
|
ttl?: number;
|
|
9
14
|
keep?: boolean;
|
package/dist/src/responses.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { PutItemCommand } from '@aws-sdk/client-dynamodb';
|
|
2
2
|
import { marshall } from '@aws-sdk/util-dynamodb';
|
|
3
|
-
import {
|
|
3
|
+
import { sortQuery } from './sortQueryString.js';
|
|
4
|
+
import { URLSearchParamsToObject } from './URLSearchParamsToObject.js';
|
|
4
5
|
export const registerResponse = async (db, responsesTable, response) => {
|
|
5
6
|
await db.send(new PutItemCommand({
|
|
6
7
|
TableName: responsesTable,
|
|
7
8
|
Item: marshall({
|
|
8
|
-
methodPathQuery: `${response.method} ${
|
|
9
|
+
methodPathQuery: `${response.method} ${response.path}${response.queryParams !== undefined ? `?${sortQuery(response.queryParams)}` : ``}`,
|
|
9
10
|
timestamp: new Date().toISOString(),
|
|
10
11
|
statusCode: response.statusCode,
|
|
11
12
|
body: response.body,
|
|
12
|
-
queryParams: response.queryParams
|
|
13
|
+
queryParams: response.queryParams !== undefined
|
|
14
|
+
? URLSearchParamsToObject(response.queryParams)
|
|
15
|
+
: undefined,
|
|
13
16
|
ttl: response.ttl,
|
|
14
17
|
keep: response.ttl,
|
|
15
18
|
}, { removeUndefinedValues: true }),
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
export const sortQueryString = (mockUrl) => {
|
|
2
2
|
const [host, query] = mockUrl.split('?', 2);
|
|
3
|
-
if ((query?.length ?? 0) === 0)
|
|
3
|
+
if (query === undefined || (query?.length ?? 0) === 0)
|
|
4
4
|
return host;
|
|
5
|
+
return `${host}?${sortQuery(new URLSearchParams(query))}`;
|
|
6
|
+
};
|
|
7
|
+
export const sortQuery = (query) => {
|
|
5
8
|
const params = [];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
if (query instanceof URLSearchParams) {
|
|
10
|
+
query.forEach((v, k) => {
|
|
11
|
+
params.push([k, v]);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
params.push(...Object.entries(query));
|
|
16
|
+
}
|
|
9
17
|
params.sort(([k1], [k2]) => (k1 ?? '').localeCompare(k2 ?? ''));
|
|
10
18
|
const sortedParams = new URLSearchParams();
|
|
11
19
|
for (const [k, v] of params) {
|
|
12
20
|
sortedParams.append(k, v);
|
|
13
21
|
}
|
|
14
|
-
return
|
|
22
|
+
return sortedParams.toString();
|
|
15
23
|
};
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { describe, test as it } from 'node:test';
|
|
3
|
-
import { sortQueryString } from './sortQueryString.js';
|
|
3
|
+
import { sortQuery, sortQueryString } from './sortQueryString.js';
|
|
4
|
+
import { URLSearchParams } from 'node:url';
|
|
4
5
|
void describe('sortQueryString', () => {
|
|
5
6
|
void it('should sort the query part of a mock URL', () => assert.deepStrictEqual(sortQueryString('api.nrfcloud.com/v1/location/agps?eci=73393515&tac=132&requestType=custom&mcc=397&mnc=73&customTypes=2'), 'api.nrfcloud.com/v1/location/agps?customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132'));
|
|
6
7
|
});
|
|
8
|
+
void describe('sortQuery', () => {
|
|
9
|
+
void it('should sort URLSearchParams', () => assert.equal(sortQuery(new URLSearchParams('eci=73393515&tac=132&requestType=custom&mcc=397&mnc=73&customTypes=2')), 'customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132'));
|
|
10
|
+
void it('should sort a Record', () => assert.equal(sortQuery({
|
|
11
|
+
eci: '73393515',
|
|
12
|
+
tac: '132',
|
|
13
|
+
requestType: 'custom',
|
|
14
|
+
mcc: '397',
|
|
15
|
+
mnc: '73',
|
|
16
|
+
customTypes: '2',
|
|
17
|
+
}), 'customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132'));
|
|
18
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bifravst/http-api-mock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Helper functions for AWS lambdas written in TypeScript.",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./*": {
|
|
@@ -34,15 +34,6 @@
|
|
|
34
34
|
],
|
|
35
35
|
"author": "Nordic Semiconductor ASA | nordicsemi.no",
|
|
36
36
|
"license": "BSD-3-Clause",
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@bifravst/eslint-config-typescript": "6.0.22",
|
|
39
|
-
"@bifravst/prettier-config": "1.0.0",
|
|
40
|
-
"@commitlint/config-conventional": "19.1.0",
|
|
41
|
-
"@types/aws-lambda": "8.10.137",
|
|
42
|
-
"@types/node": "20.12.7",
|
|
43
|
-
"globstar": "1.0.0",
|
|
44
|
-
"husky": "9.0.11"
|
|
45
|
-
},
|
|
46
37
|
"lint-staged": {
|
|
47
38
|
"*.ts": [
|
|
48
39
|
"prettier --write",
|
|
@@ -91,21 +82,27 @@
|
|
|
91
82
|
"README.md"
|
|
92
83
|
],
|
|
93
84
|
"prettier": "@bifravst/prettier-config",
|
|
94
|
-
"
|
|
95
|
-
"@aws-sdk/client-cloudformation": "^3.
|
|
96
|
-
"@aws-sdk/client-
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"@aws-sdk/client-cloudformation": "^3.554.0",
|
|
87
|
+
"@aws-sdk/client-dynamodb": "^3.554.0",
|
|
88
|
+
"@aws-sdk/client-sts": "^3.554.0",
|
|
89
|
+
"@aws-sdk/util-dynamodb": "^3.554.0",
|
|
90
|
+
"@bifravst/aws-cdk-lambda-helpers": "^1.4.0",
|
|
97
91
|
"@bifravst/run": "^1.2.0",
|
|
98
92
|
"@nordicsemiconductor/cloudformation-helpers": "^9.0.3",
|
|
93
|
+
"@nordicsemiconductor/from-env": "^3.0.1",
|
|
99
94
|
"aws-cdk-lib": "^2.137.0",
|
|
100
95
|
"cdk": "^2.137.0",
|
|
101
96
|
"chalk": "^5.3.0",
|
|
102
97
|
"tsx": "^4.7.2"
|
|
103
98
|
},
|
|
104
|
-
"
|
|
105
|
-
"@
|
|
106
|
-
"@
|
|
107
|
-
"@
|
|
108
|
-
"@
|
|
109
|
-
"@
|
|
99
|
+
"devDependencies": {
|
|
100
|
+
"@bifravst/eslint-config-typescript": "6.0.22",
|
|
101
|
+
"@bifravst/prettier-config": "1.0.0",
|
|
102
|
+
"@commitlint/config-conventional": "19.1.0",
|
|
103
|
+
"@types/aws-lambda": "8.10.137",
|
|
104
|
+
"@types/node": "20.12.7",
|
|
105
|
+
"globstar": "1.0.0",
|
|
106
|
+
"husky": "9.0.11"
|
|
110
107
|
}
|
|
111
108
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { URLSearchParamsToObject } from './URLSearchParamsToObject.js'
|
|
4
|
+
|
|
5
|
+
void describe('URLSearchParamsToObject()', () => {
|
|
6
|
+
void it('should convert URLSearchParams to a plain object', () =>
|
|
7
|
+
assert.deepEqual(
|
|
8
|
+
URLSearchParamsToObject(
|
|
9
|
+
new URLSearchParams(
|
|
10
|
+
'eci=73393515&tac=132&requestType=custom&mcc=397&mnc=73&customTypes=2',
|
|
11
|
+
),
|
|
12
|
+
),
|
|
13
|
+
{
|
|
14
|
+
eci: '73393515',
|
|
15
|
+
tac: '132',
|
|
16
|
+
requestType: 'custom',
|
|
17
|
+
mcc: '397',
|
|
18
|
+
mnc: '73',
|
|
19
|
+
customTypes: '2',
|
|
20
|
+
},
|
|
21
|
+
))
|
|
22
|
+
})
|
package/src/mock.spec.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, mock as testMock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mock } from './mock.js'
|
|
4
|
+
import type { AttributeValue, DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
5
|
+
import { unmarshall } from '@aws-sdk/util-dynamodb'
|
|
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
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
|
+
import { registerResponse } from './responses.js'
|
|
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
|
+
) => Promise<void>
|
|
14
|
+
|
|
15
|
+
export const mockResponse =
|
|
16
|
+
(db: DynamoDBClient, responsesTable: string): MockResponseFn =>
|
|
17
|
+
async (methodPathQuery, response) => {
|
|
18
|
+
const [method, pathWithQuery] = methodPathQuery.split(' ', 2)
|
|
19
|
+
if (!/^[A-Z]+$/.test(method ?? ''))
|
|
20
|
+
throw new Error(`Invalid method ${method} in ${methodPathQuery}!`)
|
|
21
|
+
if (pathWithQuery === undefined)
|
|
22
|
+
throw new Error(`Missing path in ${methodPathQuery}!`)
|
|
23
|
+
const [path, query] = pathWithQuery.split('?', 2) as [
|
|
24
|
+
string,
|
|
25
|
+
string | undefined,
|
|
26
|
+
]
|
|
27
|
+
if (path.startsWith('/'))
|
|
28
|
+
throw new Error(`Path ${path} must not start with /!`)
|
|
29
|
+
|
|
30
|
+
const bodyParts = []
|
|
31
|
+
if (response.headers !== undefined) {
|
|
32
|
+
for (const [k, v] of response.headers.entries()) {
|
|
33
|
+
bodyParts.push(`${k}: ${v}`)
|
|
34
|
+
}
|
|
35
|
+
bodyParts.push('')
|
|
36
|
+
}
|
|
37
|
+
if (response.body !== undefined) bodyParts.push(response.body)
|
|
38
|
+
await registerResponse(db, responsesTable, {
|
|
39
|
+
path,
|
|
40
|
+
method: method ?? 'GET',
|
|
41
|
+
queryParams: new URLSearchParams(query),
|
|
42
|
+
body: bodyParts.length > 0 ? bodyParts.join('\n') : undefined,
|
|
43
|
+
statusCode: response.status,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type HttpAPIMock = {
|
|
48
|
+
response: MockResponseFn
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const mock = ({
|
|
52
|
+
db,
|
|
53
|
+
responsesTable,
|
|
54
|
+
}: {
|
|
55
|
+
db: DynamoDBClient
|
|
56
|
+
responsesTable: string
|
|
57
|
+
}): HttpAPIMock => ({
|
|
58
|
+
response: mockResponse(db, responsesTable),
|
|
59
|
+
})
|
package/src/responses.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { PutItemCommand, type DynamoDBClient } from '@aws-sdk/client-dynamodb'
|
|
2
2
|
import { marshall } from '@aws-sdk/util-dynamodb'
|
|
3
|
-
import {
|
|
3
|
+
import { sortQuery } from './sortQueryString.js'
|
|
4
|
+
import { URLSearchParamsToObject } from './URLSearchParamsToObject.js'
|
|
4
5
|
|
|
5
6
|
export type Response = {
|
|
6
7
|
// e.g. 'GET'
|
|
7
8
|
method: string
|
|
8
9
|
// without leading slash
|
|
9
10
|
path: string
|
|
10
|
-
queryParams?:
|
|
11
|
+
queryParams?: URLSearchParams
|
|
11
12
|
statusCode?: number
|
|
13
|
+
/**
|
|
14
|
+
* Header + Body
|
|
15
|
+
*
|
|
16
|
+
* @see splitMockResponse
|
|
17
|
+
*/
|
|
12
18
|
body?: string
|
|
13
19
|
ttl?: number
|
|
14
20
|
// Whether to delete the message after sending it
|
|
@@ -25,11 +31,14 @@ export const registerResponse = async (
|
|
|
25
31
|
TableName: responsesTable,
|
|
26
32
|
Item: marshall(
|
|
27
33
|
{
|
|
28
|
-
methodPathQuery: `${response.method} ${
|
|
34
|
+
methodPathQuery: `${response.method} ${response.path}${response.queryParams !== undefined ? `?${sortQuery(response.queryParams)}` : ``}`,
|
|
29
35
|
timestamp: new Date().toISOString(),
|
|
30
36
|
statusCode: response.statusCode,
|
|
31
37
|
body: response.body,
|
|
32
|
-
queryParams:
|
|
38
|
+
queryParams:
|
|
39
|
+
response.queryParams !== undefined
|
|
40
|
+
? URLSearchParamsToObject(response.queryParams)
|
|
41
|
+
: undefined,
|
|
33
42
|
ttl: response.ttl,
|
|
34
43
|
keep: response.ttl,
|
|
35
44
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
import { describe, test as it } from 'node:test'
|
|
3
|
-
import { sortQueryString } from './sortQueryString.js'
|
|
3
|
+
import { sortQuery, sortQueryString } from './sortQueryString.js'
|
|
4
|
+
import { URLSearchParams } from 'node:url'
|
|
4
5
|
|
|
5
6
|
void describe('sortQueryString', () => {
|
|
6
7
|
void it('should sort the query part of a mock URL', () =>
|
|
@@ -11,3 +12,27 @@ void describe('sortQueryString', () => {
|
|
|
11
12
|
'api.nrfcloud.com/v1/location/agps?customTypes=2&eci=73393515&mcc=397&mnc=73&requestType=custom&tac=132',
|
|
12
13
|
))
|
|
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
|
+
})
|
package/src/sortQueryString.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
export const sortQueryString = (mockUrl: string): string => {
|
|
2
2
|
const [host, query] = mockUrl.split('?', 2) as [string, string | undefined]
|
|
3
|
-
if ((query?.length ?? 0) === 0) return host
|
|
3
|
+
if (query === undefined || (query?.length ?? 0) === 0) return host
|
|
4
|
+
return `${host}?${sortQuery(new URLSearchParams(query))}`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const sortQuery = (
|
|
8
|
+
query: URLSearchParams | Record<string, string>,
|
|
9
|
+
): string => {
|
|
4
10
|
const params: string[][] = []
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
if (query instanceof URLSearchParams) {
|
|
12
|
+
query.forEach((v, k) => {
|
|
13
|
+
params.push([k, v])
|
|
14
|
+
})
|
|
15
|
+
} else {
|
|
16
|
+
params.push(...Object.entries(query))
|
|
17
|
+
}
|
|
8
18
|
params.sort(([k1], [k2]) => (k1 ?? '').localeCompare(k2 ?? ''))
|
|
9
19
|
const sortedParams = new URLSearchParams()
|
|
10
20
|
for (const [k, v] of params) {
|
|
11
21
|
sortedParams.append(k as string, v as string)
|
|
12
22
|
}
|
|
13
|
-
return
|
|
23
|
+
return sortedParams.toString()
|
|
14
24
|
}
|