/*
* 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.server.materials;
import com.thoughtworks.go.config.*;
import com.thoughtworks.go.config.materials.ScmMaterial;
import com.thoughtworks.go.config.materials.dependency.DependencyMaterial;
import com.thoughtworks.go.config.materials.svn.SvnMaterial;
import com.thoughtworks.go.config.materials.svn.SvnMaterialConfig;
import com.thoughtworks.go.domain.PipelineGroups;
import com.thoughtworks.go.domain.materials.Material;
import com.thoughtworks.go.domain.materials.MaterialConfig;
import com.thoughtworks.go.helper.MaterialConfigsMother;
import com.thoughtworks.go.helper.MaterialsMother;
import com.thoughtworks.go.helper.PipelineConfigMother;
import com.thoughtworks.go.i18n.LocalizedMessage;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.materials.postcommit.PostCommitHookImplementer;
import com.thoughtworks.go.server.materials.postcommit.PostCommitHookMaterialType;
import com.thoughtworks.go.server.materials.postcommit.PostCommitHookMaterialTypeResolver;
import com.thoughtworks.go.server.perf.MDUPerformanceLogger;
import com.thoughtworks.go.server.service.GoConfigService;
import com.thoughtworks.go.server.service.MaterialConfigConverter;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.serverhealth.*;
import com.thoughtworks.go.util.ProcessManager;
import com.thoughtworks.go.util.ReflectionUtil;
import com.thoughtworks.go.util.SystemEnvironment;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.Mockito;
import java.util.*;
import static com.thoughtworks.go.helper.MaterialUpdateMessageMatcher.matchMaterialUpdateMessage;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
public class MaterialUpdateServiceTest {
private MaterialUpdateService service;
private static final SvnMaterial svnMaterial = MaterialsMother.svnMaterial();
private static final DependencyMaterial dependencyMaterial = MaterialsMother.dependencyMaterial();
private SCMMaterialSource scmMaterialSource;
private DependencyMaterialUpdateNotifier dependencyMaterialUpdateNotifier;
private MaterialUpdateQueue queue;
private MaterialUpdateCompletedTopic completed;
private ConfigMaterialUpdateQueue configQueue;
private GoConfigWatchList watchList;
private GoConfigService goConfigService;
private static final SvnMaterialConfig MATERIAL_CONFIG = MaterialConfigsMother.svnMaterialConfig();
private Username username;
private HttpLocalizedOperationResult result;
private PostCommitHookMaterialTypeResolver postCommitHookMaterialType;
private PostCommitHookMaterialType validMaterialType;
private PostCommitHookMaterialType invalidMaterialType;
private ServerHealthService serverHealthService;
private SystemEnvironment systemEnvironment;
private MaterialConfigConverter materialConfigConverter;
private DependencyMaterialUpdateQueue dependencyMaterialUpdateQueue;
@Before
public void setUp() throws Exception {
queue = mock(MaterialUpdateQueue.class);
configQueue = mock(ConfigMaterialUpdateQueue.class);
watchList = mock(GoConfigWatchList.class);
completed = mock(MaterialUpdateCompletedTopic.class);
goConfigService = mock(GoConfigService.class);
postCommitHookMaterialType = mock(PostCommitHookMaterialTypeResolver.class);
serverHealthService = mock(ServerHealthService.class);
systemEnvironment = new SystemEnvironment();
scmMaterialSource = mock(SCMMaterialSource.class);
dependencyMaterialUpdateNotifier = mock(DependencyMaterialUpdateNotifier.class);
materialConfigConverter = mock(MaterialConfigConverter.class);
MDUPerformanceLogger mduPerformanceLogger = mock(MDUPerformanceLogger.class);
dependencyMaterialUpdateQueue = mock(DependencyMaterialUpdateQueue.class);
service = new MaterialUpdateService(queue,configQueue , completed,watchList, goConfigService, systemEnvironment,
serverHealthService, postCommitHookMaterialType, mduPerformanceLogger, materialConfigConverter, dependencyMaterialUpdateQueue);
service.registerMaterialSources(scmMaterialSource);
service.registerMaterialUpdateCompleteListener(scmMaterialSource);
service.registerMaterialUpdateCompleteListener(dependencyMaterialUpdateNotifier);
HashSet<MaterialConfig> materialConfigs = new HashSet(Collections.singleton(MATERIAL_CONFIG));
HashSet<Material> materials = new HashSet(Collections.singleton(svnMaterial));
when(goConfigService.getSchedulableMaterials()).thenReturn(materialConfigs);
when(materialConfigConverter.toMaterials(materialConfigs)).thenReturn(materials);
username = new Username(new CaseInsensitiveString("loser"));
result = new HttpLocalizedOperationResult();
validMaterialType = mock(PostCommitHookMaterialType.class);
when(validMaterialType.isKnown()).thenReturn(true);
when(validMaterialType.isValid(anyString())).thenReturn(true);
invalidMaterialType = mock(PostCommitHookMaterialType.class);
when(invalidMaterialType.isKnown()).thenReturn(false);
when(invalidMaterialType.isValid(anyString())).thenReturn(false);
}
@After
public void teardown() throws Exception {
systemEnvironment.reset(SystemEnvironment.MATERIAL_UPDATE_INACTIVE_TIMEOUT);
}
@Test
public void shouldSendMaterialUpdateMessageForAllSchedulableMaterials_onTimer() throws Exception {
when(scmMaterialSource.materialsForUpdate()).thenReturn(new HashSet<>(Arrays.asList(svnMaterial)));
service.onTimer();
Mockito.verify(queue).post(matchMaterialUpdateMessage(svnMaterial));
}
@Test
public void shouldPostUpdateMessageOnUpdateQueueForNonConfigMaterial_updateMaterial() {
assertTrue(service.updateMaterial(svnMaterial));
Mockito.verify(queue).post(matchMaterialUpdateMessage(svnMaterial));
Mockito.verify(configQueue, times(0)).post(any(MaterialUpdateMessage.class));
}
@Test
public void shouldPostUpdateMessageOnConfigQueueForConfigMaterial_updateMaterial() {
when(watchList.hasConfigRepoWithFingerprint(svnMaterial.getFingerprint())).thenReturn(true);
assertTrue(service.updateMaterial(svnMaterial));
Mockito.verify(configQueue).post(matchMaterialUpdateMessage(svnMaterial));
Mockito.verify(queue, times(0)).post(any(MaterialUpdateMessage.class));
}
@Test
public void shouldPostUpdateMessageOnDependencyMaterialUpdateQueueForDependencyMaterial_updateMaterial() {
when(watchList.hasConfigRepoWithFingerprint(svnMaterial.getFingerprint())).thenReturn(false);
assertTrue(service.updateMaterial(dependencyMaterial));
Mockito.verify(dependencyMaterialUpdateQueue).post(matchMaterialUpdateMessage(dependencyMaterial));
Mockito.verify(queue, times(0)).post(any(MaterialUpdateMessage.class));
Mockito.verify(configQueue, times(0)).post(any(MaterialUpdateMessage.class));
}
@Test
public void shouldAllowConcurrentUpdatesForNonAutoUpdateMaterials_updateMaterial() throws Exception {
ScmMaterial material = mock(ScmMaterial.class);
when(material.isAutoUpdate()).thenReturn(false);
MaterialUpdateMessage message = new MaterialUpdateMessage(material, 0);
doNothing().when(queue).post(message);
assertTrue(service.updateMaterial(material)); //prune inprogress queue to have this material in it
assertTrue(service.updateMaterial(material)); // immediately notify another check-in
verify(queue, times(2)).post(message);
verify(material).isAutoUpdate();
}
@Test
public void shouldNotAllowConcurrentUpdatesForAutoUpdateConfigMaterials_updateMaterial() throws Exception {
ScmMaterial material = mock(ScmMaterial.class);
when(material.isAutoUpdate()).thenReturn(true);
when(material.getFingerprint()).thenReturn("fingerprint");
when(watchList.hasConfigRepoWithFingerprint("fingerprint")).thenReturn(true);
MaterialUpdateMessage message = new MaterialUpdateMessage(material, 0);
doNothing().when(configQueue).post(message);
assertTrue(service.updateMaterial(material)); //prune inprogress queue to have this material in it
assertFalse(service.updateMaterial(material)); // immediately notify another check-in
verify(configQueue, times(1)).post(message);
verify(material).isAutoUpdate();
}
@Test
public void shouldNotAllowConcurrentUpdatesForAutoUpdateMaterials_updateMaterial() throws Exception {
ScmMaterial material = mock(ScmMaterial.class);
when(material.isAutoUpdate()).thenReturn(true);
MaterialUpdateMessage message = new MaterialUpdateMessage(material, 0);
doNothing().when(queue).post(message);
assertTrue(service.updateMaterial(material)); //prune inprogress queue to have this material in it
assertFalse(service.updateMaterial(material)); // immediately notify another check-in
verify(queue, times(1)).post(message);
verify(material).isAutoUpdate();
}
@Test
public void shouldAllowPostCommitNotificationsToPassThroughToTheQueue_WhenTheSameMaterialIsNotCurrentlyInProgressAndMaterialIsAutoUpdateTrue() throws Exception {
ScmMaterial material = mock(ScmMaterial.class);
when(material.isAutoUpdate()).thenReturn(true);
MaterialUpdateMessage message = new MaterialUpdateMessage(material, 0);
doNothing().when(queue).post(message);
service.updateMaterial(material); // first call to the method
verify(queue, times(1)).post(message);
verify(material, never()).isAutoUpdate();
}
@Test
public void shouldAllowPostCommitNotificationsToPassThroughToTheQueue_WhenTheSameMaterialIsNotCurrentlyInProgressAndMaterialIsAutoUpdateFalse() throws Exception {
ScmMaterial material = mock(ScmMaterial.class);
when(material.isAutoUpdate()).thenReturn(false);
MaterialUpdateMessage message = new MaterialUpdateMessage(material, 0);
doNothing().when(queue).post(message);
service.updateMaterial(material); // first call to the method
verify(queue, times(1)).post(message);
verify(material, never()).isAutoUpdate();
}
@Test
public void shouldReturn401WhenUserIsNotAnAdmin_WhenInvokingPostCommitHookMaterialUpdate() {
when(goConfigService.isUserAdmin(username)).thenReturn(false);
service.notifyMaterialsForUpdate(username, new HashMap(), result);
HttpLocalizedOperationResult unauthorizedResult = new HttpLocalizedOperationResult();
unauthorizedResult.unauthorized(LocalizedMessage.string("API_ACCESS_UNAUTHORIZED"), HealthStateType.unauthorised());
assertThat(result, is(unauthorizedResult));
verify(goConfigService).isUserAdmin(username);
}
@Test
public void shouldReturn400WhenTypeIsMissing_WhenInvokingPostCommitHookMaterialUpdate() {
when(goConfigService.isUserAdmin(username)).thenReturn(true);
service.notifyMaterialsForUpdate(username, new HashMap(), result);
HttpLocalizedOperationResult badRequestResult = new HttpLocalizedOperationResult();
badRequestResult.badRequest(LocalizedMessage.string("API_BAD_REQUEST"));
assertThat(result, is(badRequestResult));
verify(goConfigService).isUserAdmin(username);
}
@Test
public void shouldReturn400WhenTypeIsInvalid_WhenInvokingPostCommitHookMaterialUpdate() {
when(goConfigService.isUserAdmin(username)).thenReturn(true);
when(postCommitHookMaterialType.toType("some_invalid_type")).thenReturn(invalidMaterialType);
final HashMap params = new HashMap();
params.put(MaterialUpdateService.TYPE, "some_invalid_type");
service.notifyMaterialsForUpdate(username, params, result);
HttpLocalizedOperationResult badRequestResult = new HttpLocalizedOperationResult();
badRequestResult.badRequest(LocalizedMessage.string("API_BAD_REQUEST"));
assertThat(result, is(badRequestResult));
verify(goConfigService).isUserAdmin(username);
}
@Test
public void shouldReturn404WhenThereAreNoMaterialsToSchedule_WhenInvokingPostCommitHookMaterialUpdate() {
when(goConfigService.isUserAdmin(username)).thenReturn(true);
PostCommitHookMaterialType materialType = mock(PostCommitHookMaterialType.class);
when(postCommitHookMaterialType.toType("type")).thenReturn(materialType);
PostCommitHookImplementer hookImplementer = mock(PostCommitHookImplementer.class);
when(materialType.getImplementer()).thenReturn(hookImplementer);
when(materialType.isKnown()).thenReturn(true);
CruiseConfig config = mock(BasicCruiseConfig.class);
when(goConfigService.currentCruiseConfig()).thenReturn(config);
when(config.getGroups()).thenReturn(new PipelineGroups());
when(hookImplementer.prune(anySet(), anyMap())).thenReturn(new HashSet<Material>());
final HashMap params = new HashMap();
params.put(MaterialUpdateService.TYPE, "type");
service.notifyMaterialsForUpdate(username, params, result);
HttpLocalizedOperationResult operationResult = new HttpLocalizedOperationResult();
operationResult.notFound(LocalizedMessage.string("MATERIAL_SUITABLE_FOR_NOTIFICATION_NOT_FOUND"), HealthStateType.general(HealthStateScope.GLOBAL));
assertThat(result, is(operationResult));
verify(hookImplementer).prune(anySet(), anyMap());
}
@Test
public void shouldReturnImplementerOfSvnPostCommitHookAndPerformMaterialUpdate_WhenInvokingPostCommitHookMaterialUpdate() {
final HashMap params = new HashMap();
params.put(MaterialUpdateService.TYPE, "svn");
when(goConfigService.isUserAdmin(username)).thenReturn(true);
final CruiseConfig cruiseConfig = new BasicCruiseConfig(PipelineConfigMother.createGroup("groupName", "pipeline1", "pipeline2"));
when(goConfigService.currentCruiseConfig()).thenReturn(cruiseConfig);
when(postCommitHookMaterialType.toType("svn")).thenReturn(validMaterialType);
final PostCommitHookImplementer svnPostCommitHookImplementer = mock(PostCommitHookImplementer.class);
final Material svnMaterial = mock(Material.class);
when(svnPostCommitHookImplementer.prune(anySet(), eq(params))).thenReturn(new HashSet(Arrays.asList(svnMaterial)));
when(validMaterialType.getImplementer()).thenReturn(svnPostCommitHookImplementer);
service.notifyMaterialsForUpdate(username, params, result);
verify(svnPostCommitHookImplementer).prune(anySet(), eq(params));
Mockito.verify(queue, times(1)).post(matchMaterialUpdateMessage(svnMaterial));
HttpLocalizedOperationResult acceptedResult = new HttpLocalizedOperationResult();
acceptedResult.accepted(LocalizedMessage.string("MATERIAL_SCHEDULE_NOTIFICATION_ACCEPTED"));
assertThat(result, is(acceptedResult));
}
@Test
public void shouldUpdateServerHealthMessageWhenHung() {
//given
service = spy(service);
systemEnvironment.set(SystemEnvironment.MATERIAL_UPDATE_INACTIVE_TIMEOUT, 1);
ProcessManager processManager = mock(ProcessManager.class);
Material material = mock(Material.class);
service.updateMaterial(material);
when(service.getProcessManager()).thenReturn(processManager);
when(material.getFingerprint()).thenReturn("fingerprint");
when(material.getUriForDisplay()).thenReturn("uri");
when(material.getLongDescription()).thenReturn("details to uniquely identify a material");
when(material.isAutoUpdate()).thenReturn(true);
when(processManager.getIdleTimeFor("fingerprint")).thenReturn(60010L);
//when
service.updateMaterial(material);
//then
verify(serverHealthService).removeByScope(HealthStateScope.forMaterialUpdate(material));
ArgumentCaptor<ServerHealthState> argumentCaptor = ArgumentCaptor.forClass(ServerHealthState.class);
verify(serverHealthService).update(argumentCaptor.capture());
assertThat(argumentCaptor.getValue().getMessage(), is("Material update for uri hung:"));
assertThat(argumentCaptor.getValue().getDescription(),
is("Material update is currently running but has not shown any activity in the last 1 minute(s). This may be hung. Details - details to uniquely identify a material"));
assertThat(argumentCaptor.getValue().getType(), is(HealthStateType.general(HealthStateScope.forMaterialUpdate(material))));
}
@Test
public void shouldNotUpdateServerHealthMessageWhenIdleTimeLessThanConfigured() {
//given
service = spy(service);
systemEnvironment.set(SystemEnvironment.MATERIAL_UPDATE_INACTIVE_TIMEOUT, 2);
ProcessManager processManager = mock(ProcessManager.class);
Material material = mock(Material.class);
service.updateMaterial(material);
when(service.getProcessManager()).thenReturn(processManager);
when(material.getFingerprint()).thenReturn("fingerprint");
when(processManager.getIdleTimeFor("fingerprint")).thenReturn(60010L);
//when
service.updateMaterial(material);
//then
verify(serverHealthService, never()).removeByScope(HealthStateScope.forMaterialUpdate(material));
verify(serverHealthService, never()).update(Matchers.<ServerHealthState>any());
}
@Test
public void shouldRemoveServerHealthMessageOnMaterialUpdateCompletion() {
Material material = mock(Material.class);
when(material.getFingerprint()).thenReturn("fingerprint");
service.onMessage(new MaterialUpdateCompletedMessage(material, 0));
verify(serverHealthService).removeByScope(HealthStateScope.forMaterialUpdate(material));
}
@Test
public void shouldNotifyAllMaterialUpdateCompleteListenersOnMaterialUpdate() {
Material material = mock(Material.class);
service.onMessage(new MaterialUpdateCompletedMessage(material, 0));
verify(dependencyMaterialUpdateNotifier).onMaterialUpdate(material);
verify(scmMaterialSource).onMaterialUpdate(material);
}
@Test
public void shouldMaterialUpdateShouldNotBeInProgressIfUpdateMaterialMessagePostFails() {
doThrow(new RuntimeException("failed")).when(queue).post(matchMaterialUpdateMessage(svnMaterial));
try {
service.updateMaterial(svnMaterial);
fail("Should have failed");
} catch (RuntimeException e) {
// should re-throw exception
}
Map<Material, Date> inProgress = (Map<Material, Date>) ReflectionUtil.getField(service, "inProgress");
assertThat(inProgress.containsKey(svnMaterial), is(false));
}
}