Retool rules tests

Extracted common code from various XxxBaseTest classes into:
- Topics class to manage messages for test topics
- HttpClients class to manage HttpClient objects for tests
- Simulators class to manage simulators for tests
- Rules class to manage start up and shutdown of rules

Merged remaining code from XxxBaseTest classes into a single
class.  Modified the Frankfurt and Usescases tests to subclass
from this new class and specify just the relevant tests to be
executed.

Issue-ID: POLICY-2385
Signed-off-by: Jim Hahn <jrh3@att.com>
Change-Id: Iaf83c9d2b205a4c343e0dde23ec86508f5773693
diff --git a/controlloop/common/controller-frankfurt/pom.xml b/controlloop/common/controller-frankfurt/pom.xml
index 9633f1f..3951170 100644
--- a/controlloop/common/controller-frankfurt/pom.xml
+++ b/controlloop/common/controller-frankfurt/pom.xml
@@ -190,9 +190,9 @@
             <optional>true</optional>
         </dependency>
         <dependency>
-            <groupId>org.onap.policy.models.policy-models-interactions</groupId>
-            <artifactId>simulators</artifactId>
-            <version>${policy.models.version}</version>
+            <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+            <artifactId>rules-test</artifactId>
+            <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtBase.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtBase.java
deleted file mode 100644
index 0ff3de5..0000000
--- a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtBase.java
+++ /dev/null
@@ -1,574 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.Properties;
-import java.util.Queue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import lombok.Getter;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.kie.api.event.rule.AfterMatchFiredEvent;
-import org.kie.api.event.rule.BeforeMatchFiredEvent;
-import org.kie.api.event.rule.DefaultAgendaEventListener;
-import org.kie.api.event.rule.DefaultRuleRuntimeEventListener;
-import org.kie.api.event.rule.MatchCancelledEvent;
-import org.kie.api.event.rule.MatchCreatedEvent;
-import org.kie.api.event.rule.ObjectDeletedEvent;
-import org.kie.api.event.rule.ObjectInsertedEvent;
-import org.kie.api.event.rule.ObjectUpdatedEvent;
-import org.kie.api.event.rule.RuleRuntimeEventListener;
-import org.kie.api.runtime.KieSession;
-import org.onap.policy.common.endpoints.event.comm.Topic;
-import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
-import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
-import org.onap.policy.common.endpoints.event.comm.TopicListener;
-import org.onap.policy.common.endpoints.http.client.HttpClientConfigException;
-import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
-import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance;
-import org.onap.policy.common.utils.coder.Coder;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.common.utils.coder.StandardCoder;
-import org.onap.policy.common.utils.resources.ResourceUtils;
-import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
-import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2;
-import org.onap.policy.drools.controller.DroolsController;
-import org.onap.policy.drools.persistence.SystemPersistence;
-import org.onap.policy.drools.persistence.SystemPersistenceConstants;
-import org.onap.policy.drools.protocol.coders.EventProtocolCoderConstants;
-import org.onap.policy.drools.system.PolicyController;
-import org.onap.policy.drools.system.PolicyControllerConstants;
-import org.onap.policy.drools.system.PolicyEngine;
-import org.onap.policy.drools.system.PolicyEngineConstants;
-import org.onap.policy.drools.util.KieUtils;
-import org.onap.policy.drools.utils.logging.LoggerUtil;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate;
-import org.onap.policy.simulators.Util;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Use Cases Tests Framework.
- */
-public abstract class FrankfurtBase {
-
-    private static final Logger logger = LoggerFactory.getLogger(FrankfurtBase.class);
-    private static final StandardCoder coder = new StandardCoder();
-
-    /**
-     * PDP-D Engine.
-     */
-    protected static final PolicyEngine pdpD = PolicyEngineConstants.getManager();
-
-    /**
-     * PDP-D Configuration Repository.
-     */
-    protected static final SystemPersistence repo = SystemPersistenceConstants.getManager();
-
-    /**
-     * Frankfurt controller and session name.
-     */
-    protected static final String CONTROLLER_NAME = "frankfurt";
-
-    /**
-     * Frankfurt controller.
-     */
-    protected static PolicyController controller;
-
-    /*
-     * Canonical Topic Names.
-     */
-    protected static final String DCAE_TOPIC = "DCAE_TOPIC";
-    protected static final String APPC_LCM_WRITE_TOPIC = "APPC-LCM-WRITE";
-    protected static final String POLICY_CL_MGT_TOPIC = "POLICY-CL-MGT";
-    protected static final String APPC_LCM_READ_TOPIC = "APPC-LCM-READ";
-    protected static final String APPC_CL_TOPIC = "APPC-CL";
-
-    protected static void initConfigDir() {
-        SystemPersistenceConstants.getManager().setConfigurationDir("src/test/resources/config");
-    }
-
-    protected void resetFacts() {
-        DroolsController drools = controller.getDrools();
-        drools.delete(ToscaPolicy.class);
-        drools.delete(ControlLoopParams.class);
-        drools.delete(ControlLoopEventManager2.class);
-        drools.delete(VirtualControlLoopEvent.class);
-    }
-
-    /**
-     * Sets up overall logging.
-     */
-    protected static void setupLogging() {
-        LoggerUtil.setLevel(LoggerUtil.ROOT_LOGGER, "WARN");
-        LoggerUtil.setLevel("org.eclipse.jetty", "WARN");
-        LoggerUtil.setLevel("org.onap.policy.controlloop", "INFO");
-        LoggerUtil.setLevel("network", "INFO");
-    }
-
-    /**
-     * Sets up Drools Logging for events of interest.
-     */
-    protected static void setupDroolsLogging() {
-        KieSession session = PolicyControllerConstants.getFactory().get(CONTROLLER_NAME).getDrools().getContainer()
-                        .getPolicySession(CONTROLLER_NAME).getKieSession();
-
-        session.addEventListener(new RuleListenerLogger());
-        session.addEventListener(new AgendaListenerLogger());
-    }
-
-    /**
-     * Sets up Http Clients specified in the property file.
-     */
-    protected static void setUpHttpClients() {
-        try {
-            HttpClientFactoryInstance.getClientFactory().build(
-                            SystemPersistenceConstants.getManager().getHttpClientProperties("frankfurt"));
-        } catch (HttpClientConfigException e) {
-            throw new IllegalArgumentException("cannot initialize HTTP clients", e);
-        }
-    }
-
-    /**
-     * Sets up Simulators for use case testing.
-     */
-    protected static void setupSimulators() throws InterruptedException {
-        Util.buildAaiSim();
-        Util.buildSoSim();
-        Util.buildVfcSim();
-        Util.buildGuardSim();
-        Util.buildSdncSim();
-    }
-
-    /**
-     * Returns the runtime Control Loop Parameters associated with a Tosca Policy.
-     */
-    protected ControlLoopParams clParameters(ToscaPolicy policy) {
-        return controller.getDrools().facts(CONTROLLER_NAME, ControlLoopParams.class).stream()
-                        .filter((params) -> params.getToscaPolicy() == policy).findFirst().get();
-    }
-
-    protected ToscaPolicy getPolicyFromResource(String resourcePath, String policyName) throws CoderException {
-        String policyJson = ResourceUtils.getResourceAsString(resourcePath);
-        ToscaServiceTemplate serviceTemplate = coder.decode(policyJson, ToscaServiceTemplate.class);
-        ToscaPolicy policy = serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
-        assertNotNull(policy);
-
-        /*
-         * name and version are used within a drl. api component and drools core will
-         * ensure that these are populated.
-         */
-        if (StringUtils.isBlank(policy.getName())) {
-            policy.setName(policyName);
-        }
-
-        if (StringUtils.isBlank(policy.getVersion())) {
-            policy.setVersion(policy.getTypeVersion());
-        }
-
-        return serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
-    }
-
-    protected ToscaPolicy getPolicyFromFile(String policyPath) throws IOException, CoderException {
-        String rawPolicy = new String(Files.readAllBytes(Paths.get(policyPath)));
-        return coder.decode(rawPolicy, ToscaPolicy.class);
-    }
-
-    private ToscaPolicy setupPolicy(ToscaPolicy policy) throws InterruptedException {
-        final KieObjectExpectedCallback<?> policyTracker = new KieObjectInsertedExpectedCallback<>(policy);
-        final KieObjectExpectedCallback<?> paramsTracker =
-                        new KieClassInsertedExpectedCallback<>(ControlLoopParams.class);
-
-        controller.getDrools().offer(policy);
-
-        assertTrue(policyTracker.isNotified());
-        assertTrue(paramsTracker.isNotified());
-
-        assertEquals(1, controller.getDrools().facts(CONTROLLER_NAME, ToscaPolicy.class).stream()
-                        .filter((anotherPolicy) -> anotherPolicy == policy).count());
-
-        assertEquals(1, controller.getDrools().facts(CONTROLLER_NAME, ControlLoopParams.class).stream()
-                        .filter((params) -> params.getToscaPolicy() == policy).count());
-        return policy;
-    }
-
-    /**
-     * Installs a policy from policy/models (examples) repo.
-     */
-    protected ToscaPolicy setupPolicyFromResource(String resourcePath, String policyName)
-                    throws CoderException, InterruptedException {
-        return setupPolicy(getPolicyFromResource(resourcePath, policyName));
-    }
-
-
-    /**
-     * Installs a given policy.
-     */
-    protected ToscaPolicy setupPolicyFromFile(String policyPath)
-                    throws IOException, CoderException, InterruptedException {
-        return setupPolicy(getPolicyFromFile(policyPath));
-    }
-
-    /**
-     * Deletes a policy.
-     */
-    protected void deletePolicy(ToscaPolicy policy) throws InterruptedException {
-        ControlLoopParams clParams = clParameters(policy);
-        assertNotNull(clParams);
-
-        final KieObjectExpectedCallback<?> policyTracker = new KieObjectDeletedExpectedCallback<>(policy);
-        final KieObjectExpectedCallback<?> clParamsTracker = new KieObjectDeletedExpectedCallback<>(clParams);
-
-        controller.getDrools().delete(CONTROLLER_NAME, policy);
-        assertTrue(policyTracker.isNotified());
-        assertTrue(clParamsTracker.isNotified());
-
-        assertEquals(0, controller.getDrools().facts(CONTROLLER_NAME, ToscaPolicy.class).stream()
-                        .filter((anotherPolicy) -> anotherPolicy == policy).count());
-
-        assertEquals(0, controller.getDrools().facts(CONTROLLER_NAME, ControlLoopParams.class).stream()
-                        .filter((params) -> params.getPolicyName() == policy.getName()).count());
-    }
-
-    /**
-     * Prepare a PDP-D to test the Use Cases.
-     */
-    protected static void preparePdpD() throws IOException {
-        KieUtils.installArtifact(Paths.get("src/main/resources/META-INF/kmodule.xml").toFile(),
-                        Paths.get("src/test/resources/frankfurt.pom").toFile(),
-                        "src/main/resources/org/onap/policy/controlloop/",
-                        Collections.singletonList(Paths.get("src/main/resources/frankfurt.drl").toFile()));
-
-        repo.setConfigurationDir("src/test/resources/config");
-        pdpD.configure(new Properties());
-
-        controller = pdpD.createPolicyController(CONTROLLER_NAME, repo.getControllerProperties(CONTROLLER_NAME));
-        pdpD.start();
-
-        setupDroolsLogging();
-    }
-
-    /**
-     * Stop PDP-D.
-     */
-    protected static void stopPdpD() {
-        PolicyControllerConstants.getFactory().shutdown(CONTROLLER_NAME);
-        pdpD.stop();
-    }
-
-    /**
-     * Stops the http clients.
-     */
-    protected static void stopHttpClients() {
-        HttpClientFactoryInstance.getClientFactory().destroy();
-    }
-
-    /**
-     * Stop Simulators.
-     */
-    protected static void stopSimulators() {
-        HttpServletServerFactoryInstance.getServerFactory().destroy();
-    }
-
-    /**
-     * Creates a Topic Sink Callback tracker.
-     */
-    protected <T> TopicCallback<T> createTopicSinkCallback(String topicName, Class<T> clazz) {
-        return new TopicCallback<>(TopicEndpointManager.getManager().getNoopTopicSink(topicName), clazz);
-    }
-
-    /**
-     * Creates a Topic Sink Callback tracker.
-     */
-    protected <T> TopicCallback<T> createTopicSinkCallbackPlain(String topicName, Class<T> clazz, Coder coder) {
-        return new TopicCallbackCoder<>(TopicEndpointManager.getManager().getNoopTopicSink(topicName), clazz, coder);
-    }
-
-    /**
-     * Creates a Topic Source Callback tracker.
-     */
-    protected <T> TopicCallback<T> createTopicSourceCallback(String topicName, Class<T> clazz) {
-        return new TopicCallback<>(TopicEndpointManager.getManager().getNoopTopicSource(topicName), clazz);
-    }
-
-    /**
-     * Injects a message on a Topic Source.
-     */
-    protected void injectOnTopic(String topicName, Path onsetPath) throws IOException {
-        TopicEndpointManager.getManager().getNoopTopicSource(topicName)
-                        .offer(new String(Files.readAllBytes(onsetPath)));
-    }
-
-    /**
-     * Injects a message on a Topic Source, with the given substitution..
-     */
-    protected void injectOnTopic(String topicName, Path path, String newText) throws IOException {
-        String text = IOUtils.toString(path.toUri(), StandardCharsets.UTF_8);
-        text = text.replace("${replaceMe}", newText);
-        TopicEndpointManager.getManager().getNoopTopicSource(topicName).offer(text);
-    }
-
-    /**
-     * Waits for LOCK acquisition and getting a Permit from PDP-X to proceed.
-     */
-    protected void waitForLockAndPermit(ToscaPolicy policy, TopicCallback<VirtualControlLoopNotification> policyClMgt) {
-        String policyName = policy.getIdentifier().getName();
-
-        // TODO register a topic listener instead of using await() ?
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        VirtualControlLoopNotification notif = policyClMgt.getMessages().remove();
-        assertEquals(ControlLoopNotificationType.ACTIVE, notif.getNotification());
-        assertEquals(policyName + ".EVENT", notif.getPolicyName());
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        notif = policyClMgt.getMessages().remove();
-        assertEquals(ControlLoopNotificationType.OPERATION, notif.getNotification());
-        assertEquals(policyName + ".EVENT.MANAGER.PROCESSING", notif.getPolicyName());
-        assertThat(notif.getMessage()).startsWith("Sending guard query");
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        notif = policyClMgt.getMessages().remove();
-        assertEquals(ControlLoopNotificationType.OPERATION, notif.getNotification());
-        assertEquals(policyName + ".EVENT.MANAGER.PROCESSING", notif.getPolicyName());
-        assertThat(notif.getMessage()).startsWith("Guard result").endsWith("Permit");
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        notif = policyClMgt.getMessages().remove();
-        assertEquals(ControlLoopNotificationType.OPERATION, notif.getNotification());
-        assertEquals(policyName + ".EVENT.MANAGER.PROCESSING", notif.getPolicyName());
-        assertThat(notif.getMessage()).startsWith("actor=");
-    }
-
-    /**
-     * Waits for a FINAL SUCCESS transaction notification.
-     */
-    protected void waitForFinalSuccess(ToscaPolicy policy, TopicCallback<VirtualControlLoopNotification> policyClMgt) {
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.FINAL_SUCCESS, policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policy.getIdentifier().getName() + ".EVENT.MANAGER.FINAL",
-                        policyClMgt.getMessages().remove().getPolicyName());
-    }
-
-    /**
-     * Logs Modifications to Working Memory.
-     */
-    static class RuleListenerLogger implements RuleRuntimeEventListener {
-        @Override
-        public void objectInserted(ObjectInsertedEvent event) {
-            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
-            logger.info("RULE {}: inserted {}", ruleName, event.getObject());
-        }
-
-        @Override
-        public void objectUpdated(ObjectUpdatedEvent event) {
-            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
-            logger.info("RULE {}: updated {}", ruleName, event.getObject());
-
-        }
-
-        @Override
-        public void objectDeleted(ObjectDeletedEvent event) {
-            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
-            logger.info("RULE {}: deleted {}", ruleName, event.getOldObject());
-        }
-    }
-
-    /**
-     * Logs Rule Matches.
-     */
-    static class AgendaListenerLogger extends DefaultAgendaEventListener {
-        @Override
-        public void matchCreated(MatchCreatedEvent event) {
-            logger.info("RULE {}: match created", event.getMatch().getRule().getName());
-        }
-
-        @Override
-        public void matchCancelled(MatchCancelledEvent event) {
-            logger.info("RULE {}: match cancelled", event.getMatch().getRule().getName());
-        }
-
-        @Override
-        public void beforeMatchFired(BeforeMatchFiredEvent event) {
-            logger.info("RULE {}: before match fired", event.getMatch().getRule().getName());
-        }
-
-        @Override
-        public void afterMatchFired(AfterMatchFiredEvent event) {
-            logger.info("RULE {}: after match fired", event.getMatch().getRule().getName());
-        }
-    }
-
-    /**
-     * Base Class to track Working Memory updates for objects of type T.
-     */
-    abstract class KieObjectExpectedCallback<T> extends DefaultRuleRuntimeEventListener {
-        protected T subject;
-
-        protected CountDownLatch countDownLatch = new CountDownLatch(1);
-
-        public KieObjectExpectedCallback(T affected) {
-            subject = affected;
-            register();
-        }
-
-        public boolean isNotified() throws InterruptedException {
-            return countDownLatch.await(9L, TimeUnit.SECONDS);
-        }
-
-        protected void callbacked() {
-            unregister();
-            countDownLatch.countDown();
-        }
-
-        public KieObjectExpectedCallback<T> register() {
-            controller.getDrools().getContainer().getPolicySession(CONTROLLER_NAME).getKieSession()
-                            .addEventListener(this);
-            return this;
-        }
-
-        public KieObjectExpectedCallback<T> unregister() {
-            controller.getDrools().getContainer().getPolicySession(CONTROLLER_NAME).getKieSession()
-                            .removeEventListener(this);
-            return this;
-        }
-    }
-
-    /**
-     * Tracks inserts in Working Memory for an object of type T.
-     */
-    class KieObjectInsertedExpectedCallback<T> extends KieObjectExpectedCallback<T> {
-        public KieObjectInsertedExpectedCallback(T affected) {
-            super(affected);
-        }
-
-        @Override
-        public void objectInserted(ObjectInsertedEvent event) {
-            if (subject == event.getObject()) {
-                callbacked();
-            }
-        }
-    }
-
-    /**
-     * Tracks deletes in Working Memory of an object of type T.
-     */
-    class KieObjectDeletedExpectedCallback<T> extends KieObjectExpectedCallback<T> {
-        public KieObjectDeletedExpectedCallback(T affected) {
-            super(affected);
-        }
-
-        @Override
-        public void objectDeleted(ObjectDeletedEvent event) {
-            if (subject == event.getOldObject()) {
-                callbacked();
-            }
-        }
-    }
-
-    /**
-     * Tracks inserts in Working Memory for any object of class T.
-     */
-    class KieClassInsertedExpectedCallback<T> extends KieObjectInsertedExpectedCallback<T> {
-
-        public KieClassInsertedExpectedCallback(T affected) {
-            super(affected);
-        }
-
-        public void objectInserted(ObjectInsertedEvent event) {
-            if (subject == event.getObject().getClass()) {
-                callbacked();
-            }
-        }
-    }
-
-    /**
-     * Tracks callbacks from topics.
-     */
-    class TopicCallback<T> implements TopicListener {
-        protected final Topic topic;
-        protected final Class<T> expectedClass;
-
-        @Getter
-        protected Queue<T> messages = new LinkedList<>();
-
-        public TopicCallback(Topic topic, Class<T> expectedClass) {
-            this.topic = topic;
-            this.expectedClass = expectedClass;
-            this.topic.register(this);
-        }
-
-        public TopicCallback<T> register() {
-            this.topic.register(this);
-            return this;
-        }
-
-        public TopicCallback<T> unregister() {
-            this.topic.unregister(this);
-            return this;
-        }
-
-        @Override
-        @SuppressWarnings("unchecked")
-        public void onTopicEvent(CommInfrastructure comm, String topic, String event) {
-            try {
-                messages.add((T) EventProtocolCoderConstants.getManager().decode(controller.getDrools().getGroupId(),
-                                controller.getDrools().getArtifactId(), topic, event));
-            } catch (Exception e) {
-                logger.warn("invalid mapping in topic {} for event {}", topic, event, e);
-            }
-        }
-    }
-
-    class TopicCallbackCoder<T> extends TopicCallback<T> {
-        private final Coder coder;
-
-        public TopicCallbackCoder(Topic topic, Class<T> expectedClass, Coder coder) {
-            super(topic, expectedClass);
-            this.coder = coder;
-        }
-
-        @Override
-        public void onTopicEvent(CommInfrastructure comm, String topic, String event) {
-            try {
-                messages.add((T) coder.decode(event, expectedClass));
-            } catch (Exception e) {
-                logger.warn("invalid mapping in topic {} for event {}", topic, event, e);
-            }
-        }
-
-    }
-}
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtTest.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtTest.java
new file mode 100644
index 0000000..7b8b99a
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtTest.java
@@ -0,0 +1,113 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.onap.policy.controlloop.common.rules.test.BaseRuleTest;
+import org.onap.policy.controlloop.common.rules.test.Listener;
+import org.onap.policy.controlloop.common.rules.test.NamedRunner;
+import org.onap.policy.controlloop.common.rules.test.TestNames;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+import org.onap.policy.simulators.Util;
+
+/**
+ * Tests use cases using Frankfurt rules.
+ *
+ * <p/>
+ * Note: this runs ALL tests (i.e., any whose names start with "test").
+ */
+@RunWith(NamedRunner.class)
+@TestNames(prefixes = {"test"})
+
+public class FrankfurtTest extends BaseRuleTest {
+    protected static final String CONTROLLER_NAME = "frankfurt";
+
+
+    /**
+     * Sets up statics.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        initStatics(CONTROLLER_NAME);
+
+        rules.configure("src/main/resources");
+        rules.start();
+        httpClients.addClients("frankfurt");
+        simulators.start(Util::buildAaiSim, Util::buildSoSim, Util::buildVfcSim, Util::buildGuardSim,
+                        Util::buildSdncSim);
+    }
+
+    /**
+     * Cleans up statics.
+     */
+    @AfterClass
+    public static void tearDownAfterClass() {
+        finishStatics();
+    }
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        init();
+    }
+
+    /**
+     * Tears down.
+     */
+    @After
+    public void tearDown() {
+        finish();
+    }
+
+    @Override
+    protected void waitForLockAndPermit(ToscaPolicy policy, Listener<VirtualControlLoopNotification> policyClMgt) {
+        String policyName = policy.getIdentifier().getName();
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.ACTIVE
+                        && (policyName + ".EVENT").equals(notif.getPolicyName()));
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.OPERATION
+                        && (policyName + ".EVENT.MANAGER.PROCESSING").equals(notif.getPolicyName())
+                        && notif.getMessage().startsWith("Sending guard query"));
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.OPERATION
+                        && (policyName + ".EVENT.MANAGER.PROCESSING").equals(notif.getPolicyName())
+                        && notif.getMessage().startsWith("Guard result") && notif.getMessage().endsWith("Permit"));
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.OPERATION
+                        && (policyName + ".EVENT.MANAGER.PROCESSING").equals(notif.getPolicyName())
+                        && notif.getMessage().startsWith("actor="));
+    }
+
+    @Override
+    protected VirtualControlLoopNotification waitForFinal(ToscaPolicy policy,
+                    Listener<VirtualControlLoopNotification> policyClMgt, ControlLoopNotificationType finalType) {
+
+        return policyClMgt.await(notif -> notif.getNotification() == finalType
+                        && (policy.getIdentifier().getName() + ".EVENT.MANAGER.FINAL").equals(notif.getPolicyName()));
+    }
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VcpeTest.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VcpeTest.java
deleted file mode 100644
index ff3f742..0000000
--- a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VcpeTest.java
+++ /dev/null
@@ -1,206 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.nio.file.Paths;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.common.utils.coder.StandardCoder;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-
-/**
- * VCPE Use Case Tests.
- */
-public class VcpeTest extends FrankfurtBase {
-
-    /**
-     * VCPE Tosca Policy File.
-     */
-    private static final String TOSCA_LEGACY_POLICY_VCPE = "src/test/resources/vcpe/tosca-legacy-vcpe.json";
-    private static final String TOSCA_COMPLIANT_POLICY_VCPE = "src/test/resources/vcpe/tosca-compliant-vcpe.json";
-
-    /*
-     * VCPE Use case Messages.
-     */
-    private static final String APPC_SUCCESS = "src/test/resources/vcpe/vcpe.appc.success.json";
-    private static final String ONSET_1 = "src/test/resources/vcpe/vcpe.onset.1.json";
-    private static final String ONSET_2 = "src/test/resources/vcpe/vcpe.onset.2.json";
-    private static final String ONSET_3 = "src/test/resources/vcpe/vcpe.onset.3.json";
-
-    /*
-     * Topic trackers used by the VCPE use case.
-     */
-    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
-    private TopicCallback<AppcLcmDmaapWrapper> appcLcmRead;
-    private TopicCallback<AppcLcmDmaapWrapper> appcLcmWrite;
-
-    /*
-     * VCPE Tosca Policy.
-     */
-    private ToscaPolicy policy;
-
-    /**
-     * Prepare PDP-D Framework for testing.
-     */
-    @BeforeClass
-    public static void prepareResouces() throws InterruptedException, CoderException, IOException {
-        initConfigDir();
-        setupLogging();
-        preparePdpD();
-        setUpHttpClients();
-        setupSimulators();
-    }
-
-    /**
-     * Take down the resources used by the test framework.
-     */
-    @AfterClass
-    public static void takeDownResources() {
-        stopPdpD();
-        stopSimulators();
-    }
-
-    /**
-     * Observe Topics.
-     */
-    @Before
-    public void topicsRegistration() {
-        resetFacts();
-
-        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
-        appcLcmRead = createTopicSinkCallbackPlain(APPC_LCM_READ_TOPIC, AppcLcmDmaapWrapper.class, new StandardCoder());
-        appcLcmWrite = createTopicSourceCallback(APPC_LCM_WRITE_TOPIC, AppcLcmDmaapWrapper.class);
-    }
-
-    /**
-     * Unregister Topic Callbacks.
-     */
-    @After
-    public void topicsUnregistration() throws InterruptedException {
-        if (policyClMgt != null) {
-            policyClMgt.unregister();
-        }
-
-        if (appcLcmRead != null) {
-            appcLcmRead.unregister();
-        }
-
-        if (appcLcmWrite != null) {
-            appcLcmWrite.unregister();
-        }
-    }
-
-    /**
-     * Sunny Day with Legacy Tosca Policy.
-     */
-    @Test
-    public void sunnyDayLegacy() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VCPE);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny Day with Tosca Compliant Policy.
-     */
-    @Test
-    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VCPE);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        sunnyDay();
-    }
-
-    /**
-     * An ONSET flood prevention test that injects a few ONSETs at once. It attempts to
-     * simulate the flooding behavior of the DCAE TCA microservice. TCA could blast tenths
-     * or hundreds of ONSETs within sub-second intervals.
-     */
-    @Test
-    public void onsetFloodPrevention() throws IOException, InterruptedException, CoderException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VCPE);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_1));
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_2));
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_3));
-
-        assertEquals(1, controller.getDrools().facts(CONTROLLER_NAME, VirtualControlLoopEvent.class).stream().count());
-        assertEquals(1, controller.getDrools().facts(CONTROLLER_NAME, CanonicalOnset.class).stream().count());
-        assertEquals(controller.getDrools().facts(CONTROLLER_NAME, CanonicalOnset.class).get(0),
-                        controller.getDrools().facts(CONTROLLER_NAME, VirtualControlLoopEvent.class).get(0));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny day scenario for the VCPE use case.
-     */
-    public void sunnyDay() throws IOException {
-
-        /* Inject an ONSET event over the DCAE topic */
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_1));
-
-        /* Wait to acquire a LOCK and a PDP-X PERMIT */
-        waitForLockAndPermit(policy, policyClMgt);
-
-        /* --- VCPE Operation Execution --- */
-
-        /*
-         * Ensure that an APPC RESTART request was sent in response to the matching ONSET
-         */
-        await().until(() -> !appcLcmRead.getMessages().isEmpty());
-        AppcLcmDmaapWrapper appcreq = appcLcmRead.getMessages().remove();
-        assertEquals("restart", appcreq.getRpcName());
-
-        /* Inject a 400 APPC Response Return over the APPC topic */
-        injectOnTopic(APPC_LCM_WRITE_TOPIC, Paths.get(APPC_SUCCESS),
-                        appcreq.getBody().getInput().getCommonHeader().getSubRequestId());
-
-        /* Ensure that RESTART response is received */
-        await().until(() -> !appcLcmWrite.getMessages().isEmpty());
-        assertEquals("restart", appcLcmWrite.getMessages().peek().getRpcName());
-        assertEquals(400, appcLcmWrite.getMessages().remove().getBody().getOutput().getStatus().getCode());
-
-        /* --- VCPE Operation Completed --- */
-
-        /* Ensure that the VCPE RESTART Operation is successfully completed */
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS,
-                        policyClMgt.getMessages().remove().getNotification());
-
-        /* --- VCPE Transaction Completed --- */
-        waitForFinalSuccess(policy, policyClMgt);
-    }
-}
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VdnsTest.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VdnsTest.java
deleted file mode 100644
index c9963a4..0000000
--- a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VdnsTest.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.nio.file.Paths;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-
-/**
- * VDNS Use Case Tests.
- */
-public class VdnsTest extends FrankfurtBase {
-
-    /**
-     * VDNS Tosca Policy File.
-     */
-    private static final String TOSCA_COMPLIANT_POLICY_VDNS = "src/test/resources/vdns/tosca-compliant-vdns.json";
-
-    /*
-     * VDNS Use case Messages.
-     */
-    private static final String ONSET = "src/test/resources/vdns/vdns.onset.json";
-
-    /*
-     * Topic trackers used by the VDNS use case.
-     */
-    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
-
-    /*
-     * VDNS Tosca Policy.
-     */
-    private ToscaPolicy policy;
-
-    /**
-     * Prepare PDP-D Framework for testing.
-     */
-    @BeforeClass
-    public static void prepareResouces() throws InterruptedException, IOException {
-        initConfigDir();
-        setupLogging();
-        preparePdpD();
-        setUpHttpClients();
-        setupSimulators();
-    }
-
-    /**
-     * Take down the resources used by the test framework.
-     */
-    @AfterClass
-    public static void takeDownResources() {
-        stopPdpD();
-        stopSimulators();
-    }
-
-    /**
-     * Observe Topics.
-     */
-    @Before
-    public void topicsRegistration() {
-        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
-    }
-
-    /**
-     * Unregister Topic Callbacks and uninstall the policy.
-     */
-    @After
-    public void topicsUnregistration() throws InterruptedException {
-        if (policyClMgt != null) {
-            policyClMgt.unregister();
-        }
-
-        // uninstall the policy
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-        if (policy != null) {
-            deletePolicy(policy);
-        }
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-    }
-
-    /**
-     * Sunny Day with Tosca Compliant Policy.
-     */
-    @Test
-    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VDNS);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny day scenario for the VCPE use case.
-     */
-    private void sunnyDay() throws IOException {
-
-        /* Inject an ONSET event over the DCAE topic */
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET));
-
-        /* Wait to acquire a LOCK and a PDP-X PERMIT */
-        waitForLockAndPermit(policy, policyClMgt);
-
-        /* Ensure that the VDNS SO Operation was successfully completed */
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS,
-                        policyClMgt.getMessages().remove().getNotification());
-
-        /* --- VDNS Transaction Completed --- */
-        waitForFinalSuccess(policy, policyClMgt);
-    }
-}
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VfwTest.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VfwTest.java
deleted file mode 100644
index 2d0cb76..0000000
--- a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VfwTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.nio.file.Paths;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.onap.policy.appc.Request;
-import org.onap.policy.appc.Response;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.common.utils.coder.StandardCoderInstantAsMillis;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-
-/**
- * VFW Use Case Tests.
- */
-public class VfwTest extends FrankfurtBase {
-
-    /**
-     * VFW Tosca Policy File.
-     */
-    private static final String TOSCA_LEGACY_POLICY_VFW = "src/test/resources/vfw/tosca-vfw.json";
-    private static final String TOSCA_COMPLIANT_POLICY_VFW = "src/test/resources/vfw/tosca-compliant-vfw.json";
-
-    /*
-     * VFW Use case Messages.
-     */
-    private static final String APPC_SUCCESS = "src/test/resources/vfw/vfw.appc.success.json";
-    private static final String ONSET = "src/test/resources/vfw/vfw.onset.json";
-
-    /*
-     * Topic trackers used by the VFW use case.
-     */
-    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
-    private TopicCallback<Response> appcClSource;
-    private TopicCallback<Request> appcClSink;
-
-    /*
-     * VFW Tosca Policy.
-     */
-    private ToscaPolicy policy;
-
-    /**
-     * Prepare PDP-D Framework for testing.
-     */
-    @BeforeClass
-    public static void prepareResouces() throws InterruptedException, IOException {
-        initConfigDir();
-        setupLogging();
-        preparePdpD();
-        setUpHttpClients();
-        setupSimulators();
-    }
-
-    /**
-     * Take down the resources used by the test framework.
-     */
-    @AfterClass
-    public static void takeDownResources() {
-        stopPdpD();
-        stopSimulators();
-    }
-
-    /**
-     * Observe Topics.
-     */
-    @Before
-    public void topicsRegistration() {
-        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
-        appcClSink = createTopicSinkCallbackPlain(APPC_CL_TOPIC, Request.class, new StandardCoderInstantAsMillis());
-        appcClSource = createTopicSourceCallback(APPC_CL_TOPIC, Response.class);
-    }
-
-    /**
-     * Unregister Topic Callbacks.
-     */
-    @After
-    public void topicsUnregistration() throws InterruptedException {
-        if (policyClMgt != null) {
-            policyClMgt.unregister();
-        }
-
-        if (appcClSource != null) {
-            appcClSource.unregister();
-        }
-
-        if (appcClSink != null) {
-            appcClSink.unregister();
-        }
-
-        // uninstall the policy
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-        if (policy != null) {
-            deletePolicy(policy);
-        }
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-    }
-
-    /**
-     * Sunny Day with Legacy Tosca Policy.
-     */
-    @Test
-    public void sunnyDayLegacy() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VFW);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny Day with Tosca Compliant Policy.
-     */
-    @Test
-    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VFW);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny day scenario for the VFW use case.
-     */
-    private void sunnyDay() throws IOException {
-
-        /* Inject an ONSET event over the DCAE topic */
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET));
-
-        /* Wait to acquire a LOCK and a PDP-X PERMIT */
-        waitForLockAndPermit(policy, policyClMgt);
-
-        /* --- VFW Operation Execution --- */
-
-        /*
-         * Ensure that an APPC RESTART request was sent in response to the matching ONSET
-         */
-        await().until(() -> !appcClSink.getMessages().isEmpty());
-        Request appcreq = appcClSink.getMessages().remove();
-        assertEquals("ModifyConfig", appcreq.getAction());
-
-        /*
-         * Inject a 400 APPC Response Return over the APPC topic, with appropriate
-         * subRequestId
-         */
-        injectOnTopic(APPC_CL_TOPIC, Paths.get(APPC_SUCCESS), appcreq.getCommonHeader().getSubRequestId());
-
-        /* Ensure that RESTART response is received */
-        await().until(() -> !appcClSource.getMessages().isEmpty());
-        assertEquals("SUCCESS", appcClSource.getMessages().remove().getStatus().getValue());
-
-        /* --- VFW Operation Completed --- */
-
-        /* Ensure that the VFW RESTART Operation is successfully completed */
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS,
-                        policyClMgt.getMessages().remove().getNotification());
-
-        /* --- VFW Transaction Completed --- */
-        waitForFinalSuccess(policy, policyClMgt);
-    }
-}
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VlbTest.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VlbTest.java
deleted file mode 100644
index 4e5a6e3..0000000
--- a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VlbTest.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.nio.file.Paths;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-
-/**
- * VLB Use Case Tests.
- */
-public class VlbTest extends FrankfurtBase {
-
-    /**
-     * VLB Tosca Policy File.
-     */
-    private static final String TOSCA_LEGACY_POLICY_VLB = "src/test/resources/vlb/tosca-vlb.json";
-    private static final String TOSCA_COMPLIANT_POLICY_VLB = "src/test/resources/vlb/tosca-compliant-vlb.json";
-
-    /*
-     * VLB Use case Messages.
-     */
-    private static final String ONSET = "src/test/resources/vlb/vlb.onset.json";
-
-    /*
-     * Topic trackers used by the VLB use case.
-     */
-    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
-
-    /*
-     * VLB Tosca Policy.
-     */
-    private ToscaPolicy policy;
-
-    /**
-     * Prepare PDP-D Framework for testing.
-     */
-    @BeforeClass
-    public static void prepareResouces() throws InterruptedException, IOException {
-        initConfigDir();
-        setupLogging();
-        preparePdpD();
-        setUpHttpClients();
-        setupSimulators();
-    }
-
-    /**
-     * Take down the resources used by the test framework.
-     */
-    @AfterClass
-    public static void takeDownResources() {
-        stopPdpD();
-        stopSimulators();
-    }
-
-    /**
-     * Observe Topics.
-     */
-    @Before
-    public void topicsRegistration() {
-        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
-    }
-
-    /**
-     * Unregister Topic Callbacks and uninstall the policy.
-     */
-    @After
-    public void topicsUnregistration() throws InterruptedException {
-        if (policyClMgt != null) {
-            policyClMgt.unregister();
-        }
-
-        // uninstall the policy
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-        if (policy != null) {
-            deletePolicy(policy);
-        }
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-    }
-
-    /**
-     * Sunny Day with Legacy Tosca Policy.
-     */
-    @Test
-    public void sunnyDayLegacy() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VLB);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny Day with Tosca Compliant Policy.
-     */
-    @Test
-    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
-        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VLB);
-        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny day scenario for the VCPE use case.
-     */
-    private void sunnyDay() throws IOException {
-
-        /* Inject an ONSET event over the DCAE topic */
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET));
-
-        /* Wait to acquire a LOCK and a PDP-X PERMIT */
-        waitForLockAndPermit(policy, policyClMgt);
-
-        /* Ensure that the VLB SO Operation was successfully completed */
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS,
-                        policyClMgt.getMessages().remove().getNotification());
-
-        /* --- VLB Transaction Completed --- */
-        waitForFinalSuccess(policy, policyClMgt);
-    }
-}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-controller.properties b/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-controller.properties
index 694f66e..025a60b 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-controller.properties
+++ b/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-controller.properties
@@ -28,10 +28,10 @@
 
 noop.source.topics.DCAE_TOPIC.events=\
     org.onap.policy.controlloop.CanonicalOnset,org.onap.policy.controlloop.CanonicalAbated
-noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalOnset.filter=\
-    [?($.closedLoopEventStatus == 'ONSET')]
-noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalAbated.filter=\
-    [?($.closedLoopEventStatus == 'ABATED')]
+noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalOnset.\
+    filter=[?($.closedLoopEventStatus == 'ONSET')]
+noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalAbated.\
+    filter=[?($.closedLoopEventStatus == 'ABATED')]
 noop.source.topics.DCAE_TOPIC.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gson
 
 noop.source.topics.APPC-CL.events=org.onap.policy.appc.Response,org.onap.policy.appc.Request
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.3.json b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.3.json
deleted file mode 100644
index 74a94eb..0000000
--- a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.3.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
-  "closedLoopAlarmStart": 1570722876324905,
-  "closedLoopAlarmEnd": 1570722876324999,
-  "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
-  "closedLoopEventStatus": "ONSET",
-  "requestID": "664be3d2-6c12-4f4b-a3e7-c349acced200",
-  "target_type": "VNF",
-  "target": "generic-vnf.vnf-id",
-  "AAI": {
-    "vserver.is-closed-loop-disabled": "false",
-    "vserver.prov-status": "ACTIVE",
-    "generic-vnf.vnf-id": "vCPE_Infrastructure_vGMUX_demo_app"
-  },
-  "from": "DCAE",
-  "version": "1.0.2"
-}
\ No newline at end of file
diff --git a/controlloop/common/controller-usecases/pom.xml b/controlloop/common/controller-usecases/pom.xml
index fb29a81..6c69e7f 100644
--- a/controlloop/common/controller-usecases/pom.xml
+++ b/controlloop/common/controller-usecases/pom.xml
@@ -180,9 +180,9 @@
             <optional>true</optional>
         </dependency>
         <dependency>
