diff --git a/packages/superagent-wrapper/src/request.ts b/packages/superagent-wrapper/src/request.ts index d4147412..41d9dd5e 100644 --- a/packages/superagent-wrapper/src/request.ts +++ b/packages/superagent-wrapper/src/request.ts @@ -138,7 +138,8 @@ const patchRequest = < }); } return pipe( - route.response[status].decode(res.body), + // deep copy to prevent modification of the inputs by sketchy codecs + route.response[status].decode(structuredClone(res.body)), E.map((body) => decodedResponse({ status, @@ -194,7 +195,8 @@ export const requestForRoute = route: Route, ): BoundRequestFactory => (params: h.RequestType): PatchedRequest => { - const reqProps = route.request.encode(params); + // deep copy to prevent modification of the inputs by sketchy codecs + const reqProps = route.request.encode(structuredClone(params)); let path = route.path; for (const key in reqProps.params) { diff --git a/packages/superagent-wrapper/test/request.test.ts b/packages/superagent-wrapper/test/request.test.ts index eee3e5ef..9f64ab3f 100644 --- a/packages/superagent-wrapper/test/request.test.ts +++ b/packages/superagent-wrapper/test/request.test.ts @@ -45,6 +45,37 @@ const PostTestRoute = h.httpRoute({ }, }); +const PostOptionalTestRoute = h.httpRoute({ + path: '/test/optional/{id}', + method: 'POST', + request: h.httpRequest({ + query: { + foo: t.string, + }, + params: { + id: NumberFromString, + }, + body: { + bar: t.number, + optional: h.optionalized({ + baz: t.boolean, + qux: h.optional(t.string), + }), + }, + }), + response: { + 200: t.type({ + id: t.number, + foo: t.string, + bar: t.number, + baz: t.boolean, + }), + 401: t.type({ + message: t.string, + }), + }, +}); + const HeaderGetTestRoute = h.httpRoute({ path: '/getHeader', method: 'GET', @@ -60,6 +91,9 @@ const TestRoutes = h.apiSpec({ 'api.v1.test': { post: PostTestRoute, }, + 'api.v1.test.optional': { + post: PostOptionalTestRoute, + }, 'api.v1.getheader': { get: HeaderGetTestRoute, }, @@ -105,6 +139,23 @@ const createTestServer = (port: number) => { } }); + testApp.post('/test/optional/:id', (req, res) => { + const filteredReq = { + query: req.query, + params: req.params, + headers: req.headers, + body: req.body, + }; + const params = E.getOrElseW((err) => { + throw new Error(JSON.stringify(err)); + })(PostTestRoute.request.decode(filteredReq)); + const response = PostTestRoute.response[200].encode({ + ...params, + baz: true, + }); + res.send(response); + }); + testApp.get(HeaderGetTestRoute.path, (req, res) => { res.send( HeaderGetTestRoute.response[200].encode({ @@ -242,6 +293,22 @@ describe('decodeExpecting', () => { 'Could not decode response 200: [{"invalid":"response"}] due to error [Invalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/id: number\nInvalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/foo: string\nInvalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/bar: number\nInvalid value undefined supplied to : { id: number, foo: string, bar: number, baz: boolean }/baz: boolean]', ); }); + + it('does not modify inputs by dropping keys with undefined values', async () => { + const body = { + id: 1337, + foo: 'test', + bar: 42, + optional: { baz: true, qux: undefined }, + }; + await apiClient['api.v1.test.optional'].post(body).decodeExpecting(200); + assert.deepEqual(body, { + id: 1337, + foo: 'test', + bar: 42, + optional: { baz: true, qux: undefined }, + }); + }); }); describe('superagent', async () => {