/* * Copyright 2017 ThoughtWorks, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.thoughtworks.go.agent; import com.thoughtworks.go.agent.service.AgentUpgradeService; import com.thoughtworks.go.agent.service.SslInfrastructureService; import com.thoughtworks.go.buildsession.BuildSessionBasedTestCase; import com.thoughtworks.go.config.AgentRegistry; import com.thoughtworks.go.config.GuidService; import com.thoughtworks.go.domain.*; import com.thoughtworks.go.matchers.RegexMatcher; import com.thoughtworks.go.plugin.access.packagematerial.PackageRepositoryExtension; import com.thoughtworks.go.plugin.access.pluggabletask.TaskExtension; import com.thoughtworks.go.plugin.access.scm.SCMExtension; import com.thoughtworks.go.plugin.infra.PluginManager; import com.thoughtworks.go.publishers.GoArtifactsManipulator; import com.thoughtworks.go.remote.BuildRepositoryRemote; import com.thoughtworks.go.remote.work.Work; import com.thoughtworks.go.server.service.AgentBuildingInfo; import com.thoughtworks.go.server.service.AgentRuntimeInfo; import com.thoughtworks.go.util.HttpService; import com.thoughtworks.go.util.SubprocessLogger; import com.thoughtworks.go.util.SystemEnvironment; import com.thoughtworks.go.websocket.*; import com.thoughtworks.go.work.SleepWork; import org.apache.http.client.HttpClient; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import java.io.IOException; import java.security.GeneralSecurityException; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class AgentWebSocketClientControllerTest { private static final int MAX_WAIT_IN_TEST = 10000; @Rule public TemporaryFolder folder = new TemporaryFolder(); @Mock private BuildRepositoryRemote loopServer; @Mock private GoArtifactsManipulator artifactsManipulator; @Mock private SslInfrastructureService sslInfrastructureService; @Mock private Work work; @Mock private AgentRegistry agentRegistry; @Mock private SubprocessLogger subprocessLogger; @Mock private SystemEnvironment systemEnvironment; @Mock private AgentUpgradeService agentUpgradeService; @Mock private PluginManager pluginManager; @Mock private PackageRepositoryExtension packageRepositoryExtension; @Mock private SCMExtension scmExtension; @Mock private TaskExtension taskExtension; @Mock private HttpService httpService; @Mock private HttpClient httpClient; private AgentWebSocketClientController agentController; @Mock private RemoteEndpoint remoteEndpoint; @Mock private WebSocketClientHandler webSocketClientHandler; @Mock private WebSocketSessionHandler webSocketSessionHandler; private String agentUuid = "uuid"; @After public void tearDown() { GuidService.deleteGuid(); } @Test public void shouldSendAgentRuntimeInfoWhenWorkIsCalled() throws Exception { when(sslInfrastructureService.isRegistered()).thenReturn(true); when(webSocketSessionHandler.isNotRunning()).thenReturn(false); ArgumentCaptor<Message> argumentCaptor = ArgumentCaptor.forClass(Message.class); agentController = createAgentController(); agentController.init(); agentController.work(); verify(webSocketSessionHandler).sendAndWaitForAcknowledgement(argumentCaptor.capture()); Message message = argumentCaptor.getValue(); assertThat(message.getAcknowledgementId(), notNullValue()); assertThat(message.getAction(), is(Action.ping)); assertThat(message.getData(), is(MessageEncoding.encodeData(agentController.getAgentRuntimeInfo()))); } @Test public void shouldHandleSecurityErrorWhenOpeningWebSocketFails() throws Exception { when(sslInfrastructureService.isRegistered()).thenReturn(true); agentController = createAgentController(); when(webSocketSessionHandler.isNotRunning()).thenReturn(true); doThrow(new GeneralSecurityException()).when(webSocketClientHandler).connect(agentController); agentController.init(); agentController.loop(); verify(agentUpgradeService).checkForUpgrade(); verify(sslInfrastructureService).registerIfNecessary(agentController.getAgentAutoRegistrationProperties()); verify(sslInfrastructureService).invalidateAgentCertificate(); } @Test public void processSetCookieAction() throws IOException, InterruptedException { agentController = createAgentController(); agentController.init(); agentController.process(new Message(Action.setCookie, MessageEncoding.encodeData("cookie"))); assertThat(agentController.getAgentRuntimeInfo().getCookie(), is("cookie")); } @Test public void processAssignWorkAction() throws IOException, InterruptedException { ArgumentCaptor<Message> argumentCaptor = ArgumentCaptor.forClass(Message.class); agentController = createAgentController(); agentController.init(); agentController.process(new Message(Action.assignWork, MessageEncoding.encodeWork(new SleepWork("work1", 0)))); assertThat(agentController.getAgentRuntimeInfo().getRuntimeStatus(), is(AgentRuntimeStatus.Idle)); verify(webSocketSessionHandler, times(1)).sendAndWaitForAcknowledgement(argumentCaptor.capture()); verify(artifactsManipulator).setProperty(null, new Property("work1_result", "done")); Message message = argumentCaptor.getAllValues().get(0); assertThat(message.getAcknowledgementId(), notNullValue()); assertThat(message.getAction(), is(Action.ping)); assertThat(message.getData(), is(MessageEncoding.encodeData(agentController.getAgentRuntimeInfo()))); } @Test public void processAssignWorkActionWithConsoleLogsThroughWebSockets() throws IOException, InterruptedException { SystemEnvironment env = new SystemEnvironment(); env.set(SystemEnvironment.WEBSOCKET_ENABLED, true); env.set(SystemEnvironment.CONSOLE_LOGS_THROUGH_WEBSOCKET_ENABLED, true); ArgumentCaptor<Message> argumentCaptor = ArgumentCaptor.forClass(Message.class); agentController = createAgentController(); agentController.init(); agentController.process(new Message(Action.assignWork, MessageEncoding.encodeWork(new SleepWork("work1", 0)))); assertThat(agentController.getAgentRuntimeInfo().getRuntimeStatus(), is(AgentRuntimeStatus.Idle)); verify(webSocketSessionHandler, times(2)).sendAndWaitForAcknowledgement(argumentCaptor.capture()); verify(artifactsManipulator).setProperty(null, new Property("work1_result", "done")); Message message = argumentCaptor.getAllValues().get(1); assertThat(message.getAcknowledgementId(), notNullValue()); assertThat(message.getAction(), is(Action.ping)); assertThat(message.getData(), is(MessageEncoding.encodeData(agentController.getAgentRuntimeInfo()))); Message message2 = argumentCaptor.getAllValues().get(0); assertThat(message2.getAcknowledgementId(), notNullValue()); assertThat(message2.getAction(), is(Action.consoleOut)); ConsoleTransmission ct = MessageEncoding.decodeData(message2.getData(), ConsoleTransmission.class); assertThat(ct.getLine(), RegexMatcher.matches("Sleeping for 0 milliseconds")); env.set(SystemEnvironment.WEBSOCKET_ENABLED, false); env.set(SystemEnvironment.CONSOLE_LOGS_THROUGH_WEBSOCKET_ENABLED, false); } @Test public void processBuildCommandWithConsoleLogsThroughWebSockets() throws Exception { ArgumentCaptor<Message> currentStatusMessageCaptor = ArgumentCaptor.forClass(Message.class); when(systemEnvironment.isConsoleLogsThroughWebsocketEnabled()).thenReturn(true); when(agentRegistry.uuid()).thenReturn(agentUuid); agentController = createAgentController(); agentController.init(); BuildSettings build = new BuildSettings(); build.setBuildId("b001"); build.setConsoleUrl("http://foo.bar/console"); build.setArtifactUploadBaseUrl("http://foo.bar/artifacts"); build.setPropertyBaseUrl("http://foo.bar/properties"); build.setBuildLocator("build1"); build.setBuildLocatorForDisplay("build1ForDisplay"); build.setBuildCommand(BuildCommand.compose( BuildCommand.echo("building"), BuildCommand.reportCurrentStatus(JobState.Building))); agentController.process(new Message(Action.build, MessageEncoding.encodeData(build))); assertThat(agentController.getAgentRuntimeInfo().getRuntimeStatus(), is(AgentRuntimeStatus.Idle)); AgentRuntimeInfo agentRuntimeInfo = cloneAgentRuntimeInfo(agentController.getAgentRuntimeInfo()); agentRuntimeInfo.busy(new AgentBuildingInfo("build1ForDisplay", "build1")); verify(webSocketSessionHandler, times(3)).sendAndWaitForAcknowledgement(currentStatusMessageCaptor.capture()); Message consoleOutMsg = currentStatusMessageCaptor.getAllValues().get(0); assertThat(consoleOutMsg.getAcknowledgementId(), notNullValue()); assertThat(consoleOutMsg.getAction(), is(Action.consoleOut)); ConsoleTransmission ct = MessageEncoding.decodeData(consoleOutMsg.getData(), ConsoleTransmission.class); assertThat(ct.getLine(), RegexMatcher.matches("building")); assertEquals(ct.getBuildId(), "b001"); Message message = currentStatusMessageCaptor.getAllValues().get(1); assertThat(message.getAcknowledgementId(), notNullValue()); assertThat(message.getAction(), is(Action.reportCurrentStatus)); assertThat(message.getData(), is(MessageEncoding.encodeData(new Report(agentRuntimeInfo, "b001", JobState.Building, null)))); Message jobCompletedMessage = currentStatusMessageCaptor.getAllValues().get(2); assertThat(jobCompletedMessage.getAcknowledgementId(), notNullValue()); assertThat(jobCompletedMessage.getAction(), is(Action.reportCompleted)); assertThat(jobCompletedMessage.getData(), is(MessageEncoding.encodeData(new Report(agentRuntimeInfo, "b001", null, JobResult.Passed)))); } @Test public void processBuildCommand() throws Exception { ArgumentCaptor<Message> currentStatusMessageCaptor = ArgumentCaptor.forClass(Message.class); when(agentRegistry.uuid()).thenReturn(agentUuid); agentController = createAgentController(); agentController.init(); BuildSettings build = new BuildSettings(); build.setBuildId("b001"); build.setConsoleUrl("http://foo.bar/console"); build.setArtifactUploadBaseUrl("http://foo.bar/artifacts"); build.setPropertyBaseUrl("http://foo.bar/properties"); build.setBuildLocator("build1"); build.setBuildLocatorForDisplay("build1ForDisplay"); build.setBuildCommand(BuildCommand.compose( BuildCommand.echo("building"), BuildCommand.reportCurrentStatus(JobState.Building))); agentController.process(new Message(Action.build, MessageEncoding.encodeData(build))); assertThat(agentController.getAgentRuntimeInfo().getRuntimeStatus(), is(AgentRuntimeStatus.Idle)); AgentRuntimeInfo agentRuntimeInfo = cloneAgentRuntimeInfo(agentController.getAgentRuntimeInfo()); agentRuntimeInfo.busy(new AgentBuildingInfo("build1ForDisplay", "build1")); verify(webSocketSessionHandler, times(2)).sendAndWaitForAcknowledgement(currentStatusMessageCaptor.capture()); Message message = currentStatusMessageCaptor.getAllValues().get(0); assertThat(message.getAcknowledgementId(), notNullValue()); assertThat(message.getAction(), is(Action.reportCurrentStatus)); assertThat(message.getData(), is(MessageEncoding.encodeData(new Report(agentRuntimeInfo, "b001", JobState.Building, null)))); Message jobCompletedMessage = currentStatusMessageCaptor.getAllValues().get(1); assertThat(jobCompletedMessage.getAcknowledgementId(), notNullValue()); assertThat(jobCompletedMessage.getAction(), is(Action.reportCompleted)); assertThat(jobCompletedMessage.getData(), is(MessageEncoding.encodeData(new Report(agentRuntimeInfo, "b001", null, JobResult.Passed)))); } private AgentRuntimeInfo cloneAgentRuntimeInfo(AgentRuntimeInfo agentRuntimeInfo) { return MessageEncoding.decodeData(MessageEncoding.encodeData(agentRuntimeInfo), AgentRuntimeInfo.class); } @Test public void processCancelBuildCommandBuild() throws IOException, InterruptedException { ArgumentCaptor<Message> argumentCaptor = ArgumentCaptor.forClass(Message.class); agentController = createAgentController(); agentController.init(); agentController.getAgentRuntimeInfo().setSupportsBuildCommandProtocol(true); final BuildSettings build = new BuildSettings(); build.setBuildId("b001"); build.setConsoleUrl("http://foo.bar/console"); build.setArtifactUploadBaseUrl("http://foo.bar/artifacts"); build.setPropertyBaseUrl("http://foo.bar/properties"); build.setBuildLocator("build1"); build.setBuildLocatorForDisplay("build1ForDisplay"); build.setBuildCommand(BuildCommand.compose( BuildSessionBasedTestCase.execSleepScript(MAX_WAIT_IN_TEST / 1000), BuildCommand.reportCurrentStatus(JobState.Building))); Thread buildingThread = new Thread(new Runnable() { @Override public void run() { try { agentController.process(new Message(Action.build, MessageEncoding.encodeData(build))); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); buildingThread.start(); waitForAgentRuntimeState(agentController.getAgentRuntimeInfo(), AgentRuntimeStatus.Building); agentController.process(new Message(Action.cancelBuild)); buildingThread.join(MAX_WAIT_IN_TEST); AgentRuntimeInfo agentRuntimeInfo = cloneAgentRuntimeInfo(agentController.getAgentRuntimeInfo()); agentRuntimeInfo.busy(new AgentBuildingInfo("build1ForDisplay", "build1")); agentRuntimeInfo.cancel(); verify(webSocketSessionHandler).sendAndWaitForAcknowledgement(argumentCaptor.capture()); assertThat(agentController.getAgentRuntimeInfo().getRuntimeStatus(), is(AgentRuntimeStatus.Idle)); Message message = argumentCaptor.getValue(); assertThat(message.getAcknowledgementId(), notNullValue()); assertThat(message.getAction(), is(Action.reportCompleted)); assertThat(message.getData(), is(MessageEncoding.encodeData(new Report(agentRuntimeInfo, "b001", null, JobResult.Cancelled)))); } @Test public void processCancelJobAction() throws IOException, InterruptedException { agentController = createAgentController(); agentController.init(); final SleepWork sleep1secWork = new SleepWork("work1", MAX_WAIT_IN_TEST); Thread buildingThread = new Thread(new Runnable() { @Override public void run() { try { agentController.process(new Message(Action.assignWork, MessageEncoding.encodeWork(sleep1secWork))); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); buildingThread.start(); waitForAgentRuntimeState(agentController.getAgentRuntimeInfo(), AgentRuntimeStatus.Building); agentController.process(new Message(Action.cancelBuild)); buildingThread.join(MAX_WAIT_IN_TEST); assertThat(agentController.getAgentRuntimeInfo().getRuntimeStatus(), is(AgentRuntimeStatus.Idle)); verify(artifactsManipulator).setProperty(null, new Property("work1_result", "done_canceled")); } private void waitForAgentRuntimeState(AgentRuntimeInfo runtimeInfo, AgentRuntimeStatus status) throws InterruptedException { int elapsed = 0; int waitStep = 100; while (elapsed <= MAX_WAIT_IN_TEST) { if (runtimeInfo.getRuntimeStatus() == status) { return; } Thread.sleep(waitStep); elapsed += waitStep; } throw new RuntimeException("wait for agent status '" + status.name() + "' timeout, current status is '" + runtimeInfo.getRuntimeStatus().name() + "'"); } @Test public void processReregisterAction() throws IOException, InterruptedException { when(agentRegistry.uuid()).thenReturn(agentUuid); agentController = createAgentController(); agentController.init(); agentController.process(new Message(Action.reregister)); verify(sslInfrastructureService).invalidateAgentCertificate(); verify(webSocketSessionHandler).stop(); } @Test public void shouldCancelPreviousRunningJobIfANewAssignWorkMessageIsReceived() throws IOException, InterruptedException { agentController = createAgentController(); agentController.init(); final SleepWork work1 = new SleepWork("work1", MAX_WAIT_IN_TEST); Thread work1Thread = new Thread(new Runnable() { @Override public void run() { try { agentController.process(new Message(Action.assignWork, MessageEncoding.encodeWork(work1))); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); work1Thread.start(); waitForAgentRuntimeState(agentController.getAgentRuntimeInfo(), AgentRuntimeStatus.Building); SleepWork work2 = new SleepWork("work2", 1); agentController.process(new Message(Action.assignWork, MessageEncoding.encodeWork(work2))); work1Thread.join(MAX_WAIT_IN_TEST); verify(artifactsManipulator).setProperty(null, new Property("work1_result", "done_canceled")); verify(artifactsManipulator).setProperty(null, new Property("work2_result", "done")); } private AgentWebSocketClientController createAgentController() { AgentWebSocketClientController controller = new AgentWebSocketClientController( loopServer, artifactsManipulator, sslInfrastructureService, agentRegistry, agentUpgradeService, subprocessLogger, systemEnvironment, pluginManager, packageRepositoryExtension, scmExtension, taskExtension, httpService, webSocketClientHandler, webSocketSessionHandler); return controller; } }