Trigger management (#101)

* initial implementation of trigger management get status and configuration

* add test for get status and configuration

* formatting

* formatting

* remove trailing whitespace

* fix scanCode complaint

* handle namespace of legacy alarms when fetching trigger document

* update dateChanged field to millis since epoch to provide consistency

* remove maxTriggers from response, add dateChangedISO to response
diff --git a/action/alarm.js b/action/alarm.js
index cda9aee..a5c217f 100644
--- a/action/alarm.js
+++ b/action/alarm.js
@@ -2,7 +2,14 @@
 
 function main(msg) {
 
+    let eventMap = {
+        CREATE: 'put',
+        READ: 'get',
+        // UPDATE: 'put',
+        DELETE: 'delete'
+    };
     // for creation -> CREATE
+    // for reading -> READ
     // for deletion -> DELETE
     var lifecycleEvent = msg.lifecycleEvent;
 
@@ -11,12 +18,11 @@
 
     var url = `https://${endpoint}/api/v1/web/whisk.system/alarmsWeb/alarmWebAction.http`;
 
-    if (lifecycleEvent !== 'CREATE' && lifecycleEvent !== 'DELETE') {
-        return Promise.reject('lifecycleEvent must be CREATE or DELETE');
-    }
-    else {
-        var method = lifecycleEvent === 'CREATE' ? 'put' : 'delete';
+    if (lifecycleEvent in eventMap) {
+        var method = eventMap[lifecycleEvent];
         return requestHelper(url, webparams, method);
+    } else {
+        return Promise.reject('unsupported lifecycleEvent');
     }
 }
 
@@ -32,7 +38,7 @@
         }, function(error, response, body) {
 
             if (!error && response.statusCode === 200) {
-                resolve();
+                resolve(body);
             }
             else {
                 if (response) {
diff --git a/action/alarmWebAction.js b/action/alarmWebAction.js
index 232d82d..31ed6fd 100644
--- a/action/alarmWebAction.js
+++ b/action/alarmWebAction.js
@@ -1,5 +1,6 @@
 var request = require('request');
 var CronJob = require('cron').CronJob;
+var moment = require('moment');
 
 function main(params) {
 
@@ -45,7 +46,7 @@
             maxTriggers: params.maxTriggers || -1,
             status: {
                 'active': true,
-                'dateChanged': new Date().toISOString()
+                'dateChanged': Date.now()
             }
         };
 
@@ -72,6 +73,38 @@
         });
 
     }
+    else if (params.__ow_method === "get") {
+        return new Promise(function (resolve, reject) {
+            verifyTriggerAuth(triggerURL, params.authKey, false)
+            .then(() => {
+                return getTrigger(db, triggerID);
+            })
+            .then(doc => {
+                var body = {
+                    config: {
+                        name: doc.name,
+                        namespace: doc.namespace,
+                        cron: doc.cron,
+                        payload: doc.payload
+                    },
+                    status: {
+                        active: doc.status.active,
+                        dateChanged: moment(doc.status.dateChanged).utc().valueOf(),
+                        dateChangedISO: moment(doc.status.dateChanged).utc().format(),
+                        reason: doc.status.reason
+                    }
+                };
+                resolve({
+                    statusCode: 200,
+                    headers: {'Content-Type': 'application/json'},
+                    body: new Buffer(JSON.stringify(body)).toString('base64')
+                });
+            })
+            .catch(err => {
+                reject(err);
+            });
+        });
+    }
     else if (params.__ow_method === "delete") {
 
         return new Promise(function (resolve, reject) {
@@ -95,7 +128,7 @@
         });
     }
     else {
-        return sendError(400, 'lifecycleEvent must be CREATE or DELETE');
+        return sendError(400, 'unsupported lifecycleEvent');
     }
 }
 
@@ -152,6 +185,32 @@
     });
 }
 
