From e831dc788bb0da680f371901c7a837a118b54d02 Mon Sep 17 00:00:00 2001
From: maaaNu
Date: Tue, 19 Sep 2023 19:09:28 +0200
Subject: [PATCH] [2.1.0]
+ Post-Execution-Functions will now be executed, even if the handler failed
- The middlewareWithErrorHandling was removed. To regain the same functionality you can now pass an option-flag, called disableErrorHandling
---
.eslintignore | 1 -
.eslintrc.js | 18 +-
.github/workflows/ci.yml | 2 +-
.prettierignore | 1 -
.prettierrc.js | 5 +-
CHANGELOG.md | 4 +
.../test-header-authentication-function.ts | 3 +-
.../test-jwt-authorization-function.ts | 3 +-
.../test-validation-function.ts | 5 +-
.../header-authentication-test.integration.ts | 1 +
.../jwt-authorization-test.integration.ts | 1 +
.../validation-test.integration.ts | 1 +
package-lock.json | 100 ++++++++++-
package.json | 3 +-
src/appInsights/Logger.ts | 4 +-
src/appInsights/appInsightsWrapper.ts | 3 +-
.../ApplicationError.ts} | 0
src/error/errorHandler.test.ts | 39 +++++
src/error/errorHandler.ts | 79 +++++++++
src/error/index.ts | 2 +
src/headerAuthentication.test.ts | 19 ++-
src/headerAuthentication.ts | 3 +-
src/index.ts | 4 +-
src/jwtAuthorization.test.ts | 5 +-
src/jwtAuthorization.ts | 3 +-
src/middleware.test.ts | 156 +++++++++++++++++-
src/middleware.ts | 146 ++++++++--------
src/middlewareWithoutErrorHandling.test.ts | 108 ------------
src/validation.test.ts | 7 +-
src/validation.ts | 3 +-
30 files changed, 494 insertions(+), 235 deletions(-)
rename src/{applicationError.ts => error/ApplicationError.ts} (100%)
create mode 100644 src/error/errorHandler.test.ts
create mode 100644 src/error/errorHandler.ts
create mode 100644 src/error/index.ts
delete mode 100644 src/middlewareWithoutErrorHandling.test.ts
diff --git a/.eslintignore b/.eslintignore
index 8fbbd8e..f06235c 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,2 @@
node_modules
dist
-*.test.ts
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
index 93739ca..b7147e4 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,17 +1,11 @@
module.exports = {
- parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
- sourceType: "module", // Allows for the use of imports
- ecmaFeatures: {
- }
- },
- settings: {
- },
- extends: [
- "plugin:@typescript-eslint/recommended",
- "plugin:prettier/recommended"
- ],
- rules: {
+ sourceType: 'module', // Allows for the use of imports
+ ecmaFeatures: {},
},
+ settings: {},
+ extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
+ rules: {},
};
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index feb9a7d..ab945c2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
- node-version: [ 15.x, 16.x ]
+ node-version: [ 16.x, 18.x ]
steps:
- uses: actions/checkout@v4
diff --git a/.prettierignore b/.prettierignore
index a967bf0..543141e 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,5 +4,4 @@ package-lock.json
*.ejs
dist
coverage
-dist/
config/*
diff --git a/.prettierrc.js b/.prettierrc.js
index 20bf3f3..4859634 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -3,5 +3,8 @@ module.exports = {
trailingComma: "all",
singleQuote: true,
printWidth: 120,
- tabWidth: 4
+ tabWidth: 4,
+ importOrder: ["^[./]"],
+ importOrderSeparation: true,
+ importOrderSortSpecifiers: true
};
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27ca53a..0b86dc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# CHANGELOG
+## 2.1.0 (19.09.2023)
++ Post-Execution-Functions will now be executed, even if the handler failed
+- The middlewareWithErrorHandling was removed. To regain the same functionality you can now pass an option-flag, called disableErrorHandling
+
## 2.0.0 (01.04.2023)
- Added auto-logging functionality to the library that enhances searchability of saved log statements in Azure AppInsights by storing context properties in commonProperties.
- Removed the MiddlewareFunction-Type in favor for the azure-built-in one
diff --git a/example/test-header-authentication-function/test-header-authentication-function.ts b/example/test-header-authentication-function/test-header-authentication-function.ts
index 9132f93..623eac8 100644
--- a/example/test-header-authentication-function/test-header-authentication-function.ts
+++ b/example/test-header-authentication-function/test-header-authentication-function.ts
@@ -1,5 +1,6 @@
import { Context } from '@azure/functions';
-import middleware from '../../src/middleware';
+
+import { middleware } from '../../src';
import headerAuthentication from '../../src/headerAuthentication';
const functionHandler = async (context: Context): Promise => {
diff --git a/example/test-jwt-authorization-function/test-jwt-authorization-function.ts b/example/test-jwt-authorization-function/test-jwt-authorization-function.ts
index adc0711..23f1760 100644
--- a/example/test-jwt-authorization-function/test-jwt-authorization-function.ts
+++ b/example/test-jwt-authorization-function/test-jwt-authorization-function.ts
@@ -1,5 +1,6 @@
import { Context, ContextBindingData } from '@azure/functions';
-import middleware from '../../src/middleware';
+
+import { middleware } from '../../src';
import authorization from '../../src/jwtAuthorization';
const functionHandler = async (context: Context): Promise => {
diff --git a/example/test-validation-function/test-validation-function.ts b/example/test-validation-function/test-validation-function.ts
index 38f3f07..1d5039b 100644
--- a/example/test-validation-function/test-validation-function.ts
+++ b/example/test-validation-function/test-validation-function.ts
@@ -1,9 +1,10 @@
import { Context, HttpRequest } from '@azure/functions';
-import middleware from '../../src/middleware';
import * as Joi from 'joi';
-import validation from '../../src/validation';
import { ObjectSchema } from 'joi';
+import { middleware } from '../../src';
+import validation from '../../src/validation';
+
const schema: ObjectSchema = Joi.object({
name: Joi.string().min(3).max(30).required(),
}).required();
diff --git a/integration-test/header-authentication-test.integration.ts b/integration-test/header-authentication-test.integration.ts
index 845685b..7736d4e 100644
--- a/integration-test/header-authentication-test.integration.ts
+++ b/integration-test/header-authentication-test.integration.ts
@@ -1,4 +1,5 @@
import axios from 'axios';
+
import waitTillFunctionReady from './waitTillFunctionReady';
describe('The example azure function is started and the header authentication should execute the request', () => {
diff --git a/integration-test/jwt-authorization-test.integration.ts b/integration-test/jwt-authorization-test.integration.ts
index d7fc5ac..44bea48 100644
--- a/integration-test/jwt-authorization-test.integration.ts
+++ b/integration-test/jwt-authorization-test.integration.ts
@@ -1,4 +1,5 @@
import axios from 'axios';
+
import waitTillFunctionReady from './waitTillFunctionReady';
// Token generated with https://jwt.io/ containing the "userId" "c8e65ca7-a008-4b1c-b52a-4ad0ee417017"
diff --git a/integration-test/validation-test.integration.ts b/integration-test/validation-test.integration.ts
index e7d6247..15b5c27 100644
--- a/integration-test/validation-test.integration.ts
+++ b/integration-test/validation-test.integration.ts
@@ -1,4 +1,5 @@
import axios from 'axios';
+
import waitTillFunctionReady from './waitTillFunctionReady';
describe('The example azure function is started and the JOI validation should', () => {
diff --git a/package-lock.json b/package-lock.json
index e2a3da6..ca48f54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@senacor/azure-function-middleware",
- "version": "2.0.1",
+ "version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@senacor/azure-function-middleware",
- "version": "2.0.1",
+ "version": "2.1.0",
"license": "MIT",
"dependencies": {
"@azure/functions": "^3.0.0",
@@ -15,6 +15,7 @@
"jwt-decode": "^3.1.2"
},
"devDependencies": {
+ "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/jest": "^27.0.3",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
@@ -1351,6 +1352,95 @@
"node": ">= 6"
}
},
+ "node_modules/@trivago/prettier-plugin-sort-imports": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.0.tgz",
+ "integrity": "sha512-YBepjbt+ZNBVmN3ev1amQH3lWCmHyt5qTbLCp/syXJRu/Kw2koXh44qayB1gMRxcL/gV8egmjN5xWSrYyfUtyw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/generator": "7.17.7",
+ "@babel/parser": "^7.20.5",
+ "@babel/traverse": "7.17.3",
+ "@babel/types": "7.17.0",
+ "javascript-natural-sort": "0.7.1",
+ "lodash": "^4.17.21"
+ },
+ "peerDependencies": {
+ "@vue/compiler-sfc": "3.x",
+ "prettier": "2.x - 3.x"
+ },
+ "peerDependenciesMeta": {
+ "@vue/compiler-sfc": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/generator": {
+ "version": "7.17.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz",
+ "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.17.0",
+ "jsesc": "^2.5.1",
+ "source-map": "^0.5.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse": {
+ "version": "7.17.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz",
+ "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.16.7",
+ "@babel/generator": "^7.17.3",
+ "@babel/helper-environment-visitor": "^7.16.7",
+ "@babel/helper-function-name": "^7.16.7",
+ "@babel/helper-hoist-variables": "^7.16.7",
+ "@babel/helper-split-export-declaration": "^7.16.7",
+ "@babel/parser": "^7.17.3",
+ "@babel/types": "^7.17.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/types": {
+ "version": "7.17.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
+ "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.16.7",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.0",
"dev": true,
@@ -3330,6 +3420,12 @@
"node": ">=8"
}
},
+ "node_modules/javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
+ "dev": true
+ },
"node_modules/jest": {
"version": "27.5.1",
"dev": true,
diff --git a/package.json b/package.json
index 48a0a66..a467b89 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@senacor/azure-function-middleware",
- "version": "2.0.1",
+ "version": "2.1.0",
"description": "Middleware for azure functions to handle authentication, authorization, error handling and logging",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -25,6 +25,7 @@
"author": "florian.rudisch@senacor.com",
"license": "MIT",
"devDependencies": {
+ "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/jest": "^27.0.3",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
diff --git a/src/appInsights/Logger.ts b/src/appInsights/Logger.ts
index b2bd24e..dbea180 100644
--- a/src/appInsights/Logger.ts
+++ b/src/appInsights/Logger.ts
@@ -1,6 +1,6 @@
-import { SeverityLevel } from 'applicationinsights/out/Declarations/Contracts';
-import { TelemetryClient } from 'applicationinsights';
import { Logger } from '@azure/functions';
+import { TelemetryClient } from 'applicationinsights';
+import { SeverityLevel } from 'applicationinsights/out/Declarations/Contracts';
const consoleDefaultLog = (message: string): void => {
console.log(message);
diff --git a/src/appInsights/appInsightsWrapper.ts b/src/appInsights/appInsightsWrapper.ts
index 6ce189e..a14d39d 100644
--- a/src/appInsights/appInsightsWrapper.ts
+++ b/src/appInsights/appInsightsWrapper.ts
@@ -1,8 +1,9 @@
import { Context, HttpRequest } from '@azure/functions';
import * as appInsights from 'applicationinsights';
-import { createAppInsightsLogger, consoleLogger } from './Logger';
import { TelemetryClient } from 'applicationinsights';
+import { consoleLogger, createAppInsightsLogger } from './Logger';
+
const telemetryClients: { [key: string]: TelemetryClient } = {};
const isDisabled =
diff --git a/src/applicationError.ts b/src/error/ApplicationError.ts
similarity index 100%
rename from src/applicationError.ts
rename to src/error/ApplicationError.ts
diff --git a/src/error/errorHandler.test.ts b/src/error/errorHandler.test.ts
new file mode 100644
index 0000000..b2e6bef
--- /dev/null
+++ b/src/error/errorHandler.test.ts
@@ -0,0 +1,39 @@
+import { Context } from '@azure/functions';
+import { mock } from 'jest-mock-extended';
+
+import { ApplicationError } from './ApplicationError';
+import { errorHandler as sut } from './errorHandler';
+
+describe('Error-Handler should', () => {
+ const contextMock = mock();
+
+ beforeEach(() => {
+ jest.restoreAllMocks();
+
+ contextMock.log.error = jest.fn();
+ });
+
+ test('return an provided ApplicationError', () => {
+ const res = sut(new ApplicationError('', 400, { error: 'critical' }), contextMock);
+ expect(res.status).toStrictEqual(400);
+ expect(res.body).toStrictEqual({ error: 'critical' });
+ });
+
+ test('return an default error-message, if no errorResponseHandler was provided', () => {
+ const res = sut('Error!', contextMock);
+ expect(res.status).toStrictEqual(500);
+ expect(res.body).toStrictEqual({ message: 'Internal server error' });
+ });
+
+ test('use the errorResponseHandler, if an errorResponseHandler was provided', () => {
+ const errorResponseHandler = () => ({
+ status: 409,
+ body: {
+ message: 'conflict!',
+ },
+ });
+ const res = sut('Error!', contextMock, { errorResponseHandler });
+ expect(res.status).toStrictEqual(409);
+ expect(res.body).toStrictEqual({ message: 'conflict!' });
+ });
+});
diff --git a/src/error/errorHandler.ts b/src/error/errorHandler.ts
new file mode 100644
index 0000000..b6dbc92
--- /dev/null
+++ b/src/error/errorHandler.ts
@@ -0,0 +1,79 @@
+import { Context } from '@azure/functions';
+
+import { Options } from '../middleware';
+import { ApplicationError } from './ApplicationError';
+
+type ErrorWithMessage = {
+ message: string;
+ stack?: string;
+};
+
+const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ 'message' in error &&
+ typeof (error as Record).message === 'string'
+ );
+};
+
+const logErrorObject = (error: object | null, context: Context) => {
+ if (error === null) {
+ context.log.error('The provided error was eq to null - unable to log a specific error-message');
+ return;
+ }
+
+ if (isErrorWithMessage(error)) {
+ context.log.error({ message: error.message, stack: error.stack });
+ } else {
+ try {
+ const errorAsJson = JSON.stringify(error);
+ if (errorAsJson === '{}') {
+ context.log.error(error.toString());
+ } else {
+ context.log.error(errorAsJson); // Log the JSON string of the error object
+ }
+ } catch (_) {
+ //Fallback in case there's an error stringify
+ context.log.error(error.toString());
+ }
+ }
+};
+
+export const errorHandler = (
+ error: unknown,
+ context: Context,
+ opts?: Options,
+): {
+ [key: string]: unknown;
+} => {
+ if (error instanceof ApplicationError) {
+ context.log.error(`Received application error with message ${error.message}`);
+ return {
+ status: error.status,
+ body: error.body,
+ };
+ }
+
+ switch (typeof error) {
+ case 'string':
+ context.log.error(error);
+ break;
+ case 'object':
+ logErrorObject(error, context);
+ break;
+ default:
+ context.log(`The error object has a type, that is not suitable for logging: ${typeof error}`);
+ }
+
+ if (opts?.errorResponseHandler === undefined) {
+ return {
+ status: 500,
+ body: {
+ message: 'Internal server error',
+ },
+ };
+ } else {
+ return opts.errorResponseHandler(error);
+ }
+};
diff --git a/src/error/index.ts b/src/error/index.ts
new file mode 100644
index 0000000..c3903e3
--- /dev/null
+++ b/src/error/index.ts
@@ -0,0 +1,2 @@
+export * from './errorHandler';
+export * from './ApplicationError';
diff --git a/src/headerAuthentication.test.ts b/src/headerAuthentication.test.ts
index 721c54e..7573b47 100644
--- a/src/headerAuthentication.test.ts
+++ b/src/headerAuthentication.test.ts
@@ -1,7 +1,8 @@
-import { mock } from 'jest-mock-extended';
import { Context, HttpRequest } from '@azure/functions';
+import { mock } from 'jest-mock-extended';
+
+import { ApplicationError } from './error';
import sut from './headerAuthentication';
-import { ApplicationError } from './applicationError';
describe('The header authentication middleware should', () => {
const contextMock = mock();
@@ -24,6 +25,7 @@ describe('The header authentication middleware should', () => {
test('fail caused by missing default "x-ms-client-principal" header', async () => {
// suppressing in order to enforce missing header
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
requestMock.headers['x-ms-client-principal-id'] = undefined;
@@ -34,12 +36,13 @@ describe('The header authentication middleware should', () => {
test('fail caused by missing default "x-ms-client-principal" header and using the provided error body', async () => {
// suppressing in order to enforce missing header
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
requestMock.headers['x-ms-client-principal-id'] = undefined;
- await expect(sut(undefined, {error: 'Please authenticate properly'})(contextMock, requestMock)).rejects.toEqual(
- new ApplicationError('Authentication error', 403, {error: 'Please authenticate properly'}),
- );
+ await expect(
+ sut(undefined, { error: 'Please authenticate properly' })(contextMock, requestMock),
+ ).rejects.toEqual(new ApplicationError('Authentication error', 403, { error: 'Please authenticate properly' }));
});
test('fail caused by passed header validation function returns false', async () => {
@@ -49,8 +52,8 @@ describe('The header authentication middleware should', () => {
});
test('fail caused by passed header validation function returns false and use the provided error body', async () => {
- await expect(sut(() => false, {error: 'Please authenticate properly'})(contextMock, requestMock)).rejects.toEqual(
- new ApplicationError('Authentication error', 403, {error: 'Please authenticate properly'}),
- );
+ await expect(
+ sut(() => false, { error: 'Please authenticate properly' })(contextMock, requestMock),
+ ).rejects.toEqual(new ApplicationError('Authentication error', 403, { error: 'Please authenticate properly' }));
});
});
diff --git a/src/headerAuthentication.ts b/src/headerAuthentication.ts
index 2babc20..9d78171 100644
--- a/src/headerAuthentication.ts
+++ b/src/headerAuthentication.ts
@@ -1,7 +1,8 @@
import { AzureFunction, Context, HttpRequest } from '@azure/functions';
-import { ApplicationError } from './applicationError';
import { HttpRequestHeaders } from '@azure/functions/types/http';
+import { ApplicationError } from './error';
+
export default (
validateUsingHeaderFn?: (headers: HttpRequestHeaders) => boolean,
errorResponseBody?: unknown,
diff --git a/src/index.ts b/src/index.ts
index 913464b..edc403b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,6 @@
export { default as headerAuthentication } from './headerAuthentication';
-export { default as middleware, middlewareWithoutErrorHandling } from './middleware';
-export { ApplicationError } from './applicationError';
+export * from './middleware';
+export * from './error';
export { Rule, default as jwtAuthorization } from './jwtAuthorization';
export { default as validation } from './validation';
export { AppInsightForHttpTrigger, AppInsightForNoNHttpTrigger } from './appInsights/appInsightsWrapper';
diff --git a/src/jwtAuthorization.test.ts b/src/jwtAuthorization.test.ts
index 3a20366..0c0ebd3 100644
--- a/src/jwtAuthorization.test.ts
+++ b/src/jwtAuthorization.test.ts
@@ -1,8 +1,9 @@
-import { mock } from 'jest-mock-extended';
import { Context, HttpRequest } from '@azure/functions';
+import { mock } from 'jest-mock-extended';
import * as JWTDecoder from 'jwt-decode';
+
+import { ApplicationError } from './error';
import sut from './jwtAuthorization';
-import { ApplicationError } from './applicationError';
jest.mock('jwt-decode');
const jwtMock = JWTDecoder as jest.Mocked;
diff --git a/src/jwtAuthorization.ts b/src/jwtAuthorization.ts
index c4c71a6..80b09fc 100644
--- a/src/jwtAuthorization.ts
+++ b/src/jwtAuthorization.ts
@@ -1,6 +1,7 @@
import { AzureFunction, Context, ContextBindingData, HttpRequest } from '@azure/functions';
import jwtDecode from 'jwt-decode';
-import { ApplicationError } from './applicationError';
+
+import { ApplicationError } from './error';
const evaluate = (rule: Rule, parameters: ContextBindingData, jwt: T) => {
const pathParameter = rule.parameterExtractor(parameters);
diff --git a/src/middleware.test.ts b/src/middleware.test.ts
index fca3a72..ce01a38 100644
--- a/src/middleware.test.ts
+++ b/src/middleware.test.ts
@@ -1,7 +1,8 @@
-import { mock } from 'jest-mock-extended';
import { Context, HttpRequest } from '@azure/functions';
-import sut from './middleware';
-import { ApplicationError } from './applicationError';
+import { mock } from 'jest-mock-extended';
+
+import { ApplicationError } from './error';
+import { middleware as sut } from './middleware';
describe('The middleware layer should', () => {
const contextMock = mock();
@@ -125,6 +126,26 @@ describe('The middleware layer should', () => {
expect(contextMock.log.error).toBeCalled();
});
+ test('fail when the handler function is failing, but execute the post-execution-functions', async () => {
+ const handlerMock = jest.fn();
+ const middlewareOneMock = jest.fn();
+ const middlewareTwoMock = jest.fn();
+ handlerMock.mockRejectedValue(Error());
+
+ await sut([middlewareOneMock], handlerMock, [middlewareTwoMock])(contextMock, requestMock);
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(contextMock.res).toEqual({
+ status: 500,
+ body: {
+ message: 'Internal server error',
+ },
+ });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(contextMock.log.error).toBeCalled();
+ });
+
test('use the provided error-handle to create a response', async () => {
const handlerMock = jest.fn();
const middlewareOneMock = jest.fn();
@@ -132,8 +153,8 @@ describe('The middleware layer should', () => {
const middlewarePostFunction = jest.fn();
handlerMock.mockRejectedValue(Error());
- const errorResponseHandler = (error: unknown, context: Context) => {
- context.res = {
+ const errorResponseHandler = () => {
+ return {
status: 1337,
body: 'My custom error response',
};
@@ -152,3 +173,128 @@ describe('The middleware layer should', () => {
// expect(middlewarePostFunction).toBeCalled(); TODO: Fix later
});
});
+
+describe('The middleware layer with disabled error-handling should', () => {
+ const contextMock = mock();
+ const requestMock = mock();
+
+ beforeEach(() => {
+ jest.restoreAllMocks();
+
+ contextMock.log.error = jest.fn();
+ });
+
+ test('successfully call the passed functions without any middleware passed', async () => {
+ const handlerMock = jest.fn();
+
+ await sut([], handlerMock, [], { disableErrorHandling: true })(contextMock, requestMock);
+
+ expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ });
+
+ test('successfully call the middleware and the passed functions', async () => {
+ const handlerMock = jest.fn();
+ const middlewareOneMock = jest.fn();
+ const middlewareTwoMock = jest.fn();
+
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [], { disableErrorHandling: true })(
+ contextMock,
+ requestMock,
+ );
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ });
+
+ test('successfully call the pre middleware and post middleware and the passed functions', async () => {
+ const handlerMock = jest.fn();
+ const middlewareOneMock = jest.fn();
+ const middlewareTwoMock = jest.fn();
+ const middlewarePostMock = jest.fn();
+
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [middlewarePostMock], {
+ disableErrorHandling: true,
+ })(contextMock, requestMock);
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewarePostMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ });
+
+ test('fail when the first middleware (beforeExecution) is failing', async () => {
+ const handlerMock = jest.fn();
+ const middlewareOneMock = jest.fn();
+ const middlewareTwoMock = jest.fn();
+ middlewareOneMock.mockRejectedValue(Error());
+
+ await expect(
+ async () =>
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [], { disableErrorHandling: true })(
+ contextMock,
+ requestMock,
+ ),
+ ).rejects.toThrow();
+ expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareTwoMock).not.toBeCalled();
+ expect(handlerMock).not.toBeCalled();
+ });
+
+ test('fail when the second middleware (beforeExecution) is failing', async () => {
+ const handlerMock = jest.fn();
+ const middlewareOneMock = jest.fn();
+ const middlewareTwoMock = jest.fn();
+ middlewareTwoMock.mockRejectedValue(Error());
+
+ await expect(
+ async () =>
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [], { disableErrorHandling: true })(
+ contextMock,
+ requestMock,
+ ),
+ ).rejects.toThrow();
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(handlerMock).not.toBeCalled();
+ });
+
+ test('fail when the handler function is failing', async () => {
+ const handlerMock = jest.fn();
+ const middlewareOneMock = jest.fn();
+ const middlewareTwoMock = jest.fn();
+ handlerMock.mockRejectedValue(Error());
+
+ await expect(
+ async () =>
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [], { disableErrorHandling: true })(
+ contextMock,
+ requestMock,
+ ),
+ ).rejects.toThrow();
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ });
+
+ test('fail when the handler function is failing and a error with status is returned', async () => {
+ const handlerMock = jest.fn();
+ const middlewareOneMock = jest.fn();
+ const middlewareTwoMock = jest.fn();
+ handlerMock.mockRejectedValue(new ApplicationError('Validation Error', 401, 'test-body'));
+
+ await expect(
+ async () =>
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [], { disableErrorHandling: true })(
+ contextMock,
+ requestMock,
+ ),
+ ).rejects.toThrow();
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ });
+});
diff --git a/src/middleware.ts b/src/middleware.ts
index ac3d6ac..20a41d3 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,102 +1,92 @@
import { AzureFunction, Context } from '@azure/functions';
-import { ApplicationError } from './applicationError';
-type ErrorWithMessage = {
- message: string;
- stack?: string;
-};
+import { errorHandler } from './error';
-const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
- return (
- typeof error === 'object' &&
- error !== null &&
- 'message' in error &&
- typeof (error as Record).message === 'string'
- );
-};
+type ErrorResult = { $failed: true; $error: unknown };
-const logErrorObject = (error: object | null, context: Context) => {
- if (error === null) {
- context.log.error('The provided error was eq to null - unable to log a specific error-message');
- return;
- }
-
- if (isErrorWithMessage(error)) {
- context.log.error({ message: error.message, stack: error.stack });
- } else {
- try {
- const errorAsJson = JSON.stringify(error);
- if (errorAsJson === '{}') {
- context.log.error(error.toString());
- }
- } catch (_) {
- //Fallback in case there's an error stringify
- context.log.error(error.toString());
- }
- }
-};
+const isErrorResult = (result: unknown | ErrorResult): result is ErrorResult => (result as ErrorResult)?.$failed;
const middlewareCore =
- (beforeExecution: AzureFunction[], handler: AzureFunction, afterExecution: AzureFunction[]) =>
- async (context: Context, ...args: unknown[]): Promise => {
+ (beforeExecution: AzureFunction[], handler: AzureFunction, postExecution: AzureFunction[]) =>
+ async (context: Context, ...args: unknown[]): Promise => {
+ let error = undefined;
+
if (beforeExecution) {
for (const middlewareFunctions of beforeExecution) {
- await middlewareFunctions(context, ...args);
+ try {
+ // TODO: Give before-execution functions a parameter to indicate if the handler failed. So each function has the ability to decide for itself, if it should get executed.
+ if (error === undefined) {
+ await middlewareFunctions(context, ...args);
+ }
+ } catch (err) {
+ error = err;
+ }
+ }
+ }
+
+ let handlerResult;
+
+ if (error === undefined) {
+ try {
+ handlerResult = await handler(context, ...args);
+ } catch (err) {
+ error = err;
}
}
- const handlerResult = await handler(context, ...args);
- if (afterExecution) {
- for (const middleware of afterExecution) {
+
+ // TODO: Give post-execution functions a parameter to indicate if the handler failed. So each function has the ability to decide for itself, if it should get executed.
+ if (postExecution) {
+ for (const middleware of postExecution) {
await middleware(context, ...args);
}
}
+
+ if (error !== undefined) {
+ context.log.error(`An uncaught error occurred in the execution of the hander: ${error}`);
+ return { $failed: true, $error: error };
+ }
return handlerResult;
};
export type Options = {
- errorResponseHandler?: (error: unknown, context: Context) => void;
+ errorResponseHandler?: (error: unknown) => {
+ [key: string]: unknown;
+ };
+ disableErrorHandling?: boolean;
};
-const middleware =
+async function middlewareWrapper(
+ beforeExecution: AzureFunction[],
+ handler: (context: Context, ...args: unknown[]) => Promise | void,
+ postExecution: AzureFunction[],
+ context: Context,
+ args: unknown[],
+ opts?: Options,
+) {
+ const result = await middlewareCore(beforeExecution, handler, postExecution)(context, ...args);
+
+ if (isErrorResult(result)) {
+ if (opts?.disableErrorHandling) {
+ throw result.$error;
+ }
+
+ context.res = errorHandler(result.$error, context, opts);
+ return;
+ }
+
+ return result;
+}
+
+export const middleware =
(beforeExecution: AzureFunction[], handler: AzureFunction, postExecution: AzureFunction[], opts?: Options) =>
- async (context: Context, ...args: unknown[]): Promise => {
+ async (context: Context, ...args: unknown[]): Promise => {
+ if (opts?.disableErrorHandling) {
+ return await middlewareWrapper(beforeExecution, handler, postExecution, context, args, opts);
+ }
+
try {
- return await middlewareCore(beforeExecution, handler, postExecution)(context, ...args);
+ return await middlewareWrapper(beforeExecution, handler, postExecution, context, args, opts);
} catch (error) {
- if (error instanceof ApplicationError) {
- context.log.error(`Received application error with message ${error.message}`);
- context.res = {
- status: error.status,
- body: error.body,
- };
- return;
- }
-
- switch (typeof error) {
- case 'string':
- context.log.error(error);
- break;
- case 'object':
- logErrorObject(error, context);
- break;
- default:
- context.log(`The error object has a type, that is not suitable for logging: ${typeof error}`);
- }
-
- if (opts?.errorResponseHandler === undefined) {
- context.res = {
- status: 500,
- body: {
- message: 'Internal server error',
- },
- };
- return;
- } else {
- opts.errorResponseHandler(error, context);
- return;
- }
+ context.res = errorHandler(error, context, opts);
}
};
-
-export default middleware;
-export const middlewareWithoutErrorHandling = middlewareCore;
diff --git a/src/middlewareWithoutErrorHandling.test.ts b/src/middlewareWithoutErrorHandling.test.ts
deleted file mode 100644
index 2fe73a6..0000000
--- a/src/middlewareWithoutErrorHandling.test.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { mock } from 'jest-mock-extended';
-import { Context, HttpRequest } from '@azure/functions';
-import { middlewareWithoutErrorHandling as sut } from './middleware';
-import { ApplicationError } from './applicationError';
-
-describe('The middleware layer should', () => {
- const contextMock = mock();
- const requestMock = mock();
-
- beforeEach(() => {
- jest.restoreAllMocks();
-
- contextMock.log.error = jest.fn();
- });
-
- test('successfully call the passed functions without any middleware passed', async () => {
- const handlerMock = jest.fn();
-
- await sut([], handlerMock, [])(contextMock, requestMock);
-
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- });
-
- test('successfully call the middleware and the passed functions', async () => {
- const handlerMock = jest.fn();
- const middlewareOneMock = jest.fn();
- const middlewareTwoMock = jest.fn();
-
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock);
-
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- });
-
- test('successfully call the pre middleware and post middleware and the passed functions', async () => {
- const handlerMock = jest.fn();
- const middlewareOneMock = jest.fn();
- const middlewareTwoMock = jest.fn();
- const middlewarePostMock = jest.fn();
-
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [middlewarePostMock])(contextMock, requestMock);
-
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewarePostMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- });
-
- test('fail when the first middleware (beforeExecution) is failing', async () => {
- const handlerMock = jest.fn();
- const middlewareOneMock = jest.fn();
- const middlewareTwoMock = jest.fn();
- middlewareOneMock.mockRejectedValue(Error());
-
- await expect(
- async () => await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock),
- ).rejects.toThrow();
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).not.toBeCalled();
- expect(handlerMock).not.toBeCalled();
- });
-
- test('fail when the second middleware (beforeExecution) is failing', async () => {
- const handlerMock = jest.fn();
- const middlewareOneMock = jest.fn();
- const middlewareTwoMock = jest.fn();
- middlewareTwoMock.mockRejectedValue(Error());
-
- await expect(
- async () => await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock),
- ).rejects.toThrow();
-
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).not.toBeCalled();
- });
-
- test('fail when the handler function is failing', async () => {
- const handlerMock = jest.fn();
- const middlewareOneMock = jest.fn();
- const middlewareTwoMock = jest.fn();
- handlerMock.mockRejectedValue(Error());
-
- await expect(
- async () => await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock),
- ).rejects.toThrow();
-
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- });
-
- test('fail when the handler function is failing and a error with status is returned', async () => {
- const handlerMock = jest.fn();
- const middlewareOneMock = jest.fn();
- const middlewareTwoMock = jest.fn();
- handlerMock.mockRejectedValue(new ApplicationError('Validation Error', 401, 'test-body'));
-
- await expect(
- async () => await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock),
- ).rejects.toThrow();
-
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- });
-});
diff --git a/src/validation.test.ts b/src/validation.test.ts
index f8c8f9d..ce18a75 100644
--- a/src/validation.test.ts
+++ b/src/validation.test.ts
@@ -1,9 +1,10 @@
-import { mock } from 'jest-mock-extended';
import { Context, HttpRequest } from '@azure/functions';
-import * as JoiValidator from 'joi';
+import { mock } from 'jest-mock-extended';
+import JoiValidator from 'joi';
import { ValidationError } from 'joi';
+
+import { ApplicationError } from './error';
import sut from './validation';
-import { ApplicationError } from './applicationError';
jest.mock('joi');
describe('The joi validator should', () => {
diff --git a/src/validation.ts b/src/validation.ts
index 2050bb6..de061c1 100644
--- a/src/validation.ts
+++ b/src/validation.ts
@@ -1,6 +1,7 @@
import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import { AnySchema } from 'joi';
-import { ApplicationError } from './applicationError';
+
+import { ApplicationError } from './error';
export default (
schema: AnySchema,