Skip to content

Commit 16717b9

Browse files
CCM-12875: Various updates
1 parent 1144590 commit 16717b9

File tree

10 files changed

+114
-276
lines changed

10 files changed

+114
-276
lines changed

lambdas/pdm-mock-lambda/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ curl https://<api-gateway-url>/patient-data-manager/FHIR/R4/DocumentReference/te
7878

7979
- `Authorization: Bearer <token>` - Required authentication token (default: `mock-pdm-token`)
8080
- `Content-Type: application/fhir+json` - Required content type
81-
- `X-Request-ID: <uuid>` - Used for request tracking and correlation.
81+
- `X-Request-ID: <uuid>` - Used for request tracking and correlation. This isn't part of the ID or response that gets returned.
8282

8383
**Response (200 OK):**
8484

@@ -134,7 +134,7 @@ Both GET and POST endpoints require the `X-Request-ID` header. If it's missing,
134134

135135
### Error Scenarios
136136

137-
The mock API supports triggering specific error responses for testing. Use these special resource IDs:
137+
The mock API supports triggering specific error responses for testing in both endpoints. Use these special resource IDs:
138138

139139
| Resource ID | Status Code | Error Code | Description |
140140
| ------------------------ | ----------- | ------------------- | ------------------------------- |
@@ -179,7 +179,7 @@ The lambda is configured via environment variables:
179179

180180
| Variable | Description | Default |
181181
| ----------------------- | ---------------------------------------- | -------------------------- |
182-
| `MOCK_ACCESS_TOKEN` | Token to use in local/dev environments | `mock-token-for-local-dev` |
183-
| `ACCESS_TOKEN_SSM_PATH` | SSM parameter path for the access token | `/mock/access-token` |
182+
| `MOCK_ACCESS_TOKEN` | Token to use in local/dev environments | `mock-pdm-token` |
183+
| `ACCESS_TOKEN_SSM_PATH` | SSM parameter path for the access token | `/dl/main/apim/access_token`|
184184
| `USE_NON_MOCK_TOKEN` | Use SSM token instead of mock token | `false` |
185185
| `LOG_LEVEL` | Logging level (DEBUG, INFO, WARN, ERROR) | `INFO` |
Lines changed: 3 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ParameterStoreService, loadConfig } from 'config';
1+
import { loadConfig } from 'config';
22