-            <groupId>org.onap.policy.models.policy-models-interactions</groupId>
-            <artifactId>simulators</artifactId>
-            <version>${policy.models.version}</version>
+            <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+            <artifactId>rules-test</artifactId>
+            <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/UsecasesBase.java b/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/UsecasesBase.java
deleted file mode 100644
index 66ad324..0000000
--- a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/UsecasesBase.java
+++ /dev/null
@@ -1,515 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.Properties;
-import java.util.Queue;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import lombok.Getter;
-import org.apache.commons.lang3.StringUtils;
-import org.kie.api.event.rule.AfterMatchFiredEvent;
-import org.kie.api.event.rule.BeforeMatchFiredEvent;
-import org.kie.api.event.rule.DefaultAgendaEventListener;
-import org.kie.api.event.rule.DefaultRuleRuntimeEventListener;
-import org.kie.api.event.rule.MatchCancelledEvent;
-import org.kie.api.event.rule.MatchCreatedEvent;
-import org.kie.api.event.rule.ObjectDeletedEvent;
-import org.kie.api.event.rule.ObjectInsertedEvent;
-import org.kie.api.event.rule.ObjectUpdatedEvent;
-import org.kie.api.event.rule.RuleRuntimeEventListener;
-import org.kie.api.runtime.KieSession;
-import org.onap.policy.common.endpoints.event.comm.Topic;
-import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
-import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
-import org.onap.policy.common.endpoints.event.comm.TopicListener;
-import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.common.utils.coder.StandardCoder;
-import org.onap.policy.common.utils.resources.ResourceUtils;
-import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
-import org.onap.policy.drools.persistence.SystemPersistence;
-import org.onap.policy.drools.persistence.SystemPersistenceConstants;
-import org.onap.policy.drools.protocol.coders.EventProtocolCoderConstants;
-import org.onap.policy.drools.system.PolicyController;
-import org.onap.policy.drools.system.PolicyControllerConstants;
-import org.onap.policy.drools.system.PolicyEngine;
-import org.onap.policy.drools.system.PolicyEngineConstants;
-import org.onap.policy.drools.util.KieUtils;
-import org.onap.policy.drools.utils.PropertyUtil;
-import org.onap.policy.drools.utils.logging.LoggerUtil;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate;
-import org.onap.policy.simulators.Util;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Use Cases Tests Framework.
- */
-public abstract class UsecasesBase {
-
-    private static final Logger logger = LoggerFactory.getLogger(UsecasesBase.class);
-    private static final StandardCoder coder = new StandardCoder();
-
-    /**
-     * PDP-D Engine.
-     */
-    protected static final PolicyEngine pdpD = PolicyEngineConstants.getManager();
-
-    /**
-     * PDP-D Configuration Repository.
-     */
-    protected static final SystemPersistence repo = SystemPersistenceConstants.getManager();
-
-    /**
-     * Usecases controller and session name.
-     */
-    protected static final String USECASES = "usecases";
-
-    /**
-     * Usecases controller.
-     */
-    protected static PolicyController usecases;
-
-    /*
-     * Canonical Topic Names.
-     */
-    protected static final String DCAE_TOPIC = "DCAE_TOPIC";
-    protected static final String APPC_LCM_WRITE_TOPIC = "APPC-LCM-WRITE";
-    protected static final String POLICY_CL_MGT_TOPIC = "POLICY-CL-MGT";
-    protected static final String APPC_LCM_READ_TOPIC = "APPC-LCM-READ";
-    protected static final String APPC_CL_TOPIC = "APPC-CL";
-
-    /**
-     * Sets up overall logging.
-     */
-    protected static void setupLogging() {
-        LoggerUtil.setLevel(LoggerUtil.ROOT_LOGGER, "WARN");
-        LoggerUtil.setLevel("org.eclipse.jetty", "WARN");
-        LoggerUtil.setLevel("org.onap.policy.controlloop", "INFO");
-        LoggerUtil.setLevel("network", "INFO");
-    }
-
-    /**
-     * Sets up Drools Logging for events of interest.
-     */
-    protected static void setupDroolsLogging() {
-        KieSession session =
-            PolicyControllerConstants.getFactory().get(USECASES)
-                .getDrools().getContainer() .getPolicySession(USECASES).getKieSession();
-
-        session.addEventListener(new RuleListenerLogger());
-        session.addEventListener(new AgendaListenerLogger());
-    }
-
-    /**
-     * Sets up Simulators for use case testing.
-     */
-    protected static void setupSimulators() throws InterruptedException {
-        Util.buildAaiSim();
-        Util.buildSoSim();
-        Util.buildVfcSim();
-        Util.buildGuardSim();
-        Util.buildSdncSim();
-    }
-
-    /**
-     * Returns the runtime Control Loop Parameters associated with a Tosca Policy.
-     */
-    protected ControlLoopParams clParameters(ToscaPolicy policy) {
-        return usecases
-            .getDrools()
-            .facts(USECASES, ControlLoopParams.class).stream()
-            .filter((params) -> params.getToscaPolicy() == policy)
-            .findFirst()
-            .get();
-    }
-
-    protected ToscaPolicy getPolicyFromResource(String resourcePath, String policyName) throws CoderException {
-        String policyJson = ResourceUtils.getResourceAsString(resourcePath);
-        ToscaServiceTemplate serviceTemplate = coder.decode(policyJson, ToscaServiceTemplate.class);
-        ToscaPolicy policy = serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
-        assertNotNull(policy);
-
-        /*
-         * name and version are used within a drl.  api component and drools core will ensure that these
-         * are populated.
-         */
-        if (StringUtils.isBlank(policy.getName())) {
-            policy.setName(policyName);
-        }
-
-        if (StringUtils.isBlank(policy.getVersion())) {
-            policy.setVersion(policy.getTypeVersion());
-        }
-
-        return serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
-    }
-
-    protected ToscaPolicy getPolicyFromFile(String policyPath) throws IOException, CoderException {
-        String rawPolicy = new String(Files.readAllBytes(Paths.get(policyPath)));
-        return coder.decode(rawPolicy, ToscaPolicy.class);
-    }
-
-    private ToscaPolicy setupPolicy(ToscaPolicy policy) throws InterruptedException {
-        final KieObjectExpectedCallback policyTracker = new KieObjectInsertedExpectedCallback<>(policy);
-        final KieObjectExpectedCallback paramsTracker = new KieClassInsertedExpectedCallback<>(ControlLoopParams.class);
-
-        usecases.getDrools().offer(policy);
-
-        assertTrue(policyTracker.isNotified());
-        assertTrue(paramsTracker.isNotified());
-
-        assertEquals(1, usecases.getDrools().facts(USECASES, ToscaPolicy.class).stream()
-                                .filter((anotherPolicy) -> anotherPolicy == policy).count());
-
-        assertEquals(1, usecases.getDrools().facts(USECASES, ControlLoopParams.class).stream()
-                                .filter((params) -> params.getToscaPolicy() == policy).count());
-        return policy;
-    }
-
-    /**
-     * Installs a policy from policy/models (examples) repo.
-     */
-    protected ToscaPolicy setupPolicyFromResource(String resourcePath, String policyName)
-            throws CoderException, InterruptedException {
-        return setupPolicy(getPolicyFromResource(resourcePath, policyName));
-    }
-
-
-    /**
-     * Installs a given policy.
-     */
-    protected ToscaPolicy setupPolicyFromFile(String policyPath)
-            throws IOException, CoderException, InterruptedException {
-        return setupPolicy(getPolicyFromFile(policyPath));
-    }
-
-    /**
-     * Deletes a policy.
-     */
-    protected void deletePolicy(ToscaPolicy policy) throws InterruptedException {
-        ControlLoopParams clParams = clParameters(policy);
-        assertNotNull(clParams);
-
-        final KieObjectExpectedCallback policyTracker = new KieObjectDeletedExpectedCallback<>(policy);
-        final KieObjectExpectedCallback clParamsTracker = new KieObjectDeletedExpectedCallback<>(clParams);
-
-        usecases.getDrools().delete(USECASES, policy);
-        assertTrue(policyTracker.isNotified());
-        assertTrue(clParamsTracker.isNotified());
-
-        assertEquals(0,
-            usecases
-                .getDrools()
-                .facts(USECASES, ToscaPolicy.class).stream()
-                .filter((anotherPolicy) -> anotherPolicy == policy)
-                .count());
-
-        assertEquals(0,
-            usecases
-                .getDrools()
-                .facts(USECASES, ControlLoopParams.class).stream()
-                .filter((params) -> params.getPolicyName() == policy.getName())
-                .count());
-    }
-
-    /**
-     * Prepare a PDP-D to test the Use Cases.
-     */
-    protected static void preparePdpD() throws IOException {
-        KieUtils.installArtifact(
-            Paths.get("src/main/resources/META-INF/kmodule.xml").toFile(),
-            Paths.get("src/test/resources/usecases.pom").toFile(),
-            "src/main/resources/org/onap/policy/controlloop/",
-            Collections.singletonList(Paths.get("src/main/resources/usecases.drl").toFile()));
-
-        repo.setConfigurationDir("src/test/resources/config");
-        PropertyUtil.setSystemProperties(repo.getSystemProperties("controlloop"));
-        pdpD.setEnvironment(repo.getEnvironmentProperties("controlloop.properties"));
-        pdpD.configure(new Properties());
-
-        usecases = pdpD.createPolicyController(USECASES, repo.getControllerProperties(USECASES));
-        pdpD.start();
-
-        setupDroolsLogging();
-    }
-
-    /**
-     * Stop PDP-D.
-     */
-    protected static void stopPdpD() {
-        PolicyControllerConstants.getFactory().shutdown(USECASES);
-        pdpD.stop();
-    }
-
-    /**
-     * Stop Simulators.
-     */
-    protected static void stopSimulators() {
-        HttpServletServerFactoryInstance.getServerFactory().destroy();
-    }
-
-    /**
-     * Creates a Topic Sink Callback tracker.
-     */
-    protected <T> TopicCallback<T> createTopicSinkCallback(String topicName, Class<T> clazz) {
-        return new TopicCallback(TopicEndpointManager.getManager().getNoopTopicSink(topicName), clazz);
-    }
-
-    /**
-     * Creates a Topic Source Callback tracker.
-     */
-    protected <T> TopicCallback<T> createTopicSourceCallback(String topicName, Class<T> clazz) {
-        return new TopicCallback(TopicEndpointManager.getManager().getNoopTopicSource(topicName), clazz);
-    }
-
-    /**
-     * Injects a message on a Topic Source.
-     */
-    protected void injectOnTopic(String topicName, Path onsetPath) throws IOException {
-        TopicEndpointManager
-            .getManager()
-            .getNoopTopicSource(topicName)
-            .offer(new String(Files.readAllBytes(onsetPath)));
-    }
-
-    /**
-     * Waits for LOCK acquisition and getting a Permit from PDP-X to proceed.
-     */
-    protected void waitForLockAndPermit(ToscaPolicy policy, TopicCallback<VirtualControlLoopNotification> policyClMgt) {
-        String policyName = policy.getIdentifier().getName();
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.ACTIVE, policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policyName + ".EVENT", policyClMgt.getMessages().remove().getPolicyName());
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION,
-            policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policyName + ".EVENT.MANAGER.OPERATION.LOCKED.GUARD_NOT_YET_QUERIED",
-            policyClMgt.getMessages().remove().getPolicyName());
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION,
-            policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policyName + ".GUARD.RESPONSE",
-            policyClMgt.getMessages().remove().getPolicyName());
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION,
-            policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policyName + ".EVENT.MANAGER.OPERATION.LOCKED.GUARD_PERMITTED",
-            policyClMgt.getMessages().remove().getPolicyName());
-    }
-
-    /**
-     * Waits for a FINAL SUCCESS transaction notification.
-     */
-    protected void waitForFinalSuccess(ToscaPolicy policy, TopicCallback<VirtualControlLoopNotification> policyClMgt) {
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.FINAL_SUCCESS, policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policy.getIdentifier().getName() + ".EVENT.MANAGER",
-            policyClMgt.getMessages().remove().getPolicyName());
-    }
-
-    /**
-     * Logs Modifications to Working Memory.
-     */
-    static class RuleListenerLogger implements RuleRuntimeEventListener {
-        @Override
-        public void objectInserted(ObjectInsertedEvent event) {
-            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
-            logger.info("RULE {}: inserted {}", ruleName, event.getObject());
-        }
-
-        @Override
-        public void objectUpdated(ObjectUpdatedEvent event) {
-            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
-            logger.info("RULE {}: updated {}", ruleName, event.getObject());
-
-        }
-
-        @Override
-        public void objectDeleted(ObjectDeletedEvent event) {
-            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
-            logger.info("RULE {}: deleted {}", ruleName, event.getOldObject());
-        }
-    }
-
-    /**
-     * Logs Rule Matches.
-     */
-    static class AgendaListenerLogger extends DefaultAgendaEventListener {
-        @Override
-        public void matchCreated(MatchCreatedEvent event) {
-            logger.info("RULE {}: match created", event.getMatch().getRule().getName());
-        }
-
-        @Override
-        public void matchCancelled(MatchCancelledEvent event) {
-            logger.info("RULE {}: match cancelled", event.getMatch().getRule().getName());
-        }
-
-        @Override
-        public void beforeMatchFired(BeforeMatchFiredEvent event) {
-            logger.info("RULE {}: before match fired", event.getMatch().getRule().getName());
-        }
-
-        @Override
-        public void afterMatchFired(AfterMatchFiredEvent event) {
-            logger.info("RULE {}: after match fired", event.getMatch().getRule().getName());
-        }
-    }
-
-    /**
-     * Base Class to track Working Memory updates for objects of type T.
-     */
-    abstract class KieObjectExpectedCallback<T> extends DefaultRuleRuntimeEventListener {
-        protected T subject;
-
-        protected CountDownLatch countDownLatch = new CountDownLatch(1);
-
-        public KieObjectExpectedCallback(T affected) {
-            subject = affected;
-            register();
-        }
-
-        public boolean isNotified() throws InterruptedException {
-            return countDownLatch.await(9L, TimeUnit.SECONDS);
-        }
-
-        protected void callbacked() {
-            unregister();
-            countDownLatch.countDown();
-        }
-
-        public KieObjectExpectedCallback<T> register() {
-            usecases.getDrools()
-                .getContainer().getPolicySession(USECASES).getKieSession()
-                .addEventListener(this);
-            return this;
-        }
-
-        public KieObjectExpectedCallback<T> unregister() {
-            usecases.getDrools()
-                .getContainer().getPolicySession(USECASES).getKieSession()
-                .removeEventListener(this);
-            return this;
-        }
-    }
-
-    /**
-     * Tracks inserts in Working Memory for an object of type T.
-     */
-    class KieObjectInsertedExpectedCallback<T> extends KieObjectExpectedCallback {
-        public KieObjectInsertedExpectedCallback(T affected) {
-            super(affected);
-        }
-
-        @Override
-        public void objectInserted(ObjectInsertedEvent event) {
-            if (subject == event.getObject()) {
-                callbacked();
-            }
-        }
-    }
-
-    /**
-     * Tracks deletes in Working Memory of an object of type T.
-     */
-    class KieObjectDeletedExpectedCallback<T> extends KieObjectExpectedCallback {
-        public KieObjectDeletedExpectedCallback(T affected) {
-            super(affected);
-        }
-
-        @Override
-        public void objectDeleted(ObjectDeletedEvent event) {
-            if (subject == event.getOldObject()) {
-                callbacked();
-            }
-        }
-    }
-
-    /**
-     * Tracks inserts in Working Memory for any object of class T.
-     */
-    class KieClassInsertedExpectedCallback<T> extends KieObjectInsertedExpectedCallback {
-        public KieClassInsertedExpectedCallback(Class<T> affected) {
-            super(affected);
-        }
-
-        public void objectInserted(ObjectInsertedEvent event) {
-            if (subject == event.getObject().getClass()) {
-                callbacked();
-            }
-        }
-    }
-
-    /**
-     * Tracks callbacks from topics.
-     */
-    class TopicCallback<T> implements TopicListener {
-        protected final Topic topic;
-        protected final Class<T> expectedClass;
-
-        @Getter
-        protected Queue<T> messages = new LinkedList<>();
-
-        public TopicCallback(Topic topic, Class<T> expectedClass) {
-            this.topic = topic;
-            this.expectedClass = expectedClass;
-            this.topic.register(this);
-        }
-
-        public TopicCallback register() {
-            this.topic.register(this);
-            return this;
-        }
-
-        public TopicCallback unregister() {
-            this.topic.unregister(this);
-            return this;
-        }
-
-        @Override
-        public void onTopicEvent(CommInfrastructure comm, String topic, String event) {
-            try {
-                messages.add((T) EventProtocolCoderConstants.getManager().decode(
-                    usecases.getDrools().getGroupId(), usecases.getDrools().getArtifactId(), topic, event));
-            } catch (Exception e) {
-                logger.warn("invalid mapping in topic {} for event {}", topic, event, e);
-            }
-        }
-    }
-}
diff --git a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/UsecasesTest.java b/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/UsecasesTest.java
new file mode 100644
index 0000000..53c15a3
--- /dev/null
+++ b/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/UsecasesTest.java
@@ -0,0 +1,120 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop;
+
+import lombok.Getter;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.onap.policy.controlloop.common.rules.test.BaseRuleTest;
+import org.onap.policy.controlloop.common.rules.test.Listener;
+import org.onap.policy.controlloop.common.rules.test.NamedRunner;
+import org.onap.policy.controlloop.common.rules.test.TestNames;
+import org.onap.policy.drools.utils.PropertyUtil;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+import org.onap.policy.simulators.Util;
+
+/**
+ * Tests use case rules. Only a subset of available tests will work with these rules, thus
+ * a "FilterRunner" is used to filter out the other test cases.
+ *
+ * <p/>
+ * Note: this only runs tests whose names start with "testV" (e.g., testVcpe(),
+ * testVdns()).
+ */
+@RunWith(NamedRunner.class)
+@TestNames(prefixes = {"testV"})
+
+public class UsecasesTest extends BaseRuleTest {
+
+    @Getter()
+    protected static final String CONTROLLER_NAME = "usecases";
+
+    /**
+     * Sets up statics.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        initStatics(CONTROLLER_NAME);
+
+        rules.configure("src/main/resources");
+        PropertyUtil.setSystemProperties(rules.getPdpdRepo().getSystemProperties("controlloop"));
+        rules.getPdpd().setEnvironment(rules.getPdpdRepo().getEnvironmentProperties("controlloop.properties"));
+
+        rules.start();
+        simulators.start(Util::buildAaiSim, Util::buildSoSim, Util::buildVfcSim, Util::buildGuardSim,
+                        Util::buildSdncSim);
+    }
+
+    /**
+     * Cleans up statics.
+     */
+    @AfterClass
+    public static void tearDownAfterClass() {
+        finishStatics();
+    }
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        init();
+    }
+
+    /**
+     * Tears down.
+     */
+    @After
+    public void tearDown() {
+        finish();
+    }
+
+    @Override
+    protected void waitForLockAndPermit(ToscaPolicy policy, Listener<VirtualControlLoopNotification> policyClMgt) {
+        String policyName = policy.getIdentifier().getName();
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.ACTIVE
+                        && (policyName + ".EVENT").equals(notif.getPolicyName()));
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.OPERATION
+                        && (policyName + ".EVENT.MANAGER.OPERATION.LOCKED.GUARD_NOT_YET_QUERIED")
+                                        .equals(notif.getPolicyName()));
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.OPERATION
+                        && (policyName + ".GUARD.RESPONSE").equals(notif.getPolicyName())
+                        && notif.getMessage().startsWith("Guard result") && notif.getMessage().endsWith("Permit"));
+
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.OPERATION
+                        && (policyName + ".EVENT.MANAGER.OPERATION.LOCKED.GUARD_PERMITTED")
+                                        .equals(notif.getPolicyName()));
+    }
+
+    @Override
+    protected VirtualControlLoopNotification waitForFinal(ToscaPolicy policy,
+                    Listener<VirtualControlLoopNotification> policyClMgt, ControlLoopNotificationType finalType) {
+
+        return policyClMgt.await(notif -> notif.getNotification() == finalType
+                        && (policy.getIdentifier().getName() + ".EVENT.MANAGER").equals(notif.getPolicyName()));
+    }
+}
diff --git a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VcpeTest.java b/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VcpeTest.java
deleted file mode 100644
index ec3b502..0000000
--- a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VcpeTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.nio.file.Paths;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-
-/**
- * VCPE Use Case Tests.
- */
-public class VcpeTest extends UsecasesBase {
-
-    /**
-     * VCPE Tosca Policy File.
-     */
-    private static final String TOSCA_LEGACY_POLICY_VCPE = "src/test/resources/vcpe/tosca-legacy-vcpe.json";
-    private static final String TOSCA_COMPLIANT_POLICY_VCPE = "src/test/resources/vcpe/tosca-compliant-vcpe.json";
-
-    /*
-     * VCPE Use case Messages.
-     */
-    private static final String APPC_SUCCESS = "src/test/resources/vcpe/vcpe.appc.success.json";
-    private static final String ONSET_1 = "src/test/resources/vcpe/vcpe.onset.1.json";
-    private static final String ONSET_2 = "src/test/resources/vcpe/vcpe.onset.2.json";
-    private static final String ONSET_3 = "src/test/resources/vcpe/vcpe.onset.3.json";
-
-    /*
-     * Topic trackers used by the VCPE use case.
-     */
-    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
-    private TopicCallback<AppcLcmDmaapWrapper> appcLcmRead;
-    private TopicCallback<AppcLcmDmaapWrapper> appcLcmWrite;
-
-    /*
-     * VCPE Tosca Policy.
-     */
-    private ToscaPolicy policy;
-
-    /**
-     * Prepare PDP-D Framework for testing.
-     */
-    @BeforeClass
-    public static void prepareResouces() throws InterruptedException, CoderException, IOException {
-        setupLogging();
-        preparePdpD();
-        setupSimulators();
-    }
-
-    /**
-     * Take down the resources used by the test framework.
-     */
-    @AfterClass
-    public static void takeDownResources() {
-        stopPdpD();
-        stopSimulators();
-    }
-
-    /**
-     * Sunny day scenario for the VCPE use case.
-     */
-    public void sunnyDay() throws IOException {
-
-        /* Inject an ONSET event over the DCAE topic */
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_1));
-
-        /* Wait to acquire a LOCK and a PDP-X PERMIT */
-        waitForLockAndPermit(policy, policyClMgt);
-
-        /* --- VCPE Operation Execution --- */
-
-        /* Ensure that an APPC RESTART request was sent in response to the matching ONSET */
-        await().until(() -> !appcLcmRead.getMessages().isEmpty());
-        assertEquals("restart", appcLcmRead.getMessages().remove().getRpcName());
-
-        /* Inject a 400 APPC Response Return over the APPC topic */
-        injectOnTopic(APPC_LCM_WRITE_TOPIC, Paths.get(APPC_SUCCESS));
-
-        /* Ensure that RESTART response is received */
-        await().until(() -> !appcLcmWrite.getMessages().isEmpty());
-        assertEquals("restart", appcLcmWrite.getMessages().peek().getRpcName());
-        assertEquals(400, appcLcmWrite.getMessages().remove().getBody().getOutput().getStatus().getCode());
-
-        /* --- VCPE Operation Completed --- */
-
-        /* Ensure that the VCPE RESTART Operation is successfully completed */
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS, policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policy.getIdentifier().getName() + ".APPC.LCM.RESPONSE",
-            policyClMgt.getMessages().remove().getPolicyName());
-
-        /* --- VCPE Transaction Completed --- */
-        waitForFinalSuccess(policy, policyClMgt);
-    }
-
-    /**
-     * Sunny Day with Legacy Tosca Policy.
-     */
-    @Test
-    public void sunnyDayLegacy() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VCPE);
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny Day with Tosca Compliant Policy.
-     */
-    @Test
-    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VCPE);
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-
-        sunnyDay();
-    }
-
-    /**
-     * An ONSET flood prevention test that injects a few ONSETs at once.
-     * It attempts to simulate the flooding behavior of the DCAE TCA microservice.
-     * TCA could blast tenths or hundreds of ONSETs within sub-second intervals.
-     */
-    @Test
-    public void onsetFloodPrevention() throws IOException, InterruptedException, CoderException {
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VCPE);
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_1));
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_2));
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET_3));
-
-        assertEquals(1, usecases.getDrools().facts(USECASES, VirtualControlLoopEvent.class).stream().count());
-        assertEquals(1, usecases.getDrools().facts(USECASES, CanonicalOnset.class).stream().count());
-        assertEquals(usecases.getDrools().facts(USECASES, CanonicalOnset.class).get(0),
-                usecases.getDrools().facts(USECASES, VirtualControlLoopEvent.class).get(0));
-
-        sunnyDay();
-    }
-
-    /**
-     * Observe Topics.
-     */
-    @Before
-    public void topicsRegistration() {
-        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
-        appcLcmRead = createTopicSinkCallback(APPC_LCM_READ_TOPIC, AppcLcmDmaapWrapper.class);
-        appcLcmWrite = createTopicSourceCallback(APPC_LCM_WRITE_TOPIC, AppcLcmDmaapWrapper.class);
-    }
-
-    /**
-     * Unregister Topic Callbacks.
-     */
-    @After
-    public void topicsUnregistration() {
-        if (policyClMgt != null) {
-            policyClMgt.unregister();
-        }
-
-        if (appcLcmRead != null) {
-            appcLcmRead.unregister();
-        }
-
-        if (appcLcmWrite != null) {
-            appcLcmWrite.unregister();
-        }
-    }
-
-    /**
-     * Uninstall Policy.
-     */
-    @After
-    public void uninstallPolicy() throws InterruptedException {
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-        if (policy != null) {
-            deletePolicy(policy);
-        }
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-    }
-
-}
diff --git a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VfwTest.java b/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VfwTest.java
deleted file mode 100644
index 8386b30..0000000
--- a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VfwTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.nio.file.Paths;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.onap.policy.appc.Request;
-import org.onap.policy.appc.Response;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-
-/**
- * VFW Use Case Tests.
- */
-public class VfwTest extends UsecasesBase {
-
-    /**
-     * VFW Tosca Policy File.
-     */
-    private static final String TOSCA_LEGACY_POLICY_VFW = "src/test/resources/vfw/tosca-vfw.json";
-    private static final String TOSCA_COMPLIANT_POLICY_VFW = "src/test/resources/vfw/tosca-compliant-vfw.json";
-
-    /*
-     * VFW Use case Messages.
-     */
-    private static final String APPC_SUCCESS = "src/test/resources/vfw/vfw.appc.success.json";
-    private static final String ONSET = "src/test/resources/vfw/vfw.onset.json";
-
-    /*
-     * Topic trackers used by the VFW use case.
-     */
-    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
-    private TopicCallback<Response> appcClSource;
-    private TopicCallback<Request> appcClSink;
-
-    /*
-     * VFW Tosca Policy.
-     */
-    private ToscaPolicy policy;
-
-    /**
-     * Prepare PDP-D Framework for testing.
-     */
-    @BeforeClass
-    public static void prepareResouces() throws InterruptedException, IOException {
-        setupLogging();
-        preparePdpD();
-        setupSimulators();
-    }
-
-    /**
-     * Take down the resources used by the test framework.
-     */
-    @AfterClass
-    public static void takeDownResources() {
-        stopPdpD();
-        stopSimulators();
-    }
-
-    /**
-     * Sunny day scenario for the VFW use case.
-     */
-    private void sunnyDay() throws IOException {
-
-        /* Inject an ONSET event over the DCAE topic */
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET));
-
-        /* Wait to acquire a LOCK and a PDP-X PERMIT */
-        waitForLockAndPermit(policy, policyClMgt);
-
-        /* --- VFW Operation Execution --- */
-
-        /* Ensure that an APPC RESTART request was sent in response to the matching ONSET */
-        await().until(() -> !appcClSink.getMessages().isEmpty());
-        assertEquals("ModifyConfig", appcClSink.getMessages().remove().getAction());
-
-        /* Inject a 400 APPC Response Return over the APPC topic */
-        injectOnTopic(APPC_CL_TOPIC, Paths.get(APPC_SUCCESS));
-
-        /* Ensure that RESTART response is received */
-        await().until(() -> !appcClSource.getMessages().isEmpty());
-        assertEquals("SUCCESS", appcClSource.getMessages().remove().getStatus().getValue());
-
-        /* --- VFW Operation Completed --- */
-
-        /* Ensure that the VFW RESTART Operation is successfully completed */
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS,
-            policyClMgt.getMessages().remove().getNotification());
-
-        /* --- VFW Transaction Completed --- */
-        waitForFinalSuccess(policy, policyClMgt);
-    }
-
-    /**
-     * Sunny Day with Legacy Tosca Policy.
-     */
-    @Test
-    public void sunnyDayLegacy() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VFW);
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny Day with Tosca Compliant Policy.
-     */
-    @Test
-    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VFW);
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-
-        sunnyDay();
-    }
-
-    /**
-     * Observe Topics.
-     */
-    @Before
-    public void topicsRegistration() {
-        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
-        appcClSink = createTopicSinkCallback(APPC_CL_TOPIC, Request.class);
-        appcClSource = createTopicSourceCallback(APPC_CL_TOPIC, Response.class);
-    }
-
-    /**
-     * Unregister Topic Callbacks.
-     */
-    @After
-    public void topicsUnregistration() {
-        if (policyClMgt != null) {
-            policyClMgt.unregister();
-        }
-
-        if (appcClSource != null) {
-            appcClSource.unregister();
-        }
-
-        if (appcClSink != null) {
-            appcClSink.unregister();
-        }
-    }
-
-    /**
-     * Uninstall Policy.
-     */
-    @After
-    public void uninstallPolicy() throws InterruptedException {
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-        if (policy != null) {
-            deletePolicy(policy);
-        }
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-    }
-
-}
diff --git a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VlbTest.java b/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VlbTest.java
deleted file mode 100644
index 73eeae8..0000000
--- a/controlloop/common/controller-usecases/src/test/java/org/onap/policy/controlloop/VlbTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP
- * ================================================================================
- * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved.
- * ================================================================================
- * 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.
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.policy.controlloop;
-
-import static org.awaitility.Awaitility.await;
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.nio.file.Paths;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.onap.policy.common.utils.coder.CoderException;
-import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
-
-/**
- * VLB Use Case Tests.
- */
-public class VlbTest extends UsecasesBase {
-
-    /**
-     * VLB Tosca Policy File.
-     */
-    private static final String TOSCA_LEGACY_POLICY_VLB = "src/test/resources/vlb/tosca-vlb.json";
-    private static final String TOSCA_COMPLIANT_POLICY_VLB = "src/test/resources/vlb/tosca-compliant-vlb.json";
-
-    /*
-     * VLB Use case Messages.
-     */
-    private static final String ONSET = "src/test/resources/vlb/vlb.onset.json";
-
-    /*
-     * Topic trackers used by the VLB use case.
-     */
-    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
-
-    /*
-     * VLB Tosca Policy.
-     */
-    private ToscaPolicy policy;
-
-    /**
-     * Prepare PDP-D Framework for testing.
-     */
-    @BeforeClass
-    public static void prepareResouces() throws InterruptedException, IOException {
-        setupLogging();
-        preparePdpD();
-        setupSimulators();
-    }
-
-    /**
-     * Take down the resources used by the test framework.
-     */
-    @AfterClass
-    public static void takeDownResources() {
-        stopPdpD();
-        stopSimulators();
-    }
-
-    /**
-     * Sunny day scenario for the VCPE use case.
-     */
-    private void sunnyDay() throws IOException {
-
-        /* Inject an ONSET event over the DCAE topic */
-        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET));
-
-        /* Wait to acquire a LOCK and a PDP-X PERMIT */
-        waitForLockAndPermit(policy, policyClMgt);
-
-        /* Ensure that the VLB SO Operation was successfully completed */
-
-        await().until(() -> !policyClMgt.getMessages().isEmpty());
-        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS, policyClMgt.getMessages().peek().getNotification());
-        assertEquals(policy.getIdentifier().getName() + ".SO.RESPONSE",
-            policyClMgt.getMessages().remove().getPolicyName());
-
-        /* --- VLB Transaction Completed --- */
-        waitForFinalSuccess(policy, policyClMgt);
-    }
-
-    /**
-     * Sunny Day with Legacy Tosca Policy.
-     */
-    @Test
-    public void sunnyDayLegacy() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VLB);
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-
-        sunnyDay();
-    }
-
-    /**
-     * Sunny Day with Tosca Compliant Policy.
-     */
-    @Test
-    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VLB);
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-
-        sunnyDay();
-    }
-
-    /**
-     * Observe Topics.
-     */
-    @Before
-    public void topicsRegistration() {
-        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
-    }
-
-    /**
-     * Unregister Topic Callbacks.
-     */
-    @After
-    public void topicsUnregistration() {
-        if (policyClMgt != null) {
-            policyClMgt.unregister();
-        }
-    }
-
-    /**
-     * Uninstall Policy.
-     */
-    @After
-    public void uninstallPolicy() throws InterruptedException {
-        assertEquals(2, usecases.getDrools().factCount(USECASES));
-        if (policy != null) {
-            deletePolicy(policy);
-        }
-        assertEquals(0, usecases.getDrools().factCount(USECASES));
-    }
-
-}
diff --git a/controlloop/common/controller-usecases/src/test/resources/config/usecases-controller.properties b/controlloop/common/controller-usecases/src/test/resources/config/usecases-controller.properties
index 483b13a..a8d202c 100644
--- a/controlloop/common/controller-usecases/src/test/resources/config/usecases-controller.properties
+++ b/controlloop/common/controller-usecases/src/test/resources/config/usecases-controller.properties
@@ -26,9 +26,12 @@
 
 noop.source.topics=DCAE_TOPIC,APPC-CL,APPC-LCM-WRITE,SDNR-CL-RSP,POLICY-CL-MGT,APPC-LCM-READ,SDNR-CL,DCAE_CL_RSP
 
