Skip to content

Commit 068dcf4

Browse files
committed
Make code more unit-testable for user's apps
By doing a few things: - Remove enums from @azure/functions-core api - Expose constructors for context and request for use in unit tests - Allow running in a "test mode" where the core api doesn't exist
1 parent 6475037 commit 068dcf4

File tree

11 files changed

+172
-189
lines changed

11 files changed

+172
-189
lines changed

src/InvocationContext.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Licensed under the MIT License.
33

44
import * as types from '@azure/functions';
5-
import { RetryContext, TraceContext, TriggerMetadata } from '@azure/functions';
6-
import { RpcInvocationRequest, RpcLog } from '@azure/functions-core';
5+
import { InvocationContextInit, LogHandler, RetryContext, TraceContext, TriggerMetadata } from '@azure/functions';
6+
import { RpcInvocationRequest } from '@azure/functions-core';
77
import { convertKeysToCamelCase } from './converters/convertKeysToCamelCase';
88
import { fromRpcRetryContext, fromRpcTraceContext } from './converters/fromRpcContext';
99

@@ -15,50 +15,48 @@ export class InvocationContext implements types.InvocationContext {
1515
retryContext?: RetryContext;
1616
extraInputs: InvocationContextExtraInputs;
1717
extraOutputs: InvocationContextExtraOutputs;
18-
#userLogCallback: UserLogCallback;
18+
#userLogHandler: LogHandler;
1919

20-
constructor(functionName: string, request: RpcInvocationRequest, userLogCallback: UserLogCallback) {
21-
this.invocationId = <string>request.invocationId;
22-
this.functionName = functionName;
23-
this.triggerMetadata = request.triggerMetadata ? convertKeysToCamelCase(request.triggerMetadata) : {};
24-
if (request.retryContext) {
25-
this.retryContext = fromRpcRetryContext(request.retryContext);
20+
constructor(init: InvocationContextInit & RpcInvocationRequest) {
21+
this.invocationId = init.invocationId;
22+
this.functionName = init.functionName;
23+
this.triggerMetadata = init.triggerMetadata ? convertKeysToCamelCase(init.triggerMetadata) : {};
24+
if (init.retryContext) {
25+
this.retryContext = fromRpcRetryContext(init.retryContext);
2626
}
27-
if (request.traceContext) {
28-
this.traceContext = fromRpcTraceContext(request.traceContext);
27+
if (init.traceContext) {
28+
this.traceContext = fromRpcTraceContext(init.traceContext);
2929
}
30-
this.#userLogCallback = userLogCallback;
30+
this.#userLogHandler = init.logHandler;
3131
this.extraInputs = new InvocationContextExtraInputs();
3232
this.extraOutputs = new InvocationContextExtraOutputs();
3333
}
3434

3535
log(...args: unknown[]): void {
36-
this.#userLogCallback(RpcLog.Level.Information, ...args);
36+
this.#userLogHandler('information', ...args);
3737
}
3838

3939
trace(...args: unknown[]): void {
40-
this.#userLogCallback(RpcLog.Level.Trace, ...args);
40+
this.#userLogHandler('trace', ...args);
4141
}
4242

4343
debug(...args: unknown[]): void {
44-
this.#userLogCallback(RpcLog.Level.Debug, ...args);
44+
this.#userLogHandler('debug', ...args);
4545
}
4646

4747
info(...args: unknown[]): void {
48-
this.#userLogCallback(RpcLog.Level.Information, ...args);
48+
this.#userLogHandler('information', ...args);
4949
}
5050

5151
warn(...args: unknown[]): void {
52-
this.#userLogCallback(RpcLog.Level.Warning, ...args);
52+
this.#userLogHandler('warning', ...args);
5353
}
5454

5555
error(...args: unknown[]): void {
56-
this.#userLogCallback(RpcLog.Level.Error, ...args);
56+
this.#userLogHandler('error', ...args);
5757
}
5858
}
5959

60-
type UserLogCallback = (level: RpcLog.Level, ...args: unknown[]) => void;
61-
6260
class InvocationContextExtraInputs implements types.InvocationContextExtraInputs {
6361
#inputs: Record<string, unknown> = {};
6462
get(inputOrName: types.FunctionInput | string): any {

src/InvocationModel.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
InvocationArguments,
99
RpcBindingInfo,
1010
RpcInvocationResponse,
11-
RpcLog,
11+
RpcLogCategory,
12+
RpcLogLevel,
1213
RpcTypedData,
1314
} from '@azure/functions-core';
1415
import { format } from 'util';
@@ -35,11 +36,12 @@ export class InvocationModel implements coreTypes.InvocationModel {
3536

3637
// eslint-disable-next-line @typescript-eslint/require-await
3738
async getArguments(): Promise<InvocationArguments> {
38-
const context = new InvocationContext(
39-
this.#functionName,
40-
this.#coreCtx.request,
41-
(level: RpcLog.Level, ...args: unknown[]) => this.#userLog(level, ...args)
42-
);
39+
const context = new InvocationContext({
40+
...this.#coreCtx.request,
41+
invocationId: nonNullProp(this.#coreCtx, 'invocationId'),
42+
functionName: this.#functionName,
43+
logHandler: (level: RpcLogLevel, ...args: unknown[]) => this.#userLog(level, ...args),
44+
});
4345

4446
const inputs: any[] = [];
4547
if (this.#coreCtx.request.inputData) {
@@ -83,7 +85,7 @@ export class InvocationModel implements coreTypes.InvocationModel {
8385

8486
response.outputData = [];
8587
for (const [name, binding] of Object.entries(this.#bindings)) {
86-
if (binding.direction === RpcBindingInfo.Direction.out) {
88+
if (binding.direction === 'out') {
8789
if (name === returnBindingKey) {
8890
response.returnValue = this.#convertOutput(binding, result);
8991
} else {
@@ -106,21 +108,21 @@ export class InvocationModel implements coreTypes.InvocationModel {
106108
}
107109
}
108110

109-
#log(level: RpcLog.Level, logCategory: RpcLog.RpcLogCategory, ...args: unknown[]): void {
111+
#log(level: RpcLogLevel, logCategory: RpcLogCategory, ...args: unknown[]): void {
110112
this.#coreCtx.log(level, logCategory, format(...args));
111113
}
112114

113-
#systemLog(level: RpcLog.Level, ...args: unknown[]) {
114-
this.#log(level, RpcLog.RpcLogCategory.System, ...args);
115+
#systemLog(level: RpcLogLevel, ...args: unknown[]) {
116+
this.#log(level, 'system', ...args);
115117
}
116118

117-
#userLog(level: RpcLog.Level, ...args: unknown[]): void {
119+
#userLog(level: RpcLogLevel, ...args: unknown[]): void {
118120
if (this.#isDone && this.#coreCtx.state !== 'postInvocationHooks') {
119121
let badAsyncMsg =
120122
"Warning: Unexpected call to 'log' on the context object after function execution has completed. Please check for asynchronous calls that are not awaited. ";
121123
badAsyncMsg += `Function name: ${this.#functionName}. Invocation Id: ${this.#coreCtx.invocationId}.`;
122-
this.#systemLog(RpcLog.Level.Warning, badAsyncMsg);
124+
this.#systemLog('warning', badAsyncMsg);
123125
}
124-
this.#log(level, RpcLog.RpcLogCategory.User, ...args);
126+
this.#log(level, 'user', ...args);
125127
}
126128
}

src/converters/toRpcHttpCookie.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { Cookie } from '@azure/functions';
5-
import { RpcHttpCookie } from '@azure/functions-core';
5+
import { RpcHttpCookie, RpcHttpCookieSameSite } from '@azure/functions-core';
66
import { toNullableBool, toNullableDouble, toNullableString, toNullableTimestamp, toRpcString } from './toRpcNullable';
77

88
/**
@@ -11,15 +11,15 @@ import { toNullableBool, toNullableDouble, toNullableString, toNullableTimestamp
1111
*/
1212
export function toRpcHttpCookie(inputCookie: Cookie): RpcHttpCookie {
1313
// Resolve RpcHttpCookie.SameSite enum, a one-off
14-
let rpcSameSite: RpcHttpCookie.SameSite = RpcHttpCookie.SameSite.None;
14+
let rpcSameSite: RpcHttpCookieSameSite = 'none';
1515
if (inputCookie && inputCookie.sameSite) {
1616
const sameSite = inputCookie.sameSite.toLocaleLowerCase();
1717
if (sameSite === 'lax') {
18-
rpcSameSite = RpcHttpCookie.SameSite.Lax;
18+
rpcSameSite = 'lax';
1919
} else if (sameSite === 'strict') {
20-
rpcSameSite = RpcHttpCookie.SameSite.Strict;
20+
rpcSameSite = 'strict';
2121
} else if (sameSite === 'none') {
22-
rpcSameSite = RpcHttpCookie.SameSite.ExplicitNone;
22+
rpcSameSite = 'explicitNone';
2323
}
2424
}
2525

src/http/HttpRequest.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,17 @@ export class HttpRequest implements types.HttpRequest {
2323

2424
#cachedUser?: HttpRequestUser | null;
2525
#uReq: uRequest;
26-
#body?: Buffer;
26+
#body?: Buffer | string;
2727

2828
constructor(rpcHttp: RpcHttpData) {
2929
const url = nonNullProp(rpcHttp, 'url');
30-
this.#body = rpcHttp.body?.bytes ? Buffer.from(rpcHttp.body?.bytes) : undefined;
30+
31+
if (rpcHttp.body?.bytes) {
32+
this.#body = Buffer.from(rpcHttp.body?.bytes);
33+
} else if (rpcHttp.body?.string) {
34+
this.#body = rpcHttp.body.string;
35+
}
36+
3137
this.#uReq = new uRequest(url, {
3238
body: this.#body,
3339
method: nonNullProp(rpcHttp, 'method'),

src/index.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,23 @@ import {
4646
TimerTriggerOptions,
4747
} from '@azure/functions';
4848
import * as coreTypes from '@azure/functions-core';
49-
import {
50-
CoreInvocationContext,
51-
FunctionCallback,
52-
registerFunction,
53-
RpcBindingInfo,
54-
setProgrammingModel,
55-
} from '@azure/functions-core';
49+
import { CoreInvocationContext, FunctionCallback } from '@azure/functions-core';
5650
import { returnBindingKey, version } from './constants';
5751
import { InvocationModel } from './InvocationModel';
5852
import { getRandomHexString } from './utils/getRandomHexString';
5953

54+
export { HttpRequest } from './http/HttpRequest';
55+
export { InvocationContext } from './InvocationContext';
56+
57+
function tryGetCoreApiLazy(): typeof coreTypes | undefined {
58+
try {
59+
// eslint-disable-next-line @typescript-eslint/no-var-requires
60+
return <typeof coreTypes>require('@azure/functions-core');
61+
} catch {
62+
return undefined;
63+
}
64+
}
65+
6066
class ProgrammingModel implements coreTypes.ProgrammingModel {
6167
name = '@azure/functions';
6268
version = version;
@@ -67,7 +73,14 @@ class ProgrammingModel implements coreTypes.ProgrammingModel {
6773

6874
let hasSetup = false;
6975
function setup() {
70-
setProgrammingModel(new ProgrammingModel());
76+
const coreApi = tryGetCoreApiLazy();
77+
if (!coreApi) {
78+
console.warn(
79+
'WARNING: Failed to detect the Azure Functions runtime. Switching "@azure/functions" package to test mode - not all features are supported.'
80+
);
81+
} else {
82+
coreApi.setProgrammingModel(new ProgrammingModel());
83+
}
7184
hasSetup = true;
7285
}
7386

@@ -193,20 +206,20 @@ export namespace app {
193206
setup();
194207
}
195208

196-
const bindings = {};
209+
const bindings: Record<string, coreTypes.RpcBindingInfo> = {};
197210

198211
const trigger = options.trigger;
199212
bindings[trigger.name] = {
200213
...trigger,
201-
direction: RpcBindingInfo.Direction.in,
214+
direction: 'in',
202215
type: /trigger$/i.test(trigger.type) ? trigger.type : trigger.type + 'Trigger',
203216
};
204217

205218
if (options.extraInputs) {
206219
for (const input of options.extraInputs) {
207220
bindings[input.name] = {
208221
...input,
209-
direction: RpcBindingInfo.Direction.in,
222+
direction: 'in',
210223
};
211224
}
212225
}
@@ -215,20 +228,27 @@ export namespace app {
215228
options.return.name = returnBindingKey;
216229
bindings[options.return.name] = {
217230
...options.return,
218-
direction: RpcBindingInfo.Direction.out,
231+
direction: 'out',
219232
};
220233
}
221234

222235
if (options.extraOutputs) {
223236
for (const output of options.extraOutputs) {
224237
bindings[output.name] = {
225238
...output,
226-
direction: RpcBindingInfo.Direction.out,
239+
direction: 'out',
227240
};
228241
}
229242
}
230243

231-
registerFunction({ name, bindings }, <FunctionCallback>options.handler);
244+
const coreApi = tryGetCoreApiLazy();
245+
if (!coreApi) {
246+
console.warn(
247+
`WARNING: Skipping call to register function "${name}" because the "@azure/functions" package is in test mode.`
248+
);
249+
} else {
250+
coreApi.registerFunction({ name, bindings }, <FunctionCallback>options.handler);
251+
}
232252
}
233253
}
234254

test/converters/toRpcHttpCookie.test.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Licensed under the MIT License.
33

44
import { Cookie } from '@azure/functions';
5-
import { RpcHttpCookie } from '@azure/functions-core';
65
import { expect } from 'chai';
76
import 'mocha';
87
import { toRpcHttpCookie } from '../../src/converters/toRpcHttpCookie';
@@ -68,16 +67,16 @@ describe('toRpcHttpCookie', () => {
6867

6968
const rpcCookies = cookieInputs.map(toRpcHttpCookie);
7069
expect(rpcCookies[0].name).to.equal('none-cookie');
71-
expect(rpcCookies[0].sameSite).to.equal(RpcHttpCookie.SameSite.ExplicitNone);
70+
expect(rpcCookies[0].sameSite).to.equal('explicitNone');
7271

7372
expect(rpcCookies[1].name).to.equal('lax-cookie');
74-
expect(rpcCookies[1].sameSite).to.equal(RpcHttpCookie.SameSite.Lax);
73+
expect(rpcCookies[1].sameSite).to.equal('lax');
7574

7675
expect(rpcCookies[2].name).to.equal('strict-cookie');
77-
expect(rpcCookies[2].sameSite).to.equal(RpcHttpCookie.SameSite.Strict);
76+
expect(rpcCookies[2].sameSite).to.equal('strict');
7877

7978
expect(rpcCookies[3].name).to.equal('default-cookie');
80-
expect(rpcCookies[3].sameSite).to.equal(RpcHttpCookie.SameSite.None);
79+
expect(rpcCookies[3].sameSite).to.equal('none');
8180
});
8281

8382
it('throws on invalid input', () => {

test/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@
44
import * as globby from 'globby';
55
import * as Mocha from 'mocha';
66
import * as path from 'path';
7-
import { setupTestCoreApi } from './setupTestCoreApi';
87

98
export async function run(): Promise<void> {
109
try {
11-
setupTestCoreApi();
12-
1310
const options: Mocha.MochaOptions = {
1411
color: true,
1512
reporter: 'mocha-multi-reporters',

test/setupTestCoreApi.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)