Add coverage to MavenDroolsController 55/92055/2
authorJim Hahn <jrh3@att.com>
Thu, 25 Jul 2019 13:49:44 +0000 (09:49 -0400)
committerJim Hahn <jrh3@att.com>
Thu, 25 Jul 2019 21:25:27 +0000 (17:25 -0400)
Change-Id: I7234c0146ef77f685a9766bb03168e385090fd1b
Issue-ID: POLICY-1772
Signed-off-by: Jim Hahn <jrh3@att.com>
feature-active-standby-management/src/main/java/org/onap/policy/drools/activestandby/DroolsPdpEntity.java
feature-active-standby-management/src/main/java/org/onap/policy/drools/activestandby/DroolsPdpImpl.java
feature-active-standby-management/src/main/java/org/onap/policy/drools/activestandby/DroolsPdpObject.java
feature-active-standby-management/src/test/java/org/onap/policy/drools/activestandby/DroolsPdpObjectTest.java
policy-management/src/main/java/org/onap/policy/drools/controller/internal/MavenDroolsController.java
policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerTest2.java [new file with mode: 0644]

index 12d716a..0055b9c 100644 (file)
@@ -2,14 +2,14 @@
  * ============LICENSE_START=======================================================
  * feature-active-standby-management
  * ================================================================================
- * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2019 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.
@@ -72,7 +72,7 @@ public class DroolsPdpEntity extends DroolsPdpObject implements Serializable {
         updatedDate = new Date();
         //When this is translated to a TimeStamp in MySQL, it assumes the date is relative
         //to the local timezone.  So, a value of Date(0) is actually Dec 31 18:00:00 CST 1969
-        //which is an invalid value for the MySql TimeStamp 
+        //which is an invalid value for the MySql TimeStamp
         designatedDate = new Date(864000000);
     }
 
@@ -135,25 +135,4 @@ public class DroolsPdpEntity extends DroolsPdpObject implements Serializable {
     public void setDesignatedDate(Date designatedDate) {
         this.designatedDate = designatedDate;
     }
-
-    @Override
-    public boolean equals(Object obj) {
-
-        if (obj instanceof DroolsPdp) {
-            DroolsPdpEntity entity = (DroolsPdpEntity) obj;
-            return this.pdpId.equals(entity.getPdpId());
-        } else {
-            return false;
-        }
-
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + (this.pdpId == null ? 0 : this.pdpId.hashCode());
-        return result;
-    }
-
 }
index 31728bc..0c50f82 100644 (file)
@@ -2,14 +2,14 @@
  * ============LICENSE_START=======================================================
  * feature-active-standby-management
  * ================================================================================
- * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved.
+ * Copyright (C) 2017-2019 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.
@@ -33,7 +33,7 @@ public class DroolsPdpImpl extends DroolsPdpObject {
 
     /**
      * Contructor.
-     * 
+     *
      * @param pdpId ID for the PDP
      * @param designated is designated
      * @param priority priority
@@ -46,11 +46,11 @@ public class DroolsPdpImpl extends DroolsPdpObject {
         this.updatedDate = updatedDate;
         //When this is translated to a TimeStamp in MySQL, it assumes the date is relative
         //to the local timezone.  So, a value of Date(0) is actually Dec 31 18:00:00 CST 1969
-        //which is an invalid value for the MySql TimeStamp 
+        //which is an invalid value for the MySql TimeStamp
         this.designatedDate = new Date(864000000);
 
     }
-    
+
     @Override
     public boolean isDesignated() {
 
@@ -61,12 +61,12 @@ public class DroolsPdpImpl extends DroolsPdpObject {
     public int getPriority() {
         return priority;
     }
-    
+
     @Override
     public void setUpdatedDate(Date date) {
         this.updatedDate = date;
     }
-    
+
     @Override
     public Date getUpdatedDate() {
         return updatedDate;
@@ -76,7 +76,7 @@ public class DroolsPdpImpl extends DroolsPdpObject {
     public String getPdpId() {
         return pdpId;
     }
-    
+
     @Override
     public void setDesignated(boolean isDesignated) {
         this.designated = isDesignated;
@@ -87,41 +87,21 @@ public class DroolsPdpImpl extends DroolsPdpObject {
     public String getSiteName() {
         return site;
     }
-    
+
     @Override
     public void setSiteName(String siteName) {
         this.site = siteName;
 
     }
-    
+
     @Override
     public Date getDesignatedDate() {
         return designatedDate;
     }
-    
+
     @Override
     public void setDesignatedDate(Date designatedDate) {
         this.designatedDate = designatedDate;
 
     }
-
-    @Override
-    public boolean equals(Object obj) {
-
-
-        if (obj instanceof DroolsPdp) {
-            DroolsPdpImpl temp = (DroolsPdpImpl) obj;
-            return this.pdpId.equals(temp.getPdpId());
-        } else {
-            return false;
-        }
-    }
-
-    @Override
-    public int hashCode() {
-        final int prime = 31;
-        int result = 1;
-        result = prime * result + (this.pdpId == null ? 0 : this.pdpId.hashCode());
-        return result;
-    }
 }
index 8ac14f0..75157bb 100644 (file)
@@ -32,6 +32,14 @@ public abstract class DroolsPdpObject implements DroolsPdp {
         }
     }
 
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (this.getPdpId() == null ? 0 : this.getPdpId().hashCode());
+        return result;
+    }
+
     private int nullSafeCompare(String one, String two) {
         if (one != null) {
             if (two != null) {
index 310f911..842d6c7 100644 (file)
@@ -65,6 +65,25 @@ public class DroolsPdpObjectTest {
         assertFalse(pdp.equals(""));
     }
 
+    @Test
+    public void testHashCode() {
+        int hc = pdp.hashCode();
+
+        // same data should yield same hash code
+        assertEquals(hc, pdp.hashCode());
+        assertEquals(hc, makePdp(PDP_ID, SITE, PRIORITY).hashCode());
+
+        // different data should yield different hash code
+        assertTrue(makePdp(PDP_ID2, SITE, PRIORITY).hashCode() != hc);
+
+        // these fields have no impact on hash code
+        assertEquals(hc, makePdp(PDP_ID, SITE, PRIORITY2).hashCode());
+        assertEquals(hc, makePdp(PDP_ID, SITE2, PRIORITY).hashCode());
+
+        // should not throw an exception
+        new MyPdp().hashCode();
+    }
+
     @Test
     public void testNullSafeCompare() {
         // self, when null
index 818cae8..b09ae85 100644 (file)
@@ -41,6 +41,7 @@ import org.kie.api.runtime.rule.QueryResultsRow;
 import org.onap.policy.common.endpoints.event.comm.TopicSink;
 import org.onap.policy.common.gson.annotation.GsonJsonIgnore;
 import org.onap.policy.common.gson.annotation.GsonJsonProperty;
+import org.onap.policy.common.utils.services.OrderedServiceImpl;
 import org.onap.policy.drools.controller.DroolsController;
 import org.onap.policy.drools.core.PolicyContainer;
 import org.onap.policy.drools.core.PolicySession;
@@ -148,7 +149,7 @@ public class MavenDroolsController implements DroolsController {
             throw new IllegalArgumentException("Missing maven version coordinate");
         }
 
-        this.policyContainer = new PolicyContainer(groupId, artifactId, version);
+        this.policyContainer = makePolicyContainer(groupId, artifactId, version);
         this.init(decoderConfigurations, encoderConfigurations);
 
         logger.debug("{}: instantiation completed ", this);
@@ -203,7 +204,7 @@ public class MavenDroolsController implements DroolsController {
         if (newGroupId.equalsIgnoreCase(this.getGroupId())
                 && newArtifactId.equalsIgnoreCase(this.getArtifactId())
                 && newVersion.equalsIgnoreCase(this.getVersion())) {
-            logger.warn("Al in the right version: " + newGroupId + ":"
+            logger.warn("All in the right version: " + newGroupId + ":"
                     + newArtifactId + ":" +  newVersion + " vs. " + this);
             return;
         }
@@ -254,18 +255,15 @@ public class MavenDroolsController implements DroolsController {
             String topic = coderConfig.getTopic();
 
             CustomGsonCoder customGsonCoder = coderConfig.getCustomGsonCoder();
-            if (coderConfig.getCustomGsonCoder() != null
-                    && coderConfig.getCustomGsonCoder().getClassContainer() != null
-                    && !coderConfig.getCustomGsonCoder().getClassContainer().isEmpty()) {
+            if (customGsonCoder != null
+                    && customGsonCoder.getClassContainer() != null
+                    && !customGsonCoder.getClassContainer().isEmpty()) {
 
-                String customGsonCoderClass = coderConfig.getCustomGsonCoder().getClassContainer();
-                if (!ReflectionUtil.isClass(this.policyContainer.getClassLoader(),
-                        customGsonCoderClass)) {
+                String customGsonCoderClass = customGsonCoder.getClassContainer();
+                if (!isClass(customGsonCoderClass)) {
                     throw makeRetrieveEx(customGsonCoderClass);
                 } else {
-                    if (logger.isInfoEnabled()) {
-                        logClassFetched(customGsonCoderClass);
-                    }
+                    logClassFetched(customGsonCoderClass);
                 }
             }
 
@@ -278,17 +276,14 @@ public class MavenDroolsController implements DroolsController {
                 String potentialCodedClass = coderFilter.getCodedClass();
                 JsonProtocolFilter protocolFilter = coderFilter.getFilter();
 
-                if (!ReflectionUtil.isClass(this.policyContainer.getClassLoader(),
-                        potentialCodedClass)) {
+                if (!isClass(potentialCodedClass)) {
                     throw makeRetrieveEx(potentialCodedClass);
                 } else {
-                    if (logger.isInfoEnabled()) {
-                        logClassFetched(potentialCodedClass);
-                    }
+                    logClassFetched(potentialCodedClass);
                 }
 
                 if (decoder) {
-                    EventProtocolCoder.manager.addDecoder(EventProtocolParams.builder()
+                    getCoderManager().addDecoder(EventProtocolParams.builder()
                             .groupId(this.getGroupId())
                             .artifactId(this.getArtifactId())
                             .topic(topic)
@@ -297,7 +292,7 @@ public class MavenDroolsController implements DroolsController {
                             .customGsonCoder(customGsonCoder)
                             .modelClassLoaderHash(this.policyContainer.getClassLoader().hashCode()));
                 } else {
-                    EventProtocolCoder.manager.addEncoder(
+                    getCoderManager().addEncoder(
                             EventProtocolParams.builder().groupId(this.getGroupId())
                                     .artifactId(this.getArtifactId()).topic(topic)
                                     .eventClass(potentialCodedClass).protocolFilter(protocolFilter)
@@ -340,7 +335,7 @@ public class MavenDroolsController implements DroolsController {
 
         for (TopicCoderFilterConfiguration coderConfig: decoderConfigurations) {
             String topic = coderConfig.getTopic();
-            EventProtocolCoder.manager.removeDecoders(this.getGroupId(), this.getArtifactId(), topic);
+            getCoderManager().removeDecoders(this.getGroupId(), this.getArtifactId(), topic);
         }
     }
 
@@ -357,14 +352,14 @@ public class MavenDroolsController implements DroolsController {
 
         for (TopicCoderFilterConfiguration coderConfig: encoderConfigurations) {
             String topic = coderConfig.getTopic();
-            EventProtocolCoder.manager.removeEncoders(this.getGroupId(), this.getArtifactId(), topic);
+            getCoderManager().removeEncoders(this.getGroupId(), this.getArtifactId(), topic);
         }
     }
 
 
     @Override
     public boolean ownsCoder(Class<? extends Object> coderClass, int modelHash) {
-        if (!ReflectionUtil.isClass(this.policyContainer.getClassLoader(), coderClass.getName())) {
+        if (!isClass(coderClass.getName())) {
             logger.error("{}{} cannot be retrieved. ", this, coderClass.getName());
             return false;
         }
@@ -473,7 +468,7 @@ public class MavenDroolsController implements DroolsController {
 
         // 1. Now, check if this topic has a decoder:
 
-        if (!EventProtocolCoder.manager.isDecodingSupported(this.getGroupId(),
+        if (!getCoderManager().isDecodingSupported(this.getGroupId(),
                 this.getArtifactId(),
                 topic)) {
 
@@ -486,7 +481,7 @@ public class MavenDroolsController implements DroolsController {
 
         Object anEvent;
         try {
-            anEvent = EventProtocolCoder.manager.decode(this.getGroupId(),
+            anEvent = getCoderManager().decode(this.getGroupId(),
                     this.getArtifactId(),
                     topic,
                     event);
@@ -520,7 +515,7 @@ public class MavenDroolsController implements DroolsController {
 
         // Broadcast
 
-        for (DroolsControllerFeatureApi feature : DroolsControllerFeatureApi.providers.getList()) {
+        for (DroolsControllerFeatureApi feature : getDroolsProviders().getList()) {
             try {
                 if (feature.beforeInsert(this, event)) {
                     return true;
@@ -536,7 +531,7 @@ public class MavenDroolsController implements DroolsController {
             logger.warn(this + "Failed to inject into PolicyContainer {}", this.getSessionNames());
         }
 
-        for (DroolsControllerFeatureApi feature : DroolsControllerFeatureApi.providers.getList()) {
+        for (DroolsControllerFeatureApi feature : getDroolsProviders().getList()) {
             try {
                 if (feature.afterInsert(this, event, successInject)) {
                     return true;
@@ -556,7 +551,7 @@ public class MavenDroolsController implements DroolsController {
 
         logger.info("{}DELIVER: {} FROM {} TO {}", this, event, this, sink);
 
-        for (DroolsControllerFeatureApi feature : DroolsControllerFeatureApi.providers.getList()) {
+        for (DroolsControllerFeatureApi feature : getDroolsProviders().getList()) {
             try {
                 if (feature.beforeDeliver(this, sink, event)) {
                     return true;
@@ -585,7 +580,7 @@ public class MavenDroolsController implements DroolsController {
         }
 
         String json =
-                EventProtocolCoder.manager.encode(sink.getTopic(), event, this);
+                getCoderManager().encode(sink.getTopic(), event, this);
 
         synchronized (this.recentSinkEvents) {
             this.recentSinkEvents.add(json);
@@ -593,7 +588,7 @@ public class MavenDroolsController implements DroolsController {
 
         boolean success = sink.send(json);
 
-        for (DroolsControllerFeatureApi feature : DroolsControllerFeatureApi.providers.getList()) {
+        for (DroolsControllerFeatureApi feature : getDroolsProviders().getList()) {
             try {
                 if (feature.afterDeliver(this, sink, event, json, success)) {
                     return true;
@@ -743,16 +738,14 @@ public class MavenDroolsController implements DroolsController {
 
     @Override
     public Map<String,Integer> factClassNames(String sessionName) {
-        if (sessionName == null || sessionName.isEmpty()) {
-            throw invalidSessNameEx(sessionName);
-        }
+        validateSessionName(sessionName);
 
         Map<String,Integer> classNames = new HashMap<>();
 
         PolicySession session = getSession(sessionName);
         KieSession kieSession = session.getKieSession();
 
-        Collection<FactHandle> facts = session.getKieSession().getFactHandles();
+        Collection<FactHandle> facts = kieSession.getFactHandles();
         for (FactHandle fact : facts) {
             try {
                 String className = kieSession.getObject(fact).getClass().getName();
@@ -769,11 +762,15 @@ public class MavenDroolsController implements DroolsController {
         return classNames;
     }
 
-    @Override
-    public long factCount(String sessionName) {
+    private void validateSessionName(String sessionName) {
         if (sessionName == null || sessionName.isEmpty()) {
             throw invalidSessNameEx(sessionName);
         }
+    }
+
+    @Override
+    public long factCount(String sessionName) {
+        validateSessionName(sessionName);
 
         PolicySession session = getSession(sessionName);
         return session.getKieSession().getFactCount();
@@ -781,9 +778,7 @@ public class MavenDroolsController implements DroolsController {
 
     @Override
     public List<Object> facts(String sessionName, String className, boolean delete) {
-        if (sessionName == null || sessionName.isEmpty()) {
-            throw invalidSessNameEx(sessionName);
-        }
+        validateSessionName(sessionName);
 
         if (className == null || className.isEmpty()) {
             throw new IllegalArgumentException("Invalid Class Name: " + className);
@@ -827,9 +822,7 @@ public class MavenDroolsController implements DroolsController {
     @Override
     public List<Object> factQuery(String sessionName, String queryName, String queriedEntity,
             boolean delete, Object... queryParams) {
-        if (sessionName == null || sessionName.isEmpty()) {
-            throw invalidSessNameEx(sessionName);
-        }
+        validateSessionName(sessionName);
 
         if (queryName == null || queryName.isEmpty()) {
             throw new IllegalArgumentException("Invalid Query Name: " + queryName);
@@ -965,7 +958,7 @@ public class MavenDroolsController implements DroolsController {
         StringBuilder builder = new StringBuilder();
         builder
             .append("MavenDroolsController [policyContainer=")
-            .append((policyContainer != null) ? policyContainer.getName() : "NULL")
+            .append(policyContainer.getName())
             .append(":")
             .append(", alive=")
             .append(alive)
@@ -975,4 +968,22 @@ public class MavenDroolsController implements DroolsController {
             .append("]");
         return builder.toString();
     }
+
+    // these may be overridden by junit tests
+
+    protected EventProtocolCoder getCoderManager() {
+        return EventProtocolCoder.manager;
+    }
+
+    protected OrderedServiceImpl<DroolsControllerFeatureApi> getDroolsProviders() {
+        return DroolsControllerFeatureApi.providers;
+    }
+
+    protected PolicyContainer makePolicyContainer(String groupId, String artifactId, String version) {
+        return new PolicyContainer(groupId, artifactId, version);
+    }
+
+    protected boolean isClass(String className) {
+        return ReflectionUtil.isClass(this.policyContainer.getClassLoader(), className);
+    }
 }
diff --git a/policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerTest2.java b/policy-management/src/test/java/org/onap/policy/drools/controller/internal/MavenDroolsControllerTest2.java
new file mode 100644 (file)
index 0000000..cd99eef
--- /dev/null
@@ -0,0 +1,1163 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2019 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.drools.controller.internal;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import org.junit.Before;
+import org.junit.Test;
+import org.kie.api.KieBase;
+import org.kie.api.definition.KiePackage;
+import org.kie.api.definition.rule.Query;
+import org.kie.api.runtime.KieContainer;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.FactHandle;
+import org.kie.api.runtime.rule.QueryResults;
+import org.kie.api.runtime.rule.QueryResultsRow;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.endpoints.event.comm.TopicSink;
+import org.onap.policy.common.utils.services.OrderedServiceImpl;
+import org.onap.policy.drools.core.PolicyContainer;
+import org.onap.policy.drools.core.PolicySession;
+import org.onap.policy.drools.features.DroolsControllerFeatureApi;
+import org.onap.policy.drools.protocol.coders.EventProtocolCoder;
+import org.onap.policy.drools.protocol.coders.EventProtocolParams;
+import org.onap.policy.drools.protocol.coders.JsonProtocolFilter;
+import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration;
+import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration.CustomGsonCoder;
+import org.onap.policy.drools.protocol.coders.TopicCoderFilterConfiguration.PotentialCoderFilter;
+
+public class MavenDroolsControllerTest2 {
+    private static final int FACT1_OBJECT = 1000;
+    private static final int FACT3_OBJECT = 1001;
+
+    private static final long FACT_COUNT = 200L;
+
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final RuntimeException RUNTIME_EX = new RuntimeException(EXPECTED_EXCEPTION);
+    private static final IllegalArgumentException ARG_EX = new IllegalArgumentException(EXPECTED_EXCEPTION);
+
+    private static final String UNKNOWN_CLASS = "unknown class";
+
+    private static final String GROUP = "my-group";
+    private static final String ARTIFACT = "my-artifact";
+    private static final String VERSION = "my-version";
+
+    private static final String GROUP2 = "my-groupB";
+    private static final String ARTIFACT2 = "my-artifactB";
+    private static final String VERSION2 = "my-versionB";
+
+    private static final String TOPIC = "my-topic";
+    private static final String TOPIC2 = "my-topic";
+
+    private static final ClassLoader CLASS_LOADER = MavenDroolsControllerTest2.class.getClassLoader();
+    private static final int CLASS_LOADER_HASHCODE = CLASS_LOADER.hashCode();
+
+    private static final String SESSION1 = "session-A";
+    private static final String SESSION2 = "session-B";
+    private static final String FULL_SESSION1 = "full-A";
+    private static final String FULL_SESSION2 = "full-B";
+
+    private static final String EVENT_TEXT = "my-event-text";
+    private static final Object EVENT = new Object();
+
+    private static final String QUERY = "my-query";
+    private static final String QUERY2 = "my-query-B";
+    private static final String ENTITY = "my-entity";
+    private static final Object PARM1 = "parmA";
+    private static final Object PARM2 = "parmB";
+
+    @Mock
+    private EventProtocolCoder coderMgr;
+    @Mock
+    private DroolsControllerFeatureApi prov1;
+    @Mock
+    private DroolsControllerFeatureApi prov2;
+    @Mock
+    private OrderedServiceImpl<DroolsControllerFeatureApi> droolsProviders;
+    @Mock
+    private TopicCoderFilterConfiguration decoder1;
+    @Mock
+    private TopicCoderFilterConfiguration decoder2;
+    @Mock
+    private TopicCoderFilterConfiguration encoder1;
+    @Mock
+    private TopicCoderFilterConfiguration encoder2;
+    @Mock
+    private PolicyContainer container;
+    @Mock
+    private CustomGsonCoder gson1;
+    @Mock
+    private CustomGsonCoder gson2;
+    @Mock
+    private PotentialCoderFilter filter1a;
+    @Mock
+    private JsonProtocolFilter jsonFilter1a;
+    @Mock
+    private PotentialCoderFilter filter1b;
+    @Mock
+    private JsonProtocolFilter jsonFilter1b;
+    @Mock
+    private PotentialCoderFilter filter2;
+    @Mock
+    private JsonProtocolFilter jsonFilter2;
+    @Mock
+    private PolicySession sess1;
+    @Mock
+    private PolicySession sess2;
+    @Mock
+    private KieSession kieSess;
+    @Mock
+    private KieSession kieSess2;
+    @Mock
+    private TopicSink sink;
+    @Mock
+    private FactHandle fact1;
+    @Mock
+    private FactHandle fact2;
+    @Mock
+    private FactHandle fact3;
+    @Mock
+    private FactHandle factex;
+    @Mock
+    private KieBase kieBase;
+    @Mock
+    private KiePackage pkg1;
+    @Mock
+    private KiePackage pkg2;
+    @Mock
+    private Query query1;
+    @Mock
+    private Query query2;
+    @Mock
+    private Query query3;
+    @Mock
+    private QueryResults queryResults;
+    @Mock
+    private QueryResultsRow row1;
+    @Mock
+    private QueryResultsRow row2;
+
+    private List<TopicCoderFilterConfiguration> decoders;
+    private List<TopicCoderFilterConfiguration> encoders;
+
+    private MavenDroolsController drools;
+
+    /**
+     * Initializes objects, including the drools controller.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(droolsProviders.getList()).thenReturn(Arrays.asList(prov1, prov2));
+
+        when(coderMgr.isDecodingSupported(GROUP, ARTIFACT, TOPIC)).thenReturn(true);
+        when(coderMgr.decode(GROUP, ARTIFACT, TOPIC, EVENT_TEXT)).thenReturn(EVENT);
+
+        when(kieSess.getFactCount()).thenReturn(FACT_COUNT);
+        when(kieSess.getFactHandles()).thenReturn(Arrays.asList(fact1, fact2, factex, fact3));
+        when(kieSess.getFactHandles(any())).thenReturn(Arrays.asList(fact1, fact3));
+        when(kieSess.getKieBase()).thenReturn(kieBase);
+        when(kieSess.getQueryResults(QUERY, PARM1, PARM2)).thenReturn(queryResults);
+
+        when(kieSess.getObject(fact1)).thenReturn(FACT1_OBJECT);
+        when(kieSess.getObject(fact2)).thenReturn("");
+        when(kieSess.getObject(fact3)).thenReturn(FACT3_OBJECT);
+        when(kieSess.getObject(factex)).thenThrow(RUNTIME_EX);
+
+        when(kieSess2.getFactHandles()).thenReturn(Collections.emptyList());
+
+        when(kieBase.getKiePackages()).thenReturn(Arrays.asList(pkg1, pkg2));
+
+        when(pkg1.getQueries()).thenReturn(Arrays.asList(query3));
+        when(pkg2.getQueries()).thenReturn(Arrays.asList(query2, query1));
+
+        when(query1.getName()).thenReturn(QUERY);
+        when(query2.getName()).thenReturn(QUERY2);
+
+        when(queryResults.iterator()).thenReturn(Arrays.asList(row1, row2).iterator());
+
+        when(row1.get(ENTITY)).thenReturn(FACT1_OBJECT);
+        when(row2.get(ENTITY)).thenReturn(FACT3_OBJECT);
+
+        when(row1.getFactHandle(ENTITY)).thenReturn(fact1);
+        when(row2.getFactHandle(ENTITY)).thenReturn(fact3);
+
+        when(sess1.getKieSession()).thenReturn(kieSess);
+        when(sess2.getKieSession()).thenReturn(kieSess2);
+
+        when(sess1.getName()).thenReturn(SESSION1);
+        when(sess2.getName()).thenReturn(SESSION2);
+
+        when(sess1.getFullName()).thenReturn(FULL_SESSION1);
+        when(sess2.getFullName()).thenReturn(FULL_SESSION2);
+
+        when(container.getClassLoader()).thenReturn(CLASS_LOADER);
+        when(container.getPolicySessions()).thenReturn(Arrays.asList(sess1, sess2));
+        when(container.insertAll(EVENT)).thenReturn(true);
+
+        when(decoder1.getTopic()).thenReturn(TOPIC);
+        when(decoder2.getTopic()).thenReturn(TOPIC2);
+
+        when(encoder1.getTopic()).thenReturn(TOPIC);
+        when(encoder2.getTopic()).thenReturn(TOPIC2);
+
+        decoders = Arrays.asList(decoder1, decoder2);
+        encoders = Arrays.asList(encoder1, encoder2);
+
+        when(decoder1.getCustomGsonCoder()).thenReturn(gson1);
+        when(encoder2.getCustomGsonCoder()).thenReturn(gson2);
+
+        when(filter1a.getCodedClass()).thenReturn(Object.class.getName());
+        when(filter1a.getFilter()).thenReturn(jsonFilter1a);
+
+        when(filter1b.getCodedClass()).thenReturn(String.class.getName());
+        when(filter1b.getFilter()).thenReturn(jsonFilter1b);
+
+        when(filter2.getCodedClass()).thenReturn(Integer.class.getName());
+        when(filter2.getFilter()).thenReturn(jsonFilter2);
+
+        when(decoder1.getCoderFilters()).thenReturn(Arrays.asList(filter1a, filter1b));
+        when(decoder2.getCoderFilters()).thenReturn(Collections.emptyList());
+
+        when(encoder1.getCoderFilters()).thenReturn(Collections.emptyList());
+        when(encoder2.getCoderFilters()).thenReturn(Arrays.asList(filter2));
+
+        when(sink.getTopic()).thenReturn(TOPIC);
+        when(sink.send(EVENT_TEXT)).thenReturn(true);
+
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, null, null);
+
+        when(coderMgr.encode(TOPIC, EVENT, drools)).thenReturn(EVENT_TEXT);
+    }
+
+    @Test
+    public void testMavenDroolsController_InvalidArgs() {
+        assertThatIllegalArgumentException().isThrownBy(() -> new MyDrools(null, ARTIFACT, VERSION, null, null))
+                        .withMessageContaining("group");
+        assertThatIllegalArgumentException().isThrownBy(() -> new MyDrools("", ARTIFACT, VERSION, null, null))
+                        .withMessageContaining("group");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> new MyDrools(GROUP, null, VERSION, null, null))
+                        .withMessageContaining("artifact");
+        assertThatIllegalArgumentException().isThrownBy(() -> new MyDrools(GROUP, "", VERSION, null, null))
+                        .withMessageContaining("artifact");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> new MyDrools(GROUP, ARTIFACT, null, null, null))
+                        .withMessageContaining("version");
+        assertThatIllegalArgumentException().isThrownBy(() -> new MyDrools(GROUP, ARTIFACT, "", null, null))
+                        .withMessageContaining("version");
+    }
+
+    @Test
+    public void testUpdateToVersion() {
+        // add coders
+        drools.updateToVersion(GROUP, ARTIFACT, VERSION2, decoders, encoders);
+
+        verify(container).updateToVersion(VERSION2);
+
+        // nothing removed the first time
+        verify(coderMgr, never()).removeDecoders(GROUP, ARTIFACT, TOPIC2);
+        verify(coderMgr, never()).removeEncoders(GROUP, ARTIFACT, TOPIC);
+
+        verify(coderMgr, times(2)).addDecoder(any());
+        verify(coderMgr, times(1)).addEncoder(any());
+
+        // remove coders
+        when(container.getVersion()).thenReturn(VERSION2);
+        drools.updateToVersion(GROUP, ARTIFACT, VERSION, null, null);
+
+        verify(container).updateToVersion(VERSION);
+
+        verify(coderMgr, times(2)).removeDecoders(GROUP, ARTIFACT, TOPIC2);
+        verify(coderMgr, times(2)).removeEncoders(GROUP, ARTIFACT, TOPIC);
+
+        // not added again
+        verify(coderMgr, times(2)).addDecoder(any());
+        verify(coderMgr, times(1)).addEncoder(any());
+    }
+
+    @Test
+    public void testUpdateToVersion_Unchanged() {
+        drools.updateToVersion(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        verify(coderMgr, never()).addDecoder(any());
+        verify(coderMgr, never()).addEncoder(any());
+    }
+
+    @Test
+    public void testUpdateToVersion_InvalidArgs() {
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.updateToVersion(null, ARTIFACT, VERSION, null, null))
+                        .withMessageContaining("group");
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.updateToVersion("", ARTIFACT, VERSION, null, null))
+                        .withMessageContaining("group");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.updateToVersion(GROUP, null, VERSION, null, null))
+                        .withMessageContaining("artifact");
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.updateToVersion(GROUP, "", VERSION, null, null))
+                        .withMessageContaining("artifact");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.updateToVersion(GROUP, ARTIFACT, null, null, null))
+                        .withMessageContaining("version");
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.updateToVersion(GROUP, ARTIFACT, "", null, null))
+                        .withMessageContaining("version");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.updateToVersion("no-group-id", ARTIFACT, VERSION, null, null))
+                        .withMessageContaining("BRAINLESS");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.updateToVersion(GROUP, "no-artifact-id", VERSION, null, null))
+                        .withMessageContaining("BRAINLESS");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.updateToVersion(GROUP, ARTIFACT, "no-version", null, null))
+                        .withMessageContaining("BRAINLESS");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.updateToVersion(GROUP2, ARTIFACT, VERSION, null, null))
+                        .withMessageContaining("coordinates must be identical");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.updateToVersion(GROUP, ARTIFACT2, VERSION, null, null))
+                        .withMessageContaining("coordinates must be identical");
+    }
+
+    @Test
+    public void testInitCoders_NullCoders() {
+        // already constructed with null coders
+        verify(coderMgr, never()).addDecoder(any());
+        verify(coderMgr, never()).addEncoder(any());
+    }
+
+    @Test
+    public void testInitCoders_NullOrEmptyFilters() {
+        when(decoder1.getCoderFilters()).thenReturn(Collections.emptyList());
+        when(decoder2.getCoderFilters()).thenReturn(null);
+
+        when(encoder1.getCoderFilters()).thenReturn(null);
+        when(encoder2.getCoderFilters()).thenReturn(Collections.emptyList());
+
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        verify(coderMgr, never()).addDecoder(any());
+        verify(coderMgr, never()).addEncoder(any());
+    }
+
+    @Test
+    public void testInitCoders_GsonClass() {
+        when(gson1.getClassContainer()).thenReturn("");
+        when(gson2.getClassContainer()).thenReturn(Long.class.getName());
+
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        // all should be added
+        verify(coderMgr, times(2)).addDecoder(any());
+        verify(coderMgr, times(1)).addEncoder(any());
+    }
+
+    @Test
+    public void testInitCoders_InvalidGsonClass() {
+        when(gson1.getClassContainer()).thenReturn(UNKNOWN_CLASS);
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders))
+                        .withMessageContaining("cannot be retrieved");
+    }
+
+    @Test
+    public void testInitCoders_InvalidFilterClass() {
+        when(filter2.getCodedClass()).thenReturn(UNKNOWN_CLASS);
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders))
+                        .withMessageContaining("cannot be retrieved");
+    }
+
+    @Test
+    public void testInitCoders_Filters() {
+
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        ArgumentCaptor<EventProtocolParams> dec = ArgumentCaptor.forClass(EventProtocolParams.class);
+        verify(coderMgr, times(2)).addDecoder(dec.capture());
+
+        ArgumentCaptor<EventProtocolParams> enc = ArgumentCaptor.forClass(EventProtocolParams.class);
+        verify(coderMgr, times(1)).addEncoder(enc.capture());
+
+        // validate parameters
+        EventProtocolParams params = dec.getAllValues().get(0);
+        assertEquals(ARTIFACT, params.getArtifactId());
+        assertEquals(gson1, params.getCustomCoder());
+        assertEquals(Object.class.getName(), params.getEventClass());
+        assertEquals(GROUP, params.getGroupId());
+        assertEquals(CLASS_LOADER_HASHCODE, params.getModelClassLoaderHash());
+        assertEquals(jsonFilter1a, params.getProtocolFilter());
+        assertEquals(TOPIC, params.getTopic());
+
+        params = dec.getAllValues().get(1);
+        assertEquals(ARTIFACT, params.getArtifactId());
+        assertEquals(gson1, params.getCustomCoder());
+        assertEquals(String.class.getName(), params.getEventClass());
+        assertEquals(GROUP, params.getGroupId());
+        assertEquals(CLASS_LOADER_HASHCODE, params.getModelClassLoaderHash());
+        assertEquals(jsonFilter1b, params.getProtocolFilter());
+        assertEquals(TOPIC, params.getTopic());
+
+        params = enc.getAllValues().get(0);
+        assertEquals(ARTIFACT, params.getArtifactId());
+        assertEquals(gson2, params.getCustomCoder());
+        assertEquals(Integer.class.getName(), params.getEventClass());
+        assertEquals(GROUP, params.getGroupId());
+        assertEquals(CLASS_LOADER_HASHCODE, params.getModelClassLoaderHash());
+        assertEquals(jsonFilter2, params.getProtocolFilter());
+        assertEquals(TOPIC, params.getTopic());
+    }
+
+    @Test
+    public void testOwnsCoder() {
+        int hc = CLASS_LOADER_HASHCODE;
+
+        // wrong hash code
+        assertFalse(drools.ownsCoder(String.class, hc + 1));
+
+        // correct hash code
+        assertTrue(drools.ownsCoder(String.class, hc));
+
+        // unknown class
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, null, null) {
+            @Override
+            protected boolean isClass(String className) {
+                return false;
+            }
+        };
+        assertFalse(drools.ownsCoder(String.class, hc));
+    }
+
+    @Test
+    public void testStart_testStop_testIsAlive() {
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        when(container.start()).thenReturn(true);
+        when(container.stop()).thenReturn(true);
+
+        assertFalse(drools.isAlive());
+
+        // start it
+        assertTrue(drools.start());
+        verify(container).start();
+        assertTrue(drools.isAlive());
+
+        // repeat - no changes
+        assertTrue(drools.start());
+        verify(container).start();
+        assertTrue(drools.isAlive());
+
+        // stop it
+        assertTrue(drools.stop());
+        verify(container).stop();
+        assertFalse(drools.isAlive());
+
+        // repeat - no changes
+        assertTrue(drools.stop());
+        verify(container).stop();
+        assertFalse(drools.isAlive());
+
+        // now check with container returning false - should still be invoked
+        when(container.start()).thenReturn(false);
+        when(container.stop()).thenReturn(false);
+        assertFalse(drools.start());
+        assertTrue(drools.isAlive());
+        assertFalse(drools.stop());
+        assertFalse(drools.isAlive());
+        verify(container, times(2)).start();
+        verify(container, times(2)).stop();
+
+        // coders should still be intact
+        verify(coderMgr, never()).removeDecoders(any(), any(), any());
+        verify(coderMgr, never()).removeEncoders(any(), any(), any());
+
+        verify(container, never()).shutdown();
+        verify(container, never()).destroy();
+    }
+
+    @Test
+    public void testShutdown() {
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        // start it
+        drools.start();
+
+        // shut down
+        drools.shutdown();
+
+        verify(container).stop();
+        assertFalse(drools.isAlive());
+
+        // coders should have been removed
+        verify(coderMgr, times(2)).removeDecoders(any(), any(), any());
+        verify(coderMgr, times(2)).removeEncoders(any(), any(), any());
+
+        verify(container).shutdown();
+        verify(container, never()).destroy();
+    }
+
+    @Test
+    public void testShutdown_Ex() {
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        // start it
+        drools.start();
+
+        when(container.stop()).thenThrow(RUNTIME_EX);
+
+        // shut down
+        drools.shutdown();
+
+        assertFalse(drools.isAlive());
+
+        verify(container).shutdown();
+        verify(container, never()).destroy();
+    }
+
+    @Test
+    public void testHalt() {
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        // start it
+        drools.start();
+
+        // halt
+        drools.halt();
+
+        verify(container).stop();
+        assertFalse(drools.isAlive());
+
+        // coders should have been removed
+        verify(coderMgr, times(2)).removeDecoders(any(), any(), any());
+        verify(coderMgr, times(2)).removeEncoders(any(), any(), any());
+
+        verify(container).destroy();
+    }
+
+    @Test
+    public void testHalt_Ex() {
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders);
+
+        // start it
+        drools.start();
+
+        when(container.stop()).thenThrow(RUNTIME_EX);
+
+        // halt
+        drools.halt();
+
+        assertFalse(drools.isAlive());
+
+        verify(container).destroy();
+    }
+
+    @Test
+    public void testRemoveCoders_Ex() {
+        drools = new MyDrools(GROUP, ARTIFACT, VERSION, decoders, encoders) {
+            @Override
+            protected void removeDecoders() {
+                throw ARG_EX;
+            }
+
+            @Override
+            protected void removeEncoders() {
+                throw ARG_EX;
+            }
+        };
+
+        drools.updateToVersion(GROUP, ARTIFACT, VERSION2, null, null);
+    }
+
+    @Test
+    public void testOfferStringString() {
+        drools.start();
+        assertTrue(drools.offer(TOPIC, EVENT_TEXT));
+
+        verify(container).insertAll(EVENT);
+    }
+
+    @Test
+    public void testOfferStringString_NoDecode() {
+        when(coderMgr.isDecodingSupported(GROUP, ARTIFACT, TOPIC)).thenReturn(false);
+
+        drools.start();
+        assertTrue(drools.offer(TOPIC, EVENT_TEXT));
+
+        verify(container, never()).insertAll(EVENT);
+    }
+
+    @Test
+    public void testOfferStringString_DecodeUnsupported() {
+        when(coderMgr.decode(GROUP, ARTIFACT, TOPIC, EVENT_TEXT))
+                        .thenThrow(new UnsupportedOperationException(EXPECTED_EXCEPTION));
+
+        drools.start();
+        assertTrue(drools.offer(TOPIC, EVENT_TEXT));
+
+        verify(container, never()).insertAll(EVENT);
+    }
+
+    @Test
+    public void testOfferStringString_DecodeEx() {
+        when(coderMgr.decode(GROUP, ARTIFACT, TOPIC, EVENT_TEXT)).thenThrow(RUNTIME_EX);
+
+        drools.start();
+        assertTrue(drools.offer(TOPIC, EVENT_TEXT));
+
+        verify(container, never()).insertAll(EVENT);
+    }
+
+    @Test
+    public void testOfferStringString_Ignored() {
+        drools.start();
+
+        drools.lock();
+        assertTrue(drools.offer(TOPIC, EVENT_TEXT));
+        assertEquals(0, drools.getRecentSourceEvents().length);
+        drools.unlock();
+
+        drools.stop();
+        assertTrue(drools.offer(TOPIC, EVENT_TEXT));
+        assertEquals(0, drools.getRecentSourceEvents().length);
+        drools.start();
+
+        // no sessions
+        when(container.getPolicySessions()).thenReturn(Collections.emptyList());
+        assertTrue(drools.offer(TOPIC, EVENT_TEXT));
+        assertEquals(0, drools.getRecentSourceEvents().length);
+    }
+
+    @Test
+    public void testOfferT() {
+        drools.start();
+        assertTrue(drools.offer(EVENT));
+        assertEquals(1, drools.getRecentSourceEvents().length);
+        assertEquals(EVENT, drools.getRecentSourceEvents()[0]);
+        verify(container).insertAll(EVENT);
+
+        verify(prov1).beforeInsert(drools, EVENT);
+        verify(prov2).beforeInsert(drools, EVENT);
+
+        verify(prov1).afterInsert(drools, EVENT, true);
+        verify(prov2).afterInsert(drools, EVENT, true);
+    }
+
+    @Test
+    public void testOfferT_Ex() {
+        when(prov1.beforeInsert(drools, EVENT)).thenThrow(RUNTIME_EX);
+        when(prov1.afterInsert(drools, EVENT, true)).thenThrow(RUNTIME_EX);
+
+        drools.start();
+        assertTrue(drools.offer(EVENT));
+        assertEquals(1, drools.getRecentSourceEvents().length);
+        assertEquals(EVENT, drools.getRecentSourceEvents()[0]);
+        verify(container).insertAll(EVENT);
+
+        // should still invoke prov2
+        verify(prov2).beforeInsert(drools, EVENT);
+
+        verify(prov2).afterInsert(drools, EVENT, true);
+    }
+
+    @Test
+    public void testOfferT_NotInserted() {
+        when(container.insertAll(EVENT)).thenReturn(false);
+
+        drools.start();
+        assertTrue(drools.offer(EVENT));
+        assertEquals(1, drools.getRecentSourceEvents().length);
+        assertEquals(EVENT, drools.getRecentSourceEvents()[0]);
+        verify(container).insertAll(EVENT);
+
+        verify(prov1).beforeInsert(drools, EVENT);
+        verify(prov2).beforeInsert(drools, EVENT);
+
+        verify(prov1).afterInsert(drools, EVENT, false);
+        verify(prov2).afterInsert(drools, EVENT, false);
+    }
+
+    @Test
+    public void testOfferT_BeforeInsertIntercept() {
+        drools.start();
+        when(prov1.beforeInsert(drools, EVENT)).thenReturn(true);
+
+        assertTrue(drools.offer(EVENT));
+        assertEquals(1, drools.getRecentSourceEvents().length);
+        assertEquals(EVENT, drools.getRecentSourceEvents()[0]);
+        verify(container, never()).insertAll(EVENT);
+
+        verify(prov1).beforeInsert(drools, EVENT);
+
+        // nothing else invoked
+        verify(prov2, never()).beforeInsert(drools, EVENT);
+        verify(prov1, never()).afterInsert(drools, EVENT, true);
+        verify(prov2, never()).afterInsert(drools, EVENT, true);
+    }
+
+    @Test
+    public void testOfferT_AfterInsertIntercept() {
+        drools.start();
+
+        when(prov1.afterInsert(drools, EVENT, true)).thenReturn(true);
+
+        assertTrue(drools.offer(EVENT));
+        assertEquals(1, drools.getRecentSourceEvents().length);
+        assertEquals(EVENT, drools.getRecentSourceEvents()[0]);
+        verify(container).insertAll(EVENT);
+
+        verify(prov1).beforeInsert(drools, EVENT);
+        verify(prov2).beforeInsert(drools, EVENT);
+
+        verify(prov1).afterInsert(drools, EVENT, true);
+
+        // prov2 is never called
+        verify(prov2, never()).afterInsert(drools, EVENT, true);
+    }
+
+    @Test
+    public void testOfferT_Ignored() {
+        drools.start();
+
+        drools.lock();
+        assertTrue(drools.offer(EVENT));
+        assertEquals(0, drools.getRecentSourceEvents().length);
+        drools.unlock();
+
+        drools.stop();
+        assertTrue(drools.offer(EVENT));
+        assertEquals(0, drools.getRecentSourceEvents().length);
+        drools.start();
+
+        // no sessions
+        when(container.getPolicySessions()).thenReturn(Collections.emptyList());
+        assertTrue(drools.offer(EVENT));
+        assertEquals(0, drools.getRecentSourceEvents().length);
+    }
+
+    @Test
+    public void testDeliver() {
+        drools.start();
+        assertTrue(drools.deliver(sink, EVENT));
+        assertEquals(1, drools.getRecentSinkEvents().length);
+        assertEquals(EVENT_TEXT, drools.getRecentSinkEvents()[0]);
+
+        verify(sink).send(EVENT_TEXT);
+
+        verify(prov1).beforeDeliver(drools, sink, EVENT);
+        verify(prov2).beforeDeliver(drools, sink, EVENT);
+
+        verify(prov1).afterDeliver(drools, sink, EVENT, EVENT_TEXT, true);
+        verify(prov2).afterDeliver(drools, sink, EVENT, EVENT_TEXT, true);
+    }
+
+    @Test
+    public void testDeliver_InvalidArgs() {
+        drools.start();
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.deliver(null, EVENT))
+                        .withMessageContaining("sink");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.deliver(sink, null))
+                        .withMessageContaining("event");
+
+        drools.lock();
+        assertThatIllegalStateException().isThrownBy(() -> drools.deliver(sink, EVENT)).withMessageContaining("locked");
+        drools.unlock();
+
+        drools.stop();
+        assertThatIllegalStateException().isThrownBy(() -> drools.deliver(sink, EVENT))
+                        .withMessageContaining("stopped");
+        drools.start();
+
+        assertEquals(0, drools.getRecentSinkEvents().length);
+    }
+
+    @Test
+    public void testDeliver_BeforeIntercept() {
+        when(prov1.beforeDeliver(drools, sink, EVENT)).thenReturn(true);
+
+        drools.start();
+        assertTrue(drools.deliver(sink, EVENT));
+        assertEquals(0, drools.getRecentSinkEvents().length);
+
+        verify(prov1).beforeDeliver(drools, sink, EVENT);
+
+        // nothing else should have been invoked
+        verify(sink, never()).send(EVENT_TEXT);
+        verify(prov2, never()).beforeDeliver(drools, sink, EVENT);
+        verify(prov1, never()).afterDeliver(drools, sink, EVENT, EVENT_TEXT, true);
+        verify(prov2, never()).afterDeliver(drools, sink, EVENT, EVENT_TEXT, true);
+    }
+
+    @Test
+    public void testDeliver_AfterIntercept() {
+        when(prov1.afterDeliver(drools, sink, EVENT, EVENT_TEXT, true)).thenReturn(true);
+
+        drools.start();
+        assertTrue(drools.deliver(sink, EVENT));
+        assertEquals(1, drools.getRecentSinkEvents().length);
+        assertEquals(EVENT_TEXT, drools.getRecentSinkEvents()[0]);
+
+        verify(prov1).beforeDeliver(drools, sink, EVENT);
+        verify(prov2).beforeDeliver(drools, sink, EVENT);
+
+        verify(sink).send(EVENT_TEXT);
+
+        verify(prov1).afterDeliver(drools, sink, EVENT, EVENT_TEXT, true);
+
+        // nothing else should have been invoked
+        verify(prov2, never()).afterDeliver(drools, sink, EVENT, EVENT_TEXT, true);
+    }
+
+    @Test
+    public void testDeliver_InterceptEx() {
+        when(prov1.beforeDeliver(drools, sink, EVENT)).thenThrow(RUNTIME_EX);
+        when(prov1.afterDeliver(drools, sink, EVENT, EVENT_TEXT, true)).thenThrow(RUNTIME_EX);
+
+        drools.start();
+        assertTrue(drools.deliver(sink, EVENT));
+
+        verify(sink).send(EVENT_TEXT);
+
+        // should still invoke prov2
+        verify(prov2).beforeDeliver(drools, sink, EVENT);
+        verify(prov2).afterDeliver(drools, sink, EVENT, EVENT_TEXT, true);
+    }
+
+    @Test
+    public void testGetXxx() {
+        assertEquals(VERSION, drools.getVersion());
+        assertEquals(ARTIFACT, drools.getArtifactId());
+        assertEquals(GROUP, drools.getGroupId());
+        assertEquals(CLASS_LOADER_HASHCODE, drools.getModelClassLoaderHash());
+        assertSame(container, drools.getContainer());
+        assertEquals(Arrays.asList(sess1, sess2), drools.getSessions());
+
+        // test junit methods - need a controller with fewer overrides
+        drools = new MavenDroolsController(GROUP, ARTIFACT, VERSION, null, null) {
+            @Override
+            protected PolicyContainer makePolicyContainer(String groupId, String artifactId, String version) {
+                return container;
+            }
+        };
+
+        assertSame(EventProtocolCoder.manager, drools.getCoderManager());
+        assertSame(DroolsControllerFeatureApi.providers, drools.getDroolsProviders());
+    }
+
+    @Test
+    public void testLock_testUnlock_testIsLocked() {
+        assertFalse(drools.isLocked());
+
+        assertTrue(drools.lock());
+        assertTrue(drools.isLocked());
+
+        assertTrue(drools.unlock());
+        assertFalse(drools.isLocked());
+
+        // repeat
+        assertTrue(drools.lock());
+        assertTrue(drools.isLocked());
+
+        assertTrue(drools.unlock());
+        assertFalse(drools.isLocked());
+    }
+
+    @Test
+    public void testGetSessionNames_testGetCanonicalSessionNames() {
+        assertEquals("[session-A, session-B]", drools.getSessionNames(true).toString());
+        assertEquals("[full-A, full-B]", drools.getSessionNames(false).toString());
+
+        assertEquals("[session-A, session-B]", drools.getSessionNames().toString());
+
+        assertEquals("[full-A, full-B]", drools.getCanonicalSessionNames().toString());
+
+        // exception case
+        when(container.getPolicySessions()).thenThrow(RUNTIME_EX);
+        assertEquals("[expected exception]", drools.getSessionNames().toString());
+    }
+
+    @Test
+    public void testGetBaseDomainNames() {
+        KieContainer kiecont = mock(KieContainer.class);
+        when(kiecont.getKieBaseNames()).thenReturn(Arrays.asList("kieA", "kieB"));
+        when(container.getKieContainer()).thenReturn(kiecont);
+
+        assertEquals("[kieA, kieB]", drools.getBaseDomainNames().toString());
+    }
+
+    @Test
+    public void testGetSession() {
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.getSession(null))
+                        .withMessageContaining("must be provided");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.getSession(""))
+                        .withMessageContaining("must be provided");
+
+        assertSame(sess1, drools.getSession(SESSION1));
+        assertSame(sess1, drools.getSession(FULL_SESSION1));
+
+        assertSame(sess2, drools.getSession(SESSION2));
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.getSession("unknown session"))
+                        .withMessageContaining("Invalid Session Name");
+    }
+
+    @Test
+    public void testFactClassNames() {
+        // copy to a sorted map so the order remains unchanged
+        Map<String, Integer> map = new TreeMap<>(drools.factClassNames(SESSION1));
+        assertEquals("{java.lang.Integer=2, java.lang.String=1}", map.toString());
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.factClassNames(null))
+                        .withMessageContaining("Invalid Session Name");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.factClassNames(""))
+                        .withMessageContaining("Invalid Session Name");
+    }
+
+    @Test
+    public void testFactCount() {
+        assertEquals(FACT_COUNT, drools.factCount(SESSION1));
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.factCount(null))
+                        .withMessageContaining("Invalid Session Name");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.factCount(""))
+                        .withMessageContaining("Invalid Session Name");
+    }
+
+    @Test
+    public void testFactsStringStringBoolean() {
+        assertEquals("[1000, 1001]", drools.facts(SESSION1, Integer.class.getName(), false).toString());
+        verify(kieSess, never()).delete(fact1);
+        verify(kieSess, never()).delete(fact2);
+        verify(kieSess, never()).delete(fact3);
+        verify(kieSess, never()).delete(factex);
+
+        // now delete - but should only delete 1 & 3
+        assertEquals("[1000, 1001]", drools.facts(SESSION1, Integer.class.getName(), true).toString());
+        verify(kieSess).delete(fact1);
+        verify(kieSess, never()).delete(fact2);
+        verify(kieSess).delete(fact3);
+        verify(kieSess, never()).delete(factex);
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.facts(null, Integer.class.getName(), false))
+                        .withMessageContaining("Invalid Session Name");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.facts("", Integer.class.getName(), false))
+                        .withMessageContaining("Invalid Session Name");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.facts(SESSION1, null, false))
+                        .withMessageContaining("Invalid Class Name");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.facts(SESSION1, "", false))
+                        .withMessageContaining("Invalid Class Name");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.facts(SESSION1, UNKNOWN_CLASS, false))
+                        .withMessageContaining("classloader");
+    }
+
+    @Test
+    public void testFactsStringStringBoolean_DeleteEx() {
+        doThrow(RUNTIME_EX).when(kieSess).delete(fact1);
+
+        assertEquals("[1000, 1001]", drools.facts(SESSION1, Integer.class.getName(), true).toString());
+
+        // should still have deleted #3
+        verify(kieSess).delete(fact3);
+    }
+
+    @Test
+    public void testFactsStringClassOfT() {
+        assertEquals("[1000, 1001]", drools.facts(SESSION1, Integer.class).toString());
+    }
+
+    @Test
+    public void testFactQuery() {
+        assertEquals("[1000, 1001]", drools.factQuery(SESSION1, QUERY, ENTITY, false, PARM1, PARM2).toString());
+
+        verify(kieSess, never()).delete(fact1);
+        verify(kieSess, never()).delete(fact3);
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.factQuery(null, QUERY, ENTITY, false, PARM1, PARM2))
+                        .withMessageContaining("Invalid Session Name");
+
+        assertThatIllegalArgumentException().isThrownBy(() -> drools.factQuery("", QUERY, ENTITY, false, PARM1, PARM2))
+                        .withMessageContaining("Invalid Session Name");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.factQuery(SESSION1, null, ENTITY, false, PARM1, PARM2))
+                        .withMessageContaining("Invalid Query Name");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.factQuery(SESSION1, "", ENTITY, false, PARM1, PARM2))
+                        .withMessageContaining("Invalid Query Name");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.factQuery(SESSION1, QUERY, null, false, PARM1, PARM2))
+                        .withMessageContaining("Invalid Queried Entity");
+
+        assertThatIllegalArgumentException()
+                        .isThrownBy(() -> drools.factQuery(SESSION1, QUERY, "", false, PARM1, PARM2))
+                        .withMessageContaining("Invalid Queried Entity");
+
+        assertThatIllegalArgumentException().isThrownBy(
+            () -> drools.factQuery(SESSION1, QUERY + "-unknown-query", ENTITY, false, PARM1, PARM2))
+            .withMessageContaining("Invalid Query Name");
+    }
+
+    @Test
+    public void testFactQuery_Delete() {
+        doThrow(RUNTIME_EX).when(kieSess).delete(fact1);
+
+        assertEquals("[1000, 1001]", drools.factQuery(SESSION1, QUERY, ENTITY, true, PARM1, PARM2).toString());
+
+        // should still delete fact #3
+        verify(kieSess).delete(fact3);
+    }
+
+    @Test
+    public void testDeleteStringT() {
+        assertTrue(drools.delete(SESSION1, FACT3_OBJECT));
+
+        verify(kieSess, never()).delete(fact1);
+        verify(kieSess).delete(fact3);
+
+        // not found
+        assertFalse(drools.delete(SESSION1, "hello"));
+
+        // repeat, but generate exception while getting the first object
+        when(kieSess.getObject(fact1)).thenThrow(RUNTIME_EX);
+        assertTrue(drools.delete(SESSION1, FACT3_OBJECT));
+
+        verify(kieSess, never()).delete(fact1);
+
+        // should still delete fact #3
+        verify(kieSess, times(2)).delete(fact3);
+    }
+
+    @Test
+    public void testDeleteT() {
+        assertTrue(drools.delete(FACT3_OBJECT));
+
+        verify(kieSess).delete(fact3);
+    }
+
+    @Test
+    public void testDeleteStringClassOfT() {
+        assertTrue(drools.delete(SESSION1, Integer.class));
+
+        verify(kieSess).delete(fact1);
+        verify(kieSess).delete(fact3);
+    }
+
+    @Test
+    public void testDeleteStringClassOfT_Ex() {
+        doThrow(RUNTIME_EX).when(kieSess).delete(fact1);
+
+        assertFalse(drools.delete(SESSION1, Integer.class));
+
+        // should still delete fact #3
+        verify(kieSess).delete(fact3);
+    }
+
+    @Test
+    public void testDeleteClassOfT() {
+        assertTrue(drools.delete(Integer.class));
+
+        verify(kieSess).delete(fact1);
+        verify(kieSess).delete(fact3);
+    }
+
+    @Test
+    public void testFetchModelClass() {
+        assertSame(Long.class, drools.fetchModelClass(Long.class.getName()));
+    }
+
+    @Test
+    public void testIsBrained() {
+        assertTrue(drools.isBrained());
+    }
+
+    @Test
+    public void testToString() {
+        assertNotNull(drools.toString());
+    }
+
+    private class MyDrools extends MavenDroolsController {
+
+        public MyDrools(String groupId, String artifactId, String version,
+                        List<TopicCoderFilterConfiguration> decoderConfigurations,
+                        List<TopicCoderFilterConfiguration> encoderConfigurations) {
+
+            super(groupId, artifactId, version, decoderConfigurations, encoderConfigurations);
+        }
+
+        @Override
+        protected EventProtocolCoder getCoderManager() {
+            return coderMgr;
+        }
+
+        @Override
+        protected OrderedServiceImpl<DroolsControllerFeatureApi> getDroolsProviders() {
+            return droolsProviders;
+        }
+
+        @Override
+        protected PolicyContainer makePolicyContainer(String groupId, String artifactId, String version) {
+            when(container.getGroupId()).thenReturn(groupId);
+            when(container.getArtifactId()).thenReturn(artifactId);
+            when(container.getVersion()).thenReturn(version);
+
+            return container;
+        }
+    }
+}