Rewriting CLIRuleTests in Scala, adding a new testhelper

Added a new testhelper to facilitate tests that rely on activations on a specified entity rather than an activationId.
diff --git a/tests/src/common/WskTestHelpers.scala b/tests/src/common/WskTestHelpers.scala
index 13c6b25..4349928 100644
--- a/tests/src/common/WskTestHelpers.scala
+++ b/tests/src/common/WskTestHelpers.scala
@@ -27,6 +27,7 @@
 
 import common.TestUtils.RunResult
 import spray.json._
+import java.time.Instant
 
 /**
  * Test fixture to ease cleaning of whisk entities created during testing.
@@ -43,7 +44,7 @@
      * in given collection.
      *
      */
-    private class AssetCleaner(assetsToDeleteAfterTest: Assets, wskprops: WskProps) {
+    class AssetCleaner(assetsToDeleteAfterTest: Assets, wskprops: WskProps) {
         def withCleaner[T <: DeleteFromCollection](cli: T, name: String, confirmDelete: Boolean = true)(
             cmd: (T, String) => RunResult): RunResult = {
             cli.sanitize(name)(wskprops) // sanitize (delete) if asset exists
@@ -134,6 +135,38 @@
     }
 
     /**
+     * Polls until it finds {@code N} activationIds from an entity. Asserts the count
+     * of the activationIds actually equal {@code N}. Takes a {@code since} parameter
+     * defining the oldest activationId to consider valid.
+     */
+    def withActivationsFromEntity(
+        wsk: WskActivation,
+        entity: String,
+        N: Int = 1,
+        since: Option[Instant] = None,
+        pollPeriod: Duration = 1 second,
+        totalWait: Duration = 30 seconds)(
+            check: Seq[CliActivation] => Unit)(
+                implicit wskprops: WskProps): Unit = {
+
+        val activationIds = wsk.pollFor(N, Some(entity), since = since)
+        withClue(s"did not find $N activations for $entity since $since") {
+            activationIds.length shouldBe N
+        }
+
+        val parsed = activationIds.map { id =>
+            wsk.parseJsonString(wsk.get(id).stdout).convertTo[CliActivation]
+        }
+        try {
+            check(parsed)
+        } catch {
+            case error: Throwable =>
+                println(s"check failed for activations $activationIds: ${parsed}")
+                throw error
+        }
+    }
+
+    /**
      * In the case that test throws an exception, print stderr and stdout
      * from the provided RunResult.
      */
diff --git a/tests/src/system/basic/CLIRuleTests.java b/tests/src/system/basic/CLIRuleTests.java
deleted file mode 100644
index 8aaed20..0000000
--- a/tests/src/system/basic/CLIRuleTests.java
+++ /dev/null
@@ -1,456 +0,0 @@
-/*
- * Copyright 2015-2016 IBM Corporation
- *
- * Licensed 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.
- */
-
-package system.basic;
-
-import static common.WskCli.Item.Action;
-import static common.WskCli.Item.Activation;
-import static common.WskCli.Item.Rule;
-import static common.WskCli.Item.Trigger;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ThreadLocalRandom;
-
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import com.google.code.tempusfugit.concurrency.ParallelRunner;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-
-import common.TestUtils;
-import common.WskCli;
-
-/**
- * Tests for rules using command line interface
- */
-@RunWith(ParallelRunner.class)
-public class CLIRuleTests {
-    private static final Boolean usePythonCLI = false;
-    private static final WskCli wsk = new WskCli(usePythonCLI);
-    private static final int RULE_DELAY = 30;
-    private static final int DELAY = 90;
-    // NEGATIVE_DELAY is used for tests when checking that something doesn't
-    // show up in activations
-    private static final int NEGATIVE_DELAY = 30;
-
-    @BeforeClass
-    public static void setUp() throws Exception {
-    }
-
-    /**
-     * rule test with one trigger and one action
-     */
-    @Test
-    public void rule1to1() throws Exception {
-        try {
-        	wsk.sanitize(Rule, "R_121");
-        	wsk.sanitize(Action, "A_121");
-        	wsk.sanitize(Trigger, "T_121");
-
-            wsk.createAction("A_121", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createTrigger("T_121");
-            wsk.createRule("R_121", "T_121", "A_121");
-            long beforeTrigger = System.currentTimeMillis();
-            wsk.trigger("T_121", "bob 121");
-
-            String expected = "The message 'bob 121' has";
-            List<String> activationIds = wsk.waitForActivations("A_121", 1, beforeTrigger, DELAY);
-            assertTrue("Not enough activation ids found", activationIds != null);
-            // the most recent id
-            String activationId = activationIds.get(0);
-            assertTrue("Expected message not found: " + expected, wsk.logsForActivationContain(activationId, expected, DELAY));
-        } finally {
-        	wsk.delete(Rule, "R_121");
-        	wsk.delete(Action, "A_121");
-            wsk.delete(Trigger, "T_121");
-        }
-    }
-
-    /**
-     * rule test with one trigger and one action (action has space in name)
-     */
-    @Test
-    public void rule1to1WithSpaceInActionName() throws Exception {
-        try {
-            wsk.sanitize(Rule, "R_121s");
-            wsk.sanitize(Action, "A_121s A_121s");
-            wsk.sanitize(Trigger, "T_121s");
-
-            wsk.createAction("A_121s A_121s", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createTrigger("T_121s");
-            wsk.createRule("R_121s", "T_121s", "A_121s A_121s");
-            long beforeTrigger = System.currentTimeMillis();
-            wsk.trigger("T_121s", "bob 121");
-
-            String expected = "The message 'bob 121' has";
-            List<String> activationIds = wsk.waitForActivations("A_121s A_121s", 1, beforeTrigger, DELAY);
-            assertTrue("Not enough activation ids found", activationIds != null);
-            // the most recent id
-            String activationId = activationIds.get(0);
-            assertTrue("Expected message not found: " + expected, wsk.logsForActivationContain(activationId, expected, DELAY));
-        } finally {
-            wsk.delete(Rule, "R_121s");
-            wsk.delete(Action, "A_121s A_121s");
-            wsk.delete(Trigger, "T_121s");
-        }
-    }
-
-    /**
-     * rule test with two triggers and one action
-     */
-    @Test
-    public void rule2to1() throws Exception {
-        try {
-            wsk.sanitize(Action, "A_221");
-            wsk.sanitize(Trigger, "T1_221");
-            wsk.sanitize(Trigger, "T2_221");
-            wsk.sanitize(Rule, "R1_221");
-            wsk.sanitize(Rule, "R2_221");
-
-            wsk.createAction("A_221", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createTrigger("T1_221");
-            wsk.createTrigger("T2_221");
-            wsk.createRule("R1_221", "T1_221", "A_221");
-            wsk.createRule("R2_221", "T2_221", "A_221");
-
-            long beforeTrigger = System.currentTimeMillis();
-            wsk.trigger("T2_221", "i'll be back");
-            wsk.trigger("T1_221", "terminator");
-
-            String expected1 = "The message 'terminator' has";
-            assertTrue("Expected message not found: " + expected1, wsk.logsForActionContain("A_221", expected1, beforeTrigger, DELAY));
-
-            String expected2 = "The message 'i'll be back' has";
-            assertTrue("Expected message not found: " + expected1, wsk.logsForActionContain("A_221", expected2, beforeTrigger, DELAY));
-
-        } finally {
-            wsk.delete(Action, "A_221");
-            wsk.delete(Trigger, "T1_221");
-            wsk.delete(Trigger, "T2_221");
-            wsk.delete(Rule, "R1_221");
-            wsk.delete(Rule, "R2_221");
-        }
-    }
-
-    /**
-     * rule test with one trigger and two actions
-     */
-    @Test
-    public void rule1to2() throws Exception {
-        try {
-            wsk.sanitize(Action, "A1_122");
-            wsk.sanitize(Action, "A2_122");
-            wsk.sanitize(Trigger, "T1_122");
-            wsk.sanitize(Rule, "R1_122");
-            wsk.sanitize(Rule, "R2_122");
-
-            wsk.createAction("A1_122", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createAction("A2_122", TestUtils.getCatalogFilename("samples/hello.js"));
-            wsk.createTrigger("T1_122");
-            wsk.createRule("R1_122", "T1_122", "A1_122");
-            wsk.createRule("R2_122", "T1_122", "A2_122");
-
-            long beforeTrigger = System.currentTimeMillis();
-            wsk.trigger("T1_122", "put a fork in it");
-
-            String expected1 = "The message 'put a fork in it' has";
-            assertTrue("Expected message not found: " + expected1, wsk.logsForActionContain("A1_122", expected1, beforeTrigger, DELAY));
-
-            String expected2 = "hello put a fork in it";
-            assertTrue("Expected message not found: " + expected2, wsk.logsForActionContain("A2_122", expected2, beforeTrigger, DELAY));
-
-        } finally {
-            wsk.delete(Action, "A1_122");
-            wsk.delete(Action, "A2_122");
-            wsk.delete(Trigger, "T1_122");
-            wsk.delete(Rule, "R1_122");
-            wsk.delete(Rule, "R2_122");
-        }
-    }
-
-    /**
-     * rule test with two triggers and two actions
-     */
-    @Test
-    public void rule2to2() throws Exception {
-        long startMilli = System.currentTimeMillis();
-        try {
-            wsk.sanitize(Action, "A1_222");
-            wsk.sanitize(Action, "A2_222");
-            wsk.sanitize(Trigger, "T1_222");
-            wsk.sanitize(Trigger, "T2_222");
-            wsk.sanitize(Rule, "Alpha");
-            wsk.sanitize(Rule, "Beta");
-            wsk.sanitize(Rule, "Gamma");
-            wsk.sanitize(Rule, "Delta");
-            long endMilli = System.currentTimeMillis();
-            System.out.format("rule2to2: %.1f seconds to sanitize\n", (endMilli - startMilli) / 1000.0);
-            startMilli = endMilli;
-
-            wsk.createAction("A1_222", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createAction("A2_222", TestUtils.getCatalogFilename("samples/hello.js"));
-            wsk.createTrigger("T1_222");
-            wsk.createTrigger("T2_222");
-            wsk.createRule("Alpha", "T1_222", "A1_222");
-            wsk.createRule("Beta", "T1_222", "A2_222");
-            wsk.createRule("Gamma", "T2_222", "A1_222");
-            wsk.createRule("Delta", "T2_222", "A2_222");
-            endMilli = System.currentTimeMillis();
-            System.out.format("rule2to2: %.1f seconds to create actions and rules\n", (endMilli - startMilli) / 1000.0);
-            startMilli = endMilli;
-
-            long beforeTrigger = System.currentTimeMillis();
-            wsk.trigger("T1_222", "XXX");
-            wsk.trigger("T2_222", "YYY");
-            endMilli = System.currentTimeMillis();
-            System.out.format("rule2to2: %.1f seconds to trigger\n", (endMilli - startMilli) / 1000.0);
-            startMilli = endMilli;
-
-            List<String> activations = wsk.waitForActivations("A1_222", 2, beforeTrigger, DELAY);
-            assertTrue("Not enough activation ids found", activations != null);
-
-            String expected1 = "The message 'XXX' has";
-            assertTrue("Expected message not found: " + expected1, wsk.logsForActionContain("A1_222", expected1, beforeTrigger, DELAY));
-            String expected2 = "The message 'YYY' has";
-            assertTrue("Expected message not found: " + expected2, wsk.logsForActionContain("A1_222", expected2, beforeTrigger, DELAY));
-            endMilli = System.currentTimeMillis();
-            System.out.format("rule2to2: %.1f seconds for first check\n", (endMilli - startMilli) / 1000.0);
-
-            startMilli = endMilli;
-
-            expected1 = "hello XXX";
-            expected2 = "hello YYY";
-            assertTrue("Expected message not found: " + expected1, wsk.logsForActionContain("A2_222", expected1, beforeTrigger, DELAY));
-            assertTrue("Expected message not found: " + expected2, wsk.logsForActionContain("A2_222", expected2, beforeTrigger, DELAY));
-            endMilli = System.currentTimeMillis();
-            System.out.format("rule2to2: %.1f seconds for second check\n", (endMilli - startMilli) / 1000.0);
-
-            startMilli = endMilli;
-        } finally {
-            wsk.delete(Action, "A1_222");
-            wsk.delete(Action, "A2_222");
-            wsk.delete(Trigger, "T1_222");
-            wsk.delete(Trigger, "T2_222");
-            wsk.delete(Rule, "Alpha");
-            wsk.delete(Rule, "Beta");
-            wsk.delete(Rule, "Gamma");
-            wsk.delete(Rule, "Delta");
-            long endMilli = System.currentTimeMillis();
-            System.out.format("rule2to2: %.1f seconds for cleanup\n", (endMilli - startMilli) / 1000.0);
-        }
-    }
-
-    /**
-     * rule test to make sure deleting a rule also disables it.
-     */
-    @Test
-    public void ruleDisable() throws Exception {
-        long startMilli = System.currentTimeMillis();
-        try {
-            wsk.sanitize(Action, "A_321");
-            wsk.sanitize(Trigger, "T_321");
-            wsk.sanitize(Rule, "R_321");
-            long endMilli = System.currentTimeMillis();
-            System.out.format("ruleDisable: %.1f seconds to sanitize\n", (endMilli - startMilli) / 1000.0);
-            startMilli = endMilli;
-
-            wsk.createAction("A_321", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createTrigger("T_321");
-            wsk.createRule("R_321", "T_321", "A_321");
-            wsk.delete(Rule, "R_321");
-            endMilli = System.currentTimeMillis();
-            System.out.format("ruleDisable: %.1f seconds to create and delete rule/action\n", (endMilli - startMilli) / 1000.0);
-            startMilli = endMilli;
-            wsk.trigger("T_321", "ralph");
-            endMilli = System.currentTimeMillis();
-            System.out.format("ruleDisable: %.1f seconds to trigger\n", (endMilli - startMilli) / 1000.0);
-            startMilli = endMilli;
-
-            // retrieve activation ids; wait for at least one
-            List<String> activations = wsk.waitForActivations("A_321", 1, startMilli, NEGATIVE_DELAY);
-            assertTrue("Unexpected activation id found: ", activations == null || activations.size() == 0);
-            endMilli = System.currentTimeMillis();
-            System.out.format("ruleDisable: %.1f seconds to get activation\n", (endMilli - startMilli) / 1000.0);
-            startMilli = endMilli;
-        } finally {
-            wsk.delete(Action, "A_321");
-            wsk.delete(Trigger, "T_321");
-            wsk.sanitize(Rule, "R_321");
-            long endMilli = System.currentTimeMillis();
-            System.out.format("ruleDisable: %.1f seconds to cleanup\n", (endMilli - startMilli) / 1000.0);
-        }
-    }
-
-    /**
-     * rule test to check if a rule can be deleted and recreated with the same
-     * name.
-     */
-    @Test
-    public void ruleRecreate() throws Exception {
-        try {
-            wsk.sanitize(Action, "A_421");
-            wsk.sanitize(Trigger, "T_421");
-            wsk.sanitize(Trigger, "T_422");
-            wsk.sanitize(Rule, "R_421");
-
-            wsk.createAction("A_421", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createTrigger("T_421");
-            wsk.createRule("R_421", "T_421", "A_421");
-            wsk.delete(Rule, "R_421");
-            wsk.createTrigger("T_422");
-            wsk.createRule("R_421", "T_422", "A_421");
-            long beforeTrigger = System.currentTimeMillis();
-            wsk.trigger("T_422", "david");
-
-            List<String> activations = wsk.waitForActivations("A_421", 1, beforeTrigger, NEGATIVE_DELAY);
-            if (activations == null || activations.size() == 0) {
-                assertFalse("Did not find any activations for A_421", true);
-                return;
-            }
-            String activationId = activations.get(0);
-            String expected = "The message 'david' has";
-            assertTrue("Expected message found: " + expected, wsk.logsForActivationContain(activationId, expected, DELAY));
-        } finally {
-            wsk.delete(Action, "A_421");
-            wsk.delete(Trigger, "T_421");
-            wsk.delete(Trigger, "T_422");
-            wsk.sanitize(Rule, "R_421");
-        }
-    }
-
-    /**
-     * rule test to check if a rule can be disable and enabled.
-     */
-    @Test
-    public void ruleDisableEnable() throws Exception {
-        try {
-            wsk.sanitize(Action, "A_621");
-            wsk.sanitize(Trigger, "T_621");
-            wsk.sanitize(Rule, "R_621");
-
-            wsk.createAction("A_621", TestUtils.getCatalogFilename("samples/wc.js"));
-            wsk.createTrigger("T_621");
-            wsk.createRule("R_621", "T_621", "A_621");
-
-            wsk.disableRule("R_621", RULE_DELAY);
-            wsk.trigger("T_621", "batman");
-
-            wsk.enableRule("R_621", RULE_DELAY);
-            long beforeTrigger = System.currentTimeMillis();
-            wsk.trigger("T_621", "bruce wayne");
-
-            List<String> activations = wsk.waitForActivations("A_621", 1, beforeTrigger, DELAY);
-            assertTrue("Not enough activation ids found", activations != null);
-
-            String activationId = activations.get(0);
-            String expected = "The message 'bruce wayne' has";
-            assertTrue("Expected message not found: " + expected, wsk.logsForActivationContain(activationId, expected, DELAY));
-
-            String unexpected = "The message 'batman' has";
-            assertFalse("Unexpected message found: " + unexpected, wsk.logsForActivationContain(activationId, unexpected, NEGATIVE_DELAY));
-        } finally {
-            wsk.delete(Action, "A_621");
-            wsk.delete(Trigger, "T_621");
-            wsk.sanitize(Rule, "R_621");
-        }
-    }
-
-    /**
-     * Test for presence of activation records for trigger, rule, and action.
-     */
-    @Test
-    public void activations() throws Exception {
-        int nameSuffix = ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
-        String action = "ACT_A_" + nameSuffix;
-        String rule = "ACT_R_" + nameSuffix;
-        String trigger = "ACT_T_" + nameSuffix;
-
-        final class ActivationInfo {
-            public String activationId;
-            public String cause;
-
-            public ActivationInfo(String activationId, String cause) {
-                this.activationId = activationId;
-                this.cause = cause;
-            }
-
-            @Override
-            public String toString() {
-                return "ActivationInfo [activationId=" + activationId + ", cause=" + cause + "]";
-            }
-        }
-
-        try {
-            wsk.sanitize(Action, action);
-            wsk.sanitize(Trigger, trigger);
-            wsk.sanitize(Rule, rule);
-
-            wsk.createAction(action, TestUtils.getCatalogFilename("utils/date.js"));
-            wsk.createTrigger(trigger);
-            wsk.createRule(rule, trigger, action);
-            wsk.trigger(trigger, "bobby 121");
-
-            // Get activations
-            Set<String> entities = new HashSet<String>(Arrays.asList(new String[] { trigger, action, rule }));
-            Map<String, ActivationInfo> activations = TestUtils.waitfor(() -> {
-                String list = wsk.list(Activation);
-                String[] lines = list.split("\\r?\\n");
-                Map<String, ActivationInfo> infos = new HashMap<String, ActivationInfo>();
-                for (String line : lines) {
-                    String[] words = line.split("\\s+");
-                    if (words.length == 2) {
-                        String entityName = words[1];
-                        String activationId = words[0];
-                        if (entities.contains(entityName)) {
-                            String activation = wsk.get(Activation, activationId);
-                            // remove "ok" line in stdout; leaving json
-                            activation = activation.substring(activation.indexOf(System.getProperty("line.separator")) + 1);
-                            JsonObject json = new JsonParser().parse(activation).getAsJsonObject();
-                            String cause = json.get("cause") != null ? json.get("cause").getAsString() : "";
-                            infos.put(entityName, new ActivationInfo(activationId, cause));
-                        }
-                    }
-                }
-                return infos.size() == 3 ? infos : null;
-            } , 8, 1, DELAY);
-
-            // Check that activations exist.
-            assertTrue("Activation not found for " + trigger, activations.containsKey(trigger));
-            assertTrue("Activation not found for " + rule, activations.containsKey(rule));
-            assertTrue("Activation not found for " + action, activations.containsKey(action));
-
-            // Check that activation cause is correct.
-            assertTrue("Wrong cause found for " + trigger, activations.get(trigger).cause.equals(""));
-            assertTrue("Wrong cause found for " + rule, activations.get(rule).cause.equals(activations.get(trigger).activationId));
-            // assertTrue("Wrong cause found for " + action, activations.get(action).cause.equals(activations.get(rule).activationId));
-
-        } finally {
-            wsk.delete(Rule, rule);
-            wsk.delete(Action, action);
-            wsk.delete(Trigger, trigger);
-        }
-    }
-
-}
diff --git a/tests/src/system/basic/WskRuleTests.scala b/tests/src/system/basic/WskRuleTests.scala
new file mode 100644
index 0000000..d7a6f4d
--- /dev/null
+++ b/tests/src/system/basic/WskRuleTests.scala
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed 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.
+ */
+
+package system.basic
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.JsHelpers
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import java.time.Instant
+
+@RunWith(classOf[JUnitRunner])
+class WskRuleTests
+    extends TestHelpers
+    with JsHelpers
+    with WskTestHelpers {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk(usePythonCLI = false)
+    val defaultAction = TestUtils.getCatalogFilename("samples/wc.js")
+    val secondAction = TestUtils.getCatalogFilename("samples/hello.js")
+
+    val testString = "this is a test"
+    val testResult = JsObject("count" -> testString.split(" ").length.toJson)
+
+    /**
+     * Sets up trigger -> rule -> action triplets. Deduplicates triggers and rules
+     * and links it all up.
+     *
+     * @param rules Tuple3s containing (rule, trigger, (actionName, actionFile))
+     */
+    def ruleSetup(rules: Seq[(String, String, (String, String))], assetHelper: AssetCleaner) = {
+        val triggers = rules.map(_._2).distinct
+        val actions = rules.map(_._3).distinct
+
+        triggers.foreach { trigger =>
+            assetHelper.withCleaner(wsk.trigger, trigger) {
+                (trigger, name) => trigger.create(name)
+            }
+        }
+
+        actions.foreach {
+            case (actionName, file) =>
+                assetHelper.withCleaner(wsk.action, actionName) {
+                    (action, name) => action.create(name, Some(file))
+                }
+        }
+
+        rules.foreach {
+            case (ruleName, triggerName, action) =>
+                assetHelper.withCleaner(wsk.rule, ruleName) {
+                    (rule, name) => rule.create(name, triggerName, action._1)
+                }
+        }
+    }
+
+    behavior of "Whisk rules"
+
+    it should "invoke the action attached on trigger fire, creating an activation for each entity including the cause" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "r1to1"
+            val triggerName = "t1to1"
+            val actionName = "a1 to 1" // spaces in name intended for greater test coverage
+
+            ruleSetup(Seq(
+                (ruleName, triggerName, (actionName, defaultAction))),
+                assetHelper)
+
+            val now = Instant.now
+            val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    triggerActivation.cause shouldBe None
+
+                    withActivationsFromEntity(wsk.activation, ruleName, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        _.head.cause shouldBe Some(triggerActivation.activationId)
+                    }
+
+                    withActivationsFromEntity(wsk.activation, actionName, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        _.head.response.result shouldBe Some(testResult)
+                    }
+            }
+    }
+
+    it should "not activate an action if the rule is deleted when the trigger is fired" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "ruleDelete"
+            val triggerName = "ruleDeleteTrigger"
+            val actionName = "ruleDeleteAction"
+
+            assetHelper.withCleaner(wsk.trigger, triggerName) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, Some(defaultAction))
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName, false) {
+                (rule, name) => rule.create(name, triggerName, actionName)
+            }
+
+            val first = wsk.trigger.fire(triggerName, Map("payload" -> "bogus".toJson))
+            wsk.rule.delete(ruleName)
+            wsk.trigger.fire(triggerName, Map("payload" -> "bogus2".toJson))
+
+            withActivation(wsk.activation, first) {
+                activation =>
+                    // tries to find 2 activations for the action, should only find 1
+                    val activations = wsk.activation.pollFor(2, Some(actionName), since = Some(Instant.ofEpochMilli(activation.start)), retries = 30)
+
+                    activations.length shouldBe 1
+            }
+    }
+
+    it should "enable and disable a rule and check action is activated only when rule is enabled" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "ruleDisable"
+            val triggerName = "ruleDisableTrigger"
+            val actionName = "ruleDisableAction"
+
+            ruleSetup(Seq(
+                (ruleName, triggerName, (actionName, defaultAction))),
+                assetHelper)
+
+            val first = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+            wsk.rule.disableRule(ruleName)
+            wsk.trigger.fire(triggerName, Map("payload" -> s"$testString with added words".toJson))
+            wsk.rule.enableRule(ruleName)
+            wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, first) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName, N = 2, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        activations =>
+                            val results = activations.map(_.response.result)
+                            results should contain theSameElementsAs Seq(Some(testResult), Some(testResult))
+                    }
+            }
+    }
+
+    it should "be able to recreate a rule with the same name and match it successfully" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val ruleName = "ruleRecreate"
+            val triggerName1 = "ruleRecreateTrigger1"
+            val triggerName2 = "ruleRecreateTrigger2"
+            val actionName = "ruleRecreateAction"
+
+            assetHelper.withCleaner(wsk.trigger, triggerName1) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.action, actionName) {
+                (action, name) => action.create(name, Some(defaultAction))
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName, false) {
+                (rule, name) => rule.create(name, triggerName1, actionName)
+            }
+
+            wsk.rule.delete(ruleName)
+
+            assetHelper.withCleaner(wsk.trigger, triggerName2) {
+                (trigger, name) => trigger.create(name)
+            }
+            assetHelper.withCleaner(wsk.rule, ruleName) {
+                (rule, name) => rule.create(name, triggerName2, actionName)
+            }
+
+            val first = wsk.trigger.fire(triggerName2, Map("payload" -> testString.toJson))
+            withActivation(wsk.activation, first) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        _.head.response.result shouldBe Some(testResult)
+                    }
+            }
+    }
+
+    it should "connect two triggers via rules to one action and activate it accordingly" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val triggerName1 = "t2to1a"
+            val triggerName2 = "t2to1b"
+            val actionName = "a2to1"
+
+            ruleSetup(Seq(
+                ("r2to1a", triggerName1, (actionName, defaultAction)),
+                ("r2to1b", triggerName2, (actionName, defaultAction))),
+                assetHelper)
+
+            val testPayloads = Seq("got three words", "got four words, period")
+
+            val run = wsk.trigger.fire(triggerName1, Map("payload" -> testPayloads(0).toJson))
+            wsk.trigger.fire(triggerName2, Map("payload" -> testPayloads(1).toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName, N = 2, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        activations =>
+                            val results = activations.map(_.response.result)
+                            val expectedResults = testPayloads.map { payload =>
+                                Some(JsObject("count" -> payload.split(" ").length.toJson))
+                            }
+
+                            results should contain theSameElementsAs expectedResults
+                    }
+            }
+    }
+
+    it should "connect one trigger to two different actions, invoking them both eventually" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val triggerName = "t1to2"
+            val actionName1 = "a1to2a"
+            val actionName2 = "a1to2b"
+
+            ruleSetup(Seq(
+                ("r1to2a", triggerName, (actionName1, defaultAction)),
+                ("r1to2b", triggerName, (actionName2, secondAction))),
+                assetHelper)
+
+            val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName1, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        _.head.response.result shouldBe Some(testResult)
+                    }
+                    withActivationsFromEntity(wsk.activation, actionName2, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        _.head.logs.get.mkString(" ") should include(s"hello $testString")
+                    }
+            }
+    }
+
+    it should "connect two triggers to two different actions, invoking them both eventually" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            val triggerName1 = "t1to2a"
+            val triggerName2 = "t1to2b"
+            val actionName1 = "a1to2a"
+            val actionName2 = "a1to2b"
+
+            ruleSetup(Seq(
+                ("r2to2a", triggerName1, (actionName1, defaultAction)),
+                ("r2to2b", triggerName1, (actionName2, secondAction)),
+                ("r2to2c", triggerName2, (actionName1, defaultAction)),
+                ("r2to2d", triggerName2, (actionName2, secondAction))),
+                assetHelper)
+
+            val testPayloads = Seq("got three words", "got four words, period")
+            val run = wsk.trigger.fire(triggerName1, Map("payload" -> testPayloads(0).toJson))
+            wsk.trigger.fire(triggerName2, Map("payload" -> testPayloads(1).toJson))
+
+            withActivation(wsk.activation, run) {
+                triggerActivation =>
+                    withActivationsFromEntity(wsk.activation, actionName1, N = 2, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        activations =>
+                            val results = activations.map(_.response.result)
+                            val expectedResults = testPayloads.map { payload =>
+                                Some(JsObject("count" -> payload.split(" ").length.toJson))
+                            }
+
+                            results should contain theSameElementsAs expectedResults
+                    }
+                    withActivationsFromEntity(wsk.activation, actionName2, N = 2, since = Some(Instant.ofEpochMilli(triggerActivation.start))) {
+                        activations =>
+                            // drops the leftmost 39 characters (timestamp + streamname)
+                            val logs = activations.map(_.logs.get.map(_.drop(39))).flatten
+                            val expectedLogs = testPayloads.map { payload => s"hello $payload!" }
+
+                            logs should contain theSameElementsAs expectedLogs
+                    }
+            }
+    }
+
+}