-noop.source.topics.DCAE_TOPIC.events=org.onap.policy.controlloop.CanonicalOnset,org.onap.policy.controlloop.CanonicalAbated
-noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalOnset.filter=[?($.closedLoopEventStatus == 'ONSET')]
-noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalAbated.filter=[?($.closedLoopEventStatus == 'ABATED')]
+noop.source.topics.DCAE_TOPIC.events=\
+    org.onap.policy.controlloop.CanonicalOnset,org.onap.policy.controlloop.CanonicalAbated
+noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalOnset.\
+    filter=[?($.closedLoopEventStatus == 'ONSET')]
+noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalAbated.\
+    filter=[?($.closedLoopEventStatus == 'ABATED')]
 noop.source.topics.DCAE_TOPIC.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gson
 
 noop.source.topics.APPC-CL.events=org.onap.policy.appc.Response,org.onap.policy.appc.Request
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/tosca-compliant-vcpe.json b/controlloop/common/controller-usecases/src/test/resources/vcpe/tosca-compliant-vcpe.json
deleted file mode 100644
index 61fb8a6..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vcpe/tosca-compliant-vcpe.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-    "type": "onap.policies.controlloop.operational.common.Drools",
-    "type_version": "1.0.0",
-    "version": "1.0.0",
-    "name": "operational.restart",
-    "metadata": {
-        "policy-id": "operational.restart"
-    },
-    "properties": {
-        "id": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
-        "timeout": 3600,
-        "abatement": false,
-        "trigger": "unique-policy-id-1-restart",
-        "operations": [
-            {
-                "id": "unique-policy-id-1-restart",
-                "description": "Restart the VM",
-                "operation": {
-                    "actor": "APPC",
-                    "operation": "Restart",
-                    "target": {
-                        "targetType": "VM"
-                    }
-                },
-                "timeout": 1200,
-                "retries": 3,
-                "success": "final_success",
-                "failure": "final_failure",
-                "failure_timeout": "final_failure_timeout",
-                "failure_retries": "final_failure_retries",
-                "failure_exception": "final_failure_exception",
-                "failure_guard": "final_failure_guard"
-            }
-        ],
-        "controllerName": "usecases"
-    }
-}
\ No newline at end of file
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/tosca-legacy-vcpe.json b/controlloop/common/controller-usecases/src/test/resources/vcpe/tosca-legacy-vcpe.json
deleted file mode 100644
index f42c07d..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vcpe/tosca-legacy-vcpe.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "type": "onap.policies.controlloop.Operational",
-  "type_version": "1.0.0",
-  "properties": {
-    "content": "controlLoop%3A%0A%20%20version%3A%202.0.0%0A%20%20controlLoopName%3A%20ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e%0A%20%20trigger_policy%3A%20unique-policy-id-1-restart%0A%20%20timeout%3A%203600%0A%20%20abatement%3A%20false%0A%20%0Apolicies%3A%0A%20%20-%20id%3A%20unique-policy-id-1-restart%0A%20%20%20%20name%3A%20Restart%20the%20VM%0A%20%20%20%20description%3A%0A%20%20%20%20actor%3A%20APPC%0A%20%20%20%20recipe%3A%20Restart%0A%20%20%20%20target%3A%0A%20%20%20%20%20%20type%3A%20VM%0A%20%20%20%20retry%3A%203%0A%20%20%20%20timeout%3A%201200%0A%20%20%20%20success%3A%20final_success%0A%20%20%20%20failure%3A%20final_failure%0A%20%20%20%20failure_timeout%3A%20final_failure_timeout%0A%20%20%20%20failure_retries%3A%20final_failure_retries%0A%20%20%20%20failure_exception%3A%20final_failure_exception%0A%20%20%20%20failure_guard%3A%20final_failure_guard"
-  },
-  "name": "vcpe",
-  "version": "1.0.0"
-}
\ No newline at end of file
diff --git a/controlloop/common/controller-usecases/src/test/resources/vfw/tosca-compliant-vfw.json b/controlloop/common/controller-usecases/src/test/resources/vfw/tosca-compliant-vfw.json
deleted file mode 100644
index c96b49c..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vfw/tosca-compliant-vfw.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
-    "type": "onap.policies.controlloop.operational.common.Drools",
-    "type_version": "1.0.0",
-    "name": "operational.modifyconfig",
-    "version": "1.0.0",
-    "metadata": {
-        "policy-id": "operational.modifyconfig"
-    },
-    "properties": {
-        "id": "ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a",
-        "timeout": 60,
-        "abatement": false,
-        "trigger": "unique-policy-id-1-modifyConfig",
-        "operations": [
-            {
-                "id": "unique-policy-id-1-modifyConfig",
-                "description": "Modify the packet generator",
-                "operation": {
-                    "actor": "APPC",
-                    "operation": "ModifyConfig",
-                    "target": {
-                        "targetType": "VNF",
-                        "entityIds": {
-                            "resourceID": "bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38"
-                        }
-                    }
-                },
-                "timeout": 300,
-                "retries": 0,
-                "success": "final_success",
-                "failure": "final_failure",
-                "failure_timeout": "final_failure_timeout",
-                "failure_retries": "final_failure_retries",
-                "failure_exception": "final_failure_exception",
-                "failure_guard": "final_failure_guard"
-            }
-        ],
-        "controllerName": "usecases"
-    }
-}
diff --git a/controlloop/common/controller-usecases/src/test/resources/vfw/tosca-vfw.json b/controlloop/common/controller-usecases/src/test/resources/vfw/tosca-vfw.json
deleted file mode 100644
index 5d1e352..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vfw/tosca-vfw.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "type": "onap.policies.controlloop.Operational",
-  "type_version": "1.0.0",
-  "properties": {
-    "content": "controlLoop%3A%0A%20%20version%3A%202.0.0%0A%20%20controlLoopName%3A%20ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a%0A%20%20services%3A%0A%20%20%20%20-%20serviceInvariantUUID%3A%20f6937c86-584c-47ae-ad29-8d41d6f0cc7c%0A%20%20%20%20%20%20serviceUUID%3A%207be584e2-0bb2-4126-adaf-ced2c77ca0b3%0A%20%20%20%20%20%20serviceName%3A%20Service_Ete_Name7ba1fbde-6187-464a-a62d-d9dd25bdf4e8%0A%20%20trigger_policy%3A%20unique-policy-id-1-modifyConfig%0A%20%20timeout%3A%2060%0A%20%20abatement%3A%20false%0A%20%0Apolicies%3A%0A%20%20-%20id%3A%20unique-policy-id-1-modifyConfig%0A%20%20%20%20name%3A%20modify%20packet%20gen%20config%0A%20%20%20%20description%3A%0A%20%20%20%20actor%3A%20APPC%0A%20%20%20%20recipe%3A%20ModifyConfig%0A%20%20%20%20target%3A%0A%20%20%20%20%20%20resourceID%3A%20bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38%0A%20%20%20%20%20%20type%3A%20VNF%0A%20%20%20%20retry%3A%200%0A%20%20%20%20timeout%3A%2030%0A%20%20%20%20success%3A%20final_success%0A%20%20%20%20failure%3A%20final_failure%0A%20%20%20%20failure_timeout%3A%20final_failure_timeout%0A%20%20%20%20failure_retries%3A%20final_failure_retries%0A%20%20%20%20failure_exception%3A%20final_failure_exception%0A%20%20%20%20failure_guard%3A%20final_failure_guard"
-  },
-  "name": "vfw",
-  "version": "1.0.0"
-}
\ No newline at end of file
diff --git a/controlloop/common/controller-usecases/src/test/resources/vfw/vfw.appc.success.json b/controlloop/common/controller-usecases/src/test/resources/vfw/vfw.appc.success.json
deleted file mode 100644
index 618b705..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vfw/vfw.appc.success.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "CommonHeader": {
-    "TimeStamp": 1506051879001,
-    "APIver": "1.01",
-    "RequestID": "c7c6a4aa-bb61-4a15-b831-ba1472dd4a65",
-    "SubRequestID": "1",
-    "RequestTrack": [],
-    "Flags": []
-  },
-  "Status": {
-    "Code": 400,
-    "Value": "SUCCESS"
-  },
-  "Payload": {
-    "generic-vnf.vnf-id": "jimmy-test-vnf2"
-  }
-}
\ No newline at end of file
diff --git a/controlloop/common/controller-usecases/src/test/resources/vlb/tosca-compliant-vlb.json b/controlloop/common/controller-usecases/src/test/resources/vlb/tosca-compliant-vlb.json
deleted file mode 100644
index 0ddd630..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vlb/tosca-compliant-vlb.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
-    "type": "onap.policies.controlloop.operational.common.Drools",
-    "type_version": "1.0.0",
-    "name": "operational.scaleout",
-    "version": "1.0.0",
-    "metadata": {
-        "policy-id": "operational.scaleout"
-    },
-    "properties": {
-        "id": "ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3",
-        "timeout": 1200,
-        "abatement": false,
-        "trigger": "unique-policy-id-1-scale-up",
-        "operations": [
-            {
-                "id": "unique-policy-id-1-scale-up",
-                "description": "Create a new VF Module",
-                "operation": {
-                    "actor": "SO",
-                    "operation": "VF Module Create",
-                    "target": {
-                        "targetType": "VFMODULE",
-                        "entityIds": {
-                            "modelInvariantId": "e6130d03-56f1-4b0a-9a1d-e1b2ebc30e0e",
-                            "modelVersionId": "94b18b1d-cc91-4f43-911a-e6348665f292",
-                            "modelName": "VfwclVfwsnkBbefb8ce2bde..base_vfw..module-0",
-                            "modelVersion": 1,
-                            "modelCustomizationId": "47958575-138f-452a-8c8d-d89b595f8164"
-                        }
-                    },
-                    "payload": {
-                        "requestParameters": "{\"usePreload\":true,\"userParams\":[]}",
-                        "configurationParameters": "[{\"ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[9]\",\"oam-ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[16]\",\"enabled\":\"$.vf-module-topology.vf-module-parameters.param[23]\"}]"
-                    }
-                },
-                "timeout": 1200,
-                "retries": 0,
-                "success": "final_success",
-                "failure": "final_failure",
-                "failure_timeout": "final_failure_timeout",
-                "failure_retries": "final_failure_retries",
-                "failure_exception": "final_failure_exception",
-                "failure_guard": "final_failure_guard"
-            }
-        ],
-        "controllerName": "usecases"
-    }
-}
diff --git a/controlloop/common/controller-usecases/src/test/resources/vlb/tosca-vlb.json b/controlloop/common/controller-usecases/src/test/resources/vlb/tosca-vlb.json
deleted file mode 100644
index 5147d99..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vlb/tosca-vlb.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "type": "onap.policies.controlloop.Operational",
-  "type_version": "1.0.0",
-  "properties": {
-    "content": "controlLoop%3A%0A%20%20version%3A%202.0.0%0A%20%20controlLoopName%3A%20ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3%0A%20%20services%3A%0A%20%20%20%20-%20serviceName%3A%20d4738992-6497-4dca-9db9%0A%20%20%20%20%20%20serviceInvariantUUID%3A%20dc112d6e-7e73-4777-9c6f-1a7fb5fd1b6f%0A%20%20%20%20%20%20serviceUUID%3A%202eea06c6-e1d3-4c3a-b9c4-478c506eeedf%0A%20%20trigger_policy%3A%20unique-policy-id-1-scale-up%0A%20%20timeout%3A%2060%0A%0Apolicies%3A%0A%20%20-%20id%3A%20unique-policy-id-1-scale-up%0A%20%20%20%20name%3A%20Create%20a%20new%20VF%20Module%0A%20%20%20%20description%3A%0A%20%20%20%20actor%3A%20SO%0A%20%20%20%20recipe%3A%20VF%20Module%20Create%0A%20%20%20%20target%3A%0A%20%20%20%20%20%20type%3A%20VFMODULE%0A%20%20%20%20%20%20modelInvariantId%3A%20e6130d03-56f1-4b0a-9a1d-e1b2ebc30e0e%0A%20%20%20%20%20%20modelVersionId%3A%2094b18b1d-cc91-4f43-911a-e6348665f292%0A%20%20%20%20%20%20modelName%3A%20VfwclVfwsnkBbefb8ce2bde..base_vfw..module-0%0A%20%20%20%20%20%20modelVersion%3A%201%0A%20%20%20%20%20%20modelCustomizationId%3A%2047958575-138f-452a-8c8d-d89b595f8164%0A%20%20%20%20payload%3A%0A%20%20%20%20%20%20requestParameters%3A%20%27%7B%22usePreload%22%3Atrue%2C%22userParams%22%3A%5B%5D%7D%27%0A%20%20%20%20%20%20configurationParameters%3A%20%27%5B%7B%22ip-addr%22%3A%22%24.vf-module-topology.vf-module-parameters.param%5B9%5D%22%2C%22oam-ip-addr%22%3A%22%24.vf-module-topology.vf-module-parameters.param%5B16%5D%22%2C%22enabled%22%3A%22%24.vf-module-topology.vf-module-parameters.param%5B23%5D%22%7D%5D%27%0A%20%20%20%20retry%3A%200%0A%20%20%20%20timeout%3A%2030%0A%20%20%20%20success%3A%20final_success%0A%20%20%20%20failure%3A%20final_failure%0A%20%20%20%20failure_timeout%3A%20final_failure_timeout%0A%20%20%20%20failure_retries%3A%20final_failure_retries%0A%20%20%20%20failure_exception%3A%20final_failure_exception%0A%20%20%20%20failure_guard%3A%20final_failure_guard%0A"
-  },
-  "name": "vlb",
-  "version": "1.0.0"
-}
\ No newline at end of file
diff --git a/controlloop/common/controller-usecases/src/test/resources/vlb/vlb.onset.json b/controlloop/common/controller-usecases/src/test/resources/vlb/vlb.onset.json
deleted file mode 100644
index 3360c0a..0000000
--- a/controlloop/common/controller-usecases/src/test/resources/vlb/vlb.onset.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
-  "closedLoopControlName": "ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3",
-  "closedLoopAlarmStart": 1484677482204798,
-  "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
-  "closedLoopEventStatus": "ONSET",
-  "requestID": "e4f95e0c-a013-4530-8e59-c5c5f9e539b6",
-  "target_type": "VNF",
-  "target": "vserver.vserver-name",
-  "AAI": {
-    "vserver.is-closed-loop-disabled": "false",
-    "vserver.prov-status": "ACTIVE",
-    "vserver.vserver-name": "OzVServer"
-  },
-  "from": "DCAE",
-  "version": "1.0.2"
-}
\ No newline at end of file
diff --git a/controlloop/common/pom.xml b/controlloop/common/pom.xml
index 934dbcd..18cd50c 100644
--- a/controlloop/common/pom.xml
+++ b/controlloop/common/pom.xml
@@ -37,6 +37,7 @@
     <module>guard</module>
     <module>coordination</module>
     <module>eventmanager</module>
+    <module>rules-test</module>
     <module>controller-frankfurt</module>
     <module>controller-usecases</module>
     <module>feature-controlloop-utils</module>