33
describe('Config', () => {
44
const originalEnv = process.env;
@@ -21,8 +21,8 @@ describe('Config', () => {
2121

2222
const config = loadConfig();
2323

24-
expect(config.mockAccessToken).toBe('mock-token-for-local-dev');
25-
expect(config.accessTokenSsmPath).toBe('/mock/access-token');
24+
expect(config.mockAccessToken).toBe('mock-pdm-token');
25+
expect(config.accessTokenSsmPath).toBe('/dl/main/apim/access_token');
2626
expect(config.useNonMockToken).toBe(false);
2727
expect(config.logLevel).toBe('INFO');
2828
});
@@ -64,99 +64,4 @@ describe('Config', () => {
6464
expect(config).toBeDefined();
6565
});
6666
});
67-
68-
describe('ParameterStoreService', () => {
69-
let service: ParameterStoreService;
70-
71-
beforeEach(() => {
72-
service = new ParameterStoreService();
73-
});
74-
75-
it('should create an instance', () => {
76-
expect(service).toBeInstanceOf(ParameterStoreService);
77-
});
78-
79-
it('should use default AWS region when AWS_REGION not set', () => {
80-
delete process.env.AWS_REGION;
81-
const serviceWithDefaultRegion = new ParameterStoreService();
82-
expect(serviceWithDefaultRegion).toBeInstanceOf(ParameterStoreService);
83-
expect(serviceWithDefaultRegion.ssmClient).toBeDefined();
84-
});
85-
86-
it('should use AWS_REGION when set', () => {
87-
process.env.AWS_REGION = 'us-east-1';
88-
const serviceWithCustomRegion = new ParameterStoreService();
89-
expect(serviceWithCustomRegion).toBeInstanceOf(ParameterStoreService);
90-
expect(serviceWithCustomRegion.ssmClient).toBeDefined();
91-
});
92-
93-
it('should cache parameter values', async () => {
94-
const mockParameter = 'test-value';
95-
const mockSend = jest.fn().mockResolvedValue({
96-
Parameter: { Value: mockParameter },
97-
});
98-
99-
(service as any).ssmClient = {
100-
send: mockSend,
101-
};
102-
103-
const result1 = await service.getParameter('/test/path');
104-
expect(result1).toBe(mockParameter);
105-
expect(mockSend).toHaveBeenCalledTimes(1);
106-
107-
const result2 = await service.getParameter('/test/path');
108-
expect(result2).toBe(mockParameter);
109-
expect(mockSend).toHaveBeenCalledTimes(1);
110-
});
111-
112-
it('should refresh cache after TTL expires', async () => {
113-
const mockParameter = 'test-value';
114-
const mockSend = jest.fn().mockResolvedValue({
115-
Parameter: { Value: mockParameter },
116-
});
117-
118-
(service as any).ssmClient = {
119-
send: mockSend,
120-
};
121-
122-
(service as any).cacheTtl = 10;
123-
124-
await service.getParameter('/test/path');
125-
expect(mockSend).toHaveBeenCalledTimes(1);
126-
127-
await new Promise((resolve) => {
128-
setTimeout(resolve, 20);
129-
});
130-
131-
await service.getParameter('/test/path');
132-
expect(mockSend).toHaveBeenCalledTimes(2);
133-
});
134-
135-
it('should throw error when parameter is not found', async () => {
136-
const mockSend = jest.fn().mockResolvedValue({
137-
Parameter: {},
138-
});
139-
140-
(service as any).ssmClient = {
141-
send: mockSend,
142-
};
143-
144-
await expect(service.getParameter('/missing/path')).rejects.toThrow(
145-
'Parameter /missing/path not found or has no value',
146-
);
147-
});
148-
149-
it('should handle SSM errors', async () => {
150-
const mockError = new Error('SSM error');
151-
const mockSend = jest.fn().mockRejectedValue(mockError);
152-
153-
(service as any).ssmClient = {
154-
send: mockSend,
155-
};
156-
157-
await expect(service.getParameter('/error/path')).rejects.toThrow(
158-
'SSM error',
159-
);
160-
});
161-
});
16267
});

lambdas/pdm-mock-lambda/src/__tests__/container.test.ts

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { SSMClient } from '@aws-sdk/client-ssm';
21
import { createContainer } from 'container';
2+
import { parameterStore } from 'utils';
33

44
jest.mock('utils', () => {
55
const actual = jest.requireActual('utils');
@@ -12,16 +12,19 @@ jest.mock('utils', () => {
1212
error: jest.fn(),
1313
child: jest.fn(),
1414
},
15+
parameterStore: {
16+
getParameter: jest.fn(),
17+
},
1518
};
1619
});
1720

