@medplum/cdk 2.1.8 → 2.1.9
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/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/config.d.ts +17 -0
- package/package.json +1 -1
- package/src/backend.ts +1 -1
- package/src/config.test.ts +487 -0
- package/src/config.ts +202 -0
- package/src/index.ts +13 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ExternalSecret, ExternalSecretPrimitive, ExternalSecretPrimitiveType, MedplumInfraConfig, MedplumSourceInfraConfig } from '@medplum/core';
|
|
2
|
+
export declare class InfraConfigNormalizer {
|
|
3
|
+
private config;
|
|
4
|
+
private clients;
|
|
5
|
+
constructor(config: MedplumSourceInfraConfig);
|
|
6
|
+
fetchParameterStoreSecret(key: string): Promise<string>;
|
|
7
|
+
fetchExternalSecret(externalSecret: ExternalSecret): Promise<ExternalSecretPrimitive>;
|
|
8
|
+
normalizeInfraConfigArray(currentVal: any[]): Promise<ExternalSecretPrimitive[] | Record<string, any>[]>;
|
|
9
|
+
normalizeValueForKey(obj: Record<string, any>, key: string): Promise<void>;
|
|
10
|
+
normalizeObjectInInfraConfig(obj: Record<string, any>): Promise<Record<string, any>>;
|
|
11
|
+
normalizeConfig(): Promise<MedplumInfraConfig>;
|
|
12
|
+
}
|
|
13
|
+
export declare function normalizeFetchedValue(key: string, rawValue: ExternalSecretPrimitive, expectedType: ExternalSecretPrimitiveType): ExternalSecretPrimitive;
|
|
14
|
+
export declare function isExternalSecretLike(obj: Record<string, any>): obj is ExternalSecret;
|
|
15
|
+
export declare function isExternalSecret(obj: Record<string, any>): obj is ExternalSecret;
|
|
16
|
+
export declare function assertValidExternalSecret(obj: Record<string, any>): asserts obj is ExternalSecret;
|
|
17
|
+
export declare function normalizeInfraConfig(config: MedplumSourceInfraConfig): Promise<MedplumInfraConfig>;
|
package/package.json
CHANGED
package/src/backend.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MedplumInfraConfig } from '@medplum/core';
|
|
2
2
|
import {
|
|
3
3
|
Duration,
|
|
4
|
+
RemovalPolicy,
|
|
4
5
|
aws_ec2 as ec2,
|
|
5
6
|
aws_ecs as ecs,
|
|
6
7
|
aws_elasticache as elasticache,
|
|
@@ -8,7 +9,6 @@ import {
|
|
|
8
9
|
aws_iam as iam,
|
|
9
10
|
aws_logs as logs,
|
|
10
11
|
aws_rds as rds,
|
|
11
|
-
RemovalPolicy,
|
|
12
12
|
aws_route53 as route53,
|
|
13
13
|
aws_s3 as s3,
|
|
14
14
|
aws_secretsmanager as secretsmanager,
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
|
|
2
|
+
import { ExternalSecret, MedplumInfraConfig, MedplumSourceInfraConfig, OperationOutcomeError } from '@medplum/core';
|
|
3
|
+
import { AwsClientStub, mockClient } from 'aws-sdk-client-mock';
|
|
4
|
+
import 'aws-sdk-client-mock-jest';
|
|
5
|
+
import {
|
|
6
|
+
InfraConfigNormalizer,
|
|
7
|
+
assertValidExternalSecret,
|
|
8
|
+
isExternalSecret,
|
|
9
|
+
normalizeFetchedValue,
|
|
10
|
+
normalizeInfraConfig,
|
|
11
|
+
} from './config';
|
|
12
|
+
|
|
13
|
+
const baseConfig = {
|
|
14
|
+
name: 'MyMedplumApp',
|
|
15
|
+
stackName: { system: 'aws_ssm_parameter_store', key: 'stackName', type: 'string' },
|
|
16
|
+
accountNumber: 'medplum123',
|
|
17
|
+
region: 'us-east-1',
|
|
18
|
+
domainName: 'foomedical.com',
|
|
19
|
+
vpcId: 'abc-321123',
|
|
20
|
+
apiPort: { system: 'aws_ssm_parameter_store', key: 'apiPort', type: 'number' },
|
|
21
|
+
apiDomainName: { system: 'aws_ssm_parameter_store', key: 'apiDomainName', type: 'string' },
|
|
22
|
+
apiSslCertArn: { system: 'aws_ssm_parameter_store', key: 'apiSslCertArn', type: 'string' },
|
|
23
|
+
apiInternetFacing: true,
|
|
24
|
+
appDomainName: 'app.foomedical.com',
|
|
25
|
+
appSslCertArn: 'arn:abc-123',
|
|
26
|
+
appApiProxy: { system: 'aws_ssm_parameter_store', key: 'appApiProxy', type: 'boolean' },
|
|
27
|
+
storageBucketName: { system: 'aws_ssm_parameter_store', key: 'storageBucketName', type: 'string' },
|
|
28
|
+
storageDomainName: 'storage.foomedical.com',
|
|
29
|
+
storageSslCertArn: 'arn:def-123',
|
|
30
|
+
signingKeyId: { system: 'aws_ssm_parameter_store', key: 'signingKeyId', type: 'string' },
|
|
31
|
+
storagePublicKey: { system: 'aws_ssm_parameter_store', key: 'storagePublicKey', type: 'string' },
|
|
32
|
+
baseUrl: 'foomedical.com',
|
|
33
|
+
maxAzs: { system: 'aws_ssm_parameter_store', key: 'maxAzs', type: 'number' },
|
|
34
|
+
rdsInstances: { system: 'aws_ssm_parameter_store', key: 'rdsInstances', type: 'number' },
|
|
35
|
+
rdsInstanceType: 'big',
|
|
36
|
+
desiredServerCount: { system: 'aws_ssm_parameter_store', key: 'desiredServerCount', type: 'number' },
|
|
37
|
+
serverImage: 'arn:our-image',
|
|
38
|
+
serverMemory: { system: 'aws_ssm_parameter_store', key: 'serverMemory', type: 'number' },
|
|
39
|
+
serverCpu: { system: 'aws_ssm_parameter_store', key: 'serverCpu', type: 'number' },
|
|
40
|
+
clamscanEnabled: false,
|
|
41
|
+
clamscanLoggingBucket: 'no_logging',
|
|
42
|
+
clamscanLoggingPrefix: 'foo_',
|
|
43
|
+
skipDns: true,
|
|
44
|
+
} as const satisfies MedplumSourceInfraConfig;
|
|
45
|
+
|
|
46
|
+
// TODO: Test throwing on missing region
|
|
47
|
+
|
|
48
|
+
const additionalContainers = [
|
|
49
|
+
{
|
|
50
|
+
name: 'BIG IMAGE',
|
|
51
|
+
image: 'arn:big_image',
|
|
52
|
+
environment: {
|
|
53
|
+
FOO: 'BAR',
|
|
54
|
+
MED: { system: 'aws_ssm_parameter_store', key: 'MED', type: 'string' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
const cloudTrailAlarms = {
|
|
60
|
+
logGroupName: { system: 'aws_ssm_parameter_store', key: 'logGroupName', type: 'string' },
|
|
61
|
+
logGroupCreate: { system: 'aws_ssm_parameter_store', key: 'logGroupCreate', type: 'boolean' },
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
describe('Config', () => {
|
|
65
|
+
describe('normalizeInfraConfig', () => {
|
|
66
|
+
let mockSSMClient: AwsClientStub<SSMClient>;
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
mockSSMClient = mockClient(SSMClient);
|
|
70
|
+
|
|
71
|
+
mockSSMClient.on(GetParameterCommand).rejects();
|
|
72
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'stackName' }).resolves({
|
|
73
|
+
Parameter: { Name: 'stackName', Value: 'MyFoomedicalStack' },
|
|
74
|
+
});
|
|
75
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'apiPort' }).resolves({
|
|
76
|
+
Parameter: { Name: 'apiPort', Value: '1337' },
|
|
77
|
+
});
|
|
78
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'apiDomainName' }).resolves({
|
|
79
|
+
Parameter: { Name: 'apiDomainName', Value: 'api.foomedical.com' },
|
|
80
|
+
});
|
|
81
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'apiSslCertArn' }).resolves({
|
|
82
|
+
Parameter: { Name: 'apiSslCertArn', Value: 'arn:foomedical_api_ssl_cert' },
|
|
83
|
+
});
|
|
84
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'appApiProxy' }).resolves({
|
|
85
|
+
Parameter: { Name: 'appApiProxy', Value: 'true' },
|
|
86
|
+
});
|
|
87
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'signingKeyId' }).resolves({
|
|
88
|
+
Parameter: { Name: 'signingKeyId', Value: 'key-abc123' },
|
|
89
|
+
});
|
|
90
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'storageBucketName' }).resolves({
|
|
91
|
+
Parameter: { Name: 'storageBucketName', Value: 'foomedical_storage_bucket' },
|
|
92
|
+
});
|
|
93
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'storagePublicKey' }).resolves({
|
|
94
|
+
Parameter: { Name: 'storagePublicKey', Value: 'VERY_LONG_KEY' },
|
|
95
|
+
});
|
|
96
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'maxAzs' }).resolves({
|
|
97
|
+
Parameter: { Name: 'maxAzs', Value: '6' },
|
|
98
|
+
});
|
|
99
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'rdsInstances' }).resolves({
|
|
100
|
+
Parameter: { Name: 'rdsInstances', Value: '10' },
|
|
101
|
+
});
|
|
102
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'desiredServerCount' }).resolves({
|
|
103
|
+
Parameter: { Name: 'desiredServerCount', Value: '0' },
|
|
104
|
+
});
|
|
105
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'serverMemory' }).resolves({
|
|
106
|
+
Parameter: { Name: 'serverMemory', Value: '16384' },
|
|
107
|
+
});
|
|
108
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'serverCpu' }).resolves({
|
|
109
|
+
Parameter: { Name: 'serverCpu', Value: '4096' },
|
|
110
|
+
});
|
|
111
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'MED' }).resolves({
|
|
112
|
+
Parameter: { Name: 'MED', Value: 'PLUM' },
|
|
113
|
+
});
|
|
114
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'logGroupName' }).resolves({
|
|
115
|
+
Parameter: { Name: 'logGroupName', Value: 'FOOMEDICAL_PROD' },
|
|
116
|
+
});
|
|
117
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'logGroupCreate' }).resolves({
|
|
118
|
+
Parameter: { Name: 'logGroupCreate', Value: 'false' },
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
mockSSMClient.restore();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('Missing `region` in config', async () => {
|
|
127
|
+
// @ts-expect-error Region must be defined
|
|
128
|
+
await expect(normalizeInfraConfig({ ...baseConfig, region: undefined })).rejects.toBeInstanceOf(
|
|
129
|
+
OperationOutcomeError
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('Valid infra source config w/ external secrets', async () => {
|
|
134
|
+
const result = await normalizeInfraConfig(baseConfig);
|
|
135
|
+
expect(result).toEqual<MedplumInfraConfig>({
|
|
136
|
+
name: 'MyMedplumApp',
|
|
137
|
+
stackName: 'MyFoomedicalStack',
|
|
138
|
+
accountNumber: 'medplum123',
|
|
139
|
+
region: 'us-east-1',
|
|
140
|
+
domainName: 'foomedical.com',
|
|
141
|
+
vpcId: 'abc-321123',
|
|
142
|
+
apiPort: 1337,
|
|
143
|
+
apiDomainName: 'api.foomedical.com',
|
|
144
|
+
apiSslCertArn: 'arn:foomedical_api_ssl_cert',
|
|
145
|
+
apiInternetFacing: true,
|
|
146
|
+
appDomainName: 'app.foomedical.com',
|
|
147
|
+
appSslCertArn: 'arn:abc-123',
|
|
148
|
+
appApiProxy: true,
|
|
149
|
+
storageBucketName: 'foomedical_storage_bucket',
|
|
150
|
+
storageDomainName: 'storage.foomedical.com',
|
|
151
|
+
storageSslCertArn: 'arn:def-123',
|
|
152
|
+
signingKeyId: 'key-abc123',
|
|
153
|
+
storagePublicKey: 'VERY_LONG_KEY',
|
|
154
|
+
baseUrl: 'foomedical.com',
|
|
155
|
+
maxAzs: 6,
|
|
156
|
+
rdsInstances: 10,
|
|
157
|
+
rdsInstanceType: 'big',
|
|
158
|
+
desiredServerCount: 0,
|
|
159
|
+
serverImage: 'arn:our-image',
|
|
160
|
+
serverMemory: 16384,
|
|
161
|
+
serverCpu: 4096,
|
|
162
|
+
clamscanEnabled: false,
|
|
163
|
+
clamscanLoggingBucket: 'no_logging',
|
|
164
|
+
clamscanLoggingPrefix: 'foo_',
|
|
165
|
+
skipDns: true,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('Valid source config w/ additional containers', async () => {
|
|
170
|
+
const result = await normalizeInfraConfig({ ...baseConfig, additionalContainers: [...additionalContainers] });
|
|
171
|
+
expect(result).toEqual<MedplumInfraConfig>({
|
|
172
|
+
name: 'MyMedplumApp',
|
|
173
|
+
stackName: 'MyFoomedicalStack',
|
|
174
|
+
accountNumber: 'medplum123',
|
|
175
|
+
region: 'us-east-1',
|
|
176
|
+
domainName: 'foomedical.com',
|
|
177
|
+
vpcId: 'abc-321123',
|
|
178
|
+
apiPort: 1337,
|
|
179
|
+
apiDomainName: 'api.foomedical.com',
|
|
180
|
+
apiSslCertArn: 'arn:foomedical_api_ssl_cert',
|
|
181
|
+
apiInternetFacing: true,
|
|
182
|
+
appDomainName: 'app.foomedical.com',
|
|
183
|
+
appSslCertArn: 'arn:abc-123',
|
|
184
|
+
appApiProxy: true,
|
|
185
|
+
storageBucketName: 'foomedical_storage_bucket',
|
|
186
|
+
storageDomainName: 'storage.foomedical.com',
|
|
187
|
+
storageSslCertArn: 'arn:def-123',
|
|
188
|
+
signingKeyId: 'key-abc123',
|
|
189
|
+
storagePublicKey: 'VERY_LONG_KEY',
|
|
190
|
+
baseUrl: 'foomedical.com',
|
|
191
|
+
maxAzs: 6,
|
|
192
|
+
rdsInstances: 10,
|
|
193
|
+
rdsInstanceType: 'big',
|
|
194
|
+
desiredServerCount: 0,
|
|
195
|
+
serverImage: 'arn:our-image',
|
|
196
|
+
serverMemory: 16384,
|
|
197
|
+
serverCpu: 4096,
|
|
198
|
+
clamscanEnabled: false,
|
|
199
|
+
clamscanLoggingBucket: 'no_logging',
|
|
200
|
+
clamscanLoggingPrefix: 'foo_',
|
|
201
|
+
skipDns: true,
|
|
202
|
+
additionalContainers: [
|
|
203
|
+
{
|
|
204
|
+
name: 'BIG IMAGE',
|
|
205
|
+
image: 'arn:big_image',
|
|
206
|
+
environment: {
|
|
207
|
+
FOO: 'BAR',
|
|
208
|
+
MED: 'PLUM',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('Valid source config w/ `cloudTrailAlarms`', async () => {
|
|
216
|
+
const result = await normalizeInfraConfig({ ...baseConfig, cloudTrailAlarms: { ...cloudTrailAlarms } });
|
|
217
|
+
expect(result).toEqual<MedplumInfraConfig>({
|
|
218
|
+
name: 'MyMedplumApp',
|
|
219
|
+
stackName: 'MyFoomedicalStack',
|
|
220
|
+
accountNumber: 'medplum123',
|
|
221
|
+
region: 'us-east-1',
|
|
222
|
+
domainName: 'foomedical.com',
|
|
223
|
+
vpcId: 'abc-321123',
|
|
224
|
+
apiPort: 1337,
|
|
225
|
+
apiDomainName: 'api.foomedical.com',
|
|
226
|
+
apiSslCertArn: 'arn:foomedical_api_ssl_cert',
|
|
227
|
+
apiInternetFacing: true,
|
|
228
|
+
appDomainName: 'app.foomedical.com',
|
|
229
|
+
appSslCertArn: 'arn:abc-123',
|
|
230
|
+
appApiProxy: true,
|
|
231
|
+
storageBucketName: 'foomedical_storage_bucket',
|
|
232
|
+
storageDomainName: 'storage.foomedical.com',
|
|
233
|
+
storageSslCertArn: 'arn:def-123',
|
|
234
|
+
signingKeyId: 'key-abc123',
|
|
235
|
+
storagePublicKey: 'VERY_LONG_KEY',
|
|
236
|
+
baseUrl: 'foomedical.com',
|
|
237
|
+
maxAzs: 6,
|
|
238
|
+
rdsInstances: 10,
|
|
239
|
+
rdsInstanceType: 'big',
|
|
240
|
+
desiredServerCount: 0,
|
|
241
|
+
serverImage: 'arn:our-image',
|
|
242
|
+
serverMemory: 16384,
|
|
243
|
+
serverCpu: 4096,
|
|
244
|
+
clamscanEnabled: false,
|
|
245
|
+
clamscanLoggingBucket: 'no_logging',
|
|
246
|
+
clamscanLoggingPrefix: 'foo_',
|
|
247
|
+
skipDns: true,
|
|
248
|
+
cloudTrailAlarms: {
|
|
249
|
+
logGroupName: 'FOOMEDICAL_PROD',
|
|
250
|
+
logGroupCreate: false,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('Invalid system', async () => {
|
|
256
|
+
await expect(
|
|
257
|
+
// @ts-expect-error System is not valid
|
|
258
|
+
normalizeInfraConfig({ ...baseConfig, apiPort: { system: 'google_drive', key: 'abc', type: 'number' } })
|
|
259
|
+
).rejects.toBeInstanceOf(OperationOutcomeError);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('Invalid AWS Param Store key', async () => {
|
|
263
|
+
await expect(
|
|
264
|
+
normalizeInfraConfig({
|
|
265
|
+
...baseConfig,
|
|
266
|
+
apiPort: { system: 'aws_ssm_parameter_store', key: 'abc', type: 'number' },
|
|
267
|
+
})
|
|
268
|
+
).rejects.toBeInstanceOf(Error);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('Invalid type specified', async () => {
|
|
272
|
+
await expect(
|
|
273
|
+
normalizeInfraConfig({
|
|
274
|
+
...baseConfig,
|
|
275
|
+
// @ts-expect-error Type 'plum' not a valid type
|
|
276
|
+
apiPort: { system: 'aws_ssm_parameter_store', key: 'abc', type: 'plum' },
|
|
277
|
+
})
|
|
278
|
+
).rejects.toBeInstanceOf(OperationOutcomeError);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('Mismatched type specified', async () => {
|
|
282
|
+
await expect(
|
|
283
|
+
normalizeInfraConfig({
|
|
284
|
+
...baseConfig,
|
|
285
|
+
// @ts-expect-error Type 'boolean' is not the proper type for `apiPort`
|
|
286
|
+
apiPort: { system: 'aws_ssm_parameter_store', key: 'apiPort', type: 'boolean' },
|
|
287
|
+
})
|
|
288
|
+
).rejects.toBeInstanceOf(OperationOutcomeError);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('normalizeFetchedValue', () => {
|
|
293
|
+
// Test [object, string] => throws
|
|
294
|
+
test('Provided object, expected string => throws', () => {
|
|
295
|
+
// @ts-expect-error rawValue must be a valid primitive, string | boolean | number
|
|
296
|
+
expect(() => normalizeFetchedValue('medplumString', { med: 'plum' }, 'string')).toThrowError(
|
|
297
|
+
OperationOutcomeError
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
// Test [string, string] => return raw
|
|
301
|
+
test('Provided string, expected string => rawValue', () => {
|
|
302
|
+
expect(normalizeFetchedValue('medplumString', 'medplum', 'string')).toEqual('medplum');
|
|
303
|
+
});
|
|
304
|
+
// Test [string, number] => number
|
|
305
|
+
test('Provided string, expected number => parseInt(string)', () => {
|
|
306
|
+
expect(normalizeFetchedValue('medplumNumber', '20', 'number')).toEqual(20);
|
|
307
|
+
});
|
|
308
|
+
// Test [number, number] => rawValue
|
|
309
|
+
test('Provided number, expected number => rawValue', () => {
|
|
310
|
+
expect(normalizeFetchedValue('medplumNumber', 20, 'number')).toEqual(20);
|
|
311
|
+
});
|
|
312
|
+
// Test [invalidNumStr, number] => throws
|
|
313
|
+
test('Provided non-numeric string, expected number => throws', () => {
|
|
314
|
+
expect(() => normalizeFetchedValue('medplumNumber', 'medplum', 'number')).toThrowError(OperationOutcomeError);
|
|
315
|
+
});
|
|
316
|
+
// Test [string, boolean] => boolean
|
|
317
|
+
test('Provided string, expected boolean => parsedBoolean', () => {
|
|
318
|
+
expect(normalizeFetchedValue('medplumBool', 'TRUE', 'boolean')).toEqual(true);
|
|
319
|
+
expect(normalizeFetchedValue('medplumBool', 'false', 'boolean')).toEqual(false);
|
|
320
|
+
expect(normalizeFetchedValue('medplumBool', 'TrUe', 'boolean')).toEqual(true);
|
|
321
|
+
expect(normalizeFetchedValue('medplumBool', 'FALSE', 'boolean')).toEqual(false);
|
|
322
|
+
});
|
|
323
|
+
// Test [invalidStr, boolean] => throws
|
|
324
|
+
test('Provided string, expected boolean => parsedBoolean', () => {
|
|
325
|
+
expect(() => normalizeFetchedValue('medplumBool', 'TRUEE', 'boolean')).toThrowError(OperationOutcomeError);
|
|
326
|
+
expect(() => normalizeFetchedValue('medplumBool', '10', 'boolean')).toThrowError(OperationOutcomeError);
|
|
327
|
+
expect(() => normalizeFetchedValue('medplumBool', '0', 'boolean')).toThrowError(OperationOutcomeError);
|
|
328
|
+
});
|
|
329
|
+
// Test [bool, number] => throws
|
|
330
|
+
test('Provided boolean, expected number => throws', () => {
|
|
331
|
+
expect(() => normalizeFetchedValue('medplumNumber', true, 'number')).toThrowError(OperationOutcomeError);
|
|
332
|
+
});
|
|
333
|
+
// Test [string, invalid_type] => throws
|
|
334
|
+
test('Provided string, expected {invalidType} => throws', () => {
|
|
335
|
+
// @ts-expect-error Plum is not a valid expectedType
|
|
336
|
+
expect(() => normalizeFetchedValue('medplum???', 'medplum', 'plum')).toThrowError(OperationOutcomeError);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('fetchParameterStoreSecret', () => {
|
|
341
|
+
let mockSSMClient: AwsClientStub<SSMClient>;
|
|
342
|
+
let configNormalizer: InfraConfigNormalizer;
|
|
343
|
+
|
|
344
|
+
beforeEach(() => {
|
|
345
|
+
mockSSMClient = mockClient(SSMClient);
|
|
346
|
+
|
|
347
|
+
mockSSMClient.on(GetParameterCommand).rejects();
|
|
348
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'stackName' }).resolves({
|
|
349
|
+
Parameter: { Name: 'stackName', Value: 'MyFoomedicalStack' },
|
|
350
|
+
});
|
|
351
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'emptyValue' }).resolves({
|
|
352
|
+
Parameter: { Name: 'emptyValue' },
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
configNormalizer = new InfraConfigNormalizer(baseConfig);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
afterEach(() => {
|
|
359
|
+
mockSSMClient.restore();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('Valid key in param store', async () => {
|
|
363
|
+
await expect(configNormalizer.fetchParameterStoreSecret('stackName')).resolves.toEqual('MyFoomedicalStack');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('Invalid key in param store', async () => {
|
|
367
|
+
await expect(configNormalizer.fetchParameterStoreSecret('medplum')).rejects.toBeInstanceOf(Error);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('Valid key with no value', async () => {
|
|
371
|
+
await expect(configNormalizer.fetchParameterStoreSecret('emptyValue')).rejects.toBeInstanceOf(
|
|
372
|
+
OperationOutcomeError
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('normalizeObjectInInfraConfig', () => {
|
|
378
|
+
let mockSSMClient: AwsClientStub<SSMClient>;
|
|
379
|
+
let configNormalizer: InfraConfigNormalizer;
|
|
380
|
+
|
|
381
|
+
beforeEach(() => {
|
|
382
|
+
mockSSMClient = mockClient(SSMClient);
|
|
383
|
+
|
|
384
|
+
mockSSMClient.on(GetParameterCommand).rejects();
|
|
385
|
+
mockSSMClient.on(GetParameterCommand, { Name: 'medplumSecret' }).resolves({
|
|
386
|
+
Parameter: { Name: 'medplumSecret', Value: 'MyMedplumSecret' },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
configNormalizer = new InfraConfigNormalizer(baseConfig);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
afterEach(() => {
|
|
393
|
+
mockSSMClient.restore();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('Array of primitives or secrets', async () => {
|
|
397
|
+
expect(
|
|
398
|
+
await configNormalizer.normalizeObjectInInfraConfig({
|
|
399
|
+
medplumStuff: [
|
|
400
|
+
'medplum',
|
|
401
|
+
{
|
|
402
|
+
system: 'aws_ssm_parameter_store',
|
|
403
|
+
key: 'medplumSecret',
|
|
404
|
+
type: 'string',
|
|
405
|
+
} satisfies ExternalSecret<'string'>,
|
|
406
|
+
],
|
|
407
|
+
})
|
|
408
|
+
).toEqual({ medplumStuff: ['medplum', 'MyMedplumSecret'] });
|
|
409
|
+
expect(
|
|
410
|
+
await configNormalizer.normalizeObjectInInfraConfig({
|
|
411
|
+
medplumStuff: [
|
|
412
|
+
{
|
|
413
|
+
system: 'aws_ssm_parameter_store',
|
|
414
|
+
key: 'medplumSecret',
|
|
415
|
+
type: 'string',
|
|
416
|
+
} satisfies ExternalSecret<'string'>,
|
|
417
|
+
'medplum',
|
|
418
|
+
],
|
|
419
|
+
})
|
|
420
|
+
).toEqual({ medplumStuff: ['MyMedplumSecret', 'medplum'] });
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe('assertValidExternalSecret', () => {
|
|
425
|
+
// Test perfectly valid secret
|
|
426
|
+
test('Valid ExternalSecret', () => {
|
|
427
|
+
expect(() =>
|
|
428
|
+
assertValidExternalSecret({
|
|
429
|
+
system: 'aws_ssm_parameter_store',
|
|
430
|
+
key: 'medplumString',
|
|
431
|
+
type: 'string',
|
|
432
|
+
} satisfies ExternalSecret<'string'>)
|
|
433
|
+
).not.toThrow();
|
|
434
|
+
});
|
|
435
|
+
// Test secret with shape but invalid type
|
|
436
|
+
test('Almost valid ExternalSecret, invalid type value', () => {
|
|
437
|
+
expect(() =>
|
|
438
|
+
assertValidExternalSecret({
|
|
439
|
+
system: 'aws_ssm_parameter_store',
|
|
440
|
+
key: 'medplumString',
|
|
441
|
+
type: 'plum',
|
|
442
|
+
})
|
|
443
|
+
).toThrowError(OperationOutcomeError);
|
|
444
|
+
});
|
|
445
|
+
// Test completely invalid secret
|
|
446
|
+
test('Invalid ExternalSecret', () => {
|
|
447
|
+
expect(() =>
|
|
448
|
+
assertValidExternalSecret({
|
|
449
|
+
key: 10,
|
|
450
|
+
type: true,
|
|
451
|
+
})
|
|
452
|
+
).toThrowError(OperationOutcomeError);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe('isExternalSecret', () => {
|
|
457
|
+
// Test perfectly valid secret
|
|
458
|
+
test('Valid ExternalSecret', () => {
|
|
459
|
+
expect(
|
|
460
|
+
isExternalSecret({
|
|
461
|
+
system: 'aws_ssm_parameter_store',
|
|
462
|
+
key: 'medplumString',
|
|
463
|
+
type: 'string',
|
|
464
|
+
} satisfies ExternalSecret<'string'>)
|
|
465
|
+
).toEqual(true);
|
|
466
|
+
});
|
|
467
|
+
// Test secret with shape but invalid type
|
|
468
|
+
test('Almost valid ExternalSecret, invalid type value', () => {
|
|
469
|
+
expect(
|
|
470
|
+
isExternalSecret({
|
|
471
|
+
system: 'aws_ssm_parameter_store',
|
|
472
|
+
key: 'medplumString',
|
|
473
|
+
type: 'plum',
|
|
474
|
+
})
|
|
475
|
+
).toEqual(false);
|
|
476
|
+
});
|
|
477
|
+
// Test completely invalid secret
|
|
478
|
+
test('Invalid ExternalSecret', () => {
|
|
479
|
+
expect(
|
|
480
|
+
isExternalSecret({
|
|
481
|
+
key: 10,
|
|
482
|
+
type: true,
|
|
483
|
+
})
|
|
484
|
+
).toEqual(false);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
});
|