From daa16e66d14874d16e80df244041f258c62b6694 Mon Sep 17 00:00:00 2001 From: Nick Caballero Date: Wed, 10 May 2023 14:17:02 -0400 Subject: [PATCH 1/3] fix: fix request body type when optional --- src/index.test.ts | 55 +++++++++++++++++++++++++++++++++++++++++------ src/index.ts | 9 +++++--- test/v1.d.ts | 31 ++++++++++++++++++++++++++ test/v1.yaml | 42 +++++++++++++++++++++++++++++++++++- 4 files changed, 126 insertions(+), 11 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index e5ab94d..6d18fff 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -285,17 +285,21 @@ describe('get()', () => { }); describe('post()', () => { + function mockResponse(code: number, body?: T) { + const mock = fetchMocker.mockResponseOnce(() => ({ status: code, body: body !== undefined ? JSON.stringify(body) : '' })); + return {body, mock}; + } + it('sends the correct method', async () => { const client = createClient(); - fetchMocker.mockResponseOnce(() => ({ status: 200, body: '{}' })); + mockResponse(200, {}); await client.post('/anyMethod', {}); expect(fetchMocker.mock.calls[0][1]?.method).toBe('POST'); }); it('sends correct options, returns success', async () => { - const mockData = { status: 'success' }; const client = createClient(); - fetchMocker.mockResponseOnce(() => ({ status: 201, body: JSON.stringify(mockData) })); + const { body: mockData } = mockResponse(201, {status: 'success'}); const { data, error, response } = await client.put('/post', { params: {}, body: { @@ -316,10 +320,9 @@ describe('post()', () => { expect(error).toBe(undefined); }); - it('supports sepecifying utf-8 encoding', async () => { - const mockData = { message: 'My reply' }; + it('supports specifying utf-8 encoding', async () => { const client = createClient(); - fetchMocker.mockResponseOnce(() => ({ status: 201, body: JSON.stringify(mockData) })); + const { body: mockData } = mockResponse(201, {status: 'My reply'}); const { data, error, response } = await client.put('/comment', { params: {}, body: { @@ -338,7 +341,7 @@ describe('post()', () => { it('returns empty object on 204', async () => { const client = createClient(); - fetchMocker.mockResponseOnce(() => ({ status: 204, body: '' })); + const { body: mockData } = mockResponse(204); const { data, error, response } = await client.put('/tag/{name}', { params: { path: { name: 'New Tag' } }, body: { description: 'This is a new tag' }, @@ -351,6 +354,44 @@ describe('post()', () => { // assert error is empty expect(error).toBe(undefined); }); + + it('request body type when optional', async() => { + const client = createClient(); + const { body: mockData } = mockResponse(201, {status: 'success'}); + const { data, error, response } = await client.post('/post/optional', { + body: { + title: '', + publish_date: 3, + body: '' + } + }) + + // assert correct data was returned + expect(data).toEqual(mockData); + expect(response.status).toBe(201); + + // assert error is empty + expect(error).toBe(undefined); + }) + + it('request body type when optional inline', async() => { + const client = createClient(); + const { body: mockData } = mockResponse(201, {status: 'success'}); + const { data, error, response } = await client.post('/post/optional/inline', { + body: { + title: '', + publish_date: 3, + body: '' + } + }) + + // assert correct data was returned + expect(data).toEqual(mockData); + expect(response.status).toBe(201); + + // assert error is empty + expect(error).toBe(undefined); + }) }); describe('delete()', () => { diff --git a/src/index.ts b/src/index.ts index b8e6582..6373e92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,16 +34,19 @@ export type FilterKeys = { [K in keyof Obj]: K extends Matchers ? /** handle "application/json", "application/vnd.api+json", "appliacation/json;charset=utf-8" and more */ export type JSONLike = `${string}json${string}`; -// fetch types +// general purpose types export type Params = O extends { parameters: any } ? { params: NonNullable } : BaseParams; -export type RequestBodyObj = O extends { requestBody: any } ? O['requestBody'] : never; +export type RequestBodyObj = O extends { requestBody?: any } ? O['requestBody'] : never; export type RequestBodyContent = undefined extends RequestBodyObj ? FilterKeys>, 'content'> | undefined : FilterKeys, 'content'>; export type RequestBodyJSON = FilterKeys, JSONLike> extends never ? FilterKeys>, JSONLike> | undefined : FilterKeys, JSONLike>; export type RequestBody = undefined extends RequestBodyJSON ? { body?: RequestBodyJSON } : { body: RequestBodyJSON }; export type QuerySerializer = (query: O extends { parameters: { query: any } } ? O['parameters']['query'] : Record) => string; -export type FetchOptions = Params & RequestBody & Omit & { querySerializer?: QuerySerializer }; +export type RequestOptions = Params & RequestBody & { querySerializer?: QuerySerializer }; export type Success = FilterKeys, 'content'>; export type Error = FilterKeys, 'content'>; + +// fetch types +export type FetchOptions = RequestOptions & Omit; export type FetchResponse = | { data: T extends { responses: any } ? NonNullable, JSONLike>> : unknown; error?: never; response: Response } | { data?: never; error: T extends { responses: any } ? NonNullable, JSONLike>> : unknown; response: Response }; diff --git a/test/v1.d.ts b/test/v1.d.ts index 340ee5e..d5a455f 100644 --- a/test/v1.d.ts +++ b/test/v1.d.ts @@ -23,6 +23,28 @@ export interface paths { }; }; }; + "/post/optional": { + post: { + requestBody: components["requestBodies"]["CreatePostOptional"]; + responses: { + 201: components["responses"]["CreatePost"]; + 500: components["responses"]["Error"]; + }; + }; + }; + "/post/optional/inline": { + post: { + requestBody?: { + content: { + "application/json": components["schemas"]["Post"]; + }; + }; + responses: { + 201: components["responses"]["CreatePost"]; + 500: components["responses"]["Error"]; + }; + }; + }; "/posts": { get: { responses: { @@ -271,6 +293,15 @@ export interface components { }; }; }; + CreatePostOptional?: { + content: { + "application/json": { + title: string; + body: string; + publish_date: number; + }; + }; + }; CreateTag?: { content: { "application/json": { diff --git a/test/v1.yaml b/test/v1.yaml index ec8bd58..d1c8277 100644 --- a/test/v1.yaml +++ b/test/v1.yaml @@ -1,4 +1,6 @@ -openapi: +openapi: 3.0.3 +info: + title: Test Specification version: '3.1' paths: /comment: @@ -19,6 +21,27 @@ paths: $ref: '#/components/responses/CreatePost' 500: $ref: '#/components/responses/Error' + /post/optional: + post: + requestBody: + $ref: '#/components/requestBodies/CreatePostOptional' + responses: + 201: + $ref: '#/components/responses/CreatePost' + 500: + $ref: '#/components/responses/Error' + /post/optional/inline: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Post' + responses: + 201: + $ref: '#/components/responses/CreatePost' + 500: + $ref: '#/components/responses/Error' /posts: get: responses: @@ -214,6 +237,23 @@ components: - title - body - publish_date + CreatePostOptional: + required: false + content: + application/json: + schema: + type: object + properties: + title: + type: string + body: + type: string + publish_date: + type: number + required: + - title + - body + - publish_date CreateTag: content: application/json: From 44f39abd85a592d2dd2a95542fe53eeab8547714 Mon Sep 17 00:00:00 2001 From: Nick Caballero Date: Wed, 10 May 2023 15:21:21 -0400 Subject: [PATCH 2/3] fix: cleanup tests --- src/index.test.ts | 49 ++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index 6d18fff..7124a64 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -285,21 +285,17 @@ describe('get()', () => { }); describe('post()', () => { - function mockResponse(code: number, body?: T) { - const mock = fetchMocker.mockResponseOnce(() => ({ status: code, body: body !== undefined ? JSON.stringify(body) : '' })); - return {body, mock}; - } - it('sends the correct method', async () => { const client = createClient(); - mockResponse(200, {}); + fetchMocker.mockResponseOnce(() => ({ status: 200, body: '{}' })); await client.post('/anyMethod', {}); expect(fetchMocker.mock.calls[0][1]?.method).toBe('POST'); }); it('sends correct options, returns success', async () => { + const mockData = { status: 'success' }; const client = createClient(); - const { body: mockData } = mockResponse(201, {status: 'success'}); + fetchMocker.mockResponseOnce(() => ({ status: 201, body: JSON.stringify(mockData) })); const { data, error, response } = await client.put('/post', { params: {}, body: { @@ -320,9 +316,10 @@ describe('post()', () => { expect(error).toBe(undefined); }); - it('supports specifying utf-8 encoding', async () => { + it('supports sepecifying utf-8 encoding', async () => { + const mockData = { message: 'My reply' }; const client = createClient(); - const { body: mockData } = mockResponse(201, {status: 'My reply'}); + fetchMocker.mockResponseOnce(() => ({ status: 201, body: JSON.stringify(mockData) })); const { data, error, response } = await client.put('/comment', { params: {}, body: { @@ -341,7 +338,7 @@ describe('post()', () => { it('returns empty object on 204', async () => { const client = createClient(); - const { body: mockData } = mockResponse(204); + fetchMocker.mockResponseOnce(() => ({ status: 204, body: '' })); const { data, error, response } = await client.put('/tag/{name}', { params: { path: { name: 'New Tag' } }, body: { description: 'This is a new tag' }, @@ -357,40 +354,36 @@ describe('post()', () => { it('request body type when optional', async() => { const client = createClient(); - const { body: mockData } = mockResponse(201, {status: 'success'}); - const { data, error, response } = await client.post('/post/optional', { + + // expect error on wrong body type + // @ts-expect-error + await client.post('/post/optional', { body: { error: true } }) + + // (no error) + await client.post('/post/optional', { body: { title: '', publish_date: 3, body: '' } }) - - // assert correct data was returned - expect(data).toEqual(mockData); - expect(response.status).toBe(201); - - // assert error is empty - expect(error).toBe(undefined); }) it('request body type when optional inline', async() => { const client = createClient(); - const { body: mockData } = mockResponse(201, {status: 'success'}); - const { data, error, response } = await client.post('/post/optional/inline', { + + // expect error on wrong body type + // @ts-expect-error + await client.post('/post/optional/inline', { body: { error: true } }) + + // (no error) + await client.post('/post/optional/inline', { body: { title: '', publish_date: 3, body: '' } }) - - // assert correct data was returned - expect(data).toEqual(mockData); - expect(response.status).toBe(201); - - // assert error is empty - expect(error).toBe(undefined); }) }); From b5f4b265aed5840b59615bbdbad61446cf3da624 Mon Sep 17 00:00:00 2001 From: Nick Caballero Date: Wed, 10 May 2023 15:24:21 -0400 Subject: [PATCH 3/3] fix: missing mock responses --- src/index.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 7124a64..c896413 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -353,6 +353,7 @@ describe('post()', () => { }); it('request body type when optional', async() => { + fetchMocker.mockResponse(() => ({ status: 201, body: '{}' })); const client = createClient(); // expect error on wrong body type @@ -370,6 +371,7 @@ describe('post()', () => { }) it('request body type when optional inline', async() => { + fetchMocker.mockResponse(() => ({ status: 201, body: '{}' })); const client = createClient(); // expect error on wrong body type