From 41cb3f12cdc4aee9cbe914239308e70935efd1dc Mon Sep 17 00:00:00 2001
From: maaaNu
Date: Tue, 9 Apr 2024 19:21:19 +0200
Subject: [PATCH] [3.0.0-alpha.0] First try to upgrade to Programming Model V4
---
example/.funcignore | 10 +
example/host.json | 2 +-
example/package-lock.json | 82 ++++-
example/package.json | 13 +-
example/src/invocationHook.ts | 17 ++
.../test-header-authentication-function.ts | 15 +
.../src/test-jwt-authorization-function.ts | 27 ++
example/src/test-validation-function.ts | 29 ++
.../function.json | 19 --
.../test-header-authentication-function.ts | 11 -
.../function.json | 19 --
.../test-jwt-authorization-function.ts | 22 --
.../test-validation-function/function.json | 19 --
.../test-validation-function.ts | 22 --
example/tsconfig.json | 11 +-
.../validation-test.integration.ts | 2 +-
package-lock.json | 118 ++++++--
package.json | 5 +-
src/appInsights/Logger.ts | 119 ++++----
src/appInsights/appInsightsWrapper.ts | 140 +++++----
src/error/errorHandler.test.ts | 6 +-
src/error/errorHandler.ts | 12 +-
src/headerAuthentication.test.ts | 66 ++++-
src/headerAuthentication.ts | 70 +++--
src/jwtAuthorization.test.ts | 31 +-
src/jwtAuthorization.ts | 33 ++-
src/middleware.test.ts | 279 +++++++++---------
src/middleware.ts | 103 ++++---
src/validation.test.ts | 107 +++----
src/validation.ts | 102 +++++--
30 files changed, 908 insertions(+), 603 deletions(-)
create mode 100644 example/.funcignore
create mode 100644 example/src/invocationHook.ts
create mode 100644 example/src/test-header-authentication-function.ts
create mode 100644 example/src/test-jwt-authorization-function.ts
create mode 100644 example/src/test-validation-function.ts
delete mode 100644 example/test-header-authentication-function/function.json
delete mode 100644 example/test-header-authentication-function/test-header-authentication-function.ts
delete mode 100644 example/test-jwt-authorization-function/function.json
delete mode 100644 example/test-jwt-authorization-function/test-jwt-authorization-function.ts
delete mode 100644 example/test-validation-function/function.json
delete mode 100644 example/test-validation-function/test-validation-function.ts
diff --git a/example/.funcignore b/example/.funcignore
new file mode 100644
index 0000000..d5b3b4a
--- /dev/null
+++ b/example/.funcignore
@@ -0,0 +1,10 @@
+*.js.map
+*.ts
+.git*
+.vscode
+__azurite_db*__.json
+__blobstorage__
+__queuestorage__
+local.settings.json
+test
+tsconfig.json
\ No newline at end of file
diff --git a/example/host.json b/example/host.json
index 291065f..9df9136 100644
--- a/example/host.json
+++ b/example/host.json
@@ -10,6 +10,6 @@
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
- "version": "[2.*, 3.0.0)"
+ "version": "[4.*, 5.0.0)"
}
}
\ No newline at end of file
diff --git a/example/package-lock.json b/example/package-lock.json
index 74a2a38..aa312b2 100644
--- a/example/package-lock.json
+++ b/example/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
- "@azure/functions": "^3.0.0",
+ "@azure/functions": "^4.2.0",
"@types/node": "^18.11.18",
"joi": "^17.9.1"
},
@@ -22,9 +22,17 @@
}
},
"node_modules/@azure/functions": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.0.0.tgz",
- "integrity": "sha512-nxOdQdYoy9JEdAPsQlBWavsRvbH5qT2fpwMcY64s1sLIT8QwtW7ebh/MJNgzeAac+JaC6IED7plDiizq8oZUNw=="
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.4.0.tgz",
+ "integrity": "sha512-debidWolFTsfapsK53ftzLtXJc3dbYYPc9UqJoEm1GAj1lS7jFMARQnbfTQPDqBIhuJxLZ9D8WVvhIEV7Hifzw==",
+ "dependencies": {
+ "cookie": "^0.6.0",
+ "long": "^4.0.0",
+ "undici": "^5.13.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ }
},
"node_modules/@babel/code-frame": {
"version": "7.22.13",
@@ -580,6 +588,14 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
+ "node_modules/@fastify/busboy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@hapi/hoek": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
@@ -1492,6 +1508,14 @@
"safe-buffer": "~5.1.1"
}
},
+ "node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3218,6 +3242,11 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
+ "node_modules/long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+ },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -4415,6 +4444,17 @@
"which-boxed-primitive": "^1.0.2"
}
},
+ "node_modules/undici": {
+ "version": "5.28.4",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
+ "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
@@ -4670,9 +4710,14 @@
},
"dependencies": {
"@azure/functions": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.0.0.tgz",
- "integrity": "sha512-nxOdQdYoy9JEdAPsQlBWavsRvbH5qT2fpwMcY64s1sLIT8QwtW7ebh/MJNgzeAac+JaC6IED7plDiizq8oZUNw=="
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.4.0.tgz",
+ "integrity": "sha512-debidWolFTsfapsK53ftzLtXJc3dbYYPc9UqJoEm1GAj1lS7jFMARQnbfTQPDqBIhuJxLZ9D8WVvhIEV7Hifzw==",
+ "requires": {
+ "cookie": "^0.6.0",
+ "long": "^4.0.0",
+ "undici": "^5.13.0"
+ }
},
"@babel/code-frame": {
"version": "7.22.13",
@@ -5132,6 +5177,11 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
+ "@fastify/busboy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="
+ },
"@hapi/hoek": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.2.1.tgz",
@@ -5918,6 +5968,11 @@
"safe-buffer": "~5.1.1"
}
},
+ "cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
+ },
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -7311,6 +7366,11 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
+ "long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
+ },
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -8266,6 +8326,14 @@
"which-boxed-primitive": "^1.0.2"
}
},
+ "undici": {
+ "version": "5.28.4",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
+ "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
+ "requires": {
+ "@fastify/busboy": "^2.0.0"
+ }
+ },
"universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
diff --git a/example/package.json b/example/package.json
index 2383f0e..8b339c8 100644
--- a/example/package.json
+++ b/example/package.json
@@ -3,11 +3,11 @@
"version": "1.0.0",
"description": "Test project to demonstrate the azure-function-middleware",
"scripts": {
- "test": "jest",
"build": "tsc",
- "start:host": "func start --port 8080 --javascript",
- "start": "npm-run-all --parallel start:host watch",
- "watch": "tsc --w"
+ "watch": "tsc -w",
+ "clean": "rimraf dist",
+ "prestart": "npm run clean && npm run build",
+ "start": "func start"
},
"keywords": [
"azure",
@@ -25,8 +25,9 @@
"typescript": "^4.4.3"
},
"dependencies": {
- "@azure/functions": "^3.0.0",
+ "@azure/functions": "^4.2.0",
"@types/node": "^18.11.18",
"joi": "^17.9.1"
- }
+ },
+ "main": "dist/example/src/*.js"
}
diff --git a/example/src/invocationHook.ts b/example/src/invocationHook.ts
new file mode 100644
index 0000000..2e44aa0
--- /dev/null
+++ b/example/src/invocationHook.ts
@@ -0,0 +1,17 @@
+import { PostInvocationContext, PreInvocationContext, app } from '@azure/functions';
+
+app.hook.preInvocation((context: PreInvocationContext) => {
+ if (context.invocationContext.options.trigger.type === 'httpTrigger') {
+ context.invocationContext.log(
+ `preInvocation hook executed for http function ${context.invocationContext.functionName}`,
+ );
+ }
+});
+
+app.hook.postInvocation((context: PostInvocationContext) => {
+ if (context.invocationContext.options.trigger.type === 'httpTrigger') {
+ context.invocationContext.log(
+ `postInvocation hook executed for http function ${context.invocationContext.functionName}`,
+ );
+ }
+});
diff --git a/example/src/test-header-authentication-function.ts b/example/src/test-header-authentication-function.ts
new file mode 100644
index 0000000..41ceb03
--- /dev/null
+++ b/example/src/test-header-authentication-function.ts
@@ -0,0 +1,15 @@
+import { HttpRequest, InvocationContext, app } from '@azure/functions';
+
+import { middleware } from '../../src';
+import headerAuthentication from '../../src/headerAuthentication';
+
+export const handler = async (request: HttpRequest, context: InvocationContext) => {
+ context.info('Function called');
+ return { status: 204 };
+};
+app.http('test-header-authentication-function', {
+ methods: ['POST'],
+ authLevel: 'anonymous',
+ route: 'authentication',
+ handler: middleware([headerAuthentication()], handler, []),
+});
diff --git a/example/src/test-jwt-authorization-function.ts b/example/src/test-jwt-authorization-function.ts
new file mode 100644
index 0000000..5576409
--- /dev/null
+++ b/example/src/test-jwt-authorization-function.ts
@@ -0,0 +1,27 @@
+import { HttpHandler, HttpRequestParams, app } from '@azure/functions';
+
+import { middleware } from '../../src';
+import authorization from '../../src/jwtAuthorization';
+
+export const handler: HttpHandler = async (req, context) => {
+ context.log('Function called');
+ return { status: 204 };
+};
+
+app.http('test-jwt-authorization-function', {
+ methods: ['POST'],
+ authLevel: 'anonymous',
+ route: 'authorization/{id}',
+ handler: middleware(
+ [
+ authorization([
+ {
+ parameterExtractor: (parameters: HttpRequestParams) => parameters.id,
+ jwtExtractor: (jwt: { userId: string }) => jwt.userId,
+ },
+ ]),
+ ],
+ handler,
+ [],
+ ),
+});
diff --git a/example/src/test-validation-function.ts b/example/src/test-validation-function.ts
new file mode 100644
index 0000000..385776a
--- /dev/null
+++ b/example/src/test-validation-function.ts
@@ -0,0 +1,29 @@
+import { HttpHandler, app } from '@azure/functions';
+import * as Joi from 'joi';
+import { ObjectSchema } from 'joi';
+
+import { PostExecutionFunction, middleware } from '../../src';
+import { requestValidation as validation } from '../../src/validation';
+
+const schema: ObjectSchema = Joi.object({
+ name: Joi.string().min(3).max(30).required(),
+}).required();
+
+export const functionHandler: HttpHandler = async (req, context) => {
+ context.info('Function called');
+ const body = (await req.json()) as { name: string };
+
+ return { status: 200, jsonBody: { text: `Hallo ${body.name}` } };
+};
+
+const postFunction: PostExecutionFunction = (_, context) => {
+ context.log('Called after function');
+ return;
+};
+
+app.http('test-validation-function', {
+ methods: ['POST'],
+ authLevel: 'anonymous',
+ route: 'validation',
+ handler: middleware([validation(schema, { printRequest: true })], functionHandler, [postFunction]),
+});
diff --git a/example/test-header-authentication-function/function.json b/example/test-header-authentication-function/function.json
deleted file mode 100644
index d3fa7c5..0000000
--- a/example/test-header-authentication-function/function.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "bindings": [{
- "authLevel": "Anonymous",
- "type": "httpTrigger",
- "direction": "in",
- "name": "req",
- "methods": [
- "post"
- ],
- "route": "authentication"
- },
- {
- "type": "http",
- "direction": "out",
- "name": "res"
- }
- ],
- "scriptFile": "../dist/example/test-header-authentication-function/test-header-authentication-function.js"
-}
\ No newline at end of file
diff --git a/example/test-header-authentication-function/test-header-authentication-function.ts b/example/test-header-authentication-function/test-header-authentication-function.ts
deleted file mode 100644
index 623eac8..0000000
--- a/example/test-header-authentication-function/test-header-authentication-function.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Context } from '@azure/functions';
-
-import { middleware } from '../../src';
-import headerAuthentication from '../../src/headerAuthentication';
-
-const functionHandler = async (context: Context): Promise => {
- context.log.info('Function called');
- context.res = { status: 204 };
-};
-
-export default middleware([headerAuthentication()], functionHandler, []);
diff --git a/example/test-jwt-authorization-function/function.json b/example/test-jwt-authorization-function/function.json
deleted file mode 100644
index 97f891d..0000000
--- a/example/test-jwt-authorization-function/function.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "bindings": [{
- "authLevel": "Anonymous",
- "type": "httpTrigger",
- "direction": "in",
- "name": "req",
- "methods": [
- "post"
- ],
- "route": "authorization/{id}"
- },
- {
- "type": "http",
- "direction": "out",
- "name": "res"
- }
- ],
- "scriptFile": "../dist/example/test-jwt-authorization-function/test-jwt-authorization-function.js"
-}
\ No newline at end of file
diff --git a/example/test-jwt-authorization-function/test-jwt-authorization-function.ts b/example/test-jwt-authorization-function/test-jwt-authorization-function.ts
deleted file mode 100644
index 23f1760..0000000
--- a/example/test-jwt-authorization-function/test-jwt-authorization-function.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Context, ContextBindingData } from '@azure/functions';
-
-import { middleware } from '../../src';
-import authorization from '../../src/jwtAuthorization';
-
-const functionHandler = async (context: Context): Promise => {
- context.log.info('Function called');
- context.res = { status: 204 };
-};
-
-export default middleware(
- [
- authorization([
- {
- parameterExtractor: (parameters: ContextBindingData) => parameters.id,
- jwtExtractor: (jwt: { userId: string }) => jwt.userId,
- },
- ]),
- ],
- functionHandler,
- [],
-);
diff --git a/example/test-validation-function/function.json b/example/test-validation-function/function.json
deleted file mode 100644
index 7afb478..0000000
--- a/example/test-validation-function/function.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "bindings": [{
- "authLevel": "Anonymous",
- "type": "httpTrigger",
- "direction": "in",
- "name": "req",
- "methods": [
- "post"
- ],
- "route": "validation"
- },
- {
- "type": "http",
- "direction": "out",
- "name": "res"
- }
- ],
- "scriptFile": "../dist/example/test-validation-function/test-validation-function.js"
-}
\ No newline at end of file
diff --git a/example/test-validation-function/test-validation-function.ts b/example/test-validation-function/test-validation-function.ts
deleted file mode 100644
index 54062d8..0000000
--- a/example/test-validation-function/test-validation-function.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Context, HttpRequest } from '@azure/functions';
-import * as Joi from 'joi';
-import { ObjectSchema } from 'joi';
-
-import { middleware } from '../../src';
-import { requestValidation as validation } from '../../src/validation';
-
-const schema: ObjectSchema = Joi.object({
- name: Joi.string().min(3).max(30).required(),
-}).required();
-
-const functionHandler = async (context: Context, req: HttpRequest): Promise => {
- context.log.info('Function called');
- context.res = { status: 200, body: { text: `Hallo ${req.body.name}` } };
-};
-
-const postFunction = (context: Context): Promise => {
- context.log('Called after function');
- return;
-};
-
-export default middleware([validation(schema)], functionHandler, [postFunction]);
diff --git a/example/tsconfig.json b/example/tsconfig.json
index 2486e98..d4c8d9f 100644
--- a/example/tsconfig.json
+++ b/example/tsconfig.json
@@ -1,8 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
- "target": "es2015",
- "declaration": true,
- "outDir": "./dist"
- },
-}
+ "target": "es6",
+ "outDir": "dist",
+ "sourceMap": true,
+ "strict": false
+ }
+}
\ No newline at end of file
diff --git a/integration-test/validation-test.integration.ts b/integration-test/validation-test.integration.ts
index 15b5c27..28d7eea 100644
--- a/integration-test/validation-test.integration.ts
+++ b/integration-test/validation-test.integration.ts
@@ -38,6 +38,6 @@ describe('The example azure function is started and the JOI validation should',
expect(responseEmptyBody.data).toEqual({ message: '"name" is required' });
expect(responseUndefinedBody.status).toEqual(400);
- expect(responseUndefinedBody.data).toEqual({ message: '"value" is required' });
+ expect(responseUndefinedBody.data).toEqual({ message: 'Unexpected end of JSON input' });
});
});
diff --git a/package-lock.json b/package-lock.json
index 97f7186..6e7698c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,18 @@
{
"name": "@senacor/azure-function-middleware",
- "version": "2.3.0",
+ "version": "2.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@senacor/azure-function-middleware",
- "version": "2.3.0",
+ "version": "2.3.1",
"license": "MIT",
"dependencies": {
- "@azure/functions": "^3.0.0",
+ "@azure/functions": "^4.0.0",
"@types/node": "^20.8.10",
"axios": "^1.1.3",
+ "durable-functions": "^3.1.0",
"jwt-decode": "^4.0.0"
},
"devDependencies": {
@@ -179,12 +180,15 @@
"peer": true
},
"node_modules/@azure/functions": {
- "version": "3.5.0",
- "license": "MIT",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.2.0.tgz",
+ "integrity": "sha512-RSECLoje4jGVpsVRjEzkna9KvmQOVeB96cg8J5J2g41QQpMWCzD1QTPI5+yI0uvOidGRLYElV1zHZjdvsGf9Nw==",
"dependencies": {
- "iconv-lite": "^0.6.3",
"long": "^4.0.0",
- "uuid": "^8.3.0"
+ "undici": "^5.13.0"
+ },
+ "engines": {
+ "node": ">=18.0"
}
},
"node_modules/@azure/logger": {
@@ -899,6 +903,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@fastify/busboy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
+ "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"license": "BSD-3-Clause",
@@ -2226,11 +2238,11 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
- "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"dependencies": {
- "follow-redirects": "^1.15.0",
+ "follow-redirects": "^1.15.4",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@@ -2745,6 +2757,45 @@
"node": ">=8"
}
},
+ "node_modules/durable-functions": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/durable-functions/-/durable-functions-3.1.0.tgz",
+ "integrity": "sha512-baSkW45/VrVvl1e27qvxEZX2z5vApiINaGUTIxx0c4H5E2Lr22QnogZrebqIhyfspZ466XKhp245nyS0HSkAYg==",
+ "dependencies": {
+ "@azure/functions": "^4.0.0",
+ "axios": "^1.6.1",
+ "debug": "~2.6.9",
+ "lodash": "^4.17.15",
+ "moment": "^2.29.2",
+ "uuid": "~3.3.2",
+ "validator": "~13.7.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ }
+ },
+ "node_modules/durable-functions/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/durable-functions/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/durable-functions/node_modules/uuid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
+ "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
+ "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
+ "bin": {
+ "uuid": "bin/uuid"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.4.335",
"dev": true,
@@ -3259,14 +3310,15 @@
"license": "ISC"
},
"node_modules/follow-redirects": {
- "version": "1.15.2",
+ "version": "1.15.5",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
+ "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
- "license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -3475,16 +3527,6 @@
"node": ">=10.17.0"
}
},
- "node_modules/iconv-lite": {
- "version": "0.6.3",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/ignore": {
"version": "5.2.4",
"dev": true,
@@ -4497,7 +4539,6 @@
},
"node_modules/lodash": {
"version": "4.17.21",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash.memoize": {
@@ -4622,6 +4663,14 @@
"node": "*"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/ms": {
"version": "2.1.2",
"license": "MIT"
@@ -5127,6 +5176,7 @@
},
"node_modules/safer-buffer": {
"version": "2.1.2",
+ "dev": true,
"license": "MIT"
},
"node_modules/saxes": {
@@ -5555,6 +5605,17 @@
"node": ">=4.2.0"
}
},
+ "node_modules/undici": {
+ "version": "5.28.3",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz",
+ "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==",
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.0"
+ }
+ },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -5614,6 +5675,7 @@
"node_modules/uuid": {
"version": "8.3.2",
"license": "MIT",
+ "peer": true,
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -5639,6 +5701,14 @@
"node": ">= 8"
}
},
+ "node_modules/validator": {
+ "version": "13.7.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
+ "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"dev": true,
diff --git a/package.json b/package.json
index 058c03e..09d384b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@senacor/azure-function-middleware",
- "version": "2.3.1",
+ "version": "3.0.0-alpha.0",
"description": "Middleware for azure functions to handle authentication, authorization, error handling and logging",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -39,13 +39,14 @@
"typescript": "^4.5.4"
},
"dependencies": {
- "@azure/functions": "^3.0.0",
+ "@azure/functions": "^4.0.0",
"@types/node": "^20.8.10",
"axios": "^1.1.3",
"jwt-decode": "^4.0.0"
},
"peerDependencies": {
"applicationinsights": "^2.5.0",
+ "durable-functions": "^3.1.0",
"joi": "^17.9.1"
}
}
diff --git a/src/appInsights/Logger.ts b/src/appInsights/Logger.ts
index d96536d..cac5dae 100644
--- a/src/appInsights/Logger.ts
+++ b/src/appInsights/Logger.ts
@@ -1,66 +1,75 @@
-import { Logger } from '@azure/functions';
import { TelemetryClient } from 'applicationinsights';
import { SeverityLevel } from 'applicationinsights/out/Declarations/Contracts';
import { stringify } from '../util/stringify';
-const consoleDefaultLog = (...args: unknown[]): void => {
- console.log(args);
-};
-
-consoleDefaultLog.error = (...args: unknown[]): void => {
- console.error(args);
-};
-
-consoleDefaultLog.warn = (...args: unknown[]): void => {
- console.warn(args);
-};
+interface Logger {
+ log(...args: any[]): void;
+ trace(...args: any[]): void;
+ debug(...args: any[]): void;
+ info(...args: any[]): void;
+ warn(...args: any[]): void;
+ error(...args: any[]): void;
+}
-consoleDefaultLog.info = (...args: unknown[]): void => {
- console.info(args);
+export const consoleLogger: Logger = {
+ debug(...args: any[]): void {
+ console.debug(args);
+ },
+ error(...args: any[]): void {
+ console.error(args);
+ },
+ info(...args: any[]): void {
+ console.info(args);
+ },
+ log(...args: any[]): void {
+ console.log(args);
+ },
+ trace(...args: any[]): void {
+ console.trace(args);
+ },
+ warn(...args: any[]): void {
+ console.warn(args);
+ },
};
-consoleDefaultLog.verbose = (...args: unknown[]): void => {
- console.debug(args);
-};
-
-export const consoleLogger: Logger = consoleDefaultLog;
-
export const createAppInsightsLogger = (telemetryClient: TelemetryClient): Logger => {
- const appInsightsLogger = (...args: unknown[]): void => {
- telemetryClient.trackTrace({
- message: stringify(args),
- severity: SeverityLevel.Information,
- });
+ return {
+ debug(...args: any[]): void {
+ telemetryClient.trackTrace({
+ message: stringify(args),
+ severity: SeverityLevel.Information,
+ });
+ },
+ error(...args: any[]): void {
+ telemetryClient.trackTrace({
+ message: stringify(args),
+ severity: SeverityLevel.Error,
+ });
+ },
+ info(...args: any[]): void {
+ telemetryClient.trackTrace({
+ message: stringify(args),
+ severity: SeverityLevel.Information,
+ });
+ },
+ log(...args: any[]): void {
+ telemetryClient.trackTrace({
+ message: stringify(args),
+ severity: SeverityLevel.Information,
+ });
+ },
+ trace(...args: any[]): void {
+ telemetryClient.trackTrace({
+ message: stringify(args),
+ severity: SeverityLevel.Information,
+ });
+ },
+ warn(...args: any[]): void {
+ telemetryClient.trackTrace({
+ message: stringify(args),
+ severity: SeverityLevel.Warning,
+ });
+ },
};
-
- appInsightsLogger.error = (...args: unknown[]): void => {
- telemetryClient.trackTrace({
- message: stringify(args),
- severity: SeverityLevel.Error,
- });
- };
-
- appInsightsLogger.warn = (...args: unknown[]): void => {
- telemetryClient.trackTrace({
- message: stringify(args),
- severity: SeverityLevel.Warning,
- });
- };
-
- appInsightsLogger.info = (...args: unknown[]): void => {
- telemetryClient.trackTrace({
- message: stringify(args),
- severity: SeverityLevel.Information,
- });
- };
-
- appInsightsLogger.verbose = (...args: unknown[]): void => {
- telemetryClient.trackTrace({
- message: stringify(args),
- severity: SeverityLevel.Verbose,
- });
- };
-
- return appInsightsLogger;
};
diff --git a/src/appInsights/appInsightsWrapper.ts b/src/appInsights/appInsightsWrapper.ts
index 169b711..de756cc 100644
--- a/src/appInsights/appInsightsWrapper.ts
+++ b/src/appInsights/appInsightsWrapper.ts
@@ -1,12 +1,10 @@
-import { Context, HttpRequest } from '@azure/functions';
+import { FunctionHandler, HttpHandler, InvocationContext } from '@azure/functions';
import * as appInsights from 'applicationinsights';
import { TelemetryClient } from 'applicationinsights';
+import { ActivityHandler } from 'durable-functions';
-import { consoleLogger, createAppInsightsLogger } from './Logger';
-
-type LogBehavior = 'always' | 'on_error' | 'on_success' | 'never';
-type LogDataSanitizer = (data: unknown) => unknown;
-const noOpLogDataSanitizer: LogDataSanitizer = (data) => data;
+import { BeforeExecutionFunction, PostExecutionFunction, isErrorResult } from '../middleware';
+import { createAppInsightsLogger } from './Logger';
const telemetryClients: { [key: string]: TelemetryClient } = {};
@@ -34,25 +32,32 @@ if (!isDisabled) {
environment: process.env.ENVIRONMENT ?? 'UNDEFINED',
...appInsights.defaultClient.commonProperties,
};
+ appInsights.defaultClient.config.disableAppInsights = isDisabled;
appInsights.start();
console.log('Started appInsights');
}
-const setupTelemetryClient = (context: Context, additionalProperties: object) => {
+const setupTelemetryClient = (
+ req: unknown,
+ context: InvocationContext,
+ additionalProperties?: {
+ [key: string]: string;
+ },
+) => {
context.log('Setting up AppInsights');
const telemetryClient = new TelemetryClient();
telemetryClient.setAutoPopulateAzureProperties(true);
telemetryClients[context.invocationId] = telemetryClient;
- context.log = createAppInsightsLogger(telemetryClient);
+ context = { ...context, ...createAppInsightsLogger(telemetryClient) };
- const { invocationId, sys } = context.bindingData;
+ const { invocationId, triggerMetadata } = context;
telemetryClient.commonProperties = {
invocationId,
- sys,
+ ...(triggerMetadata !== undefined && triggerMetadata),
...additionalProperties,
...appInsights.defaultClient.commonProperties,
};
@@ -60,23 +65,27 @@ const setupTelemetryClient = (context: Context, additionalProperties: object) =>
context.log('Set up AppInsights');
};
-const setupAppInsightForHttpTrigger = async (context: Context): Promise => {
- if (isDisabled) {
- context.log = consoleLogger;
- return;
- }
+const setupAppInsightForHttpTrigger: BeforeExecutionFunction = async (req, context) => {
+ setupTelemetryClient(req, context);
+};
- setupTelemetryClient(context, { params: context.req?.params });
+const setupAppInsightForNonHttpTrigger: BeforeExecutionFunction = async (req, context) => {
+ setupTelemetryClient(req, context);
};
-const setupAppInsightForNonHttpTrigger = async (context: Context): Promise => {
- if (isDisabled) {
- context.log = consoleLogger;
- return;
- }
+type FinalizeAppInsightWithConfig> = T extends (
+ ...a: infer U
+) => infer R
+ ? (...a: [...U, LogBehavior, LogDataSanitizer]) => R
+ : never;
+type FinalizeAppInsightWithCurriedConfiguration = (
+ logBodyBehavior: LogBehavior,
+ bodySanitizer: LogDataSanitizer,
+) => PostExecutionFunction;
- setupTelemetryClient(context, { workflowData: context.bindingData.workflowData });
-};
+type LogBehavior = 'always' | 'on_error' | 'on_success' | 'never';
+type LogDataSanitizer = (data: unknown) => unknown;
+const noOpLogDataSanitizer: LogDataSanitizer = (data) => data;
const shouldLog = (logBehavior: LogBehavior, isError: boolean) => {
switch (logBehavior) {
@@ -91,22 +100,21 @@ const shouldLog = (logBehavior: LogBehavior, isError: boolean) => {
}
};
-const finalizeAppInsightForHttpTrigger = async (
- context: Context,
- req: HttpRequest,
- logBodyBehavior: LogBehavior = 'on_error',
- bodySanitizer: LogDataSanitizer = noOpLogDataSanitizer,
+const finalizeAppInsightForHttpTrigger: PostExecutionFunction = async (req, context, result) =>
+ finalizeAppInsightForHttpTriggerWithConfig(req, context, result, 'on_error', noOpLogDataSanitizer);
+const finalizeAppInsightForHttpTriggerWithConfig: FinalizeAppInsightWithConfig = async (
+ req,
+ context,
+ res,
+ logBodyBehavior,
+ bodySanitizer,
): Promise => {
- if (isDisabled) {
- return;
- }
-
context.log('Finalizing AppInsights');
const telemetryClient = telemetryClients[context.invocationId];
if (telemetryClient === undefined) {
- context.log.error(`No telemetry client could be found for invocationId ${context.invocationId}`);
+ context.error(`No telemetry client could be found for invocationId ${context.invocationId}`);
return;
}
@@ -116,20 +124,31 @@ const finalizeAppInsightForHttpTrigger = async (
...telemetryClient.commonProperties,
};
- if (context.res === undefined) {
- context.log.warn("context.res is empty and properly shouldn't be");
+ if (res === undefined) {
+ context.warn("res is empty and properly shouldn't be");
+ }
+
+ if (isErrorResult(res)) {
+ return;
+ }
+
+ const result = res.$result;
+
+ if (result == undefined) {
+ context.warn("res is empty and properly shouldn't be");
+ return;
}
- if (shouldLog(logBodyBehavior, context?.res?.status >= 400)) {
- context.log('Request body:', context.req?.body ? bodySanitizer(context.req?.body) : 'NO_REQ_BODY');
- context.log('Response body:', context.res?.body ? bodySanitizer(context.res?.body) : 'NO_RES_BODY');
+ if (shouldLog(logBodyBehavior, result.status ? result.status >= 400 : true)) {
+ context.log('Request body:', result.body ? bodySanitizer(result.body) : 'NO_REQ_BODY');
+ context.log('Response body:', result.body ? bodySanitizer(result.body) : 'NO_RES_BODY');
}
telemetryClient.trackRequest({
- name: context.executionContext.functionName,
- resultCode: context.res?.status ?? '0',
+ name: context.functionName,
+ resultCode: result.status ?? '0',
// important so that requests with a non-OK response show up as failed
- success: context.res?.status < 400 ?? 'true',
+ success: result.status ? result.status < 400 : true,
url: req.url,
duration: 0,
id: correlationContext?.operation?.id ?? 'UNDEFINED',
@@ -140,14 +159,22 @@ const finalizeAppInsightForHttpTrigger = async (
context.log('Finalized AppInsights');
};
-const finalizeAppInsightForNonHttpTrigger = async (context: Context): Promise => {
+const finalizeAppInsightForNonHttpTrigger: PostExecutionFunction = async (req, context, result) =>
+ finalizeAppInsightForNonHttpTriggerWithConfig(req, context, result, 'on_error', noOpLogDataSanitizer);
+const finalizeAppInsightForNonHttpTriggerWithConfig: FinalizeAppInsightWithConfig = async (
+ req,
+ context,
+ result,
+ logBodyBehavior: LogBehavior = 'on_error',
+ bodySanitizer: LogDataSanitizer = noOpLogDataSanitizer,
+): Promise => {
if (isDisabled) {
return;
}
const telemetryClient = telemetryClients[context.invocationId];
if (telemetryClient === undefined) {
- context.log.error(`No telemetry client could be found for invocationId ${context.invocationId}`);
+ context.error(`No telemetry client could be found for invocationId ${context.invocationId}`);
return;
}
@@ -158,11 +185,11 @@ const finalizeAppInsightForNonHttpTrigger = async (context: Context): Promise = {
+ setup: BeforeExecutionFunction;
+ finalize: PostExecutionFunction;
+ finalizeWithConfig: FinalizeAppInsightWithCurriedConfiguration;
+};
+
+const AppInsightForHttpTrigger: AppInsightsObject = {
setup: setupAppInsightForHttpTrigger,
- finalizeAppInsight: finalizeAppInsightForHttpTrigger,
- finalizeWithConfig:
- (logBodyBehavior: LogBehavior, bodySanitizer: LogDataSanitizer) => (context: Context, req: HttpRequest) =>
- finalizeAppInsightForHttpTrigger(context, req, logBodyBehavior, bodySanitizer),
+ finalize: finalizeAppInsightForHttpTrigger,
+ finalizeWithConfig: (logBodyBehavior: LogBehavior, bodySanitizer: LogDataSanitizer) => (req, context, res) =>
+ finalizeAppInsightForHttpTriggerWithConfig(req, context, res, logBodyBehavior, bodySanitizer),
};
-const AppInsightForNoNHttpTrigger = {
+const AppInsightForNoNHttpTrigger: AppInsightsObject = {
setup: setupAppInsightForNonHttpTrigger,
- finalizeAppInsight: finalizeAppInsightForNonHttpTrigger,
+ finalize: finalizeAppInsightForNonHttpTrigger,
+ finalizeWithConfig: (logBodyBehavior: LogBehavior, bodySanitizer: LogDataSanitizer) => (req, context, res) =>
+ finalizeAppInsightForHttpTriggerWithConfig(req, context, res, logBodyBehavior, bodySanitizer),
};
export { AppInsightForHttpTrigger, AppInsightForNoNHttpTrigger, LogBehavior, LogDataSanitizer };
diff --git a/src/error/errorHandler.test.ts b/src/error/errorHandler.test.ts
index b2e6bef..fd5c44c 100644
--- a/src/error/errorHandler.test.ts
+++ b/src/error/errorHandler.test.ts
@@ -1,16 +1,14 @@
-import { Context } from '@azure/functions';
+import { InvocationContext } 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();
+ const contextMock = mock();
beforeEach(() => {
jest.restoreAllMocks();
-
- contextMock.log.error = jest.fn();
});
test('return an provided ApplicationError', () => {
diff --git a/src/error/errorHandler.ts b/src/error/errorHandler.ts
index cc1b492..0d431d2 100644
--- a/src/error/errorHandler.ts
+++ b/src/error/errorHandler.ts
@@ -1,4 +1,4 @@
-import { Context } from '@azure/functions';
+import { InvocationContext } from '@azure/functions';
import { Options } from '../middleware';
import { stringify } from '../util/stringify';
@@ -6,26 +6,26 @@ import { ApplicationError } from './ApplicationError';
export const errorHandler = (
error: unknown,
- context: Context,
+ context: InvocationContext,
opts?: Options,
): {
[key: string]: unknown;
} => {
if (error instanceof ApplicationError) {
- context.log.error(`Received application error with message ${error.message}`);
+ context.error(`Received application error with message ${error.message}`);
return {
status: error.status,
- body: error.body,
+ jsonBody: error.body,
};
}
const errorAsString = stringify(error);
- context.log.error(errorAsString);
+ context.error(errorAsString);
if (opts?.errorResponseHandler === undefined) {
return {
status: 500,
- body: {
+ jsonBody: {
message: 'Internal server error',
},
};
diff --git a/src/headerAuthentication.test.ts b/src/headerAuthentication.test.ts
index 7573b47..f833262 100644
--- a/src/headerAuthentication.test.ts
+++ b/src/headerAuthentication.test.ts
@@ -1,12 +1,14 @@
-import { Context, HttpRequest } from '@azure/functions';
-import { mock } from 'jest-mock-extended';
+import { HttpHandler, HttpRequest, InvocationContext } from '@azure/functions';
+import { mock, mockDeep } from 'jest-mock-extended';
import { ApplicationError } from './error';
import sut from './headerAuthentication';
+import { MiddlewareResult } from './middleware';
describe('The header authentication middleware should', () => {
- const contextMock = mock();
- const requestMock = mock();
+ const contextMock = mock();
+ const requestMock = mockDeep();
+ const initialMiddlewareResult: MiddlewareResult> = { $failed: false, $result: undefined };
beforeEach(() => {
jest.restoreAllMocks();
@@ -14,13 +16,17 @@ describe('The header authentication middleware should', () => {
});
test('successfully resolves when the default "x-ms-client-principal" header is present', async () => {
- requestMock.headers['x-ms-client-principal-id'] = 'Test Principal';
+ requestMock.headers.get.mockImplementationOnce((name) =>
+ name === 'x-ms-client-principal-id' ? 'Test Principal' : null,
+ );
- await expect(sut()(contextMock, requestMock)).resolves.not.toThrow();
+ await expect(sut()(requestMock, contextMock, initialMiddlewareResult)).resolves.not.toThrow();
});
test('successfully resolves when the passed header validation function returns true', async () => {
- await expect(sut(() => true)(contextMock, requestMock)).resolves.not.toThrow();
+ await expect(
+ sut({ validateUsingHeaderFn: () => true })(requestMock, contextMock, initialMiddlewareResult),
+ ).resolves.not.toThrow();
});
test('fail caused by missing default "x-ms-client-principal" header', async () => {
@@ -29,7 +35,7 @@ describe('The header authentication middleware should', () => {
// @ts-ignore
requestMock.headers['x-ms-client-principal-id'] = undefined;
- await expect(sut()(contextMock, requestMock)).rejects.toEqual(
+ await expect(sut()(requestMock, contextMock, initialMiddlewareResult)).rejects.toEqual(
new ApplicationError('Authentication error', 403, 'No sophisticated credentials provided'),
);
});
@@ -41,19 +47,53 @@ describe('The header authentication middleware should', () => {
requestMock.headers['x-ms-client-principal-id'] = undefined;
await expect(
- sut(undefined, { error: 'Please authenticate properly' })(contextMock, requestMock),
+ sut({ errorResponseBody: { error: 'Please authenticate properly' } })(
+ requestMock,
+ contextMock,
+ initialMiddlewareResult,
+ ),
).rejects.toEqual(new ApplicationError('Authentication error', 403, { error: 'Please authenticate properly' }));
});
test('fail caused by passed header validation function returns false', async () => {
- await expect(sut(() => false)(contextMock, requestMock)).rejects.toEqual(
- new ApplicationError('Authentication error', 403, 'No sophisticated credentials provided'),
- );
+ await expect(
+ sut({ validateUsingHeaderFn: () => false })(requestMock, contextMock, initialMiddlewareResult),
+ ).rejects.toEqual(new ApplicationError('Authentication error', 403, 'No sophisticated credentials provided'));
});
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),
+ sut({ validateUsingHeaderFn: () => false, errorResponseBody: { error: 'Please authenticate properly' } })(
+ requestMock,
+ contextMock,
+ initialMiddlewareResult,
+ ),
+ ).rejects.toEqual(new ApplicationError('Authentication error', 403, { error: 'Please authenticate properly' }));
+ });
+
+ test('do nothing if the passed result indicates an error in a prev function and skipIfResultIsFaulty is true', async () => {
+ await expect(
+ sut({
+ validateUsingHeaderFn: () => false,
+ errorResponseBody: { error: 'Please authenticate properly' },
+ skipIfResultIsFaulty: true,
+ })(requestMock, contextMock, {
+ $failed: true,
+ $error: Error(),
+ }),
+ ).resolves.not.toThrow();
+ });
+
+ test('execute the function even if the passed result indicates an error in a prev function, but skipIfResultIsFaulty is false', async () => {
+ await expect(
+ sut({
+ validateUsingHeaderFn: () => false,
+ errorResponseBody: { error: 'Please authenticate properly' },
+ skipIfResultIsFaulty: false,
+ })(requestMock, contextMock, {
+ $failed: true,
+ $error: Error(),
+ }),
).rejects.toEqual(new ApplicationError('Authentication error', 403, { error: 'Please authenticate properly' }));
});
});
diff --git a/src/headerAuthentication.ts b/src/headerAuthentication.ts
index 9d78171..23d8ca8 100644
--- a/src/headerAuthentication.ts
+++ b/src/headerAuthentication.ts
@@ -1,39 +1,45 @@
-import { AzureFunction, Context, HttpRequest } from '@azure/functions';
-import { HttpRequestHeaders } from '@azure/functions/types/http';
+import { HttpHandler } from '@azure/functions';
+import { Headers } from 'undici';
import { ApplicationError } from './error';
+import { BeforeExecutionFunction } from './middleware';
-export default (
- validateUsingHeaderFn?: (headers: HttpRequestHeaders) => boolean,
- errorResponseBody?: unknown,
-): AzureFunction => {
- return (context: Context, req: HttpRequest): Promise => {
- if (validateUsingHeaderFn) {
- const validationResult = validateUsingHeaderFn(req.headers);
- if (validationResult) {
- return Promise.resolve();
- } else {
- return Promise.reject(
- new ApplicationError(
- 'Authentication error',
- 403,
- errorResponseBody ?? 'No sophisticated credentials provided',
- ),
- );
- }
+type ValidationFunction = (headers: Headers) => boolean;
+
+const defaultHeaderValidation: ValidationFunction = (headers) => !!headers.get('x-ms-client-principal-id');
+const defaultErrorResponseBody = 'No sophisticated credentials provided';
+
+export type HeaderAuthenticationOptions = {
+ validateUsingHeaderFn: ValidationFunction;
+ errorResponseBody: unknown;
+ skipIfResultIsFaulty: boolean;
+};
+
+export default (opts?: Partial): BeforeExecutionFunction => {
+ const validateUsingHeaderFn = opts?.validateUsingHeaderFn ?? defaultHeaderValidation;
+ const errorResponseBody = opts?.errorResponseBody ?? defaultErrorResponseBody;
+ const skipIfResultIsFaulty = opts?.skipIfResultIsFaulty ?? true;
+
+ return (req, context, result): Promise => {
+ if (skipIfResultIsFaulty && result.$failed) {
+ context.info('Skipping header-authentication because the result is faulty.');
+ return Promise.resolve();
+ }
+
+ context.info('Executing header authentication.');
+ const validationResult = validateUsingHeaderFn(req.headers);
+ if (validationResult) {
+ context.info('Header authentication was successful.');
+ return Promise.resolve();
} else {
- const authenticationHeader = req.headers['x-ms-client-principal-id'];
- if (!!authenticationHeader) {
- return Promise.resolve();
- } else {
- return Promise.reject(
- new ApplicationError(
- 'Authentication error',
- 403,
- errorResponseBody ?? 'No sophisticated credentials provided',
- ),
- );
- }
+ context.info('Header authentication was NOT successful.');
+ return Promise.reject(
+ new ApplicationError(
+ 'Authentication error',
+ 403,
+ errorResponseBody ?? 'No sophisticated credentials provided',
+ ),
+ );
}
};
};
diff --git a/src/jwtAuthorization.test.ts b/src/jwtAuthorization.test.ts
index 35fd997..09f4c7e 100644
--- a/src/jwtAuthorization.test.ts
+++ b/src/jwtAuthorization.test.ts
@@ -1,15 +1,17 @@
-import { Context, HttpRequest } from '@azure/functions';
-import { mock } from 'jest-mock-extended';
+import { HttpHandler, HttpRequest, InvocationContext } from '@azure/functions';
+import { mock, mockDeep } from 'jest-mock-extended';
import * as JWTDecoder from 'jwt-decode';
import { ApplicationError } from './error';
import sut from './jwtAuthorization';
+import { MiddlewareResult } from './middleware';
jest.mock('jwt-decode');
const jwtMock = JWTDecoder as jest.Mocked;
describe('The authorization middleware should', () => {
- const contextMock = mock();
- const requestMock = mock();
+ const contextMock = mock();
+ const requestMock = mockDeep();
+ const initialMiddlewareResult: MiddlewareResult> = { $failed: false, $result: undefined };
beforeEach(() => {
jest.restoreAllMocks();
@@ -17,7 +19,7 @@ describe('The authorization middleware should', () => {
});
test('successfully validate the passed authorization token', async () => {
- requestMock.headers.authorization = 'Bearer token';
+ requestMock.headers.get.mockImplementationOnce((name) => (name === 'authorization' ? 'Bearer token' : null));
jwtMock.jwtDecode.mockReturnValue('JWT-TEST');
await sut([
@@ -28,20 +30,21 @@ describe('The authorization middleware should', () => {
},
parameterExtractor: () => 'test',
},
- ])(contextMock, requestMock);
+ ])(requestMock, contextMock, initialMiddlewareResult);
- expect(contextMock.bindingData).toHaveProperty('jwt', 'JWT-TEST');
+ expect(contextMock.extraInputs).toHaveProperty('jwt', 'JWT-TEST');
});
test('fail caused by missing authorization header', async () => {
- requestMock.headers.authorization = '';
+ requestMock.headers.get.mockReturnValue(null);
const jwtExtractorMock = jest.fn();
const parameterExtractorMock = jest.fn();
await expect(
sut([{ jwtExtractor: jwtExtractorMock, parameterExtractor: parameterExtractorMock }])(
- contextMock,
requestMock,
+ contextMock,
+ initialMiddlewareResult,
),
).rejects.toEqual(new ApplicationError('Authorization error', 401));
@@ -49,14 +52,16 @@ describe('The authorization middleware should', () => {
});
test('fail caused by a incorrectly formatted authorization header', async () => {
- requestMock.headers.authorization = 'Bearer';
+ requestMock.headers.get.mockImplementationOnce((name) => (name === 'authorization' ? 'Bearer' : null));
+
const jwtExtractorMock = jest.fn();
const parameterExtractorMock = jest.fn();
await expect(
sut([{ jwtExtractor: jwtExtractorMock, parameterExtractor: parameterExtractorMock }])(
- contextMock,
requestMock,
+ contextMock,
+ initialMiddlewareResult,
),
).rejects.toEqual(new ApplicationError('Authorization error', 401));
@@ -64,7 +69,7 @@ describe('The authorization middleware should', () => {
});
test('fail caused by second rule failing and therefore chaining failed', async () => {
- requestMock.headers.authorization = 'Bearer token';
+ requestMock.headers.get.mockImplementationOnce((name) => (name === 'authorization' ? 'Bearer' : null));
jwtMock.jwtDecode.mockReturnValue('JWT-TEST');
await expect(
@@ -77,7 +82,7 @@ describe('The authorization middleware should', () => {
jwtExtractor: () => 'failed',
parameterExtractor: () => 'test',
},
- ])(contextMock, requestMock),
+ ])(requestMock, contextMock, initialMiddlewareResult),
).rejects.toEqual(new ApplicationError('Authorization error', 401, 'Unauthorized'));
});
});
diff --git a/src/jwtAuthorization.ts b/src/jwtAuthorization.ts
index b135eb2..aa51e3d 100644
--- a/src/jwtAuthorization.ts
+++ b/src/jwtAuthorization.ts
@@ -1,23 +1,40 @@
-import { AzureFunction, Context, ContextBindingData, HttpRequest } from '@azure/functions';
+import { HttpHandler } from '@azure/functions';
+import { HttpRequestParams } from '@azure/functions/types/http';
import { jwtDecode } from 'jwt-decode';
import { ApplicationError } from './error';
+import { BeforeExecutionFunction } from './middleware';
-const evaluate = (rule: Rule, parameters: ContextBindingData, jwt: T) => {
+const evaluate = (rule: Rule, parameters: HttpRequestParams, jwt: T) => {
const pathParameter = rule.parameterExtractor(parameters);
const jwtParameter = rule.jwtExtractor(jwt);
return pathParameter === jwtParameter;
};
export type Rule = {
- parameterExtractor: (parameters: ContextBindingData) => string;
+ parameterExtractor: (parameters: HttpRequestParams) => string;
jwtExtractor: (jwt: T) => string;
};
-export default (rules: Rule[], errorResponseBody?: unknown): AzureFunction => {
- return (context: Context, req: HttpRequest): Promise => {
- const authorizationHeader = req.headers.authorization;
- const parameters = context.bindingData;
+export type JwtAuthorizationOptions = {
+ skipIfResultIsFaulty: boolean;
+};
+
+export default (
+ rules: Rule[],
+ errorResponseBody?: unknown,
+ opts?: Partial,
+): BeforeExecutionFunction => {
+ const skipIfResultIsFaulty = opts?.skipIfResultIsFaulty ?? true;
+
+ return (req, context, result) => {
+ if (skipIfResultIsFaulty && result.$failed) {
+ context.info('Skipping jwt-authorization because the result is faulty.');
+ return Promise.resolve();
+ }
+
+ const authorizationHeader = req.headers.get('authorization');
+ const parameters = req.params;
if (authorizationHeader) {
const token = authorizationHeader.split(' ')[1];
if (token) {
@@ -30,7 +47,7 @@ export default (rules: Rule[], errorResponseBody?: unknown): AzureFunction
new ApplicationError('Authorization error', 401, errorResponseBody ?? 'Unauthorized'),
);
} else {
- context.bindingData = { ...context.bindingData, ...{ jwt } };
+ context.extraInputs = { ...context.extraInputs, ...{ jwt } };
return Promise.resolve();
}
}
diff --git a/src/middleware.test.ts b/src/middleware.test.ts
index f97e056..7b8c0aa 100644
--- a/src/middleware.test.ts
+++ b/src/middleware.test.ts
@@ -1,25 +1,23 @@
-import { Context, HttpRequest } from '@azure/functions';
+import { HttpRequest, InvocationContext } from '@azure/functions';
import { mock } from 'jest-mock-extended';
import { ApplicationError } from './error';
import { middleware as sut } from './middleware';
describe('The middleware layer should', () => {
- const contextMock = mock();
+ 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);
+ await sut([], handlerMock, [])(requestMock, contextMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
});
test('successfully call the middleware and the passed functions', async () => {
@@ -27,11 +25,11 @@ describe('The middleware layer should', () => {
const middlewareOneMock = jest.fn();
const middlewareTwoMock = jest.fn();
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock);
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(requestMock, contextMock);
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
});
test('successfully call the pre middleware and post middleware and the passed functions', async () => {
@@ -40,110 +38,110 @@ describe('The middleware layer should', () => {
const middlewareTwoMock = jest.fn();
const middlewarePostMock = jest.fn();
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [middlewarePostMock])(contextMock, requestMock);
+ await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [middlewarePostMock])(requestMock, contextMock);
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewarePostMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewarePostMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
});
- test('fail when one middleware is failing', async () => {
+ test('return an error when one middleware is failing', async () => {
const handlerMock = jest.fn();
const middlewareOneMock = jest.fn();
const middlewareTwoMock = jest.fn();
middlewareOneMock.mockRejectedValue(Error());
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock);
+ const res = await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(requestMock, contextMock);
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).not.toBeCalled();
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $error: Error(), $failed: true });
expect(handlerMock).not.toBeCalled();
- expect(contextMock.res).toEqual({
+ expect(res).toEqual({
status: 500,
body: {
message: 'Internal server error',
},
});
- expect(contextMock.log.error).toBeCalled();
+ expect(contextMock.error).toBeCalled();
});
- test('fail when the second middleware is failing', async () => {
+ test('return an error when second middleware is failing', async () => {
const handlerMock = jest.fn();
const middlewareOneMock = jest.fn();
const middlewareTwoMock = jest.fn();
middlewareTwoMock.mockRejectedValue(Error());
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock);
+ const res = await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(requestMock, contextMock);
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
expect(handlerMock).not.toBeCalled();
- expect(contextMock.res).toEqual({
+ expect(res).toEqual({
status: 500,
body: {
message: 'Internal server error',
},
});
- expect(contextMock.log.error).toBeCalled();
+ expect(contextMock.error).toBeCalled();
});
- test('fail when the handler function is failing', async () => {
+ test('return an error when the handler function is failing', async () => {
const handlerMock = jest.fn();
const middlewareOneMock = jest.fn();
const middlewareTwoMock = jest.fn();
handlerMock.mockRejectedValue(Error());
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock);
+ const res = await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(requestMock, contextMock);
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(contextMock.res).toEqual({
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
+ expect(res).toEqual({
status: 500,
body: {
message: 'Internal server error',
},
});
- expect(contextMock.log.error).toBeCalled();
+ expect(contextMock.error).toBeCalled();
});
- test('fail when the handler function is failing and a error with status is returned', async () => {
+ test('return an error 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 sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(contextMock, requestMock);
+ const res = await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [])(requestMock, contextMock);
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(contextMock.res).toEqual({
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
+ expect(res).toEqual({
status: 401,
body: 'test-body',
});
- expect(contextMock.log.error).toBeCalled();
+ expect(contextMock.error).toBeCalled();
});
- test('fail when the handler function is failing, but execute the post-execution-functions', async () => {
+ test('return an error if 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);
+ const res = await sut([middlewareOneMock], handlerMock, [middlewareTwoMock])(requestMock, contextMock);
- expect(middlewareOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(contextMock.res).toEqual({
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
+ expect(res).toEqual({
status: 500,
body: {
message: 'Internal server error',
},
});
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(contextMock.log.error).toBeCalled();
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $error: Error(), $failed: true });
+ expect(contextMock.error).toBeCalled();
});
test('use the provided error-handle to create a response', async () => {
@@ -160,92 +158,20 @@ describe('The middleware layer should', () => {
};
};
- await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [middlewarePostFunction], {
+ const res = await sut([middlewareOneMock, middlewareTwoMock], handlerMock, [middlewarePostFunction], {
errorResponseHandler,
- })(contextMock, requestMock);
+ })(requestMock, contextMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(contextMock.res).toEqual({
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
+ expect(res).toEqual({
status: 1337,
body: 'My custom error response',
});
- expect(contextMock.log.error).toBeCalled();
- // expect(middlewarePostFunction).toBeCalled(); TODO: Fix later
- });
-
- 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);
+ expect(contextMock.error).toBeCalled();
+ expect(middlewarePostFunction).toHaveBeenCalledWith(requestMock, contextMock, {
+ $error: Error(),
+ $failed: true,
+ });
});
test('dynamically filter items, which resolve to false', async () => {
@@ -260,12 +186,95 @@ describe('The middleware layer should', () => {
await sut([excludeFunction() && middlewareOneMock, middlewareTwoMock], handlerMock, [
includeFunction() && middlewarePostOneMock,
excludeFunction() && middlewarePostTwoMock,
- ])(contextMock, requestMock);
+ ])(requestMock, contextMock);
+
+ expect(middlewareOneMock).not.toHaveBeenCalled();
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
+ expect(middlewarePostOneMock).toHaveBeenCalledWith(requestMock, contextMock, {
+ $failed: false,
+ $result: undefined,
+ });
+ expect(middlewarePostTwoMock).not.toHaveBeenCalled();
+ });
+
+ describe('fail if error-handling is disabled and', () => {
+ test('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 })(
+ requestMock,
+ contextMock,
+ ),
+ ).rejects.toThrow();
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, {
+ $failed: true,
+ $error: Error(),
+ });
+ expect(handlerMock).not.toBeCalled();
+ });
- expect(middlewareOneMock).not.toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewareTwoMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewarePostOneMock).toHaveBeenCalledWith(contextMock, requestMock);
- expect(middlewarePostTwoMock).not.toHaveBeenCalledWith(contextMock, requestMock);
- expect(handlerMock).toHaveBeenCalledWith(contextMock, requestMock);
+ test('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 })(
+ requestMock,
+ contextMock,
+ ),
+ ).rejects.toThrow();
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).not.toBeCalled();
+ });
+
+ test('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 })(
+ requestMock,
+ contextMock,
+ ),
+ ).rejects.toThrow();
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
+ });
+
+ test('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 })(
+ requestMock,
+ contextMock,
+ ),
+ ).rejects.toThrow();
+
+ expect(middlewareOneMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(middlewareTwoMock).toHaveBeenCalledWith(requestMock, contextMock, { $failed: false });
+ expect(handlerMock).toHaveBeenCalledWith(requestMock, contextMock);
+ });
});
});
diff --git a/src/middleware.ts b/src/middleware.ts
index ba05513..d9924ca 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,106 +1,121 @@
-import { AzureFunction, Context } from '@azure/functions';
+import { FunctionHandler, HttpRequest, InvocationContext } from '@azure/functions';
import { errorHandler } from './error';
+import { stringify } from './util/stringify';
-type ErrorResult = { $failed: true; $error: unknown };
+export type ExceptionalResult = { $failed: true; $error: Error };
+export type MiddlewareResult = ExceptionalResult | { $failed: false; $result: Awaited };
-const isErrorResult = (result: unknown | ErrorResult): result is ErrorResult => (result as ErrorResult)?.$failed;
+export type BeforeExecutionFunction = T extends (...a: infer U) => infer R
+ ? (...a: [...U, MiddlewareResult]) => unknown
+ : never;
+
+export type PostExecutionFunction = BeforeExecutionFunction;
+
+export const isErrorResult = (result: MiddlewareResult | ExceptionalResult): result is ExceptionalResult =>
+ (result as ExceptionalResult)?.$failed;
const middlewareCore =
- (beforeExecution: (AzureFunction | false)[], handler: AzureFunction, postExecution: (AzureFunction | false)[]) =>
- async (context: Context, ...args: unknown[]): Promise => {
- let error = undefined;
+ (
+ beforeExecution: (BeforeExecutionFunction | false)[],
+ handler: T,
+ postExecution: (PostExecutionFunction | false)[],
+ ) =>
+ async (request: HttpRequest, context: InvocationContext): Promise | Error> => {
+ let handlerResult: MiddlewareResult> = { $failed: false, $result: undefined };
if (beforeExecution) {
const middlewareFunctions = beforeExecution.filter(
- (predicate): predicate is AzureFunction => predicate !== false,
+ (predicate): predicate is BeforeExecutionFunction => predicate !== false,
);
for (const middlewareFunction of middlewareFunctions) {
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 middlewareFunction(context, ...args);
- }
+ await middlewareFunction(request, context, handlerResult);
} catch (err) {
- error = err;
+ if (err instanceof Error) {
+ handlerResult = { $failed: true, $error: err };
+ }
}
}
}
- let handlerResult;
-
- if (error === undefined) {
+ if (!handlerResult.$failed) {
try {
- handlerResult = await handler(context, ...args);
+ handlerResult = { $failed: false, $result: await handler(request, context) };
} catch (err) {
- error = err;
+ if (err instanceof Error) {
+ handlerResult = { $failed: true, $error: err };
+ }
}
}
- // 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) {
const middlewareFunctions = postExecution.filter(
- (predicate): predicate is AzureFunction => predicate !== false,
+ (predicate): predicate is PostExecutionFunction => predicate !== false,
);
for (const middlewareFunction of middlewareFunctions) {
- await middlewareFunction(context, ...args);
+ await middlewareFunction(request, context, handlerResult);
}
}
- if (error !== undefined) {
- context.log.error(`An uncaught error occurred in the execution of the handler: ${error}`);
- return { $failed: true, $error: error };
+ if (isErrorResult(handlerResult)) {
+ context.error(`An uncaught error occurred in the execution of the handler: ${stringify(handlerResult)}`);
+ return handlerResult.$error;
}
- return handlerResult;
+
+ if (handlerResult.$result === undefined) {
+ return new Error('Illegal-State: Result of the handler was empty.');
+ }
+
+ return handlerResult.$result;
};
export type Options = {
errorResponseHandler?: (
error: unknown,
- context: Context,
+ context: InvocationContext,
) => {
[key: string]: unknown;
};
disableErrorHandling?: boolean;
};
-async function middlewareWrapper(
- beforeExecution: (AzureFunction | false)[],
- handler: (context: Context, ...args: unknown[]) => Promise | void,
- postExecution: (AzureFunction | false)[],
- context: Context,
- args: unknown[],
+async function middlewareWrapper(
+ beforeExecution: (BeforeExecutionFunction | false)[],
+ handler: T,
+ postExecution: (PostExecutionFunction | false)[],
+ request: any,
+ context: InvocationContext,
opts?: Options,
) {
- const result = await middlewareCore(beforeExecution, handler, postExecution)(context, ...args);
+ const result = await middlewareCore(beforeExecution, handler, postExecution)(request, context);
- if (isErrorResult(result)) {
+ if (result instanceof Error) {
if (opts?.disableErrorHandling) {
- throw result.$error;
+ throw result;
}
- context.res = errorHandler(result.$error, context, opts);
- return;
+ return errorHandler(result, context, opts);
}
return result;
}
export const middleware =
- (
- beforeExecution: (AzureFunction | false)[],
- handler: AzureFunction,
- postExecution: (AzureFunction | false)[],
+ (
+ beforeExecution: (BeforeExecutionFunction | false)[],
+ handler: T,
+ postExecution: (PostExecutionFunction | false)[],
opts?: Options,
) =>
- async (context: Context, ...args: unknown[]): Promise => {
+ async (request: HttpRequest, context: InvocationContext): Promise => {
if (opts?.disableErrorHandling) {
- return await middlewareWrapper(beforeExecution, handler, postExecution, context, args, opts);
+ return await middlewareWrapper(beforeExecution, handler, postExecution, request, context, opts);
}
try {
- return await middlewareWrapper(beforeExecution, handler, postExecution, context, args, opts);
+ return await middlewareWrapper(beforeExecution, handler, postExecution, request, context, opts);
} catch (error) {
- context.res = errorHandler(error, context, opts);
+ return errorHandler(error, context, opts);
}
};
diff --git a/src/validation.test.ts b/src/validation.test.ts
index 14d35a1..8fbfe8f 100644
--- a/src/validation.test.ts
+++ b/src/validation.test.ts
@@ -1,65 +1,70 @@
-import { Context, HttpRequest } from '@azure/functions';
+import { HttpHandler, HttpRequest, InvocationContext } from '@azure/functions';
import { mockDeep } from 'jest-mock-extended';
import Joi from 'joi';
import { ApplicationError } from './error';
+import { MiddlewareResult } from './middleware';
import { requestValidation, responseValidation } from './validation';
describe('The requestValidation should', () => {
const exampleSchema = Joi.object({ example: Joi.string().required() });
- const contextMock = mockDeep();
+ const contextMock = mockDeep();
const requestMock = mockDeep();
+ const initialMiddlewareResult: MiddlewareResult> = { $failed: false, $result: undefined };
- beforeEach(() => {
- contextMock.log.verbose = jest.fn();
- jest.restoreAllMocks();
- });
+ // https://github.com/marchaos/jest-mock-extended/issues/29
+ const requestMethodMock = jest.fn();
+ Object.defineProperty(requestMock, 'method', { get: requestMethodMock });
+
+ const requestBodyMock = jest.fn();
+ Object.defineProperty(requestMock, 'body', { get: requestBodyMock });
test('successfully validate the passed object', async () => {
- requestMock.method = 'POST';
- requestMock.body = { example: 'test-body' };
+ requestMethodMock.mockReturnValue('POST');
+ requestBodyMock.mockReturnValue({ example: 'test-body' });
- const result = await requestValidation(exampleSchema)(contextMock, requestMock);
+ const result = await requestValidation(exampleSchema)(requestMock, contextMock, initialMiddlewareResult);
expect(result).toBeUndefined();
});
test('successfully validate the object extracted through the passed function', async () => {
- requestMock.method = 'GET';
+ requestMethodMock.mockReturnValue('GET');
const result = await requestValidation(exampleSchema, {
extractValidationContentFromRequest: () => ({
example: 'test-extracted-content',
}),
- })(contextMock, requestMock);
+ })(requestMock, contextMock, initialMiddlewareResult);
expect(result).toBeUndefined();
});
test('fail when the validation was not successful', async () => {
- requestMock.method = 'POST';
- requestMock.body = 'test-body';
+ requestMethodMock.mockReturnValue('POST');
+ requestBodyMock.mockReturnValue('test-body');
- await expect(requestValidation(exampleSchema)(contextMock, requestMock)).rejects.toThrowError(
- new ApplicationError('Validation Error', 400),
- );
+ await expect(
+ requestValidation(exampleSchema)(requestMock, contextMock, initialMiddlewareResult),
+ ).rejects.toThrowError(new ApplicationError('Validation Error', 400));
});
test('do not throw an error, even when the validation was not successful, if throwing is disabled', async () => {
- requestMock.method = 'POST';
- requestMock.body = 'test-body';
-
- requestMock.method = 'POST';
- requestMock.body = 'test-body';
+ requestMethodMock.mockReturnValue('POST');
+ requestBodyMock.mockReturnValue({ example: 'test-body' });
await expect(
- requestValidation(exampleSchema, { shouldThrowOnValidationError: false })(contextMock, requestMock),
+ requestValidation(exampleSchema, { shouldThrowOnValidationError: false })(
+ requestMock,
+ contextMock,
+ initialMiddlewareResult,
+ ),
).resolves.toEqual(undefined);
});
test('fail when the validation was not successful with transformed error message', async () => {
- requestMock.method = 'POST';
- requestMock.body = { fail: 'test-body' };
+ requestMethodMock.mockReturnValue('POST');
+ requestBodyMock.mockReturnValue({ example: { fail: 'test-body' } });
await expect(
requestValidation(exampleSchema, {
@@ -67,79 +72,79 @@ describe('The requestValidation should', () => {
type: 'Validation Error',
message,
}),
- })(contextMock, requestMock),
+ })(requestMock, contextMock, initialMiddlewareResult),
).rejects.toThrowError(new ApplicationError('Validation Error', 400));
});
});
describe('The responseValidation should', () => {
+ const initialMiddlewareResult: MiddlewareResult> = { $failed: false, $result: undefined };
+
const exampleSchema = Joi.object({
status: Joi.number().required(),
- body: Joi.object({
+ jsonBody: Joi.object({
example: Joi.string().required(),
}),
});
- const contextMock = mockDeep();
+ const contextMock = mockDeep();
const requestMock = mockDeep();
beforeEach(() => {
- contextMock.log.verbose = jest.fn();
jest.restoreAllMocks();
});
test('do nothing, if the response is valid', async () => {
- contextMock.res = {};
- contextMock.res.status = 201;
- contextMock.res.body = { example: 'test-body' };
+ initialMiddlewareResult.$result = { status: 201, jsonBody: { example: 'test-body' } };
- const result = await responseValidation(exampleSchema)(contextMock, requestMock);
+ const result = await responseValidation(exampleSchema)(requestMock, contextMock, initialMiddlewareResult);
expect(result).toBeUndefined();
});
test('do nothing, if the object, extracted through the passed function, is valid', async () => {
- contextMock.res = {};
- contextMock.res.body = { example: 'test-body' };
+ initialMiddlewareResult.$result = { jsonBody: { example: 'test-body' } };
+ const extractValidationContentFromRequest = () => ({
+ status: 201,
+ jsonBody: { example: 'not-test-body' },
+ });
const result = await responseValidation(exampleSchema, {
- extractValidationContentFromRequest: (context) => ({ status: 201, body: context?.res?.body }),
- })(contextMock, requestMock);
+ extractValidationContentFromRequest,
+ })(requestMock, contextMock, initialMiddlewareResult);
expect(result).toBeUndefined();
});
test('throw, if the response is invalid', async () => {
- contextMock.res = {};
- contextMock.res.status = 201;
- contextMock.res.body = { fail: 'this fails' };
+ initialMiddlewareResult.$result = { status: 201, jsonBody: { fail: 'this-fails' } };
- await expect(responseValidation(exampleSchema)(contextMock, requestMock)).rejects.toThrowError(
- new ApplicationError('Internal server error', 500),
- );
+ await expect(
+ responseValidation(exampleSchema)(requestMock, contextMock, initialMiddlewareResult),
+ ).rejects.toThrowError(new ApplicationError('Internal server error', 500));
});
test('do not throw an error, even when the validation was not successful, if throwing is disabled', async () => {
- contextMock.res = {};
- contextMock.res.status = 201;
- contextMock.res.body = { fail: 'this fails' };
+ initialMiddlewareResult.$result = { status: 201, jsonBody: { fail: 'this-fails' } };
await expect(
- responseValidation(exampleSchema, { shouldThrowOnValidationError: false })(contextMock, requestMock),
+ responseValidation(exampleSchema, { shouldThrowOnValidationError: false })(
+ requestMock,
+ contextMock,
+ initialMiddlewareResult,
+ ),
).resolves.toEqual(undefined);
});
test('fail when the validation was not successful with transformed error message', async () => {
- contextMock.res = {};
- contextMock.res.status = 201;
- contextMock.res.body = { fail: 'this fails' };
+ initialMiddlewareResult.$result = { status: 201, jsonBody: { fail: 'this-fails' } };
await expect(
responseValidation(exampleSchema, {
- transformErrorMessage: (message) => ({
+ transformErrorMessage: (message: string) => ({
type: 'Validation Error',
message,
}),
- })(contextMock, requestMock),
+ })(requestMock, contextMock, initialMiddlewareResult),
).rejects.toThrowError(new ApplicationError('Internal server error', 500));
});
});
diff --git a/src/validation.ts b/src/validation.ts
index 8761aa0..97e2eb2 100644
--- a/src/validation.ts
+++ b/src/validation.ts
@@ -1,57 +1,97 @@
-import { AzureFunction, Context, HttpRequest } from '@azure/functions';
+import { FunctionHandler, FunctionResult, HttpHandler, HttpRequest, InvocationContext } from '@azure/functions';
import { AnySchema } from 'joi';
import { ApplicationError } from './error';
+import { BeforeExecutionFunction, MiddlewareResult, PostExecutionFunction, isErrorResult } from './middleware';
+import { stringify } from './util/stringify';
type ValidationOptions = Partial<{
transformErrorMessage: (message: string) => unknown;
- extractValidationContentFromRequest: (context: Context, req: HttpRequest) => unknown;
+ extractValidationContentFromRequest: (
+ req: HttpRequest,
+ context: InvocationContext,
+ result: MiddlewareResult>,
+ ) => unknown;
shouldThrowOnValidationError: boolean;
+ skipIfResultIsFaulty: boolean;
+ printRequest: boolean;
}>;
-export function requestValidation(schema: AnySchema, opts?: ValidationOptions): AzureFunction {
- return (context: Context, req: HttpRequest): Promise => {
- context.log.verbose('Validating the request.');
+export function requestValidation(schema: AnySchema, opts?: ValidationOptions): BeforeExecutionFunction {
+ const skipIfResultIsFaulty = opts?.skipIfResultIsFaulty ?? true;
+ const printRequest = opts?.printRequest ?? false;
- const toBeValidatedContent = opts?.extractValidationContentFromRequest?.(context, req) ?? req.body;
- const shouldThrowOnValidationError = opts?.shouldThrowOnValidationError ?? true;
+ return async (req, context, result): Promise => {
+ if (skipIfResultIsFaulty && result.$failed) {
+ context.info('Skipping request-validation because the result is faulty.');
+ return Promise.resolve();
+ }
- const validationResult = schema.validate(toBeValidatedContent);
- if (validationResult && validationResult.error) {
- context.log.error('The request did not match the given schema.');
- context.log.verbose(validationResult);
+ const clonedRequest = req.clone(); //see https://github.com/nuxt/nuxt/issues/19245
+ context.info('Validating the request.');
+ try {
+ const toBeValidatedContent =
+ opts?.extractValidationContentFromRequest?.(clonedRequest, context, result) ??
+ (await clonedRequest.json());
+ const shouldThrowOnValidationError = opts?.shouldThrowOnValidationError ?? true;
- if (shouldThrowOnValidationError) {
+ const validationResult = schema.validate(toBeValidatedContent);
+ if (validationResult && validationResult.error) {
+ context.error(
+ `The request did not match the given schema.${printRequest ? stringify(toBeValidatedContent) : ''}`,
+ );
+ context.info(validationResult);
+
+ if (shouldThrowOnValidationError) {
+ return Promise.reject(
+ new ApplicationError(
+ 'Validation Error',
+ 400,
+ opts?.transformErrorMessage
+ ? opts?.transformErrorMessage(validationResult.error.message)
+ : {
+ message: validationResult.error.message,
+ },
+ ),
+ );
+ }
+ }
+
+ context.info('Finished validating the request.');
+ return Promise.resolve();
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ context.error(`The Json was probably ill-defined: ${error}`);
+ //see https://fetch.spec.whatwg.org/#dom-body-json
return Promise.reject(
- new ApplicationError(
- 'Validation Error',
- 400,
- opts?.transformErrorMessage
- ? opts?.transformErrorMessage(validationResult.error.message)
- : {
- message: validationResult.error.message,
- },
- ),
+ new ApplicationError('Validation Error', 400, {
+ message: error.message,
+ }),
);
}
+ context.error(`Unexpected server error occurred: ${error}`);
+ return Promise.reject(
+ new ApplicationError('Internal Server Error', 500, {
+ message: 'An internal Server Error occurred while validating the request.',
+ }),
+ );
}
-
- context.log.verbose('Finished validating the request.');
- return Promise.resolve();
};
}
-export function responseValidation(schema: AnySchema, opts?: ValidationOptions): AzureFunction {
- return (context: Context, req: HttpRequest): Promise => {
- context.log.verbose('Validating the server-response.');
+export function responseValidation(schema: AnySchema, opts?: ValidationOptions): PostExecutionFunction {
+ return (req, context, result): Promise => {
+ context.info('Validating the server-response.');
- const toBeValidatedContent = opts?.extractValidationContentFromRequest?.(context, req) ?? context.res;
+ const toBeValidatedContent =
+ opts?.extractValidationContentFromRequest?.(req, context, result) ??
+ (isErrorResult>(result) ? result.$error : result.$result);
const shouldThrowOnValidationError = opts?.shouldThrowOnValidationError ?? true;
const validationResult = schema.validate(toBeValidatedContent);
if (validationResult && validationResult.error) {
- context.log.error('The response did not match the given schema.');
- context.log.verbose(validationResult);
+ context.error('The response did not match the given schema.');
+ context.info(validationResult);
if (shouldThrowOnValidationError) {
return Promise.reject(
@@ -68,7 +108,7 @@ export function responseValidation(schema: AnySchema, opts?: ValidationOptions):
}
}
- context.log.verbose('Finished validating the response.');
+ context.info('Finished validating the response.');
return Promise.resolve();
};
}