Mercurial > hg > thermostat-ng > agent
changeset 2693:d2535ae16b77
Add tests for commands agent plugin.
Reviewed-by: ebaron
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-June/023684.html
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); + } +}