add complete invocation test for ngrok (failing for now)
diff --git a/package-lock.json b/package-lock.json
index f2f7890..64dbc66 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2222,6 +2222,33 @@
                 }
             }
         },
+        "mock-require": {
+            "version": "3.0.3",
+            "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz",
+            "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==",
+            "dev": true,
+            "requires": {
+                "get-caller-file": "^1.0.2",
+                "normalize-path": "^2.1.1"
+            },
+            "dependencies": {
+                "get-caller-file": {
+                    "version": "1.0.3",
+                    "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+                    "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+                    "dev": true
+                },
+                "normalize-path": {
+                    "version": "2.1.1",
+                    "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+                    "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
+                    "dev": true,
+                    "requires": {
+                        "remove-trailing-separator": "^1.0.1"
+                    }
+                }
+            }
+        },
         "ms": {
             "version": "2.1.2",
             "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -2833,6 +2860,12 @@
                 "es6-error": "^4.0.1"
             }
         },
+        "remove-trailing-separator": {
+            "version": "1.1.0",
+            "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+            "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
+            "dev": true
+        },
         "request": {
             "version": "2.88.2",
             "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
diff --git a/package.json b/package.json
index 4e43645..801767f 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
         "get-port": "^5.1.1",
         "mocha": "^7.1.0",
         "mocha-multi-reporters": "^1.1.7",
+        "mock-require": "^3.0.3",
         "nock": "^12.0.2",
         "nyc": "^15.0.0",
         "strip-ansi": "^6.0.0",
diff --git a/src/agentmgr.js b/src/agentmgr.js
index 3264cc1..c1aa673 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -17,7 +17,7 @@
 
 'use strict';
 
-const NgrokAgent = require('./ngrok');
+const NgrokAgent = require('./agents/ngrok');
 const fs = require('fs-extra');
 const sleep = require('util').promisify(setTimeout);
 
diff --git a/src/ngrok.js b/src/agents/ngrok.js
similarity index 97%
rename from src/ngrok.js
rename to src/agents/ngrok.js
index 4551ef9..d778b57 100644
--- a/src/ngrok.js
+++ b/src/agents/ngrok.js
@@ -63,7 +63,7 @@
 
         console.log(`Ngrok forwarding: ${ngrokUrl} => http://localhost:${this.ngrokServerPort} (auth: ${this.ngrokAuth})`);
 
-        return fs.readFileSync(`${__dirname}/../agent/agent-ngrok.js`, {encoding: 'utf8'});
+        return fs.readFileSync(`${__dirname}/../../agent/agent-ngrok.js`, {encoding: 'utf8'});
     }
 
     async stop() {
diff --git a/test/ngrok.test.js b/test/ngrok.test.js
index fc4a033..0ae21b3 100644
--- a/test/ngrok.test.js
+++ b/test/ngrok.test.js
@@ -19,11 +19,30 @@
 
 'use strict';
 
-const Debugger = require("../src/debugger");
-
 const test = require('./test');
+let Debugger = require("../src/debugger");
+
 const assert = require('assert');
 const nock = require('nock');
+const fetch = require('isomorphic-fetch');
+const mockRequire = require('mock-require');
+
+function mockNgrokLibrary(connect, kill) {
+    mockRequire("ngrok", {
+        connect: connect || function() {
+            console.log('ngrok.connect called');
+        },
+        kill: kill || function() {
+            console.log('ngrok.kill called');
+        }
+    });
+    // the modules have been loaded from another test file before,
+    // so we need to re-require them in the reverse order
+    // to make the mockRequire("ngrok") have an effect
+    mockRequire.reRequire("../src/agents/ngrok");
+    mockRequire.reRequire("../src/agentmgr");
+    Debugger = mockRequire.reRequire("../src/debugger");
+}
 
 describe('ngrok',  function() {
     this.timeout(30000);
@@ -67,11 +86,113 @@
         const dbgr = new Debugger(argv);
         await dbgr.start();
         // no need to run() for this test
-        dbgr.run();
         await dbgr.stop();
 
         assert(ngrok.isDone(), "Expected these HTTP requests: " + ngrok.pendingMocks().join());
     });
 
-    // TODO: test ngrokHandler, POST to local server
+    /*
+
+    Runtime setup:
+
+        [ wskdebug ]------<start>-----+
+            ^                         |
+            |                         |
+         <handle>                     |
+            |                         v
+        [ local server ]<---------[ local ngrok ]
+                                      ^
+                                      |
+                                      |
+                                  [ ngrok.io ]
+                                      ^
+                                      |
+        [ openwhisk action ]----------+
+
+    Test setup:
+
+        [ wskdebug ]------<start>-----+
+            ^                         |
+            |                         |
+         <handle>                     |
+            |                         v
+        [ local server ]         [ MOCKED ngrok ]
+            ^                         |
+            |                    <pass on port>
+            |                         |
+            |                         |
+        [ MOCKED invocation call ] <--+
+    */
+
+    it("should handle action invocation using ngrok", async function() {
+        const actionName = "myaction";
+
+        // port of the local server started by wskdebug to be expecting calls from ngrok
+        // which we will do in this test
+        let ngrokServerPort;
+        mockNgrokLibrary(function(opts) {
+            ngrokServerPort = opts.addr;
+            return "https://UNIT_TEST.ngrok.io";
+        });
+
+        // should not use this code if we specify local sources which return CORRECT
+        const code = `const main = () => ({ msg: 'WRONG' });`;
+
+        let ngrokAuth;
+
+        test.mockAction(actionName, code);
+        test.mockCreateBackupAction(actionName);
+
+        // ngrok agent installation
+        // custom version instead of test.mockInstallAgent() to catch the ngrokAuth
+        test.openwhiskNock()
+            .put(
+                `${test.openwhiskApiUrlActions()}/${actionName}?overwrite=true`,
+                body => {
+                    ngrokAuth = body.parameters.find(e => e.key === "$ngrokAuth").value;
+                    return body.annotations.some(v => v.key === "wskdebug" && v.value === true);
+                }
+            )
+            .matchHeader("authorization", test.openwhiskApiAuthHeader())
+            .reply(200, test.nodejsActionDescription(actionName));
+
+
+        // wskdebug myaction --ngrok -p ${test.port}
+        const argv = {
+            port: test.port,
+            action: actionName,
+            sourcePath: "action.js",
+            ngrok: true
+        };
+        process.chdir("test/nodejs/plain-flat");
+
+        const dbgr = new Debugger(argv);
+        await dbgr.start();
+        dbgr.run();
+
+        // wait for everything to startup
+        await test.sleep(10);
+
+        try {
+
+            const response = await fetch(`http://127.0.0.1:${ngrokServerPort}`, {
+                method: "POST",
+                headers: {
+                    authorization: ngrokAuth
+                },
+                body: JSON.stringify({
+                    $activationId: "1234567890"
+                })
+            });
+
+            assert.strictEqual(response.status, 200);
+            const result = await response.json();
+            assert.strictEqual(result.msg, "CORRECT");
+
+        } finally {
+            await dbgr.stop();
+        }
+
+        assert(nock.isDone(), "Expected these HTTP requests: " + nock.pendingMocks().join());
+    });
 });
