changeset 2693:d2535ae16b77

Add tests for commands agent plugin. Reviewed-by: ebaron Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-June/023684.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Wed, 14 Jun 2017 12:33:08 +0200
parents a5425acc4260
children 80b17b58e7a7
files plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/WebSocketClientFacade.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/WebSocketClientFacadeImpl.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/AgentSocketOnMessageCallback.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/CmdChannelAgentSocket.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/GsonFacade.java plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/GsonFacadeImpl.java plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/receiver/PingReceiverTest.java plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/socket/AgentSocketOnMessageCallbackTest.java plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/socket/CmdChannelAgentSocketTest.java plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/receiver/ReceiverRegistryTest.java
diffstat 12 files changed, 992 insertions(+), 23 deletions(-) [+]
line wrap: on
line diff
--- a/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java	Mon Jun 12 19:04:06 2017 +0200
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/CommandsBackend.java	Wed Jun 14 12:33:08 2017 +0200
@@ -51,7 +51,6 @@
 import org.apache.felix.scr.annotations.Service;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
-import org.eclipse.jetty.websocket.client.WebSocketClient;
 import org.osgi.framework.BundleContext;
 
 import com.redhat.thermostat.backend.Backend;
@@ -81,8 +80,11 @@
     private static final String ENDPOINT_FORMAT = "%s/systems/%s/agents/%s";
     private static final String UNKNOWN_CREDS = "UNKNOWN:UNKNOWN";
 
-    private CmdChannelAgentSocket socket;
-    private WebSocketClient wsClient;
+    private final WsClientCreator wsClientCreator;
+    private final CredentialsCreator credsCreator;
+    private final ConfigCreator configCreator;
+    private final CountDownLatch socketConnectLatch;
+    private WebSocketClientFacade wsClient;
     private boolean isActive;
     private StorageCredentials creds;
     private PluginConfiguration config;
@@ -98,7 +100,19 @@
     private ConfigurationInfoSource commandInfo;
 
     public CommandsBackend() {
+        this(new WsClientCreator(), new CredentialsCreator(), new ConfigCreator(), new CountDownLatch(1));
+    }
+    
+    // For testing purposes
+    CommandsBackend(WsClientCreator creator,
+                    CredentialsCreator credsCreator,
+                    ConfigCreator configCreator,
+                    CountDownLatch socketConnectLatch) {
         super(NAME, DESCRIPTION, VENDOR);
+        this.wsClientCreator = creator;
+        this.credsCreator = credsCreator;
+        this.configCreator = configCreator;
+        this.socketConnectLatch = socketConnectLatch;
     }
 
     @Override
@@ -116,11 +130,8 @@
             // nothing to do
             return true;
         }
-        if (socket != null) {
-            socket.closeSession();
-        }
         if (wsClient != null) {
-            wsClient.destroy();
+            wsClient.stop();
         }
         isActive = false;
         return true;
@@ -128,7 +139,7 @@
 
     @Override
     public boolean isActive() {
-        return true;
+        return isActive;
     }
 
     @Override
