support openwhisk credentials stored in .env file and Adobe I/O Runtime variable names (#73)

* in verbose mode log where openwhisk credentials are picked up from
* AIO_* vars should take precedence over ~/.wskprops (and WSK_CONFIG_FILE)
diff --git a/.gitignore b/.gitignore
index 73ce74b..aa36988 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@
 build
 coverage.lcov
 test-results
+
+.env
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index a39ebd8..ec6eb41 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -824,6 +824,11 @@
                 "esutils": "^2.0.2"
             }
         },
+        "dotenv": {
+            "version": "8.2.0",
+            "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
+            "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
+        },
         "emoji-regex": {
             "version": "8.0.0",
             "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -2353,6 +2358,12 @@
                 }
             }
         },
+        "mock-fs": {
+            "version": "4.12.0",
+            "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.12.0.tgz",
+            "integrity": "sha512-/P/HtrlvBxY4o/PzXY9cCNBrdylDNxg7gnrv2sMNxj+UJ2m8jSpl0/A6fuJeNAWr99ZvGWH8XCbE0vmnM5KupQ==",
+            "dev": true
+        },
         "mock-require": {
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz",
diff --git a/package.json b/package.json
index f53afc3..cac0c22 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
         "clone": "^2.1.2",
         "debug": "^4.1.1",
         "dockerode": "^3.2.0",
+        "dotenv": "^8.2.0",
         "fetch-retry": "^3.1.0",
         "fs-extra": "^8.1.0",
         "get-port": "^5.1.1",
@@ -69,6 +70,7 @@
         "eslint-plugin-mocha": "^6.3.0",
         "mocha": "^7.1.0",
         "mocha-multi-reporters": "^1.1.7",
+        "mock-fs": "^4.12.0",
         "mock-require": "^3.0.3",
         "nock": "^12.0.2",
         "nyc": "^15.0.0",
diff --git a/src/debugger.js b/src/debugger.js
index f2e1596..40a829a 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -55,7 +55,7 @@
 
         this.wskProps = wskprops.get();
         if (Object.keys(this.wskProps).length === 0) {
-            log.error(`Error: Missing openwhisk credentials. Found no ~/.wskprops file or WSK_* environment variable.`);
+            log.error(`Error: Missing openwhisk credentials. Found no ~/.wskprops or .env file or WSK_* environment variable.`);
             process.exit(1);
         }
         if (argv.ignoreCerts) {
diff --git a/src/wskprops.js b/src/wskprops.js
index 4f4c8fb..9d6ac49 100644
--- a/src/wskprops.js
+++ b/src/wskprops.js
@@ -21,20 +21,24 @@
 
 'use strict';
 
+const log = require('./log');
+
+const dotenv = require('dotenv');
 const path = require('path');
 const fs = require('fs-extra');
 
 const ENV_PARAMS = ['OW_APIHOST', 'OW_AUTH', 'OW_NAMESPACE', 'OW_APIGW_ACCESS_TOKEN'];
 
-function getWskPropsFile() {
+function getWskPropsUserHomeFile() {
     const Home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
-    return process.env.WSK_CONFIG_FILE || path.format({ dir: Home, base: '.wskprops' });
+    return path.format({ dir: Home, base: '.wskprops' });
 }
 
 function readWskPropsFile() {
-    const wskFilePath = getWskPropsFile();
+    const wskFilePath = process.env.WSK_CONFIG_FILE || getWskPropsUserHomeFile();
 
     if (fs.existsSync(wskFilePath)) {
+        log.verbose(`Using openwhisk credentials from ${wskFilePath}${process.env.WSK_CONFIG_FILE ? " (set by WSK_CONFIG_FILE)" : ""}`);
         return fs.readFileSync(wskFilePath, 'utf8');
     } else {
         return null;
@@ -55,17 +59,38 @@
     return wskProps;
 }
 
+function getAioEnvProps() {
+    const envProps = {};
+    // do first, as OW_* ones later shall take precedence
+    if (process.env.AIO_runtime_auth) {
+        envProps.apihost = "https://adobeioruntime.net";
+        envProps.auth = process.env.AIO_runtime_auth;
+        envProps.namespace = process.env.AIO_runtime_namespace;
+        log.verbose(`Using openwhisk credential from AIO_runtime_auth environment variable`);
+    }
+    return envProps;
+}
+
 function getWskEnvProps() {
     const envProps = {};
     ENV_PARAMS.forEach((envName) => {
-        if (process.env[envName]) envProps[envName.slice(3).toLowerCase()] = process.env[envName];
+        if (process.env[envName]) {
+            const key = envName.slice(3).toLowerCase();
+            envProps[key] = process.env[envName];
+            if (key === "auth" || key === "api_key") {
+                log.verbose(`Using openwhisk credential from ${envName} environment variable`);
+            }
+        }
     });
     return envProps;
 }
 
 module.exports = {
     get() {
-        const props = Object.assign(getWskProps(), getWskEnvProps());
+        // load .env file if present
+        dotenv.config();
+
+        const props = Object.assign(getWskProps(), getAioEnvProps(), getWskEnvProps());
         if (props.auth) {
             props.api_key = props.auth;
             delete props.auth;
diff --git a/test/test.js b/test/test.js
index 3d501ea..ce8904d 100644
--- a/test/test.js
+++ b/test/test.js
@@ -41,7 +41,11 @@
 }
 
 async function beforeEach() {
+    delete process.env.OW_AUTH;
+    delete process.env.OW_NAMESPACE;
+    delete process.env.OW_APIHOST;
     process.env.WSK_CONFIG_FILE = path.join(process.cwd(), "test/wskprops");
+
     openwhisk = nock(FAKE_OPENWHISK_SERVER);
     mockOpenwhiskSwagger(openwhisk);
 
diff --git a/test/wskprops.test.js b/test/wskprops.test.js
new file mode 100644
index 0000000..8f81a4b
--- /dev/null
+++ b/test/wskprops.test.js
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/* eslint-env mocha */
+
+'use strict';
+
+const wskprops = require('../src/wskprops');
+const assert = require('assert');
+const mockFs = require('mock-fs');
+const os = require('os');
+
+function resetEnvVars() {
+    delete process.env.OW_AUTH;
+    delete process.env.OW_NAMESPACE;
+    delete process.env.OW_APIHOST;
+    delete process.env.WSK_CONFIG_FILE;
+    delete process.env.AIO_runtime_auth;
+    delete process.env.AIO_runtime_namespace;
+}
+
+describe('wskprops', function() {
+
+    beforeEach(function() {
+        resetEnvVars();
+    });
+
+    afterEach(function() {
+        resetEnvVars();
+        mockFs.restore();
+    });
+
+    it("should read WSK_CONFIG_FILE", async function() {
+        process.env.WSK_CONFIG_FILE = "some/wskprops";
+        mockFs({
+            "some/wskprops":
+`APIHOST=https://some-wskprops
+NAMESPACE=some-wskprops-namespace
+AUTH=some-wskprops-auth`
+        });
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://some-wskprops");
+        assert.strictEqual(props.namespace, "some-wskprops-namespace");
+        assert.strictEqual(props.api_key, "some-wskprops-auth");
+    });
+
+    it("should read ~/.wskprops", async function() {
+        mockFs({
+            [`${os.homedir()}/.wskprops`]:
+`APIHOST=https://home-wskprops
+NAMESPACE=home-wskprops-namespace
+AUTH=home-wskprops-auth`
+        });
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://home-wskprops");
+        assert.strictEqual(props.namespace, "home-wskprops-namespace");
+        assert.strictEqual(props.api_key, "home-wskprops-auth");
+    });
+
+    it("should read OW_* vars", async function() {
+        process.env.OW_APIHOST = "https://ow_apihost";
+        process.env.OW_NAMESPACE = "ow_namespace";
+        process.env.OW_AUTH = "ow_auth";
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://ow_apihost");
+        assert.strictEqual(props.namespace, "ow_namespace");
+        assert.strictEqual(props.api_key, "ow_auth");
+    });
+
+    it("should give OW_* vars precedence over WSK_CONFIG_FILE", async function() {
+        process.env.WSK_CONFIG_FILE = "some/wskprops";
+
+        process.env.OW_APIHOST = "https://ow_apihost";
+        process.env.OW_NAMESPACE = "ow_namespace";
+        process.env.OW_AUTH = "ow_auth";
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://ow_apihost");
+        assert.strictEqual(props.namespace, "ow_namespace");
+        assert.strictEqual(props.api_key, "ow_auth");
+    });
+
+    it("should read AIO_* vars", async function() {
+        process.env.AIO_runtime_namespace = "aio_namespace";
+        process.env.AIO_runtime_auth = "aio_auth";
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://adobeioruntime.net");
+        assert.strictEqual(props.namespace, "aio_namespace");
+        assert.strictEqual(props.api_key, "aio_auth");
+    });
+
+    it("should give AIO_* vars precedence over WSK_CONFIG_FILE", async function() {
+        process.env.WSK_CONFIG_FILE = "some/wskprops";
+        process.env.AIO_runtime_namespace = "aio_namespace";
+        process.env.AIO_runtime_auth = "aio_auth";
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://adobeioruntime.net");
+        assert.strictEqual(props.namespace, "aio_namespace");
+        assert.strictEqual(props.api_key, "aio_auth");
+    });
+
+    it("should give AIO_* vars precedence over ~/.wskprops", async function() {
+        mockFs({
+            [`${os.homedir()}/.wskprops`]:
+`APIHOST=https://home-wskprops
+NAMESPACE=home-wskprops-namespace
+AUTH=home-wskprops-auth`
+        });
+
+        process.env.AIO_runtime_namespace = "aio_namespace";
+        process.env.AIO_runtime_auth = "aio_auth";
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://adobeioruntime.net");
+        assert.strictEqual(props.namespace, "aio_namespace");
+        assert.strictEqual(props.api_key, "aio_auth");
+    });
+
+    it("should give OW_* precedence over AIO_* vars", async function() {
+        process.env.AIO_runtime_namespace = "aio_namespace";
+        process.env.AIO_runtime_auth = "aio_auth";
+
+        process.env.OW_APIHOST = "https://ow_apihost";
+        process.env.OW_NAMESPACE = "ow_namespace";
+        process.env.OW_AUTH = "ow_auth";
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://ow_apihost");
+        assert.strictEqual(props.namespace, "ow_namespace");
+        assert.strictEqual(props.api_key, "ow_auth");
+    });
+
+    it("should read AIO_* from .env", async function() {
+        mockFs({
+            ".env":
+`AIO_runtime_namespace=aio_namespace
+AIO_runtime_auth=aio_auth`
+        });
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://adobeioruntime.net");
+        assert.strictEqual(props.namespace, "aio_namespace");
+        assert.strictEqual(props.api_key, "aio_auth");
+    });
+
+    it("should read WSK_CONFIG_FILE from .env", async function() {
+        mockFs({
+            ".env": "WSK_CONFIG_FILE=some/wskprops",
+            "some/wskprops":
+`APIHOST=https://some-wskprops
+NAMESPACE=some-wskprops-namespace
+AUTH=some-wskprops-auth`
+        });
+
+        const props = wskprops.get();
+        assert.strictEqual(props.apihost, "https://some-wskprops");
+        assert.strictEqual(props.namespace, "some-wskprops-namespace");
+        assert.strictEqual(props.api_key, "some-wskprops-auth");
+    });
+
+});