diff --git a/README.md b/README.md index b89e652..df69430 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Here is the general flow of the pipeline: ## Running locally +### Install core tools + +1. Run `./scripts/install-func-cli.ps1` in PowerShell to install the latest nightly build of Azure Functions Core Tools + ### Build 1. Run `npm run install` and `npm run build` in the root directory, and in the test app folders (`app/v3` and `app/v4`) diff --git a/app/v4/src/functions/httpTriggerForHooks.ts b/app/v4/src/functions/httpTriggerForHooks.ts new file mode 100644 index 0000000..1be5185 --- /dev/null +++ b/app/v4/src/functions/httpTriggerForHooks.ts @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; + +export async function httpTriggerForHooks( + request: HttpRequest, + extraInput: string, + context: InvocationContext +): Promise { + context.log(`httpTriggerForHooks was triggered with second input ${extraInput}`); + return { body: 'hookBodyResponse' }; +} + +app.http('httpTriggerForHooks', { + methods: ['GET', 'POST'], + authLevel: 'anonymous', + handler: httpTriggerForHooks, +}); diff --git a/app/v4/src/index.ts b/app/v4/src/index.ts index e5d1eeb..2bc7044 100644 --- a/app/v4/src/index.ts +++ b/app/v4/src/index.ts @@ -1,9 +1,65 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { registerHook } from '@azure/functions-core'; +import { app } from '@azure/functions'; -registerHook('postInvocation', async () => { +app.hook.appStart((context) => { + context.hookData.testData = 'appStartHookData123'; + console.log('appStart hook executed.'); +}); + +app.hook.appTerminate((context) => { + console.log(`appTerminate hook executed with hook data ${context.hookData.testData}`); +}); + +app.hook.preInvocation((context) => { + if (context.invocationContext.functionName === 'httpTriggerForHooks') { + context.invocationContext.log( + `preInvocation hook executed with inputs ${JSON.stringify(context.inputs.map((v) => v.constructor.name))}` + ); + + context.inputs.push('extraTestInput12345'); + + const oldHandler: any = context.functionHandler; + context.functionHandler = (...args) => { + context.invocationContext.log('extra log from updated functionHandler'); + return oldHandler(...args); + }; + + context.hookData.testData = 'preInvocationHookData123'; + + // Validate readonly properties + try { + // @ts-expect-error by-design + context.hookData = {}; + } catch (err) { + context.invocationContext.log(`Ignored error: ${err.message}`); + } + + try { + // @ts-expect-error by-design + context.invocationContext = {}; + } catch (err) { + context.invocationContext.log(`Ignored error: ${err.message}`); + } + } +}); + +const hookToDispose = app.hook.preInvocation((context) => { + context.invocationContext.log('This should never run.'); +}); +hookToDispose.dispose(); + +app.hook.postInvocation((context) => { + if (context.invocationContext.functionName === 'httpTriggerForHooks') { + const resultString = JSON.stringify(context.result); + context.invocationContext.log( + `postInvocation hook executed with error: ${context.error}, result: ${resultString}, hook data: ${context.hookData.testData}` + ); + } +}); + +app.hook.postInvocation(async () => { // Add slight delay to ensure logs show up before the invocation finishes // See these issues for more info: // https://github.com/Azure/azure-functions-host/issues/9238 diff --git a/src/hooks.test.ts b/src/hooks.test.ts new file mode 100644 index 0000000..69a89b5 --- /dev/null +++ b/src/hooks.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { default as fetch } from 'node-fetch'; +import { getFuncUrl } from './constants'; +import { model, waitForOutput } from './global.test'; + +describe('hooks', () => { + before(function (this: Mocha.Context) { + if (model === 'v3') { + this.skip(); + } + }); + + it('app start', async () => { + await waitForOutput(`Executing 1 "appStart" hooks`, { checkFullOutput: true }); + await waitForOutput(`appStart hook executed.`, { checkFullOutput: true }); + await waitForOutput(`Executed "appStart" hooks`, { checkFullOutput: true }); + }); + + it('invocation', async () => { + const outputUrl = getFuncUrl('httpTriggerForHooks'); + + const response = await fetch(outputUrl, { method: 'POST' }); + expect(response.status).to.equal(200); + const body = await response.text(); + expect(body).to.equal('hookBodyResponse'); + + // pre invocation + await waitForOutput(`Executing 1 "preInvocation" hooks`); + await waitForOutput(`preInvocation hook executed with inputs ["HttpRequest"]`); + await waitForOutput(`Ignored error: Cannot assign to read only property 'hookData'`); + await waitForOutput(`Ignored error: Cannot assign to read only property 'invocationContext'`); + await waitForOutput(`Executed "preInvocation" hooks`); + + // invocation + await waitForOutput(`extra log from updated functionHandler`); + await waitForOutput(`httpTriggerForHooks was triggered with second input extraTestInput12345`); + + // post invocation + await waitForOutput(`Executing 2 "postInvocation" hooks`); + await waitForOutput( + `postInvocation hook executed with error: null, result: {"body":"hookBodyResponse"}, hook data: preInvocationHookData123` + ); + await waitForOutput(`Executed "postInvocation" hooks`); + }); +});