@@ -140,10 +151,10 @@
     protected void componentActivated(BundleContext ctx) {
         Version version = new Version(ctx.getBundle());
         super.setVersion(version.getVersionInfo());
-        creds = new FileStorageCredentials(paths.getUserAgentAuthConfigFile());
-        config = new PluginConfiguration(commandInfo, PLUGIN_ID);
+        creds = credsCreator.create(paths);
+        config = configCreator.createConfig(commandInfo);
         try {
-            wsClient = new WebSocketClient();
+            wsClient = wsClientCreator.createClient();
             wsClient.start();
         } catch (Exception e) {
             logger.log(Level.WARNING,
@@ -178,15 +189,14 @@
             String agent = "testAgent";
             URI agentUri = new URI(String.format(ENDPOINT_FORMAT, microserviceURL, "ignoreMe", agent));
             AgentSocketOnMessageCallback onMsgCallback = new AgentSocketOnMessageCallback(receiverReg);
-            CountDownLatch connected = new CountDownLatch(1);
             CmdChannelAgentSocket agentSocket = new CmdChannelAgentSocket(
-                    onMsgCallback, connected, agent);
+                    onMsgCallback, socketConnectLatch, agent);
             ClientUpgradeRequest agentRequest = new ClientUpgradeRequest();
             agentRequest.setHeader(HttpHeader.AUTHORIZATION.asString(),
                     getBasicAuthHeaderValue());
             wsClient.connect(agentSocket, agentUri, agentRequest);
             logger.fine("WebSocket connect initiated.");
-            expired = !connected.await(10, TimeUnit.SECONDS);
+            expired = !socketConnectLatch.await(10, TimeUnit.SECONDS);
         } catch (IOException | URISyntaxException | InterruptedException e) {
             logger.warning("Failed to connect to endpoint. Reason: " + e.getMessage());
             logger.log(Level.FINE, "Failed to connect to endpoint", e);
@@ -201,7 +211,7 @@
         }
     }
 
