Repository: Azure/azure-functions-durable-js
Pull request #136 (2020-01-07 06:15:27)

Fix tokenSource format issue in callHttp API
There was an issue with the `callHttp` API where it didn't correctly encode token source values in a way that was understood by the Durable extension. If a user tried to use managed identities, they would see an error similar to the following:

> Could not create an instance of type Microsoft.Azure.WebJobs.Extensions.DurableTask.ITokenSource. Type is an interface or abstract class and cannot be instantiated. Path 'resource', line 11, position 25."

The problem was that the JSON returned by Durable JS that represented a `callHttp` action was missing the `kind` field that tells the durable extension parser how to interpret the token source data. The implementation of this field made incorrect assumptions about how TypeScript works. This field has been fixed.

The unit test for this was unfortunately flawed. It verified that `callHttp` produced the right TypeScript output but it failed to verify whether that TypeScript object correctly represented the JSON expected by the durable extension. I've fixed this test as well as part of the PR.

**Pending tasks**
- [x] Test end-to-end on Functions 2.0 host
- [x] Add sample function to demonstrate `callHttp` usage

FYI @mhoeger this is the issue you ran into!

cgillum: Thanks for the review. In the latest iteration of this PR I've added a sample that shows how to fetch the list of all Azure Subscriptions available to the current identity.

Patch:
diff --git a/samples/ListAzureSubscriptions/function.json b/samples/ListAzureSubscriptions/function.json
new file mode 100644
index 0000000..7d95b29
--- /dev/null
+++ b/samples/ListAzureSubscriptions/function.json
@@ -0,0 +1,9 @@
+{
+  "bindings": [
+  {
+    "name": "context",
+    "type": "orchestrationTrigger",
+    "direction": "in"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/samples/ListAzureSubscriptions/index.js b/samples/ListAzureSubscriptions/index.js
new file mode 100644
index 0000000..6604cea
--- /dev/null
+++ b/samples/ListAzureSubscriptions/index.js
@@ -0,0 +1,13 @@
+const df = require("durable-functions");
+
+module.exports = df.orchestrator(function*(context) {
+    // More information on the use of managed identities in the callHttp API:
+    // https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-http-features#managed-identities
+    var res = yield context.df.callHttp(
+        "GET",
+        "https://management.azure.com/subscriptions?api-version=2019-06-01",
+        undefined,
+        undefined,
+        new df.ManagedIdentityTokenSource("https://management.core.windows.net"));
+    return res;
+});
diff --git a/src/durableorchestrationcontext.ts b/src/durableorchestrationcontext.ts
index 58a00ad..4fd1581 100644
--- a/src/durableorchestrationcontext.ts
+++ b/src/durableorchestrationcontext.ts
@@ -1,5 +1,5 @@
-import { DurableHttpRequest, DurableLock, EntityId, ITaskMethods, LockState,
-     RetryOptions, Task, TimerTask } from "./classes";
+import { EntityId, ITaskMethods, RetryOptions, Task, TimerTask } from "./classes";
+import { TokenSource } from "./tokensource";
 
 /**
  * Parameter data for orchestration bindings that can be used to schedule
@@ -130,7 +130,12 @@ export class DurableOrchestrationContext {
      *
      * @param req The durable HTTP request to schedule.
      */
-    public callHttp(req: DurableHttpRequest): Task {
+    public callHttp(
+            method: string,
+            uri: string,
+            content?: string | object,
+            headers?: { [key: string]: string },
+            tokenSource?: TokenSource): Task {
         throw new Error("This is a placeholder");
     }
 
diff --git a/src/tokensource.ts b/src/tokensource.ts
index 928bd0a..c27a7b0 100644
--- a/src/tokensource.ts
+++ b/src/tokensource.ts
@@ -17,7 +17,7 @@
  */
 export class ManagedIdentityTokenSource {
     /** @hidden */
-    public kind: "AzureManagedIdentity";
+    public readonly kind: string = "AzureManagedIdentity";
 
     /**
      * Returns a `ManagedIdentityTokenSource` object.


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index 9aa2b2f..82e9f81 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -709,15 +709,26 @@ describe("Orchestrator", () => {
 
             orchestrator(mockContext);
 
+            // This is the exact protocol expected by the durable extension
             expect(mockContext.doneValue).to.be.deep.equal(
-                new OrchestratorState({
+                {
                     isDone: false,
                     output: undefined,
                     actions:
-                    [
-                        [ new CallHttpAction(req) ],
-                    ],
-                }),
+                    [[{
+                        actionType: 8,
+                        httpRequest: {
+                            method: req.method,
+                            uri: req.uri,
+                            content: req.content,
+                            headers: req.headers,
+                            tokenSource: {
+                                resource: "https://management.core.windows.net",
+                                kind: "AzureManagedIdentity",
+                            },
+                        },
+                    }]],
+                },
             );
         });
 
diff --git a/test/testobjects/TestOrchestrations.ts b/test/testobjects/TestOrchestrations.ts
index d3cbffc..0b80a3e 100644
--- a/test/testobjects/TestOrchestrations.ts
+++ b/test/testobjects/TestOrchestrations.ts
@@ -1,4 +1,5 @@
 import * as df from "../../src";
+import { IOrchestrationFunctionContext } from "../../src/classes";
 
 export class TestOrchestrations {
     public static AnyAOrB: any = df.orchestrator(function*(context: any) {
@@ -182,7 +183,7 @@ export class TestOrchestrations {
         return output;
     });
 
-    public static SendHttpRequest: any = df.orchestrator(function*(context: any) {
+    public static SendHttpRequest: any = df.orchestrator(function*(context: IOrchestrationFunctionContext) {
         const input = context.df.getInput() as df.DurableHttpRequest;
         const output = yield context.df.callHttp(
             input.method,