+function getTrigger(triggerDB, triggerID, retry = true) {
+
+    return new Promise(function(resolve, reject) {
+
+        triggerDB.get(triggerID, function (err, existing) {
+            if (err) {
+                if (retry) {
+                    var parts = triggerID.split('/');
+                    var id = parts[0] + '/_/' + parts[2];
+                    getTrigger(triggerDB, id, false)
+                    .then(doc => {
+                        resolve(doc);
+                    })
+                    .catch(err => {
+                        reject(err);
+                    });
+                } else {
+                    reject(sendError(err.statusCode, 'could not find the trigger in the database'));
+                }
+            } else {
+                resolve(existing);
+            }
+        });
+    });
+}
+
 function updateTrigger(triggerDB, triggerID, retryCount) {
 
     return new Promise(function(resolve, reject) {
diff --git a/provider/lib/utils.js b/provider/lib/utils.js
index 5103356..c804cb8 100644
--- a/provider/lib/utils.js
+++ b/provider/lib/utils.js
@@ -170,7 +170,7 @@
                     var updatedTrigger = existing;
                     var status = {
                         'active': false,
-                        'dateChanged': new Date().toISOString(),
+                        'dateChanged': Date.now(),
                         'reason': {'kind': 'AUTO', 'statusCode': statusCode, 'message': message}
                     };
                     updatedTrigger.status = status;
diff --git a/tests/src/test/scala/system/packages/AlarmsFeedTests.scala b/tests/src/test/scala/system/packages/AlarmsFeedTests.scala
index c175722..5377287 100644
--- a/tests/src/test/scala/system/packages/AlarmsFeedTests.scala
+++ b/tests/src/test/scala/system/packages/AlarmsFeedTests.scala
@@ -17,16 +17,16 @@
 package system.packages
 
 import org.junit.runner.RunWith
-import org.scalatest.FlatSpec
+import org.scalatest.{FlatSpec, Inside}
 import org.scalatest.junit.JUnitRunner
-
 import common.TestHelpers
 import common.Wsk
 import common.WskProps
 import common.WskTestHelpers
 import spray.json.DefaultJsonProtocol.IntJsonFormat
 import spray.json.DefaultJsonProtocol.StringJsonFormat
-import spray.json.pimpAny
+import spray.json.DefaultJsonProtocol.BooleanJsonFormat
+import spray.json.{JsObject, JsString, pimpAny}
 
 /**
  * Tests for alarms trigger service
@@ -34,6 +34,7 @@
 @RunWith(classOf[JUnitRunner])
 class AlarmsFeedTests
     extends FlatSpec
+    with Inside
     with TestHelpers
     with WskTestHelpers {
 
@@ -100,4 +101,66 @@
         activations should be(3)
     }
 
+    it should "return correct status and configuration" in withAssetCleaner(wskprops) {
+        val currentTime = s"${System.currentTimeMillis}"
+
+        (wp, assetHelper) =>
+            implicit val wskProps = wp
+            val triggerName = s"dummyAlarmsTrigger-${System.currentTimeMillis}"
+            val packageName = "dummyAlarmsPackage"
+
+            // the package alarms should be there
+            val packageGetResult = wsk.pkg.get("/whisk.system/alarms")
+            println("fetched package alarms")
+            packageGetResult.stdout should include("ok")
+
+            // create package binding
+            assetHelper.withCleaner(wsk.pkg, packageName) {
+                (pkg, name) => pkg.bind("/whisk.system/alarms", name)
+            }
+
+            val triggerPayload = JsObject(
+                "test" -> JsString("alarmsTest")
+            )
+            val cronString = "* * * * * *"
+            val maxTriggers = -1
+
+            // create whisk stuff
+            val feedCreationResult = assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) =>
+                    trigger.create(name, feed = Some(s"$packageName/alarm"), parameters = Map(
+                        "trigger_payload" -> triggerPayload,
+                        "cron" -> cronString.toJson))
+            }
+            feedCreationResult.stdout should include("ok")
+
+            val actionName = s"$packageName/alarm"
+            val run = wsk.action.invoke(actionName, parameters = Map(
+                "triggerName" -> triggerName.toJson,
+                "lifecycleEvent" -> "READ".toJson,
+                "authKey" -> wskProps.authKey.toJson
+            ))
+
+            withActivation(wsk.activation, run) {
+                activation =>
+                    activation.response.success shouldBe true
+
+                    inside (activation.response.result) {
+                        case Some(result) =>
+                            val config = result.getFields("config").head.asInstanceOf[JsObject].fields
+                            val status = result.getFields("status").head.asInstanceOf[JsObject].fields
+
+                            config should contain("name" -> triggerName.toJson)
+                            config should contain("cron" -> cronString.toJson)
+                            config should contain("payload" -> triggerPayload)
+                            config should contain key "namespace"
+
+                            status should contain("active" -> true.toJson)
+                            status should contain key "dateChanged"
+                            status should contain key "dateChangedISO"
+                            status should not(contain key "reason")
+                    }
+            }
+
+    }
 }