-    private String getBasicAuthHeaderValue() {
+    String getBasicAuthHeaderValue() {
         String username = creds.getUsername();
         char[] pwdChar = creds.getPassword();
         String userpassword;
@@ -218,4 +228,26 @@
                 .encode(userpassword.getBytes());
         return "Basic " + encodedAuthorization;
     }
+    
+    protected void bindPaths(CommonPaths paths) {
+        this.paths = paths;
+    }
+    
+    static class WsClientCreator {
+        WebSocketClientFacade createClient() {
+            return new WebSocketClientFacadeImpl();
+        }
+    }
+    
+    static class CredentialsCreator {
+        StorageCredentials create(CommonPaths paths) {
+            return new FileStorageCredentials(paths.getUserAgentAuthConfigFile());
+        }
+    }
+    
+    static class ConfigCreator {
+        PluginConfiguration createConfig(ConfigurationInfoSource source) {
+            return new PluginConfiguration(source, PLUGIN_ID);
+        }
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/WebSocketClientFacade.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+
+import com.redhat.thermostat.commands.agent.internal.socket.CmdChannelAgentSocket;
+
+/**
+ * Facade for a websocket client object. Allows for better testing
+ * of the CommandsBackend code.
+ *
+ */
+interface WebSocketClientFacade {
+    
+    void start();
+    
+    void connect(CmdChannelAgentSocket socket, URI connectURI, ClientUpgradeRequest request) throws IOException;
+    
+    void stop();
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/WebSocketClientFacadeImpl.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+
+import com.redhat.thermostat.commands.agent.internal.socket.CmdChannelAgentSocket;
+import com.redhat.thermostat.common.utils.LoggingUtils;
+
+class WebSocketClientFacadeImpl implements WebSocketClientFacade {
+    
+    private static final Logger logger = LoggingUtils.getLogger(WebSocketClientFacadeImpl.class);
+    private final WebSocketClient client;
+    private CmdChannelAgentSocket socket;
+    
+    WebSocketClientFacadeImpl() {
+        this.client = new WebSocketClient();
+    }
+
+    @Override
+    public void start() {
+        try {
+            client.start();
+        } catch (Exception e) {
+            logger.log(Level.WARNING,
+                    "Failed to start websocket client. Reason: "
+                            + e.getMessage());
+        }
+    }
+
+    @Override
+    public void connect(CmdChannelAgentSocket socket, URI connectURI, ClientUpgradeRequest request) throws IOException {
+        this.socket = socket;
+        client.connect(socket, connectURI, request);
+    }
+
+    @Override
+    public void stop() {
+        if (socket != null) {
+            socket.closeSession();
+        }
+        client.destroy();
+    }
+
+}
--- a/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/AgentSocketOnMessageCallback.java	Mon Jun 12 19:04:06 2017 +0200
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/AgentSocketOnMessageCallback.java	Wed Jun 14 12:33:08 2017 +0200
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.commands.agent.internal.socket;
 
 import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -82,20 +83,26 @@
         }
     }
 
-    private static class CmdChannelRequestHandler extends Thread {
+    static class CmdChannelRequestHandler extends Thread {
 
         private static final Logger logger = LoggingUtils.getLogger(CmdChannelRequestHandler.class);
         private final Session session;
         private final AgentRequest request;
         private final ReceiverRegistry receivers;
         private final Gson gson;
+        private final CountDownLatch sentLatch;
 
-        private CmdChannelRequestHandler(Session session, AgentRequest request, ReceiverRegistry receivers, Gson gson) {
+        CmdChannelRequestHandler(Session session, AgentRequest request, ReceiverRegistry receivers, Gson gson) {
+            this(session, request, receivers, gson, null);
+        }
+        
+        CmdChannelRequestHandler(Session session, AgentRequest request, ReceiverRegistry receivers, Gson gson, CountDownLatch sentLatch) {
             super("Thermostat-WS-CMD-CH-Handler");
             this.session = session;
             this.request = request;
             this.receivers = receivers;
             this.gson = gson;
+            this.sentLatch = sentLatch;
         }
 
         @Override
@@ -137,6 +144,9 @@
                     RemoteEndpoint endpoint = session.getRemote();
                     endpoint.sendString(gson.toJson(response));
                 }
+                if (sentLatch != null) {
+                    sentLatch.countDown(); // synchronizes for tests
+                }
             } catch (IOException e) {
                 logger.warning(
                         "Failed to send response to microservice endpoint. Reason: "
--- a/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/CmdChannelAgentSocket.java	Mon Jun 12 19:04:06 2017 +0200
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/CmdChannelAgentSocket.java	Wed Jun 14 12:33:08 2017 +0200
@@ -67,20 +67,26 @@
 
     private static final Logger logger = LoggingUtils
             .getLogger(CmdChannelAgentSocket.class);
-    private final Gson gson;
+    private final GsonFacade gson;
     private final CountDownLatch connectLatch;
     private final OnMessageCallBack onMessage;
     private final String agentId;
     private Session session;
 
     public CmdChannelAgentSocket(OnMessageCallBack onMessage, CountDownLatch connect, String agentId) {
+        this(onMessage, connect, agentId,
+                new GsonFacadeImpl(new GsonBuilder()
+                .registerTypeAdapterFactory(new MessageTypeAdapterFactory())
+                .serializeNulls()
+                .create()));
+    }
+    
+    // for testing purposes
+    CmdChannelAgentSocket(OnMessageCallBack onMessage, CountDownLatch connect, String agentId, GsonFacade gson) {
         this.onMessage = onMessage;
         this.connectLatch = connect;
         this.agentId = agentId;
-        this.gson = new GsonBuilder()
-                .registerTypeAdapterFactory(new MessageTypeAdapterFactory())
-                .serializeNulls()
-                .create();
+        this.gson = gson;
     }
 
     @OnWebSocketFrame
@@ -144,7 +150,7 @@
     @OnWebSocketMessage
     public void onMessage(final Session session, final String msg) {
         final Message message = gson.fromJson(msg, Message.class);
-        onMessage.run(session, message, gson);
+        onMessage.run(session, message, gson.toGson());
     }
 
     public void closeSession() {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/GsonFacade.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal.socket;
+
+import com.google.gson.Gson;
+import com.redhat.thermostat.commands.model.Message;
+
+/**
+ * Facade for a Gson object. Allows for better testing
+ * of the CmdChannelAgentSocket code. Gson is a final class.
+ *
+ */
+interface GsonFacade {
+
+    Message fromJson(String json, Class<Message> clazz);
+    
+    Gson toGson();
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/main/java/com/redhat/thermostat/commands/agent/internal/socket/GsonFacadeImpl.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal.socket;
+
+import com.google.gson.Gson;
+import com.redhat.thermostat.commands.model.Message;
+
+class GsonFacadeImpl implements GsonFacade {
+
+    private final Gson gson;
+    
+    GsonFacadeImpl(Gson gson) {
+        this.gson = gson;
+    }
+    
+    @Override
+    public Message fromJson(String json, Class<Message> clazz) {
+        return gson.fromJson(json, clazz);
+    }
+
+    @Override
+    public Gson toGson() {
+        return gson;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/CommandsBackendTest.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+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.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Version;
+
+import com.redhat.thermostat.commands.agent.internal.CommandsBackend.ConfigCreator;
+import com.redhat.thermostat.commands.agent.internal.CommandsBackend.CredentialsCreator;
+import com.redhat.thermostat.commands.agent.internal.CommandsBackend.WsClientCreator;
+import com.redhat.thermostat.commands.agent.internal.socket.CmdChannelAgentSocket;
+import com.redhat.thermostat.common.config.experimental.ConfigurationInfoSource;
+import com.redhat.thermostat.common.plugins.PluginConfiguration;
+import com.redhat.thermostat.shared.config.CommonPaths;
+import com.redhat.thermostat.storage.core.StorageCredentials;
+
+public class CommandsBackendTest {
+
+    private static final String GW_URL = "ws://example.com/commands/v1";
+    private CommandsBackend backend;
+    private StorageCredentials creds;
+    private BundleContext bundleContext;
+    private WebSocketClientFacade client;
+    private CountDownLatch socketConnect;
+    
+    @Before
+    public void setup() throws IOException {
+        socketConnect = new CountDownLatch(1);
+        creds = mock(StorageCredentials.class);
+        CredentialsCreator credsCreator = mock(CredentialsCreator.class);
+        when(credsCreator.create(any(CommonPaths.class))).thenReturn(creds);
+        WsClientCreator creator = mock(WsClientCreator.class);
+        client = mock(WebSocketClientFacade.class);
+        when(creator.createClient()).thenReturn(client);
+        ConfigCreator configCreator = mock(ConfigCreator.class);
+        PluginConfiguration config = mock(PluginConfiguration.class);
+        when(config.getGatewayURL()).thenReturn(GW_URL);
+        when(configCreator.createConfig(any(ConfigurationInfoSource.class))).thenReturn(config);
+        backend = new CommandsBackend(creator, credsCreator, configCreator, socketConnect);
+        backend.bindPaths(mock(CommonPaths.class));
+        bundleContext = mock(BundleContext.class);
+        Bundle bundle = mock(Bundle.class);
+        when(bundle.getVersion()).thenReturn(mock(Version.class));
+        when(bundleContext.getBundle()).thenReturn(bundle);
+        backend.componentActivated(bundleContext);
+    }
+    
+    @Test
+    public void testGetBasicAuthHeaderValueNoCreds() {
+        doUnknownCredsTest(backend);
+    }
+    
+    @Test
+    public void testGetBasicAuthHeaderValueWithNoUsername() {
+        when(creds.getPassword()).thenReturn(new char[] { 'a', 'b', 'c' });
+        doUnknownCredsTest(backend);
+    }
+    
+    @Test
+    public void testGetBasicAuthHeaderValueWithNoPassword() {
+        when(creds.getUsername()).thenReturn("foo-user");
+        doUnknownCredsTest(backend);
+    }
+    
+    @Test
+    public void canGetBasicAuthHeaderValueWithUsernamePwd() {
+        String password = "foo";
+        String username = "foo-user";
+        when(creds.getPassword()).thenReturn(password.toCharArray());
+        when(creds.getUsername()).thenReturn(username);
+        String expected = base64EncodedHeader(username + ":" + password);
+        assertEquals(expected, backend.getBasicAuthHeaderValue());
+    }
+    
+    @Test
+    public void testComponentActivated() {
+        // setup invokes it. only do verification here
+        verify(client).start();
+    }
+    
+    @Test
+    public void testActivateSuccess() throws IOException {
+        String password = "foo";
+        String username = "foo-user";
+        when(creds.getPassword()).thenReturn(password.toCharArray());
+        when(creds.getUsername()).thenReturn(username);
+        ArgumentCaptor<URI> uriCaptor = ArgumentCaptor.forClass(URI.class);
+        ArgumentCaptor<ClientUpgradeRequest> reqCaptor = ArgumentCaptor.forClass(ClientUpgradeRequest.class);
+        // release connect latch
+        socketConnect.countDown();
+        boolean success = backend.activate();
+        verify(client).connect(any(CmdChannelAgentSocket.class), uriCaptor.capture(), reqCaptor.capture());
+        assertTrue("Expected successful activation", success);
+        URI uri = uriCaptor.getValue();
+        String expectedURI = GW_URL + "/systems/ignoreMe/agents/testAgent";
+        assertEquals(expectedURI, uri.toString());
+        ClientUpgradeRequest req = reqCaptor.getValue();
+        String expectedHeader = base64EncodedHeader(username + ":" + password);
+        String actualHeader = req.getHeader(HttpHeader.AUTHORIZATION.asString());
+        assertEquals(expectedHeader, actualHeader);
+        assertTrue("Expected backend to be active", backend.isActive());
+    }
+    
+    @Test
+    public void testActivateFail() throws IOException {
+        // set up for failure
+        doThrow(IOException.class).when(client).connect(any(CmdChannelAgentSocket.class), any(URI.class), any(ClientUpgradeRequest.class));
+        
+        boolean success = backend.activate();
+        assertFalse("Expected unsuccessful activation", success);
+        assertFalse(backend.isActive());
+    }
+    
+    @Test
+    public void testDeactivate() throws IOException {
+        // release connect latch
+        socketConnect.countDown();
+        boolean success = backend.activate();
+        assertTrue(success);
+        success = backend.deactivate();
+        verify(client).stop();
+        assertTrue(success);
+        assertFalse(backend.isActive());
+    }
+
+    private void doUnknownCredsTest(CommandsBackend backend) {
+        String unknown = "UNKNOWN:UNKNOWN";
+        String expected = base64EncodedHeader(unknown);
+        String actual = backend.getBasicAuthHeaderValue();
+        assertEquals(expected, actual);
+    }
+
+    private String base64EncodedHeader(String usernamePassword) {
+        @SuppressWarnings("restriction")
+        String expectedCreds = new sun.misc.BASE64Encoder().encode(usernamePassword.getBytes());
+        return "Basic " + expectedCreds;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/receiver/PingReceiverTest.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal.receiver;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.TreeMap;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.commands.model.AgentRequest;
+import com.redhat.thermostat.commands.model.WebSocketResponse;
+import com.redhat.thermostat.commands.model.WebSocketResponse.ResponseType;
+
+public class PingReceiverTest {
+
+    @Test
+    public void canReceiveWithProperSequence() {
+        PingReceiver receiver = new PingReceiver();
+        AgentRequest req = new AgentRequest(123L, new TreeMap<String, String>());
+        WebSocketResponse resp = receiver.receive(req);
+        assertEquals(ResponseType.OK, resp.getResponseType());
+        assertEquals(123L, resp.getSequenceId());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/socket/AgentSocketOnMessageCallbackTest.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal.socket;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Session;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.redhat.thermostat.commands.agent.internal.receiver.PingReceiver;
+import com.redhat.thermostat.commands.agent.internal.socket.AgentSocketOnMessageCallback.CmdChannelRequestHandler;
+import com.redhat.thermostat.commands.agent.internal.typeadapters.MessageTypeAdapterFactory;
+import com.redhat.thermostat.commands.agent.receiver.ReceiverRegistry;
+import com.redhat.thermostat.commands.agent.receiver.RequestReceiver;
+import com.redhat.thermostat.commands.model.AgentRequest;
+import com.redhat.thermostat.commands.model.ClientRequest;
+import com.redhat.thermostat.commands.model.WebSocketResponse;
+import com.redhat.thermostat.commands.model.WebSocketResponse.ResponseType;
+import com.redhat.thermostat.shared.config.InvalidConfigurationException;
+
+public class AgentSocketOnMessageCallbackTest {
+
+    private Gson gson;
+    
+    @Before
+    public void setup() {
+        gson = new GsonBuilder().registerTypeAdapterFactory(new MessageTypeAdapterFactory()).create();
+    }
+    
+    /**
+     * Request handling is asynchronous, thus the test synchronizes using a CountDownLatch.
+     * It cannot make assumptions on what is being sent on the session, though, as this is
+     * being handled after the receiver did it's work.
+     * 
+     * @throws InterruptedException
+     */
+    @Test
+    public void handlesAgentRequestsProperly() throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        TestReceiverRegistry reg = new TestReceiverRegistry(latch);
+        AgentSocketOnMessageCallback cb = new AgentSocketOnMessageCallback(reg);
+        Session session = mock(Session.class);
+        when(session.getRemote()).thenReturn(mock(RemoteEndpoint.class)); // Prevent spurious NPEs
+        SortedMap<String, String> params = new TreeMap<>();
+        String receiverName = "foo-bar";
+        params.put("receiver", receiverName);
+        AgentRequest agentRequest = new AgentRequest(333L, params);
+        
+        // Main method under test
+        cb.run(session, agentRequest, gson);
+        
+        // wait for request to be handled
+        latch.await();
+        assertEquals(receiverName, reg.clazz);
+        assertEquals(333L, reg.response.getSequenceId());
+    }
+    
+    @Test
+    public void handlerThreadSendsResponseToSession() throws InterruptedException, IOException {
+        ArgumentCaptor<String> jsonCaptor = ArgumentCaptor.forClass(String.class);
+        CountDownLatch receiverHandled = new CountDownLatch(1);
+        CountDownLatch sentLatch = new CountDownLatch(1);
+        TestReceiverRegistry reg = new TestReceiverRegistry(receiverHandled);
+        Session session = mock(Session.class);
+        RemoteEndpoint mockEndpoint = mock(RemoteEndpoint.class);
+        when(session.getRemote()).thenReturn(mockEndpoint);
+        SortedMap<String, String> params = new TreeMap<>();
+        params.put("receiver", "ignored");
+        AgentRequest agentRequest = new AgentRequest(344L, params);
+        
+        CmdChannelRequestHandler handler = new CmdChannelRequestHandler(session, agentRequest, reg, gson, sentLatch);
+        handler.start(); // start asynchronously
+        
+        receiverHandled.await(); // wait for receiver to handle request
+        sentLatch.await(); // wait for sending to actually happen
+        verify(mockEndpoint).sendString(jsonCaptor.capture());
+        String json = jsonCaptor.getValue();
+        String expected = "{\"type\":100,\"sequence\":344,\"payload\":{\"respType\":\"OK\"}}";
+        assertEquals(expected, json);
+    }
+    
+    /**
+     * A "receiver=<name>" argument is expected at a bare minimum. If no receiver name is
+     * specified an error response is expected to be sent to the client.
+     * 
+     * @throws InterruptedException
+     * @throws IOException
+     */
+    @Test
+    public void noReceiverSendsErrorResponseToSession() throws InterruptedException, IOException {
+        ArgumentCaptor<String> jsonCaptor = ArgumentCaptor.forClass(String.class);
+        CountDownLatch sentLatch = new CountDownLatch(1);
+        Session session = mock(Session.class);
+        RemoteEndpoint mockEndpoint = mock(RemoteEndpoint.class);
+        when(session.getRemote()).thenReturn(mockEndpoint);
+        SortedMap<String, String> emptyParams = new TreeMap<>();
+        AgentRequest agentRequest = new AgentRequest(888L, emptyParams);
+        
+        CmdChannelRequestHandler handler = new CmdChannelRequestHandler(session, agentRequest, mock(ReceiverRegistry.class), gson, sentLatch);
+        handler.start(); // start asynchronously
+        
+        sentLatch.await(); // wait for sending to actually happen
+        verify(mockEndpoint).sendString(jsonCaptor.capture());
+        String json = jsonCaptor.getValue();
+        String expected = "{\"type\":100,\"sequence\":888,\"payload\":{\"respType\":\"ERROR\"}}";
+        assertEquals(expected, json);
+    }
+    
+    /**
+     * When the connect to the endpoint fails due to authentication/authorization issues a
+     * WebSocketResponse is being sent back. This case needs to be handled.
+     */
+    @Test(expected = InvalidConfigurationException.class)
+    public void handlesAuthFailResponsesProperly() {
+        WebSocketResponse response = new WebSocketResponse(WebSocketResponse.UNKNOWN_SEQUENCE, ResponseType.AUTH_FAIL);
+        AgentSocketOnMessageCallback cb = new AgentSocketOnMessageCallback(mock(ReceiverRegistry.class));
+        cb.run(null, response, gson); // throws exception
+    }
+    
+    /**
+     * There are other message types the agent endpoint is not expected to receive directly.
+     * They are for client->gateway interactions or something else entirely.
+     */
+    @Test(expected = IllegalStateException.class)
+    public void unexpectedMessageTypesThrowException() {
+        ClientRequest request = new ClientRequest(212);
+        AgentSocketOnMessageCallback cb = new AgentSocketOnMessageCallback(mock(ReceiverRegistry.class));
+        cb.run(null, request, gson); // throws exception
+    }
+
+    static class TestReceiverRegistry extends ReceiverRegistry {
+
+        private final CountDownLatch latch;
+        private String clazz;
+        private WebSocketResponse response;
+
+        TestReceiverRegistry(CountDownLatch latch) {
+            super(null);
+            this.latch = latch;
+        }
+
+        @Override
+        public RequestReceiver getReceiver(String clazz) {
+            this.clazz = clazz;
+            return new PingReceiver() {
+                @Override
+                public WebSocketResponse receive(AgentRequest request) {
+                    WebSocketResponse resp = super.receive(request);
+                    response = resp;
+                    latch.countDown();
+                    return resp;
+                }
+            };
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/internal/socket/CmdChannelAgentSocketTest.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.internal.socket;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Matchers.eq;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.Frame.Type;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import com.google.gson.Gson;
+import com.redhat.thermostat.commands.agent.internal.socket.CmdChannelAgentSocket.OnMessageCallBack;
+import com.redhat.thermostat.commands.model.Message;
+
+public class CmdChannelAgentSocketTest {
+
+    @Test(timeout = 2000)
+    public void connectReleasesConnectLatch() throws InterruptedException {
+        CountDownLatch latch = new CountDownLatch(1);
+        CmdChannelAgentSocket socket = new CmdChannelAgentSocket(null, latch, "foo-agent");
+        socket.onConnect(mock(Session.class)); // should release latch
+        latch.await();
+    }
+    
+    @Test
+    public void canHandlePings() throws InterruptedException, IOException {
+        CountDownLatch connectLatch = new CountDownLatch(1);
+        Session session = mock(Session.class);
+        RemoteEndpoint remoteEndpoint = mock(RemoteEndpoint.class);
+        when(session.getRemote()).thenReturn(remoteEndpoint);
+        CmdChannelAgentSocket socket = new CmdChannelAgentSocket(null, connectLatch, "foo-agent");
+        socket.onConnect(session);
+        connectLatch.await();
+        ByteBuffer pingPayload = ByteBuffer.wrap("ping".getBytes());
+        Frame pingFrame = mock(Frame.class);
+        when(pingFrame.getType()).thenReturn(Type.PING);
+        when(pingFrame.getPayload()).thenReturn(pingPayload.slice());
+        when(pingFrame.hasPayload()).thenReturn(true);
+        socket.onFrame(pingFrame);
+        ArgumentCaptor<ByteBuffer> payloadCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
+        verify(remoteEndpoint).sendPong(payloadCaptor.capture());
+        ByteBuffer pongPayload = payloadCaptor.getValue();
+        String actual = BufferUtil.toUTF8String(pongPayload);
+        assertEquals("ping", actual);
+    }
+    
+    @Test
+    public void callsCallBackOnStringMessage() {
+        final GsonFacade gsonFacade = mock(GsonFacade.class);
+        final Gson myGson = new Gson();
+        when(gsonFacade.toGson()).thenReturn(myGson);
+        final Session mySession = mock(Session.class);
+        final boolean[] hasRun = new boolean[1];
+        CmdChannelAgentSocket socket = new CmdChannelAgentSocket(new OnMessageCallBack() {
+            
+            @Override
+            public void run(Session session, Message msg, Gson gson) {
+                assertSame(mySession, session);
+                assertSame(myGson, gson);
+                hasRun[0] = true;
+            }
+        }, (CountDownLatch)null, "some-agent", gsonFacade);
+        
+        String strMessage = "{ \"foo\": \"bar\" }";
+        socket.onMessage(mySession, strMessage);
+        verify(gsonFacade).fromJson(eq(strMessage), eq(Message.class));
+        assertTrue(hasRun[0]);
+    }
+    
+    @Test
+    public void verifyCloseSession() throws InterruptedException {
+        CountDownLatch connectLatch = new CountDownLatch(1);
+        Session session = mock(Session.class);
+        CmdChannelAgentSocket socket = new CmdChannelAgentSocket(null, connectLatch, "foo-agent");
+        socket.onConnect(session);
+        connectLatch.await();
+        socket.closeSession();
+        verify(session).close();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/commands/agent/src/test/java/com/redhat/thermostat/commands/agent/receiver/ReceiverRegistryTest.java	Wed Jun 14 12:33:08 2017 +0200
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.commands.agent.receiver;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+import java.util.Collection;
+
+import org.junit.Test;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+import com.redhat.thermostat.testutils.StubBundleContext;
+
+public class ReceiverRegistryTest {
+    
+    private static final String FILTER_FORMAT = "(&(objectclass=*)(servicename=%s))";
+
+    @Test
+    public void testRegister() throws InvalidSyntaxException {
+        StubBundleContext context = new StubBundleContext();
+        ReceiverRegistry reg = new ReceiverRegistry(context);
+        RequestReceiver receiver = mock(RequestReceiver.class);
+        
+        reg.registerReceiver(receiver);
+        
+        context.isServiceRegistered(RequestReceiver.class.getName(), receiver.getClass());
+        Collection<?> services = context.getServiceReferences(RequestReceiver.class, String.format(FILTER_FORMAT, receiver.getClass().getName()));
+        assertEquals(1, services.size());
+        @SuppressWarnings("unchecked")
+        ServiceReference<RequestReceiver> sr = (ServiceReference<RequestReceiver>)services.iterator().next();
+        String serviceName = (String)sr.getProperty("servicename");
+        assertEquals(receiver.getClass().getName(), serviceName);
+    }
+    
+    @Test
+    public void testGetReceiver() {
+        StubBundleContext context = new StubBundleContext();
+        ReceiverRegistry reg = new ReceiverRegistry(context);
+        assertNull(reg.getReceiver(String.class.getName()));
+        
+        RequestReceiver receiver = mock(RequestReceiver.class);
+        reg.registerReceiver(receiver);
+        RequestReceiver actual = reg.getReceiver(receiver.getClass().getName());
+        assertSame(receiver, actual);
+    }
+}