Skip to content

Commit bd33c58

Browse files
committed
test: add ssm-service tests
Signed-off-by: Giovanni De Giorgio <[email protected]>
1 parent 1980e27 commit bd33c58

File tree

8 files changed

+200
-10
lines changed

8 files changed

+200
-10
lines changed

libs/providers/aws-ssm/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/providers/aws-ssm/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.0.1",
44
"dependencies": {
55
"@aws-sdk/client-ssm": "^3.759.0",
6+
"lru-cache": "^11.0.2",
67
"tslib": "^2.3.0"
78
},
89
"main": "./src/index.js",

libs/providers/aws-ssm/src/lib/aws-ssm-provider.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ const MOCK_SSM_CLIENT_CONFIG: SSMClientConfig = {
99
},
1010
};
1111

12-
describe(AwsSsmProvider.name, () => {});
12+
describe(AwsSsmProvider.name, () => {
13+
it('should pass', () => {
14+
expect(true);
15+
});
16+
});

libs/providers/aws-ssm/src/lib/aws-ssm-provider.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { GetParameterCommandInput, SSMClient, SSMClientConfig } from '@aws-sdk/client-ssm';
1010
import { AwsSsmProviderConfig } from './types';
1111
import { SSMService } from './ssm-service';
12+
import { Cache } from './cache';
1213

