From 9f3d17f092c93d783f2c7c9e543a74cd160aaefc Mon Sep 17 00:00:00 2001 From: mhoeger Date: Thu, 21 Nov 2019 13:59:10 -0800 Subject: [PATCH 01/11] updates to make 2.0 worker 1.0 compatible --- src/WorkerChannel.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index c6c4aae6..8727fbfd 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -92,7 +92,11 @@ export class WorkerChannel implements IWorkerChannel { */ public workerInitRequest(requestId: string, msg: rpc.WorkerInitRequest) { // TODO: add capability from host to go to "non-breaking" mode +<<<<<<< HEAD if (msg.capabilities && msg.capabilities.V2Compatable) { +======= + if (msg.hostVersion) { +>>>>>>> updates to make 2.0 worker 1.0 compatible this._v1WorkerBehavior = true; } From 2c217c9906aed09b8bc0fa336ddbf92417bf1c0f Mon Sep 17 00:00:00 2001 From: mhoeger Date: Thu, 21 Nov 2019 16:07:02 -0800 Subject: [PATCH 02/11] split validation logic so it's both v1 and v2 compatible --- src/WorkerChannel.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index 8727fbfd..5d518037 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -92,27 +92,19 @@ export class WorkerChannel implements IWorkerChannel { */ public workerInitRequest(requestId: string, msg: rpc.WorkerInitRequest) { // TODO: add capability from host to go to "non-breaking" mode -<<<<<<< HEAD if (msg.capabilities && msg.capabilities.V2Compatable) { -======= - if (msg.hostVersion) { ->>>>>>> updates to make 2.0 worker 1.0 compatible this._v1WorkerBehavior = true; } // Validate version let version = process.version; if (this._v1WorkerBehavior) { - if (version.startsWith("v12.")) - { + if (version.startsWith("v12.")) { systemWarn("The Node.js version you are using (" + version + ") is not fully supported with Azure Functions V2. We recommend using one the following major versions: 8, 10."); - } - } else { - if (version.startsWith("v8.")) - { + } else if (version.startsWith("v8.")) { let msg = "Incompatible Node.js version. The version you are using (" + version + ") is not supported with Azure Functions V3. Please use one of the following major versions: 10, 12."; systemError(msg); - throw msg; + throw new Error(msg); } } From f57858a12ca74dfdef050ae825f13a50da426802 Mon Sep 17 00:00:00 2001 From: mhoeger Date: Thu, 21 Nov 2019 17:01:45 -0800 Subject: [PATCH 03/11] hard-coded solution to camelCase timer trigger while proper fix is out of scope for timeline --- src/Context.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Context.ts b/src/Context.ts index c13918cd..5e3d23a6 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -20,7 +20,11 @@ export function CreateContextAndInputs(info: FunctionInfo, request: rpc.IInvocat input = httpInput = fromRpcHttp(binding.data.http); } else { // TODO: Don't hard code fix for camelCase https://github.com/Azure/azure-functions-nodejs-worker/issues/188 +<<<<<<< HEAD if (v1WorkerBehavior && info.getTimerTriggerName() === binding.name) { +======= + if (info.getTimerTriggerName() === binding.name) { +>>>>>>> hard-coded solution to camelCase timer trigger while proper fix is out of scope for timeline input = convertKeysToCamelCase(binding)["data"]; } else { input = fromTypedData(binding.data); From 35337f6b05f7338386001beeb386a39e0febdb1b Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Mon, 25 Nov 2019 14:59:09 -0700 Subject: [PATCH 04/11] added test --- .../Constants.cs | 1 + .../StorageEndToEndTests.cs | 7 +++++++ .../TimerTriggerQueueOutput/function.json | 18 ++++++++++++++++++ .../TimerTriggerQueueOutput/index.js | 11 +++++++++++ 4 files changed, 37 insertions(+) create mode 100644 test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json create mode 100644 test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js diff --git a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs index da2b6e9d..4f22ad2d 100644 --- a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs +++ b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs @@ -14,6 +14,7 @@ public static class Queue { public static string StorageConnectionStringSetting = Environment.GetEnvironmentVariable("AzureWebJobsStorage"); public static string OutputBindingName = "test-output-node"; public static string InputBindingName = "test-input-node"; + public static string TimerOutputBindingName = "timer-output-node"; } // CosmosDB tests diff --git a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs index 646a4fc5..af5dcbf9 100644 --- a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs +++ b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs @@ -33,5 +33,12 @@ public async Task QueueTriggerAndOutput_Succeeds() var queueMessage = await StorageHelpers.ReadFromQueue(Constants.Queue.OutputBindingName); Assert.Equal(expectedQueueMessage, queueMessage); } + + [Fact] + public async Task TimerTriggerRunsOnStartup() + { + var queueMessage = await StorageHelpers.ReadFromQueue(Constants.Queue.TimerOutputBindingName); + Assert.True(String.Equal(queueMessage, "true") || String.Equal(queueMessage, "false")); + } } } \ No newline at end of file diff --git a/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json b/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json new file mode 100644 index 00000000..910fb151 --- /dev/null +++ b/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json @@ -0,0 +1,18 @@ +{ + "bindings": [ + { + "name": "myTimer", + "type": "timerTrigger", + "direction": "in", + "schedule": "0 0 0 1 1 0", + "runOnStartup": true + }, + { + "name": "$return", + "type": "queue", + "direction": "out", + "queueName": "test-output-node", + "connection": "AzureWebJobsStorage" + } + ] +} \ No newline at end of file diff --git a/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js b/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js new file mode 100644 index 00000000..37f69277 --- /dev/null +++ b/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js @@ -0,0 +1,11 @@ +module.exports = async function (context, myTimer) { + var timeStamp = new Date().toISOString(); + + if (myTimer.isPastDue) + { + context.log('JavaScript is running late!'); + } + context.log('JavaScript timer trigger function ran!', timeStamp); + + return myTimer.isPastDue; +}; \ No newline at end of file From ec52d57fb6306969b7000a4fa1dfe440fd074037 Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Mon, 25 Nov 2019 16:16:39 -0700 Subject: [PATCH 05/11] adding unit test --- src/Context.ts | 4 ---- test/ContextTests.ts | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Context.ts b/src/Context.ts index 5e3d23a6..c13918cd 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -20,11 +20,7 @@ export function CreateContextAndInputs(info: FunctionInfo, request: rpc.IInvocat input = httpInput = fromRpcHttp(binding.data.http); } else { // TODO: Don't hard code fix for camelCase https://github.com/Azure/azure-functions-nodejs-worker/issues/188 -<<<<<<< HEAD if (v1WorkerBehavior && info.getTimerTriggerName() === binding.name) { -======= - if (info.getTimerTriggerName() === binding.name) { ->>>>>>> hard-coded solution to camelCase timer trigger while proper fix is out of scope for timeline input = convertKeysToCamelCase(binding)["data"]; } else { input = fromTypedData(binding.data); diff --git a/test/ContextTests.ts b/test/ContextTests.ts index a497f796..f3bf0071 100644 --- a/test/ContextTests.ts +++ b/test/ContextTests.ts @@ -26,7 +26,11 @@ describe('Context', () => { _context = context; }); +<<<<<<< HEAD it ('camelCases timer trigger input when appropriate', async () => { +======= + it ('camelCases timer trigger input', async () => { +>>>>>>> adding unit test var inputDataValue: rpc.IParameterBinding = { name: "myTimer", data: { From 051ab2711558eb39d2c644bc7ef2d2d0e5863721 Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Mon, 25 Nov 2019 16:27:46 -0700 Subject: [PATCH 06/11] testing both v1 behavior and v2 behavior --- test/ContextTests.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/ContextTests.ts b/test/ContextTests.ts index f3bf0071..a497f796 100644 --- a/test/ContextTests.ts +++ b/test/ContextTests.ts @@ -26,11 +26,7 @@ describe('Context', () => { _context = context; }); -<<<<<<< HEAD it ('camelCases timer trigger input when appropriate', async () => { -======= - it ('camelCases timer trigger input', async () => { ->>>>>>> adding unit test var inputDataValue: rpc.IParameterBinding = { name: "myTimer", data: { From 3505bd3b64c6efb0eff03a38183889b43b6ac5f0 Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Mon, 25 Nov 2019 16:29:29 -0700 Subject: [PATCH 07/11] dont code without intellisense --- .../Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs index af5dcbf9..4d4b3bf4 100644 --- a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs +++ b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs @@ -38,7 +38,7 @@ public async Task QueueTriggerAndOutput_Succeeds() public async Task TimerTriggerRunsOnStartup() { var queueMessage = await StorageHelpers.ReadFromQueue(Constants.Queue.TimerOutputBindingName); - Assert.True(String.Equal(queueMessage, "true") || String.Equal(queueMessage, "false")); + Assert.True(string.Equals(queueMessage, "true") || string.Equals(queueMessage, "false")); } } } \ No newline at end of file From 0be167eb93ed3415b169aa1f1c8c7d37fc37bde1 Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Mon, 25 Nov 2019 16:52:44 -0700 Subject: [PATCH 08/11] remove e2e test until have better test on functions host v2 and v3 --- .../Constants.cs | 1 - .../StorageEndToEndTests.cs | 7 ------- .../TimerTriggerQueueOutput/function.json | 18 ------------------ .../TimerTriggerQueueOutput/index.js | 11 ----------- 4 files changed, 37 deletions(-) delete mode 100644 test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json delete mode 100644 test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js diff --git a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs index 4f22ad2d..da2b6e9d 100644 --- a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs +++ b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/Constants.cs @@ -14,7 +14,6 @@ public static class Queue { public static string StorageConnectionStringSetting = Environment.GetEnvironmentVariable("AzureWebJobsStorage"); public static string OutputBindingName = "test-output-node"; public static string InputBindingName = "test-input-node"; - public static string TimerOutputBindingName = "timer-output-node"; } // CosmosDB tests diff --git a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs index 4d4b3bf4..646a4fc5 100644 --- a/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs +++ b/test/end-to-end/Azure.Functions.NodejsWorker.E2E/Azure.Functions.NodejsWorker.E2E/StorageEndToEndTests.cs @@ -33,12 +33,5 @@ public async Task QueueTriggerAndOutput_Succeeds() var queueMessage = await StorageHelpers.ReadFromQueue(Constants.Queue.OutputBindingName); Assert.Equal(expectedQueueMessage, queueMessage); } - - [Fact] - public async Task TimerTriggerRunsOnStartup() - { - var queueMessage = await StorageHelpers.ReadFromQueue(Constants.Queue.TimerOutputBindingName); - Assert.True(string.Equals(queueMessage, "true") || string.Equals(queueMessage, "false")); - } } } \ No newline at end of file diff --git a/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json b/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json deleted file mode 100644 index 910fb151..00000000 --- a/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/function.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "bindings": [ - { - "name": "myTimer", - "type": "timerTrigger", - "direction": "in", - "schedule": "0 0 0 1 1 0", - "runOnStartup": true - }, - { - "name": "$return", - "type": "queue", - "direction": "out", - "queueName": "test-output-node", - "connection": "AzureWebJobsStorage" - } - ] -} \ No newline at end of file diff --git a/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js b/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js deleted file mode 100644 index 37f69277..00000000 --- a/test/end-to-end/testFunctionApp/TimerTriggerQueueOutput/index.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = async function (context, myTimer) { - var timeStamp = new Date().toISOString(); - - if (myTimer.isPastDue) - { - context.log('JavaScript is running late!'); - } - context.log('JavaScript timer trigger function ran!', timeStamp); - - return myTimer.isPastDue; -}; \ No newline at end of file From 874cc711617dc05660446e27c443f5f659a7977f Mon Sep 17 00:00:00 2001 From: mhoeger Date: Mon, 25 Nov 2019 12:18:30 -0800 Subject: [PATCH 09/11] properly convert data when defined as output array --- src/WorkerChannel.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index 5d518037..f7532d15 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -188,10 +188,22 @@ export class WorkerChannel implements IWorkerChannel { response.returnValue = toTypedData(result.return); } else { let returnBinding = info.getReturnBinding(); - response.returnValue = returnBinding ? returnBinding.converter(result.return) : toTypedData(result.return); + // $return binding is found: return result data to $return binding + if (returnBinding) { + response.returnValue = returnBinding.converter(result.return); + // $return binding is not found: read result as object of outputs + } else if (result.return) { + response.outputData = Object.keys(info.outputBindings) + .filter(key => result.return[key] !== undefined) + .map(key => { + name: key, + data: info.outputBindings[key].converter(result.bindings[key]) + }); + } } } - if (result.bindings) { + // Data from return supersedes data from context.bindings + if (result.bindings && !response.outputData) { response.outputData = Object.keys(info.outputBindings) .filter(key => result.bindings[key] !== undefined) .map(key => { From d575c4811352049d2510d2b8cd2885d361696214 Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Wed, 27 Nov 2019 12:28:15 -0700 Subject: [PATCH 10/11] fix worker validation logic and warning message on ts changes --- scripts/generateProtos.js | 4 +++- src/WorkerChannel.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/generateProtos.js b/scripts/generateProtos.js index 314d89ce..14d7adbe 100644 --- a/scripts/generateProtos.js +++ b/scripts/generateProtos.js @@ -22,7 +22,9 @@ async function generateProtos() { .catch(err => console.log(`Could not compile to JavaScript: ${err}`)); // Don't generate with Node.js v12 until resolved: https://github.com/protobufjs/protobuf.js/issues/1275 - if (!process.version.startsWith("v12")) { + if (process.version.startsWith("v12") && process.platform === 'win32') { + console.warn("Warning! Could not compile to TypeScript for Node.js 12 and Windows OS. Do not change public interfaces."); + } else { genTs(allFiles) .then(data => console.log("Compiled to TypeScript.")) .catch(err => console.log(`Could not compile to TypeScript: ${err}`)); diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index f7532d15..ae7df3e4 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -101,7 +101,9 @@ export class WorkerChannel implements IWorkerChannel { if (this._v1WorkerBehavior) { if (version.startsWith("v12.")) { systemWarn("The Node.js version you are using (" + version + ") is not fully supported with Azure Functions V2. We recommend using one the following major versions: 8, 10."); - } else if (version.startsWith("v8.")) { + } + } else { + if (version.startsWith("v8.")) { let msg = "Incompatible Node.js version. The version you are using (" + version + ") is not supported with Azure Functions V3. Please use one of the following major versions: 10, 12."; systemError(msg); throw new Error(msg); From 773b315d81ef8d8a06e7bfc3bcbcab6428be874f Mon Sep 17 00:00:00 2001 From: Marie Hoeger Date: Wed, 27 Nov 2019 17:39:27 -0700 Subject: [PATCH 11/11] add tests --- src/WorkerChannel.ts | 5 +- test/WorkerChannelTests.ts | 295 ++++++++++++++++++++++++++++--------- 2 files changed, 228 insertions(+), 72 deletions(-) diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index ae7df3e4..93016d70 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -106,7 +106,7 @@ export class WorkerChannel implements IWorkerChannel { if (version.startsWith("v8.")) { let msg = "Incompatible Node.js version. The version you are using (" + version + ") is not supported with Azure Functions V3. Please use one of the following major versions: 10, 12."; systemError(msg); - throw new Error(msg); + throw msg; } } @@ -199,7 +199,7 @@ export class WorkerChannel implements IWorkerChannel { .filter(key => result.return[key] !== undefined) .map(key => { name: key, - data: info.outputBindings[key].converter(result.bindings[key]) + data: info.outputBindings[key].converter(result.return[key]) }); } } @@ -217,7 +217,6 @@ export class WorkerChannel implements IWorkerChannel { } catch (e) { response.result = this.getStatus(e) } - this._eventStream.write({ requestId: requestId, invocationResponse: response diff --git a/test/WorkerChannelTests.ts b/test/WorkerChannelTests.ts index 3c8a72ae..7727bfc9 100644 --- a/test/WorkerChannelTests.ts +++ b/test/WorkerChannelTests.ts @@ -6,12 +6,64 @@ import * as sinon from 'sinon'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; import 'mocha'; import { load } from 'grpc'; +import { FunctionInfo } from '../src/FunctionInfo'; describe('WorkerChannel', () => { - var channel: WorkerChannel; - var stream: TestEventStream; - var loader: sinon.SinonStubbedInstance; - var functions; + let channel: WorkerChannel; + let stream: TestEventStream; + let loader: sinon.SinonStubbedInstance; + let functions; + const getTriggerDataMock: () => { [k: string]: rpc.ITypedData } = () => { + return { + "Headers": { + json: JSON.stringify({Connection: 'Keep-Alive'}) + }, + "Sys": { + json: JSON.stringify({MethodName: 'test-js', UtcNow: '2018', RandGuid: '3212'}) + } + } + }; + const httpInputData = { + name: "req", + data: { + data: "http", + http: + { + body: + { + data: "string", + body: "blahh" + }, + rawBody: + { + data: "string", + body: "blahh" + } + } + } + }; + const httpInputBinding = { + type: "http", + direction: 0, + dataType: 1 + }; + const httpOutputBinding = { + type: "http", + direction: 1, + dataType: 1 + }; + const httpReturnBinding = { + bindings: { + req: httpInputBinding, + $return: httpOutputBinding + } + }; + const httpResBinding = { + bindings: { + req: httpInputBinding, + res: httpOutputBinding + } + }; beforeEach(() => { stream = new TestEventStream(); @@ -27,6 +79,7 @@ describe('WorkerChannel', () => { } }; + // Test Functions Host V2 compatability with Node.js 8 if (process.version.startsWith("v8")) { initMessage.workerInitRequest.capabilities["V2Compatable"] = "true"; } @@ -227,41 +280,12 @@ describe('WorkerChannel', () => { name: 'test', outputBindings: {} }) - - var triggerDataMock: { [k: string]: rpc.ITypedData } = { - "Headers": { - json: JSON.stringify({Connection: 'Keep-Alive'}) - }, - "Sys": { - json: JSON.stringify({MethodName: 'test-js', UtcNow: '2018', RandGuid: '3212'}) - } - }; - - var inputDataValue = { - name: "req", - data: { - data: "http", - http: - { - body: - { - data: "string", - body: "blahh" - }, - rawBody: - { - data: "string", - body: "blahh" - } - } - } - }; var actualInvocationRequest: rpc.IInvocationRequest = { functionId: 'id', invocationId: '1', - inputData: [inputDataValue], - triggerMetadata: triggerDataMock, + inputData: [httpInputData], + triggerMetadata: getTriggerDataMock(), }; stream.addTestMessage({ @@ -271,7 +295,7 @@ describe('WorkerChannel', () => { V2Compatable: "true" } } - }) + }); stream.addTestMessage({ invocationRequest: actualInvocationRequest @@ -288,8 +312,8 @@ describe('WorkerChannel', () => { }); // triggerMedata will be augmented with inpuDataValue since "RpcHttpTriggerMetadataRemoved" capability is set to true and therefore not populated by the host. - expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.$request)).to.equal(JSON.stringify(inputDataValue.data)); - expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.req)).to.equal(JSON.stringify(inputDataValue.data)); + expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.$request)).to.equal(JSON.stringify(httpInputData.data)); + expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.req)).to.equal(JSON.stringify(httpInputData.data)); }); it ('invokes function', () => { @@ -298,41 +322,124 @@ describe('WorkerChannel', () => { name: 'test', outputBindings: {} }) - - var triggerDataMock: { [k: string]: rpc.ITypedData } = { - "Headers": { - json: JSON.stringify({Connection: 'Keep-Alive'}) - }, - "Sys": { - json: JSON.stringify({MethodName: 'test-js', UtcNow: '2018', RandGuid: '3212'}) - } + + var actualInvocationRequest: rpc.IInvocationRequest = { + functionId: 'id', + invocationId: '1', + inputData: [httpInputData], + triggerMetadata: getTriggerDataMock(), }; - var inputDataValue = { - name: "req", - data: { - data: "http", - http: - { - body: - { - data: "string", - body: "blahh" - }, - rawBody: - { - data: "string", - body: "blahh" - } + stream.addTestMessage({ + invocationRequest: actualInvocationRequest + }); + + sinon.assert.calledWithMatch(stream.written, { + invocationResponse: { + invocationId: '1', + result: { + status: rpc.StatusResult.Status.Success + }, + outputData: [] + } + }); + + // triggerMedata will not be augmented with inpuDataValue since we are running Functions Host V3 compatability. + expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.$request)).to.be.undefined; + expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.req)).to.be.undefined; + }); + + it ('returns correct data with $return binding', () => { + let httpResponse; + loader.getFunc.returns((context) => { httpResponse = context.res; context.done(null, { body: { hello: "world" }})}); + loader.getInfo.returns(new FunctionInfo(httpReturnBinding)); + + var actualInvocationRequest: rpc.IInvocationRequest = { + functionId: 'id', + invocationId: '1', + inputData: [httpInputData], + triggerMetadata: getTriggerDataMock(), + }; + + stream.addTestMessage({ + invocationRequest: actualInvocationRequest + }); + + sinon.assert.calledWithMatch(stream.written, { + invocationResponse: { + invocationId: '1', + result: { + status: rpc.StatusResult.Status.Success + }, + outputData: [{ + data: { + http: httpResponse + }, + name: "$return" + }], + returnValue: { + http: { + body: { json: "{\"hello\":\"world\"}" }, + cookies: [], + headers: { }, + statusCode: undefined } - } + } + } + }); + }); + + it ('returns string data with $return binding and V2 compat', () => { + let httpResponse; + loader.getFunc.returns((context) => { httpResponse = context.res; context.done(null, { body: { hello: "world" }})}); + loader.getInfo.returns(new FunctionInfo(httpReturnBinding)); + + stream.addTestMessage({ + workerInitRequest: { + hostVersion: "3.0.0000", + capabilities: { + V2Compatable: "true" + } + } + }); + + var actualInvocationRequest: rpc.IInvocationRequest = { + functionId: 'id', + invocationId: '1', + inputData: [httpInputData], + triggerMetadata: getTriggerDataMock(), }; + stream.addTestMessage({ + invocationRequest: actualInvocationRequest + }); + + sinon.assert.calledWithMatch(stream.written, { + invocationResponse: { + invocationId: '1', + result: { + status: rpc.StatusResult.Status.Success + }, + outputData: [{ + data: { + http: httpResponse + }, + name: "$return" + }], + returnValue: { json: "{\"body\":{\"hello\":\"world\"}}" } + } + }); + }); + + it ('serializes output binding data through context.done', () => { + loader.getFunc.returns((context) => context.done(null, { res: { body: { hello: "world" }}})); + loader.getInfo.returns(new FunctionInfo(httpResBinding)); + var actualInvocationRequest: rpc.IInvocationRequest = { functionId: 'id', invocationId: '1', - inputData: [inputDataValue], - triggerMetadata: triggerDataMock, + inputData: [httpInputData], + triggerMetadata: getTriggerDataMock(), }; stream.addTestMessage({ @@ -345,13 +452,63 @@ describe('WorkerChannel', () => { result: { status: rpc.StatusResult.Status.Success }, - outputData: [] + outputData: [{ + data: { + http: { + body: { json: "{\"hello\":\"world\"}" }, + cookies: [], + headers: { }, + statusCode: undefined + } + }, + name: "res" + }] } }); + }); - // triggerMedata will be augmented with inpuDataValue since "RpcHttpTriggerMetadataRemoved" capability is set to true and therefore not populated by the host. - expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.$request)).to.be.undefined; - expect(JSON.stringify(actualInvocationRequest.triggerMetadata!.req)).to.be.undefined; + it ('serializes output binding data through context.done with V2 compat', () => { + let httpResponse; + loader.getFunc.returns((context) => { httpResponse = context.res; context.done(null, { res: { body: { hello: "world" }}})}); + loader.getInfo.returns(new FunctionInfo(httpResBinding)); + + stream.addTestMessage({ + workerInitRequest: { + hostVersion: "3.0.0000", + capabilities: { + V2Compatable: "true" + } + } + }); + + var actualInvocationRequest: rpc.IInvocationRequest = { + functionId: 'id', + invocationId: '1', + inputData: [httpInputData], + triggerMetadata: getTriggerDataMock(), + }; + + stream.addTestMessage({ + invocationRequest: actualInvocationRequest + }); + + sinon.assert.calledWithMatch(stream.written, { + invocationResponse: { + invocationId: '1', + result: { + status: rpc.StatusResult.Status.Success + }, + outputData: [{ + data: { + http: httpResponse + }, + name: "res" + }], + returnValue: { + json: "{\"res\":{\"body\":{\"hello\":\"world\"}}}" + } + } + }); }); it ('throws for malformed messages', () => {