@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
JavaScript
"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,