1314
export class AwsSsmProvider implements Provider {
1415
metadata = {
@@ -18,18 +19,29 @@ export class AwsSsmProvider implements Provider {
1819
readonly runsOn = 'server';
1920
readonly service: SSMService;
2021
hooks = [];
22+
cache: Cache;
2123

2224
constructor(config: AwsSsmProviderConfig) {
2325
this.service = new SSMService(config.ssmClientConfig);
26+
this.cache = new Cache(config.cacheOpts);
2427
}
2528

2629
async resolveBooleanEvaluation(
2730
flagKey: string,
2831
defaultValue: boolean,
2932
context: EvaluationContext,
3033
): Promise<ResolutionDetails<boolean>> {
34+
const cachedValue = this.cache.get(flagKey);
35+
if (cachedValue) {
36+
return {
37+
value: cachedValue.value,
38+
reason: StandardResolutionReasons.CACHED,
39+
};
40+
}
3141
try {
32-
return await this.service.getBooleanValue(flagKey, defaultValue);
42+
const res = await this.service.getBooleanValue(flagKey);
43+
this.cache.set(flagKey, res);
44+
return res;
3345
} catch (e) {
3446
return {
3547
value: defaultValue,
@@ -43,8 +55,17 @@ export class AwsSsmProvider implements Provider {
4355
defaultValue: string,
4456
context: EvaluationContext,
4557
): Promise<ResolutionDetails<string>> {
58+
const cachedValue = this.cache.get(flagKey);
59+
if (cachedValue) {
60+
return {
61+
value: cachedValue.value,
62+
reason: StandardResolutionReasons.CACHED,
63+
};
64+
}
4665
try {
47-
return await this.service.getStringValue(flagKey);
66+
const res = await this.service.getStringValue(flagKey);
67+
this.cache.set(flagKey, res);
68+
return res;
4869
} catch (e) {
4970
return {
5071
value: defaultValue,
@@ -53,19 +74,47 @@ export class AwsSsmProvider implements Provider {
5374
}
5475
}
5576

56-
resolveNumberEvaluation(
77+
async resolveNumberEvaluation(
5778
flagKey: string,
5879
defaultValue: number,
5980
context: EvaluationContext,
6081
): Promise<ResolutionDetails<number>> {
61-
throw new Error('Method not implemented.');
82+
const cachedValue = this.cache.get(flagKey);
83+
if (cachedValue) {
84+
return {
85+
value: cachedValue.value,
86+
reason: StandardResolutionReasons.CACHED,
87+
};
88+
}
89+
try {
90+
return await this.service.getNumberValue(flagKey);
91+
} catch (e) {
92+
return {
93+
value: defaultValue,
94+
reason: StandardResolutionReasons.DEFAULT,
95+
};
96+
}
6297
}
6398

64-
resolveObjectEvaluation<U extends JsonValue>(
99+
async resolveObjectEvaluation<U extends JsonValue>(
65100
flagKey: string,
66101
defaultValue: U,
67102
context: EvaluationContext,
68103
): Promise<ResolutionDetails<U>> {
69-
throw new Error('Method not implemented.');
104+
const cachedValue = this.cache.get(flagKey);
105+
if (cachedValue) {
106+
return {
107+
value: cachedValue.value,
108+
reason: StandardResolutionReasons.CACHED,
109+
};
110+
}
111+
try {
112+
return await this.service.getObjectValue(flagKey);
113+
} catch (e) {
114+
return {
115+
value: defaultValue,
116+
reason: StandardResolutionReasons.DEFAULT,
117+
};
118+
}
70119
}
71120
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ResolutionDetails } from '@openfeature/core';
2+
import { LRUCacheOpts } from './types';
3+
import { LRUCache } from 'lru-cache';
4+
5+
export class Cache {
6+
private cache: LRUCache<string, ResolutionDetails<any>>;
7+
private ttl: number;
8+
private enabled: boolean;
9+
constructor(opts: LRUCacheOpts) {
10+
this.cache = new LRUCache({
11+
maxSize: opts.size,
12+
sizeCalculation: () => 1,
13+
});
14+
this.ttl = opts.ttl;
15+
this.enabled = opts.enabled;
16+
}
17+
18+
get(key: string): ResolutionDetails<any> | undefined {
19+
if (!this.enabled) {
20+
return undefined;
21+
}
22+
return this.cache.get(key);
23+
}
24+
25+
set(key: string, value: ResolutionDetails<any>): void {
26+
if (!this.enabled) {
27+
return;
28+
}
29+
this.cache.set(key, value);
30+
}
31+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { ParseError, StandardResolutionReasons } from '@openfeature/core';
2+
import { SSMService } from './ssm-service';
3+
4+
describe(SSMService.name, () => {
5+
describe(SSMService.prototype.getBooleanValue.name, () => {
6+
beforeEach(() => {
7+
jest.clearAllMocks();
8+
});
9+
describe(`when _getParamFromSSM returns "true"`, () => {
10+
it(`should return a ResolutionDetails with value true`, async () => {
11+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue('true');
12+
const service = new SSMService({});
13+
const result = await service.getBooleanValue('test');
14+
expect(result).toEqual({ value: true, reason: StandardResolutionReasons.STATIC });
15+
});
16+
});
17+
describe(`when _getParamFromSSM returns "false"`, () => {
18+
it(`should return a ResolutionDetails with value true`, async () => {
19+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue('false');
20+
const service = new SSMService({});
21+
const result = await service.getBooleanValue('test');
22+
expect(result).toEqual({ value: false, reason: StandardResolutionReasons.STATIC });
23+
});
24+
});
25+
describe(`when _getParamFromSSM returns an invalid value`, () => {
26+
it('should throw a ParseError', () => {
27+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue('invalid boolean');
28+
const service = new SSMService({});
29+
expect(() => service.getBooleanValue('test')).rejects.toThrow(ParseError);
30+
});
31+
});
32+
});
33+
describe(SSMService.prototype.getStringValue.name, () => {
34+
beforeEach(() => {
35+
jest.clearAllMocks();
36+
});
37+
describe(`when _getParamFromSSM returns a valid value`, () => {
38+
it(`should return a ResolutionDetails with that value`, async () => {
39+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue('example');
40+
const service = new SSMService({});
41+
const result = await service.getStringValue('example');
42+
expect(result).toEqual({ value: 'example', reason: StandardResolutionReasons.STATIC });
43+
});
44+
});
45+
});
46+
describe(SSMService.prototype.getNumberValue.name, () => {
47+
beforeEach(() => {
48+
jest.clearAllMocks();
49+
});
50+
describe(`when _getParamFromSSM returns a valid number`, () => {
51+
it(`should return a ResolutionDetails with value true`, async () => {
52+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue('1478');
53+
const service = new SSMService({});
54+
const result = await service.getNumberValue('test');
55+
expect(result).toEqual({ value: 1478, reason: StandardResolutionReasons.STATIC });
56+
});
57+
});
58+
describe(`when _getParamFromSSM returns a value that is not a number`, () => {
59+
it(`should return a ParseError`, async () => {
60+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue('invalid number');
61+
const service = new SSMService({});
62+
expect(() => service.getNumberValue('test')).rejects.toThrow(ParseError);
63+
});
64+
});
65+
});
66+
describe(SSMService.prototype.getObjectValue.name, () => {
67+
beforeEach(() => {
68+
jest.clearAllMocks();
69+
});
70+
describe(`when _getParamFromSSM returns a valid object`, () => {
71+
it(`should return a ResolutionDetails with that object`, async () => {
72+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue(JSON.stringify({ test: true }));
73+
const service = new SSMService({});
74+
const result = await service.getObjectValue('test');
75+
expect(result).toEqual({ value: { test: true }, reason: StandardResolutionReasons.STATIC });
76+
});
77+
});
78+
describe(`when _getParamFromSSM returns an invalid object`, () => {
79+
it(`should return a ParseError`, async () => {
80+
jest.spyOn(SSMService.prototype, '_getValueFromSSM').mockResolvedValue('invalid object');
81+
const service = new SSMService({});
82+
expect(() => service.getObjectValue('test')).rejects.toThrow(ParseError);
83+
});
84+
});
85+
});
86+
});

libs/providers/aws-ssm/src/lib/ssm-service.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ResolutionDetails,
77
StandardResolutionReasons,
88
} from '@openfeature/core';
9+
import { Cache } from './cache';
910

1011
export class SSMService {
1112
client: SSMClient;
@@ -14,7 +15,7 @@ export class SSMService {
1415
this.client = new SSMClient(config);
1516
}
1617

17-
async getBooleanValue(name: string, defaultValue: boolean): Promise<ResolutionDetails<boolean>> {
18+
async getBooleanValue(name: string): Promise<ResolutionDetails<boolean>> {
1819
const res = await this._getValueFromSSM(name);
1920
let result: boolean;
2021
switch (res) {
@@ -43,7 +44,7 @@ export class SSMService {
4344

4445
async getNumberValue(name: string): Promise<ResolutionDetails<number>> {
4546
const res = await this._getValueFromSSM(name);
46-
if (Number.isNaN(res)) {
47+
if (Number.isNaN(Number(res))) {
4748
throw new ParseError(`${res} is not a number`);
4849
}
4950
return {
@@ -64,7 +65,7 @@ export class SSMService {
6465
}
6566
}
6667

67-
private async _getValueFromSSM(name: string): Promise<string> {
68+
async _getValueFromSSM(name: string): Promise<string> {
6869
const command: GetParameterCommand = new GetParameterCommand({
6970
Name: name,
7071
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { SSMClientConfig } from '@aws-sdk/client-ssm';
2+
import { LRUCache } from 'lru-cache';
23

34
export type AwsSsmProviderConfig = {
45
ssmClientConfig: SSMClientConfig;
6+
cacheOpts: LRUCacheOpts;
7+
};
8+
9+
export type LRUCacheOpts = {
10+
enabled: boolean;
11+
ttl: number;
12+
size: number;
513
};

0 commit comments

Comments
 (0)