Skip to content

Commit 470be4b

Browse files
committed
test(jmespath): full test coverage
1 parent c72a4f7 commit 470be4b

File tree

1 file changed

+388
-0
lines changed

1 file changed

+388
-0
lines changed
Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
/**
2+
* Test Compliance with the JMESPath specification
3+
*
4+
* @group unit/jmespath/coverage
5+
*/
6+
import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64';
7+
import {
8+
search,
9+
EmptyExpressionError,
10+
ArityError,
11+
LexerError,
12+
JMESPathError,
13+
VariadicArityError,
14+
} from '../../src';
15+
import { Functions } from '../../src/Functions.js';
16+
import { Parser } from '../../src/Parser.js';
17+
import { TreeInterpreter } from '../../src/TreeInterpreter.js';
18+
import { brotliDecompressSync } from 'node:zlib';
19+
import { PowertoolsFunctions } from '../../src/PowertoolsFunctions.js';
20+
import { extractDataFromEnvelope, SQS } from '../../src/envelopes.js';
21+
22+
describe('Coverage tests', () => {
23+
// These expressions tests are not part of the compliance suite, but are added to ensure coverage
24+
describe('expressions', () => {
25+
it('throws an error if the index is an invalid value', () => {
26+
// Prepare
27+
const invalidIndexExpression = 'foo.*.notbaz[-a]';
28+
29+
// Act & Assess
30+
expect(() => search(invalidIndexExpression, {})).toThrow(LexerError);
31+
});
32+
33+
it('throws an error if the expression is not a string', () => {
34+
// Prepare
35+
const notAStringExpression = 3;
36+
37+
// Act & Assess
38+
expect(() =>
39+
search(notAStringExpression as unknown as string, {})
40+
).toThrow(EmptyExpressionError);
41+
});
42+
43+
it('throws a lexer error when encounteirng a single equal for equality', () => {
44+
// Prepare
45+
const expression = '=';
46+
47+
// Act & Assess
48+
expect(() => {
49+
search(expression, {});
50+
}).toThrow(LexerError);
51+
});
52+
53+
it('returns null when max_by is called with an empty list', () => {
54+
// Prepare
55+
const expression = 'max_by(@, &foo)';
56+
57+
// Act
58+
const result = search(expression, []);
59+
60+
// Assess
61+
expect(result).toBe(null);
62+
});
63+
64+
it('returns null when min_by is called with an empty list', () => {
65+
// Prepare
66+
const expression = 'min_by(@, &foo)';
67+
68+
// Act
69+
const result = search(expression, []);
70+
71+
// Assess
72+
expect(result).toBe(null);
73+
});
74+
75+
it('returns the correct max value', () => {
76+
// Prepare
77+
const expression = 'max(@)';
78+
79+
// Act
80+
const result = search(expression, ['z', 'b']);
81+
82+
// Assess
83+
expect(result).toBe('z');
84+
});
85+
86+
it('returns the correct min value', () => {
87+
// Prepare
88+
const expression = 'min(@)';
89+
90+
// Act
91+
const result = search(expression, ['z', 'b']);
92+
93+
// Assess
94+
expect(result).toBe('b');
95+
});
96+
});
97+
98+
describe('type checking', () => {
99+
class TestFunctions extends Functions {
100+
@TestFunctions.signature({
101+
argumentsSpecs: [['any'], ['any']],
102+
})
103+
public funcTest(): void {
104+
return;
105+
}
106+
107+
@TestFunctions.signature({
108+
argumentsSpecs: [['any'], ['any']],
109+
variadic: true,
110+
})
111+
public funcTestArityError(): void {
112+
return;
113+
}
114+
}
115+
116+
it('throws an arity error if the function is called with the wrong number of arguments', () => {
117+
// Prepare
118+
const expression = 'test(@, @, @)';
119+
120+
// Act & Assess
121+
expect(() =>
122+
search(expression, {}, { customFunctions: new TestFunctions() })
123+
).toThrow(ArityError);
124+
});
125+
126+
it('throws an arity error if the function is called with the wrong number of arguments', () => {
127+
// Prepare
128+
const expression = 'test_arity_error(@)';
129+
130+
// Act & Assess
131+
expect(() =>
132+
search(expression, {}, { customFunctions: new TestFunctions() })
133+
).toThrow(VariadicArityError);
134+
});
135+
});
136+
137+
describe('class: Parser', () => {
138+
it('clears the cache when purgeCache is called', () => {
139+
// Prepare
140+
const parser = new Parser();
141+
142+
// Act
143+
const parsedResultA = parser.parse('test(@, @)');
144+
parser.purgeCache();
145+
const parsedResultB = parser.parse('test(@, @)');
146+
147+
// Assess
148+
expect(parsedResultA).not.toBe(parsedResultB);
149+
});
150+
});
151+
152+
describe('class: TreeInterpreter', () => {
153+
it('throws an error when visiting an invalid node', () => {
154+
// Prepare
155+
const interpreter = new TreeInterpreter();
156+
157+
// Act & Assess
158+
expect(() => {
159+
interpreter.visit(
160+
{
161+
type: 'invalid',
162+
value: 'invalid',
163+
children: [],
164+
},
165+
{}
166+
);
167+
}).toThrow(JMESPathError);
168+
});
169+
170+
it('returns null when visiting a field with no value', () => {
171+
// Prepare
172+
const interpreter = new TreeInterpreter();
173+
174+
// Act
175+
const result = interpreter.visit(
176+
{
177+
type: 'field',
178+
value: undefined,
179+
children: [],
180+
},
181+
{}
182+
);
183+
184+
// Assess
185+
expect(result).toBe(null);
186+
});
187+
188+
it('throws an error when receiving an invalid comparator', () => {
189+
// Prepare
190+
const interpreter = new TreeInterpreter();
191+
192+
// Act & Assess
193+
expect(() => {
194+
interpreter.visit(
195+
{
196+
type: 'comparator',
197+
value: 'invalid',
198+
children: [
199+
{
200+
type: 'field',
201+
value: 'a',
202+
children: [],
203+
},
204+
{
205+
type: 'field',
206+
value: 'b',
207+
children: [],
208+
},
209+
],
210+
},
211+
{}
212+
);
213+
}).toThrow(JMESPathError);
214+
});
215+
216+
it('throws an error when receiving a function with an invalid name', () => {
217+
// Prepare
218+
const interpreter = new TreeInterpreter();
219+
220+
// Act & Assess
221+
expect(() => {
222+
interpreter.visit(
223+
{
224+
type: 'function_expression',
225+
value: 1, // function name must be a string
226+
children: [],
227+
},
228+
{}
229+
);
230+
}).toThrow(JMESPathError);
231+
});
232+
233+
it('throws an error when receiving an index expression with an invalid index', () => {
234+
// Prepare
235+
const interpreter = new TreeInterpreter();
236+
237+
// Act & Assess
238+
expect(() => {
239+
interpreter.visit(
240+
{
241+
type: 'index',
242+
value: 'invalid', // index must be a number
243+
children: [],
244+
},
245+
[]
246+
);
247+
}).toThrow(JMESPathError);
248+
});
249+
250+
it('returns an empty array when slicing an empty array', () => {
251+
// Prepare
252+
const interpreter = new TreeInterpreter();
253+
254+
// Act
255+
const result = interpreter.visit(
256+
{
257+
type: 'slice',
258+
value: [0, 0, 1],
259+
children: [],
260+
},
261+
[]
262+
);
263+
264+
// Assess
265+
expect(result).toEqual([]);
266+
});
267+
});
268+
269+
describe('function: extractDataFromEnvelope', () => {
270+
it('extracts the data from a known envelope', () => {
271+
// Prepare
272+
const event = {
273+
Records: [
274+
{
275+
body: '{"foo":"bar"}',
276+
},
277+
],
278+
};
279+
280+
// Act
281+
const data = extractDataFromEnvelope(event, SQS);
282+
283+
// Assess
284+
expect(data).toStrictEqual([{ foo: 'bar' }]);
285+
});
286+
});
287+
288+
describe('class: PowertoolsFunctions', () => {
289+
it('decodes a json string', () => {
290+
// Prepare
291+
const event = '{"user":"xyz","product_id":"123456789"}';
292+
293+
// Act
294+
const data = extractDataFromEnvelope(event, 'powertools_json(@)', {
295+
customFunctions: new PowertoolsFunctions(),
296+
});
297+
298+
// Assess
299+
expect(data).toStrictEqual({
300+
user: 'xyz',
301+
product_id: '123456789',
302+
});
303+
});
304+
305+
it('decodes a base64 gzip string', () => {
306+
// Prepare
307+
const event = {
308+
payload:
309+
'H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==',
310+
};
311+
312+
// Act
313+
const data = extractDataFromEnvelope(
314+
event,
315+
'powertools_base64_gzip(payload) | powertools_json(@).logGroup',
316+
{
317+
customFunctions: new PowertoolsFunctions(),
318+
}
319+
);
320+
321+
// Assess
322+
expect(data).toStrictEqual('/aws/lambda/powertools-example');
323+
});
324+
325+
it('decodes a base64 string', () => {
326+
// Prepare
327+
const event = {
328+
payload:
329+
'eyJ1c2VyX2lkIjogMTIzLCAicHJvZHVjdF9pZCI6IDEsICJxdWFudGl0eSI6IDIsICJwcmljZSI6IDEwLjQwLCAiY3VycmVuY3kiOiAiVVNEIn0=',
330+
};
331+
332+
// Act
333+
const data = extractDataFromEnvelope(
334+
event,
335+
'powertools_json(powertools_base64(payload))',
336+
{
337+
customFunctions: new PowertoolsFunctions(),
338+
}
339+
);
340+
341+
// Assess
342+
expect(data).toStrictEqual({
343+
user_id: 123,
344+
product_id: 1,
345+
quantity: 2,
346+
price: 10.4,
347+
currency: 'USD',
348+
});
349+
});
350+
351+
it('uses the custom function extending the powertools custom functions', () => {
352+
// Prepare
353+
class CustomFunctions extends PowertoolsFunctions {
354+
public constructor() {
355+
super();
356+
}
357+
@PowertoolsFunctions.signature({
358+
argumentsSpecs: [['string']],
359+
})
360+
public funcDecodeBrotliCompression(value: string): string {
361+
const encoded = fromBase64(value, 'base64');
362+
const uncompressed = brotliDecompressSync(encoded);
363+
364+
return uncompressed.toString();
365+
}
366+
}
367+
const event = {
368+
Records: [
369+
{
370+
application: 'messaging-app',
371+
datetime: '2022-01-01T00:00:00.000Z',
372+
notification: 'GyYA+AXhZKk/K5DkanoQSTYpSKMwwxXh8DRWVo9A1hLqAQ==',
373+
},
374+
],
375+
};
376+
377+
// Act
378+
const messages = extractDataFromEnvelope<string>(
379+
event,
380+
'Records[*].decode_brotli_compression(notification) | [*].powertools_json(@).message',
381+
{ customFunctions: new CustomFunctions() }
382+
);
383+
384+
// Assess
385+
expect(messages).toStrictEqual(['hello world']);
386+
});
387+
});
388+
});

0 commit comments

Comments
 (0)