fix(frontend): refactor api error handling

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-01-14 22:35:37 +01:00
parent e93144eb40
commit 57bfca7b15
44 changed files with 387 additions and 465 deletions

View file

@ -3,6 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiError } from '../api-error'
import type { ApiErrorResponse } from '../api-error-response'
import { ApiResponse } from '../api-response'
import { defaultConfig, defaultHeaders } from '../default-config'
import deepmerge from 'deepmerge'
@ -14,10 +16,8 @@ import deepmerge from 'deepmerge'
*/
export abstract class ApiRequestBuilder<ResponseType> {
private readonly targetUrl: string
private overrideExpectedResponseStatus: number | undefined
private customRequestOptions = defaultConfig
private customRequestHeaders = new Headers(defaultHeaders)
private customStatusCodeErrorMapping: Record<number, string> | undefined
protected requestBody: BodyInit | undefined
/**
@ -25,14 +25,11 @@ export abstract class ApiRequestBuilder<ResponseType> {
*
* @param endpoint The target endpoint without a leading slash.
*/
constructor(endpoint: string) {
constructor(endpoint: string, private apiI18nKey: string) {
this.targetUrl = `api/private/${endpoint}`
}
protected async sendRequestAndVerifyResponse(
httpMethod: RequestInit['method'],
defaultExpectedStatus: number
): Promise<ApiResponse<ResponseType>> {
protected async sendRequestAndVerifyResponse(httpMethod: RequestInit['method']): Promise<ApiResponse<ResponseType>> {
const response = await fetch(this.targetUrl, {
...this.customRequestOptions,
method: httpMethod,
@ -40,20 +37,19 @@ export abstract class ApiRequestBuilder<ResponseType> {
body: this.requestBody
})
if (this.customStatusCodeErrorMapping && this.customStatusCodeErrorMapping[response.status]) {
throw new Error(this.customStatusCodeErrorMapping[response.status])
}
const expectedStatus = this.overrideExpectedResponseStatus
? this.overrideExpectedResponseStatus
: defaultExpectedStatus
if (response.status !== expectedStatus) {
throw new Error(`Expected response status code ${expectedStatus} but received ${response.status}.`)
if (response.status >= 400) {
const apiErrorResponse = await this.readApiErrorResponseFromBody(response)
const statusText = response.status === 400 ? apiErrorResponse?.error ?? 'unknown' : response.statusText
throw new ApiError(response.status, statusText, this.apiI18nKey, apiErrorResponse?.error)
}
return new ApiResponse(response)
}
private async readApiErrorResponseFromBody(response: Response): Promise<ApiErrorResponse | undefined> {
return response.json().catch(() => undefined) as Promise<ApiErrorResponse | undefined>
}
/**
* Adds an HTTP header to the API request. Previous headers with the same name will get overridden on subsequent calls
* with the same name.
@ -78,30 +74,6 @@ export abstract class ApiRequestBuilder<ResponseType> {
return this
}
/**
* Adds a mapping from response status codes to error messages. An error with the specified message will be thrown
* when the status code of the response matches one of the defined ones.
*
* @param mapping The mapping from response status codes to error messages.
* @return The API request instance itself for chaining.
*/
withStatusCodeErrorMapping(mapping: Record<number, string>): this {
this.customStatusCodeErrorMapping = mapping
return this
}
/**
* Sets the expected status code of the response. Can be used to override the default expected status code.
* An error will be thrown when the status code of the response does not match the expected one.
*
* @param expectedCode The expected status code of the response.
* @return The API request instance itself for chaining.
*/
withExpectedStatusCode(expectedCode: number): this {
this.overrideExpectedResponseStatus = expectedCode
return this
}
/**
* Send the prepared API call as a GET request. A default status code of 200 is expected.
*

View file

@ -3,6 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiError } from '../api-error'
import type { ApiErrorResponse } from '../api-error-response'
import { DeleteApiRequestBuilder } from './delete-api-request-builder'
import { expectFetch } from './test-utils/expect-fetch'
@ -19,7 +21,7 @@ describe('DeleteApiRequestBuilder', () => {
describe('sendRequest without body', () => {
it('without headers', async () => {
expectFetch('api/private/test', 204, { method: 'DELETE' })
await new DeleteApiRequestBuilder<string, undefined>('test').sendRequest()
await new DeleteApiRequestBuilder<string, undefined>('test', 'test').sendRequest()
})
it('with single header', async () => {
@ -29,7 +31,7 @@ describe('DeleteApiRequestBuilder', () => {
method: 'DELETE',
headers: expectedHeaders
})
await new DeleteApiRequestBuilder<string, undefined>('test').withHeader('test', 'true').sendRequest()
await new DeleteApiRequestBuilder<string, undefined>('test', 'test').withHeader('test', 'true').sendRequest()
})
it('with overriding single header', async () => {
@ -39,7 +41,7 @@ describe('DeleteApiRequestBuilder', () => {
method: 'DELETE',
headers: expectedHeaders
})
await new DeleteApiRequestBuilder<string, undefined>('test')
await new DeleteApiRequestBuilder<string, undefined>('test', 'test')
.withHeader('test', 'true')
.withHeader('test', 'false')
.sendRequest()
@ -53,7 +55,7 @@ describe('DeleteApiRequestBuilder', () => {
method: 'DELETE',
headers: expectedHeaders
})
await new DeleteApiRequestBuilder<string, undefined>('test')
await new DeleteApiRequestBuilder<string, undefined>('test', 'test')
.withHeader('test', 'true')
.withHeader('test2', 'false')
.sendRequest()
@ -69,7 +71,7 @@ describe('DeleteApiRequestBuilder', () => {
headers: expectedHeaders,
body: '{"test":true,"foo":"bar"}'
})
await new DeleteApiRequestBuilder('test')
await new DeleteApiRequestBuilder('test', 'test')
.withJsonBody({
test: true,
foo: 'bar'
@ -82,12 +84,7 @@ describe('DeleteApiRequestBuilder', () => {
method: 'DELETE',
body: 'HedgeDoc'
})
await new DeleteApiRequestBuilder('test').withBody('HedgeDoc').sendRequest()
})
it('sendRequest with expected status code', async () => {
expectFetch('api/private/test', 200, { method: 'DELETE' })
await new DeleteApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
await new DeleteApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest()
})
describe('sendRequest with custom options', () => {
@ -96,7 +93,7 @@ describe('DeleteApiRequestBuilder', () => {
method: 'DELETE',
cache: 'force-cache'
})
await new DeleteApiRequestBuilder<string, undefined>('test')
await new DeleteApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -108,7 +105,7 @@ describe('DeleteApiRequestBuilder', () => {
method: 'DELETE',
cache: 'no-store'
})
await new DeleteApiRequestBuilder<string, undefined>('test')
await new DeleteApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -124,7 +121,7 @@ describe('DeleteApiRequestBuilder', () => {
cache: 'force-cache',
integrity: 'test'
})
await new DeleteApiRequestBuilder<string, undefined>('test')
await new DeleteApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache',
integrity: 'test'
@ -133,37 +130,29 @@ describe('DeleteApiRequestBuilder', () => {
})
})
describe('sendRequest with custom error map', () => {
it('for valid status code', async () => {
expectFetch('api/private/test', 204, { method: 'DELETE' })
await new DeleteApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
})
it('for invalid status code 1', async () => {
describe('failing sendRequest', () => {
it('with bad request without api error name', async () => {
expectFetch('api/private/test', 400, { method: 'DELETE' })
const request = new DeleteApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('noooooo')
const request = new DeleteApiRequestBuilder<string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion'))
})
it('for invalid status code 2', async () => {
expectFetch('api/private/test', 401, { method: 'DELETE' })
const request = new DeleteApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('not you!')
it('with bad request with api error name', async () => {
expectFetch('api/private/test', 400, { method: 'DELETE' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new DeleteApiRequestBuilder<string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion'))
})
it('with non bad request error', async () => {
expectFetch('api/private/test', 401, { method: 'DELETE' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new DeleteApiRequestBuilder<string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion'))
})
})
})

View file

@ -21,6 +21,6 @@ export class DeleteApiRequestBuilder<ResponseType = void, RequestBodyType = unkn
* @see ApiRequestBuilder#sendRequest
*/
sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('DELETE', 204)
return this.sendRequestAndVerifyResponse('DELETE')
}
}

View file

@ -3,6 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiError } from '../api-error'
import type { ApiErrorResponse } from '../api-error-response'
import { GetApiRequestBuilder } from './get-api-request-builder'
import { expectFetch } from './test-utils/expect-fetch'
@ -20,7 +22,7 @@ describe('GetApiRequestBuilder', () => {
describe('sendRequest', () => {
it('without headers', async () => {
expectFetch('api/private/test', 200, { method: 'GET' })
await new GetApiRequestBuilder<string>('test').sendRequest()
await new GetApiRequestBuilder<string>('test', 'test').sendRequest()
})
it('with single header', async () => {
@ -30,7 +32,7 @@ describe('GetApiRequestBuilder', () => {
method: 'GET',
headers: expectedHeaders
})
await new GetApiRequestBuilder<string>('test').withHeader('test', 'true').sendRequest()
await new GetApiRequestBuilder<string>('test', 'test').withHeader('test', 'true').sendRequest()
})
it('with overriding single header', async () => {
@ -40,7 +42,7 @@ describe('GetApiRequestBuilder', () => {
method: 'GET',
headers: expectedHeaders
})
await new GetApiRequestBuilder<string>('test')
await new GetApiRequestBuilder<string>('test', 'test')
.withHeader('test', 'true')
.withHeader('test', 'false')
.sendRequest()
@ -54,25 +56,20 @@ describe('GetApiRequestBuilder', () => {
method: 'GET',
headers: expectedHeaders
})
await new GetApiRequestBuilder<string>('test')
await new GetApiRequestBuilder<string>('test', 'test')
.withHeader('test', 'true')
.withHeader('test2', 'false')
.sendRequest()
})
})
it('sendRequest with expected status code', async () => {
expectFetch('api/private/test', 200, { method: 'GET' })
await new GetApiRequestBuilder<string>('test').withExpectedStatusCode(200).sendRequest()
})
describe('sendRequest with custom options', () => {
it('with one option', async () => {
expectFetch('api/private/test', 200, {
method: 'GET',
cache: 'force-cache'
})
await new GetApiRequestBuilder<string>('test')
await new GetApiRequestBuilder<string>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -84,7 +81,7 @@ describe('GetApiRequestBuilder', () => {
method: 'GET',
cache: 'no-store'
})
await new GetApiRequestBuilder<string>('test')
await new GetApiRequestBuilder<string>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -100,7 +97,7 @@ describe('GetApiRequestBuilder', () => {
cache: 'force-cache',
integrity: 'test'
})
await new GetApiRequestBuilder<string>('test')
await new GetApiRequestBuilder<string>('test', 'test')
.withCustomOptions({
cache: 'force-cache',
integrity: 'test'
@ -109,37 +106,29 @@ describe('GetApiRequestBuilder', () => {
})
})
describe('sendRequest with custom error map', () => {
it('for valid status code', async () => {
expectFetch('api/private/test', 200, { method: 'GET' })
await new GetApiRequestBuilder<string>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
})
it('for invalid status code 1', async () => {
describe('failing sendRequest', () => {
it('with bad request without api error name', async () => {
expectFetch('api/private/test', 400, { method: 'GET' })
const request = new GetApiRequestBuilder<string>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('noooooo')
const request = new GetApiRequestBuilder<string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion'))
})
it('for invalid status code 2', async () => {
expectFetch('api/private/test', 401, { method: 'GET' })
const request = new GetApiRequestBuilder<string>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('not you!')
it('with bad request with api error name', async () => {
expectFetch('api/private/test', 400, { method: 'GET' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new GetApiRequestBuilder<string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion'))
})
it('with non bad request error', async () => {
expectFetch('api/private/test', 401, { method: 'GET' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new GetApiRequestBuilder<string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion'))
})
})
})

View file

@ -17,6 +17,6 @@ export class GetApiRequestBuilder<ResponseType> extends ApiRequestBuilder<Respon
* @see ApiRequestBuilder#sendRequest
*/
sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('GET', 200)
return this.sendRequestAndVerifyResponse('GET')
}
}

View file

@ -3,6 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiError } from '../api-error'
import type { ApiErrorResponse } from '../api-error-response'
import { PostApiRequestBuilder } from './post-api-request-builder'
import { expectFetch } from './test-utils/expect-fetch'
@ -20,7 +22,7 @@ describe('PostApiRequestBuilder', () => {
describe('sendRequest without body', () => {
it('without headers', async () => {
expectFetch('api/private/test', 201, { method: 'POST' })
await new PostApiRequestBuilder<string, undefined>('test').sendRequest()
await new PostApiRequestBuilder<string, undefined>('test', 'test').sendRequest()
})
it('with single header', async () => {
@ -30,7 +32,7 @@ describe('PostApiRequestBuilder', () => {
method: 'POST',
headers: expectedHeaders
})
await new PostApiRequestBuilder<string, undefined>('test').withHeader('test', 'true').sendRequest()
await new PostApiRequestBuilder<string, undefined>('test', 'test').withHeader('test', 'true').sendRequest()
})
it('with overriding single header', async () => {
@ -40,7 +42,7 @@ describe('PostApiRequestBuilder', () => {
method: 'POST',
headers: expectedHeaders
})
await new PostApiRequestBuilder<string, undefined>('test')
await new PostApiRequestBuilder<string, undefined>('test', 'test')
.withHeader('test', 'true')
.withHeader('test', 'false')
.sendRequest()
@ -54,7 +56,7 @@ describe('PostApiRequestBuilder', () => {
method: 'POST',
headers: expectedHeaders
})
await new PostApiRequestBuilder<string, undefined>('test')
await new PostApiRequestBuilder<string, undefined>('test', 'test')
.withHeader('test', 'true')
.withHeader('test2', 'false')
.sendRequest()
@ -70,7 +72,7 @@ describe('PostApiRequestBuilder', () => {
headers: expectedHeaders,
body: '{"test":true,"foo":"bar"}'
})
await new PostApiRequestBuilder('test')
await new PostApiRequestBuilder('test', 'test')
.withJsonBody({
test: true,
foo: 'bar'
@ -83,12 +85,7 @@ describe('PostApiRequestBuilder', () => {
method: 'POST',
body: 'HedgeDoc'
})
await new PostApiRequestBuilder('test').withBody('HedgeDoc').sendRequest()
})
it('sendRequest with expected status code', async () => {
expectFetch('api/private/test', 200, { method: 'POST' })
await new PostApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
await new PostApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest()
})
describe('sendRequest with custom options', () => {
@ -97,7 +94,7 @@ describe('PostApiRequestBuilder', () => {
method: 'POST',
cache: 'force-cache'
})
await new PostApiRequestBuilder<string, undefined>('test')
await new PostApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -109,7 +106,7 @@ describe('PostApiRequestBuilder', () => {
method: 'POST',
cache: 'no-store'
})
await new PostApiRequestBuilder<string, undefined>('test')
await new PostApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -125,7 +122,7 @@ describe('PostApiRequestBuilder', () => {
cache: 'force-cache',
integrity: 'test'
})
await new PostApiRequestBuilder<string, undefined>('test')
await new PostApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache',
integrity: 'test'
@ -134,37 +131,29 @@ describe('PostApiRequestBuilder', () => {
})
})
describe('sendRequest with custom error map', () => {
it('for valid status code', async () => {
expectFetch('api/private/test', 201, { method: 'POST' })
await new PostApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
})
it('for invalid status code 1', async () => {
describe('failing sendRequest', () => {
it('with bad request without api error name', async () => {
expectFetch('api/private/test', 400, { method: 'POST' })
const request = new PostApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('noooooo')
const request = new PostApiRequestBuilder<string, string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion'))
})
it('for invalid status code 2', async () => {
expectFetch('api/private/test', 401, { method: 'POST' })
const request = new PostApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('not you!')
it('with bad request with api error name', async () => {
expectFetch('api/private/test', 400, { method: 'POST' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new PostApiRequestBuilder<string, string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion'))
})
it('with non bad request error', async () => {
expectFetch('api/private/test', 401, { method: 'POST' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new PostApiRequestBuilder<string, string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion'))
})
})
})

View file

@ -21,6 +21,6 @@ export class PostApiRequestBuilder<ResponseType, RequestBodyType> extends ApiReq
* @see ApiRequestBuilder#sendRequest
*/
sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('POST', 201)
return this.sendRequestAndVerifyResponse('POST')
}
}

View file

@ -3,6 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ApiError } from '../api-error'
import type { ApiErrorResponse } from '../api-error-response'
import { PutApiRequestBuilder } from './put-api-request-builder'
import { expectFetch } from './test-utils/expect-fetch'
@ -20,7 +22,7 @@ describe('PutApiRequestBuilder', () => {
describe('sendRequest without body', () => {
it('without headers', async () => {
expectFetch('api/private/test', 200, { method: 'PUT' })
await new PutApiRequestBuilder<string, undefined>('test').sendRequest()
await new PutApiRequestBuilder<string, undefined>('test', 'test').sendRequest()
})
it('with single header', async () => {
@ -30,7 +32,7 @@ describe('PutApiRequestBuilder', () => {
method: 'PUT',
headers: expectedHeaders
})
await new PutApiRequestBuilder<string, undefined>('test').withHeader('test', 'true').sendRequest()
await new PutApiRequestBuilder<string, undefined>('test', 'test').withHeader('test', 'true').sendRequest()
})
it('with overriding single header', async () => {
@ -40,7 +42,7 @@ describe('PutApiRequestBuilder', () => {
method: 'PUT',
headers: expectedHeaders
})
await new PutApiRequestBuilder<string, undefined>('test')
await new PutApiRequestBuilder<string, undefined>('test', 'test')
.withHeader('test', 'true')
.withHeader('test', 'false')
.sendRequest()
@ -54,7 +56,7 @@ describe('PutApiRequestBuilder', () => {
method: 'PUT',
headers: expectedHeaders
})
await new PutApiRequestBuilder<string, undefined>('test')
await new PutApiRequestBuilder<string, undefined>('test', 'test')
.withHeader('test', 'true')
.withHeader('test2', 'false')
.sendRequest()
@ -70,7 +72,7 @@ describe('PutApiRequestBuilder', () => {
headers: expectedHeaders,
body: '{"test":true,"foo":"bar"}'
})
await new PutApiRequestBuilder('test')
await new PutApiRequestBuilder('test', 'test')
.withJsonBody({
test: true,
foo: 'bar'
@ -83,12 +85,7 @@ describe('PutApiRequestBuilder', () => {
method: 'PUT',
body: 'HedgeDoc'
})
await new PutApiRequestBuilder('test').withBody('HedgeDoc').sendRequest()
})
it('sendRequest with expected status code', async () => {
expectFetch('api/private/test', 200, { method: 'PUT' })
await new PutApiRequestBuilder<string, undefined>('test').withExpectedStatusCode(200).sendRequest()
await new PutApiRequestBuilder('test', 'test').withBody('HedgeDoc').sendRequest()
})
describe('sendRequest with custom options', () => {
@ -97,7 +94,7 @@ describe('PutApiRequestBuilder', () => {
method: 'PUT',
cache: 'force-cache'
})
await new PutApiRequestBuilder<string, undefined>('test')
await new PutApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -109,7 +106,7 @@ describe('PutApiRequestBuilder', () => {
method: 'PUT',
cache: 'no-store'
})
await new PutApiRequestBuilder<string, undefined>('test')
await new PutApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache'
})
@ -125,7 +122,7 @@ describe('PutApiRequestBuilder', () => {
cache: 'force-cache',
integrity: 'test'
})
await new PutApiRequestBuilder<string, undefined>('test')
await new PutApiRequestBuilder<string, undefined>('test', 'test')
.withCustomOptions({
cache: 'force-cache',
integrity: 'test'
@ -134,37 +131,29 @@ describe('PutApiRequestBuilder', () => {
})
})
describe('sendRequest with custom error map', () => {
it('for valid status code', async () => {
expectFetch('api/private/test', 200, { method: 'PUT' })
await new PutApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
})
it('for invalid status code 1', async () => {
describe('failing sendRequest', () => {
it('with bad request without api error name', async () => {
expectFetch('api/private/test', 400, { method: 'PUT' })
const request = new PutApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('noooooo')
const request = new PutApiRequestBuilder<string, string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'unknown', 'test', 'testExplosion'))
})
it('for invalid status code 2', async () => {
expectFetch('api/private/test', 401, { method: 'PUT' })
const request = new PutApiRequestBuilder<string, undefined>('test')
.withStatusCodeErrorMapping({
400: 'noooooo',
401: 'not you!'
})
.sendRequest()
await expect(request).rejects.toThrow('not you!')
it('with bad request with api error name', async () => {
expectFetch('api/private/test', 400, { method: 'PUT' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new PutApiRequestBuilder<string, string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(400, 'testExplosion', 'test', 'testExplosion'))
})
it('with non bad request error', async () => {
expectFetch('api/private/test', 401, { method: 'PUT' }, {
message: 'The API has exploded!',
error: 'testExplosion'
} as ApiErrorResponse)
const request = new PutApiRequestBuilder<string, string>('test', 'test').sendRequest()
await expect(request).rejects.toEqual(new ApiError(401, 'forbidden', 'test', 'testExplosion'))
})
})
})

View file

@ -21,6 +21,6 @@ export class PutApiRequestBuilder<ResponseType, RequestBodyType> extends ApiRequ
* @see ApiRequestBuilder#sendRequest
*/
sendRequest(): Promise<ApiResponse<ResponseType>> {
return this.sendRequestAndVerifyResponse('PUT', 200)
return this.sendRequestAndVerifyResponse('PUT')
}
}

View file

@ -14,7 +14,12 @@ import { Mock } from 'ts-mockery'
* @param requestStatusCode the status code the mocked request should return
* @param expectedOptions additional options
*/
export const expectFetch = (expectedUrl: string, requestStatusCode: number, expectedOptions: RequestInit): void => {
export const expectFetch = (
expectedUrl: string,
requestStatusCode: number,
expectedOptions: RequestInit,
responseBody?: unknown
): void => {
global.fetch = jest.fn((fetchUrl: RequestInfo | URL, fetchOptions?: RequestInit): Promise<Response> => {
expect(fetchUrl).toEqual(expectedUrl)
expect(fetchOptions).toStrictEqual({
@ -25,8 +30,20 @@ export const expectFetch = (expectedUrl: string, requestStatusCode: number, expe
})
return Promise.resolve(
Mock.of<Response>({
status: requestStatusCode
status: requestStatusCode,
statusText: mapCodeToText(requestStatusCode),
json: jest.fn(() => (responseBody ? Promise.resolve(responseBody) : Promise.reject()))
})
)
})
}
const mapCodeToText = (code: number): string => {
switch (code) {
case 400:
return 'bad_request'
case 401:
return 'forbidden'
default:
return 'unknown_code'
}
}