diff --git a/controlloop/common/rules-test/pom.xml b/controlloop/common/rules-test/pom.xml
new file mode 100644
index 0000000..ddd897c
--- /dev/null
+++ b/controlloop/common/rules-test/pom.xml
@@ -0,0 +1,165 @@
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+  ================================================================================
+  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.
+  ============LICENSE_END=========================================================
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+
+        <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+        <artifactId>drools-applications-common</artifactId>
+        <version>1.6.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>rules-test</artifactId>
+    <description>Common Utilities to facilitate testing via JUnit</description>
+    <packaging>jar</packaging>
+
+    <properties>
+        <powermock.version>2.0.4</powermock.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>events</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>appc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>appclcm</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.common</groupId>
+            <artifactId>utils</artifactId>
+            <version>${version.policy.common}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.common</groupId>
+            <artifactId>policy-endpoints</artifactId>
+            <version>${version.policy.common}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions</groupId>
+            <artifactId>simulators</artifactId>
+            <version>${policy.models.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>events</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+            <artifactId>eventmanager</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.drools-pdp</groupId>
+            <artifactId>policy-management</artifactId>
+            <version>${version.policy.drools-pdp}</version>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.powermock</groupId>
+            <artifactId>powermock-api-mockito2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.common</groupId>
+            <artifactId>utils-test</artifactId>
+            <version>${version.policy.common}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <pluginManagement>
+            <plugins>
+                <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven
+                    build itself. -->
+                <plugin>
+                    <groupId>org.eclipse.m2e</groupId>
+                    <artifactId>lifecycle-mapping</artifactId>
+                    <version>1.0.0</version>
+                    <configuration>
+                        <lifecycleMappingMetadata>
+                            <pluginExecutions>
+                                <pluginExecution>
+                                    <pluginExecutionFilter>
+                                        <groupId>org.jacoco</groupId>
+                                        <artifactId>
+                                            jacoco-maven-plugin
+                                        </artifactId>
+                                        <versionRange>
+                                            [0.7.1.201405082137,)
+                                        </versionRange>
+                                        <goals>
+                                            <goal>prepare-agent</goal>
+                                        </goals>
+                                    </pluginExecutionFilter>
+                                    <action>
+                                        <ignore />
+                                    </action>
+                                </pluginExecution>
+                            </pluginExecutions>
+                        </lifecycleMappingMetadata>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+</project>
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/BaseRuleTest.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/BaseRuleTest.java
new file mode 100644
index 0000000..711a617
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/BaseRuleTest.java
@@ -0,0 +1,496 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.junit.Test;
+import org.onap.policy.appc.Request;
+import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.coder.StandardCoderInstantAsMillis;
+import org.onap.policy.controlloop.ControlLoopNotificationType;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+
+/**
+ * Superclass used for rule tests.
+ */
+public abstract class BaseRuleTest {
+    /*
+     * Canonical Topic Names.
+     */
+    protected static final String DCAE_TOPIC = "DCAE_TOPIC";
+    protected static final String APPC_LCM_WRITE_TOPIC = "APPC-LCM-WRITE";
+    protected static final String POLICY_CL_MGT_TOPIC = "POLICY-CL-MGT";
+    protected static final String APPC_LCM_READ_TOPIC = "APPC-LCM-READ";
+    protected static final String APPC_CL_TOPIC = "APPC-CL";
+
+    /*
+     * Constants for each test case.
+     */
+
+    // service123 (i.e., multi-operation policy)
+    private static final String SERVICE123_TOSCA_COMPLIANT_POLICY = "service123/tosca-compliant-service123.json";
+    private static final String SERVICE123_ONSET = "service123/service123.onset.json";
+    private static final String SERVICE123_APPC_RESTART_FAILURE = "service123/service123.appc.restart.failure.json";
+    private static final String SERVICE123_APPC_REBUILD_FAILURE = "service123/service123.appc.rebuild.failure.json";
+    private static final String SERVICE123_APPC_MIGRATE_SUCCESS = "service123/service123.appc.migrate.success.json";
+
+    // duplicates (i.e., mutliple events in the engine at the same time)
+    private static final String DUPLICATES_TOSCA_COMPLIANT_POLICY = "duplicates/tosca-compliant-duplicates.json";
+    private static final String DUPLICATES_ONSET_1 = "duplicates/duplicates.onset.1.json";
+    private static final String DUPLICATES_ONSET_2 = "duplicates/duplicates.onset.2.json";
+    private static final String DUPLICATES_APPC_SUCCESS = "duplicates/duplicates.appc.success.json";
+
+    // VCPE
+    private static final String VCPE_TOSCA_LEGACY_POLICY = "vcpe/tosca-legacy-vcpe.json";
+    private static final String VCPE_TOSCA_COMPLIANT_POLICY = "vcpe/tosca-compliant-vcpe.json";
+    private static final String VCPE_ONSET_1 = "vcpe/vcpe.onset.1.json";
+    private static final String VCPE_ONSET_2 = "vcpe/vcpe.onset.2.json";
+    private static final String VCPE_ONSET_3 = "vcpe/vcpe.onset.3.json";
+    private static final String VCPE_APPC_SUCCESS = "vcpe/vcpe.appc.success.json";
+
+    // VDNS
+    private static final String VDNS_TOSCA_COMPLIANT_POLICY = "vdns/tosca-compliant-vdns.json";
+    private static final String VDNS_ONSET = "vdns/vdns.onset.json";
+
+    // VFW
+    private static final String VFW_TOSCA_LEGACY_POLICY = "vfw/tosca-vfw.json";
+    private static final String VFW_TOSCA_COMPLIANT_POLICY = "vfw/tosca-compliant-vfw.json";
+    private static final String VFW_ONSET = "vfw/vfw.onset.json";
+    private static final String VFW_APPC_SUCCESS = "vfw/vfw.appc.success.json";
+
+    // VLB
+    private static final String VLB_TOSCA_LEGACY_POLICY = "vlb/tosca-vlb.json";
+    private static final String VLB_TOSCA_COMPLIANT_POLICY = "vlb/tosca-compliant-vlb.json";
+    private static final String VLB_ONSET = "vlb/vlb.onset.json";
+
+    /*
+     * Coders used to decode requests and responses.
+     */
+    private static final Coder APPC_LEGACY_CODER = new StandardCoderInstantAsMillis();
+    private static final Coder APPC_LCM_CODER = new StandardCoder();
+
+    // these may be overridden by junit tests
+    private static Function<String, Rules> ruleMaker = Rules::new;
+    private static Supplier<HttpClients> httpClientMaker = HttpClients::new;
+    private static Supplier<Simulators> simMaker = Simulators::new;
+    private static Supplier<Topics> topicMaker = Topics::new;
+
+    protected static Rules rules;
+    protected static HttpClients httpClients;
+    protected static Simulators simulators;
+
+
+    // used to inject and wait for messages
+    @Getter(AccessLevel.PROTECTED)
+    private Topics topics;
+
+    // used to wait for messages on SINK topics
+    protected Listener<VirtualControlLoopNotification> policyClMgt;
+    protected Listener<Request> appcClSink;
+    protected Listener<AppcLcmDmaapWrapper> appcLcmRead;
+
+    protected PolicyController controller;
+
+    /*
+     * Tosca Policy that was loaded.
+     */
+    protected ToscaPolicy policy;
+
+
+    /**
+     * Initializes {@link #rules}, {@link #httpClients}, and {@link #simulators}.
+     *
+     * @param controllerName the rule controller name
+     */
+    public static void initStatics(String controllerName) {
+        rules = ruleMaker.apply(controllerName);
+        httpClients = httpClientMaker.get();
+        simulators = simMaker.get();
+    }
+
+    /**
+     * Destroys {@link #httpClients}, {@link #simulators}, and {@link #rules}.
+     */
+    public static void finishStatics() {
+        httpClients.destroy();
+        simulators.destroy();
+        rules.destroy();
+    }
+
+    /**
+     * Initializes {@link #topics} and {@link #controller}.
+     */
+    public void init() {
+        topics = topicMaker.get();
+        controller = rules.getController();
+    }
+
+    /**
+     * Destroys {@link #topics} and resets the rule facts.
+     */
+    public void finish() {
+        topics.destroy();
+        rules.resetFacts();
+    }
+
+    // Service123 (i.e., Policy with multiple operations)
+
+    /**
+     * Service123 with Tosca Compliant Policy.
+     */
+    @Test
+    public void testService123Compliant() {
+        policyClMgt = topics.createListener(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class, controller);
+        appcLcmRead = topics.createListener(APPC_LCM_READ_TOPIC, AppcLcmDmaapWrapper.class, APPC_LCM_CODER);
+
+        assertEquals(0, controller.getDrools().factCount(rules.getControllerName()));
+        policy = rules.setupPolicyFromFile(SERVICE123_TOSCA_COMPLIANT_POLICY);
+        assertEquals(2, controller.getDrools().factCount(rules.getControllerName()));
+
+        // inject an ONSET event over the DCAE topic
+        topics.inject(DCAE_TOPIC, SERVICE123_ONSET);
+
+        /* Wait to acquire a LOCK and a PDP-X PERMIT */
+        waitForLockAndPermit(policy, policyClMgt);
+
+        // restart request should be sent and fail four times (i.e., because retry=3)
+        for (int count = 0; count < 4; ++count) {
+            AppcLcmDmaapWrapper appcreq = appcLcmRead.await(req -> "restart".equals(req.getRpcName()));
+
+            topics.inject(APPC_LCM_WRITE_TOPIC, SERVICE123_APPC_RESTART_FAILURE,
+                            appcreq.getBody().getInput().getCommonHeader().getSubRequestId());
+        }
+
+        // rebuild request should be sent and fail once
+        AppcLcmDmaapWrapper appcreq = appcLcmRead.await(req -> "rebuild".equals(req.getRpcName()));
+
+        topics.inject(APPC_LCM_WRITE_TOPIC, SERVICE123_APPC_REBUILD_FAILURE,
+                        appcreq.getBody().getInput().getCommonHeader().getSubRequestId());
+
+        // migrate request should be sent and succeed
+        appcreq = appcLcmRead.await(req -> "migrate".equals(req.getRpcName()));
+
+        topics.inject(APPC_LCM_WRITE_TOPIC, SERVICE123_APPC_MIGRATE_SUCCESS,
+                        appcreq.getBody().getInput().getCommonHeader().getSubRequestId());
+
+        /* --- Operation Completed --- */
+
+        waitForOperationSuccess();
+
+        /* --- Transaction Completed --- */
+        waitForFinalSuccess(policy, policyClMgt);
+    }
+
+    // Duplicate events
+
+    /**
+     * This test case tests the scenario where 3 events occur and 2 of the requests refer
+     * to the same target entity while the 3rd is for another entity. The expected result
+     * is that the event with the duplicate target entity will have a final success result
+     * for one of the events, and a rejected message for the one that was unable to obtain
+     * the lock. The event that is referring to a different target entity should be able
+     * to obtain a lock since it is a different target. After processing of all events
+     * there should only be the policy and params objects left in memory.
+     */
+    @Test
+    public void testDuplicatesEvents() {
+        policyClMgt = topics.createListener(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class, controller);
+        appcLcmRead = topics.createListener(APPC_LCM_READ_TOPIC, AppcLcmDmaapWrapper.class, APPC_LCM_CODER);
+
+        assertEquals(0, controller.getDrools().factCount(rules.getControllerName()));
+        policy = rules.setupPolicyFromFile(DUPLICATES_TOSCA_COMPLIANT_POLICY);
+        assertEquals(2, controller.getDrools().factCount(rules.getControllerName()));
+
+        /*
+         * Inject ONSET events over the DCAE topic. First and last have the same target
+         * entity, but different request IDs - only one should succeed. The middle one is
+         * for a different target entity, so it should succeed.
+         */
+        topics.inject(DCAE_TOPIC, DUPLICATES_ONSET_1, UUID.randomUUID().toString());
+        topics.inject(DCAE_TOPIC, DUPLICATES_ONSET_2);
+        topics.inject(DCAE_TOPIC, DUPLICATES_ONSET_1, UUID.randomUUID().toString());
+
+        // one should immediately generate a FINAL failure
+        waitForFinal(policy, policyClMgt, ControlLoopNotificationType.FINAL_FAILURE);
+
+        // should see two restarts
+        for (int count = 0; count < 2; ++count) {
+            AppcLcmDmaapWrapper appcreq = appcLcmRead.await(req -> "restart".equals(req.getRpcName()));
+
+            // indicate success
+            topics.inject(APPC_LCM_WRITE_TOPIC, DUPLICATES_APPC_SUCCESS,
+                            appcreq.getBody().getInput().getCommonHeader().getSubRequestId());
+        }
+
+        // should see two FINAL successes
+        VirtualControlLoopNotification notif1 = waitForFinalSuccess(policy, policyClMgt);
+        VirtualControlLoopNotification notif2 = waitForFinalSuccess(policy, policyClMgt);
+
+        // get the list of target names so we can ensure there's one of each
+        List<String> actual = List.of(notif1, notif2).stream().map(notif -> notif.getAai().get("generic-vnf.vnf-id"))
+                        .sorted().collect(Collectors.toList());
+
+        assertEquals(List.of("duplicate-VNF", "vCPE_Infrastructure_vGMUX_demo_app").toString(), actual.toString());
+    }
+
+    // VCPE
+
+    /**
+     * Sunny Day with Legacy Tosca Policy.
+     */
+    @Test
+    public void testVcpeSunnyDayLegacy() {
+        appcLcmSunnyDay(VCPE_TOSCA_LEGACY_POLICY, VCPE_ONSET_1, "restart");
+    }
+
+    /**
+     * Sunny Day with Tosca Compliant Policy.
+     */
+    @Test
+    public void testVcpeSunnyDayCompliant() {
+        appcLcmSunnyDay(VCPE_TOSCA_COMPLIANT_POLICY, VCPE_ONSET_1, "restart");
+    }
+
+    /**
+     * An ONSET flood prevention test that injects a few ONSETs at once. It attempts to
+     * simulate the flooding behavior of the DCAE TCA microservice. TCA could blast tens
+     * or hundreds of ONSETs within sub-second intervals.
+     */
+    @Test
+    public void testVcpeOnsetFloodPrevention() {
+        appcLcmSunnyDay(VCPE_TOSCA_COMPLIANT_POLICY, List.of(VCPE_ONSET_1, VCPE_ONSET_2, VCPE_ONSET_3), "restart");
+    }
+
+    // VDNS
+
+    /**
+     * Sunny Day with Tosca Compliant Policy.
+     */
+    @Test
+    public void testVdnsSunnyDayCompliant() {
+        httpSunnyDay(VDNS_TOSCA_COMPLIANT_POLICY, VDNS_ONSET);
+    }
+
+    // VFW
+
+    /**
+     * VFW Sunny Day with Legacy Tosca Policy.
+     */
+    @Test
+    public void testVfwSunnyDayLegacy() {
+        appcLegacySunnyDay(VFW_TOSCA_LEGACY_POLICY, VFW_ONSET, "ModifyConfig");
+    }
+
+    /**
+     * VFW Sunny Day with Tosca Compliant Policy.
+     */
+    @Test
+    public void testVfwSunnyDayCompliant() {
+        appcLegacySunnyDay(VFW_TOSCA_COMPLIANT_POLICY, VFW_ONSET, "ModifyConfig");
+    }
+
+    // VLB
+
+    /**
+     * Sunny Day with Legacy Tosca Policy.
+     */
+    @Test
+    public void testVlbSunnyDayLegacy() {
+        httpSunnyDay(VLB_TOSCA_LEGACY_POLICY, VLB_ONSET);
+    }
+
+    /**
+     * Sunny Day with Tosca Compliant Policy.
+     */
+    @Test
+    public void testVlbSunnyDayCompliant() {
+        httpSunnyDay(VLB_TOSCA_COMPLIANT_POLICY, VLB_ONSET);
+    }
+
+    /**
+     * Sunny day scenario for use cases that use APPC-LCM.
+     *
+     * @param policyFile file containing the ToscaPolicy to be loaded
+     * @param onsetFile file containing the ONSET to be injected
+     * @param operation expected APPC operation request
+     */
+    protected void appcLcmSunnyDay(String policyFile, String onsetFile, String operation) {
+        appcLcmSunnyDay(policyFile, List.of(onsetFile), operation);
+    }
+
+    /**
+     * Sunny day scenario for use cases that use APPC-LCM.
+     *
+     * @param policyFile file containing the ToscaPolicy to be loaded
+     * @param onsetFiles list of files containing the ONSET to be injected
+     * @param operation expected APPC operation request
+     */
+    protected void appcLcmSunnyDay(String policyFile, List<String> onsetFiles, String operation) {
+        policyClMgt = topics.createListener(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class, controller);
+        appcLcmRead = topics.createListener(APPC_LCM_READ_TOPIC, AppcLcmDmaapWrapper.class, APPC_LCM_CODER);
+
+        assertEquals(0, controller.getDrools().factCount(rules.getControllerName()));
+        policy = rules.setupPolicyFromFile(policyFile);
+        assertEquals(2, controller.getDrools().factCount(rules.getControllerName()));
+
+        // inject several ONSET events over the DCAE topic
+        for (String onsetFile : onsetFiles) {
+            topics.inject(DCAE_TOPIC, onsetFile);
+        }
+
+        /* Wait to acquire a LOCK and a PDP-X PERMIT */
+        waitForLockAndPermit(policy, policyClMgt);
+
+        /*
+         * Ensure that an APPC RESTART request was sent in response to the matching ONSET
+         */
+        AppcLcmDmaapWrapper appcreq = appcLcmRead.await(req -> operation.equals(req.getRpcName()));
+
+        /*
+         * Inject a 400 APPC Response Return over the APPC topic, with appropriate
+         * subRequestId
+         */
+        topics.inject(APPC_LCM_WRITE_TOPIC, VCPE_APPC_SUCCESS,
+                        appcreq.getBody().getInput().getCommonHeader().getSubRequestId());
+
+        /* --- Operation Completed --- */
+
+        waitForOperationSuccess();
+
+        /* --- Transaction Completed --- */
+        waitForFinalSuccess(policy, policyClMgt);
+    }
+
+    /**
+     * Sunny day scenario for use cases that use Legacy APPC.
+     *
+     * @param policyFile file containing the ToscaPolicy to be loaded
+     * @param onsetFile file containing the ONSET to be injected
+     * @param operation expected APPC operation request
+     */
+    protected void appcLegacySunnyDay(String policyFile, String onsetFile, String operation) {
+        policyClMgt = topics.createListener(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class, controller);
+        appcClSink = topics.createListener(APPC_CL_TOPIC, Request.class, APPC_LEGACY_CODER);
+
+        assertEquals(0, controller.getDrools().factCount(rules.getControllerName()));
+        policy = rules.setupPolicyFromFile(policyFile);
+        assertEquals(2, controller.getDrools().factCount(rules.getControllerName()));
+
+        /* Inject an ONSET event over the DCAE topic */
+        topics.inject(DCAE_TOPIC, onsetFile);
+
+        /* Wait to acquire a LOCK and a PDP-X PERMIT */
+        waitForLockAndPermit(policy, policyClMgt);
+
+        /*
+         * Ensure that an APPC RESTART request was sent in response to the matching ONSET
+         */
+        Request appcreq = appcClSink.await(req -> operation.equals(req.getAction()));
+
+        /*
+         * Inject a 400 APPC Response Return over the APPC topic, with appropriate
+         * subRequestId
+         */
+        topics.inject(APPC_CL_TOPIC, VFW_APPC_SUCCESS, appcreq.getCommonHeader().getSubRequestId());
+
+        /* --- Operation Completed --- */
+
+        waitForOperationSuccess();
+
+        /* --- Transaction Completed --- */
+        waitForFinalSuccess(policy, policyClMgt);
+    }
+
+    /**
+     * Sunny day scenario for use cases that use an HTTP simulator.
+     *
+     * @param policyFile file containing the ToscaPolicy to be loaded
+     * @param onsetFile file containing the ONSET to be injected
+     * @param operation expected APPC operation request
+     */
+    protected void httpSunnyDay(String policyFile, String onsetFile) {
+        policyClMgt = topics.createListener(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class, controller);
+
+        assertEquals(0, controller.getDrools().factCount(rules.getControllerName()));
+        policy = rules.setupPolicyFromFile(policyFile);
+        assertEquals(2, controller.getDrools().factCount(rules.getControllerName()));
+
+        /* Inject an ONSET event over the DCAE topic */
+        topics.inject(DCAE_TOPIC, onsetFile);
+
+        /* Wait to acquire a LOCK and a PDP-X PERMIT */
+        waitForLockAndPermit(policy, policyClMgt);
+
+        /* --- Operation Completed --- */
+
+        waitForOperationSuccess();
+
+        /* --- Transaction Completed --- */
+        waitForFinalSuccess(policy, policyClMgt);
+    }
+
+    /**
+     * Waits for a OPERATION SUCCESS transaction notification.
+     */
+    protected void waitForOperationSuccess() {
+        policyClMgt.await(notif -> notif.getNotification() == ControlLoopNotificationType.OPERATION_SUCCESS);
+    }
+
+    /**
+     * Waits for a FINAL SUCCESS transaction notification.
+     *
+     * @return the FINAL SUCCESS notification
+     */
+    protected VirtualControlLoopNotification waitForFinalSuccess(ToscaPolicy policy,
+                    Listener<VirtualControlLoopNotification> policyClMgt) {
+
+        return this.waitForFinal(policy, policyClMgt, ControlLoopNotificationType.FINAL_SUCCESS);
+    }
+
+    /**
+     * Waits for notifications for LOCK acquisition and GUARD Permit so that event
+     * processing may proceed.
+     */
+    protected abstract void waitForLockAndPermit(ToscaPolicy policy,
+                    Listener<VirtualControlLoopNotification> policyClMgt);
+
+    /**
+     * Waits for a FINAL transaction notification.
+     *
+     * @param finalType FINAL_xxx type for which to wait
+     *
+     * @return the FINAL notification
+     */
+    protected abstract VirtualControlLoopNotification waitForFinal(ToscaPolicy policy,
+                    Listener<VirtualControlLoopNotification> policyClMgt, ControlLoopNotificationType finalType);
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/HttpClients.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/HttpClients.java
new file mode 100644
index 0000000..eb729ad
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/HttpClients.java
@@ -0,0 +1,67 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import org.onap.policy.common.endpoints.http.client.HttpClientConfigException;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactory;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
+import org.onap.policy.drools.persistence.SystemPersistenceConstants;
+
+/**
+ * Mechanism by which junit tests can manage HTTP Clients.
+ */
+public class HttpClients {
+
+    /**
+     * Constructs the object.
+     */
+    public HttpClients() {
+        super();
+    }
+
+
+    /**
+     * Adds Http Clients specified in the property file.
+     *
+     * @param propFilePrefix prefix prepended to "-http-client.properties" to yield the
+     *        full name of the property file containing http client properties
+     */
+    public void addClients(String propFilePrefix) {
+        try {
+            getClientFactory().build(SystemPersistenceConstants.getManager().getHttpClientProperties(propFilePrefix));
+        } catch (HttpClientConfigException e) {
+            throw new IllegalArgumentException("cannot initialize HTTP clients", e);
+        }
+    }
+
+    /**
+     * Destroys all Http Clients.
+     */
+    public void destroy() {
+        getClientFactory().destroy();
+    }
+
+    // these methods may be overridden by junit tests
+
+    protected HttpClientFactory getClientFactory() {
+        return HttpClientFactoryInstance.getClientFactory();
+    }
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Listener.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Listener.java
new file mode 100644
index 0000000..5042f54
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Listener.java
@@ -0,0 +1,180 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpoint;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
+import org.onap.policy.common.endpoints.event.comm.TopicListener;
+import org.onap.policy.common.endpoints.event.comm.TopicSink;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Listener for messages published on a topic SINK.
+ *
+ * @param <T> message type
+ */
+public class Listener<T> implements TopicListener {
+    private static final Logger logger = LoggerFactory.getLogger(Listener.class);
+    public static long DEFAULT_WAIT_SEC = 5L;
+
+    private final TopicSink sink;
+    private final Function<String, T> decoder;
+    private final BlockingQueue<T> messages = new LinkedBlockingQueue<>();
+
+    /**
+     * Constructs the object.
+     *
+     * @param topicName name of the NOOP topic SINK on which to listen
+     * @param decoder function that takes a topic name and a message and decodes it into
+     *        the desired type
+     */
+    public Listener(String topicName, Function<String, T> decoder) {
+        this.sink = getTopicManager().getNoopTopicSink(topicName);
+        this.decoder = decoder;
+        this.sink.register(this);
+    }
+
+    /**
+     * Determines if there are any messages waiting.
+     *
+     * @return {@code true} if there are no messages waiting, {@code false} otherwise
+     */
+    public boolean isEmpty() {
+        return messages.isEmpty();
+    }
+
+    /**
+     * Waits, for the default amount of time, for a message to be published to the topic.
+     *
+     * @return the message that was published
+     * @throws TopicException if interrupted or no message is received within the
+     *         specified time
+     */
+    public T await() {
+        return await(DEFAULT_WAIT_SEC, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Waits, for the specified period of time, for a message to be published to the
+     * topic.
+     *
+     * @param twait maximum time to wait
+     * @param unit time unit
+     * @return the message that was published
+     * @throws TopicException if interrupted or no message is received within the
+     *         specified time
+     */
+    public T await(long twait, TimeUnit unit) {
+        return await(twait, unit, msg -> true);
+    }
+
+    /**
+     * Waits, for the default amount of time, for a message to be published to the topic.
+     *
+     * @param filter filter used to select the message of interest; preceding messages
+     *        that do not pass the filter are discarded
+     * @return the message that was published
+     * @throws TopicException if interrupted or no message is received within the
+     *         specified time
+     */
+    public T await(Predicate<T> filter) {
+        return await(DEFAULT_WAIT_SEC, TimeUnit.SECONDS, filter);
+    }
+
+    /**
+     * Waits, for the specified period of time, for a message to be published to the
+     * topic.
+     *
+     * @param twait maximum time to wait
+     * @param unit time unit
+     * @param filter filter used to select the message of interest; preceding messages
+     *        that do not pass the filter are discarded
+     * @return the message that was published
+     * @throws TopicException if interrupted or no message is received within the
+     *         specified time
+     */
+    public T await(long twait, TimeUnit unit, Predicate<T> filter) {
+        long endMs = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(twait, unit);
+
+        for (;;) {
+            try {
+                long remainingMs = endMs - System.currentTimeMillis();
+                if (remainingMs < 0) {
+                    throw new TimeoutException();
+                }
+
+                T msg = pollMessage(remainingMs);
+                if (msg == null) {
+                    throw new TimeoutException();
+                }
+
+                if (filter.test(msg)) {
+                    return msg;
+                }
+
+                logger.info("message discarded by the filter on topic {}", sink.getTopic());
+
+            } catch (InterruptedException e) {
+                logger.warn("'await' interrupted on topic {}", sink.getTopic());
+                Thread.currentThread().interrupt();
+                throw new TopicException(e);
+
+            } catch (TimeoutException e) {
+                logger.warn("'await' timed out on topic {}", sink.getTopic());
+                throw new TopicException(e);
+            }
+        }
+    }
+
+    /**
+     * Unregisters the listener.
+     */
+    public void unregister() {
+        sink.unregister(this);
+    }
+
+    @Override
+    public void onTopicEvent(CommInfrastructure commType, String topic, String event) {
+        try {
+            messages.add(decoder.apply(event));
+        } catch (RuntimeException e) {
+            logger.warn("cannot decode message on topic {} for event {}", topic, event, e);
+        }
+    }
+
+    // these methods may be overridden by junit tests
+
+    protected TopicEndpoint getTopicManager() {
+        return TopicEndpointManager.getManager();
+    }
+
+    protected T pollMessage(long remainingMs) throws InterruptedException {
+        return messages.poll(remainingMs, TimeUnit.MILLISECONDS);
+    }
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/NamedRunner.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/NamedRunner.java
new file mode 100644
index 0000000..1cbe5a5
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/NamedRunner.java
@@ -0,0 +1,85 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import org.junit.Ignore;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+
+/**
+ * Runs tests listed via the {@link TestNames} annotation.
+ */
+public class NamedRunner extends BlockJUnit4ClassRunner {
+
+    /**
+     * Constructs the object.
+     */
+    public NamedRunner(Class<?> testClass) throws InitializationError {
+        super(testClass);
+    }
+
+    @Override
+    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
+        Description description = describeChild(method);
+
+        if (method.getAnnotation(Ignore.class) != null) {
+            notifier.fireTestIgnored(description);
+
+        } else if (!isNamed(description.getTestClass(), method.getName())) {
+            notifier.fireTestIgnored(description);
+
+        } else {
+            runLeaf(methodBlock(method), description, notifier);
+        }
+    }
+
+    /**
+     * Determines if the test is in the list of tests to be included.
+     *
+     * @param testClass class under test
+     * @param testName name of the test of interest
+     * @return {@code true} if the test is in the list, {@code false} otherwise
+     */
+    private boolean isNamed(Class<?> testClass, String testName) {
+        TestNames annot = testClass.getAnnotation(TestNames.class);
+        if (annot == null) {
+            // no annotation - everything passes
+            return true;
+        }
+
+        for (String name : annot.names()) {
+            if (testName.equals(name)) {
+                return true;
+            }
+        }
+
+        for (String prefix : annot.prefixes()) {
+            if (testName.startsWith(prefix)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Rules.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Rules.java
new file mode 100644
index 0000000..7549c6b
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Rules.java
@@ -0,0 +1,413 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
+import org.kie.api.event.rule.AfterMatchFiredEvent;
+import org.kie.api.event.rule.BeforeMatchFiredEvent;
+import org.kie.api.event.rule.DefaultAgendaEventListener;
+import org.kie.api.event.rule.DefaultRuleRuntimeEventListener;
+import org.kie.api.event.rule.MatchCancelledEvent;
+import org.kie.api.event.rule.MatchCreatedEvent;
+import org.kie.api.event.rule.ObjectDeletedEvent;
+import org.kie.api.event.rule.ObjectInsertedEvent;
+import org.kie.api.event.rule.ObjectUpdatedEvent;
+import org.kie.api.event.rule.RuleRuntimeEventListener;
+import org.kie.api.runtime.KieSession;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.resources.ResourceUtils;
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.persistence.SystemPersistence;
+import org.onap.policy.drools.persistence.SystemPersistenceConstants;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.drools.system.PolicyControllerConstants;
+import org.onap.policy.drools.system.PolicyControllerFactory;
+import org.onap.policy.drools.system.PolicyEngine;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+import org.onap.policy.drools.util.KieUtils;
+import org.onap.policy.drools.utils.logging.LoggerUtil;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Mechanism by which junit tests can manage the rule engine.
+ */
+public class Rules {
+    private static final Logger logger = LoggerFactory.getLogger(Rules.class);
+    private static final StandardCoder coder = new StandardCoder();
+
+    /**
+     * PDP-D Engine.
+     */
+    @Getter
+    private final PolicyEngine pdpd = makeEngine();
+
+    /**
+     * PDP-D Configuration Repository.
+     */
+    @Getter
+    private final SystemPersistence pdpdRepo = makePdpdRepo();
+
+
+    @Getter
+    private final String controllerName;
+
+    @Getter
+    private PolicyController controller;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param controllerName name of the controller
+     */
+    public Rules(String controllerName) {
+        this.controllerName = controllerName;
+    }
+
+    /**
+     * Configures various items, including the PDP-D Engine.
+     *
+     * @param resourceDir path to resource directory
+     */
+    public void configure(String resourceDir) {
+        pdpdRepo.setConfigurationDir("src/test/resources/config");
+
+        try {
+            File kmoduleFile = new File(resourceDir + "/META-INF/kmodule.xml");
+            File pomFile = new File("src/test/resources/" + controllerName + ".pom");
+            String resourceDir2 = resourceDir + "/org/onap/policy/controlloop/";
+            File ruleFile = new File(resourceDir + "/" + controllerName + ".drl");
+            List<File> ruleFiles = Collections.singletonList(ruleFile);
+
+            installArtifact(kmoduleFile, pomFile, resourceDir2, ruleFiles);
+        } catch (IOException e) {
+            throw new IllegalArgumentException("cannot configure KIE session for " + controllerName, e);
+        }
+
+        setupLogging();
+
+        pdpd.configure(new Properties());
+    }
+
+    /**
+     * Starts various items, including the PDP-D Engine.
+     */
+    public void start() {
+        controller = pdpd.createPolicyController(controllerName, pdpdRepo.getControllerProperties(controllerName));
+        pdpd.start();
+
+        setupDroolsLogging();
+    }
+
+    /**
+     * Stop PDP-D.
+     */
+    public void destroy() {
+        getControllerFactory().shutdown(controllerName);
+        pdpd.stop();
+    }
+
+    /**
+     * Removes various facts from working memory, including the Policy and Params, as well
+     * as any event managers and events.
+     */
+    public void resetFacts() {
+        List<Class<?>> classes = List.of(ToscaPolicy.class, ControlLoopParams.class, ControlLoopEventManager2.class,
+                        ControlLoopEvent.class);
+
+        // delete all objects of the listed classes
+        DroolsController drools = controller.getDrools();
+        classes.forEach(drools::delete);
+
+        // wait for them to be deleted
+        for (Class<?> clazz : classes) {
+            await(clazz.getSimpleName()).atMost(5, TimeUnit.SECONDS)
+                            .until(() -> drools.facts(controllerName, clazz).isEmpty());
+        }
+
+        /*
+         * We can't delete this class directly; we have to wait for the rules to clean it
+         * up, because the rule also cleans up a number of other associated objects.
+         */
+        Class<?> clazz = ControlLoopEventManager.class;
+        await(clazz.getSimpleName()).atMost(5, TimeUnit.SECONDS)
+                        .until(() -> drools.facts(controllerName, clazz).isEmpty());
+    }
+
+    /**
+     * Installs a policy from policy/models (examples) repo.
+     */
+    public ToscaPolicy setupPolicyFromTemplate(String templatePath, String policyName) {
+        try {
+            return setupPolicy(getPolicyFromTemplate(templatePath, policyName));
+
+        } catch (InterruptedException | CoderException e) {
+            throw new IllegalArgumentException("policy " + policyName, e);
+        }
+    }
+
+    private ToscaPolicy getPolicyFromTemplate(String resourcePath, String policyName) throws CoderException {
+        String policyJson = ResourceUtils.getResourceAsString(resourcePath);
+        if (policyJson == null) {
+            throw new CoderException(new FileNotFoundException(resourcePath));
+        }
+
+        ToscaServiceTemplate serviceTemplate = coder.decode(policyJson, ToscaServiceTemplate.class);
+        ToscaPolicy policy = serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
+        assertNotNull(policy);
+
+        /*
+         * name and version are used within a drl. api component and drools core will
+         * ensure that these are populated.
+         */
+        if (StringUtils.isBlank(policy.getName())) {
+            policy.setName(policyName);
+        }
+
+        if (StringUtils.isBlank(policy.getVersion())) {
+            policy.setVersion(policy.getTypeVersion());
+        }
+
+        return serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
+    }
+
+    /**
+     * Installs a given policy.
+     */
+    public ToscaPolicy setupPolicyFromFile(String policyPath) {
+        try {
+            return setupPolicy(getPolicyFromFile(policyPath));
+
+        } catch (InterruptedException | IOException | CoderException e) {
+            throw new IllegalArgumentException("policy " + policyPath, e);
+        }
+    }
+
+    private ToscaPolicy getPolicyFromFile(String policyPath) throws IOException, CoderException {
+        String policyJson = ResourceUtils.getResourceAsString(policyPath);
+        if (policyJson == null) {
+            throw new CoderException(new FileNotFoundException(policyPath));
+        }
+
+        return coder.decode(policyJson, ToscaPolicy.class);
+    }
+
+    private ToscaPolicy setupPolicy(ToscaPolicy policy) throws InterruptedException {
+        final KieObjectExpectedCallback<?> policyTracker = new KieObjectInsertedExpectedCallback<>(policy);
+        final KieObjectExpectedCallback<?> paramsTracker =
+                        new KieClassInsertedExpectedCallback<>(ControlLoopParams.class);
+
+        controller.getDrools().offer(policy);
+
+        assertTrue(policyTracker.isNotified());
+        assertTrue(paramsTracker.isNotified());
+
+        assertEquals(1, controller.getDrools().facts(controllerName, ToscaPolicy.class).stream()
+                        .filter((anotherPolicy) -> anotherPolicy == policy).count());
+
+        assertEquals(1, controller.getDrools().facts(controllerName, ControlLoopParams.class).stream()
+                        .filter((params) -> params.getToscaPolicy() == policy).count());
+        return policy;
+    }
+
+    /**
+     * Sets up overall logging.
+     */
+    private void setupLogging() {
+        LoggerUtil.setLevel(LoggerUtil.ROOT_LOGGER, "WARN");
+        LoggerUtil.setLevel("org.eclipse.jetty", "WARN");
+        LoggerUtil.setLevel("org.onap.policy.controlloop", "INFO");
+        LoggerUtil.setLevel("network", "INFO");
+    }
+
+    /**
+     * Sets up Drools Logging for events of interest.
+     */
+    private void setupDroolsLogging() {
+        KieSession session = getKieSession();
+
+        session.addEventListener(new RuleListenerLogger());
+        session.addEventListener(new AgendaListenerLogger());
+    }
+
+    /**
+     * Logs Modifications to Working Memory.
+     */
+    private static class RuleListenerLogger implements RuleRuntimeEventListener {
+        @Override
+        public void objectInserted(ObjectInsertedEvent event) {
+            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
+            logger.info("RULE {}: inserted {}", ruleName, event.getObject());
+        }
+
+        @Override
+        public void objectUpdated(ObjectUpdatedEvent event) {
+            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
+            logger.info("RULE {}: updated {}", ruleName, event.getObject());
+
+        }
+
+        @Override
+        public void objectDeleted(ObjectDeletedEvent event) {
+            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
+            logger.info("RULE {}: deleted {}", ruleName, event.getOldObject());
+        }
+    }
+
+    /**
+     * Logs Rule Matches.
+     */
+    private static class AgendaListenerLogger extends DefaultAgendaEventListener {
+        @Override
+        public void matchCreated(MatchCreatedEvent event) {
+            logger.info("RULE {}: match created", event.getMatch().getRule().getName());
+        }
+
+        @Override
+        public void matchCancelled(MatchCancelledEvent event) {
+            logger.info("RULE {}: match cancelled", event.getMatch().getRule().getName());
+        }
+
+        @Override
+        public void beforeMatchFired(BeforeMatchFiredEvent event) {
+            logger.info("RULE {}: before match fired", event.getMatch().getRule().getName());
+        }
+
+        @Override
+        public void afterMatchFired(AfterMatchFiredEvent event) {
+            logger.info("RULE {}: after match fired", event.getMatch().getRule().getName());
+        }
+    }
+
+    /**
+     * Base Class to track Working Memory updates for objects of type T.
+     */
+    private abstract class KieObjectExpectedCallback<T> extends DefaultRuleRuntimeEventListener {
+        protected T subject;
+
+        protected CountDownLatch countDownLatch = new CountDownLatch(1);
+
+        public KieObjectExpectedCallback(T affected) {
+            subject = affected;
+            register();
+        }
+
+        public boolean isNotified() throws InterruptedException {
+            return countDownLatch.await(9L, TimeUnit.SECONDS);
+        }
+
+        protected void callbacked() {
+            unregister();
+            countDownLatch.countDown();
+        }
+
+        public KieObjectExpectedCallback<T> register() {
+            getKieSession().addEventListener(this);
+            return this;
+        }
+
+        public KieObjectExpectedCallback<T> unregister() {
+            getKieSession().removeEventListener(this);
+            return this;
+        }
+    }
+
+    /**
+     * Tracks inserts in Working Memory for an object of type T.
+     */
+    private class KieObjectInsertedExpectedCallback<T> extends KieObjectExpectedCallback<T> {
+        public KieObjectInsertedExpectedCallback(T affected) {
+            super(affected);
+        }
+
+        @Override
+        public void objectInserted(ObjectInsertedEvent event) {
+            if (subject == event.getObject()) {
+                callbacked();
+            }
+        }
+    }
+
+    /**
+     * Tracks inserts in Working Memory for any object of class T.
+     */
+    private class KieClassInsertedExpectedCallback<T> extends KieObjectInsertedExpectedCallback<T> {
+
+        public KieClassInsertedExpectedCallback(T affected) {
+            super(affected);
+        }
+
+        public void objectInserted(ObjectInsertedEvent event) {
+            if (subject == event.getObject().getClass()) {
+                callbacked();
+            }
+        }
+    }
+
+    // these may be overridden by junit tests
+
+
+    protected PolicyEngine makeEngine() {
+        return PolicyEngineConstants.getManager();
+    }
+
+
+    protected SystemPersistence makePdpdRepo() {
+        return SystemPersistenceConstants.getManager();
+    }
+
+    protected KieSession getKieSession() {
+        return getControllerFactory().get(controllerName).getDrools().getContainer().getPolicySession(controllerName)
+                        .getKieSession();
+    }
+
+    protected PolicyControllerFactory getControllerFactory() {
+        return PolicyControllerConstants.getFactory();
+    }
+
+    protected void installArtifact(File kmoduleFile, File pomFile, String resourceDir, List<File> ruleFiles)
+                    throws IOException {
+
+        KieUtils.installArtifact(kmoduleFile, pomFile, resourceDir, ruleFiles);
+    }
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/SimulatorException.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/SimulatorException.java
new file mode 100644
index 0000000..f9880d0
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/SimulatorException.java
@@ -0,0 +1,44 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+/**
+ * Exception thrown by <i>Simulators</i>.
+ */
+public class SimulatorException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public SimulatorException() {
+        super();
+    }
+
+    public SimulatorException(String message) {
+        super(message);
+    }
+
+    public SimulatorException(Throwable cause) {
+        super(cause);
+    }
+
+    public SimulatorException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Simulators.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Simulators.java
new file mode 100644
index 0000000..4a325a4
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Simulators.java
@@ -0,0 +1,85 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import java.util.LinkedList;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.Getter;
+import org.onap.policy.common.endpoints.http.server.HttpServletServer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Simulator container.
+ */
+public class Simulators {
+    private static final Logger logger = LoggerFactory.getLogger(Simulators.class);
+
+    @Getter(AccessLevel.PROTECTED)
+    private final List<HttpServletServer> servers = new LinkedList<>();
+
+    /**
+     * Constructs the object.
+     */
+    public Simulators() {
+        super();
+    }
+
+    /**
+     * Invokes the given functions to start the simulators. Destroys <i>all</i> of the
+     * simulators if any fail to start.
+     *
+     * @param builders functions to invoke to build the simulators
+     */
+    public void start(SimulatorBuilder... builders) {
+        try {
+            for (SimulatorBuilder builder : builders) {
+                servers.add(builder.build());
+            }
+        } catch (InterruptedException e) {
+            logger.warn("interrupted building the simulators");
+            destroy();
+            Thread.currentThread().interrupt();
+            throw new SimulatorException(e);
+        }
+    }
+
+    /**
+     * Stops all of the simulators.
+     */
+    public void destroy() {
+        for (HttpServletServer server : servers) {
+            try {
+                server.shutdown();
+            } catch (RuntimeException e) {
+                logger.warn("error stopping simulator {}", server.getName(), e);
+            }
+        }
+
+        servers.clear();
+    }
+
+    @FunctionalInterface
+    public static interface SimulatorBuilder {
+        public HttpServletServer build() throws InterruptedException;
+    }
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/TestNames.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/TestNames.java
new file mode 100644
index 0000000..5f3d856
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/TestNames.java
@@ -0,0 +1,44 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for a test CLASS. Used to identify the names of the test methods to be
+ * included.
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TestNames {
+    /**
+     * Test method names to be included.
+     */
+    String[] names() default {};
+
+    /**
+     * Prefixes of test method names to be included.
+     */
+    String[] prefixes() default {};
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/TopicException.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/TopicException.java
new file mode 100644
index 0000000..01a8669
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/TopicException.java
@@ -0,0 +1,44 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+/**
+ * Exception thrown by <i>Topics</i>.
+ */
+public class TopicException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public TopicException() {
+        super();
+    }
+
+    public TopicException(String message) {
+        super(message);
+    }
+
+    public TopicException(Throwable cause) {
+        super(cause);
+    }
+
+    public TopicException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Topics.java b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Topics.java
new file mode 100644
index 0000000..f71559a
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/java/org/onap/policy/controlloop/common/rules/test/Topics.java
@@ -0,0 +1,176 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Function;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpoint;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.resources.ResourceUtils;
+import org.onap.policy.drools.protocol.coders.EventProtocolCoder;
+import org.onap.policy.drools.protocol.coders.EventProtocolCoderConstants;
+import org.onap.policy.drools.system.PolicyController;
+
+/**
+ * Mechanism by which junit tests can manage topic messages.
+ */
+public class Topics {
+    /**
+     * Wherever this string appears within an input file, it is replaced by a value passed
+     * as a parameter to the {@link #inject(String, String, String)} method.
+     */
+    private static final String REPLACE_ME = "${replaceMe}";
+
+    /**
+     * Listeners that have been created and registered by "this" object.
+     */
+    private final List<Listener<?>> listeners = new LinkedList<>();
+
+
+    /**
+     * Constructs the object.
+     */
+    public Topics() {
+        super();
+    }
+
+    /**
+     * Unregisters all of the listeners.
+     */
+    public void destroy() {
+        listeners.forEach(Listener::unregister);
+    }
+
+    /**
+     * Injects the content of the given file onto a NOOP topic SOURCE.
+     *
+     * @param topicName topic on which to inject
+     * @param file file whose content is to be injected
+     */
+    public void inject(String topicName, String file) {
+        inject(topicName, file, REPLACE_ME);
+    }
+
+    /**
+     * Injects the content of the given file onto a NOOP topic SOURCE, with the given
+     * substitution.
+     *
+     * @param topicName topic on which to inject
+     * @param file file whose content is to be injected
+     * @param newText text to be substituted for occurrences of "${replaceMe}" in the
+     *        source file
+     */
+    public void inject(String topicName, String file, String newText) {
+        try {
+            String text = ResourceUtils.getResourceAsString(file);
+            if (text == null) {
+                throw new FileNotFoundException(file);
+            }
+            text = text.replace(REPLACE_ME, newText);
+            getTopicManager().getNoopTopicSource(topicName).offer(text);
+        } catch (IOException e) {
+            throw new TopicException(e);
+        }
+    }
+
+    /**
+     * Creates a listener for messages published on a NOOP topic SINK. Messages are
+     * decoded using the coder associated with the controller.
+     *
+     * @param <T> message type
+     * @param topicName name of the topic on which to listen
+     * @param expectedClass type of message expected
+     * @param controller controller whose decoders are to be used
+     * @return a new listener
+     */
+    public <T> Listener<T> createListener(String topicName, Class<T> expectedClass, PolicyController controller) {
+        EventProtocolCoder mgr = getProtocolCoder();
+        String groupId = controller.getDrools().getGroupId();
+        String artifactId = controller.getDrools().getArtifactId();
+
+        // @formatter:off
+        return createListener(topicName,
+            event -> expectedClass.cast(mgr.decode(groupId, artifactId, topicName, event)));
+        // @formatter:on
+    }
+
+    /**
+     * Creates a listener for messages published on a NOOP topic SINK. Messages are
+     * decoded using the specified coder.
+     *
+     * @param <T> message type
+     * @param topicName name of the topic on which to listen
+     * @param expectedClass type of message expected
+     * @param coder coder to decode the messages
+     * @return a new listener
+     */
+    public <T> Listener<T> createListener(String topicName, Class<T> expectedClass, Coder coder) {
+        Function<String, T> decoder = event -> {
+            try {
+                return coder.decode(event, expectedClass);
+            } catch (CoderException e) {
+                throw new IllegalArgumentException("cannot decode message", e);
+            }
+        };
+
+        return createListener(topicName, decoder);
+    }
+
+    /**
+     * Creates a listener for messages published on a NOOP topic SINK. Messages are
+     * decoded using the specified decoder.
+     *
+     * @param <T> message type
+     * @param topicName name of the topic on which to listen
+     * @param decoder function that takes a message and decodes it into the desired type
+     * @return a new listener
+     */
+    public <T> Listener<T> createListener(String topicName, Function<String, T> decoder) {
+        Listener<T> listener = makeListener(topicName, decoder);
+        listeners.add(listener);
+
+        return listener;
+    }
+
+    // these methods may be overridden by junit tests
+
+    protected TopicEndpoint getTopicManager() {
+        return TopicEndpointManager.getManager();
+    }
+
+    protected EventProtocolCoder getProtocolCoder() {
+        return EventProtocolCoderConstants.getManager();
+    }
+
+    protected <T> Listener<T> makeListener(String topicName, Function<String, T> decoder) {
+        return new Listener<>(topicName, decoder) {
+            @Override
+            protected TopicEndpoint getTopicManager() {
+                return Topics.this.getTopicManager();
+            }
+        };
+    }
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.appc.success.json b/controlloop/common/rules-test/src/main/resources/duplicates/duplicates.appc.success.json
similarity index 100%
copy from controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.appc.success.json
copy to controlloop/common/rules-test/src/main/resources/duplicates/duplicates.appc.success.json
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.onset.1.json b/controlloop/common/rules-test/src/main/resources/duplicates/duplicates.onset.1.json
similarity index 62%
rename from controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.onset.1.json
rename to controlloop/common/rules-test/src/main/resources/duplicates/duplicates.onset.1.json
index d08ee47..70f4844 100644
--- a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.onset.1.json
+++ b/controlloop/common/rules-test/src/main/resources/duplicates/duplicates.onset.1.json
@@ -1,16 +1,16 @@
 {
-  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+  "closedLoopControlName": "ControlLoop-Duplicates-48f0c2c3-a172-4192-9ae3-052274181b6e",
   "closedLoopAlarmStart": 1463679805324,
   "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
   "closedLoopEventStatus": "ONSET",
-  "requestID": "664be3d2-6c12-4f4b-a3e7-c349acced200",
+  "requestID": "${replaceMe}",
   "target_type": "VNF",
   "target": "generic-vnf.vnf-id",
   "AAI": {
     "vserver.is-closed-loop-disabled": "false",
     "vserver.prov-status": "ACTIVE",
-    "generic-vnf.vnf-id": "vCPE_Infrastructure_vGMUX_demo_app"
+    "generic-vnf.vnf-id": "duplicate-VNF"
   },
   "from": "DCAE",
   "version": "1.0.2"
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.2.json b/controlloop/common/rules-test/src/main/resources/duplicates/duplicates.onset.2.json
similarity index 82%
rename from controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.2.json
rename to controlloop/common/rules-test/src/main/resources/duplicates/duplicates.onset.2.json
index d08ee47..0462436 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.2.json
+++ b/controlloop/common/rules-test/src/main/resources/duplicates/duplicates.onset.2.json
@@ -1,5 +1,5 @@
 {
-  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+  "closedLoopControlName": "ControlLoop-Duplicates-48f0c2c3-a172-4192-9ae3-052274181b6e",
   "closedLoopAlarmStart": 1463679805324,
   "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
   "closedLoopEventStatus": "ONSET",
@@ -13,4 +13,4 @@
   },
   "from": "DCAE",
   "version": "1.0.2"
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json b/controlloop/common/rules-test/src/main/resources/duplicates/tosca-compliant-duplicates.json
similarity index 89%
copy from controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
copy to controlloop/common/rules-test/src/main/resources/duplicates/tosca-compliant-duplicates.json
index b876446..d674727 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
+++ b/controlloop/common/rules-test/src/main/resources/duplicates/tosca-compliant-duplicates.json
@@ -7,7 +7,8 @@
         "policy-id": "operational.restart"
     },
     "properties": {
-        "id": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+        "controllerName": "usecases",
+        "id": "ControlLoop-Duplicates-48f0c2c3-a172-4192-9ae3-052274181b6e",
         "timeout": 3600,
         "abatement": false,
         "trigger": "unique-policy-id-1-restart",
@@ -31,7 +32,6 @@
                 "failure_exception": "final_failure_exception",
                 "failure_guard": "final_failure_guard"
             }
-        ],
-        "controllerName": "frankfurt"
+        ]
     }
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json b/controlloop/common/rules-test/src/main/resources/service123/service123.appc.migrate.success.json
similarity index 80%
copy from controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json
copy to controlloop/common/rules-test/src/main/resources/service123/service123.appc.migrate.success.json
index b221b6b..563e2c7 100644
--- a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json
+++ b/controlloop/common/rules-test/src/main/resources/service123/service123.appc.migrate.success.json
@@ -6,17 +6,17 @@
         "api-ver": "5.00",
         "originator-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
         "request-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
-        "sub-request-id": "1",
+        "sub-request-id": "${replaceMe}",
         "flags": {}
       },
       "status": {
         "code": 400,
-        "message": "Restart Successful"
+        "message": "Migrate Successful"
       }
     }
   },
   "version": "2.0",
-  "rpc-name": "restart",
+  "rpc-name": "migrate",
   "correlation-id": "664be3d2-6c12-4f4b-a3e7-c349acced200-1",
   "type": "response"
 }
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json b/controlloop/common/rules-test/src/main/resources/service123/service123.appc.rebuild.failure.json
similarity index 76%
copy from controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json
copy to controlloop/common/rules-test/src/main/resources/service123/service123.appc.rebuild.failure.json
index b221b6b..88e70ef 100644
--- a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json
+++ b/controlloop/common/rules-test/src/main/resources/service123/service123.appc.rebuild.failure.json
@@ -6,17 +6,17 @@
         "api-ver": "5.00",
         "originator-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
         "request-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
-        "sub-request-id": "1",
+        "sub-request-id": "${replaceMe}",
         "flags": {}
       },
       "status": {
-        "code": 400,
-        "message": "Restart Successful"
+        "code": 401,
+        "message": "Rebuild Failed"
       }
     }
   },
   "version": "2.0",
-  "rpc-name": "restart",
+  "rpc-name": "rebuild",
   "correlation-id": "664be3d2-6c12-4f4b-a3e7-c349acced200-1",
   "type": "response"
 }
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json b/controlloop/common/rules-test/src/main/resources/service123/service123.appc.restart.failure.json
similarity index 81%
rename from controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json
rename to controlloop/common/rules-test/src/main/resources/service123/service123.appc.restart.failure.json
index b221b6b..7376c8a 100644
--- a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.appc.success.json
+++ b/controlloop/common/rules-test/src/main/resources/service123/service123.appc.restart.failure.json
@@ -6,12 +6,12 @@
         "api-ver": "5.00",
         "originator-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
         "request-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
-        "sub-request-id": "1",
+        "sub-request-id": "${replaceMe}",
         "flags": {}
       },
       "status": {
-        "code": 400,
-        "message": "Restart Successful"
+        "code": 401,
+        "message": "Restart Failed"
       }
     }
   },
