/* * Copyright 2016 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.config; import com.thoughtworks.go.config.commands.CheckedUpdateCommand; import com.thoughtworks.go.config.commands.EntityConfigUpdateCommand; import com.thoughtworks.go.config.exceptions.ConfigFileHasChangedException; import com.thoughtworks.go.config.materials.MaterialConfigs; import com.thoughtworks.go.config.materials.mercurial.HgMaterialConfig; import com.thoughtworks.go.config.update.ConfigUpdateCheckFailedException; import com.thoughtworks.go.domain.NullTask; import com.thoughtworks.go.helper.ConfigFileFixture; import com.thoughtworks.go.helper.PipelineMother; import com.thoughtworks.go.helper.StageConfigMother; import com.thoughtworks.go.server.domain.Username; import com.thoughtworks.go.util.GoConfigFileHelper; import com.thoughtworks.go.util.LogFixture; import com.thoughtworks.go.util.Procedure; import com.thoughtworks.go.util.ReflectionUtil; import org.apache.commons.io.FileUtils; import org.apache.log4j.Level; import org.junit.Test; import java.io.File; import static com.thoughtworks.go.config.PipelineConfigs.DEFAULT_GROUP; import static com.thoughtworks.go.helper.ConfigFileFixture.*; import static com.thoughtworks.go.util.DataStructureUtils.a; import static com.thoughtworks.go.util.LogFixture.logFixtureFor; import static com.thoughtworks.go.util.TestUtils.assertContains; import static com.thoughtworks.go.util.TestUtils.sizeIs; import static org.hamcrest.Matchers.*; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.*; import static org.mockito.Mockito.*; public abstract class GoConfigDaoTestBase { protected GoConfigFileHelper configHelper; protected GoConfigDao goConfigDao; protected CachedGoConfig cachedGoConfig; @Test public void shouldCreateCruiseConfigFromBasicConfigFile() throws Exception { CruiseConfig cruiseConfig = GoConfigFileHelper.load(WITH_3_AGENT_CONFIG); assertThat(cruiseConfig, is(notNullValue())); PipelineConfig pipelineConfig = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")); assertThat(pipelineConfig.size(), is(1)); StageConfig stageConfig = pipelineConfig.get(0); assertThat(stageConfig.name(), is(new CaseInsensitiveString("mingle"))); assertThat(pipelineConfig.materialConfigs(), is(notNullValue())); final JobConfig cardList = stageConfig.jobConfigByInstanceName("cardlist", true); assertThat(cardList.name(), is(new CaseInsensitiveString("cardlist"))); assertThat(stageConfig.jobConfigByInstanceName("bluemonkeybutt", true).name(), is(new CaseInsensitiveString("bluemonkeybutt"))); assertThat(cardList.tasks(), sizeIs(1)); assertThat(cardList.tasks().first(), instanceOf(NullTask.class)); } @Test public void shouldGetAgents() throws Exception { CruiseConfig cruiseConfig = GoConfigFileHelper.load(WITH_3_AGENT_CONFIG); Agents agents = cruiseConfig.agents(); assertThat(agents.size(), is(3)); final AgentConfig approvedAgentConfig = agents.getAgentByUuid("3"); assertThat(approvedAgentConfig.getHostname(), is("test3.com")); assertThat(approvedAgentConfig.getIpAddress(), is("192.168.0.3")); assertThat(approvedAgentConfig.getResources().toString(), is("jdk1.4")); final AgentConfig deniedAgentConfig = agents.getAgentByUuid("2"); assertThat(deniedAgentConfig.isDisabled(), is(true)); } @Test public void shouldThrowExceptionIfFileIsInvalid() throws Exception { try { useConfigString("invalid config file"); goConfigDao.load(); fail("Should have thrown a parse exception"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("Content is not allowed in prolog.")); } } @Test public void shouldGetArtifactsFromBuildPlan() throws Exception { CruiseConfig cruiseConfig = GoConfigFileHelper.load(WITH_3_AGENT_CONFIG); final ArtifactPlans cardListArtifacts = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true).artifactPlans(); assertThat(cardListArtifacts.size(), is(0)); assertThat(cruiseConfig.jobConfigByName("pipeline1", "mingle", "bluemonkeybutt", true).artifactPlans().size(), is(1)); } @Test public void shouldAddPipelineToConfigFile() throws Exception { CruiseConfig cruiseConfig = goConfigDao.load(); int oldsize = cruiseConfig.numberOfPipelines(); PipelineConfig pipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("spring", "ut", "www.spring.com"); goConfigDao.addPipeline(pipelineConfig, DEFAULT_GROUP); cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.numberOfPipelines(), is(oldsize + 1)); assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("spring")), is(pipelineConfig)); } @Test public void shouldFailToAddDuplicatePipelineToConfigFile() throws Exception { CruiseConfig cruiseConfig = goConfigDao.load(); int oldsize = cruiseConfig.numberOfPipelines(); PipelineConfig pipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("spring", "ut", "www.spring.com"); goConfigDao.addPipeline(pipelineConfig, DEFAULT_GROUP); cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.numberOfPipelines(), is(oldsize + 1)); assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("spring")), is(pipelineConfig)); PipelineConfig dupPipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("spring", "ut", "www.spring.com"); try { goConfigDao.addPipeline(dupPipelineConfig, DEFAULT_GROUP); } catch (RuntimeException ex) { assertThat(ex.getMessage(), is("You have defined multiple pipelines called 'spring'. Pipeline names must be unique.")); return; } fail("Should have thrown"); } @Test public void shouldFailWhenConfigUpdateCannotBeMergedWithLatestRevision() throws Exception { final String originalMd5 = goConfigDao.load().getMd5(); goConfigDao.updateConfig(configHelper.addPipelineCommand(originalMd5, "p1", "stage1", "build1")); final String md5WhenPipelineIsAdded = goConfigDao.load().getMd5(); goConfigDao.updateConfig(configHelper.changeJobNameCommand(md5WhenPipelineIsAdded, "p1", "stage1", "build1", "new_build")); try { goConfigDao.updateConfig(new NoOverwriteUpdateConfigCommand() { public String unmodifiedMd5() { return md5WhenPipelineIsAdded; } public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { deletePipeline(cruiseConfig); return cruiseConfig; } private void deletePipeline(CruiseConfig cruiseConfig) { cruiseConfig.getGroups().get(0).remove(0); } }); fail("should not have allowed no-overwrite stale update"); } catch (RuntimeException e) { assertThat(e.getMessage(), is(ConfigFileHasChangedException.CONFIG_CHANGED_PLEASE_REFRESH)); } } @Test public void shouldNotFailNoOverwriteUpdateWhenEditingUnmodifiedCopy() throws Exception { final String md5 = goConfigDao.md5OfConfigFile(); try { ConfigSaveState configSaveState = goConfigDao.updateConfig(new NoOverwriteUpdateConfigCommand() { public String unmodifiedMd5() { return md5; } public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.getEnvironments().add(new BasicEnvironmentConfig(new CaseInsensitiveString("foo"))); return cruiseConfig; } }); assertThat(configSaveState, is(ConfigSaveState.UPDATED)); } catch (RuntimeException e) { fail("should not have failed for edit on unmodified config."); } } @Test public void shouldNotFailUpdateWithOverwritePermittedWhenEditingStaleCopy() throws Exception { try { goConfigDao.updateConfig(new UpdateConfigCommand() { public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.getEnvironments().add(new BasicEnvironmentConfig(new CaseInsensitiveString("foo"))); return cruiseConfig; } }); } catch (RuntimeException e) { fail("should not have failed for edit when overwrite allowed."); } } @Test public void shouldFeedCloneOfConfigBackToCommand() throws Exception { CheckedTestUpdateCommand command = new CheckedTestUpdateCommand(cachedGoConfig.loadForEditing().getMd5(), true) { @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { PipelineConfig pipelineConfig = new PipelineConfig(new CaseInsensitiveString("foo"), "#{bar}-${COUNT}", null, false, new MaterialConfigs(new HgMaterialConfig("url", null)), a(StageConfigMother.custom("stage", "job"))); pipelineConfig.addParam(new ParamConfig("bar", "baz")); cruiseConfig.addPipeline("my-group", pipelineConfig); return cruiseConfig; } }; goConfigDao.updateConfig(command); assertThat(command.after.pipelineConfigByName(new CaseInsensitiveString("foo")).getLabelTemplate(), is("baz-${COUNT}")); assertThat(command.after.getEnvironments().size(), is(0)); command.after.addEnvironment("bar"); assertThat(cachedGoConfig.currentConfig().getEnvironments().size(), is(0)); } @Test public void shouldNotUpdateIfCannotContinueIfTheCommandIsPreprocessable() throws Exception { CheckedTestUpdateCommand command = new CheckedTestUpdateCommand(cachedGoConfig.loadForEditing().getMd5(), false); try { goConfigDao.updateConfig(command); fail("should have failed as check returned false"); } catch (ConfigUpdateCheckFailedException ignored) { } assertThat(command.wasUpdated, is(false)); assertThat(command.after, not(nullValue())); } @Test public void shouldPerformUpdateIfCanContinue() throws Exception { CheckedTestUpdateCommand command = new CheckedTestUpdateCommand(cachedGoConfig.loadForEditing().getMd5(), true); goConfigDao.updateConfig(command); assertThat(command.wasUpdated, is(true)); } @Test public void shouldBePassedTheLatestCruiseConfigWhileCheckingBeforeUpdate() { configHelper.addTemplate("my-template", "my-stage"); configHelper.addPipeline("pipeline", "stage"); configHelper.addPipelineWithTemplate(PipelineConfigs.DEFAULT_GROUP, "my-pipeline", "my-template"); CheckedTestUpdateCommand command = spy(new CheckedTestUpdateCommand(cachedGoConfig.loadForEditing().getMd5(), true)); goConfigDao.updateConfig(command); verify(command).canContinue(cachedGoConfig.currentConfig()); } @Test public void shouldAddEnvironmentToConfigFile() throws Exception { CruiseConfig cruiseConfig = goConfigDao.load(); int oldsize = cruiseConfig.getEnvironments().size(); goConfigDao.addEnvironment(new BasicEnvironmentConfig(new CaseInsensitiveString("foo-environment"))); cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.getEnvironments().size(), is(oldsize + 1)); } @Test public void shouldAddPipelineOnTheTopOfSameGroupWhenGivenGroupExist() throws Exception { PipelineConfig springConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("spring", "ut", "www.spring.com"); PipelineConfig mingleConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("mingle", "ut", "www.spring.com"); goConfigDao.addPipeline(springConfig, "group1"); goConfigDao.addPipeline(mingleConfig, "group1"); CruiseConfig cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.numbersOfPipeline("group1"), is(2)); assertThat(cruiseConfig.find("group1", 0), is(mingleConfig)); } @Test public void shouldAddPipelineToTheNewGroupWhenGivenGroupDoesNotExist() throws Exception { PipelineConfig springConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("spring", "ut", "www.spring.com"); PipelineConfig mingleConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("mingle", "ut", "www.spring.com"); goConfigDao.addPipeline(springConfig, "group1"); goConfigDao.addPipeline(mingleConfig, "group"); CruiseConfig cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.numbersOfPipeline("group1"), is(1)); assertThat(cruiseConfig.numbersOfPipeline("group"), is(1)); } @Test public void shouldAddPipelineToDefaultGroupWhenNoGroupNameSpecified() throws Exception { PipelineConfig springConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("spring", "ut", "www.spring.com"); goConfigDao.addPipeline(springConfig, null); CruiseConfig cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.numbersOfPipeline(DEFAULT_GROUP), is(1)); } @Test public void shouldAddPipelineToTheTopOfConfigFile() throws Exception { goConfigDao.load(); PipelineConfig pipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("addedFirst", "ut", "www.spring.com"); PipelineConfig pipelineConfig2 = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("addedSecond", "ut", "www.spring.com"); goConfigDao.addPipeline(pipelineConfig, DEFAULT_GROUP); goConfigDao.addPipeline(pipelineConfig2, DEFAULT_GROUP); goConfigDao.load(); final File configFile = new File(goConfigDao.fileLocation()); final String content = FileUtils.readFileToString(configFile); final int indexOfSecond = content.indexOf("addedSecond"); final int indexOfFirst = content.indexOf("addedFirst"); assertThat(indexOfSecond, is(not(-1))); assertThat(indexOfFirst, is(not(-1))); assertTrue(indexOfSecond < indexOfFirst); } @Test public void shouldNotAddInvalidPipelineToConfigFile() throws Exception { CruiseConfig cruiseConfig = goConfigDao.load(); int oldsize = cruiseConfig.numberOfPipelines(); PipelineConfig pipelineConfig = PipelineMother.twoBuildPlansWithResourcesAndSvnMaterialsAtUrl("", "ut", "www.spring.com"); try { goConfigDao.addPipeline(pipelineConfig, DEFAULT_GROUP); fail(); } catch (Exception ignored) { } cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.numberOfPipelines(), is(oldsize)); } @Test public void shouldOverwriteConfigContentAfterSave() throws Exception { useConfigString(WITH_3_AGENT_CONFIG); cachedGoConfig.save(CONFIG_WITH_ANT_BUILDER, false); CruiseConfig cruiseConfig = goConfigDao.load(); assertThat(cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true).tasks().size(), is(1)); } @Test public void shouldNotChangeCurrentConfigIfInvalid() throws Exception { useConfigString(WITH_3_AGENT_CONFIG); CruiseConfig cruiseConfig = goConfigDao.load(); try { cachedGoConfig.save("This is invalid Cruise", false); fail(); } catch (Exception ignored) { } assertCurrentConfigIs(cruiseConfig); } @Test public void shouldNotAllowTypeForArtifactsBecausePolymorphismIsUsedInstead() throws Exception { try { cachedGoConfig.save(INVALID_CONFIG_WITH_TYPE_FOR_ARTIFACT, false); fail(); } catch (Exception e) { assertContains(e.toString(), "'type' is not allowed"); } } @Test public void shouldNotAllowOldXml() throws Exception { try { cachedGoConfig.save(ConfigFileFixture.VERSION_5, false); fail(); } catch (Exception e) { assertThat(e.getMessage(), containsString("Value '5' of attribute 'schemaVersion' of element 'cruise' is not valid")); } } @Test public void shouldLogAnyErrorMessageIncludingTheValidationError() throws Exception { try (LogFixture logger = logFixtureFor(GoFileConfigDataSource.class, Level.DEBUG)) { try { cachedGoConfig.save(INVALID_CONFIG_WITH_TYPE_FOR_ARTIFACT, false); fail(); } catch (Exception e) { assertThat(logger.getLog(), containsString( "'type' is not allowed to appear in element 'test'.")); } } } @Test public void should_NOT_allowUpdateOf_serverId() throws Exception { useConfigString(ConfigFileFixture.CRUISE); String oldServerId = goConfigDao.load().server().getServerId(); Exception ex = null; try { GoConfigFileHelper.withServerIdImmutability(new Procedure() { public void call() { goConfigDao.updateConfig(new UpdateConfigCommand() { public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { ReflectionUtil.setField(cruiseConfig.server(), "serverId", "new-value"); return cruiseConfig; } }); } }); fail("should not save with modified serverId"); } catch (Exception e) { ex = e; } assertThat(ex.getMessage(), is("The value of 'serverId' uniquely identifies a Go server instance. This field cannot be modified.")); CruiseConfig config = goConfigDao.load(); assertThat(config.server().getServerId(), is(oldServerId)); } @Test public void shouldNotConfigMultipleTrackingTools() throws Exception { try { useConfigString(INVALID_CONFIG_WITH_MULTIPLE_TRACKINGTOOLS); } catch (Exception e) { assertThat(e.getMessage(), containsString("Invalid content was found starting with element 'trackingtool'. One of '{timer, environmentvariables, dependencies, materials}")); } } @Test public void shouldMergeWithLatestConfigWhenConfigUpdatedWithOlderMd5() { configHelper.addMailHost(getMailhost("mailhost.local.old")); final String oldMd5 = goConfigDao.md5OfConfigFile(); configHelper.addMailHost(getMailhost("mailhost.local")); ConfigSaveState configSaveState = goConfigDao.updateConfig(configHelper.addPipelineCommand(oldMd5, "p2", "stage1", "build1")); CruiseConfig updatedConfig = goConfigDao.load(); assertThat(updatedConfig.hasPipelineNamed(new CaseInsensitiveString("p2")), is(true)); assertThat(updatedConfig.mailHost().getHostName(), is("mailhost.local")); assertThat(configSaveState, is(ConfigSaveState.MERGED)); } @Test public void shouldNotUpdatePipelineConfigIfUserDoesNotHaveRequiredPermissionsToDoSo() { CachedGoConfig cachedConfigService = mock(CachedGoConfig.class); CruiseConfig cruiseConfig = mock(CruiseConfig.class); when(cachedConfigService.currentConfig()).thenReturn(cruiseConfig); goConfigDao = new GoConfigDao(cachedConfigService); EntityConfigUpdateCommand command = mock(EntityConfigUpdateCommand.class); when(command.canContinue(cruiseConfig)).thenReturn(false); try { goConfigDao.updateConfig(command, new Username(new CaseInsensitiveString("user"))); fail("Expected to throw exception of type:" + ConfigUpdateCheckFailedException.class.getName()); } catch (Exception e) { assertTrue(e instanceof ConfigUpdateCheckFailedException); } verify(cachedConfigService).currentConfig(); verifyNoMoreInteractions(cachedConfigService); } @Test public void shouldUpdateValidEntity() { CachedGoConfig cachedConfigService = mock(CachedGoConfig.class); CruiseConfig cruiseConfig = mock(CruiseConfig.class); when(cachedConfigService.currentConfig()).thenReturn(cruiseConfig); EntityConfigUpdateCommand saveCommand = mock(EntityConfigUpdateCommand.class); when(saveCommand.isValid(cruiseConfig)).thenReturn(true); when(saveCommand.canContinue(cruiseConfig)).thenReturn(true); goConfigDao = new GoConfigDao(cachedConfigService); Username currentUser = new Username(new CaseInsensitiveString("user")); goConfigDao.updateConfig(saveCommand, currentUser); verify(cachedConfigService).writeEntityWithLock(saveCommand, currentUser); } private void assertCurrentConfigIs(CruiseConfig cruiseConfig) throws Exception { CruiseConfig currentConfig = goConfigDao.load(); assertThat(currentConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).size(), is(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).size())); } public void useConfigString(String config) throws Exception { configHelper.writeXmlToConfigFile(ConfigMigrator.migrate(config)); } class CheckedTestUpdateCommand implements NoOverwriteUpdateConfigCommand, CheckedUpdateCommand, ConfigAwareUpdate { private final String md5; private final boolean canContinue; private boolean wasUpdated; private CruiseConfig after; CheckedTestUpdateCommand(String md5, boolean canContinue) { this.md5 = md5; this.canContinue = canContinue; } public boolean canContinue(CruiseConfig cruiseConfig) { return canContinue; } public String unmodifiedMd5() { return md5; } public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { wasUpdated = true; return cruiseConfig; } public void afterUpdate(CruiseConfig cruiseConfig) { after = cruiseConfig; } public CruiseConfig configAfter() { return after; } } private MailHost getMailhost(String hostname) { return new MailHost(hostname, 9999, "user", "password", true, false, "from@local", "admin@local"); } }