UNPKG

@nestjs-aws/systems-manager

Version:

NestJS module for AWS Systems Manager (Parameter Store & Secrets Manager). Seamlessly integrate AWS SSM parameters and secrets into your NestJS applications with TypeScript support, automatic refresh, and hierarchical configuration management.

1,052 lines (1,051 loc) 89.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const systems_manager_service_1 = require("./systems-manager.service"); const constants_1 = require("./constants"); const services_1 = require("./services"); describe('SystemsManagerService', () => { let service; const mockParameters = [ { Name: '/app/config/database-host', Value: 'localhost' }, { Name: '/app/config/database-port', Value: '5432' }, { Name: '/app/config/api-key', Value: 'secret-key-123' }, { Name: '/app/config/timeout', Value: '30' }, { Name: '/app/config/feature-flag', Value: 'true' }, ]; const mockConfig = { awsRegion: 'us-east-1', awsParamStorePath: '/app/config', awsParamStoreContinueOnError: false, }; beforeEach(async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: mockParameters, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); service = module.get(systems_manager_service_1.SystemsManagerService); }); afterEach(() => { jest.clearAllMocks(); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('constructor', () => { it('should initialize with parameters from AWS', () => { expect(service).toBeInstanceOf(systems_manager_service_1.SystemsManagerService); }); it('should extract parameter names from full paths', () => { expect(service.get('database-host')).toBe('localhost'); expect(service.get('database-port')).toBe('5432'); expect(service.get('api-key')).toBe('secret-key-123'); }); it('should handle parameters with multiple path segments', async () => { const moduleWithDeepPath = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/production/us-east-1/database/primary-host', Value: 'prod-db.example.com', }, ], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const serviceWithDeepPath = moduleWithDeepPath.get(systems_manager_service_1.SystemsManagerService); expect(serviceWithDeepPath.get('primary-host')).toBe('prod-db.example.com'); }); it('should handle empty parameter array', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const emptyService = module.get(systems_manager_service_1.SystemsManagerService); expect(emptyService).toBeDefined(); expect(emptyService.get('any-key')).toBeUndefined(); }); }); describe('get', () => { it('should return parameter value by key', () => { expect(service.get('database-host')).toBe('localhost'); expect(service.get('api-key')).toBe('secret-key-123'); expect(service.get('timeout')).toBe('30'); }); it('should return undefined for non-existent key', () => { expect(service.get('non-existent-key')).toBeUndefined(); expect(service.get('')).toBeUndefined(); expect(service.get('random')).toBeUndefined(); }); it('should handle keys with special characters', async () => { const moduleWithSpecialChars = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/api-key-v2', Value: 'key-value' }, { Name: '/app/config/db_connection', Value: 'connection-string', }, ], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const specialService = moduleWithSpecialChars.get(systems_manager_service_1.SystemsManagerService); expect(specialService.get('api-key-v2')).toBe('key-value'); expect(specialService.get('db_connection')).toBe('connection-string'); }); it('should be case-sensitive', () => { expect(service.get('database-host')).toBe('localhost'); expect(service.get('Database-Host')).toBeUndefined(); expect(service.get('DATABASE-HOST')).toBeUndefined(); }); }); describe('getAsNumber', () => { it('should convert string to number', () => { expect(service.getAsNumber('database-port')).toBe(5432); expect(service.getAsNumber('timeout')).toBe(30); }); it('should return NaN for non-numeric values', () => { expect(service.getAsNumber('database-host')).toBeNaN(); expect(service.getAsNumber('api-key')).toBeNaN(); }); it('should return NaN for non-existent key', () => { expect(service.getAsNumber('non-existent')).toBeNaN(); }); it('should handle decimal numbers', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/rate-limit', Value: '99.5' }, { Name: '/app/config/percentage', Value: '0.95' }, ], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const decimalService = module.get(systems_manager_service_1.SystemsManagerService); expect(decimalService.getAsNumber('rate-limit')).toBe(99.5); expect(decimalService.getAsNumber('percentage')).toBe(0.95); }); it('should handle negative numbers', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/offset', Value: '-10' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const negativeService = module.get(systems_manager_service_1.SystemsManagerService); expect(negativeService.getAsNumber('offset')).toBe(-10); }); it('should handle zero', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/retries', Value: '0' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const zeroService = module.get(systems_manager_service_1.SystemsManagerService); expect(zeroService.getAsNumber('retries')).toBe(0); }); it('should handle scientific notation', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/large-number', Value: '1e5' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const scientificService = module.get(systems_manager_service_1.SystemsManagerService); expect(scientificService.getAsNumber('large-number')).toBe(100000); }); }); describe('edge cases', () => { it('should handle parameters with same final segment name', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/database/host', Value: 'db1.example.com' }, { Name: '/app/cache/host', Value: 'cache1.example.com' }, ], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const duplicateService = module.get(systems_manager_service_1.SystemsManagerService); const hostValue = duplicateService.get('host'); expect(hostValue).toBe('cache1.example.com'); }); it('should handle parameters with undefined values', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/optional', Value: undefined }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const undefinedService = module.get(systems_manager_service_1.SystemsManagerService); expect(undefinedService.get('optional')).toBeUndefined(); }); it('should handle parameters with empty string values', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/empty', Value: '' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const emptyService = module.get(systems_manager_service_1.SystemsManagerService); expect(emptyService.get('empty')).toBe(''); }); it('should handle parameters ending with slash', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/trailing/', Value: 'test-value' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const trailingService = module.get(systems_manager_service_1.SystemsManagerService); expect(trailingService.get('')).toBe('test-value'); }); }); describe('getOrDefault', () => { it('should return parameter value if key exists', () => { expect(service.getOrDefault('database-host', 'default-host')).toBe('localhost'); expect(service.getOrDefault('api-key', 'default-key')).toBe('secret-key-123'); }); it('should return default value if key does not exist', () => { expect(service.getOrDefault('non-existent', 'default-value')).toBe('default-value'); expect(service.getOrDefault('missing-key', 'fallback')).toBe('fallback'); }); it('should handle empty string as default', () => { expect(service.getOrDefault('non-existent', '')).toBe(''); }); it('should return empty string value if it exists', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/empty', Value: '' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const emptyService = module.get(systems_manager_service_1.SystemsManagerService); expect(emptyService.getOrDefault('empty', 'default')).toBe(''); }); }); describe('getAsBoolean', () => { it('should return true for "true" value', () => { expect(service.getAsBoolean('feature-flag')).toBe(true); }); it('should return true for "1" value', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/enabled', Value: '1' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const boolService = module.get(systems_manager_service_1.SystemsManagerService); expect(boolService.getAsBoolean('enabled')).toBe(true); }); it('should return true for "yes" value', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/active', Value: 'yes' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const boolService = module.get(systems_manager_service_1.SystemsManagerService); expect(boolService.getAsBoolean('active')).toBe(true); }); it('should return false for "false" value', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/disabled', Value: 'false' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const boolService = module.get(systems_manager_service_1.SystemsManagerService); expect(boolService.getAsBoolean('disabled')).toBe(false); }); it('should return false for "0" value', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/off', Value: '0' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const boolService = module.get(systems_manager_service_1.SystemsManagerService); expect(boolService.getAsBoolean('off')).toBe(false); }); it('should return false for non-existent key', () => { expect(service.getAsBoolean('non-existent')).toBe(false); }); it('should be case-insensitive', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/upper', Value: 'TRUE' }, { Name: '/app/config/mixed', Value: 'Yes' }, ], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const boolService = module.get(systems_manager_service_1.SystemsManagerService); expect(boolService.getAsBoolean('upper')).toBe(true); expect(boolService.getAsBoolean('mixed')).toBe(true); }); }); describe('getAsJSON', () => { it('should parse JSON object', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/json-obj', Value: '{"host":"localhost","port":5432}', }, ], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const jsonService = module.get(systems_manager_service_1.SystemsManagerService); const result = jsonService.getAsJSON('json-obj'); expect(result).toEqual({ host: 'localhost', port: 5432 }); expect(result.host).toBe('localhost'); expect(result.port).toBe(5432); }); it('should parse JSON array', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/json-arr', Value: '["item1","item2","item3"]', }, ], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const jsonService = module.get(systems_manager_service_1.SystemsManagerService); const result = jsonService.getAsJSON('json-arr'); expect(result).toEqual(['item1', 'item2', 'item3']); expect(result.length).toBe(3); }); it('should throw error for invalid JSON', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/invalid', Value: 'not-json' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const jsonService = module.get(systems_manager_service_1.SystemsManagerService); expect(() => jsonService.getAsJSON('invalid')).toThrow(SyntaxError); }); }); describe('has', () => { it('should return true for existing keys', () => { expect(service.has('database-host')).toBe(true); expect(service.has('database-port')).toBe(true); expect(service.has('api-key')).toBe(true); }); it('should return false for non-existent keys', () => { expect(service.has('non-existent')).toBe(false); expect(service.has('missing-key')).toBe(false); expect(service.has('')).toBe(false); }); it('should work with empty string values', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/empty', Value: '' }], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const emptyService = module.get(systems_manager_service_1.SystemsManagerService); expect(emptyService.has('empty')).toBe(true); }); }); describe('getAllKeys', () => { it('should return all parameter keys', () => { const keys = service.getAllKeys(); expect(keys).toContain('database-host'); expect(keys).toContain('database-port'); expect(keys).toContain('api-key'); expect(keys).toContain('timeout'); expect(keys).toContain('feature-flag'); expect(keys.length).toBe(5); }); it('should return empty array when no parameters exist', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const emptyService = module.get(systems_manager_service_1.SystemsManagerService); const keys = emptyService.getAllKeys(); expect(keys).toEqual([]); expect(keys.length).toBe(0); }); it('should return array that can be iterated', () => { const keys = service.getAllKeys(); let count = 0; keys.forEach((key) => { expect(typeof key).toBe('string'); count++; }); expect(count).toBe(5); }); }); describe('getAll', () => { it('should return all parameters as object', () => { const all = service.getAll(); expect(all).toEqual({ 'database-host': 'localhost', 'database-port': '5432', 'api-key': 'secret-key-123', timeout: '30', 'feature-flag': 'true', }); }); it('should return a copy of parameters', () => { const all1 = service.getAll(); const all2 = service.getAll(); expect(all1).toEqual(all2); expect(all1).not.toBe(all2); }); it('should return empty object when no parameters exist', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [], }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: mockConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const emptyService = module.get(systems_manager_service_1.SystemsManagerService); const all = emptyService.getAll(); expect(all).toEqual({}); expect(Object.keys(all).length).toBe(0); }); it('should not affect internal state when modified', () => { const all = service.getAll(); all['new-key'] = 'new-value'; expect(service.has('new-key')).toBe(false); expect(service.get('new-key')).toBeUndefined(); }); }); describe('refresh', () => { it('should refresh parameters from AWS', async () => { const fetchParametersSpy = jest.spyOn(service['parameterFetcher'], 'fetchParameters'); const newParameters = [ { Name: '/app/config/database-host', Value: 'updated-host' }, { Name: '/app/config/database-port', Value: '3306' }, ]; fetchParametersSpy.mockResolvedValue(newParameters); expect(service.get('database-host')).toBe('localhost'); expect(service.get('database-port')).toBe('5432'); await service.refresh(); expect(fetchParametersSpy).toHaveBeenCalledWith('us-east-1', '/app/config', false); expect(service.get('database-host')).toBe('updated-host'); expect(service.get('database-port')).toBe('3306'); fetchParametersSpy.mockRestore(); }); it('should update all parameter access methods after refresh', async () => { const fetchParametersSpy = jest.spyOn(service['parameterFetcher'], 'fetchParameters'); const newParameters = [ { Name: '/app/config/new-key', Value: 'new-value' }, { Name: '/app/config/count', Value: '42' }, ]; fetchParametersSpy.mockResolvedValue(newParameters); expect(service.has('new-key')).toBe(false); expect(service.getAllKeys()).not.toContain('new-key'); await service.refresh(); expect(service.has('new-key')).toBe(true); expect(service.get('new-key')).toBe('new-value'); expect(service.getAsNumber('count')).toBe(42); expect(service.getAllKeys()).toContain('new-key'); expect(service.getAllKeys()).toContain('count'); fetchParametersSpy.mockRestore(); }); it('should remove old parameters after refresh', async () => { const fetchParametersSpy = jest.spyOn(service['parameterFetcher'], 'fetchParameters'); const newParameters = [ { Name: '/app/config/only-this', Value: 'value' }, ]; fetchParametersSpy.mockResolvedValue(newParameters); expect(service.has('database-host')).toBe(true); expect(service.getAllKeys().length).toBe(5); await service.refresh(); expect(service.has('database-host')).toBe(false); expect(service.has('only-this')).toBe(true); expect(service.getAllKeys()).toEqual(['only-this']); fetchParametersSpy.mockRestore(); }); it('should handle refresh with empty parameters', async () => { const fetchParametersSpy = jest.spyOn(service['parameterFetcher'], 'fetchParameters'); fetchParametersSpy.mockResolvedValue([]); expect(service.getAllKeys().length).toBe(5); await service.refresh(); expect(service.getAllKeys()).toEqual([]); expect(service.get('database-host')).toBeUndefined(); fetchParametersSpy.mockRestore(); }); it('should propagate errors from getSSMParameters', async () => { const fetchParametersSpy = jest.spyOn(service['parameterFetcher'], 'fetchParameters'); const testError = new Error('AWS API Error'); fetchParametersSpy.mockRejectedValue(testError); await expect(service.refresh()).rejects.toThrow('AWS API Error'); fetchParametersSpy.mockRestore(); }); it('should use config values from constructor', async () => { const customConfig = { awsRegion: 'eu-west-1', awsParamStorePath: '/custom/path', awsParamStoreContinueOnError: true, }; const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: mockParameters, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, { provide: 'PARAM_STORE_CONFIG', useValue: customConfig, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const customService = module.get(systems_manager_service_1.SystemsManagerService); const fetchParametersSpy = jest.spyOn(customService['parameterFetcher'], 'fetchParameters'); fetchParametersSpy.mockResolvedValue([]); await customService.refresh(); expect(fetchParametersSpy).toHaveBeenCalledWith('eu-west-1', '/custom/path', true); fetchParametersSpy.mockRestore(); }); }); describe('Parameter Hierarchy Support', () => { describe('with preserveHierarchy disabled (default behavior)', () => { it('should store parameters with only last segment', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/database/host', Value: 'localhost' }, { Name: '/app/config/database/port', Value: '5432' }, { Name: '/app/config/api/key', Value: 'secret' }, ], }, { provide: 'PARAM_STORE_CONFIG', useValue: { awsRegion: 'us-east-1', awsParamStorePath: '/app/config', awsParamStoreContinueOnError: false, preserveHierarchy: false, }, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const service = module.get(systems_manager_service_1.SystemsManagerService); expect(service.get('host')).toBe('localhost'); expect(service.get('port')).toBe('5432'); expect(service.get('key')).toBe('secret'); expect(service.get('database.host')).toBeUndefined(); expect(service.get('api.key')).toBeUndefined(); }); }); describe('with preserveHierarchy enabled', () => { it('should preserve path structure with default separator', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/database/host', Value: 'localhost' }, { Name: '/app/config/database/port', Value: '5432' }, { Name: '/app/config/api/key', Value: 'secret' }, ], }, { provide: 'PARAM_STORE_CONFIG', useValue: { awsRegion: 'us-east-1', awsParamStorePath: '/app/config', awsParamStoreContinueOnError: false, preserveHierarchy: true, pathSeparator: '.', }, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const service = module.get(systems_manager_service_1.SystemsManagerService); expect(service.get('database.host')).toBe('localhost'); expect(service.get('database.port')).toBe('5432'); expect(service.get('api.key')).toBe('secret'); expect(service.get('host')).toBeUndefined(); expect(service.get('port')).toBeUndefined(); expect(service.get('key')).toBeUndefined(); }); it('should use custom path separator', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/database/host', Value: 'localhost' }, { Name: '/app/config/api/auth/token', Value: 'secret-token' }, ], }, { provide: 'PARAM_STORE_CONFIG', useValue: { awsRegion: 'us-east-1', awsParamStorePath: '/app/config', awsParamStoreContinueOnError: false, preserveHierarchy: true, pathSeparator: '/', }, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const service = module.get(systems_manager_service_1.SystemsManagerService); expect(service.get('database/host')).toBe('localhost'); expect(service.get('api/auth/token')).toBe('secret-token'); }); it('should handle deeply nested paths', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [ { Name: '/app/config/api/auth/jwt/secret', Value: 'jwt-secret', }, { Name: '/app/config/api/auth/jwt/expiry', Value: '3600', }, ], }, { provide: 'PARAM_STORE_CONFIG', useValue: { awsRegion: 'us-east-1', awsParamStorePath: '/app/config', awsParamStoreContinueOnError: false, preserveHierarchy: true, pathSeparator: '.', }, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const service = module.get(systems_manager_service_1.SystemsManagerService); expect(service.get('api.auth.jwt.secret')).toBe('jwt-secret'); expect(service.get('api.auth.jwt.expiry')).toBe('3600'); expect(service.getAsNumber('api.auth.jwt.expiry')).toBe(3600); }); it('should handle single-level parameters', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/version', Value: '1.0.0' }], }, { provide: 'PARAM_STORE_CONFIG', useValue: { awsRegion: 'us-east-1', awsParamStorePath: '/app/config', awsParamStoreContinueOnError: false, preserveHierarchy: true, pathSeparator: '.', }, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const service = module.get(systems_manager_service_1.SystemsManagerService); expect(service.get('version')).toBe('1.0.0'); }); it('should handle parameters with trailing slashes', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService, { provide: constants_1.AWS_PARAM_STORE_PROVIDER, useValue: [{ Name: '/app/config/database/', Value: 'db-value' }], }, { provide: 'PARAM_STORE_CONFIG', useValue: { awsRegion: 'us-east-1', awsParamStorePath: '/app/config', awsParamStoreContinueOnError: false, preserveHierarchy: true, pathSeparator: '.', }, }, { provide: constants_1.AWS_SECRETS_MANAGER_PROVIDER, useValue: {}, }, services_1.ParameterStoreFetcherService, services_1.SecretsManagerFetcherService, ], }).compile(); const service = module.get(systems_manager_service_1.SystemsManagerService); expect(service.get('database')).toBe('db-value'); }); it('should work with all service methods', async () => { const module = await testing_1.Test.createTestingModule({ providers: [ systems_manager_service_1.SystemsManagerService,