diff --git a/controlloop/common/controller-usecases/src/test/resources/vfw/vfw.onset.json b/controlloop/common/rules-test/src/main/resources/service123/service123.onset.json
similarity index 64%
rename from controlloop/common/controller-usecases/src/test/resources/vfw/vfw.onset.json
rename to controlloop/common/rules-test/src/main/resources/service123/service123.onset.json
index 7782867..a78cb53 100644
--- a/controlloop/common/controller-usecases/src/test/resources/vfw/vfw.onset.json
+++ b/controlloop/common/rules-test/src/main/resources/service123/service123.onset.json
@@ -1,9 +1,9 @@
 {
-  "closedLoopControlName": "ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a",
+  "closedLoopControlName": "ControlLoop-Service123-cbed919f-2212-4ef7-8051-fe6308da1bda",
   "closedLoopAlarmStart": 1463679805324,
-  "closedLoopEventClient": "microservice.stringmatcher",
+  "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
   "closedLoopEventStatus": "ONSET",
-  "requestID": "c7c6a4aa-bb61-4a15-b831-ba1472dd4a65",
+  "requestID": "664be3d2-6c12-4f4b-a3e7-c349acced200",
   "target_type": "VNF",
   "target": "generic-vnf.vnf-name",
   "AAI": {
diff --git a/controlloop/common/rules-test/src/main/resources/service123/tosca-compliant-service123.json b/controlloop/common/rules-test/src/main/resources/service123/tosca-compliant-service123.json
new file mode 100644
index 0000000..4eaa9e7
--- /dev/null
+++ b/controlloop/common/rules-test/src/main/resources/service123/tosca-compliant-service123.json
@@ -0,0 +1,75 @@
+{
+    "type": "onap.policies.controlloop.operational.common.Drools",
+    "type_version": "1.0.0",
+    "version": "1.0.0",
+    "name": "operational.service123",
+    "metadata": {
+        "policy-id": "operational.service123"
+    },
+    "properties": {
+        "controllerName": "usecases",
+        "id": "ControlLoop-Service123-cbed919f-2212-4ef7-8051-fe6308da1bda",
+        "timeout": 60,
+        "abatement": true,
+        "trigger": "unique-policy-id-1-restart",
+        "operations": [
+            {
+                "id": "unique-policy-id-1-restart",
+                "description": "Restart the VM",
+                "operation": {
+                    "actor": "APPC",
+                    "operation": "Restart",
+                    "target": {
+                        "targetType": "VM"
+                    }
+                },
+                "timeout": 20,
+                "retries": 3,
+                "success": "final_success",
+                "failure": "unique-policy-id-2-rebuild",
+                "failure_timeout": "unique-policy-id-2-rebuild",
+                "failure_retries": "unique-policy-id-2-rebuild",
+                "failure_guard": "unique-policy-id-2-rebuild",
+                "failure_exception": "final_failure_exception"
+            },
+            {
+                "id": "unique-policy-id-2-rebuild",
+                "name": "Rebuild Policy",
+                "operation": {
+                    "actor": "APPC",
+                    "operation": "Rebuild",
+                    "target": {
+                        "targetType": "VM"
+                    }
+                },
+                "timeout": 10,
+                "retries": 0,
+                "success": "final_success",
+                "failure": "unique-policy-id-3-migrate",
+                "failure_timeout": "unique-policy-id-3-migrate",
+                "failure_retries": "unique-policy-id-3-migrate",
+                "failure_guard": "unique-policy-id-3-migrate",
+                "failure_exception": "final_failure_exception"
+            },
+            {
+                "id": "unique-policy-id-3-migrate",
+                "name": "Migrate Policy",
+                "operation": {
+                    "actor": "APPC",
+                    "operation": "Migrate",
+                    "target": {
+                        "targetType": "VM"
+                    }
+                },
+                "timeout": 30,
+                "retries": 0,
+                "success": "final_success",
+                "failure": "final_failure",
+                "failure_timeout": "final_failure_timeout",
+                "failure_retries": "final_failure_retries",
+                "failure_guard": "final_failure_guard",
+                "failure_exception": "final_failure_exception"
+            }
+        ]
+    }
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json b/controlloop/common/rules-test/src/main/resources/vcpe/tosca-compliant-vcpe.json
similarity index 95%
rename from controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
rename to controlloop/common/rules-test/src/main/resources/vcpe/tosca-compliant-vcpe.json
index b876446..b70145b 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
+++ b/controlloop/common/rules-test/src/main/resources/vcpe/tosca-compliant-vcpe.json
@@ -7,6 +7,7 @@
         "policy-id": "operational.restart"
     },
     "properties": {
+        "controllerName": "usecases",
         "id": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
         "timeout": 3600,
         "abatement": false,
@@ -31,7 +32,6 @@
                 "failure_exception": "final_failure_exception",
                 "failure_guard": "final_failure_guard"
             }
-        ],
-        "controllerName": "frankfurt"
+        ]
     }
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-legacy-vcpe.json b/controlloop/common/rules-test/src/main/resources/vcpe/tosca-legacy-vcpe.json
similarity index 99%
rename from controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-legacy-vcpe.json
rename to controlloop/common/rules-test/src/main/resources/vcpe/tosca-legacy-vcpe.json
index f42c07d..cb0806e 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-legacy-vcpe.json
+++ b/controlloop/common/rules-test/src/main/resources/vcpe/tosca-legacy-vcpe.json
@@ -6,4 +6,4 @@
   },
   "name": "vcpe",
   "version": "1.0.0"
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.appc.success.json b/controlloop/common/rules-test/src/main/resources/vcpe/vcpe.appc.success.json
similarity index 100%
rename from controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.appc.success.json
rename to controlloop/common/rules-test/src/main/resources/vcpe/vcpe.appc.success.json
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.1.json b/controlloop/common/rules-test/src/main/resources/vcpe/vcpe.onset.1.json
similarity index 100%
rename from controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.1.json
rename to controlloop/common/rules-test/src/main/resources/vcpe/vcpe.onset.1.json
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.onset.2.json b/controlloop/common/rules-test/src/main/resources/vcpe/vcpe.onset.2.json
similarity index 100%
rename from controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.onset.2.json
rename to controlloop/common/rules-test/src/main/resources/vcpe/vcpe.onset.2.json
diff --git a/controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.onset.3.json b/controlloop/common/rules-test/src/main/resources/vcpe/vcpe.onset.3.json
similarity index 100%
rename from controlloop/common/controller-usecases/src/test/resources/vcpe/vcpe.onset.3.json
rename to controlloop/common/rules-test/src/main/resources/vcpe/vcpe.onset.3.json
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vdns/tosca-compliant-vdns.json b/controlloop/common/rules-test/src/main/resources/vdns/tosca-compliant-vdns.json
similarity index 97%
rename from controlloop/common/controller-frankfurt/src/test/resources/vdns/tosca-compliant-vdns.json
rename to controlloop/common/rules-test/src/main/resources/vdns/tosca-compliant-vdns.json
index 1f4cb41..918e927 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vdns/tosca-compliant-vdns.json
+++ b/controlloop/common/rules-test/src/main/resources/vdns/tosca-compliant-vdns.json
@@ -7,6 +7,7 @@
         "policy-id": "operational.scale.up"
     },
     "properties": {
+        "controllerName": "usecases",
         "id": "ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3",
         "timeout": 60,
         "abatement": false,
@@ -42,7 +43,6 @@
                 "failure_exception": "final_failure_exception",
                 "failure_guard": "final_failure_guard"
             }
-        ],
-        "controllerName": "frankfurt"
+        ]
     }
 }
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vdns/vdns.onset.json b/controlloop/common/rules-test/src/main/resources/vdns/vdns.onset.json
similarity index 100%
rename from controlloop/common/controller-frankfurt/src/test/resources/vdns/vdns.onset.json
rename to controlloop/common/rules-test/src/main/resources/vdns/vdns.onset.json
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-compliant-vfw.json b/controlloop/common/rules-test/src/main/resources/vfw/tosca-compliant-vfw.json
similarity index 96%
rename from controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-compliant-vfw.json
rename to controlloop/common/rules-test/src/main/resources/vfw/tosca-compliant-vfw.json
index 47cb09d..f551456 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-compliant-vfw.json
+++ b/controlloop/common/rules-test/src/main/resources/vfw/tosca-compliant-vfw.json
@@ -7,6 +7,7 @@
         "policy-id": "operational.modifyconfig"
     },
     "properties": {
+        "controllerName": "usecases",
         "id": "ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a",
         "timeout": 60,
         "abatement": false,
@@ -34,7 +35,6 @@
                 "failure_exception": "final_failure_exception",
                 "failure_guard": "final_failure_guard"
             }
-        ],
-        "controllerName": "frankfurt"
+        ]
     }
 }
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-vfw.json b/controlloop/common/rules-test/src/main/resources/vfw/tosca-vfw.json
similarity index 99%
rename from controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-vfw.json
rename to controlloop/common/rules-test/src/main/resources/vfw/tosca-vfw.json
index 5d1e352..35a8396 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-vfw.json
+++ b/controlloop/common/rules-test/src/main/resources/vfw/tosca-vfw.json
@@ -6,4 +6,4 @@
   },
   "name": "vfw",
   "version": "1.0.0"
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.appc.success.json b/controlloop/common/rules-test/src/main/resources/vfw/vfw.appc.success.json
similarity index 99%
rename from controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.appc.success.json
rename to controlloop/common/rules-test/src/main/resources/vfw/vfw.appc.success.json
index d7e6ec3..eb19c68 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.appc.success.json
+++ b/controlloop/common/rules-test/src/main/resources/vfw/vfw.appc.success.json
@@ -14,4 +14,4 @@
   "Payload": {
     "generic-vnf.vnf-id": "jimmy-test-vnf2"
   }
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.onset.json b/controlloop/common/rules-test/src/main/resources/vfw/vfw.onset.json
similarity index 100%
rename from controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.onset.json
rename to controlloop/common/rules-test/src/main/resources/vfw/vfw.onset.json
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-compliant-vlb.json b/controlloop/common/rules-test/src/main/resources/vlb/tosca-compliant-vlb.json
similarity index 97%
rename from controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-compliant-vlb.json
rename to controlloop/common/rules-test/src/main/resources/vlb/tosca-compliant-vlb.json
index aeb22bb..8a3b64a 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-compliant-vlb.json
+++ b/controlloop/common/rules-test/src/main/resources/vlb/tosca-compliant-vlb.json
@@ -7,6 +7,7 @@
         "policy-id": "operational.scaleout"
     },
     "properties": {
+        "controllerName": "usecases",
         "id": "ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3",
         "timeout": 1200,
         "abatement": false,
@@ -42,7 +43,6 @@
                 "failure_exception": "final_failure_exception",
                 "failure_guard": "final_failure_guard"
             }
