Skip to content

Commit 3ce4aec

Browse files
committed
Written unit tests for DynamoDBProvider
1 parent ef9abb1 commit 3ce4aec

File tree

2 files changed

+331
-1
lines changed

2 files changed

+331
-1
lines changed

packages/parameters/src/DynamoDBProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class DynamoDBProvider extends BaseProvider {
5656
}
5757

5858
private async formatResult(result: QueryCommandOutput, items: Record<string, string | undefined>): Promise<Record<string, string | undefined>> {
59-
if (result.Items !== undefined) {
59+
if (result.Items !== undefined && result.Count !== 0) {
6060
result.Items.forEach(item => {
6161
const itemUnmarshalled = unmarshall(item);
6262
items[itemUnmarshalled[this.sortAttr]] = itemUnmarshalled[this.valueAttr];
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2+
import { DynamoDBProvider } from '../../src/DynamoDBProvider';
3+
import { marshall } from '@aws-sdk/util-dynamodb';
4+
import { toBase64 } from '@aws-sdk/util-base64-node';
5+
6+
const clientSpy = jest.spyOn(DynamoDBClient.prototype, 'send');
7+
8+
describe('Class: DynamoDBProvider', () => {
9+
10+
beforeEach(() => {
11+
jest.clearAllMocks();
12+
});
13+
14+
describe('Method: init', () => {
15+
16+
test('when initialized, it sets the tableName correctly', () => {
17+
18+
// Prepare
19+
const provider = new DynamoDBProvider('TestTable');
20+
21+
// Act / Assess
22+
expect(provider.tableName).toEqual('TestTable');
23+
24+
});
25+
26+
test('when initialized, and no attributes are passed, it uses the default values', () => {
27+
28+
// Prepare
29+
const provider = new DynamoDBProvider('TestTable');
30+
31+
// Act / Assess
32+
expect(provider.keyAttr).toEqual('id');
33+
expect(provider.sortAttr).toEqual('sk');
34+
expect(provider.valueAttr).toEqual('value');
35+
36+
});
37+
38+
test('when initialized, and ALL attributes are passed, it overwrites ALL the default values', () => {
39+
40+
// Prepare
41+
const provider = new DynamoDBProvider('TestTable', {
42+
keyAttr: 'foo',
43+
sortAttr: 'bar',
44+
valueAttr: 'baz'
45+
});
46+
47+
// Act / Assess
48+
expect(provider.keyAttr).toEqual('foo');
49+
expect(provider.sortAttr).toEqual('bar');
50+
expect(provider.valueAttr).toEqual('baz');
51+
52+
});
53+
54+
test('when initialized, and SOME attributes are passed, it PARTIALLY overwrites the correct default values', () => {
55+
56+
// Prepare
57+
const provider = new DynamoDBProvider('TestTable', {
58+
sortAttr: 'bar',
59+
});
60+
61+
// Act / Assess
62+
expect(provider.keyAttr).toEqual('id');
63+
expect(provider.sortAttr).toEqual('bar');
64+
expect(provider.valueAttr).toEqual('value');
65+
66+
});
67+
68+
});
69+
70+
describe('Method: get', () => {
71+
72+
test('when called, and the cache is empty, it returns a value from remote', async () => {
73+
74+
// Prepare
75+
const provider = new DynamoDBProvider('TestTable');
76+
clientSpy.mockImplementation(() => ({ 'Item': { ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'value': 'foo' }) } }));
77+
78+
// Act
79+
const value = await provider.get('my-parameter');
80+
81+
// Assess
82+
expect(value).toEqual('foo');
83+
expect(clientSpy).toBeCalledWith(expect.objectContaining({
84+
input: expect.objectContaining({
85+
TableName: 'TestTable',
86+
Key: {
87+
'id': {
88+
S: 'my-parameter'
89+
}
90+
}
91+
})
92+
}));
93+
94+
});
95+
96+
test('when called, and a non-expired value exists in the cache, it returns it', async () => {
97+
98+
// Prepare
99+
const provider = new DynamoDBProvider('TestTable');
100+
clientSpy.mockImplementation(() => ({ 'Item': { ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'value': 'foo' }) } }));
101+
const ttl = new Date();
102+
provider.store.set([ 'my-parameter', undefined ].toString(), { value: 'bar', ttl: ttl.setSeconds(ttl.getSeconds() + 600) });
103+
104+
// Act
105+
const value = await provider.get('my-parameter');
106+
107+
// Assess
108+
expect(value).toEqual('bar');
109+
expect(clientSpy).toBeCalledTimes(0);
110+
111+
});
112+
113+
test('when called, and an expired value exists in the cache, it returns a value from remote', async () => {
114+
115+
// Prepare
116+
const provider = new DynamoDBProvider('TestTable');
117+
clientSpy.mockImplementation(() => ({ 'Item': { ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'value': 'foo' }) } }));
118+
const ttl = new Date();
119+
provider.store.set([ 'my-parameter', undefined ].toString(), { value: 'bar', ttl: ttl.setSeconds(ttl.getSeconds() - 600) });
120+
121+
// Act
122+
const value = await provider.get('my-parameter');
123+
124+
// Assess
125+
expect(value).toEqual('foo');
126+
expect(clientSpy).toBeCalledTimes(1);
127+
128+
});
129+
130+
test('when called with custom sdkOptions, it uses them, and it returns a value from remote', async () => {
131+
132+
// Prepare
133+
const provider = new DynamoDBProvider('TestTable');
134+
clientSpy.mockImplementation(() => ({ 'Item': { ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'value': 'foo' }) } }));
135+
136+
// Act
137+
const value = await provider.get('my-parameter', { sdkOptions: { Limit: 1 } });
138+
139+
// Assess
140+
expect(value).toEqual('foo');
141+
expect(clientSpy).toBeCalledWith(expect.objectContaining({
142+
input: expect.objectContaining({
143+
Limit: 1
144+
})
145+
}));
146+
147+
});
148+
149+
test('when called with custom sdkOptions that should be overwritten, it use the correct ones, and it returns a value from remote', async () => {
150+
151+
// Prepare
152+
const provider = new DynamoDBProvider('TestTable');
153+
clientSpy.mockImplementation(() => ({ 'Item': { ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'value': 'foo' }) } }));
154+
155+
// Act
156+
const value = await provider.get('my-parameter', { sdkOptions: { TableName: 'THIS_SHOULD_BE_OVERWRITTEN', Limit: 1 } });
157+
158+
// Assess
159+
expect(value).toEqual('foo');
160+
expect(clientSpy).toBeCalledWith(expect.objectContaining({
161+
input: expect.objectContaining({
162+
TableName: 'TestTable',
163+
Limit: 1
164+
})
165+
}));
166+
167+
});
168+
169+
test('when called, and the parameter DOES NOT exist, it returns undefined', async () => {
170+
const provider = new DynamoDBProvider('TestTable');
171+
clientSpy.mockImplementation(() => ({}));
172+
173+
// Act
174+
const value = await provider.get('my-parameter');
175+
176+
// Assess
177+
expect(value).toBeUndefined();
178+
expect(clientSpy).toBeCalledTimes(1);
179+
});
180+
181+
});
182+
183+
describe('Method: getMultiple', () => {
184+
185+
test('when called, and the cache is empty, it returns a values from remote', async () => {
186+
187+
// Prepare
188+
const provider = new DynamoDBProvider('TestTable');
189+
clientSpy.mockImplementation(() => ({ 'Items': [
190+
{ ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'sk': 'param-a' }), ...marshall({ 'value': 'foo' }) },
191+
{ ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'sk': 'param-b' }), ...marshall({ 'value': 'bar' }) }
192+
] }));
193+
194+
// Act
195+
const value = await provider.getMultiple('my-path');
196+
197+
// Assess
198+
expect(value).toEqual({
199+
'param-a': 'foo',
200+
'param-b': 'bar'
201+
});
202+
expect(clientSpy).toBeCalledWith(expect.objectContaining({
203+
input: expect.objectContaining({
204+
TableName: 'TestTable',
205+
ExpressionAttributeNames: {
206+
'#keyAttr': 'id'
207+
},
208+
ExpressionAttributeValues: {
209+
':path': {
210+
S: 'my-path'
211+
}
212+
}
213+
})
214+
}));
215+
216+
});
217+
218+
test('when called with transform auto, it returns all the values transformed correctly ', async () => {
219+
220+
// Prepare
221+
const encoder = new TextEncoder();
222+
const mockData = JSON.stringify({ foo: 'bar' });
223+
const mockBinary = toBase64(encoder.encode('my-value'));
224+
const provider = new DynamoDBProvider('TestTable');
225+
clientSpy.mockImplementation(() => ({ 'Items': [
226+
{ ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'sk': 'param-a.json' }), ...marshall({ 'value': mockData }) },
227+
{ ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'sk': 'param-b.binary' }), ...marshall({ 'value': mockBinary }) }
228+
] }));
229+
230+
// Act
231+
const value = await provider.getMultiple('my-path', { transform: 'auto' });
232+
233+
// Assess
234+
expect(value).toEqual({
235+
'param-a.json': { foo: 'bar' },
236+
'param-b.binary': 'my-value'
237+
});
238+
239+
});
240+
241+
test('when called, and NOT all the parameters are retrieved in a single request, it keeps querying until it returns all the values', async () => {
242+
243+
// Prepare
244+
const provider = new DynamoDBProvider('TestTable');
245+
clientSpy
246+
.mockImplementationOnce(() => ({
247+
'Items': [
248+
{ ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'sk': 'param-a' }), ...marshall({ 'value': 'foo' }) },
249+
],
250+
'LastEvaluatedKey': { ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'sk': 'param-a' }), ...marshall({ 'value': 'foo' }) }
251+
}))
252+
.mockImplementationOnce(() => ({
253+
'Items': [
254+
{ ...marshall({ 'id': 'my-parameter' }), ...marshall({ 'sk': 'param-b' }), ...marshall({ 'value': 'bar' }) }
255+
]
256+
}));
257+
258+
// Act
259+
const value = await provider.getMultiple('my-path');
260+
261+
// Assess
262+
expect(value).toEqual({
263+
'param-a': 'foo',
264+
'param-b': 'bar'
265+
});
266+
expect(clientSpy).toBeCalledTimes(2);
267+
// First Call
268+
expect(clientSpy).toBeCalledWith(expect.objectContaining({
269+
input: expect.objectContaining({
270+
TableName: 'TestTable',
271+
ExpressionAttributeNames: {
272+
'#keyAttr': 'id'
273+
},
274+
ExpressionAttributeValues: {
275+
':path': {
276+
S: 'my-path'
277+
}
278+
}
279+
})
280+
}));
281+
// Second Call
282+
expect(clientSpy).toBeCalledWith(expect.objectContaining({
283+
input: expect.objectContaining({
284+
TableName: 'TestTable',
285+
ExpressionAttributeNames: {
286+
'#keyAttr': 'id'
287+
},
288+
ExpressionAttributeValues: {
289+
':path': {
290+
S: 'my-path'
291+
}
292+
},
293+
ExclusiveStartKey: {
294+
'id': {
295+
S: 'my-parameter'
296+
},
297+
'sk': {
298+
S: 'param-a'
299+
},
300+
'value': {
301+
S: 'foo'
302+
}
303+
}
304+
})
305+
}));
306+
307+
});
308+
309+
test('when called, and NO results are returned, it returns an empty object', async () => {
310+
311+
// Prepare
312+
const provider = new DynamoDBProvider('TestTable');
313+
clientSpy
314+
.mockImplementationOnce(() => ({
315+
'Items': [],
316+
'Count': 0
317+
}));
318+
319+
// Act
320+
const value = await provider.getMultiple('my-path');
321+
322+
// Assess
323+
expect(value).toEqual({});
324+
expect(clientSpy).toBeCalledTimes(1);
325+
326+
});
327+
328+
});
329+
330+
});

0 commit comments

Comments
 (0)