diff --git a/jest.config.ts b/jest.config.ts index 5150d438..25655890 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -33,7 +33,7 @@ const config: Config.InitialOptions = { '/test/e2e/client.axios.spec.ts', '/test/e2e/client.babel.spec.ts', ], - modulePathIgnorePatterns: ['/test/e2e/generated'], + modulePathIgnorePatterns: ['/test/e2e/generated'], }, ], collectCoverageFrom: ['/src/**/*.ts', '!/src/**/*.d.ts', '!/bin', '!/dist'], diff --git a/package.json b/package.json index 710a2f37..83bdcd70 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "test:update": "jest --selectProjects UNIT --updateSnapshot", "test:watch": "jest --selectProjects UNIT --watch", "test:coverage": "jest --selectProjects UNIT --coverage", - "test:e2e": "jest --selectProjects E2E --runInBand", + "test:e2e": "jest --selectProjects E2E --runInBand --verbose", "eslint": "eslint .", "eslint:fix": "eslint . --fix", "prepublishOnly": "yarn run clean && yarn run release", diff --git a/src/templates/core/angular/getHeaders.hbs b/src/templates/core/angular/getHeaders.hbs index b0e395c7..db232a7f 100644 --- a/src/templates/core/angular/getHeaders.hbs +++ b/src/templates/core/angular/getHeaders.hbs @@ -1,40 +1,45 @@ -const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise => { - const token = await resolve(options, config.TOKEN); - const username = await resolve(options, config.USERNAME); - const password = await resolve(options, config.PASSWORD); - const additionalHeaders = await resolve(options, config.HEADERS); - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, +const getHeaders = (config: OpenAPIConfig, options: ApiRequestOptions): Observable => { + return forkJoin({ + token: resolve(options, config.TOKEN), + username: resolve(options, config.USERNAME), + password: resolve(options, config.PASSWORD), + additionalHeaders: resolve(options, config.HEADERS), }) - .filter(([_, value]) => isDefined(value)) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); + .pipe( + map(({ token, username, password, additionalHeaders }) => { + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + }) + .filter(([_, value]) => isDefined(value)) + .reduce((headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), {} as Record); - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } - if (options.body) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } + if (options.body) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } - return new HttpHeaders(headers); + return new HttpHeaders(headers); + }) + ); }; diff --git a/src/templates/core/angular/request.hbs b/src/templates/core/angular/request.hbs index 29d8beed..ff3e410a 100644 --- a/src/templates/core/angular/request.hbs +++ b/src/templates/core/angular/request.hbs @@ -1,8 +1,9 @@ {{>header}} import { HttpClient, HttpHeaders } from '@angular/common/http'; -import type { HttpResponse } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import type { HttpResponse, HttpErrorResponse } from '@angular/common/http'; +import { catchError, forkJoin, map, mergeMap, of, throwError } from 'rxjs'; +import type { Observable } from 'rxjs'; import { ApiError } from './ApiError'; import type { ApiRequestOptions } from './ApiRequestOptions'; @@ -54,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI'; {{>angular/getResponseBody}} -{{>functions/catchErrors}} +{{>functions/catchErrorCodes}} /** @@ -62,38 +63,47 @@ import type { OpenAPIConfig } from './OpenAPI'; * @param config The OpenAPI configuration object * @param http The Angular HTTP client * @param options The request options from the service - * @returns CancelablePromise + * @returns Observable * @throws ApiError */ export const request = (config: OpenAPIConfig, http: HttpClient, options: ApiRequestOptions): Observable => { - return new Observable(subscriber => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - getHeaders(config, options).then(headers => { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); - sendRequest(config, options, http, url, formData, body, headers) - .subscribe(response => { - const responseBody = getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - const result: ApiResult = { - url, - ok: response.ok, - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - }; - - catchErrors(options, result); - - subscriber.next(result.body); - }); - }); - } catch (error) { - subscriber.error(error); - } - }); + return getHeaders(config, options).pipe( + mergeMap( headers => { + return sendRequest(config, options, http, url, formData, body, headers); + }), + map(response => { + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader(response, options.responseHeader); + return { + url, + ok: response.ok, + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + } as ApiResult; + }), + catchError((error: HttpErrorResponse) => { + if (!error.status) { + return throwError(() => error); + } + return of({ + url, + ok: error.ok, + status: error.status, + statusText: error.statusText, + body: error.statusText, + } as ApiResult); + }), + map(result => { + catchErrorCodes(options, result); + return result.body as T; + }), + catchError((error: ApiError) => { + return throwError(() => error); + }), + ); }; - diff --git a/src/templates/core/axios/request.hbs b/src/templates/core/axios/request.hbs index 1ec09e8a..439aebe8 100644 --- a/src/templates/core/axios/request.hbs +++ b/src/templates/core/axios/request.hbs @@ -55,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI'; {{>axios/getResponseBody}} -{{>functions/catchErrors}} +{{>functions/catchErrorCodes}} /** @@ -86,7 +86,7 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C body: responseHeader ?? responseBody, }; - catchErrors(options, result); + catchErrorCodes(options, result); resolve(result.body); } diff --git a/src/templates/core/fetch/request.hbs b/src/templates/core/fetch/request.hbs index 13bf6a3e..4af6f944 100644 --- a/src/templates/core/fetch/request.hbs +++ b/src/templates/core/fetch/request.hbs @@ -52,7 +52,7 @@ import type { OpenAPIConfig } from './OpenAPI'; {{>fetch/getResponseBody}} -{{>functions/catchErrors}} +{{>functions/catchErrorCodes}} /** @@ -83,7 +83,7 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C body: responseHeader ?? responseBody, }; - catchErrors(options, result); + catchErrorCodes(options, result); resolve(result.body); } diff --git a/src/templates/core/functions/catchErrors.hbs b/src/templates/core/functions/catchErrorCodes.hbs similarity index 82% rename from src/templates/core/functions/catchErrors.hbs rename to src/templates/core/functions/catchErrorCodes.hbs index 8bc916fb..b99916a8 100644 --- a/src/templates/core/functions/catchErrors.hbs +++ b/src/templates/core/functions/catchErrorCodes.hbs @@ -1,4 +1,4 @@ -const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => { +const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { const errors: Record = { 400: 'Bad Request', 401: 'Unauthorized', diff --git a/src/templates/core/node/request.hbs b/src/templates/core/node/request.hbs index ba82a166..8405467d 100644 --- a/src/templates/core/node/request.hbs +++ b/src/templates/core/node/request.hbs @@ -56,7 +56,7 @@ import type { OpenAPIConfig } from './OpenAPI'; {{>node/getResponseBody}} -{{>functions/catchErrors}} +{{>functions/catchErrorCodes}} /** @@ -87,7 +87,7 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C body: responseHeader ?? responseBody, }; - catchErrors(options, result); + catchErrorCodes(options, result); resolve(result.body); } diff --git a/src/templates/core/xhr/request.hbs b/src/templates/core/xhr/request.hbs index 7732cd6e..47f92870 100644 --- a/src/templates/core/xhr/request.hbs +++ b/src/templates/core/xhr/request.hbs @@ -55,7 +55,7 @@ import type { OpenAPIConfig } from './OpenAPI'; {{>xhr/getResponseBody}} -{{>functions/catchErrors}} +{{>functions/catchErrorCodes}} /** @@ -86,7 +86,7 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C body: responseHeader ?? responseBody, }; - catchErrors(options, result); + catchErrorCodes(options, result); resolve(result.body); } diff --git a/src/utils/registerHandlebarTemplates.ts b/src/utils/registerHandlebarTemplates.ts index d2bb51c0..bf77cbdc 100644 --- a/src/utils/registerHandlebarTemplates.ts +++ b/src/utils/registerHandlebarTemplates.ts @@ -26,7 +26,7 @@ import fetchGetResponseHeader from '../templates/core/fetch/getResponseHeader.hb import fetchRequest from '../templates/core/fetch/request.hbs'; import fetchSendRequest from '../templates/core/fetch/sendRequest.hbs'; import functionBase64 from '../templates/core/functions/base64.hbs'; -import functionCatchErrors from '../templates/core/functions/catchErrors.hbs'; +import functionCatchErrorCodes from '../templates/core/functions/catchErrorCodes.hbs'; import functionGetFormData from '../templates/core/functions/getFormData.hbs'; import functionGetQueryString from '../templates/core/functions/getQueryString.hbs'; import functionGetUrl from '../templates/core/functions/getUrl.hbs'; @@ -167,7 +167,7 @@ export const registerHandlebarTemplates = (root: { Handlebars.registerPartial('base', Handlebars.template(partialBase)); // Generic functions used in 'request' file @see src/templates/core/request.hbs for more info - Handlebars.registerPartial('functions/catchErrors', Handlebars.template(functionCatchErrors)); + Handlebars.registerPartial('functions/catchErrorCodes', Handlebars.template(functionCatchErrorCodes)); Handlebars.registerPartial('functions/getFormData', Handlebars.template(functionGetFormData)); Handlebars.registerPartial('functions/getQueryString', Handlebars.template(functionGetQueryString)); Handlebars.registerPartial('functions/getUrl', Handlebars.template(functionGetUrl)); diff --git a/test/__snapshots__/index.spec.ts.snap b/test/__snapshots__/index.spec.ts.snap index 2b48d1d1..9b6019c1 100644 --- a/test/__snapshots__/index.spec.ts.snap +++ b/test/__snapshots__/index.spec.ts.snap @@ -469,7 +469,7 @@ const getResponseBody = async (response: Response): Promise => { return; }; -const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => { +const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { const errors: Record = { 400: 'Bad Request', 401: 'Unauthorized', @@ -519,7 +519,7 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C body: responseHeader ?? responseBody, }; - catchErrors(options, result); + catchErrorCodes(options, result); resolve(result.body); } @@ -3370,7 +3370,7 @@ const getResponseBody = async (response: Response): Promise => { return; }; -const catchErrors = (options: ApiRequestOptions, result: ApiResult): void => { +const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { const errors: Record = { 400: 'Bad Request', 401: 'Unauthorized', @@ -3420,7 +3420,7 @@ export const request = (config: OpenAPIConfig, options: ApiRequestOptions): C body: responseHeader ?? responseBody, }; - catchErrors(options, result); + catchErrorCodes(options, result); resolve(result.body); } diff --git a/test/e2e/v2.angular.spec.ts b/test/e2e/v2.angular.spec.ts index 2f4d396b..e33e14b1 100644 --- a/test/e2e/v2.angular.spec.ts +++ b/test/e2e/v2.angular.spec.ts @@ -51,4 +51,68 @@ describe('v2.angular', () => { }); expect(result).toBeDefined(); }); + + it('should throw known error (500)', async () => { + const error = await browser.evaluate(async () => { + try { + await new Promise((resolve, reject) => { + const { ErrorService } = (window as any).api; + ErrorService.testErrorCode(500).subscribe(resolve, reject); + }); + } catch (e) { + const error = e as any; + return JSON.stringify({ + name: error.name, + message: error.message, + url: error.url, + status: error.status, + statusText: error.statusText, + body: error.body, + }); + } + return; + }); + expect(error).toBe( + JSON.stringify({ + name: 'ApiError', + message: 'Custom message: Internal Server Error', + url: 'http://localhost:3000/base/api/v1.0/error?status=500', + status: 500, + statusText: 'Internal Server Error', + body: 'Internal Server Error', + }) + ); + }); + + it('should throw unknown error (409)', async () => { + const error = await browser.evaluate(async () => { + try { + await new Promise((resolve, reject) => { + const { ErrorService } = (window as any).api; + ErrorService.testErrorCode(409).subscribe(resolve, reject); + }); + } catch (e) { + const error = e as any; + return JSON.stringify({ + name: error.name, + message: error.message, + url: error.url, + status: error.status, + statusText: error.statusText, + body: error.body, + }); + } + return; + }); + expect(error).toBe( + JSON.stringify({ + name: 'ApiError', + message: 'Generic Error', + url: 'http://localhost:3000/base/api/v1.0/error?status=409', + status: 409, + statusText: 'Conflict', + body: 'Conflict', + }) + ); + }); }); diff --git a/test/e2e/v3.angular.spec.ts b/test/e2e/v3.angular.spec.ts index e5e2d75a..4f048b12 100644 --- a/test/e2e/v3.angular.spec.ts +++ b/test/e2e/v3.angular.spec.ts @@ -83,4 +83,70 @@ describe('v3.angular', () => { }); expect(result).toBeDefined(); }); + + it('should throw known error (500)', async () => { + const error = await browser.evaluate(async () => { + try { + await new Promise((resolve, reject) => { + const { ErrorService } = (window as any).api; + ErrorService.testErrorCode(500).subscribe(resolve, reject); + }); + } catch (e) { + const error = e as any; + return JSON.stringify({ + name: error.name, + message: error.message, + url: error.url, + status: error.status, + statusText: error.statusText, + body: error.body, + }); + } + return; + }); + expect(error).toBe( + JSON.stringify({ + name: 'ApiError', + message: 'Custom message: Internal Server Error', + url: 'http://localhost:3000/base/api/v1.0/error?status=500', + status: 500, + statusText: 'Internal Server Error', + body: 'Internal Server Error', + }) + ); + }); + + it('should throw unknown error (409)', async () => { + const error = await browser.evaluate(async () => { + const { ErrorService } = (window as any).api; + ErrorService.testErrorCode(409).subscribe(console.log, console.log); + try { + await new Promise((resolve, reject) => { + // const { ErrorService } = (window as any).api; + ErrorService.testErrorCode(409).subscribe(resolve, reject); + }); + } catch (e) { + const error = e as any; + return JSON.stringify({ + name: error.name, + message: error.message, + url: error.url, + status: error.status, + statusText: error.statusText, + body: error.body, + }); + } + return; + }); + expect(error).toBe( + JSON.stringify({ + name: 'ApiError', + message: 'Generic Error', + url: 'http://localhost:3000/base/api/v1.0/error?status=409', + status: 409, + statusText: 'Conflict', + body: 'Conflict', + }) + ); + }); });