-        ],
-        "controllerName": "frankfurt"
+        ]
     }
 }
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-vlb.json b/controlloop/common/rules-test/src/main/resources/vlb/tosca-vlb.json
similarity index 99%
rename from controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-vlb.json
rename to controlloop/common/rules-test/src/main/resources/vlb/tosca-vlb.json
index 5147d99..c4f2b81 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-vlb.json
+++ b/controlloop/common/rules-test/src/main/resources/vlb/tosca-vlb.json
@@ -6,4 +6,4 @@
   },
   "name": "vlb",
   "version": "1.0.0"
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vlb/vlb.onset.json b/controlloop/common/rules-test/src/main/resources/vlb/vlb.onset.json
similarity index 99%
rename from controlloop/common/controller-frankfurt/src/test/resources/vlb/vlb.onset.json
rename to controlloop/common/rules-test/src/main/resources/vlb/vlb.onset.json
index 3360c0a..23ad03c 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vlb/vlb.onset.json
+++ b/controlloop/common/rules-test/src/main/resources/vlb/vlb.onset.json
@@ -13,4 +13,4 @@
   },
   "from": "DCAE",
   "version": "1.0.2"
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/BaseRuleTestTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/BaseRuleTestTest.java
new file mode 100644
index 0000000..745013b
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/BaseRuleTestTest.java
@@ -0,0 +1,459 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.appc.CommonHeader;
+import org.onap.policy.appc.Request;
+import org.onap.policy.appclcm.AppcLcmBody;
+import org.onap.policy.appclcm.AppcLcmCommonHeader;
+import org.onap.policy.appclcm.AppcLcmDmaapWrapper;
+import org.onap.policy.appclcm.AppcLcmInput;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.coder.StandardCoderInstantAsMillis;
+import org.onap.policy.controlloop.ControlLoopNotificationType;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyIdentifier;
+import org.powermock.reflect.Whitebox;
+
+public class BaseRuleTestTest {
+    private static final String CONTROLLER_NAME = "my-controller-name";
+    private static final String POLICY_NAME = "my-policy-name";
+
+    // saved values
+    private static Function<String, Rules> ruleMaker;
+    private static Supplier<HttpClients> httpClientMaker;
+    private static Supplier<Simulators> simMaker;
+    private static Supplier<Topics> topicMaker;
+
+    private BaseRuleTest base;
+    private LinkedList<VirtualControlLoopNotification> clMgtQueue;
+    private Queue<AppcLcmDmaapWrapper> appcLcmQueue;
+    private Queue<Request> appcLegacyQueue;
+    private int permitCount;
+    private int finalCount;
+
+    @Mock
+    private PolicyController controller;
+    @Mock
+    private Rules rules;
+    @Mock
+    private HttpClients httpClients;
+    @Mock
+    private Simulators simulators;
+    @Mock
+    private Topics topics;
+    @Mock
+    private Listener<VirtualControlLoopNotification> policyClMgt;
+    @Mock
+    private Listener<Request> appcClSink;
+    @Mock
+    private Listener<AppcLcmDmaapWrapper> appcLcmRead;
+    @Mock
+    private DroolsController drools;
+    @Mock
+    private ToscaPolicy policy;
+    @Mock
+    private ToscaPolicyIdentifier policyIdent;
+
+
+    /**
+     * Saves static values from the class.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        ruleMaker = Whitebox.getInternalState(BaseRuleTest.class, "ruleMaker");
+        httpClientMaker = Whitebox.getInternalState(BaseRuleTest.class, "httpClientMaker");
+        simMaker = Whitebox.getInternalState(BaseRuleTest.class, "simMaker");
+        topicMaker = Whitebox.getInternalState(BaseRuleTest.class, "topicMaker");
+    }
+
+    /**
+     * Restores static values.
+     */
+    @AfterClass
+    public static void tearDownAfterClass() {
+        Whitebox.setInternalState(BaseRuleTest.class, "ruleMaker", ruleMaker);
+        Whitebox.setInternalState(BaseRuleTest.class, "httpClientMaker", httpClientMaker);
+        Whitebox.setInternalState(BaseRuleTest.class, "simMaker", simMaker);
+        Whitebox.setInternalState(BaseRuleTest.class, "topicMaker", topicMaker);
+    }
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(policy.getIdentifier()).thenReturn(policyIdent);
+        when(policyIdent.getName()).thenReturn(POLICY_NAME);
+
+        when(drools.factCount(CONTROLLER_NAME)).thenReturn(0L);
+        when(controller.getDrools()).thenReturn(drools);
+
+        when(rules.getControllerName()).thenReturn(CONTROLLER_NAME);
+        when(rules.getController()).thenReturn(controller);
+        when(rules.setupPolicyFromFile(any())).thenAnswer(args -> {
+            when(drools.factCount(CONTROLLER_NAME)).thenReturn(2L);
+            return policy;
+        });
+
+        when(topics.createListener(BaseRuleTest.POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class, controller))
+                        .thenReturn(policyClMgt);
+        when(topics.createListener(eq(BaseRuleTest.APPC_LCM_READ_TOPIC), eq(AppcLcmDmaapWrapper.class),
+                        any(StandardCoder.class))).thenReturn(appcLcmRead);
+        when(topics.createListener(eq(BaseRuleTest.APPC_CL_TOPIC), eq(Request.class),
+                        any(StandardCoderInstantAsMillis.class))).thenReturn(appcClSink);
+
+        Function<String, Rules> ruleMaker = this::makeRules;
+        Supplier<HttpClients> httpClientMaker = this::makeHttpClients;
+        Supplier<Simulators> simMaker = this::makeSim;
+        Supplier<Topics> topicMaker = this::makeTopics;
+
+        Whitebox.setInternalState(BaseRuleTest.class, "ruleMaker", ruleMaker);
+        Whitebox.setInternalState(BaseRuleTest.class, "httpClientMaker", httpClientMaker);
+        Whitebox.setInternalState(BaseRuleTest.class, "simMaker", simMaker);
+        Whitebox.setInternalState(BaseRuleTest.class, "topicMaker", topicMaker);
+
+        clMgtQueue = new LinkedList<>();
+        appcLcmQueue = new LinkedList<>();
+        appcLegacyQueue = new LinkedList<>();
+
+        when(policyClMgt.await(any())).thenAnswer(args -> {
+            VirtualControlLoopNotification notif = clMgtQueue.remove();
+            Predicate<VirtualControlLoopNotification> pred = args.getArgument(0);
+            assertTrue(pred.test(notif));
+            return notif;
+        });
+
+        when(appcLcmRead.await(any())).thenAnswer(args -> {
+            AppcLcmDmaapWrapper req = appcLcmQueue.remove();
+            Predicate<AppcLcmDmaapWrapper> pred = args.getArgument(0);
+            assertTrue(pred.test(req));
+            return req;
+        });
+
+        when(appcClSink.await(any())).thenAnswer(args -> {
+            Request req = appcLegacyQueue.remove();
+            Predicate<Request> pred = args.getArgument(0);
+            assertTrue(pred.test(req));
+            return req;
+        });
+
+        permitCount = 0;
+        finalCount = 0;
+
+        base = new MyTest();
+
+        BaseRuleTest.initStatics(CONTROLLER_NAME);
+        base.init();
+    }
+
+    @Test
+    public void testInitStatics() {
+        assertSame(rules, BaseRuleTest.rules);
+        assertSame(httpClients, BaseRuleTest.httpClients);
+        assertSame(simulators, BaseRuleTest.simulators);
+    }
+
+    @Test
+    public void testFinishStatics() {
+        BaseRuleTest.finishStatics();
+
+        verify(rules).destroy();
+        verify(httpClients).destroy();
+        verify(simulators).destroy();
+    }
+
+    @Test
+    public void testInit() {
+        assertSame(topics, base.getTopics());
+        assertSame(controller, base.controller);
+    }
+
+    @Test
+    public void testFinish() {
+        base.finish();
+
+        verify(topics).destroy();
+        verify(rules).resetFacts();
+    }
+
+    @Test
+    public void testTestService123Compliant() {
+        enqueueAppcLcm("restart", "restart", "restart", "restart", "rebuild", "migrate");
+        enqueueClMgt(ControlLoopNotificationType.OPERATION_SUCCESS);
+        enqueueClMgt(ControlLoopNotificationType.FINAL_SUCCESS);
+
+        base.testService123Compliant();
+
+        assertEquals(1, permitCount);
+        assertEquals(1, finalCount);
+
+        assertTrue(appcLcmQueue.isEmpty());
+        assertTrue(clMgtQueue.isEmpty());
+
+        // initial event
+        verify(topics).inject(eq(BaseRuleTest.DCAE_TOPIC), any());
+
+        // replies to each APPC request
+        verify(topics, times(6)).inject(eq(BaseRuleTest.APPC_LCM_WRITE_TOPIC), any(), any());
+    }
+
+    @Test
+    public void testTestDuplicatesEvents() {
+        enqueueAppcLcm("restart", "restart");
+        enqueueClMgt(ControlLoopNotificationType.FINAL_FAILURE);
+        enqueueClMgt(ControlLoopNotificationType.FINAL_SUCCESS);
+        enqueueClMgt(ControlLoopNotificationType.FINAL_SUCCESS);
+
+        clMgtQueue.get(1).setAai(Map.of("generic-vnf.vnf-id", "duplicate-VNF"));
+        clMgtQueue.get(2).setAai(Map.of("generic-vnf.vnf-id", "vCPE_Infrastructure_vGMUX_demo_app"));
+
+        base.testDuplicatesEvents();
+
+        assertEquals(0, permitCount);
+        assertEquals(3, finalCount);
+
+        assertTrue(appcLcmQueue.isEmpty());
+        assertTrue(clMgtQueue.isEmpty());
+
+        // initial events
+        verify(topics).inject(eq(BaseRuleTest.DCAE_TOPIC), any());
+        verify(topics, times(2)).inject(eq(BaseRuleTest.DCAE_TOPIC), any(), any());
+
+        // two restarts
+        verify(topics, times(2)).inject(eq(BaseRuleTest.APPC_LCM_WRITE_TOPIC), any(), any());
+    }
+
+    @Test
+    public void testTestVcpeSunnyDayLegacy() {
+        checkAppcLcmPolicy("restart", base::testVcpeSunnyDayLegacy);
+    }
+
+    @Test
+    public void testTestVcpeSunnyDayCompliant() {
+        checkAppcLcmPolicy("restart", base::testVcpeSunnyDayCompliant);
+    }
+
+    @Test
+    public void testTestVcpeOnsetFloodPrevention() {
+        enqueueAppcLcm("restart");
+        enqueueClMgt(ControlLoopNotificationType.OPERATION_SUCCESS);
+        enqueueClMgt(ControlLoopNotificationType.FINAL_SUCCESS);
+
+        base.testVcpeOnsetFloodPrevention();
+
+        assertEquals(1, permitCount);
+        assertEquals(1, finalCount);
+
+        assertTrue(appcLcmQueue.isEmpty());
+        assertTrue(clMgtQueue.isEmpty());
+
+        // initial events
+        verify(topics, times(3)).inject(eq(BaseRuleTest.DCAE_TOPIC), any());
+
+        // one restart
+        verify(topics).inject(eq(BaseRuleTest.APPC_LCM_WRITE_TOPIC), any(), any());
+    }
+
+    @Test
+    public void testTestVdnsSunnyDayCompliant() {
+        checkHttpPolicy(base::testVdnsSunnyDayCompliant);
+    }
+
+    @Test
+    public void testTestVfwSunnyDayLegacy() {
+        checkAppcLegacyPolicy("ModifyConfig", base::testVfwSunnyDayLegacy);
+    }
+
+    @Test
+    public void testTestVfwSunnyDayCompliant() {
+        checkAppcLegacyPolicy("ModifyConfig", base::testVfwSunnyDayCompliant);
+    }
+
+    @Test
+    public void testTestVlbSunnyDayLegacy() {
+        checkHttpPolicy(base::testVlbSunnyDayLegacy);
+    }
+
+    @Test
+    public void testTestVlbSunnyDayCompliant() {
+        checkHttpPolicy(base::testVlbSunnyDayCompliant);
+    }
+
+    protected void checkAppcLcmPolicy(String operation, Runnable test) {
+        enqueueAppcLcm(operation);
+        enqueueClMgt(ControlLoopNotificationType.OPERATION_SUCCESS);
+        enqueueClMgt(ControlLoopNotificationType.FINAL_SUCCESS);
+
+        test.run();
+
+        assertEquals(1, permitCount);
+        assertEquals(1, finalCount);
+
+        assertTrue(appcLcmQueue.isEmpty());
+        assertTrue(clMgtQueue.isEmpty());
+
+        // initial event
+        verify(topics).inject(eq(BaseRuleTest.DCAE_TOPIC), any());
+
+        // reply to each APPC request
+        verify(topics).inject(eq(BaseRuleTest.APPC_LCM_WRITE_TOPIC), any(), any());
+    }
+
+    protected void checkAppcLegacyPolicy(String operation, Runnable test) {
+        enqueueAppcLegacy(operation);
+        enqueueClMgt(ControlLoopNotificationType.OPERATION_SUCCESS);
+        enqueueClMgt(ControlLoopNotificationType.FINAL_SUCCESS);
+
+        test.run();
+
+        assertEquals(1, permitCount);
+        assertEquals(1, finalCount);
+
+        assertTrue(appcLcmQueue.isEmpty());
+        assertTrue(clMgtQueue.isEmpty());
+
+        // initial event
+        verify(topics).inject(eq(BaseRuleTest.DCAE_TOPIC), any());
+
+        // reply to each APPC request
+        verify(topics).inject(eq(BaseRuleTest.APPC_CL_TOPIC), any(), any());
+    }
+
+    protected void checkHttpPolicy(Runnable test) {
+        enqueueClMgt(ControlLoopNotificationType.OPERATION_SUCCESS);
+        enqueueClMgt(ControlLoopNotificationType.FINAL_SUCCESS);
+
+        test.run();
+
+        assertEquals(1, permitCount);
+        assertEquals(1, finalCount);
+
+        assertTrue(clMgtQueue.isEmpty());
+
+        // initial event
+        verify(topics).inject(eq(BaseRuleTest.DCAE_TOPIC), any());
+    }
+
+    private void enqueueClMgt(ControlLoopNotificationType type) {
+        VirtualControlLoopNotification notif = new VirtualControlLoopNotification();
+        notif.setNotification(type);
+        notif.setPolicyName(POLICY_NAME + ".EVENT.MANAGER.FINAL");
+
+        clMgtQueue.add(notif);
+    }
+
+    private void enqueueAppcLcm(String... operationNames) {
+        for (String oper : operationNames) {
+            AppcLcmDmaapWrapper req = new AppcLcmDmaapWrapper();
+            req.setRpcName(oper);
+
+            AppcLcmBody body = new AppcLcmBody();
+            req.setBody(body);
+
+            AppcLcmInput input = new AppcLcmInput();
+            body.setInput(input);
+
+            AppcLcmCommonHeader header = new AppcLcmCommonHeader();
+            input.setCommonHeader(header);
+
+            header.setSubRequestId("my-subrequest-id");
+
+            appcLcmQueue.add(req);
+        }
+    }
+
+    private void enqueueAppcLegacy(String... operationNames) {
+        for (String oper : operationNames) {
+            Request req = new Request();
+            req.setAction(oper);
+
+            CommonHeader header = new CommonHeader();
+            req.setCommonHeader(header);
+
+            header.setSubRequestId("my-subrequest-id");
+
+            appcLegacyQueue.add(req);
+        }
+    }
+
+    private Rules makeRules(String controllerName) {
+        return rules;
+    }
+
+    private HttpClients makeHttpClients() {
+        return httpClients;
+    }
+
+    private Simulators makeSim() {
+        return simulators;
+    }
+
+    private Topics makeTopics() {
+        return topics;
+    }
+
+    /*
+     * We don't want junit trying to run this, so it's marked "Ignore".
+     */
+    @Ignore
+    private class MyTest extends BaseRuleTest {
+
+        @Override
+        protected void waitForLockAndPermit(ToscaPolicy policy, Listener<VirtualControlLoopNotification> policyClMgt) {
+            permitCount++;
+        }
+
+        @Override
+        protected VirtualControlLoopNotification waitForFinal(ToscaPolicy policy,
+                        Listener<VirtualControlLoopNotification> policyClMgt, ControlLoopNotificationType finalType) {
+            finalCount++;
+            return policyClMgt.await(notif -> notif.getNotification() == finalType);
+        }
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/ExceptionsTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/ExceptionsTest.java
new file mode 100644
index 0000000..a42ff6f
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/ExceptionsTest.java
@@ -0,0 +1,38 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.onap.policy.common.utils.test.ExceptionsTester;
+import org.onap.policy.controlloop.common.rules.test.SimulatorException;
+import org.onap.policy.controlloop.common.rules.test.TopicException;
+
+public class ExceptionsTest {
+
+    @Test
+    public void test() {
+        assertEquals(4, new ExceptionsTester().testAllException(TopicException.class));
+
+        assertEquals(4, new ExceptionsTester().testAllException(SimulatorException.class));
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/HttpClientsTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/HttpClientsTest.java
new file mode 100644
index 0000000..5db8353
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/HttpClientsTest.java
@@ -0,0 +1,94 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.Properties;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.endpoints.http.client.HttpClient;
+import org.onap.policy.common.endpoints.http.client.HttpClientConfigException;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactory;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
+import org.onap.policy.controlloop.common.rules.test.HttpClients;
+import org.onap.policy.drools.persistence.SystemPersistenceConstants;
+
+public class HttpClientsTest {
+    private static final String CLIENT_NAME = "MY-CLIENT";
+
+    @Mock
+    private HttpClientFactory factory;
+
+
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        SystemPersistenceConstants.getManager().setConfigurationDir("src/test/resources");
+    }
+
+    @After
+    public void tearDown() {
+        HttpClientFactoryInstance.getClientFactory().destroy();
+    }
+
+    @Test
+    public void test() throws HttpClientConfigException {
+        MockitoAnnotations.initMocks(this);
+
+        HttpClientFactoryInstance.getClientFactory().destroy();
+
+        HttpClients clients = new HttpClients();
+
+        clients.addClients("my");
+
+        // should find the client now
+        HttpClient client = HttpClientFactoryInstance.getClientFactory().get(CLIENT_NAME);
+        assertNotNull(client);
+
+        clients.destroy();
+
+        // destroyed - should NOT find the client
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> HttpClientFactoryInstance.getClientFactory().get(CLIENT_NAME));
+
+        // unknown property file
+        assertThatIllegalArgumentException().isThrownBy(() -> clients.addClients("unknown"));
+
+        // force exception from builder
+        HttpClients clients2 = new HttpClients() {
+            @Override
+            protected HttpClientFactory getClientFactory() {
+                return factory;
+            }
+        };
+
+        when(factory.build(any(Properties.class))).thenThrow(new HttpClientConfigException("expected exception"));
+
+        assertThatIllegalArgumentException().isThrownBy(() -> clients2.addClients("my"))
+                        .withMessage("cannot initialize HTTP clients");
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/ListenerTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/ListenerTest.java
new file mode 100644
index 0000000..07ccc40
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/ListenerTest.java
@@ -0,0 +1,202 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpoint;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
+import org.onap.policy.common.endpoints.event.comm.bus.NoopTopicSink;
+import org.onap.policy.common.endpoints.parameters.TopicParameters;
+import org.onap.policy.controlloop.common.rules.test.Listener;
+import org.onap.policy.controlloop.common.rules.test.TopicException;
+
+public class ListenerTest {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final String MY_TOPIC = "my-topic";
+    private static final String MESSAGE = "the-message";
+    private static final String MESSAGE2 = "other-message";
+    private static final String MSG_SUFFIX = "s";
+    private static final String DECODED_MESSAGE = MESSAGE + MSG_SUFFIX;
+
+    @Mock
+    private NoopTopicSink sink;
+    @Mock
+    private TopicEndpoint mgr;
+
+    private Listener<String> listener;
+
+    /**
+     * Creates topics.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        TopicEndpointManager.getManager().shutdown();
+
+        TopicParameters params = new TopicParameters();
+        params.setTopic(MY_TOPIC);
+        params.setManaged(true);
+        params.setTopicCommInfrastructure("NOOP");
+
+        TopicEndpointManager.getManager().addTopicSinks(List.of(params));
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() {
+        TopicEndpointManager.getManager().shutdown();
+    }
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mgr.getNoopTopicSink(MY_TOPIC)).thenReturn(sink);
+
+        listener = new Listener<>(MY_TOPIC, msg -> msg + MSG_SUFFIX) {
+            @Override
+            protected TopicEndpoint getTopicManager() {
+                return mgr;
+            }
+        };
+    }
+
+    @Test
+    public void testListener() {
+        verify(sink).register(listener);
+    }
+
+    @Test
+    public void testAwait_testAwaitLongTimeUnit_testIsEmpty() {
+        assertTrue(listener.isEmpty());
+
+        listener.onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MESSAGE);
+        assertFalse(listener.isEmpty());
+
+        assertEquals(DECODED_MESSAGE, listener.await());
+
+        assertTrue(listener.isEmpty());
+    }
+
+    @Test
+    public void testAwaitPredicateOfT() {
+        listener.onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MESSAGE);
+        listener.onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MESSAGE2);
+        assertEquals(MESSAGE2 + MSG_SUFFIX, listener.await(msg -> msg.startsWith("other-")));
+    }
+
+    /**
+     * Tests await() when the remaining time is negative.
+     */
+    @Test
+    public void testAwaitLongTimeUnitPredicateNoTime() {
+        assertThatThrownBy(() -> listener.await(-1, TimeUnit.SECONDS)).isInstanceOf(TopicException.class);
+    }
+
+    /**
+     * Tests await() when the poll() returns {@code null}.
+     */
+    @Test
+    public void testAwaitLongTimeUnitPredicateNoMessage() {
+        assertThatThrownBy(() -> listener.await(1, TimeUnit.MILLISECONDS)).isInstanceOf(TopicException.class);
+    }
+
+    /**
+     * Tests await() when the poll() is interrupted.
+     */
+    @Test
+    public void testAwaitLongTimeUnitPredicateInterrupted() throws InterruptedException {
+        listener = new Listener<String>(MY_TOPIC, msg -> msg) {
+            @Override
+            protected String pollMessage(long remainingMs) throws InterruptedException {
+                throw new InterruptedException(EXPECTED_EXCEPTION);
+            }
+        };
+
+        AtomicReference<TopicException> exref = new AtomicReference<>();
+        CountDownLatch interrupted = new CountDownLatch(1);
+
+        Thread thread = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    listener.await();
+                } catch (TopicException e) {
+                    exref.set(e);
+                }
+
+                if (Thread.currentThread().isInterrupted()) {
+                    interrupted.countDown();
+                }
+            }
+        };
+
+        thread.start();
+        assertTrue(interrupted.await(5, TimeUnit.SECONDS));
+        assertNotNull(exref.get());
+    }
+
+    @Test
+    public void testUnregister() {
+        listener.unregister();
+        verify(sink).unregister(listener);
+    }
+
+    @Test
+    public void testOnTopicEvent() {
+        listener = new Listener<>(MY_TOPIC, msg -> {
+            throw new IllegalArgumentException(EXPECTED_EXCEPTION);
+        });
+
+        // onTopicEvent() should not throw an exception
+        assertThatCode(() -> listener.onTopicEvent(CommInfrastructure.NOOP, MY_TOPIC, MESSAGE))
+                        .doesNotThrowAnyException();
+
+        // should not have queued a message
+        assertTrue(listener.isEmpty());
+    }
+
+    @Test
+    public void testGetTopicManager() {
+        // use a listener with a real manager
+        assertNotNull(new Listener<>(MY_TOPIC, msg -> msg).getTopicManager());
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/NamedRunnerTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/NamedRunnerTest.java
new file mode 100644
index 0000000..fe9ff80
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/NamedRunnerTest.java
@@ -0,0 +1,87 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.AfterClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Tests NamedRunner. The tests don't do much, because all we really want to check is
+ * which tests are executed based on the {@link TestNames} annotation.
+ */
+@RunWith(NamedRunner.class)
+@TestNames(names = {"testAbc", "testDef", "testIgnore"}, prefixes = {"testGhi", "testJkl"})
+public class NamedRunnerTest {
+
+    private static int testCount = 0;
+
+    @AfterClass
+    public static void tearDownAfterClass() {
+        assertEquals(5, testCount);
+    }
+
+    @Test
+    public void testAbc() {
+        checkTest();
+    }
+
+    @Test
+    public void testAbc2() {
+        fail("should not run");
+    }
+
+    @Test
+    public void testDef() {
+        checkTest();
+    }
+
+    @Test
+    @Ignore
+    public void testIgnore() {
+        fail("should not run");
+    }
+
+    @Test
+    public void testGhi1() {
+        checkTest();
+    }
+
+    @Test
+    public void testGhi2() {
+        checkTest();
+    }
+
+    @Test
+    public void testJkl() {
+        checkTest();
+    }
+
+
+    private static void checkTest() {
+        ++testCount;
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/NamedRunnerTest2.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/NamedRunnerTest2.java
new file mode 100644
index 0000000..1ed5b20
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/NamedRunnerTest2.java
@@ -0,0 +1,58 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Tests NamedRunner when the TestNames annotation is missing - all tests should be
+ * executed.
+ */
+@RunWith(NamedRunner.class)
+public class NamedRunnerTest2 {
+
+    private static int testCount = 0;
+
+    @AfterClass
+    public static void tearDownAfterClass() {
+        assertEquals(2, testCount);
+    }
+
+    @Test
+    public void testAbc() {
+        checkTest();
+    }
+
+    @Test
+    public void testDef() {
+        checkTest();
+    }
+
+
+    private static void checkTest() {
+        ++testCount;
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/RulesTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/RulesTest.java
new file mode 100644
index 0000000..28cb977
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/RulesTest.java
@@ -0,0 +1,341 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.kie.api.definition.rule.Rule;
+import org.kie.api.event.rule.AfterMatchFiredEvent;
+import org.kie.api.event.rule.AgendaEventListener;
+import org.kie.api.event.rule.BeforeMatchFiredEvent;
+import org.kie.api.event.rule.MatchCancelledEvent;
+import org.kie.api.event.rule.MatchCreatedEvent;
+import org.kie.api.event.rule.ObjectDeletedEvent;
+import org.kie.api.event.rule.ObjectInsertedEvent;
+import org.kie.api.event.rule.ObjectUpdatedEvent;
+import org.kie.api.event.rule.RuleRuntimeEventListener;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.Match;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.controlloop.ControlLoopEvent;
+import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.persistence.SystemPersistence;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.drools.system.PolicyControllerFactory;
+import org.onap.policy.drools.system.PolicyEngine;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+
+public class RulesTest {
+    private static final String CONTROLLER_NAME = "rulesTest";
+    private static final String POLICY_FILE = "src/test/resources/tosca-policy.json";
+    private static final String MY_POLICY = "operational.restart";
+    private static final String RESOURCE_DIR = "src/test/resources";
+    private static final String MY_RULE_NAME = "my-rule-name";
+    private static final String MY_TEXT = "my text";
+
+    @Mock
+    private PolicyEngine engine;
+    @Mock
+    private SystemPersistence repo;
+    @Mock
+    private PolicyController controller;
+    @Mock
+    private KieSession kieSession;
+    @Mock
+    private PolicyControllerFactory controllerFactory;
+    @Mock
+    private DroolsController drools;
+
+    private List<Object> facts;
+    private List<RuleRuntimeEventListener> ruleListeners;
+    private List<AgendaEventListener> agendaListeners;
+    private Properties properties;
+    private boolean installed;
+
+    private Rules rules;
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        facts = new LinkedList<>();
+        ruleListeners = new LinkedList<>();
+        agendaListeners = new LinkedList<>();
+        installed = false;
+        properties = new Properties();
+
+        when(engine.createPolicyController(any(), any())).thenReturn(controller);
+        when(repo.getControllerProperties(CONTROLLER_NAME)).thenReturn(properties);
+        when(controller.getDrools()).thenReturn(drools);
+
+        when(drools.facts(eq(CONTROLLER_NAME), any())).thenAnswer(args -> {
+            Class<?> clazz = args.getArgument(1);
+            return facts.stream().filter(obj -> obj.getClass() == clazz).collect(Collectors.toList());
+        });
+
+        // notify listeners when objects are added to drools
+        when(drools.offer(any())).thenAnswer(args -> {
+            Object object = args.getArgument(0);
+            notifyInserted(object);
+
+            if (!(object instanceof ToscaPolicy)) {
+                return true;
+            }
+
+            // "insert" Params objects associated with the policy (i.e., mimic the rules)
+            ToscaPolicy policy = (ToscaPolicy) object;
+            ControlLoopParams params = new ControlLoopParams();
+            params.setToscaPolicy(policy);
+            notifyInserted(params);
+
+            return true;
+        });
+
+        when(drools.delete(any())).thenAnswer(args -> {
+            Class<?> clazz = args.getArgument(0);
+            facts.removeIf(obj -> obj.getClass() == clazz);
+            return null;
+        });
+
+        // handle rule listener registration and deregistration with the kieSession
+        doAnswer(args -> {
+            ruleListeners.add(args.getArgument(0));
+            return null;
+        }).when(kieSession).addEventListener(any(RuleRuntimeEventListener.class));
+
+        doAnswer(args -> {
+            ruleListeners.remove(args.getArgument(0));
+            return null;
+        }).when(kieSession).removeEventListener(any(RuleRuntimeEventListener.class));
+
+        // handle agenda listener registration and deregistration with the kieSession
+        doAnswer(args -> {
+            agendaListeners.add(args.getArgument(0));
+            return null;
+        }).when(kieSession).addEventListener(any(AgendaEventListener.class));
+
+        doAnswer(args -> {
+            agendaListeners.remove(args.getArgument(0));
+            return null;
+        }).when(kieSession).removeEventListener(any(AgendaEventListener.class));
+
+        rules = new MyRules();
+
+        rules.configure(RESOURCE_DIR);
+        rules.start();
+    }
+
+    @Test
+    public void testRules() {
+        assertEquals(CONTROLLER_NAME, rules.getControllerName());
+
+        assertSame(engine, rules.getPdpd());
+        assertSame(repo, rules.getPdpdRepo());
+        assertSame(controller, rules.getController());
+    }
+
+    @Test
+    public void testStart() throws Exception {
+        verify(repo).setConfigurationDir("src/test/resources/config");
+        assertTrue(installed);
+        verify(engine).configure(any(Properties.class));
+        verify(engine).createPolicyController(CONTROLLER_NAME, properties);
+        verify(engine).start();
+
+        verify(kieSession).addEventListener(any(RuleRuntimeEventListener.class));
+        verify(kieSession).addEventListener(any(AgendaEventListener.class));
+    }
+
+    @Test
+    public void testDestroy() {
+        rules.destroy();
+
+        verify(controllerFactory).shutdown(CONTROLLER_NAME);
+        verify(engine).stop();
+    }
+
+    @Test
+    public void testResetFacts() {
+        rules.resetFacts();
+
+        verify(drools).delete(ToscaPolicy.class);
+        verify(drools).delete(ControlLoopParams.class);
+        verify(drools).delete(ControlLoopEventManager2.class);
+        verify(drools).delete(ControlLoopEvent.class);
+    }
+
+    @Test
+    public void testSetupPolicyFromTemplate_testGetPolicyFromTemplate() {
+        rules.setupPolicyFromTemplate("tosca-template.json", MY_POLICY);
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> rules.setupPolicyFromTemplate("missing-file.json", "a-policy"));
+    }
+
+    @Test
+    public void testSetupPolicyFromFile_testGetPolicyFromFile_testSetupPolicy() {
+        assertNotNull(rules.setupPolicyFromFile(POLICY_FILE));
+
+        assertThatIllegalArgumentException().isThrownBy(() -> rules.setupPolicyFromFile("missing-file.json"));
+    }
+
+    @Test
+    public void testRuleListenerLogger() {
+        Rule rule = mock(Rule.class);
+        when(rule.getName()).thenReturn(MY_RULE_NAME);
+
+        // insertions - with and without rule name
+        ObjectInsertedEvent insert = mock(ObjectInsertedEvent.class);
+        when(insert.getObject()).thenReturn(MY_TEXT);
+        ruleListeners.forEach(listener -> listener.objectInserted(insert));
+        when(insert.getRule()).thenReturn(rule);
+        ruleListeners.forEach(listener -> listener.objectInserted(insert));
+
+        // updates - with and without rule name
+        ObjectUpdatedEvent update = mock(ObjectUpdatedEvent.class);
+        when(update.getObject()).thenReturn(MY_TEXT);
+        ruleListeners.forEach(listener -> listener.objectUpdated(update));
+        when(update.getRule()).thenReturn(rule);
+        ruleListeners.forEach(listener -> listener.objectUpdated(update));
+
+        // deletions - with and without rule name
+        ObjectDeletedEvent delete = mock(ObjectDeletedEvent.class);
+        when(delete.getOldObject()).thenReturn(MY_TEXT);
+        ruleListeners.forEach(listener -> listener.objectDeleted(delete));
+        when(delete.getRule()).thenReturn(rule);
+        ruleListeners.forEach(listener -> listener.objectDeleted(delete));
+    }
+
+    @Test
+    public void testAgendaListenerLogger() {
+        Rule rule = mock(Rule.class);
+        when(rule.getName()).thenReturn(MY_RULE_NAME);
+
+        Match match = mock(Match.class);
+        when(match.getRule()).thenReturn(rule);
+
+        // create
+        MatchCreatedEvent create = mock(MatchCreatedEvent.class);
+        when(create.getMatch()).thenReturn(match);
+        agendaListeners.forEach(listener -> listener.matchCreated(create));
+
+        // cancel
+        MatchCancelledEvent cancel = mock(MatchCancelledEvent.class);
+        when(cancel.getMatch()).thenReturn(match);
+        agendaListeners.forEach(listener -> listener.matchCancelled(cancel));
+
+        // before-fire
+        BeforeMatchFiredEvent before = mock(BeforeMatchFiredEvent.class);
+        when(before.getMatch()).thenReturn(match);
+        agendaListeners.forEach(listener -> listener.beforeMatchFired(before));
+
+        // after-fire
+        AfterMatchFiredEvent after = mock(AfterMatchFiredEvent.class);
+        when(after.getMatch()).thenReturn(match);
+        agendaListeners.forEach(listener -> listener.afterMatchFired(after));
+    }
+
+    @Test
+    public void testMakePdpd_testMakePdpdRepo() {
+        // need rules that makes real objects
+        rules = new Rules(CONTROLLER_NAME);
+
+        assertNotNull(rules.getPdpd());
+        assertNotNull(rules.getPdpdRepo());
+    }
+
+    protected void notifyInserted(Object object) {
+        // add it to our list
+        facts.add(object);
+
+        // increase code coverage by adding random objects
+        ObjectInsertedEvent event0 = mock(ObjectInsertedEvent.class);
+        when(event0.getObject()).thenReturn(new Object());
+        ruleListeners.forEach(listener -> listener.objectInserted(event0));
+
+        // increase code coverage by adding a random object
+        ObjectInsertedEvent event = mock(ObjectInsertedEvent.class);
+        when(event.getObject()).thenReturn(object);
+
+        if (object instanceof ToscaPolicy) {
+            // increase code coverage by associating it with a random rule
+            Rule rule = mock(Rule.class);
+            when(rule.getName()).thenReturn(MY_RULE_NAME);
+            when(event.getRule()).thenReturn(rule);
+        }
+
+        ruleListeners.forEach(listener -> listener.objectInserted(event));
+    }
+
+    private class MyRules extends Rules {
+        public MyRules() {
+            super(CONTROLLER_NAME);
+        }
+
+        @Override
+        protected PolicyEngine makeEngine() {
+            return engine;
+        }
+
+        @Override
+        protected SystemPersistence makePdpdRepo() {
+            return repo;
+        }
+
+        @Override
+        protected KieSession getKieSession() {
+            return kieSession;
+        }
+
+        @Override
+        protected PolicyControllerFactory getControllerFactory() {
+            return controllerFactory;
+        }
+
+        @Override
+        protected void installArtifact(File kmoduleFile, File pomFile, String resourceDir, List<File> ruleFiles) {
+            installed = true;
+        }
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/SimulatorsTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/SimulatorsTest.java
new file mode 100644
index 0000000..8a87c4f
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/SimulatorsTest.java
@@ -0,0 +1,105 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.endpoints.http.server.HttpServletServer;
+import org.onap.policy.controlloop.common.rules.test.SimulatorException;
+import org.onap.policy.controlloop.common.rules.test.Simulators;
+import org.onap.policy.controlloop.common.rules.test.Simulators.SimulatorBuilder;
+
+public class SimulatorsTest {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+
+    @Mock
+    private HttpServletServer server1;
+    @Mock
+    private HttpServletServer server2;
+    @Mock
+    private HttpServletServer server3;
+
+    private Simulators simulators;
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        simulators = new Simulators();
+    }
+
+    @Test
+    public void testStart() {
+        simulators.start(() -> server1, () -> server2);
+        assertEquals(List.of(server1, server2), simulators.getServers());
+
+        verify(server1, never()).shutdown();
+        verify(server2, never()).shutdown();
+    }
+
+    /**
+     * Tests start() when one of the builders throws an exception.
+     */
+    @Test
+    public void testStartException() {
+        SimulatorBuilder exbuilder = () -> {
+            throw new InterruptedException(EXPECTED_EXCEPTION);
+        };
+
+        assertThatThrownBy(() -> simulators.start(() -> server1, () -> server2, exbuilder, () -> server3))
+                        .isInstanceOf(SimulatorException.class);
+
+        assertTrue(simulators.getServers().isEmpty());
+
+        verify(server1).shutdown();
+        verify(server2).shutdown();
+
+        // shouldn't have reached this builder, so nothing to shut down
+        verify(server3, never()).shutdown();
+    }
+
+    @Test
+    public void testDestroy() {
+        simulators.start(() -> server1, () -> server2, () -> server3);
+
+        doThrow(new IllegalStateException(EXPECTED_EXCEPTION)).when(server2).shutdown();
+
+        simulators.destroy();
+
+        verify(server1).shutdown();
+        verify(server3).shutdown();
+
+        assertTrue(simulators.getServers().isEmpty());
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/TopicsTest.java b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/TopicsTest.java
new file mode 100644
index 0000000..6ed8c38
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/java/org/onap/policy/controlloop/common/rules/test/TopicsTest.java
@@ -0,0 +1,230 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.common.rules.test;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import lombok.ToString;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpoint;
+import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager;
+import org.onap.policy.common.endpoints.event.comm.bus.NoopTopicSink;
+import org.onap.policy.common.endpoints.event.comm.bus.NoopTopicSource;
+import org.onap.policy.common.endpoints.parameters.TopicParameters;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.drools.controller.DroolsController;
+import org.onap.policy.drools.protocol.coders.EventProtocolCoder;
+import org.onap.policy.drools.system.PolicyController;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+
+public class TopicsTest {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final String MY_SOURCE_TOPIC = "my-source-topic";
+    private static final String MY_SINK_TOPIC = "my-sink-topic";
+    private static final String MY_GROUP = "my-group";
+    private static final String MY_ARTIFACT = "my-artifact";
+    private static final String MESSAGE = "{\"text\": \"hello\"}";
+    private static final String TEXT = "hello";
+    private static final String INJECT_FILE = "src/test/resources/topics.json";
+    private static final String POLICY_NAME = "my-policy";
+
+    @Mock
+    private DroolsController drools;
+    @Mock
+    private PolicyController controller;
+    @Mock
+    private EventProtocolCoder protocolCoder;
+    @Mock
+    private NoopTopicSink sink;
+    @Mock
+    private NoopTopicSource source;
+    @Mock
+    private TopicEndpoint mgr;
+
+    private ToscaPolicy policy;
+
+    private Topics topics;
+
+    /**
+     * Creates topics.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        TopicEndpointManager.getManager().shutdown();
+
+        TopicParameters params = new TopicParameters();
+        params.setTopic(MY_SOURCE_TOPIC);
+        params.setManaged(true);
+        params.setTopicCommInfrastructure("NOOP");
+        TopicEndpointManager.getManager().addTopicSources(List.of(params));
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() {
+        TopicEndpointManager.getManager().shutdown();
+    }
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        policy = new ToscaPolicy();
+        policy.setName(POLICY_NAME);
+        policy.setVersion("1.0.0");
+
+        when(drools.getGroupId()).thenReturn(MY_GROUP);
+        when(drools.getArtifactId()).thenReturn(MY_ARTIFACT);
+
+        when(controller.getDrools()).thenReturn(drools);
+
+        when(protocolCoder.decode(MY_GROUP, MY_ARTIFACT, MY_SINK_TOPIC, MESSAGE)).thenReturn(TEXT);
+
+        when(mgr.getNoopTopicSink(MY_SINK_TOPIC)).thenReturn(sink);
+        when(mgr.getNoopTopicSource(MY_SOURCE_TOPIC)).thenReturn(source);
+
+        topics = new Topics() {
+            @Override
+            protected TopicEndpoint getTopicManager() {
+                return mgr;
+            }
+
+            @Override
+            protected EventProtocolCoder getProtocolCoder() {
+                return protocolCoder;
+            }
+        };
+    }
+
+    @Test
+    public void testDestroy() {
+        Listener<String> listener1 = topics.createListener(MY_SINK_TOPIC, msg -> msg);
+        Listener<String> listener2 = topics.createListener(MY_SINK_TOPIC, msg -> msg + "a suffix");
+
+        topics.destroy();
+
+        verify(sink).unregister(listener1);
+        verify(sink).unregister(listener2);
+    }
+
+    @Test
+    public void testInjectStringFile() throws IOException {
+        topics.inject(MY_SOURCE_TOPIC, INJECT_FILE);
+
+        // nothing should have been replaced
+        String expected = new String(Files.readAllBytes(Paths.get(INJECT_FILE)));
+        verify(source).offer(expected);
+    }
+
+    @Test
+    public void testInjectStringFileString() throws IOException {
+        topics.inject(MY_SOURCE_TOPIC, INJECT_FILE, "hello");
+
+        // text should have been replaced with "hello"
+        String expected = new String(Files.readAllBytes(Paths.get("src", "test", "resources", "topicsReplaced.json")));
+        verify(source).offer(expected);
+
+        // exception reading file
+        assertThatThrownBy(() -> topics.inject(MY_SOURCE_TOPIC, "missing-file.json", "some text"))
+                        .isInstanceOf(TopicException.class);
+    }
+
+    @Test
+    public void testCreateListenerStringClassOfTPolicyController() {
+        Listener<String> listener = topics.createListener(MY_SINK_TOPIC, String.class, controller);
+        listener.onTopicEvent(CommInfrastructure.NOOP, MY_SINK_TOPIC, MESSAGE);
+
+        assertEquals(TEXT, listener.await());
+    }
+
+    @Test
+    public void testCreateListenerStringClassOfTCoder() {
+        Listener<Data> listener = topics.createListener(MY_SINK_TOPIC, Data.class, new StandardCoder());
+        listener.onTopicEvent(CommInfrastructure.NOOP, MY_SINK_TOPIC, MESSAGE);
+
+        Data expected = new Data();
+        expected.text = TEXT;
+        assertEquals(expected.toString(), listener.await().toString());
+    }
+
+    /**
+     * Tests createListener() when the coder throws an exception.
+     */
+    @Test
+    public void testCreateListenerStringClassOfTCoderException() {
+        StandardCoder coder = new StandardCoder() {
+            @Override
+            public <T> T decode(String arg0, Class<T> arg1) throws CoderException {
+                throw new CoderException(EXPECTED_EXCEPTION);
+            }
+        };
+
+        Listener<Data> listener = topics.createListener(MY_SINK_TOPIC, Data.class, coder);
+
+        // onTopicEvent() should not throw an exception
+        assertThatCode(() -> listener.onTopicEvent(CommInfrastructure.NOOP, MY_SINK_TOPIC, MESSAGE))
+                        .doesNotThrowAnyException();
+
+        // should not have queued a message
+        assertThatThrownBy(() -> listener.await(0, TimeUnit.MILLISECONDS)).isInstanceOf(TopicException.class);
+    }
+
+    @Test
+    public void testCreateListenerStringFunctionOfStringT() {
+        Listener<String> listener = topics.createListener(MY_SINK_TOPIC, msg -> msg);
+        listener.onTopicEvent(CommInfrastructure.NOOP, MY_SINK_TOPIC, MESSAGE);
+
+        assertEquals(MESSAGE, listener.await());
+    }
+
+    @Test
+    public void testGetTopicManager_testGetProtocolCoder() {
+        // use a topic with a real manager
+        topics = new Topics();
+
+        assertNotNull(topics.getTopicManager());
+        assertNotNull(topics.getProtocolCoder());
+    }
+
+    @ToString
+    private static class Data {
+        private String text;
+    }
+}
diff --git a/controlloop/common/rules-test/src/test/resources/META-INF/kmodule.xml b/controlloop/common/rules-test/src/test/resources/META-INF/kmodule.xml
new file mode 100644
index 0000000..07041c6
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/META-INF/kmodule.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+  ================================================================================
+  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.
+  ============LICENSE_END=========================================================
+  -->
+<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule">
+    <kbase name="onap.policies.controlloop.operational.common.Drools" default="false" equalsBehavior="equality"/>
+    <kbase name="onap.policies.controlloop.Operational" equalsBehavior="equality"
+           packages="org.onap.policy.controlloop" includes="onap.policies.controlloop.operational.common.Drools">
+        <ksession name="rulesTest"/>
+    </kbase>
+</kmodule>
diff --git a/controlloop/common/rules-test/src/test/resources/config/rulesTest-controller.properties b/controlloop/common/rules-test/src/test/resources/config/rulesTest-controller.properties
new file mode 100644
index 0000000..11df5e9
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/config/rulesTest-controller.properties
@@ -0,0 +1,25 @@
+#
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+
+controller.name=rulesTest
+
+rules.groupId=org.onap.policy.controlloop
+rules.artifactId=rulesTest
+rules.version=1.0.0
diff --git a/controlloop/common/rules-test/src/test/resources/my-http-client.properties b/controlloop/common/rules-test/src/test/resources/my-http-client.properties
new file mode 100644
index 0000000..7d4ef90
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/my-http-client.properties
@@ -0,0 +1,26 @@
+#
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+#
+
+http.client.services=MY-CLIENT
+
+http.client.services.MY-CLIENT.managed=true
+http.client.services.MY-CLIENT.host=localhost
+http.client.services.MY-CLIENT.port=6669
+http.client.services.MY-CLIENT.contextUriPath=some/path
diff --git a/controlloop/common/rules-test/src/test/resources/rulesTest.drl b/controlloop/common/rules-test/src/test/resources/rulesTest.drl
new file mode 100644
index 0000000..11e99e9
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/rulesTest.drl
@@ -0,0 +1,96 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * 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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop;
+
+import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
+import org.onap.policy.controlloop.CanonicalOnset;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+import org.onap.policy.controlloop.ControlLoopNotificationType;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2.NewEventStatus;
+import org.onap.policy.controlloop.eventmanager.ControlLoopOperationManager2;
+import org.onap.policy.controlloop.utils.ControlLoopUtils;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+
+import org.slf4j.LoggerFactory;
+import org.slf4j.Logger;
+
+import org.onap.policy.drools.system.PolicyEngineConstants;
+
+rule "STARTED"
+    when
+    then
+    System.out.println(drools.getRule().getName());
+end
+
+/*
+*
+* Called when the ControlLoopParams object has been inserted into working memory from the BRMSGW.
+*
+*/
+rule "INSERT.PARAMS"
+    when
+        $params : ControlLoopParams()
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: {} : TOSCA-POLICY=[{}]", $params.getClosedLoopControlName(), $params.getPolicyName() + "."
+        + drools.getRule().getName(), $params.getToscaPolicy());
+end
+
+/*
+*
+* Called when a Tosca Policy is present.
+*
+*/
+rule "NEW.TOSCA.POLICY"
+    when
+        $policy : ToscaPolicy()
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: [{}|{}|{}|{}]: CONTENT: {}", drools.getRule().getName(),
+                $policy.getType(), $policy.getTypeVersion(), $policy.getName(),
+                $policy.getVersion(), $policy);
+
+    ControlLoopParams params = ControlLoopUtils.toControlLoopParams($policy);
+    if (params != null) {
+        insert(params);
+    }
+end
+
+/*
+ * Remove Control Loop Parameters.
+ */
+rule "REMOVE.PARAMS"
+    when
+        $params : ControlLoopParams( $policyName :  getPolicyName(), $policyVersion : getPolicyVersion() )
+        not ( ToscaPolicy( getName() == $policyName, getVersion() == $policyVersion ) )
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: [{}|{}|{}]", drools.getRule().getName(),
+                $params.getPolicyScope(), $params.getPolicyName(), $params.getPolicyVersion());
+
+    retract($params);
+end
diff --git a/controlloop/common/rules-test/src/test/resources/rulesTest.pom b/controlloop/common/rules-test/src/test/resources/rulesTest.pom
new file mode 100644
index 0000000..625e736
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/rulesTest.pom
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+  ================================================================================
+  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.
+  ============LICENSE_END=========================================================
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.onap.policy.controlloop</groupId>
+    <artifactId>rulesTest</artifactId>
+    <version>1.0.0</version>
+</project>
diff --git a/controlloop/common/rules-test/src/test/resources/topics.json b/controlloop/common/rules-test/src/test/resources/topics.json
new file mode 100644
index 0000000..4e08bee
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/topics.json
@@ -0,0 +1,5 @@
+{
+  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+  "text": "${replaceMe}",
+  "moreText": "${replaceMe}"
+}
diff --git a/controlloop/common/rules-test/src/test/resources/topicsReplaced.json b/controlloop/common/rules-test/src/test/resources/topicsReplaced.json
new file mode 100644
index 0000000..743ef4c
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/topicsReplaced.json
@@ -0,0 +1,5 @@
+{
+  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+  "text": "hello",
+  "moreText": "hello"
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json b/controlloop/common/rules-test/src/test/resources/tosca-policy.json
similarity index 95%
copy from controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
copy to controlloop/common/rules-test/src/test/resources/tosca-policy.json
index b876446..42f54ab 100644
--- a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
+++ b/controlloop/common/rules-test/src/test/resources/tosca-policy.json
@@ -7,6 +7,7 @@
         "policy-id": "operational.restart"
     },
     "properties": {
+        "controllerName": "rulesTest",
         "id": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
         "timeout": 3600,
         "abatement": false,
@@ -31,7 +32,6 @@
                 "failure_exception": "final_failure_exception",
                 "failure_guard": "final_failure_guard"
             }
-        ],
-        "controllerName": "frankfurt"
+        ]
     }
-}
\ No newline at end of file
+}
diff --git a/controlloop/common/rules-test/src/test/resources/tosca-template.json b/controlloop/common/rules-test/src/test/resources/tosca-template.json
new file mode 100644
index 0000000..3057560
--- /dev/null
+++ b/controlloop/common/rules-test/src/test/resources/tosca-template.json
@@ -0,0 +1,45 @@
+{
+  "tosca_definitions_version": "tosca_simple_yaml_1_1_0",
+  "topology_template": {
+    "policies": [
+      {
+        "operational.restart": {
+          "type": "onap.policies.controlloop.operational.common.Drools",
+          "type_version": "1.0.0",
+          "version": "1.0.0",
+          "metadata": {
+            "policy-id": "operational.restart"
+          },
+          "properties": {
+            "id": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+            "timeout": 3600,
+            "abatement": true,
+            "trigger": "unique-policy-id-1-restart",
+            "operations": [
+              {
+                "id": "unique-policy-id-1-restart",
+                "description": "Restart the VM",
+                "operation": {
+                  "actor": "APPC",
+                  "operation": "Restart",
+                  "target": {
+                    "targetType": "VNF"
+                  }
+                },
+                "timeout": 1200,
+                "retries": 3,
+                "success": "final_success",
+                "failure": "final_failure",
+                "failure_timeout": "final_failure_timeout",
+                "failure_retries": "final_failure_retries",
+                "failure_exception": "final_failure_exception",
+                "failure_guard": "final_failure_guard"
+              }
+            ],
+            "controllerName": "usecases"
+          }
+        }
+      }
+    ]
+  }
+}