diff --git a/test/test.js b/test/test.js
index 81f5b23..5a266d7 100644
--- a/test/test.js
+++ b/test/test.js
@@ -70,6 +70,18 @@
     );
 }
 
+function openwhiskNock() {
+    return openwhisk;
+}
+
+function openwhiskApiUrlActions() {
+    return `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions`;
+}
+
+function openwhiskApiAuthHeader() {
+    return `Basic ${FAKE_OPENWHISK_AUTH}`;
+}
+
 function agentRetryResponse() {
     return {
         response: {
@@ -101,8 +113,8 @@
 function mockAction(name, code, binary=false) {
     // reading action without code
     openwhisk
-        .get(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .get(`${openwhiskApiUrlActions()}/${name}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .query({"code":"false"})
         .reply(200, nodejsActionDescription(name, binary));
 
@@ -112,56 +124,78 @@
 
     // reading action with code
     openwhisk
-        .get(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .get(`${openwhiskApiUrlActions()}/${name}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, action);
 }
 
-function expectAgent(name, code, binary=false) {
+function mockCreateBackupAction(name, binary=false) {
     const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX;
 
     // wskdebug creating the backup action
     openwhisk
-        .put(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${backupName}?overwrite=true`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .put(`${openwhiskApiUrlActions()}/${backupName}?overwrite=true`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, nodejsActionDescription(backupName, binary));
+}
 
-    // wskdebug creating the backup action
+function mockInstallAgent(name) {
+    // wskdebug overwriting the action with the agent
     openwhisk
         .put(
-            `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}?overwrite=true`,
+            `${openwhiskApiUrlActions()}/${name}?overwrite=true`,
             body => body.annotations.some(v => v.key === "wskdebug" && v.value === true)
         )
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, nodejsActionDescription(name));
+}
+
+function mockReadBackupAction(name, code, binary=false) {
+    const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX;
 
     // reading it later on restore
     openwhisk
-        .get(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${backupName}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .get(`${openwhiskApiUrlActions()}/${backupName}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, Object.assign(nodejsActionDescription(backupName, binary), { exec: { code } }));
+}
 
+function mockRestoreAction(name, code, binary=false) {
     // restoring action
     openwhisk
         .put(
-            `/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}?overwrite=true`,
+            `${openwhiskApiUrlActions()}/${name}?overwrite=true`,
             body => body.exec && body.exec.code === code
         )
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, nodejsActionDescription(name, binary));
+}
+
+function mockRemoveBackupAction(name) {
+    const backupName = name + WSKDEBUG_BACKUP_ACTION_SUFFIX;
 
     // removing backup after restore
     openwhisk
-        .delete(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${backupName}`)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .delete(`${openwhiskApiUrlActions()}/${backupName}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200);
 }
 
+function expectAgent(name, code, binary=false) {
+    mockCreateBackupAction(name, binary);
+    mockInstallAgent(name);
+
+    // shutdown/restore process
+    mockReadBackupAction(name, code, binary);
+    mockRestoreAction(name, code, binary);
+    mockRemoveBackupAction(name);
+}
+
 function nockActivation(name, bodyFn) {
     return openwhisk
-        .post(`/api/v1/namespaces/${FAKE_OPENWHISK_NAMESPACE}/actions/${name}`, bodyFn)
+        .post(`${openwhiskApiUrlActions()}/${name}`, bodyFn)
         .query(true) // support both ?blocking=true and non blocking (no query params)
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`);
+        .matchHeader("authorization", openwhiskApiAuthHeader());
 }
 
 function mockAgentPoll(name) {
@@ -328,7 +362,7 @@
         .get('/')
         .optionally()
         .matchHeader("accept", "application/json")
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, {
             "api_paths": ["/api/v1"],
             "description": "OpenWhisk",
@@ -394,7 +428,7 @@
         .get('/api/v1')
         .optionally()
         .matchHeader("accept", "application/json")
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200,{
             "api_version":"1.0.0",
             "api_version_path":"v1",
@@ -411,7 +445,7 @@
         .get('/api/v1/api-docs')
         .optionally()
         .matchHeader("accept", "application/json")
-        .matchHeader("authorization", `Basic ${FAKE_OPENWHISK_AUTH}`)
+        .matchHeader("authorization", openwhiskApiAuthHeader())
         .reply(200, JSON.parse(fs.readFileSync("./test/openwhisk-swagger.json")));
 }
 
@@ -487,7 +521,16 @@
     mockActionAndInvocation,
     mockActionDoubleInvocation,
     // advanced
+    openwhiskNock,
+    openwhiskApiUrlActions,
+    openwhiskApiAuthHeader,
     mockAction,
+    mockCreateBackupAction,
+    mockInstallAgent,
+    mockReadBackupAction,
+    mockRestoreAction,
+    mockRemoveBackupAction,
+    nodejsActionDescription,
     expectAgent,
     nockActivation,
     expectAgentInvocation,