|
1 | 1 | import { describe, expect, it } from 'vitest';
|
2 |
| -import { ZodError } from 'zod'; |
| 2 | +import { ZodError, z } from 'zod'; |
3 | 3 | import { ApiGatewayEnvelope } from '../../../src/envelopes/index.js';
|
4 | 4 | import { ParseError } from '../../../src/errors.js';
|
| 5 | +import { JSONStringified } from '../../../src/helpers.js'; |
5 | 6 | import type { APIGatewayProxyEvent } from '../../../src/types/schema.js';
|
6 |
| -import { TestSchema, getTestEvent } from '../schema/utils.js'; |
7 |
| - |
8 |
| -describe('API Gateway REST Envelope', () => { |
9 |
| - const eventsPath = 'apigw-rest'; |
10 |
| - const eventPrototype = getTestEvent<APIGatewayProxyEvent>({ |
11 |
| - eventsPath, |
| 7 | +import { getTestEvent, omit } from '../schema/utils.js'; |
| 8 | + |
| 9 | +describe('Envelope: API Gateway REST', () => { |
| 10 | + const schema = z |
| 11 | + .object({ |
| 12 | + message: z.string(), |
| 13 | + }) |
| 14 | + .strict(); |
| 15 | + const baseEvent = getTestEvent<APIGatewayProxyEvent>({ |
| 16 | + eventsPath: 'apigw-rest', |
12 | 17 | filename: 'no-auth',
|
13 | 18 | });
|
14 | 19 |
|
15 | 20 | describe('Method: parse', () => {
|
16 |
| - it('should throw if the payload does not match the schema', () => { |
17 |
| - // Prepare |
18 |
| - const event = { ...eventPrototype }; |
19 |
| - event.body = JSON.stringify({ name: 'foo' }); |
20 |
| - |
21 |
| - // Act & Assess |
22 |
| - expect(() => ApiGatewayEnvelope.parse(event, TestSchema)).toThrow( |
23 |
| - ParseError |
24 |
| - ); |
25 |
| - }); |
26 |
| - |
27 |
| - it('should throw if the body is null', () => { |
| 21 | + it('throws if the payload does not match the schema', () => { |
28 | 22 | // Prepare
|
29 |
| - const event = { ...eventPrototype }; |
30 |
| - event.body = null; |
| 23 | + const event = structuredClone(baseEvent); |
31 | 24 |
|
32 | 25 | // Act & Assess
|
33 |
| - expect(() => ApiGatewayEnvelope.parse(event, TestSchema)).toThrow( |
34 |
| - ParseError |
| 26 | + expect(() => ApiGatewayEnvelope.parse(event, schema)).toThrow( |
| 27 | + expect.objectContaining({ |
| 28 | + message: expect.stringContaining('Failed to parse API Gateway body'), |
| 29 | + cause: expect.objectContaining({ |
| 30 | + issues: [ |
| 31 | + { |
| 32 | + code: 'invalid_type', |
| 33 | + expected: 'object', |
| 34 | + received: 'null', |
| 35 | + path: ['body'], |
| 36 | + message: 'Expected object, received null', |
| 37 | + }, |
| 38 | + ], |
| 39 | + }), |
| 40 | + }) |
35 | 41 | );
|
36 | 42 | });
|
37 | 43 |
|
38 |
| - it('should parse and return the inner schema in an envelope', () => { |
| 44 | + it('parses an API Gateway REST event with plain text', () => { |
39 | 45 | // Prepare
|
40 |
| - const event = { ...eventPrototype }; |
41 |
| - const payload = { name: 'foo', age: 42 }; |
42 |
| - event.body = JSON.stringify(payload); |
| 46 | + const event = structuredClone(baseEvent); |
| 47 | + event.body = 'hello world'; |
43 | 48 |
|
44 | 49 | // Act
|
45 |
| - const parsedEvent = ApiGatewayEnvelope.parse(event, TestSchema); |
| 50 | + const result = ApiGatewayEnvelope.parse(event, z.string()); |
46 | 51 |
|
47 | 52 | // Assess
|
48 |
| - expect(parsedEvent).toEqual(payload); |
| 53 | + expect(result).toEqual('hello world'); |
49 | 54 | });
|
50 |
| - }); |
51 | 55 |
|
52 |
| - describe('Method: safeParse', () => { |
53 |
| - it('should not throw if the payload does not match the schema', () => { |
| 56 | + it('parses an API Gateway REST event with JSON-stringified body', () => { |
54 | 57 | // Prepare
|
55 |
| - const event = { ...eventPrototype }; |
56 |
| - event.body = JSON.stringify({ name: 'foo' }); |
| 58 | + const event = structuredClone(baseEvent); |
| 59 | + event.body = JSON.stringify({ message: 'hello world' }); |
57 | 60 |
|
58 | 61 | // Act
|
59 |
| - const parseResult = ApiGatewayEnvelope.safeParse(event, TestSchema); |
| 62 | + const result = ApiGatewayEnvelope.parse(event, JSONStringified(schema)); |
60 | 63 |
|
61 | 64 | // Assess
|
62 |
| - expect(parseResult).toEqual({ |
63 |
| - success: false, |
64 |
| - error: expect.any(ParseError), |
65 |
| - originalEvent: event, |
66 |
| - }); |
67 |
| - |
68 |
| - if (!parseResult.success && parseResult.error) { |
69 |
| - expect(parseResult.error.cause).toBeInstanceOf(ZodError); |
70 |
| - } |
| 65 | + expect(result).toStrictEqual({ message: 'hello world' }); |
71 | 66 | });
|
72 | 67 |
|
73 |
| - it('should not throw if the body is null', () => { |
| 68 | + it('parses an API Gateway REST event with binary body', () => { |
74 | 69 | // Prepare
|
75 |
| - const event = { ...eventPrototype }; |
76 |
| - event.body = null; |
| 70 | + const event = structuredClone(baseEvent); |
| 71 | + event.body = 'aGVsbG8gd29ybGQ='; // base64 encoded 'hello world' |
| 72 | + // @ts-expect-error - we know the headers exist |
| 73 | + event.headers['content-type'] = 'application/octet-stream'; |
| 74 | + event.isBase64Encoded = true; |
77 | 75 |
|
78 | 76 | // Act
|
79 |
| - const parseResult = ApiGatewayEnvelope.safeParse(event, TestSchema); |
| 77 | + const result = ApiGatewayEnvelope.parse(event, z.string()); |
80 | 78 |
|
81 | 79 | // Assess
|
82 |
| - expect(parseResult).toEqual({ |
83 |
| - success: false, |
84 |
| - error: expect.any(ParseError), |
85 |
| - originalEvent: event, |
86 |
| - }); |
87 |
| - |
88 |
| - if (!parseResult.success && parseResult.error) { |
89 |
| - expect(parseResult.error.cause).toBeInstanceOf(ZodError); |
90 |
| - } |
| 80 | + expect(result).toEqual('aGVsbG8gd29ybGQ='); |
91 | 81 | });
|
| 82 | + }); |
92 | 83 |
|
93 |
| - it('should not throw if the event is invalid', () => { |
| 84 | + describe('Method: safeParse', () => { |
| 85 | + it('parses an API Gateway REST event', () => { |
94 | 86 | // Prepare
|
95 |
| - const event = getTestEvent({ eventsPath, filename: 'invalid' }); |
| 87 | + const event = structuredClone(baseEvent); |
| 88 | + event.body = JSON.stringify({ message: 'hello world' }); |
96 | 89 |
|
97 | 90 | // Act
|
98 |
| - const parseResult = ApiGatewayEnvelope.safeParse(event, TestSchema); |
| 91 | + const result = ApiGatewayEnvelope.safeParse( |
| 92 | + event, |
| 93 | + JSONStringified(schema) |
| 94 | + ); |
99 | 95 |
|
100 | 96 | // Assess
|
101 |
| - expect(parseResult).toEqual({ |
102 |
| - success: false, |
103 |
| - error: expect.any(ParseError), |
104 |
| - originalEvent: event, |
| 97 | + expect(result).toEqual({ |
| 98 | + success: true, |
| 99 | + data: { message: 'hello world' }, |
105 | 100 | });
|
106 | 101 | });
|
107 | 102 |
|
108 |
| - it('should parse and return the inner schema in an envelope', () => { |
| 103 | + it('returns an error if the event is not a valid API Gateway REST event', () => { |
109 | 104 | // Prepare
|
110 |
| - const event = { ...eventPrototype }; |
111 |
| - const payload = { name: 'foo', age: 42 }; |
112 |
| - event.body = JSON.stringify(payload); |
| 105 | + const event = omit(['path'], structuredClone(baseEvent)); |
113 | 106 |
|
114 | 107 | // Act
|
115 |
| - const parsedEvent = ApiGatewayEnvelope.safeParse(event, TestSchema); |
| 108 | + const result = ApiGatewayEnvelope.safeParse(event, schema); |
116 | 109 |
|
117 | 110 | // Assess
|
118 |
| - expect(parsedEvent).toEqual({ |
119 |
| - success: true, |
120 |
| - data: payload, |
| 111 | + expect(result).toEqual({ |
| 112 | + success: false, |
| 113 | + error: new ParseError('Failed to parse API Gateway body', { |
| 114 | + cause: new ZodError([ |
| 115 | + { |
| 116 | + code: 'invalid_type', |
| 117 | + expected: 'string', |
| 118 | + received: 'undefined', |
| 119 | + path: ['path'], |
| 120 | + message: 'Required', |
| 121 | + }, |
| 122 | + { |
| 123 | + code: 'invalid_type', |
| 124 | + expected: 'object', |
| 125 | + received: 'null', |
| 126 | + path: ['body'], |
| 127 | + message: 'Expected object, received null', |
| 128 | + }, |
| 129 | + ]), |
| 130 | + }), |
| 131 | + originalEvent: event, |
121 | 132 | });
|
122 | 133 | });
|
123 | 134 | });
|
|
0 commit comments