/* * 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.config; import com.googlecode.junit.ext.JunitExtRunner; import com.googlecode.junit.ext.RunIf; import com.thoughtworks.go.config.elastic.ElasticProfile; import com.thoughtworks.go.config.exceptions.GoConfigInvalidException; import com.thoughtworks.go.config.materials.*; import com.thoughtworks.go.config.materials.Filter; import com.thoughtworks.go.config.materials.git.GitMaterialConfig; import com.thoughtworks.go.config.materials.mercurial.HgMaterialConfig; import com.thoughtworks.go.config.materials.perforce.P4MaterialConfig; import com.thoughtworks.go.config.materials.svn.SvnMaterialConfig; import com.thoughtworks.go.config.materials.tfs.TfsMaterialConfig; import com.thoughtworks.go.config.merge.MergeConfigOrigin; import com.thoughtworks.go.config.pluggabletask.PluggableTask; import com.thoughtworks.go.config.preprocessor.ConfigParamPreprocessor; import com.thoughtworks.go.config.preprocessor.ConfigRepoPartialPreprocessor; import com.thoughtworks.go.config.remote.ConfigOrigin; import com.thoughtworks.go.config.remote.ConfigRepoConfig; import com.thoughtworks.go.config.remote.FileConfigOrigin; import com.thoughtworks.go.config.remote.PartialConfig; import com.thoughtworks.go.config.server.security.ldap.BaseConfig; import com.thoughtworks.go.config.validation.*; import com.thoughtworks.go.domain.*; import com.thoughtworks.go.domain.config.Admin; import com.thoughtworks.go.domain.config.Configuration; import com.thoughtworks.go.domain.config.ConfigurationProperty; import com.thoughtworks.go.domain.config.RepositoryMetadataStoreHelper; import com.thoughtworks.go.domain.label.PipelineLabel; import com.thoughtworks.go.domain.materials.MaterialConfig; import com.thoughtworks.go.domain.packagerepository.PackageDefinition; import com.thoughtworks.go.domain.packagerepository.PackageRepository; import com.thoughtworks.go.helper.ConfigFileFixture; import com.thoughtworks.go.helper.MaterialConfigsMother; import com.thoughtworks.go.helper.StageConfigMother; import com.thoughtworks.go.junitext.EnhancedOSChecker; import com.thoughtworks.go.plugin.access.packagematerial.PackageConfiguration; import com.thoughtworks.go.plugin.access.packagematerial.PackageConfigurations; import com.thoughtworks.go.plugin.access.packagematerial.PackageMetadataStore; import com.thoughtworks.go.plugin.access.packagematerial.RepositoryMetadataStore; import com.thoughtworks.go.plugin.access.pluggabletask.PluggableTaskConfigStore; import com.thoughtworks.go.plugin.access.pluggabletask.TaskPreference; import com.thoughtworks.go.plugin.api.config.Property; import com.thoughtworks.go.plugin.api.material.packagerepository.PackageMaterialProperty; import com.thoughtworks.go.plugin.api.material.packagerepository.RepositoryConfiguration; import com.thoughtworks.go.plugin.api.response.validation.ValidationResult; import com.thoughtworks.go.plugin.api.task.TaskConfig; import com.thoughtworks.go.plugin.api.task.TaskExecutor; import com.thoughtworks.go.plugin.api.task.TaskView; import com.thoughtworks.go.security.GoCipher; import com.thoughtworks.go.util.*; import com.thoughtworks.go.util.command.HgUrlArgument; import com.thoughtworks.go.util.command.UrlArgument; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.Transformer; import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; import org.hamcrest.core.Is; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; import static com.thoughtworks.go.helper.ConfigFileFixture.CONFIG_WITH_ANT_BUILDER; import static com.thoughtworks.go.helper.ConfigFileFixture.CONFIG_WITH_NANT_AND_EXEC_BUILDER; import static com.thoughtworks.go.junitext.EnhancedOSChecker.DO_NOT_RUN_ON; import static com.thoughtworks.go.junitext.EnhancedOSChecker.WINDOWS; import static com.thoughtworks.go.plugin.api.config.Property.*; import static com.thoughtworks.go.util.GoConstants.CONFIG_SCHEMA_VERSION; import static com.thoughtworks.go.util.TestUtils.sizeIs; import static java.lang.String.format; import static java.util.Arrays.asList; import static org.apache.commons.collections.CollectionUtils.collect; import static org.apache.commons.io.IOUtils.toInputStream; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsCollectionContaining.hasItem; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.*; @RunWith(JunitExtRunner.class) public class MagicalGoConfigXmlLoaderTest { private MagicalGoConfigXmlLoader xmlLoader; static final String INVALID_DESTINATION_DIRECTORY_MESSAGE = "Invalid Destination Directory. Every material needs a different destination directory and the directories should not be nested"; private ConfigCache configCache = new ConfigCache(); @Before public void setup() throws Exception { RepositoryMetadataStoreHelper.clear(); xmlLoader = new MagicalGoConfigXmlLoader(configCache, ConfigElementImplementationRegistryMother.withNoPlugins()); } @After public void tearDown() throws Exception { RepositoryMetadataStoreHelper.clear(); } @Test public void shouldLoadConfigFile() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.CONFIG).config; PipelineConfig pipelineConfig1 = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")); assertThat(pipelineConfig1.size(), is(2)); assertThat(pipelineConfig1.getLabelTemplate(), is(PipelineLabel.COUNT_TEMPLATE)); StageConfig stage1 = pipelineConfig1.get(0); assertThat(stage1.name(), is(new CaseInsensitiveString("stage1"))); assertThat(stage1.allBuildPlans().size(), is(1)); assertThat("Should require approval", stage1.requiresApproval(), is(true)); AdminsConfig admins = stage1.getApproval().getAuthConfig(); assertThat(admins, hasItem((Admin) new AdminRole(new CaseInsensitiveString("admin")))); assertThat(admins, hasItem((Admin) new AdminRole(new CaseInsensitiveString("qa_lead")))); assertThat(admins, hasItem((Admin) new AdminUser(new CaseInsensitiveString("jez")))); StageConfig stage2 = pipelineConfig1.get(1); assertThat("Should not require approval", stage2.requiresApproval(), is(false)); JobConfig plan = stage1.jobConfigByInstanceName("plan1", true); assertThat(plan.name(), is(new CaseInsensitiveString("plan1"))); assertThat(plan.resources(), is(new Resources("tiger, lion"))); assertThat(plan.getTabs().size(), is(2)); assertThat(plan.getTabs().first().getName(), is("Emma")); assertThat(plan.getTabs().first().getPath(), is("logs/emma/index.html")); assertThat(pipelineConfig1.materialConfigs().size(), is(1)); shouldBeSvnMaterial(pipelineConfig1.materialConfigs().first()); PipelineConfig pipelineConfig2 = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline2")); shouldBeHgMaterial(pipelineConfig2.materialConfigs().first()); PipelineConfig pipelineConfig3 = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline3")); MaterialConfig p4Material = pipelineConfig3.materialConfigs().first(); shouldBeP4Material(p4Material); PipelineConfig pipelineConfig4 = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline4")); shouldBeGitMaterial(pipelineConfig4.materialConfigs().first()); } @Test public void shouldLoadConfigWithConfigRepo() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.ONE_CONFIG_REPO).config; assertThat(cruiseConfig.getConfigRepos().size(), is(1)); ConfigRepoConfig configRepo = cruiseConfig.getConfigRepos().get(0); assertThat(configRepo.getMaterialConfig(), Is.<MaterialConfig>is(new GitMaterialConfig("https://github.com/tomzo/gocd-indep-config-part.git"))); } @Test public void shouldLoadConfigWithConfigRepoAndPluginName() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.configWithConfigRepos( " <config-repos>\n" + " <config-repo plugin=\"myplugin\">\n" + " <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n" + " </config-repo >\n" + " </config-repos>\n" )).config; assertThat(cruiseConfig.getConfigRepos().size(), is(1)); ConfigRepoConfig configRepo = cruiseConfig.getConfigRepos().get(0); assertThat(configRepo.getConfigProviderPluginName(), is("myplugin")); } @Test public void shouldLoadConfigWith2ConfigRepos() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.configWithConfigRepos( " <config-repos>\n" + " <config-repo plugin=\"myplugin\">\n" + " <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n" + " </config-repo >\n" + " <config-repo plugin=\"myplugin\">\n" + " <git url=\"https://github.com/tomzo/gocd-refmain-config-part.git\" />\n" + " </config-repo >\n" + " </config-repos>\n" )).config; assertThat(cruiseConfig.getConfigRepos().size(), is(2)); ConfigRepoConfig configRepo1 = cruiseConfig.getConfigRepos().get(0); assertThat(configRepo1.getMaterialConfig(), Is.<MaterialConfig>is(new GitMaterialConfig("https://github.com/tomzo/gocd-indep-config-part.git"))); ConfigRepoConfig configRepo2 = cruiseConfig.getConfigRepos().get(1); assertThat(configRepo2.getMaterialConfig(), Is.<MaterialConfig>is(new GitMaterialConfig("https://github.com/tomzo/gocd-refmain-config-part.git"))); } @Test public void shouldLoadConfigWithConfigRepoAndConfiguration() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.configWithConfigRepos( " <config-repos>\n" + " <config-repo >\n" + " <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n" + " <configuration>\n" + " <property>\n" + " <key>pattern</key>\n" + " <value>*.gocd.xml</value>\n" + " </property>\n" + " </configuration>\n" + " </config-repo >\n" + " </config-repos>\n" )).config; assertThat(cruiseConfig.getConfigRepos().size(), is(1)); ConfigRepoConfig configRepo = cruiseConfig.getConfigRepos().get(0); assertThat(configRepo.getConfiguration().size(), is(1)); assertThat(configRepo.getConfiguration().getProperty("pattern").getValue(), is("*.gocd.xml")); } @Test(expected = XsdValidationException.class) public void shouldThrowXsdValidationException_WhenNoRepository() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.configWithConfigRepos( " <config-repos>\n" + " <config-repo plugin=\"myplugin\">\n" + " </config-repo >\n" + " </config-repos>\n" )).config; } @Test(expected = XsdValidationException.class) public void shouldThrowXsdValidationException_When2RepositoriesInSameConfigElement() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.configWithConfigRepos( " <config-repos>\n" + " <config-repo plugin=\"myplugin\">\n" + " <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n" + " <git url=\"https://github.com/tomzo/gocd-refmain-config-part.git\" />\n" + " </config-repo >\n" + " </config-repos>\n" )).config; } @Test(expected = GoConfigInvalidException.class) public void shouldFailValidation_WhenSameMaterialUsedBy2ConfigRepos() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.configWithConfigRepos( " <config-repos>\n" + " <config-repo plugin=\"myplugin\">\n" + " <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n" + " </config-repo >\n" + " <config-repo plugin=\"myotherplugin\">\n" + " <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n" + " </config-repo >\n" + " </config-repos>\n" )).config; } private ConfigRepoPartialPreprocessor findConfigRepoPartialPreprocessor() { List<GoConfigPreprocessor> preprocessors = MagicalGoConfigXmlLoader.PREPROCESSORS; for (GoConfigPreprocessor preprocessor : preprocessors) { if (preprocessor instanceof ConfigRepoPartialPreprocessor) return (ConfigRepoPartialPreprocessor) preprocessor; } return null; } @Test public void shouldSetConfigOriginInCruiseConfig_AfterLoadingConfigFile() throws Exception { GoConfigHolder goConfigHolder = xmlLoader.loadConfigHolder(ConfigFileFixture.CONFIG, new MagicalGoConfigXmlLoader.Callback() { @Override public void call(CruiseConfig cruiseConfig) { cruiseConfig.setPartials(asList(new PartialConfig())); } }); assertThat(goConfigHolder.config.getOrigin(), Is.<ConfigOrigin>is(new MergeConfigOrigin())); assertThat(goConfigHolder.configForEdit.getOrigin(), Is.<ConfigOrigin>is(new FileConfigOrigin())); } @Test public void shouldSetConfigOriginInPipeline_AfterLoadingConfigFile() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigFileFixture.CONFIG).config; PipelineConfig pipelineConfig1 = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")); assertThat(pipelineConfig1.getOrigin(), Is.<ConfigOrigin>is(new FileConfigOrigin())); } @Test public void shouldSetConfigOriginInEnvironment_AfterLoadingConfigFile() { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <agents>" + " <physical uuid='1'/>" + " <physical uuid='2'/>" + " </agents>" + " </environment>" + "</environments>"); EnvironmentsConfig environmentsConfig = ConfigMigrator.loadWithMigration(content).config.getEnvironments(); EnvironmentConfig uat = environmentsConfig.get(0); assertThat(uat.getOrigin(), Is.<ConfigOrigin>is(new FileConfigOrigin())); } @Test public void shouldSupportMultipleAgentsFromSameBox() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ConfigMigrator.migrate(ConfigFileFixture.WITH_MULTIPLE_LOCAL_AGENT_CONFIG)).config; assertThat(cruiseConfig.agents().size(), is(2)); assertThat(cruiseConfig.agents().get(0).getHostname(), is(cruiseConfig.agents().get(1).getHostname())); assertThat(cruiseConfig.agents().get(0).getIpAddress(), is(cruiseConfig.agents().get(1).getIpAddress())); assertThat(cruiseConfig.agents().get(0).getUuid(), is(not(cruiseConfig.agents().get(1).getUuid()))); } @Test public void shouldLoadAntBuilder() throws Exception { CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(FileUtil.readToEnd(toInputStream(CONFIG_WITH_ANT_BUILDER))).config; JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true); assertThat(plan.tasks(), sizeIs(1)); AntTask builder = (AntTask) plan.tasks().first(); assertThat(builder.getTarget(), is("all")); final ArtifactPlans cardListArtifacts = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true).artifactPlans(); assertThat(cardListArtifacts.size(), is(1)); ArtifactPlan artifactPlan = cardListArtifacts.get(0); assertThat(artifactPlan.getArtifactType(), is(ArtifactType.unit)); } @Test public void shouldLoadNAntBuilder() throws Exception { CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(toInputStream( CONFIG_WITH_NANT_AND_EXEC_BUILDER)).config; JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true); BuildTask builder = (BuildTask) plan.tasks().findFirstByType(NantTask.class); assertThat(builder.getTarget(), is("all")); } @Test public void shouldLoadExecBuilder() throws Exception { CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(toInputStream(CONFIG_WITH_NANT_AND_EXEC_BUILDER)).config; JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true); ExecTask builder = (ExecTask) plan.tasks().findFirstByType(ExecTask.class); assertThat(builder, is(new ExecTask("ls", "-la", "workdir"))); builder = (ExecTask) plan.tasks().get(2); assertThat(builder, is(new ExecTask("ls", "", (String) null))); } @Test public void shouldLoadRakeBuilderWithEmptyOnCancel() throws Exception { CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(toInputStream(CONFIG_WITH_NANT_AND_EXEC_BUILDER)).config; JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true); RakeTask builder = (RakeTask) plan.tasks().findFirstByType(RakeTask.class); assertThat(builder, notNullValue()); } @Test public void shouldMigrateAnEmptyArtifactSourceToStar() throws Exception { GoConfigHolder holder = ConfigMigrator.loadWithMigration(toInputStream(ConfigFileFixture.configWithArtifactSourceAs(""))); CruiseConfig cruiseConfig = holder.config; JobConfig plan = cruiseConfig.jobConfigByName("pipeline", "stage", "job", true); assertThat(plan.artifactPlans().get(0).getSrc(), is("*")); } @Test public void shouldMigrateAnArtifactSourceWithJustWhitespaceToStar() throws Exception { GoConfigHolder holder = ConfigMigrator.loadWithMigration(toInputStream(ConfigFileFixture.configWithArtifactSourceAs(" \t "))); CruiseConfig cruiseConfig = holder.config; JobConfig plan = cruiseConfig.jobConfigByName("pipeline", "stage", "job", true); assertThat(plan.artifactPlans().get(0).getSrc(), is("*")); } @Test public void shouldRetainArtifactSourceThatIsNotWhitespace() throws Exception { GoConfigHolder holder = ConfigMigrator.loadWithMigration(toInputStream(ConfigFileFixture.configWithArtifactSourceAs("t "))); CruiseConfig cruiseConfig = holder.config; JobConfig plan = cruiseConfig.jobConfigByName("pipeline", "stage", "job", true); assertThat(plan.artifactPlans().get(0).getSrc(), is("t ")); } @Test public void shouldLoadBuildPlanFromXmlPartial() throws Exception { String buildXmlPartial = "<job name=\"functional\">\n" + " <artifacts>\n" + " <artifact src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + "</job>"; JobConfig build = xmlLoader.fromXmlPartial(toInputStream(buildXmlPartial), JobConfig.class); assertThat(build.name(), is(new CaseInsensitiveString("functional"))); assertThat(build.artifactPlans().size(), is(1)); } @Test public void shouldLoadIgnoresFromSvnPartial() throws Exception { String buildXmlPartial = "<svn url=\"file:///tmp/testSvnRepo/project1/trunk\" >\n" + " <filter>\n" + " <ignore pattern=\"x\"/>\n" + " </filter>\n" + " </svn>"; MaterialConfig svnMaterial = xmlLoader.fromXmlPartial(toInputStream(buildXmlPartial), SvnMaterialConfig.class); Filter parsedFilter = svnMaterial.filter(); Filter expectedFilter = new Filter(); expectedFilter.add(new IgnoredFiles("x")); assertThat(parsedFilter, is(expectedFilter)); } @Test public void shouldLoadIgnoresFromHgPartial() throws Exception { String buildXmlPartial = "<hg url=\"file:///tmp/testSvnRepo/project1/trunk\" >\n" + " <filter>\n" + " <ignore pattern=\"x\"/>\n" + " </filter>\n" + " </hg>"; MaterialConfig hgMaterial = xmlLoader.fromXmlPartial(toInputStream(buildXmlPartial), HgMaterialConfig.class); Filter parsedFilter = hgMaterial.filter(); Filter expectedFilter = new Filter(); expectedFilter.add(new IgnoredFiles("x")); assertThat(parsedFilter, is(expectedFilter)); } @Test public void shouldLoadMaterialWithAutoUpdate() throws Exception { MaterialConfig material = xmlLoader.fromXmlPartial("<hg url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"false\"/>", HgMaterialConfig.class); assertThat(material.isAutoUpdate(), is(false)); material = xmlLoader.fromXmlPartial("<git url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"false\"/>", GitMaterialConfig.class); assertThat(material.isAutoUpdate(), is(false)); material = xmlLoader.fromXmlPartial("<svn url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"false\"/>", SvnMaterialConfig.class); assertThat(material.isAutoUpdate(), is(false)); material = xmlLoader.fromXmlPartial("<p4 port='localhost:1666' autoUpdate='false' ><view/></p4>", P4MaterialConfig.class); assertThat(material.isAutoUpdate(), is(false)); } @Test public void autoUpdateShouldBeTrueByDefault() throws Exception { MaterialConfig hgMaterial = xmlLoader.fromXmlPartial("<hg url=\"file:///tmp/testSvnRepo/project1/trunk\"/>", HgMaterialConfig.class); assertThat(hgMaterial.isAutoUpdate(), is(true)); } @Test public void autoUpdateShouldUnderstandTrue() throws Exception { MaterialConfig hgMaterial = xmlLoader.fromXmlPartial("<hg url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"true\"/>", HgMaterialConfig.class); assertThat(hgMaterial.isAutoUpdate(), is(true)); } @Test public void shouldValidateBooleanAutoUpdateOnMaterials() throws Exception { String noAutoUpdate = " <materials>\n" + " <svn url=\"/hgrepo2\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertValid(noAutoUpdate); String validAutoUpdate = " <materials>\n" + " <svn url=\"/hgrepo2\" autoUpdate='true'/>\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertValid(validAutoUpdate); String invalidautoUpdate = " <materials>\n" + " <git url=\"/hgrepo2\" autoUpdate=\"fooo\"/>\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid("'fooo' is not a valid value for 'boolean'.", invalidautoUpdate); } @Test public void shouldInvalidateAutoUpdateOnDependencyMaterial() throws Exception { String noAutoUpdate = " <materials>\n" + " <pipeline pipelineName=\"pipeline\" stageName=\"stage\" autoUpdate=\"true\"/>\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid("Attribute 'autoUpdate' is not allowed to appear in element 'pipeline'.", noAutoUpdate); } @Test public void shouldInvalidateAutoUpdateIfTheSameMaterialHasDifferentValuesForAutoUpdate() throws Exception { String noAutoUpdate = " <materials>\n" + " <svn url=\"/hgrepo2\" autoUpdate='true' dest='first'/>\n" + " <svn url=\"/hgrepo2\" autoUpdate='false' dest='second'/>\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid( "Material of type Subversion (/hgrepo2) is specified more than once in the configuration with different values", noAutoUpdate); } @Test public void shouldLoadFromSvnPartial() throws Exception { String buildXmlPartial = "<svn url=\"http://foo.bar\" username=\"cruise\" password=\"password\" materialName=\"http___foo.bar\"/>"; MaterialConfig materialConfig = xmlLoader.fromXmlPartial(toInputStream(buildXmlPartial), SvnMaterialConfig.class); MaterialConfig svnMaterial = MaterialConfigsMother.svnMaterialConfig("http://foo.bar", null, "cruise", "password", false, null); assertThat(materialConfig, is(svnMaterial)); } @Test public void shouldAcceptLdapConfiguration_withNoManagerDn() throws Exception { String ldapCfg = "<ldap uri=\"foo\" searchFilter=\"filter\" >" + "<bases><base>base1</base></bases>" + "</ldap>"; LdapConfig ldapConfig = xmlLoader.fromXmlPartial(toInputStream(ldapCfg), LdapConfig.class); assertThat(ldapConfig.managerDn(), is("")); assertThat(ldapConfig.managerPassword(), is("")); } @Test public void shouldAcceptLdapConfiguration_withoutSearchFilter() throws Exception { String ldapCfg = "<ldap uri=\"foo\" managerDn=\"foo\" managerPassword=\"bar\">" + "<bases><base>base1</base></bases>" + "</ldap>"; LdapConfig ldapConfig = xmlLoader.fromXmlPartial(toInputStream(ldapCfg), LdapConfig.class); assertThat(ldapConfig.searchFilter(), is("")); } @Test public void shouldLoadGetFromSvnPartialForDir() throws Exception { String buildXmlPartial = "<jobs>\n" + " <job name=\"functional\">\n" + " <tasks>\n" + " <fetchartifact stage='dev' job='unit' srcdir='dist' dest='lib' />\n" + " </tasks>\n" + " </job>\n" + "</jobs>"; JobConfigs jobs = xmlLoader.fromXmlPartial(toInputStream(buildXmlPartial), JobConfigs.class); JobConfig job = jobs.first(); Tasks fetch = job.tasks(); assertThat(fetch.size(), is(1)); FetchTask task = (FetchTask) fetch.first(); assertThat(task.getStage(), is(new CaseInsensitiveString("dev"))); assertThat(task.getJob().toString(), is("unit")); assertThat(task.getSrc(), is("dist")); assertThat(task.getDest(), is("lib")); } @Test public void shouldAllowEmptyOnCancel() throws Exception { String buildXmlPartial = "<jobs>\n" + " <job name=\"functional\">\n" + " <tasks>\n" + " <exec command='ls'>\n" + " <oncancel/>\n" + " </exec>\n" + " </tasks>\n" + " </job>\n" + "</jobs>"; JobConfigs jobs = xmlLoader.fromXmlPartial(toInputStream(buildXmlPartial), JobConfigs.class); JobConfig job = jobs.first(); Tasks tasks = job.tasks(); assertThat(tasks.size(), is(1)); ExecTask execTask = (ExecTask) tasks.get(0); assertThat(execTask.cancelTask(), is(instanceOf(NullTask.class))); } @Test public void shouldLoadIgnoresFromGitPartial() throws Exception { String gitPartial = "<git url='file:///tmp/testGitRepo/project1' >\n" + " <filter>\n" + " <ignore pattern='x'/>\n" + " </filter>\n" + " </git>"; GitMaterialConfig gitMaterial = xmlLoader.fromXmlPartial(toInputStream(gitPartial), GitMaterialConfig.class); assertThat(gitMaterial.getBranch(), is(GitMaterialConfig.DEFAULT_BRANCH)); Filter parsedFilter = gitMaterial.filter(); Filter expectedFilter = new Filter(); expectedFilter.add(new IgnoredFiles("x")); assertThat(parsedFilter, is(expectedFilter)); } @Test public void shouldLoadShallowFlagFromGitPartial() throws Exception { String gitPartial = "<git url='file:///tmp/testGitRepo/project1' shallowClone=\"true\" />"; GitMaterialConfig gitMaterial = xmlLoader.fromXmlPartial(toInputStream(gitPartial), GitMaterialConfig.class); assertTrue(gitMaterial.isShallowClone()); } @Test public void shouldLoadBranchFromGitPartial() throws Exception { String gitPartial = "<git url='file:///tmp/testGitRepo/project1' branch='foo'/>"; GitMaterialConfig gitMaterial = xmlLoader.fromXmlPartial(toInputStream(gitPartial), GitMaterialConfig.class); assertThat(gitMaterial.getBranch(), is("foo")); } @Test public void shouldLoadIgnoresFromP4Partial() throws Exception { String gitPartial = "<p4 port=\"localhost:8080\">\n" + " <filter>\n" + " <ignore pattern=\"x\"/>\n" + " </filter>\n" + " <view></view>\n" + "</p4>"; MaterialConfig p4Material = xmlLoader.fromXmlPartial(toInputStream(gitPartial), P4MaterialConfig.class); Filter parsedFilter = p4Material.filter(); Filter expectedFilter = new Filter(); expectedFilter.add(new IgnoredFiles("x")); assertThat(parsedFilter, is(expectedFilter)); } @Test public void shouldLoadStageFromXmlPartial() throws Exception { String stageXmlPartial = "<stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + "</stage>\n"; StageConfig stage = xmlLoader.fromXmlPartial(toInputStream(stageXmlPartial), StageConfig.class); assertThat(stage.name(), is(new CaseInsensitiveString("mingle"))); assertThat(stage.allBuildPlans().size(), is(1)); assertThat(stage.jobConfigByInstanceName("functional", true), is(notNullValue())); } @Test public void shouldLoadStageArtifactPurgeSettingsFromXmlPartial() throws Exception { String stageXmlPartial = "<stage name=\"mingle\" artifactCleanupProhibited=\"true\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + "</stage>\n"; StageConfig stage = xmlLoader.fromXmlPartial(toInputStream(stageXmlPartial), StageConfig.class); assertThat(stage.isArtifactCleanupProhibited(), is(true)); stageXmlPartial = "<stage name=\"mingle\" artifactCleanupProhibited=\"false\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + "</stage>\n"; stage = xmlLoader.fromXmlPartial(toInputStream(stageXmlPartial), StageConfig.class); assertThat(stage.isArtifactCleanupProhibited(), is(false)); stageXmlPartial = "<stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + "</stage>\n"; stage = xmlLoader.fromXmlPartial(toInputStream(stageXmlPartial), StageConfig.class); assertThat(stage.isArtifactCleanupProhibited(), is(false)); } @Test public void shouldLoadPartialConfigWithPipeline() throws Exception { String partialConfigWithPipeline = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<pipelines group=\"first\">\n" + "<pipeline name=\"pipeline\">\n" + " <materials>\n" + " <hg url=\"/hgrepo\"/>\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>\n"; PartialConfig partialConfig = xmlLoader.fromXmlPartial(toInputStream(partialConfigWithPipeline), PartialConfig.class); assertThat(partialConfig.getGroups().size(), is(1)); PipelineConfig pipeline = partialConfig.getGroups().get(0).getPipelines().get(0); assertThat(pipeline.name(), is(new CaseInsensitiveString("pipeline"))); assertThat(pipeline.size(), is(1)); assertThat(pipeline.findBy(new CaseInsensitiveString("mingle")).jobConfigByInstanceName("functional", true), is(notNullValue())); } @Test public void shouldLoadPartialConfigWithEnvironment() throws Exception { String partialConfigWithPipeline = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<environments>" + " <environment name='uat'>" + " <agents>" + " <physical uuid='1'/>" + " <physical uuid='2'/>" + " </agents>" + " </environment>" + " <environment name='prod'>" + " <agents>" + " <physical uuid='2'/>" + " </agents>" + " </environment>" + "</environments>" + "</cruise>\n"; PartialConfig partialConfig = xmlLoader.fromXmlPartial(toInputStream(partialConfigWithPipeline), PartialConfig.class); EnvironmentsConfig environmentsConfig = partialConfig.getEnvironments(); assertThat(environmentsConfig.size(), is(2)); EnvironmentPipelineMatchers matchers = environmentsConfig.matchers(); assertThat(matchers.size(), is(2)); ArrayList<String> uat_uuids = new ArrayList<String>() {{ add("1"); add("2"); }}; ArrayList<String> prod_uuids = new ArrayList<String>() {{ add("2"); }}; assertThat(matchers, hasItem(new EnvironmentPipelineMatcher(new CaseInsensitiveString("uat"), uat_uuids, new EnvironmentPipelinesConfig()))); assertThat(matchers, hasItem(new EnvironmentPipelineMatcher(new CaseInsensitiveString("prod"), prod_uuids, new EnvironmentPipelinesConfig()))); } @Test public void shouldLoadPipelineFromXmlPartial() throws Exception { String pipelineXmlPartial = "<pipeline name=\"pipeline\">\n" + " <materials>\n" + " <hg url=\"/hgrepo\"/>\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n"; PipelineConfig pipeline = xmlLoader.fromXmlPartial(toInputStream(pipelineXmlPartial), PipelineConfig.class); assertThat(pipeline.name(), is(new CaseInsensitiveString("pipeline"))); assertThat(pipeline.size(), is(1)); assertThat(pipeline.findBy(new CaseInsensitiveString("mingle")).jobConfigByInstanceName("functional", true), is(notNullValue())); } @Test public void shouldBeAbleToExplicitlyLockAPipeline() throws Exception { String pipelineXmlPartial = "<pipeline name=\"pipeline\" isLocked=\"true\">\n" + " <materials>\n" + " <hg url=\"/hgrepo\"/>\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n"; PipelineConfig pipeline = xmlLoader.fromXmlPartial(toInputStream(pipelineXmlPartial), PipelineConfig.class); assertThat(pipeline.hasExplicitLock(), is(true)); assertThat(pipeline.explicitLock(), is(true)); } @Test public void shouldBeAbleToExplicitlyUnlockAPipeline() throws Exception { String pipelineXmlPartial = "<pipeline name=\"pipeline\" isLocked=\"false\">\n" + " <materials>\n" + " <hg url=\"/hgrepo\"/>\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n"; PipelineConfig pipeline = xmlLoader.fromXmlPartial(toInputStream(pipelineXmlPartial), PipelineConfig.class); assertThat(pipeline.hasExplicitLock(), is(true)); assertThat(pipeline.explicitLock(), is(false)); } @Test public void shouldUnderstandNoExplicitLockOnAPipeline() throws Exception { String pipelineXmlPartial = "<pipeline name=\"pipeline\">\n" + " <materials>\n" + " <hg url=\"/hgrepo\"/>\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n"; PipelineConfig pipeline = xmlLoader.fromXmlPartial(toInputStream(pipelineXmlPartial), PipelineConfig.class); assertThat(pipeline.hasExplicitLock(), is(false)); try { pipeline.explicitLock(); fail("Should throw exception if call explicit lock without first checking to see if there is one"); } catch (Exception e) { assertThat(e.getMessage(), containsString("There is no explicit lock on the pipeline 'pipeline'.")); } } @Test public void shouldLoadPipelineWithP4MaterialFromXmlPartial() throws Exception { String pipelineWithP4MaterialXmlPartial = "<pipeline name=\"pipeline\">\n" + " <materials>\n" + " <p4 port=\"10.18.3.241:9999\" username=\"cruise\" password=\"password\" " + " useTickets=\"true\">\n" + " <view><![CDATA[//depot/dev/... //lumberjack/...]]></view>\n" + " </p4>" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n"; PipelineConfig pipeline = xmlLoader.fromXmlPartial(toInputStream(pipelineWithP4MaterialXmlPartial), PipelineConfig.class); assertThat(pipeline.name(), is(new CaseInsensitiveString("pipeline"))); MaterialConfig material = pipeline.materialConfigs().first(); assertThat(material, is(instanceOf(P4MaterialConfig.class))); assertThat(((P4MaterialConfig) material).getUseTickets(), is(true)); } @Test public void shouldThrowExceptionWhenXmlDoesNotMapToXmlPartial() throws Exception { String stageXmlPartial = "<stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + "</stage>\n"; try { xmlLoader.fromXmlPartial(toInputStream(stageXmlPartial), JobConfig.class); fail("Should not be able to load stage into jobConfig"); } catch (Exception e) { assertThat(e.getMessage(), is("Unable to parse element <stage> for class JobConfig")); } } @Test public void shouldThrowExceptionWhenCommandIsEmpty() throws Exception { String jobWithCommand = "<job name=\"functional\">\n" + " <tasks>\n" + " <exec command=\"\" arguments=\"\" />\n" + " </tasks>\n" + " </job>\n"; String configWithInvalidCommand = ConfigFileFixture.withCommand(jobWithCommand); try { ConfigMigrator.loadWithMigration(toInputStream(configWithInvalidCommand)); fail("Should not allow empty command"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Command is invalid. \"\" should conform to the pattern - \\S+(.*\\S+)*")); } } @Test public void shouldThrowExceptionWhenCommandsContainTrailingSpaces() throws Exception { String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + " <pipelines group='first'>" + " <pipeline name='Test'>" + " <materials>" + " <hg url='../manual-testing/ant_hg/dummy' />" + " </materials>" + " <stage name='Functional'>" + " <jobs>" + " <job name='Functional'>" + " <tasks>" + " <exec command='bundle ' args='arguments' />" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + " </pipeline>" + " </pipelines>" + "</cruise>"; try { ConfigMigrator.loadWithMigration(configXml); fail("Should not allow command with trailing spaces"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Command is invalid. \"bundle \" should conform to the pattern - \\S+(.*\\S+)*")); } } @Test public void shouldThrowExceptionWhenCommandsContainLeadingSpaces() throws Exception { String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + " <pipelines group='first'>" + " <pipeline name='Test'>" + " <materials>" + " <hg url='../manual-testing/ant_hg/dummy' />" + " </materials>" + " <stage name='Functional'>" + " <jobs>" + " <job name='Functional'>" + " <tasks>" + " <exec command=' bundle' args='arguments' />" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + " </pipeline>" + " </pipelines>" + "</cruise>"; try { ConfigMigrator.loadWithMigration(configXml); fail("Should not allow command with trailing spaces"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Command is invalid. \" bundle\" should conform to the pattern - \\S+(.*\\S+)*")); } } @Test public void shouldSupportCommandWithWhiteSpace() throws Exception { String jobWithCommand = "<job name=\"functional\">\n" + " <tasks>\n" + " <exec command=\"c:\\program files\\cmd.exe\" args=\"arguments\" />\n" + " </tasks>\n" + " </job>\n"; String configWithCommand = ConfigFileFixture.withCommand(jobWithCommand); CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(toInputStream(configWithCommand)).config; Task task = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).first().allBuildPlans().first().tasks().first(); assertThat(task, is(instanceOf(ExecTask.class))); assertThat(task, is(new ExecTask("c:\\program files\\cmd.exe", "arguments", (String) null))); } @Test public void shouldLoadMingleConfigForPipeline() throws Exception { String configWithCommand = ConfigFileFixture.withMingleConfig("<mingle baseUrl=\"https://foo.bar/baz\" projectIdentifier=\"cruise-performance\"/>"); MingleConfig mingleConfig = ConfigMigrator.loadWithMigration(toInputStream(configWithCommand)).config.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getMingleConfig(); assertThat(mingleConfig, is(new MingleConfig("https://foo.bar/baz", "cruise-performance"))); configWithCommand = ConfigFileFixture.withMingleConfig( "<mingle baseUrl=\"https://foo.bar/baz\" projectIdentifier=\"cruise-performance\"><mqlGroupingConditions>foo = bar!=baz</mqlGroupingConditions></mingle>"); mingleConfig = ConfigMigrator.loadWithMigration(toInputStream(configWithCommand)).config.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getMingleConfig(); assertThat(mingleConfig, is(new MingleConfig("https://foo.bar/baz", "cruise-performance", "foo = bar!=baz"))); configWithCommand = ConfigFileFixture.withMingleConfig("<mingle baseUrl=\"https://foo.bar/baz\" projectIdentifier=\"cruise-performance\"><mqlGroupingConditions/></mingle>"); mingleConfig = ConfigMigrator.loadWithMigration(toInputStream(configWithCommand)).config.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getMingleConfig(); assertThat(mingleConfig, is(new MingleConfig("https://foo.bar/baz", "cruise-performance", ""))); } private void shouldBeSvnMaterial(MaterialConfig material) { assertThat(material, is(instanceOf(SvnMaterialConfig.class))); SvnMaterialConfig svnMaterial = (SvnMaterialConfig) material; assertThat(svnMaterial.getUrl(), is("svnUrl")); assertThat(svnMaterial.isCheckExternals(), is(true)); } private void shouldBeHgMaterial(MaterialConfig material) { assertThat(material, is(instanceOf(HgMaterialConfig.class))); HgMaterialConfig hgMaterial = (HgMaterialConfig) material; assertThat(hgMaterial.getUrlArgument(), is(new HgUrlArgument("http://username:password@hgUrl.com"))); } private void shouldBeP4Material(MaterialConfig material) { assertThat(material, is(instanceOf(P4MaterialConfig.class))); P4MaterialConfig p4Material = (P4MaterialConfig) material; assertThat(p4Material.getServerAndPort(), is("localhost:1666")); assertThat(p4Material.getUserName(), is("cruise")); assertThat(p4Material.getPassword(), is("password")); assertThat(p4Material.getView(), is("//depot/dir1/... //lumberjack/...")); } private void shouldBeGitMaterial(MaterialConfig material) { assertThat(material, is(instanceOf(GitMaterialConfig.class))); GitMaterialConfig gitMaterial = (GitMaterialConfig) material; assertThat(gitMaterial.getUrlArgument(), is(new UrlArgument("git://username:password@gitUrl"))); } @Test public void shouldNotAllowEmptyAuthInApproval() throws Exception { try { xmlLoader.loadConfigHolder(FileUtil.readToEnd(IOUtils.toInputStream(ConfigFileFixture.STAGE_WITH_EMPTY_AUTH))); fail("Should not allow approval with empty authorization"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("The content of element 'authorization' is not complete")); } } @Test public void shouldNotAllowEmptyRoles() throws Exception { try { xmlLoader.loadConfigHolder(FileUtil.readToEnd(IOUtils.toInputStream(ConfigFileFixture.CONFIG_WITH_EMPTY_ROLES))); fail("Should not allow approval with empty roles"); } catch (Exception expected) { assertThat(expected.getMessage(), is("The content of element 'roles' is not complete. One of '{baseRole}' is expected.")); } } @Test public void shouldNotAllowEmptyUser() throws Exception { try { xmlLoader.loadConfigHolder(FileUtil.readToEnd(IOUtils.toInputStream(ConfigFileFixture.CONFIG_WITH_EMPTY_USER))); fail("Should not allow approval with empty user"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("Value '' with length = '0' is not facet-valid with respect to minLength '1'")); } } @Test public void shouldNotAllowDuplicateRoles() throws Exception { try { xmlLoader.loadConfigHolder(FileUtil.readToEnd(IOUtils.toInputStream(ConfigFileFixture.CONFIG_WITH_DUPLICATE_ROLE))); fail("Should not allow approval with empty user"); } catch (Exception expected) { assertThat(expected.getMessage(), is("Role names should be unique. Duplicate names found.")); } } @Test public void shouldNotAllowDuplicateUsersInARole() throws Exception { try { xmlLoader.loadConfigHolder(FileUtil.readToEnd(IOUtils.toInputStream(ConfigFileFixture.CONFIG_WITH_DUPLICATE_USER))); fail("Should not allow role with duplicate user"); } catch (RuntimeException expected) { assertThat(expected.getMessage(), containsString("User 'ps' already exists in 'admin'.")); } } /** * This is a test for a specific bug at a customer installation caused by a StackOverflowException in Xerces. * It seems to be caused by a regex bug in nonEmptyString. */ @Test public void shouldLoadConfigurationFileWithComplexNonEmptyString() throws Exception { String customerXML = this.getClass().getResource("/data/p4_heavy_cruise_config.xml").getFile(); assertThat(loadWithMigration(customerXML), not(nullValue())); } private CruiseConfig loadWithMigration(String file) throws Exception { FileInputStream input = new FileInputStream(file); return ConfigMigrator.loadWithMigration(input).config; } @Test public void shouldNotAllowEmptyViewForPerforce() throws Exception { try { String p4XML = this.getClass().getResource("/data/p4-cruise-config-empty-view.xml").getFile(); loadWithMigration(p4XML); fail("Should not accept p4 section with empty view."); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("P4 view cannot be empty.")); } } @Test public void shouldLoadPipelineWithMultipleMaterials() throws Exception { String pipelineXmlPartial = "<pipeline name=\"pipeline\">\n" + " <materials>\n" + " <svn url=\"/hgrepo1\" dest=\"folder1\" />\n" + " <svn url=\"/hgrepo2\" dest=\"folder2\" />\n" + " <svn url=\"/hgrepo3\" dest=\"folder3\" />\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <log src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n"; PipelineConfig pipeline = xmlLoader.fromXmlPartial(toInputStream(pipelineXmlPartial), PipelineConfig.class); assertThat(pipeline.materialConfigs().size(), is(3)); ScmMaterialConfig material = (ScmMaterialConfig) pipeline.materialConfigs().get(0); assertThat(material.getFolder(), is("folder1")); } @Test public void shouldThrowErrorIfMultipleMaterialsHaveSameFolders() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo1\" dest=\"folder1\" />\n" + " <svn url=\"/hgrepo2\" dest=\"folder1\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid(INVALID_DESTINATION_DIRECTORY_MESSAGE, materials); } @Test public void shouldThrowErrorIfOneOfMultipleMaterialsHasNoFolder() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo1\" />\n" + " <svn url=\"/hgrepo2\" dest=\"folder1\" />\n" + " </materials>\n"; String message = "Destination directory is required when specifying multiple scm materials"; MagicalGoConfigXmlLoaderFixture.assertNotValid(message, materials); } @Test public void shouldThrowErrorIfOneOfMultipleMaterialsIsNested() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo1\" dest=\"folder1\"/>\n" + " <svn url=\"/hgrepo2\" dest=\"folder1/folder2\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid(INVALID_DESTINATION_DIRECTORY_MESSAGE, materials); } //This is bug #2337 @Test public void shouldNotThrowErrorIfMultipleMaterialsHaveSimilarNamesBug2337() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo1\" dest=\"folder1/folder2\"/>\n" + " <svn url=\"/hgrepo2\" dest=\"folder1/folder2different\" />\n" + " </materials>\n"; assertValidMaterials(materials); } //This is bug #2337 @Test public void shouldNotThrowErrorIfMultipleMaterialsHaveSimilarNamesInDifferentOrder() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\"folder1/folder2different\" />\n" + " <svn url=\"/hgrepo1\" dest=\"folder1/folder2\"/>\n" + " </materials>\n"; assertValidMaterials(materials); } @Test public void shouldNotAllowfoldersOutsideWorkingDirectory() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\"folder1/folder2/../folder3\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertValid(materials); String materials2 = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\"../../..\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid( "File path is invalid. \"../../..\" should conform to the pattern - (([.]\\/)?[.][^. ]+)|([^. ].+[^. ])|([^. ][^. ])|([^. ])", materials2); } @Test public void shouldAllowPathStartWithDotSlash() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\"./folder3\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertValid(materials); } @Test public void shouldAllowHiddenFolders() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\".folder3\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertValid(materials); materials = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\"./.folder3\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertValid(materials); } @Test @RunIf(value = EnhancedOSChecker.class, arguments = {DO_NOT_RUN_ON, WINDOWS}) public void shouldNotAllowAbsoluteDestFolderNamesOnLinux() throws Exception { String materials1 = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\"/tmp/foo\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid("Dest folder '/tmp/foo' is not valid. It must be a sub-directory of the working folder.", materials1); } @Test @RunIf(value = EnhancedOSChecker.class, arguments = {EnhancedOSChecker.WINDOWS}) public void shouldNotAllowAbsoluteDestFolderNamesOnWindows() throws Exception { String materials1 = " <materials>\n" + " <svn url=\"/hgrepo2\" dest=\"C:\\tmp\\foo\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid("Dest folder 'C:\\tmp\\foo' is not valid. It must be a sub-directory of the working folder.", materials1); } @Test public void shouldNotThrowErrorIfMultipleMaterialsHaveSameNames() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo1\" dest=\"folder1/folder2\"/>\n" + " <svn url=\"/hgrepo2\" dest=\"folder1/folder2\" />\n" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertNotValid(INVALID_DESTINATION_DIRECTORY_MESSAGE, materials); } @Test public void shouldSupportHgGitSvnP4ForMultipleMaterials() throws Exception { String materials = " <materials>\n" + " <svn url=\"/hgrepo1\" dest=\"folder1\"/>\n" + " <git url=\"/hgrepo2\" dest=\"folder2\"/>\n" + " <hg url=\"/hgrepo2\" dest=\"folder3\"/>\n" + " <p4 port=\"localhost:1666\" dest=\"folder4\">\n" + " <view>asd</view>" + " </p4>" + " </materials>\n"; MagicalGoConfigXmlLoaderFixture.assertValid(materials); } @Test public void shouldLoadPipelinesWithGroupName() throws Exception { CruiseConfig config = ConfigMigrator.loadWithMigration(ConfigFileFixture.PIPELINE_GROUPS).config; assertThat(config.getGroups().first().getGroup(), is("studios")); assertThat(config.getGroups().get(1).getGroup(), is("perfessionalservice")); } @Test public void shouldLoadTasksWithExecutionCondition() throws Exception { CruiseConfig config = ConfigMigrator.loadWithMigration(ConfigFileFixture.TASKS_WITH_CONDITION).config; JobConfig job = config.jobConfigByName("pipeline1", "mingle", "cardlist", true); assertThat(job.tasks().size(), is(2)); assertThat(job.tasks().findFirstByType(AntTask.class).getConditions().get(0), is(new RunIfConfig("failed"))); RunIfConfigs conditions = job.tasks().findFirstByType(NantTask.class).getConditions(); assertThat(conditions.get(0), is(new RunIfConfig("failed"))); assertThat(conditions.get(1), is(new RunIfConfig("any"))); assertThat(conditions.get(2), is(new RunIfConfig("passed"))); } @Test public void shouldLoadTasksWithOnCancel() throws Exception { CruiseConfig config = ConfigMigrator.loadWithMigration(ConfigFileFixture.TASKS_WITH_ON_CANCEL).config; JobConfig job = config.jobConfigByName("pipeline1", "mingle", "cardlist", true); Task task = job.tasks().findFirstByType(AntTask.class); assertThat(task.hasCancelTask(), is(true)); assertThat(task.cancelTask(), is(new ExecTask("kill.rb", "", "utils"))); Task task2 = job.tasks().findFirstByType(ExecTask.class); assertThat(task2.hasCancelTask(), is(false)); } @Test public void shouldNotLoadTasksWithOnCancelTaskNested() throws Exception { try { ConfigMigrator.loadWithMigration(ConfigFileFixture.TASKS_WITH_ON_CANCEL_NESTED); fail("Should not allow nesting of 'oncancel' within task inside oncancel"); } catch (Exception expected) { // Carrots are good for your eyes } } @Test public void shouldNotLoadTasksWithEmptyOnCancelTask() throws Exception { try { ConfigMigrator.loadWithMigration(ConfigFileFixture.TASKS_WITH_EMPTY_ON_CANCEL); fail("Should not allow empty 'oncancel'"); } catch (Exception expected) { } } @Test public void shouldAllowBothCounterAndMaterialNameInLabelTemplate() throws Exception { CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(ConfigFileFixture.LABEL_TEMPLATE_WITH_LABEL_TEMPLATE("1.3.0-${COUNT}-${git}")).config; assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("cruise")).getLabelTemplate(), is("1.3.0-${COUNT}-${git}")); } @Test public void shouldAllowBothCounterAndTruncatedGitMaterialInLabelTemplate() throws Exception { CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(ConfigFileFixture.LABEL_TEMPLATE_WITH_LABEL_TEMPLATE("1.3.0-${COUNT}-${git[:7]}", CONFIG_SCHEMA_VERSION)).config; assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("cruise")).getLabelTemplate(), is("1.3.0-${COUNT}-${git[:7]}")); } @Test public void shouldAllowHashCharacterInLabelTemplate() throws Exception { CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(ConfigFileFixture.LABEL_TEMPLATE_WITH_LABEL_TEMPLATE("1.3.0-${COUNT}-${git}##", CONFIG_SCHEMA_VERSION)).config; assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("cruise")).getLabelTemplate(), is("1.3.0-${COUNT}-${git}#")); } @Test public void shouldNotAllowInvalidLabelTemplate() throws Exception { assertPipelineLabelTemplate("1.3.0"); assertPipelineLabelTemplate("1.3.0-{COUNT}"); assertPipelineLabelTemplate("1.3.0-$COUNT}"); assertPipelineLabelTemplate("1.3.0-${COUNT"); assertPipelineLabelTemplate("1.3.0-${}"); assertPipelineLabelTemplate("1.3.0-${COUNT}-${git:7]}"); assertPipelineLabelTemplate("1.3.0-${COUNT}-${git[:7}"); assertPipelineLabelTemplate("1.3.0-${COUNT}-${git[7]}"); assertPipelineLabelTemplate("1.3.0-${COUNT}-${git[:]}"); assertPipelineLabelTemplate("1.3.0-${COUNT}-${git[:-1]}"); } public void assertPipelineLabelTemplate(String labelTemplate) { try { ConfigMigrator.loadWithMigration(ConfigFileFixture.LABEL_TEMPLATE_WITH_LABEL_TEMPLATE(labelTemplate, 75)); fail("should have failed"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Label is invalid.")); } } @Test public void shouldLoadMaterialNameIfPresent() throws Exception { CruiseConfig config = ConfigMigrator.loadWithMigration(ConfigFileFixture.MATERIAL_WITH_NAME).config; MaterialConfigs materialConfigs = config.pipelineConfigByName(new CaseInsensitiveString("pipeline")).materialConfigs(); assertThat(materialConfigs.get(0).getName(), is(new CaseInsensitiveString("svn"))); assertThat(materialConfigs.get(1).getName(), is(new CaseInsensitiveString("hg"))); } @Test public void shouldLoadPipelineWithTimer() throws Exception { CruiseConfig config = ConfigMigrator.loadWithMigration(ConfigFileFixture.PIPELINE_WITH_TIMER).config; PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString("pipeline")); assertThat(pipelineConfig.getTimer(), is(new TimerConfig("0 15 10 ? * MON-FRI", false))); } @Test public void shouldLoadConfigWithEnvironment() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <agents>" + " <physical uuid='1'/>" + " <physical uuid='2'/>" + " </agents>" + " </environment>" + " <environment name='prod'>" + " <agents>" + " <physical uuid='2'/>" + " </agents>" + " </environment>" + "</environments>"); EnvironmentsConfig environmentsConfig = ConfigMigrator.loadWithMigration(content).config.getEnvironments(); EnvironmentPipelineMatchers matchers = environmentsConfig.matchers(); assertThat(matchers.size(), is(2)); ArrayList<String> uat_uuids = new ArrayList<String>() {{ add("1"); add("2"); }}; ArrayList<String> prod_uuids = new ArrayList<String>() {{ add("2"); }}; assertThat(matchers, hasItem(new EnvironmentPipelineMatcher(new CaseInsensitiveString("uat"), uat_uuids, new EnvironmentPipelinesConfig()))); assertThat(matchers, hasItem(new EnvironmentPipelineMatcher(new CaseInsensitiveString("prod"), prod_uuids, new EnvironmentPipelinesConfig()))); } @Test public void shouldNotLoadConfigWithEmptyTemplates() throws Exception { String content = ConfigFileFixture.configWithTemplates( "<templates>" + "</templates>"); try { ConfigMigrator.loadWithMigration(content); fail("Should not allow empty templates block"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("The content of element 'templates' is not complete. One of '{pipeline}' is expected.")); } } @Test public void shouldNotLoadConfigWhenPipelineHasNoStages() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='pipeline1'>\n" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>"; try { ConfigMigrator.loadWithMigration(content); fail("Should not allow Pipeline with No Stages"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("Pipeline 'pipeline1' does not have any stages configured. A pipeline must have at least one stage.")); } } @Test public void shouldNotAllowReferencingTemplateThatDoesNotExist() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='pipeline1' template='abc'>\n" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>"; try { ConfigMigrator.loadWithMigration(content); fail("shouldNotAllowReferencingTemplateThatDoesNotExist"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("Pipeline 'pipeline1' refers to non-existent template 'abc'.")); } } @Test public void shouldAllowPipelineToReferenceTemplate() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts'>" + "</server>" + "<pipelines>\n" + "<pipeline name='pipeline1' template='abc'>\n" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + "</pipeline>\n" + "</pipelines>\n" + "<templates>\n" + " <pipeline name='abc'>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>\n" + "</templates>\n" + "</cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; PipelineConfig pipelineConfig = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")); assertThat(pipelineConfig.size(), is(1)); } @Test public void shouldAllowAdminInPipelineGroups() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' >" + "</server>" + "<pipelines group=\"first\">\n" + "<authorization>" + " <admins>\n" + " <user>foo</user>\n" + " </admins>" + "</authorization>" + "<pipeline name='pipeline1' template='abc'>\n" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + "</pipeline>\n" + "</pipelines>\n" + "<templates>\n" + " <pipeline name='abc'>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>\n" + "</templates>\n" + "</cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.schemaVersion(), is(CONFIG_SCHEMA_VERSION)); assertThat(cruiseConfig.findGroup("first").isUserAnAdmin(new CaseInsensitiveString("foo"), new ArrayList<>()), is(true)); } @Test public void shouldAllowAdminWithRoleInPipelineGroups() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' >" + "<security>\n" + " <roles>\n" + " <role name=\"bar\">\n" + " <users>" + " <user>foo</user>" + " </users>" + " </role>" + " </roles>" + "</security>" + "</server>" + "<pipelines group=\"first\">\n" + "<authorization>" + " <admins>\n" + " <role>bar</role>\n" + " </admins>" + "</authorization>" + "<pipeline name='pipeline1' template='abc'>\n" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + "</pipeline>\n" + "</pipelines>\n" + "<templates>\n" + " <pipeline name='abc'>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>\n" + "</templates>\n" + "</cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.schemaVersion(), is(CONFIG_SCHEMA_VERSION)); assertThat(cruiseConfig.findGroup("first").isUserAnAdmin(new CaseInsensitiveString("foo"), asList(new RoleConfig(new CaseInsensitiveString("bar")))), is(true)); } @Test public void shouldAddJobTimeoutAttributeToServerTagAndDefaultItTo60_37xsl() { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' siteUrl='http://www.someurl.com/go' secureSiteUrl='https://www.someotherurl.com/go' >" + "</server></cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.server().getJobTimeout(), is("0")); } @Test public void shouldGetTheJobTimeoutFromServerTag_37xsl() { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' siteUrl='http://www.someurl.com/go' secureSiteUrl='https://www.someotherurl.com/go' jobTimeout='30' >" + "</server></cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.server().getJobTimeout(), is("30")); } @Test public void shouldHaveJobTimeoutAttributeOnJob_37xsl() { String content = CONFIG_WITH_ANT_BUILDER; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; JobConfig jobConfig = cruiseConfig.findJob("pipeline1", "mingle", "cardlist"); assertThat(jobConfig.getTimeout(), is("5")); } @Test public void shouldAllowSiteUrlandSecureSiteUrlAttributes() { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' siteUrl='http://www.someurl.com/go' secureSiteUrl='https://www.someotherurl.com/go' >" + "</server></cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.server().getSiteUrl(), is(new ServerSiteUrlConfig("http://www.someurl.com/go"))); assertThat(cruiseConfig.server().getSecureSiteUrl(), is(new ServerSiteUrlConfig("https://www.someotherurl.com/go"))); } @Test public void shouldAllowPurgeStartAndPurgeUptoAttributes() { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' purgeStart='1' purgeUpto='3'>" + "</server></cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.server().getPurgeStart(), is(1.0)); assertThat(cruiseConfig.server().getPurgeUpto(), is(3.0)); } @Test public void shouldAllowDoublePurgeStartAndPurgeUptoAttributes() { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' purgeStart='1.2' purgeUpto='3.4'>" + "</server></cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.server().getPurgeStart(), is(1.2)); assertThat(cruiseConfig.server().getPurgeUpto(), is(3.4)); } @Test public void shouldAllowNullPurgeStartAndEnd() { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts'>" + "</server></cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.server().getPurgeStart(), is(nullValue())); assertThat(cruiseConfig.server().getPurgeUpto(), is(nullValue())); } @Test public void shouldNotAllowAPipelineThatReferencesATemplateToHaveStages() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='pipeline1' template='abc'>\n" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + " <stage name='badstage'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + "</pipeline>\n" + "</pipelines>\n" + "<templates>\n" + " <pipeline name='abc'>\n" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>\n" + "</templates>\n" + "</cruise>"; try { ConfigMigrator.loadWithMigration(content); fail("shouldn't have stages and template"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("Cannot add stage 'badstage' to pipeline 'pipeline1', which already references template 'abc'.")); } } @Test public void shouldLoadConfigWithPipelineTemplate() throws Exception { String content = ConfigFileFixture.configWithTemplates( "<templates>" + " <pipeline name='erbshe'>" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>" + "</templates>"); TemplatesConfig templates = ConfigMigrator.loadWithMigration(content).config.getTemplates(); assertThat(templates.size(), is(1)); assertThat(templates.get(0).size(), is(1)); assertThat(templates.get(0).get(0), is(StageConfigMother.custom("stage1", "job1"))); } @Test public void shouldLoadConfigWith2PipelineTemplates() throws Exception { String content = ConfigFileFixture.configWithTemplates( "<templates>" + " <pipeline name='erbshe'>" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>" + " <pipeline name='erbshe2'>" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>" + "</templates>"); TemplatesConfig templates = ConfigMigrator.loadWithMigration(content).config.getTemplates(); assertThat(templates.size(), is(2)); assertThat(templates.get(0).name(), is(new CaseInsensitiveString("erbshe"))); assertThat(templates.get(1).name(), is(new CaseInsensitiveString("erbshe2"))); } @Test public void shouldOnlySupportUniquePipelineTemplates() throws Exception { String content = ConfigFileFixture.configWithTemplates( "<templates>" + " <pipeline name='erbshe'>" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>" + " <pipeline name='erbshe'>" + " <stage name='stage1'>" + " <jobs>" + " <job name='job1' />" + " </jobs>" + " </stage>" + " </pipeline>" + "</templates>"); try { ConfigMigrator.loadWithMigration(content); fail("should not allow same template names"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("Duplicate unique value [erbshe] declared for identity constraint")); } } @Test public void shouldNotAllowEmptyPipelineTemplates() throws Exception { String content = ConfigFileFixture.configWithTemplates( "<templates>" + " <pipeline name='erbshe'>" + " </pipeline>" + "</templates>"); try { ConfigMigrator.loadWithMigration(content); fail("should NotAllowEmptyPipelineTemplates"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString("The content of element 'pipeline' is not complete. One of '{authorization, stage}' is expected")); } } @Test public void shouldNotAllowJobToHaveTheRunOnAllAgentsMarkerInItsName() throws Exception { String invalidJobName = format("%s-%s-%s", "invalid-name", RunOnAllAgentsJobTypeConfig.MARKER, 1); testForInvalidJobName(invalidJobName, RunOnAllAgentsJobTypeConfig.MARKER); } @Test public void shouldNotAllowJobToHaveTheRunInstanceMarkerInItsName() throws Exception { String invalidJobName = format("%s-%s-%s", "invalid-name", RunMultipleInstanceJobTypeConfig.MARKER, 1); testForInvalidJobName(invalidJobName, RunMultipleInstanceJobTypeConfig.MARKER); } private void testForInvalidJobName(String invalidJobName, String marker) { String content = ConfigFileFixture.configWithPipeline( " <pipeline name=\"dev\">\n" + " <materials>\n" + " <svn url=\"file:///tmp/svn/repos/fifth\" />\n" + " </materials>\n" + " <stage name=\"AutoStage\">\n" + " <jobs>\n" + " <job name=\"" + invalidJobName + "\">\n" + " <tasks>\n" + " <exec command=\"ls\" args=\"-lah\" />\n" + " </tasks>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>" ); try { ConfigMigrator.loadWithMigration(content); fail("should not allow jobs with with name '" + marker + "'"); } catch (Exception expected) { assertThat(expected.getMessage(), containsString(String.format("A job cannot have '%s' in it's name: %s because it is a reserved keyword", marker, invalidJobName))); } } @Test public void shouldAllow_NonRunOnAllAgentJobToHavePartsOfTheRunOnAll_and_NonRunMultipleInstanceJobToHavePartsOfTheRunInstance_AgentsMarkerInItsName() throws Exception { String content = ConfigFileFixture.configWithPipeline( " <pipeline name=\"dev\">\n" + " <materials>\n" + " <svn url=\"file:///tmp/svn/repos/fifth\" />\n" + " </materials>\n" + " <stage name=\"AutoStage\">\n" + " <jobs>\n" + " <job name=\"valid-name-runOnAll\" >\n" + " <tasks>\n" + " <exec command=\"ls\" args=\"-lah\" />\n" + " </tasks>\n" + " </job>\n" + " <job name=\"valid-name-runInstance\" >\n" + " <tasks>\n" + " <exec command=\"ls\" args=\"-lah\" />\n" + " </tasks>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>"); ConfigMigrator.loadWithMigration(content); // should not fail with a validation exception } @Test public void shouldLoadLargeConfigFileInReasonableTime() throws Exception { String content = FileUtil.readToEnd(getClass().getResourceAsStream("/data/big-cruise-config.xml")); // long start = System.currentTimeMillis(); GoConfigHolder configHolder = ConfigMigrator.loadWithMigration(content); // assertThat(System.currentTimeMillis() - start, lessThan(new Long(2000))); assertThat(configHolder.config.schemaVersion(), is(CONFIG_SCHEMA_VERSION)); } @Test public void shouldLoadConfigWithPipelinesMatchingUpWithPipelineDefinitionCaseInsensitively() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <pipelines>" + " <pipeline name='piPeline1'/>" + " </pipelines>" + " </environment>" + "</environments>"); EnvironmentsConfig environmentsConfig = ConfigMigrator.loadWithMigration(content).config.getEnvironments(); EnvironmentPipelineMatcher matcher = environmentsConfig.matchersForPipeline("pipeline1"); assertThat(matcher, is(new EnvironmentPipelineMatcher(new CaseInsensitiveString("uat"), new ArrayList<>(), new EnvironmentPipelinesConfig(new CaseInsensitiveString("piPeline1"))))); } @Test public void shouldNotAllowConfigWithUnknownPipeline() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <pipelines>" + " <pipeline name='notpresent'/>" + " </pipelines>" + " </environment>" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); fail("Should not have allowed referencing of an unknown pipeline under an environment."); } catch (Exception e) { assertThat(e.getMessage(), containsString("Environment 'uat' refers to an unknown pipeline 'notpresent'.")); } } @Test public void shouldNotAllowDuplicatePipelineAcrossEnvironments() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <pipelines>" + " <pipeline name='pipeline1'/>" + " </pipelines>" + " </environment>" + " <environment name='prod'>" + " <pipelines>" + " <pipeline name='Pipeline1'/>" + " </pipelines>" + " </environment>" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); fail("Should not have allowed duplicate pipeline reference across environments"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Associating pipeline(s) which is already part of uat environment")); } } @Test public void shouldNotAllowDuplicatePipelinesInASingleEnvironment() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <pipelines>" + " <pipeline name='pipeline1'/>" + " <pipeline name='Pipeline1'/>" + " </pipelines>" + " </environment>" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); fail("Should not have allowed duplicate pipeline reference under an environment"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Cannot add pipeline 'Pipeline1' to the environment")); } } @Test public void shouldNotAllowConfigWithEnvironmentsWithSameNames() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat' />" + " <environment name='uat' />" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); fail("Should not support 2 environments with the same same"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Duplicate unique value [uat] declared for identity constraint \"uniqueEnvironmentName\" of element \"environments\"")); } } @Test public void shouldNotAllowConfigWithInvalidName() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='exclamation is invalid !' />" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); fail("XSD should not allow invalid characters"); } catch (Exception e) { assertThat(e.getMessage(), containsString("\"exclamation is invalid !\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldNotAllowConfigWithAbsentReferencedAgentUuid() throws Exception { String content = ConfigFileFixture.configWithEnvironmentsAndAgents( "<environments>" + " <environment name='uat'>" + " <agents>" + " <physical uuid='missing' />" + " </agents>" + " </environment>" + "</environments>", "<agents>" + " <agent uuid='1' hostname='test1.com' ipaddress='192.168.0.1' />" + "</agents>"); try { ConfigMigrator.loadWithMigration(content); fail("XSD should not allow reference to absent agent"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Environment 'uat' has an invalid agent uuid 'missing'")); } } @Test public void shouldAllowConfigWithEmptyPipeline() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <pipelines/>" + " </environment>" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); } catch (Exception e) { fail("should not allow empty pipelines block under an environment"); } } @Test public void shouldAllowConfigWithEmptyAgents() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat'>" + " <agents/>" + " </environment>" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); } catch (Exception e) { fail("should not allow empty agents block under an environment"); } } @Test public void shouldNotAllowConfigWithDuplicateAgentUuidInEnvironment() throws Exception { String content = ConfigFileFixture.configWithEnvironmentsAndAgents( "<environments>" + " <environment name='uat'>" + " <agents>" + " <physical uuid='1' />" + " <physical uuid='1' />" + " </agents>" + " </environment>" + "</environments>", "<agents>" + " <agent uuid='1' hostname='test1.com' ipaddress='192.168.0.1' />" + "</agents>"); try { ConfigMigrator.loadWithMigration(content); fail("XSD should not allow duplicate agent uuid in environment"); } catch (Exception e) { assertThat(e.getMessage(), containsString( "Duplicate unique value [1] declared for identity constraint \"uniqueEnvironmentAgentsUuid\" of element \"agents\".")); } } @Test public void shouldNotAllowConfigWithEmptyEnvironmentsBlock() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + "</environments>"); try { ConfigMigrator.loadWithMigration(content); fail("XSD should not allow empty environments block"); } catch (Exception e) { assertThat(e.getMessage(), containsString( "The content of element 'environments' is not complete. One of '{environment}' is expected.")); } } @Test public void shouldAllowConfigWithNoAgentsAndNoPipelinesInEnvironment() throws Exception { String content = ConfigFileFixture.configWithEnvironments( "<environments>" + " <environment name='uat' />" + "</environments>"); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; assertThat(config.getEnvironments().size(), is(1)); } @Test public void shouldAllowConfigWithEnvironmentReferencingDisabledAgent() throws Exception { String content = ConfigFileFixture.configWithEnvironmentsAndAgents( "<environments>" + " <environment name='uat'>" + " <agents>" + " <physical uuid='1' />" + " </agents>" + " </environment>" + "</environments>", "<agents>" + " <agent uuid='1' hostname='test1.com' ipaddress='192.168.0.1' isDisabled='true' />" + "</agents>"); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; assertThat(config.getEnvironments().matchers().size(), is(1)); } @Test public void shouldSupportEnvironmentVariablesInEnvironment() throws Exception { String content = ConfigFileFixture.configWithEnvironmentsAndAgents( "<environments>" + " <environment name='uat'>" + " <environmentvariables> " + " <variable name='VAR_NAME_1'><value>variable_name_value_1</value></variable>" + " <variable name='CRUISE_ENVIRONEMNT_NAME'><value>variable_name_value_2</value></variable>" + " </environmentvariables> " + " </environment>" + "</environments>", "<agents>" + " <agent uuid='1' hostname='test1.com' ipaddress='192.168.0.1' isDisabled='true' />" + "</agents>"); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; EnvironmentConfig element = new BasicEnvironmentConfig(new CaseInsensitiveString("uat")); element.addEnvironmentVariable("VAR_NAME_1", "variable_name_value_1"); element.addEnvironmentVariable("CRUISE_ENVIRONEMNT_NAME", "variable_name_value_2"); assertThat(config.getEnvironments(), hasItem(element)); } @Test public void shouldAllowCDATAInEnvironmentVariableValues() throws Exception { //TODO : This should be fixed as part of #4865 //String multiLinedata = "\nsome data\nfoo bar"; String multiLinedata = "some data\nfoo bar"; String content = ConfigFileFixture.configWithEnvironmentsAndAgents( "<environments>" + " <environment name='uat'>" + " <environmentvariables> " + " <variable name='cdata'><value><![CDATA[" + multiLinedata + "]]></value></variable>" + " </environmentvariables> " + " </environment>" + "</environments>", "<agents>" + " <agent uuid='1' hostname='test1.com' ipaddress='192.168.0.1' isDisabled='true' />" + "</agents>"); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; EnvironmentConfig element = new BasicEnvironmentConfig(new CaseInsensitiveString("uat")); element.addEnvironmentVariable("cdata", multiLinedata); assertThat(config.getEnvironments().get(0), is(element)); } @Test public void shouldAllowOnlyOneTimerOnAPipeline() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <timer>1 1 1 * * ? *</timer>" + " <timer>2 2 2 * * ? *</timer>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='cardlist' />" + " </jobs>" + " </stage>" + "</pipeline>", 81); try { ConfigMigrator.loadWithMigration(content); fail("XSD should not allow duplicate timer in pipeline"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Invalid content was found starting with element 'timer'.")); } } @Test public void shouldValidateTimerSpec() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <timer>BAD BAD TIMER!!!!!</timer>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='cardlist' />" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); try { ConfigMigrator.loadWithMigration(content); fail("XSD should validate timer spec"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Invalid cron syntax")); } } @Test public void shouldNotAllowIllegalValueForRunOnAllAgents() throws Exception { try { loadJobWithRunOnAllAgents("bad_value"); fail("should have failed as runOnAllAgents' value is not valid(boolean)"); } catch (Exception e) { assertThat(e.getMessage(), containsString("'bad_value' is not a valid value for 'boolean'")); } } @Test public void shouldNotAllowIllegalValueForRunMultipleInstanceJob() throws Exception { try { loadJobWithRunMultipleInstance("-1"); fail("should have failed as runOnAllAgents' value is not valid(boolean)"); } catch (Exception e) { assertThat(e.getMessage(), containsString("'-1' is not facet-valid with respect to minInclusive '1' for type 'positiveInteger'")); } try { loadJobWithRunMultipleInstance("abcd"); fail("should have failed as runOnAllAgents' value is not valid(boolean)"); } catch (Exception e) { assertThat(e.getMessage(), containsString("'abcd' is not a valid value for 'integer'")); } } @Test public void shouldSupportEnvironmentVariablesInAJob() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " <environmentvariables>" + " <variable name='JOB_VARIABLE'><value>job variable</value></variable>" + " </environmentvariables>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; JobConfig jobConfig = new JobConfig("do-something"); jobConfig.addVariable("JOB_VARIABLE", "job variable"); assertThat(cruiseConfig.findJob("pipeline1", "mingle", "do-something"), is(jobConfig)); } @Test public void shouldSupportEnvironmentVariablesInAPipeline() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <environmentvariables>" + " <variable name='PIPELINE_VARIABLE'><value>pipeline variable</value></variable>" + " </environmentvariables>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getVariables(), hasItem(new EnvironmentVariableConfig("PIPELINE_VARIABLE", "pipeline variable"))); } @Test public void shouldSupportEnvironmentVariablesInAStage() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <environmentvariables>" + " <variable name='STAGE_VARIABLE'><value>stage variable</value></variable>" + " </environmentvariables>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getFirstStageConfig().getVariables(), hasItem(new EnvironmentVariableConfig("STAGE_VARIABLE", "stage variable"))); } @Test public void shouldNotAllowDuplicateEnvironmentVariablesInAJob() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " <environmentvariables>" + " <variable name='JOB_VARIABLE'><value>job variable</value></variable>" + " <variable name='JOB_VARIABLE'><value>job variable</value></variable>" + " </environmentvariables>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); try { ConfigMigrator.loadWithMigration(content); fail("Should not allow duplicate variable names"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Environment Variable name 'JOB_VARIABLE' is not unique for job 'do-something'.")); } } @Test public void shouldNotAllowDuplicateParamsInAPipeline() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='dev'>\n" + " <params>" + " <param name='same-name'>ls</param>" + " <param name='same-name'>/tmp</param>" + " </params>" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>"; try { ConfigMigrator.loadWithMigration(content); fail("Should not allow duplicate params"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Param name 'same-name' is not unique for pipeline 'dev'.")); } } @Test public void shouldNotAllowParamsToBeUsedInNames() throws Exception { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts' />" + "<pipelines>\n" + "<pipeline name='dev'>\n" + " <params>" + " <param name='command'>ls</param>" + " </params>" + " <materials>\n" + " <svn url =\"svnurl\"/>" + " </materials>\n" + " <stage name='stage#{command}ab'>" + " <jobs>" + " <job name='job1'>" + " <tasks>" + " <exec command='/bin/#{command}##{b}' args='#{dir}'/>" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>"; try { ConfigMigrator.loadWithMigration(content); fail("Should not allow params in stage name"); } catch (Exception e) { assertThat(e.getMessage(), containsString("\"stage#{command}ab\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldNotAllowDuplicateEnvironmentVariablesInAPipeline() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <environmentvariables>" + " <variable name='PIPELINE_VARIABLE'><value>pipeline variable</value></variable>" + " <variable name='PIPELINE_VARIABLE'><value>pipeline variable</value></variable>" + " </environmentvariables>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); try { ConfigMigrator.loadWithMigration(content); fail("Should not allow duplicate variable names"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Variable name 'PIPELINE_VARIABLE' is not unique for pipeline 'pipeline1'.")); } } @Test public void shouldNotAllowDuplicateEnvironmentVariablesInAStage() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <environmentvariables>" + " <variable name='STAGE_VARIABLE'><value>stage variable</value></variable>" + " <variable name='STAGE_VARIABLE'><value>stage variable</value></variable>" + " </environmentvariables>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); try { ConfigMigrator.loadWithMigration(content); fail("Should not allow duplicate variable names"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Variable name 'STAGE_VARIABLE' is not unique for stage 'mingle'.")); } } @Test public void shouldNotAllowDuplicateEnvironmentVariablesInAnEnvironment() throws Exception { String content = ConfigFileFixture.configWithEnvironmentsAndAgents( "<environments>" + " <environment name='uat'>" + " <environmentvariables> " + " <variable name='FOO'><value>foo</value></variable>" + " <variable name='FOO'><value>foo</value></variable>" + " </environmentvariables> " + " </environment>" + "</environments>", "<agents>" + " <agent uuid='1' hostname='test1.com' ipaddress='192.168.0.1' isDisabled='true' />" + "</agents>"); try { ConfigMigrator.loadWithMigration(content); fail("Should not allow duplicate variable names"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Variable name 'FOO' is not unique for environment 'uat'.")); } } @Test public void shouldAllowParamsInEnvironmentVariablesInAPipeline() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <params>" + " <param name=\"some_param\">param_name</param>" + " </params>" + " <environmentvariables>" + " <variable name='#{some_param}'><value>stage variable</value></variable>" + " </environmentvariables>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(content).config; assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getVariables(), hasItem(new EnvironmentVariableConfig("param_name", "stage variable"))); } @Test public void shouldSupportRunOnAllAgents() throws Exception { CruiseConfig cruiseConfig = loadJobWithRunOnAllAgents("true"); JobConfig job = cruiseConfig.findJob("pipeline1", "mingle", "do-something"); JobConfig jobConfig = new JobConfig("do-something"); jobConfig.setRunOnAllAgents(true); assertThat(job, is(jobConfig)); } @Test public void shouldSupportRunMultipleInstance() throws Exception { CruiseConfig cruiseConfig = loadJobWithRunMultipleInstance("10"); JobConfig job = cruiseConfig.findJob("pipeline1", "mingle", "do-something"); JobConfig jobConfig = new JobConfig("do-something"); jobConfig.setRunInstanceCount(10); assertThat(job, is(jobConfig)); } @Test public void shouldUnderstandEncryptedPasswordAttributeForSvnMaterial() throws Exception { String password = "abc"; String encryptedPassword = new GoCipher().encrypt(password); String content = ConfigFileFixture.configWithPipeline(format( "<pipeline name='pipeline1'>" + " <materials>" + " <svn url='svnurl' username='admin' encryptedPassword='%s'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", encryptedPassword), CONFIG_SCHEMA_VERSION); GoConfigHolder configHolder = ConfigMigrator.loadWithMigration(content); CruiseConfig cruiseConfig = configHolder.config; SvnMaterialConfig svnMaterialConfig = (SvnMaterialConfig) cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).materialConfigs().get(0); assertThat(svnMaterialConfig.getEncryptedPassword(), is(encryptedPassword)); assertThat(svnMaterialConfig.getPassword(), is(password)); CruiseConfig configForEdit = configHolder.configForEdit; svnMaterialConfig = (SvnMaterialConfig) configForEdit.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).materialConfigs().get(0); assertThat(svnMaterialConfig.getEncryptedPassword(), is(encryptedPassword)); assertThat(svnMaterialConfig.getPassword(), is("abc")); assertThat(ReflectionUtil.getField(svnMaterialConfig, "password"), is(nullValue())); } @Test public void shouldSupportEmptyPipelineGroup() throws Exception { PipelineConfigs group = new BasicPipelineConfigs("defaultGroup", new Authorization()); CruiseConfig config = new BasicCruiseConfig(group); ByteArrayOutputStream stream = new ByteArrayOutputStream(); new MagicalGoConfigXmlWriter(configCache, ConfigElementImplementationRegistryMother.withNoPlugins()).write(config, stream, true); GoConfigHolder configHolder = new MagicalGoConfigXmlLoader(new ConfigCache(), ConfigElementImplementationRegistryMother.withNoPlugins()) .loadConfigHolder(stream.toString()); assertThat(configHolder.config.findGroup("defaultGroup"), is(group)); } private CruiseConfig loadJobWithRunOnAllAgents(String value) throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something' runOnAllAgents='" + value + "'/>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); return ConfigMigrator.loadWithMigration(content).config; } private CruiseConfig loadJobWithRunMultipleInstance(String value) throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + " <materials>" + " <svn url ='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something' runInstanceCount='" + value + "'/>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); return ConfigMigrator.loadWithMigration(content).config; } private void assertValidMaterials(String materials) throws Exception { createConfig(materials); } private CruiseConfig createConfig(String materials) throws Exception { String pipelineXmlPartial = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<cruise " + " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " + " xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" " + " schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + " <server artifactsdir=\"logs\">\n" + " </server>\n" + "<pipelines>\n" + " <pipeline name=\"pipeline-name\">\n" + materials + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\">\n" + " <artifacts>\n" + " <artifact src=\"artifact1.xml\" dest=\"cruise-output\" />\n" + " </artifacts>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>\n" + "</pipelines>" + "</cruise>\n"; return ConfigMigrator.loadWithMigration(toInputStream(pipelineXmlPartial)).config; } @Test public void shouldAllowResourcesWithParamsForJobs() throws Exception { CruiseConfig cruiseConfig = new BasicCruiseConfig(); cruiseConfig.addTemplate(new PipelineTemplateConfig(new CaseInsensitiveString("template"), stageWithJobResource("#{PLATFORM}"))); PipelineConfig pipelineConfig = new PipelineConfig(new CaseInsensitiveString("pipeline"), new MaterialConfigs()); pipelineConfig.setTemplateName(new CaseInsensitiveString("template")); pipelineConfig.addParam(new ParamConfig("PLATFORM", "windows")); cruiseConfig.addPipeline("group", pipelineConfig); List<ConfigErrors> errorses = MagicalGoConfigXmlLoader.validate(cruiseConfig); assertThat(errorses.isEmpty(), is(true)); } //BUG: #5209 @Test public void shouldAllowRoleWithParamsForStageInTemplate() throws Exception { CruiseConfig cruiseConfig = new BasicCruiseConfig(); cruiseConfig.server().security().addRole(new RoleConfig(new CaseInsensitiveString("role"))); cruiseConfig.addTemplate(new PipelineTemplateConfig(new CaseInsensitiveString("template"), stageWithAuth("#{ROLE}"))); PipelineConfig pipelineConfig = new PipelineConfig(new CaseInsensitiveString("pipeline"), new MaterialConfigs()); pipelineConfig.setTemplateName(new CaseInsensitiveString("template")); pipelineConfig.addParam(new ParamConfig("ROLE", "role")); cruiseConfig.addPipeline("group", pipelineConfig); List<ConfigErrors> errorses = MagicalGoConfigXmlLoader.validate(cruiseConfig); assertThat(errorses.isEmpty(), is(true)); } private StageConfig stageWithAuth(String role) { StageConfig stage = stageWithJobResource("foo"); stage.getApproval().getAuthConfig().add(new AdminRole(new CaseInsensitiveString(role))); return stage; } @Test public void shouldAllowOnlyOneOfTrackingToolOrMingleConfigInSourceXml() { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='pipeline1'>" + "<trackingtool link=\"https://some-tracking-tool/projects/go/cards/${ID}\" regex=\"##(\\d+)\" />" + " <mingle baseUrl=\"https://some-tracking-tool/\" projectIdentifier=\"go\">" + " <mqlGroupingConditions>status > 'In Dev'</mqlGroupingConditions>" + " </mingle>" + " <materials>" + " <svn url='svnurl'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); try { ConfigMigrator.loadWithMigration(content); fail("Should not allow mingle config and tracking tool together"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Invalid content was found starting with element 'mingle'.")); } } @Test public void shouldAllowTFSMaterial() { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + " <materials>" + " <tfs url='tfsurl' username='foo' password='bar' projectPath='project-path' />" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); GoConfigHolder goConfigHolder = ConfigMigrator.loadWithMigration(content); MaterialConfigs materialConfigs = goConfigHolder.config.pipelineConfigByName(new CaseInsensitiveString("some_pipeline")).materialConfigs(); assertThat(materialConfigs.size(), is(1)); TfsMaterialConfig materialConfig = (TfsMaterialConfig) materialConfigs.get(0); assertThat(materialConfig, is(new TfsMaterialConfig(new GoCipher(), UrlArgument.create("tfsurl"), "foo", "", "bar", "project-path"))); } @Test public void shouldAllowAnEnvironmentVariableToBeMarkedAsSecure_WithValueInItsOwnTag() throws Exception { String cipherText = new GoCipher().encrypt("plainText"); String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + "<environmentvariables>\n" + " <variable name=\"var_name\" secure=\"true\"><encryptedValue>" + cipherText + "</encryptedValue></variable>\n" + " </environmentvariables>" + " <materials>" + " <svn url='svnurl'/>" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString("some_pipeline")); EnvironmentVariablesConfig variables = pipelineConfig.getVariables(); assertThat(variables.size(), is(1)); EnvironmentVariableConfig environmentVariableConfig = variables.get(0); assertThat(environmentVariableConfig.getEncryptedValue(), is(cipherText)); assertThat(environmentVariableConfig.isSecure(), is(true)); } @Test public void shouldMigrateEmptyEnvironmentVariable() throws Exception { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + "<environmentvariables>\n" + " <variable name=\"var_name\" />\n" + " </environmentvariables>" + " <materials>" + " <svn url='svnurl'/>" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", 48); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString("some_pipeline")); EnvironmentVariablesConfig variables = pipelineConfig.getVariables(); assertThat(variables.size(), is(1)); EnvironmentVariableConfig environmentVariableConfig = variables.get(0); assertThat(environmentVariableConfig.getName(), is("var_name")); assertThat(environmentVariableConfig.getValue().isEmpty(), is(true)); } @Test public void should_NOT_AllowTFSMaterial_toHaveWorkSpaceNameLongerThan_30_characters() { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + " <materials>" + " <tfs url='tfsurl' username='foo' password='bar' workspace='0123456789012345678901234567890' projectPath='project-path' />" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", 46); Exception ex = null; try { ConfigMigrator.loadWithMigration(content); fail("Should have failed!"); } catch (Exception e) { ex = e; } assertThat(ex.getMessage(), containsString("Value '0123456789012345678901234567890' with length = '31' is not facet-valid with respect to maxLength '30' for type 'tfsWorkspaceType'")); } @Test public void shouldAllowAnEnvironmentVariableToBeMarkedAsSecure_WithEncryptedValueInItsOwnTag() throws Exception { String value = "abc"; String encryptedValue = new GoCipher().encrypt(value); String content = ConfigFileFixture.configWithPipeline(format( "<pipeline name='some_pipeline'>" + "<environmentvariables>\n" + " <variable name=\"var_name\" secure=\"true\"><encryptedValue>%s</encryptedValue></variable>\n" + " </environmentvariables>" + " <materials>" + " <svn url='svnurl'/>" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", encryptedValue), CONFIG_SCHEMA_VERSION); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString("some_pipeline")); EnvironmentVariablesConfig variables = pipelineConfig.getVariables(); assertThat(variables.size(), is(1)); EnvironmentVariableConfig environmentVariableConfig = variables.get(0); assertThat(environmentVariableConfig.getEncryptedValue(), is(encryptedValue)); assertThat(environmentVariableConfig.isSecure(), is(true)); } @Test public void shouldNotAllowWorkspaceOwnerAndWorkspaceAsAttributesOnTfsMaterial() { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + " <materials>" + " <tfs url='tfsurl' username='foo' password='bar' projectPath='project-path' />" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); try { ConfigMigrator.loadWithMigration(content); } catch (Exception e) { fail("Valid TFS tag for migration 51 and above"); } } @Test public void shouldMigrateConfigToSplitUsernameAndDomainAsAttributeOnTfsMaterial() { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + " <materials>" + " <tfs url='tfsurl' username='domain\\username' password='bar' projectPath='project-path' />" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", 52); try { CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; PipelineConfig pipeline = config.pipelineConfigByName(new CaseInsensitiveString("some_pipeline")); TfsMaterialConfig material = (TfsMaterialConfig) pipeline.materialConfigs().get(0); assertThat(material.getUsername(), is("username")); assertThat(material.getDomain(), is("domain")); } catch (Exception e) { fail("Valid TFS tag for migration 51 and above"); } } @Test public void shouldAllowUserToSpecify_PathFromAncestor_forFetchArtifactFromAncestor() { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='uppest_pipeline'>" + " <materials>" + " <git url=\"foo\" />" + " </materials>" + " <stage name='uppest_stage'>" + " <jobs>" + " <job name='uppest_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>" + "<pipeline name='up_pipeline'>" + " <materials>" + " <pipeline pipelineName=\"uppest_pipeline\" stageName=\"uppest_stage\"/>" + " </materials>" + " <stage name='up_stage'>" + " <jobs>" + " <job name='up_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>" + "<pipeline name='down_pipeline'>" + " <materials>" + " <pipeline pipelineName=\"up_pipeline\" stageName=\"up_stage\"/>" + " </materials>" + " <stage name='down_stage'>" + " <jobs>" + " <job name='down_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>" + "<pipeline name='downest_pipeline'>" + " <materials>" + " <pipeline pipelineName=\"down_pipeline\" stageName=\"down_stage\"/>" + " </materials>" + " <stage name='downest_stage'>" + " <jobs>" + " <job name='downest_job'>" + " <tasks>" + " <fetchartifact pipeline=\"uppest_pipeline/up_pipeline/down_pipeline\" stage=\"uppest_stage\" job=\"uppest_job\" srcfile=\"src\" dest=\"dest\"/>" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); GoConfigHolder holder = ConfigMigrator.loadWithMigration(content); assertThat(holder.config.pipelineConfigByName(new CaseInsensitiveString("downest_pipeline")).getFetchTasks().get(0), is(new FetchTask(new CaseInsensitiveString("uppest_pipeline/up_pipeline/down_pipeline"), new CaseInsensitiveString("uppest_stage"), new CaseInsensitiveString("uppest_job"), "src", "dest"))); } @Test public void should_NOT_allowUserToSpecifyFetchStage_afterUpstreamStage() { String content = ConfigFileFixture.configWithPipeline( "<pipeline name='up_pipeline'>" + " <materials>" + " <git url=\"/tmp/git\"/>" + " </materials>" + " <stage name='up_stage'>" + " <jobs>" + " <job name='up_job'>" + " </job>" + " </jobs>" + " </stage>" + " <stage name='up_stage_2'>" + " <jobs>" + " <job name='up_job'>" + " </job>" + " </jobs>" + " </stage>" + " <stage name='up_stage_3'>" + " <jobs>" + " <job name='up_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>" + "<pipeline name='down_pipeline'>" + " <materials>" + " <pipeline pipelineName=\"up_pipeline\" stageName=\"up_stage\"/>" + " </materials>" + " <stage name='down_stage'>" + " <jobs>" + " <job name='down_job'>" + " <tasks>" + " <fetchartifact pipeline=\"up_pipeline\" stage=\"up_stage_2\" job=\"up_job\" srcfile=\"src\" dest=\"dest\"/>" + " </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", CONFIG_SCHEMA_VERSION); try { ConfigMigrator.loadWithMigration(content); fail("should not have permitted fetch from parent pipeline's stage after the one downstream depends on"); } catch (Exception e) { assertThat(e.getMessage(), containsString( "\"down_pipeline :: down_stage :: down_job\" tries to fetch artifact from stage \"up_pipeline :: up_stage_2\" which does not complete before \"down_pipeline\" pipeline's dependencies.")); } } @Test public void shouldAddDefaultCommndRepositoryLocationIfNoValueIsGiven() { String content = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifactsDir'>" + "</server>" + "<pipelines>" + "<pipeline name='some_pipeline'>" + " <materials>" + " <hg url='hgurl' />" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>" + "</pipelines>" + "</cruise>"; try { GoConfigHolder goConfigHolder = ConfigMigrator.loadWithMigration(content); assertThat(goConfigHolder.config.server().getCommandRepositoryLocation(), is("default")); } catch (Exception e) { fail("Should not come here"); } } @Test public void shouldDeserializeGroupXml() throws Exception { String partialXml = "<pipelines group=\"group_name\">\n" + " <pipeline name=\"new_name\">\n" + " <materials>\n" + " <svn url=\"file:///tmp/foo\" />\n" + " </materials>\n" + " <stage name=\"stage_name\">\n" + " <jobs>\n" + " <job name=\"job_name\" />\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>\n" + "</pipelines>"; PipelineConfigs pipelineConfigs = xmlLoader.fromXmlPartial(partialXml, BasicPipelineConfigs.class); PipelineConfig pipeline = pipelineConfigs.findBy(new CaseInsensitiveString("new_name")); assertThat(pipeline, is(notNullValue())); assertThat(pipeline.materialConfigs().size(), is(1)); MaterialConfig material = pipeline.materialConfigs().get(0); assertThat(material, is(Matchers.instanceOf(SvnMaterialConfig.class))); assertThat(material.getUriForDisplay(), is("file:///tmp/foo")); assertThat(pipeline.size(), is(1)); assertThat(pipeline.get(0).getJobs().size(), is(1)); } @Test public void shouldRegisterAllGoConfigValidators() { List<String> list = (List<String>) collect(MagicalGoConfigXmlLoader.VALIDATORS, new Transformer() { @Override public Object transform(Object o) { return o.getClass().getCanonicalName(); } }); assertThat(list, hasItem(ArtifactDirValidator.class.getCanonicalName())); assertThat(list, hasItem(EnvironmentAgentValidator.class.getCanonicalName())); assertThat(list, hasItem(EnvironmentPipelineValidator.class.getCanonicalName())); assertThat(list, hasItem(ServerIdImmutabilityValidator.class.getCanonicalName())); assertThat(list, hasItem(CommandRepositoryLocationValidator.class.getCanonicalName())); } @Test public void shouldLoadAfterMigration62() { final String content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<cruise schemaVersion='62'>\n" + " <server artifactsdir=\"artifacts\">\n" + " <security>" + " <ldap uri='some_url' managerDn='some_manager_dn' managerPassword='foo' searchFilter='(sAMAccountName={0})'>" + " <bases>" + " <base value='ou=Enterprise,ou=Principal,dc=corporate,dc=thoughtworks,dc=com'/>" + " </bases>" + " </ldap>" + " </security>" + " </server>" + " </cruise>"; GoConfigHolder goConfigHolder = ConfigMigrator.loadWithMigration(content); assertThat(goConfigHolder.config.server().security().ldapConfig().isEnabled(), is(false)); assertThat(goConfigHolder.config.server().security().securityAuthConfigs().get(0).getProperty("SearchBases").getValue(), is("ou=Enterprise,ou=Principal,dc=corporate,dc=thoughtworks,dc=com")); } @Test public void shouldResolvePackageReferenceElementForAMaterialInConfig() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<repositories>\n" + " <repository id='repo-id' name='name'>\n" + " <pluginConfiguration id='plugin-id' version='1.0'/>\n" + " <configuration>\n" + " <property>\n" + " <key>url</key>\n" + " <value>http://go</value>\n" + " </property>\n" + " </configuration>\n" + " <packages>\n" + " <package id='package-id' name='name'>\n" + " <configuration>\n" + " <property>\n" + " <key>name</key>\n" + " <value>go-agent</value>\n" + " </property>\n" + " </configuration>\n" + " </package>\n" + " </packages>\n" + " </repository>\n" + " </repositories>" + "<pipelines group=\"group_name\">\n" + " <pipeline name=\"new_name\">\n" + " <materials>\n" + " <package ref='package-id' />\n" + " </materials>\n" + " <stage name=\"stage_name\">\n" + " <jobs>\n" + " <job name=\"job_name\" />\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>\n" + "</pipelines></cruise>"; GoConfigHolder goConfigHolder = xmlLoader.loadConfigHolder(xml); PackageDefinition packageDefinition = goConfigHolder.config.getPackageRepositories().first().getPackages().first(); PipelineConfig pipelineConfig = goConfigHolder.config.pipelineConfigByName(new CaseInsensitiveString("new_name")); PackageMaterialConfig packageMaterialConfig = (PackageMaterialConfig) pipelineConfig.materialConfigs().get(0); assertThat(packageMaterialConfig.getPackageDefinition(), is(packageDefinition)); } @Test public void shouldBeAbleToResolveSecureConfigPropertiesForPackages() throws Exception { String encryptedValue = new GoCipher().encrypt("secure-two"); String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<repositories>\n" + " <repository id='repo-id' name='name'>\n" + " <pluginConfiguration id='plugin-id' version='1.0'/>\n" + " <configuration>\n" + " <property>\n" + " <key>plain</key>\n" + " <value>value</value>\n" + " </property>\n" + " <property>\n" + " <key>secure-one</key>\n" + " <value>secure-value</value>\n" + " </property>\n" + " <property>\n" + " <key>secure-two</key>\n" + " <encryptedValue>" + encryptedValue + "</encryptedValue>\n" + " </property>\n" + " </configuration>\n" + " <packages>\n" + " <package id='package-id' name='name'>\n" + " <configuration>\n" + " <property>\n" + " <key>plain</key>\n" + " <value>value</value>\n" + " </property>\n" + " <property>\n" + " <key>secure-one</key>\n" + " <value>secure-value</value>\n" + " </property>\n" + " <property>\n" + " <key>secure-two</key>\n" + " <encryptedValue>" + encryptedValue + "</encryptedValue>\n" + " </property>\n" + " </configuration>\n" + " </package>\n" + " </packages>\n" + " </repository>\n" + " </repositories>" + "<pipelines group=\"group_name\">\n" + " <pipeline name=\"new_name\">\n" + " <materials>\n" + " <package ref='package-id' />\n" + " </materials>\n" + " <stage name=\"stage_name\">\n" + " <jobs>\n" + " <job name=\"job_name\" />\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>\n" + "</pipelines></cruise>"; //meta data of package PackageConfigurations packageConfigurations = new PackageConfigurations(); packageConfigurations.addConfiguration(new PackageConfiguration("plain")); packageConfigurations.addConfiguration(new PackageConfiguration("secure-one").with(PackageConfiguration.SECURE, true)); packageConfigurations.addConfiguration(new PackageConfiguration("secure-two").with(PackageConfiguration.SECURE, true)); PackageMetadataStore.getInstance().addMetadataFor("plugin-id", packageConfigurations); RepositoryMetadataStore.getInstance().addMetadataFor("plugin-id", packageConfigurations); GoConfigHolder goConfigHolder = xmlLoader.loadConfigHolder(xml); PackageDefinition packageDefinition = goConfigHolder.config.getPackageRepositories().first().getPackages().first(); PipelineConfig pipelineConfig = goConfigHolder.config.pipelineConfigByName(new CaseInsensitiveString("new_name")); PackageMaterialConfig packageMaterialConfig = (PackageMaterialConfig) pipelineConfig.materialConfigs().get(0); assertThat(packageMaterialConfig.getPackageDefinition(), is(packageDefinition)); Configuration repoConfig = packageMaterialConfig.getPackageDefinition().getRepository().getConfiguration(); assertThat(repoConfig.get(0).getConfigurationValue().getValue(), is("value")); assertThat(repoConfig.get(1).getEncryptedValue(), is(new GoCipher().encrypt("secure-value"))); assertThat(repoConfig.get(2).getEncryptedValue(), is(encryptedValue)); Configuration packageConfig = packageMaterialConfig.getPackageDefinition().getConfiguration(); assertThat(packageConfig.get(0).getConfigurationValue().getValue(), is("value")); assertThat(packageConfig.get(1).getEncryptedValue(), is(new GoCipher().encrypt("secure-value"))); assertThat(packageConfig.get(2).getEncryptedValue(), is(encryptedValue)); } @Test public void shouldResolvePackageRepoReferenceElementForAPackageInConfig() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<repositories>\n" + " <repository id='repo-id' name='name'>\n" + " <pluginConfiguration id='plugin-id' version='1.0'/>\n" + " <configuration>\n" + " <property>\n" + " <key>url</key>\n" + " <value>http://go</value>\n" + " </property>\n" + " </configuration>\n" + " <packages>\n" + " <package id='package-id' name='name'>\n" + " <configuration>\n" + " <property>\n" + " <key>name</key>\n" + " <value>go-agent</value>\n" + " </property>\n" + " </configuration>\n" + " </package>\n" + " </packages>\n" + " </repository>\n" + " </repositories></cruise>"; GoConfigHolder goConfigHolder = xmlLoader.loadConfigHolder(xml); PackageRepository packageRepository = goConfigHolder.config.getPackageRepositories().first(); PackageDefinition packageDefinition = packageRepository.getPackages().first(); assertThat(packageDefinition.getRepository(), is(packageRepository)); } @Test public void shouldFailValidationIfPackageDefinitionWithDuplicateFingerprintExists() throws Exception { com.thoughtworks.go.plugin.api.material.packagerepository.PackageConfiguration packageConfiguration = new com.thoughtworks.go.plugin.api.material.packagerepository.PackageConfiguration(); packageConfiguration.add(new PackageMaterialProperty("PKG-KEY1")); RepositoryConfiguration repositoryConfiguration = new RepositoryConfiguration(); repositoryConfiguration.add(new PackageMaterialProperty("REPO-KEY1")); repositoryConfiguration.add(new PackageMaterialProperty("REPO-KEY2").with(REQUIRED, false).with(PART_OF_IDENTITY, false)); repositoryConfiguration.add(new PackageMaterialProperty("REPO-KEY3").with(REQUIRED, false).with(PART_OF_IDENTITY, false).with(SECURE, true)); PackageMetadataStore.getInstance().addMetadataFor("plugin-1", new PackageConfigurations(packageConfiguration)); RepositoryMetadataStore.getInstance().addMetadataFor("plugin-1", new PackageConfigurations(repositoryConfiguration)); String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<repositories>\n" + " <repository id='repo-id-1' name='name-1'>\n" + " <pluginConfiguration id='plugin-1' version='1.0'/>\n" + " <configuration>\n" + " <property>\n" + " <key>REPO-KEY1</key>\n" + " <value>repo-key1</value>\n" + " </property>\n" + " <property>\n" + " <key>REPO-KEY2</key>\n" + " <value>repo-key2</value>\n" + " </property>\n" + " <property>\n" + " <key>REPO-KEY3</key>\n" + " <value>repo-key3</value>\n" + " </property>\n" + " </configuration>\n" + " <packages>\n" + " <package id='package-id-1' name='name-1'>\n" + " <configuration>\n" + " <property>\n" + " <key>PKG-KEY1</key>\n" + " <value>pkg-key1</value>\n" + " </property>\n" + " </configuration>\n" + " </package>\n" + " </packages>\n" + " </repository>\n" + " <repository id='repo-id-2' name='name-2'>\n" + " <pluginConfiguration id='plugin-1' version='1.0'/>\n" + " <configuration>\n" + " <property>\n" + " <key>REPO-KEY1</key>\n" + " <value>repo-key1</value>\n" + " </property>\n" + " <property>\n" + " <key>REPO-KEY2</key>\n" + " <value>another-repo-key2</value>\n" + " </property>\n" + " <property>\n" + " <key>REPO-KEY3</key>\n" + " <value>another-repo-key3</value>\n" + " </property>\n" + " </configuration>\n" + " <packages>\n" + " <package id='package-id-2' name='name-2'>\n" + " <configuration>\n" + " <property>\n" + " <key>PKG-KEY1</key>\n" + " <value>pkg-key1</value>\n" + " </property>\n" + " </configuration>\n" + " </package>\n" + " </packages>\n" + " </repository>\n" + " </repositories>" + "</cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown duplicate fingerprint exception"); } catch (GoConfigInvalidException e) { assertThat(e.getMessage(), is("Cannot save package or repo, found duplicate packages. [Repo Name: 'name-1', Package Name: 'name-1'], [Repo Name: 'name-2', Package Name: 'name-2']")); } } final static String REPO = " <repository id='repo-id' name='name1'><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration>%s</repository>"; final static String REPO_WITH_NAME = " <repository id='%s' name='%s'><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration>%s</repository>"; final static String REPO_WITH_MISSING_ID = " <repository name='name1'><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration><packages>%s</packages></repository>"; final static String REPO_WITH_INVALID_ID = " <repository id='id with space' name='name1'><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration>%s</repository>"; final static String REPO_WITH_EMPTY_ID = " <repository id='' name='name1'><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration>%s</repository>"; final static String REPO_WITH_MISSING_NAME = " <repository id='id' ><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration>%s</repository>"; final static String REPO_WITH_INVALID_NAME = " <repository id='id' name='name with space'><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration>%s</repository>"; final static String REPO_WITH_EMPTY_NAME = " <repository id='id' name=''><pluginConfiguration id='id' version='1.0'/><configuration><property><key>url</key><value>http://go</value></property></configuration>%s</repository>"; final static String PACKAGE = "<package id='package-id' name='name'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>"; final static String PACKAGE_WITH_MISSING_ID = "<package name='name'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>"; final static String PACKAGE_WITH_INVALID_ID = "<package id='id with space' name='name'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>"; final static String PACKAGE_WITH_EMPTY_ID = "<package id='' name='name'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>"; final static String PACKAGE_WITH_MISSING_NAME = "<package id='id'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>"; final static String PACKAGE_WITH_INVALID_NAME = "<package id='id' name='name with space'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>"; final static String PACKAGE_WITH_EMPTY_NAME = "<package id='id' name=''><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>"; private String withPackages(String repo, String packages) { return format(repo, packages); } @Test public void shouldThrowXsdValidationWhenPackageRepositoryIdsAreDuplicate() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO, "") + withPackages(REPO, "") + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Duplicate unique value [repo-id] declared for identity constraint \"uniqueRepositoryId\" of element \"repositories\".")); } } @Test public void shouldThrowXsdValidationWhenPackageRepositoryNamesAreDuplicate() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + format(REPO_WITH_NAME, "1", "repo", "") + format(REPO_WITH_NAME, "2", "repo", "") + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Duplicate unique value [repo] declared for identity constraint \"uniqueRepositoryName\" of element \"repositories\".")); } } @Test public void shouldThrowXsdValidationWhenPackageIdsAreDuplicate() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO, format("<packages>%s%s</packages>", PACKAGE, PACKAGE)) + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Duplicate unique value [package-id] declared for identity constraint \"uniquePackageId\" of element \"cruise\".")); } } @Test public void shouldThrowXsdValidationWhenPackageRepositoryIdIsEmpty() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_EMPTY_ID, "") + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Repo id is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldThrowXsdValidationWhenPackageRepositoryIdIsInvalid() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_INVALID_ID, "") + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Repo id is invalid. \"id with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldThrowXsdValidationWhenPackageRepositoryNameIsMissing() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_MISSING_NAME, "") + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("\"Name\" is required for Repository")); } } @Test public void shouldThrowXsdValidationWhenPackageRepositoryNameIsEmpty() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_EMPTY_NAME, "") + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Name is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldThrowXsdValidationWhenPackageRepositoryNameIsInvalid() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_INVALID_NAME, "") + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Name is invalid. \"name with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldGenerateRepoAndPkgIdWhenMissing() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_MISSING_ID, PACKAGE_WITH_MISSING_ID) + " </repositories></cruise>"; GoConfigHolder configHolder = xmlLoader.loadConfigHolder(xml); assertThat(configHolder.config.getPackageRepositories().get(0).getId(), is(notNullValue())); assertThat(configHolder.config.getPackageRepositories().get(0).getPackages().get(0).getId(), is(notNullValue())); } @Test public void shouldThrowXsdValidationWhenPackageIdIsEmpty() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_EMPTY_ID, PACKAGE_WITH_EMPTY_ID) + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Repo id is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldThrowXsdValidationWhenPackageIdIsInvalid() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_INVALID_ID, PACKAGE_WITH_INVALID_ID) + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Repo id is invalid. \"id with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldThrowXsdValidationWhenPackageNameIsMissing() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_MISSING_NAME, PACKAGE_WITH_MISSING_NAME) + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("\"Name\" is required for Repository")); } } @Test public void shouldThrowXsdValidationWhenPackageNameIsEmpty() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_EMPTY_NAME, PACKAGE_WITH_EMPTY_NAME) + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Name is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldThrowXsdValidationWhenPackageNameIsInvalid() throws Exception { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n" + withPackages(REPO_WITH_INVALID_NAME, PACKAGE_WITH_INVALID_NAME) + " </repositories></cruise>"; try { xmlLoader.loadConfigHolder(xml); fail("should have thrown XsdValidationException"); } catch (XsdValidationException e) { assertThat(e.getMessage(), is("Name is invalid. \"name with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*")); } } @Test public void shouldLoadAutoUpdateValueForPackageWhenLoadedFromConfigFile() throws Exception { String configTemplate = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>" + "<repositories>" + " <repository id='2ef830d7-dd66-42d6-b393-64a84646e557' name='GoYumRepo'>" + " <pluginConfiguration id='yum' version='1' />" + " <configuration>" + " <property>" + " <key>REPO_URL</key>" + " <value>http://fake-yum-repo/go/yum/no-arch</value>" + " </property>" + " </configuration>" + " <packages>" + " <package id='88a3beca-cbe2-4c4d-9744-aa0cda3f371c' name='1' autoUpdate='%s'>" + " <configuration>" + " <property>" + " <key>REPO_URL</key>" + " <value>http://fake-yum-repo/go/yum/no-arch</value>" + " </property>" + " </configuration>" + " </package>" + " </packages>" + " </repository>" + "</repositories>" + "</cruise>"; String configContent = String.format(configTemplate, false); GoConfigHolder holder = xmlLoader.loadConfigHolder(configContent); PackageRepository packageRepository = holder.config.getPackageRepositories().find("2ef830d7-dd66-42d6-b393-64a84646e557"); PackageDefinition aPackage = packageRepository.findPackage("88a3beca-cbe2-4c4d-9744-aa0cda3f371c"); assertThat(aPackage.isAutoUpdate(), is(false)); configContent = String.format(configTemplate, true); holder = xmlLoader.loadConfigHolder(configContent); packageRepository = holder.config.getPackageRepositories().find("2ef830d7-dd66-42d6-b393-64a84646e557"); aPackage = packageRepository.findPackage("88a3beca-cbe2-4c4d-9744-aa0cda3f371c"); assertThat(aPackage.isAutoUpdate(), is(true)); } @Test public void shouldAllowColonsInPipelineLabelTemplate() { String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<repositories>\n" + " <repository id='repo-id' name='repo_name'>\n" + " <pluginConfiguration id='plugin-id' version='1.0'/>\n" + " <configuration>\n" + " <property>\n" + " <key>url</key>\n" + " <value>http://go</value>\n" + " </property>\n" + " </configuration>\n" + " <packages>\n" + " <package id='package-id' name='pkg_name'>\n" + " <configuration>\n" + " <property>\n" + " <key>name</key>\n" + " <value>go-agent</value>\n" + " </property>\n" + " </configuration>\n" + " </package>\n" + " </packages>\n" + " </repository>\n" + " </repositories>" + "<pipelines group=\"group_name\">\n" + " <pipeline name=\"new_name\" labeltemplate=\"${COUNT}-${repo_name:pkg_name}\">\n" + " <materials>\n" + " <package ref='package-id' />\n" + " </materials>\n" + " <stage name=\"stage_name\">\n" + " <jobs>\n" + " <job name=\"job_name\" />\n" + " </jobs>\n" + " </stage>\n" + " </pipeline>\n" + "</pipelines></cruise>"; GoConfigHolder holder = ConfigMigrator.loadWithMigration(xml); assertThat(holder.config.getAllPipelineConfigs().get(0).materialConfigs().get(0).getName().toString(), is("repo_name:pkg_name")); } @Test public void shouldAllowEmptyAuthorizationTagUnderEachTemplateWhileLoading() throws Exception { String configString = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + " <templates>" + " <pipeline name='template-name'>" + " <authorization>" + " <admins>" + " </admins>" + " </authorization>" + " <stage name='stage-name'>" + " <jobs>" + " <job name='job-name'/>" + " </jobs>" + " </stage>" + " </pipeline>" + " </templates>" + "</cruise>"; CruiseConfig configForEdit = ConfigMigrator.loadWithMigration(configString).configForEdit; PipelineTemplateConfig template = configForEdit.getTemplateByName(new CaseInsensitiveString("template-name")); Authorization authorization = template.getAuthorization(); assertThat(authorization, is(not(nullValue()))); assertThat(authorization.getAdminsConfig().getUsers(), is(empty())); assertThat(authorization.getAdminsConfig().getRoles(), is(empty())); } @Test public void shouldAllowPluggableTaskConfiguration() throws Exception { String configString = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + " <pipelines>" + "<pipeline name='pipeline1'>" + " <materials>" + " <svn url='svnurl' username='admin' password='%s'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'><tasks>" + " <task>" + " <pluginConfiguration id='plugin-id-1' version='1.0'/>" + " <configuration>" + " <property><key>url</key><value>http://fake-go-server</value></property>" + " <property><key>username</key><value>godev</value></property>" + " <property><key>password</key><value>password</value></property>" + " </configuration>" + " </task> </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline></pipelines>" + "</cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configString).configForEdit; PipelineConfig pipelineConfig = cruiseConfig.getAllPipelineConfigs().get(0); JobConfig jobConfig = pipelineConfig.getFirstStageConfig().getJobs().get(0); Tasks tasks = jobConfig.getTasks(); assertThat(tasks.size(), is(1)); assertThat(tasks.get(0) instanceof PluggableTask, is(true)); PluggableTask task = (PluggableTask) tasks.get(0); assertThat(task.getTaskType(), is("pluggable_task_plugin_id_1")); assertThat(task.getTypeForDisplay(), is("Pluggable Task")); final Configuration configuration = task.getConfiguration(); assertThat(configuration.listOfConfigKeys().size(), is(3)); assertThat(configuration.listOfConfigKeys(), is(asList("url", "username", "password"))); Collection values = CollectionUtils.collect(configuration.listOfConfigKeys(), new Transformer() { @Override public Object transform(Object o) { ConfigurationProperty property = configuration.getProperty((String) o); return property.getConfigurationValue().getValue(); } }); assertThat(new ArrayList<>(values), is(asList("http://fake-go-server", "godev", "password"))); } @Test public void shouldBeAbleToResolveSecureConfigPropertiesForPluggableTasks() throws Exception { String encryptedValue = new GoCipher().encrypt("password"); String configString = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + " <pipelines>" + "<pipeline name='pipeline1'>" + " <materials>" + " <svn url='svnurl' username='admin' password='%s'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'><tasks>" + " <task>" + " <pluginConfiguration id='plugin-id-1' version='1.0'/>" + " <configuration>" + " <property><key>username</key><value>godev</value></property>" + " <property><key>password</key><value>password</value></property>" + " </configuration>" + " </task> </tasks>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline></pipelines>" + "</cruise>"; //meta data of package PluggableTaskConfigStore.store().setPreferenceFor("plugin-id-1", new TaskPreference(new com.thoughtworks.go.plugin.api.task.Task() { @Override public TaskConfig config() { TaskConfig taskConfig = new TaskConfig(); taskConfig.addProperty("username").with(Property.SECURE, false); taskConfig.addProperty("password").with(Property.SECURE, true); return taskConfig; } @Override public TaskExecutor executor() { return null; } @Override public TaskView view() { return null; } @Override public ValidationResult validate(TaskConfig configuration) { return null; } })); GoConfigHolder goConfigHolder = xmlLoader.loadConfigHolder(configString); PipelineConfig pipelineConfig = goConfigHolder.config.pipelineConfigByName(new CaseInsensitiveString("pipeline1")); PluggableTask task = (PluggableTask) pipelineConfig.getStage("mingle").getJobs().getJob(new CaseInsensitiveString("do-something")).getTasks().first(); assertFalse(task.getConfiguration().getProperty("username").isSecure()); assertTrue(task.getConfiguration().getProperty("password").isSecure()); } @Test public void shouldAllowTemplateViewConfigToBeSpecified() { String configXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<cruise xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " + " xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifactsDir'>" + " <security>" + " <roles>" + " <role name='role1'>" + " <users>" + " <user>jyoti</user>" + " <user>duck</user>" + " </users>" + " </role>" + " </roles>" + " </security>" + " </server>" + " <templates>" + " <pipeline name='template1'>" + " <authorization>" + " <view>" + " <user>foo</user>" + " <role>role1</role>" + " </view>" + " </authorization>" + " <stage name='build'>" + " <jobs>" + " <job name='test1'>" + " </job>" + " </jobs>" + " </stage>" + " </pipeline>" + " </templates>" + "</cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configXml).config; ViewConfig expectedViewConfig = new ViewConfig(new AdminUser(new CaseInsensitiveString("foo")), new AdminRole(new RoleConfig(new CaseInsensitiveString("role1"), new RoleUser("duck"), new RoleUser("jyoti")))); assertThat(cruiseConfig.getTemplateByName(new CaseInsensitiveString("template1")).getAuthorization().getViewConfig(), is(expectedViewConfig)); } @Test public void shouldAllowPipelineGroupAdminsToViewTemplateByDefault() { String configXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<cruise xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " + " xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifactsDir'>" + " <security>" + " <roles>" + " <role name='role1'>" + " <users>" + " <user>jyoti</user>" + " <user>duck</user>" + " </users>" + " </role>" + " </roles>" + " </security>" + " </server>" + " <templates>" + " <pipeline name='template1'>" + " <authorization>" + " <admins>" + " <user>foo</user>" + " <role>role1</role>" + " </admins>" + " </authorization>" + " <stage name='build'>" + " <jobs>" + " <job name='test1'>" + " </job>" + " </jobs>" + " </stage>" + " </pipeline>" + " </templates>" + "</cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configXml).config; assertThat(cruiseConfig.getTemplateByName(new CaseInsensitiveString("template1")).getAuthorization().isAllowGroupAdmins(), is(true)); } @Test public void shouldNotAllowGroupAdminsToViewTemplateIfTheOptionIsDisabled() { String configXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<cruise xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " + " xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifactsDir'>" + " <security>" + " <roles>" + " <role name='role1'>" + " <users>" + " <user>jyoti</user>" + " <user>duck</user>" + " </users>" + " </role>" + " </roles>" + " </security>" + " </server>" + " <templates>" + " <pipeline name='template1'>" + " <authorization allGroupAdminsAreViewers='false'>" + " <admins>" + " <user>foo</user>" + " <role>role1</role>" + " </admins>" + " </authorization>" + " <stage name='build'>" + " <jobs>" + " <job name='test1'>" + " </job>" + " </jobs>" + " </stage>" + " </pipeline>" + " </templates>" + "</cruise>"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configXml).config; assertThat(cruiseConfig.getTemplateByName(new CaseInsensitiveString("template1")).getAuthorization().isAllowGroupAdmins(), is(false)); } @Test public void shouldSerializeJobElasticProfileId() throws Exception { String configWithJobElasticProfileId = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server>\n" + " <elastic jobStarvationTimeout=\"10\">\n" + " <profiles>\n" + " <profile id='unit-test' pluginId='aws'>\n" + " <property>\n" + " <key>instance-type</key>\n" + " <value>m1.small</value>\n" + " </property>\n" + " </profile>\n" + " </profiles>\n" + " </elastic>\n" + "</server>\n" + "<pipelines group=\"first\">\n" + "<pipeline name=\"pipeline\">\n" + " <materials>\n" + " <hg url=\"/hgrepo\"/>\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\" elasticProfileId=\"unit-test\">\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>\n"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configWithJobElasticProfileId).configForEdit; String elasticProfileId = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline")).getStage("mingle").jobConfigByConfigName("functional").getElasticProfileId(); assertThat(elasticProfileId, is("unit-test")); } @Test public void shouldSerializeElasticAgentProfiles() throws Exception { String configWithElasticProfile = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<server artifactsdir='artifacts'>\n" + " <elastic jobStarvationTimeout=\"2\">\n" + " <profiles>\n" + " <profile id=\"foo\" pluginId=\"docker\">\n" + " <property>\n" + " <key>USERNAME</key>\n" + " <value>bob</value>\n" + " </property>\n" + " </profile>\n" + " </profiles>\n" + " </elastic>\n" + "</server>\n" + "</cruise>\n"; CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configWithElasticProfile).configForEdit; assertThat(cruiseConfig.server().getElasticConfig().getJobStarvationTimeout(), is(120000L)); assertThat(cruiseConfig.server().getElasticConfig().getProfiles().size(), is(1)); ElasticProfile elasticProfile = cruiseConfig.server().getElasticConfig().getProfiles().find("foo"); assertThat(elasticProfile, is(notNullValue())); assertThat(elasticProfile.getPluginId(), is("docker")); assertThat(elasticProfile.size(), is(1)); assertThat(elasticProfile.getProperty("USERNAME").getValue(), is("bob")); } @Test public void shouldNotAllowJobElasticProfileIdAndResourcesTogether() throws Exception { String configWithJobElasticProfile = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n" + "<pipelines group=\"first\">\n" + "<pipeline name=\"pipeline\">\n" + " <materials>\n" + " <hg url=\"/hgrepo\"/>\n" + " </materials>\n" + " <stage name=\"mingle\">\n" + " <jobs>\n" + " <job name=\"functional\" elasticProfileId=\"docker.unit-test\">\n" + " <resources>\n" + " <resource>foo</resource>\n" + " </resources>\n" + " </job>\n" + " </jobs>\n" + " </stage>\n" + "</pipeline>\n" + "</pipelines>\n" + "</cruise>\n"; try { CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configWithJobElasticProfile).configForEdit; fail("expected exception!"); } catch (Exception e) { assertThat(e.getCause().getCause(), instanceOf(GoConfigInvalidException.class)); assertThat(e.getCause().getCause().getMessage(), is("Job cannot have both `resource` and `elasticProfileId`, No profile defined corresponding to profile_id 'docker.unit-test', Job cannot have both `resource` and `elasticProfileId`")); } } @Test public void shouldGetConfigRepoPreprocessor() { MagicalGoConfigXmlLoader loader = new MagicalGoConfigXmlLoader(null, null); assertThat(loader.getPreprocessorOfType(ConfigRepoPartialPreprocessor.class) instanceof ConfigRepoPartialPreprocessor, is(true)); assertThat(loader.getPreprocessorOfType(ConfigParamPreprocessor.class) instanceof ConfigParamPreprocessor, is(true)); } @Test public void shouldMigrateEncryptedEnvironmentVariablesWithNewlineAndSpaces_XslMigrationFrom88To90() throws Exception { String plainText = "something"; String encryptedValue = new GoCipher().encrypt(plainText); String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue).insert(2, "\r\n" + " ").toString(); String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + "<environmentvariables>\n" + " <variable name=\"var_name\" secure=\"true\"><encryptedValue>" + encryptedValueWithWhitespaceAndNewline + "</encryptedValue></variable>\n" + " </environmentvariables>" + " <materials>" + " <svn url='svnurl'/>" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", 88); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; assertThat(config.allPipelines().get(0).getVariables().get(0).getValue(), is(plainText)); assertThat(config.allPipelines().get(0).getVariables().get(0).getEncryptedValue(), is(encryptedValue)); } @Test public void shouldMigrateEncryptedPluginPropertyValueWithNewlineAndSpaces_XslMigrationFrom88To90() throws Exception { String plainText = "something"; String encryptedValue = new GoCipher().encrypt(plainText); String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue).insert(2, "\r\n" + " ").toString(); String content = ConfigFileFixture.configWithPluggableScm( "<scm id=\"f7c309f5-ea4d-41c5-9c43-95d79fa9ec7b\" name=\"gocd-private\">\n" + " <pluginConfiguration id=\"github.pr\" version=\"1\" />\n" + " <configuration>\n" + " <property>\n" + " <key>plainTextKey</key>\n" + " <value>https://url/some_path</value>\n" + " </property>\n" + " <property>\n" + " <key>secureKey</key>\n" + " <encryptedValue>" + encryptedValueWithWhitespaceAndNewline + "</encryptedValue>\n" + " </property>\n" + " </configuration>\n" + " </scm>", 88); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; assertThat(config.getSCMs().get(0).getConfiguration().getProperty("secureKey").getValue(), is(plainText)); assertThat(config.getSCMs().get(0).getConfiguration().getProperty("secureKey").getEncryptedValue(), is(encryptedValue)); assertThat(config.getSCMs().get(0).getConfiguration().getProperty("plainTextKey").getValue(), is("https://url/some_path")); } @Test public void shouldMigrateEncryptedMaterialPasswordWithNewlineAndSpaces_XslMigrationFrom88To90() throws Exception { String plainText = "something"; String encryptedValue = new GoCipher().encrypt(plainText); String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue).insert(2, "\r\n" + " ").toString(); String content = ConfigFileFixture.configWithPipeline( "<pipeline name='some_pipeline'>" + " <materials>" + " <svn url='asdsa' username='user' encryptedPassword='" + encryptedValueWithWhitespaceAndNewline + "' dest='svn'>" +"<filter>\n" + " <ignore pattern='**/*' />\n" + " </filter>" +"</svn>" +"<tfs url='tfsurl' username='user' domain='domain' encryptedPassword='"+encryptedValueWithWhitespaceAndNewline+"' projectPath='path' dest='tfs' />" +"<p4 port='host:9999' username='user' encryptedPassword='"+encryptedValueWithWhitespaceAndNewline+"' dest='perforce'>\n" + " <view><![CDATA[view]]></view>\n" + " </p4>" + " </materials>" + " <stage name='some_stage'>" + " <jobs>" + " <job name='some_job'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", 88); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; MaterialConfigs materialConfigs = config.allPipelines().get(0).materialConfigs(); SvnMaterialConfig svnMaterialConfig = (SvnMaterialConfig) materialConfigs.get(0); assertThat(svnMaterialConfig.getPassword(), is(plainText)); assertThat(svnMaterialConfig.getEncryptedPassword(), is(encryptedValue)); assertThat(svnMaterialConfig.getFilterAsString(), is("**/*")); TfsMaterialConfig tfs = (TfsMaterialConfig) materialConfigs.get(1); assertThat(tfs.getPassword(), is(plainText)); assertThat(tfs.getEncryptedPassword(), is(encryptedValue)); assertThat(tfs.getUrl(), is("tfsurl")); P4MaterialConfig p4 = (P4MaterialConfig) materialConfigs.get(2); assertThat(p4.getPassword(), is(plainText)); assertThat(p4.getEncryptedPassword(), is(encryptedValue)); assertThat(p4.getServerAndPort(), is("host:9999")); } @Test public void shouldMigrateServerMailhostEncryptedPasswordWithNewlineAndSpaces_XslMigrationFrom88To90() throws Exception { String plainText = "something"; String encryptedValue = new GoCipher().encrypt(plainText); String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue).insert(2, "\r\n" + " ").toString(); String content = ConfigFileFixture.config( "<server artifactsdir='artifacts'>\n" + " <mailhost hostname='host' port='25' username='user' encryptedPassword='"+encryptedValueWithWhitespaceAndNewline+"' tls='false' from='user@domain.com' admin='admin@domain.com' />\n" + " </server>", 88); CruiseConfig config = ConfigMigrator.loadWithMigration(content).config; assertThat(config.server().mailHost().getPassword(), is(plainText)); assertThat(config.server().mailHost().getEncryptedPassword(), is(encryptedValue)); assertThat(config.server().mailHost().getHostName(), is("host")); } @Test public void shouldFailValidationForPipelineWithDuplicateStageNames() throws Exception { try { xmlLoader.loadConfigHolder(ConfigFileFixture.PIPELINES_WITH_DUPLICATE_STAGE_NAME); fail(); } catch (Exception e) { assertTrue(e instanceof RuntimeException); assertThat(e.getMessage(), is("You have defined multiple stages called 'mingle'. Stage names are case-insensitive and must be unique.")); } } @Test public void shouldThrowExceptionIfBuildPlansExistWithTheSameNameWithinAPipeline() throws Exception { try { xmlLoader.loadConfigHolder(ConfigFileFixture.JOBS_WITH_SAME_NAME); } catch (Exception e) { assertTrue(e instanceof XsdValidationException); assertThat(e.getMessage(), is("Duplicate unique value [unit] declared for identity constraint \"uniqueJob\" of element \"jobs\".")); } } @Test public void shouldThrowExceptionIfPipelineDoesNotContainAnyBuildPlans() throws Exception { try { xmlLoader.loadConfigHolder(ConfigFileFixture.STAGE_WITH_NO_JOBS); } catch (Exception e) { assertTrue(e instanceof XsdValidationException); assertThat(e.getMessage(), is("The content of element 'jobs' is not complete. One of '{job}' is expected.")); } } private StageConfig stageWithJobResource(String resourceName) { StageConfig stage = StageConfigMother.custom("stage", "job"); JobConfigs configs = stage.allBuildPlans(); Resource resource = new Resource(); resource.setName(resourceName); configs.get(0).resources().add(resource); return stage; } }