@medplum/cdk 2.1.7 → 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medplum/cdk",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "description": "Medplum CDK Infra as Code",
5
5
  "author": "Medplum <hello@medplum.com>",
6
6
  "license": "Apache-2.0",
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
+ });