|
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