Skip to content

Commit

Permalink
Synchronize http client with the template (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
kibertoad authored Nov 28, 2022
1 parent e1e32fe commit 495af2f
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 36 deletions.
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {
sendPut,
sendPutBinary,
sendDelete,
sendPatch,
sendGet,
Expand All @@ -11,4 +12,5 @@ export type {
DeleteRequestOptions,
RequestOptions,
Response,
HttpRequestContext,
} from './src/http/httpClient'
7 changes: 1 addition & 6 deletions jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@
"testMatch": ["<rootDir>/src/**/*.(spec|test).ts", "<rootDir>/test/**/*.(spec|test).ts"],

"transform": {
"^.+\\.ts$": "ts-jest"
"^.+\\.ts$": ["ts-jest", { "diagnostics": false }]
},
"testEnvironment": "node",
"reporters": ["default"],
"globals": {
"ts-jest": {
"diagnostics": false
}
},
"collectCoverageFrom": ["./src/**/*.ts"],
"coveragePathIgnorePatterns": ["<rootDir>/node_modules/", "<rootDir>/coverage/"],
"coverageThreshold": {
Expand Down
22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"undici": "^5.10.0"
"undici": "^5.13.0"
},
"devDependencies": {
"@types/jest": "^28.1.8",
"@types/node": "^18.7.13",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"auto-changelog": "^2.4.0",
"eslint": "^8.23.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^26.8.7",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^28.1.3",
"prettier": "^2.7.1",
"ts-jest": "^28.0.8",
"typescript": "4.8.2"
"jest": "^29.3.1",
"prettier": "^2.8.0",
"ts-jest": "^29.0.3",
"typescript": "4.9.3"
}
}
80 changes: 71 additions & 9 deletions src/http/httpClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { MockAgent, setGlobalDispatcher } from 'undici'

import { sendDelete, sendGet, sendPatch, sendPost, sendPut } from './httpClient'
import { sendDelete, sendGet, sendPatch, sendPost, sendPut, sendPutBinary } from './httpClient'
import mockProduct1 from './mock-data/mockProduct1.json'
import mockProductsLimit3 from './mock-data/mockProductsLimit3.json'

const JSON_HEADERS = {
'content-type': 'application/json',
}

const TEXT_HEADERS = {
'content-type': 'text/plain',
}

describe('httpClient', () => {
let mockAgent: MockAgent
beforeEach(() => {
Expand Down Expand Up @@ -39,9 +43,7 @@ describe('httpClient', () => {
method: 'GET',
})
.reply(200, 'just text', {
headers: {
'content-type': 'text/plain',
},
headers: TEXT_HEADERS,
})

const result = await sendGet('https://fakestoreapi.com', 'products/1')
Expand Down Expand Up @@ -79,7 +81,7 @@ describe('httpClient', () => {
path: '/products/1',
method: 'DELETE',
})
.reply(204)
.reply(204, undefined, { headers: TEXT_HEADERS })

const result = await sendDelete('https://fakestoreapi.com', 'products/1')

Expand All @@ -100,7 +102,7 @@ describe('httpClient', () => {
method: 'DELETE',
query,
})
.reply(204)
.reply(204, undefined, { headers: TEXT_HEADERS })

const result = await sendDelete('https://fakestoreapi.com', 'products', {
query,
Expand Down Expand Up @@ -135,7 +137,7 @@ describe('httpClient', () => {
})
.reply(200, { id: 21 }, { headers: JSON_HEADERS })

const result = await sendPost('https://fakestoreapi.com', 'products')
const result = await sendPost('https://fakestoreapi.com', 'products', undefined)

expect(result.body).toEqual({ id: 21 })
})
Expand Down Expand Up @@ -200,7 +202,7 @@ describe('httpClient', () => {
})
.reply(200, { id: 21 }, { headers: JSON_HEADERS })

const result = await sendPut('https://fakestoreapi.com', 'products/1')
const result = await sendPut('https://fakestoreapi.com', 'products/1', undefined)

expect(result.body).toEqual({ id: 21 })
})
Expand Down Expand Up @@ -241,6 +243,66 @@ describe('httpClient', () => {
})
})

describe('PUT binary', () => {
it('PUT without queryParams', async () => {
const client = mockAgent.get('https://fakestoreapi.com')
client
.intercept({
path: '/products/1',
method: 'PUT',
})
.reply(200, { id: 21 }, { headers: JSON_HEADERS })

const result = await sendPutBinary(
'https://fakestoreapi.com',
'products/1',
Buffer.from('text'),
)

expect(result.body).toEqual({ id: 21 })
})

it('PUT with queryParams', async () => {
const client = mockAgent.get('https://fakestoreapi.com')
const query = {
limit: 3,
}

client
.intercept({
path: '/products/1',
method: 'PUT',
query,
})
.reply(200, { id: 21 }, { headers: JSON_HEADERS })

const result = await sendPutBinary(
'https://fakestoreapi.com',
'products/1',
Buffer.from('text'),
{
query,
},
)

expect(result.body).toEqual({ id: 21 })
})

it('PUT that returns 400 throws an error', async () => {
const client = mockAgent.get('https://fakestoreapi.com')
client
.intercept({
path: '/products/1',
method: 'PUT',
})
.reply(400, { errorCode: 'err' }, { headers: JSON_HEADERS })

await expect(
sendPutBinary('https://fakestoreapi.com', 'products/1', Buffer.from('text')),
).rejects.toThrow('Response status code 400: Bad Request')
})
})

describe('PATCH', () => {
it('PATCH without queryParams', async () => {
const client = mockAgent.get('https://fakestoreapi.com')
Expand All @@ -265,7 +327,7 @@ describe('httpClient', () => {
})
.reply(200, { id: 21 }, { headers: JSON_HEADERS })

const result = await sendPatch('https://fakestoreapi.com', 'products/1')
const result = await sendPatch('https://fakestoreapi.com', 'products/1', undefined)

expect(result.body).toEqual({ id: 21 })
})
Expand Down
73 changes: 63 additions & 10 deletions src/http/httpClient.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import type { Dispatcher } from 'undici'
import type { Readable } from 'stream'

import { request } from 'undici'
import type { Dispatcher, FormData } from 'undici'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type RecordObject = Record<string, any>

export type HttpRequestContext = {
reqId: string
}

export type GetRequestOptions = {
headers?: RecordObject
query?: RecordObject
timeout?: number
throwOnError?: boolean
reqContext?: HttpRequestContext
}

export type DeleteRequestOptions = GetRequestOptions
Expand All @@ -18,6 +25,7 @@ export type RequestOptions = {
query?: RecordObject
timeout?: number
throwOnError?: boolean
reqContext?: HttpRequestContext
}

const defaultOptions: GetRequestOptions = {
Expand All @@ -40,7 +48,10 @@ export async function sendGet<T>(
const response = await request(url, {
method: 'GET',
query: options.query,
headers: options.headers,
headers: {
'x-request-id': options.reqContext?.reqId,
...options.headers,
},
bodyTimeout: options.timeout,
throwOnError: options.throwOnError,
})
Expand All @@ -65,7 +76,10 @@ export async function sendDelete<T>(
const response = await request(url, {
method: 'DELETE',
query: options.query,
headers: options.headers,
headers: {
'x-request-id': options.reqContext?.reqId,
...options.headers,
},
bodyTimeout: options.timeout,
throwOnError: options.throwOnError,
})
Expand All @@ -84,15 +98,18 @@ export async function sendDelete<T>(
export async function sendPost<T>(
baseUrl: string,
path: string,
body?: RecordObject | undefined,
body: RecordObject | undefined,
options: RequestOptions = defaultOptions,
): Promise<Response<T>> {
const url = resolveUrl(baseUrl, path)
const response = await request(url, {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
query: options.query,
headers: options.headers,
headers: {
'x-request-id': options.reqContext?.reqId,
...options.headers,
},
bodyTimeout: options.timeout,
throwOnError: options.throwOnError,
})
Expand All @@ -111,15 +128,48 @@ export async function sendPost<T>(
export async function sendPut<T>(
baseUrl: string,
path: string,
body?: RecordObject | undefined,
body: RecordObject | undefined,
options: RequestOptions = defaultOptions,
): Promise<Response<T>> {
const url = resolveUrl(baseUrl, path)
const response = await request(url, {
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
query: options.query,
headers: options.headers,
headers: {
'x-request-id': options.reqContext?.reqId,
...options.headers,
},
bodyTimeout: options.timeout,
throwOnError: options.throwOnError,
})

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const resolvedBody = await resolveBody(response)

return {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
body: resolvedBody,
headers: response.headers,
statusCode: response.statusCode,
}
}

export async function sendPutBinary<T>(
baseUrl: string,
path: string,
body: Buffer | Uint8Array | Readable | null | FormData,
options: RequestOptions = defaultOptions,
): Promise<Response<T>> {
const url = resolveUrl(baseUrl, path)
const response = await request(url, {
method: 'PUT',
body,
query: options.query,
headers: {
'x-request-id': options.reqContext?.reqId,
...options.headers,
},
bodyTimeout: options.timeout,
throwOnError: options.throwOnError,
})
Expand All @@ -138,15 +188,18 @@ export async function sendPut<T>(
export async function sendPatch<T>(
baseUrl: string,
path: string,
body?: RecordObject | undefined,
body: RecordObject | undefined,
options: RequestOptions = defaultOptions,
): Promise<Response<T>> {
const url = resolveUrl(baseUrl, path)
const response = await request(url, {
method: 'PATCH',
body: body ? JSON.stringify(body) : undefined,
query: options.query,
headers: options.headers,
headers: {
'x-request-id': options.reqContext?.reqId,
...options.headers,
},
bodyTimeout: options.timeout,
throwOnError: options.throwOnError,
})
Expand All @@ -165,7 +218,7 @@ export async function sendPatch<T>(
async function resolveBody(response: Dispatcher.ResponseData) {
const contentType = response.headers['content-type']
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return contentType?.startsWith('application/json')
return !contentType || contentType?.startsWith('application/json')
? await response.body.json()
: await response.body.text()
}
Expand Down

0 comments on commit 495af2f

Please sign in to comment.