18-
jest.mock('@aws-sdk/client-ssm');
19-
2021
describe('Container', () => {
2122
let container: ReturnType<typeof createContainer>;
23+
const originalEnv = process.env;
2224

2325
beforeEach(() => {
2426
jest.clearAllMocks();
27+
process.env = { ...originalEnv };
2528
process.env.MOCK_ACCESS_TOKEN = 'test-token';
2629
process.env.ACCESS_TOKEN_SSM_PATH = '/test/path';
2730
process.env.USE_NON_MOCK_TOKEN = 'false';
@@ -30,6 +33,10 @@ describe('Container', () => {
3033
container = createContainer();
3134
});
3235

36+
afterEach(() => {
37+
process.env = originalEnv;
38+
});
39+
3340
it('should create a container with all required dependencies', () => {
3441
expect(container).toBeDefined();
3542
expect(container.authenticator).toBeDefined();
@@ -94,50 +101,38 @@ describe('Container', () => {
94101
it('should wire getAccessToken to authenticator when using SSM token', async () => {
95102
const mockTokenValue = JSON.stringify({
96103
access_token: 'ssm-stored-token',
97-
expires_at: 1765187843,
104+
expires_at: 1_765_187_843,
98105
token_type: 'Bearer',
99106
});
100107

101-
const mockSend = jest.fn().mockResolvedValue({
102-
Parameter: { Value: mockTokenValue },
108+
(parameterStore.getParameter as jest.Mock).mockResolvedValue({
109+
Value: mockTokenValue,
103110
});
104111

105112
process.env.USE_NON_MOCK_TOKEN = 'true';
106113
process.env.ACCESS_TOKEN_SSM_PATH = '/test/token/path';
107114
process.env.MOCK_ACCESS_TOKEN = 'unused-mock-token';
108115

109-
(SSMClient as jest.MockedClass<typeof SSMClient>).mockImplementation(
110-
() =>
111-
({
112-
send: mockSend,
113-
}) as any,
114-
);
115-
116116
const testContainer = createContainer();
117117

118118
const result = await testContainer.authenticator({
119119
headers: { Authorization: 'Bearer ssm-stored-token' },
120120
});
121121

122122
expect(result.isValid).toBe(true);
123-
expect(mockSend).toHaveBeenCalled();
123+
expect(parameterStore.getParameter).toHaveBeenCalledWith(
124+
'/test/token/path',
125+
);
124126
});
125127

126128
it('should handle invalid JSON format in SSM parameter', async () => {
127-
const mockSend = jest.fn().mockResolvedValue({
128-
Parameter: { Value: 'invalid-json' },
129+
(parameterStore.getParameter as jest.Mock).mockResolvedValue({
130+
Value: 'invalid-json',
129131
});
130132

131133
process.env.USE_NON_MOCK_TOKEN = 'true';
132134
process.env.ACCESS_TOKEN_SSM_PATH = '/test/token/path';
133135

134-
(SSMClient as jest.MockedClass<typeof SSMClient>).mockImplementation(
135-
() =>
136-
({
137-
send: mockSend,
138-
}) as any,
139-
);
140-
141136
const testContainer = createContainer();
142137

143138
await expect(
@@ -146,4 +141,23 @@ describe('Container', () => {
146141
}),
147142
).rejects.toThrow('Invalid access token format in SSM parameter');
148143
});
144+
145+
it('should handle missing SSM parameter', async () => {
146+
(parameterStore.getParameter as jest.Mock).mockResolvedValue({
147+
Value: undefined,
148+
});
149+
150+
process.env.USE_NON_MOCK_TOKEN = 'true';
151+
process.env.ACCESS_TOKEN_SSM_PATH = '/test/token/path';
152+
153+
const testContainer = createContainer();
154+
155+
await expect(
156+
testContainer.authenticator({
157+
headers: { Authorization: 'Bearer any-token' },
158+
}),
159+
).rejects.toThrow(
160+
'Access token parameter "/test/token/path" not found in SSM',
161+
);
162+
});
149163
});

lambdas/pdm-mock-lambda/src/__tests__/handlers.test.ts

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ describe('GET Resource Handler', () => {
7979
);
8080
expect(body.subject.identifier.value).toBe('9912003071');
8181
expect(body.content[0].attachment.contentType).toBe('application/pdf');
82-
expect(body.content[0].attachment.data).toBe('XYZ');
82+
expect(body.content[0].attachment.data).toBeDefined();
83+
expect(body.content[0].attachment.data).toMatch(/^[A-Za-z0-9+/=]+$/);
8384
expect(body.content[0].attachment.title).toBe('Dummy PDF');
8485
});
8586

@@ -97,7 +98,7 @@ describe('GET Resource Handler', () => {
9798
expect(response.statusCode).toBe(400);
9899
const body = JSON.parse(response.body);
99100
expect(body.resourceType).toBe('OperationOutcome');
100-
expect(body.issue[0].details.coding[0].code).toBe('INVALID_REQUEST');
101+
expect(body.issue[0].details.text).toBe('INVALID_REQUEST');
101102
});
102103

103104
it('should return empty response for empty-response scenario', async () => {
@@ -116,7 +117,7 @@ describe('GET Resource Handler', () => {
116117
});
117118

118119
describe('error scenarios', () => {
119-
const errorCases = [
120+
it.each([
120121
{
121122
id: 'error-400-invalid',
122123
expectedStatus: 400,
@@ -152,10 +153,9 @@ describe('GET Resource Handler', () => {
152153
expectedStatus: 503,
153154
expectedCode: 'SERVICE_UNAVAILABLE',
154155
},
155-
];
156-
157-
for (const { expectedCode, expectedStatus, id } of errorCases) {
158-
it(`should return ${expectedStatus} error for ${id}`, async () => {
156+
])(
157+
'should return $expectedStatus error for $id',
158+
async ({ expectedCode, expectedStatus, id }) => {
159159
const handler = createGetResourceHandler(mockLogger);
160160
const event = createMockEvent({
161161
pathParameters: { id },
@@ -169,9 +169,9 @@ describe('GET Resource Handler', () => {
169169
expect(response.statusCode).toBe(expectedStatus);
170170
const body = JSON.parse(response.body);
171171
expect(body.resourceType).toBe('OperationOutcome');
172-
expect(body.issue[0].details.coding[0].code).toBe(expectedCode);
173-
});
174-
}
172+
expect(body.issue[0].details.text).toBe(expectedCode);
173+
},
174+
);
175175
});
176176

177177
it('should return 400 error when X-Request-ID header is missing', async () => {
@@ -274,31 +274,13 @@ describe('POST Create Resource Handler', () => {
274274
expect(body.id).toBe(xRequestId);
275275
});
276276

277-
it('should return 400 error for invalid JSON', async () => {
277+
it('should return empty response when X-Request-ID is empty-response', async () => {
278278
const handler = createCreateResourceHandler(mockLogger);
279279
const event = createMockEvent({
280280
httpMethod: 'POST',
281-
body: 'invalid-json{',
282-
headers: {
283-
'X-Request-ID': 'invalid-json-test-1234-5678-9abc-def012345678',
284-
},
285-
});
286-
287-
const response = await handler(event);
288-
289-
expect(response.statusCode).toBe(400);
290-
const body = JSON.parse(response.body);
291-
expect(body.resourceType).toBe('OperationOutcome');
292-
expect(body.issue[0].details.coding[0].code).toBe('INVALID_REQUEST');
293-
});
294-
295-
it('should return empty response when emptyResponse flag is set', async () => {
296-
const handler = createCreateResourceHandler(mockLogger);
297-
const event = createMockEvent({
298-
httpMethod: 'POST',
299-
body: JSON.stringify({ emptyResponse: true }),
281+
body: JSON.stringify({}),
300282
headers: {
301-
'X-Request-ID': 'empty-test-1234-5678-9abc-def012345678',
283+
'X-Request-ID': 'empty-response',
302284
},
303285
});
304286

@@ -308,20 +290,20 @@ describe('POST Create Resource Handler', () => {
308290
expect(response.body).toBe('{}');
309291
});
310292

311-
it('should trigger error scenarios via triggerError field', async () => {
293+
it('should trigger error scenarios via X-Request-ID', async () => {
312294
const handler = createCreateResourceHandler(mockLogger);
313295
const event = createMockEvent({
314296
httpMethod: 'POST',
315-
body: JSON.stringify({ triggerError: 'error-409-conflict' }),
297+
body: JSON.stringify({}),
316298
headers: {
317-
'X-Request-ID': 'error-test-1234-5678-9abc-def012345678',
299+
'X-Request-ID': 'error-409-conflict',
318300
},
319301
});
320302

321303
const response = await handler(event);
322304

323305
expect(response.statusCode).toBe(409);
324306
const body = JSON.parse(response.body);
325-
expect(body.issue[0].details.coding[0].code).toBe('CONFLICT');
307+
expect(body.issue[0].details.text).toBe('CONFLICT');
326308
});
327309
});

0 commit comments

Comments
 (0)