com.thoughtworks.go.config.MagicalGoConfigXmlLoaderTest.java Source code

Java tutorial

Introduction

Here is the source code for com.thoughtworks.go.config.MagicalGoConfigXmlLoaderTest.java

Source

/*
 * Copyright 2019 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.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.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.registry.ConfigElementImplementationRegistrar;
import com.thoughtworks.go.config.registry.ConfigElementImplementationRegistry;
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.rules.Allow;
import com.thoughtworks.go.config.rules.Deny;
import com.thoughtworks.go.config.rules.Rules;
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.MaterialConfigsMother;
import com.thoughtworks.go.helper.StageConfigMother;
import com.thoughtworks.go.junitext.EnhancedOSChecker;
import com.thoughtworks.go.plugin.access.artifact.ArtifactMetadataStore;
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.info.PluginDescriptor;
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.plugin.domain.artifact.ArtifactPluginInfo;
import com.thoughtworks.go.plugin.domain.artifact.Capabilities;
import com.thoughtworks.go.plugin.domain.common.Metadata;
import com.thoughtworks.go.plugin.domain.common.PluggableInstanceSettings;
import com.thoughtworks.go.plugin.domain.common.PluginConfiguration;
import com.thoughtworks.go.plugin.infra.plugininfo.GoPluginDescriptor;
import com.thoughtworks.go.security.AESCipherProvider;
import com.thoughtworks.go.security.AESEncrypter;
import com.thoughtworks.go.security.GoCipher;
import com.thoughtworks.go.security.ResetCipher;
import com.thoughtworks.go.util.*;
import com.thoughtworks.go.util.command.UrlArgument;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.junit.Rule;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;

import static com.thoughtworks.go.config.PipelineConfig.*;
import static com.thoughtworks.go.domain.packagerepository.ConfigurationPropertyMother.create;
import static com.thoughtworks.go.helper.ConfigFileFixture.*;
import static com.thoughtworks.go.helper.MaterialConfigsMother.git;
import static com.thoughtworks.go.helper.MaterialConfigsMother.tfs;
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 java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.*;

@EnableRuleMigrationSupport
public class MagicalGoConfigXmlLoaderTest {
    @Rule
    public final ResetCipher resetCipher = new ResetCipher();

    private MagicalGoConfigXmlLoader xmlLoader;
    private 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();
    private final SystemEnvironment systemEnvironment = new SystemEnvironment();
    private MagicalGoConfigXmlWriter xmlWriter;
    private GoConfigMigration goConfigMigration;

    @BeforeEach
    void setup() {
        RepositoryMetadataStoreHelper.clear();
        ConfigElementImplementationRegistry registry = ConfigElementImplementationRegistryMother.withNoPlugins();
        new ConfigElementImplementationRegistrar(registry).initialize();
        xmlLoader = new MagicalGoConfigXmlLoader(configCache, registry);
        xmlWriter = new MagicalGoConfigXmlWriter(configCache, registry);
        goConfigMigration = new GoConfigMigration(new TimeProvider(), registry);
    }

    @AfterEach
    void tearDown() {
        systemEnvironment.setProperty("go.enforce.server.immutability", "N");
        RepositoryMetadataStoreHelper.clear();
        ArtifactMetadataStore.instance().clear();
    }

    @Test
    void shouldLoadConfigFile() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(CONFIG).config;
        PipelineConfig pipelineConfig1 = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1"));
        assertThat(pipelineConfig1.size()).isEqualTo(2);
        assertThat(pipelineConfig1.getLabelTemplate()).isEqualTo(PipelineLabel.COUNT_TEMPLATE);

        StageConfig stage1 = pipelineConfig1.get(0);
        assertThat(stage1.name()).isEqualTo(new CaseInsensitiveString("stage1"));
        assertThat(stage1.allBuildPlans().size()).isEqualTo(1);
        assertThat(stage1.requiresApproval()).as("Should require approval").isTrue();
        AdminsConfig admins = stage1.getApproval().getAuthConfig();
        assertThat(admins).contains((Admin) new AdminRole(new CaseInsensitiveString("admin")));
        assertThat(admins).contains((Admin) new AdminRole(new CaseInsensitiveString("qa_lead")));
        assertThat(admins).contains((Admin) new AdminUser(new CaseInsensitiveString("jez")));

        StageConfig stage2 = pipelineConfig1.get(1);
        assertThat(stage2.requiresApproval()).as("Should not require approval").isFalse();

        JobConfig plan = stage1.jobConfigByInstanceName("plan1", true);
        assertThat(plan.name()).isEqualTo(new CaseInsensitiveString("plan1"));
        assertThat(plan.resourceConfigs().resourceNames()).contains("tiger", "lion");
        assertThat(plan.getTabs().size()).isEqualTo(2);
        assertThat(plan.getTabs().first().getName()).isEqualTo("Emma");
        assertThat(plan.getTabs().first().getPath()).isEqualTo("logs/emma/index.html");
        assertThat(pipelineConfig1.materialConfigs().size()).isEqualTo(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
    void shouldLoadConfigWithConfigRepo() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(ONE_CONFIG_REPO).config;
        assertThat(cruiseConfig.getConfigRepos().size()).isEqualTo(1);
        ConfigRepoConfig configRepo = cruiseConfig.getConfigRepos().get(0);
        assertThat(configRepo.getMaterialConfig())
                .isEqualTo(git("https://github.com/tomzo/gocd-indep-config-part.git"));
    }

    @Test
    void shouldLoadConfigWithConfigRepoAndPluginName() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(configWithConfigRepos(
                "  <config-repos>\n" + "    <config-repo pluginId=\"myplugin\" id=\"repo-id\">\n"
                        + "      <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n"
                        + "    </config-repo >\n" + "  </config-repos>\n")).config;
        assertThat(cruiseConfig.getConfigRepos().size()).isEqualTo(1);
        ConfigRepoConfig configRepo = cruiseConfig.getConfigRepos().get(0);
        assertThat(configRepo.getPluginId()).isEqualTo("myplugin");
    }

    @Test
    void shouldLoadConfigWith2ConfigRepos() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(configWithConfigRepos(
                "  <config-repos>\n" + "    <config-repo pluginId=\"myplugin\" id=\"repo-id1\">\n"
                        + "      <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n"
                        + "    </config-repo >\n" + "    <config-repo pluginId=\"myplugin\" id=\"repo-id2\">\n"
                        + "      <git url=\"https://github.com/tomzo/gocd-refmain-config-part.git\" />\n"
                        + "    </config-repo >\n" + "  </config-repos>\n")).config;
        assertThat(cruiseConfig.getConfigRepos().size()).isEqualTo(2);
        ConfigRepoConfig configRepo1 = cruiseConfig.getConfigRepos().get(0);
        assertThat(configRepo1.getMaterialConfig())
                .isEqualTo(git("https://github.com/tomzo/gocd-indep-config-part.git"));
        ConfigRepoConfig configRepo2 = cruiseConfig.getConfigRepos().get(1);
        assertThat(configRepo2.getMaterialConfig())
                .isEqualTo(git("https://github.com/tomzo/gocd-refmain-config-part.git"));
    }

    @Test
    void shouldLoadConfigWithConfigRepoAndConfiguration() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(
                configWithConfigRepos("  <config-repos>\n" + "    <config-repo id=\"id1\" pluginId=\"gocd-xml\">\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()).isEqualTo(1);
        ConfigRepoConfig configRepo = cruiseConfig.getConfigRepos().get(0);

        assertThat(configRepo.getConfiguration().size()).isEqualTo(1);
        assertThat(configRepo.getConfiguration().getProperty("pattern").getValue()).isEqualTo("*.gocd.xml");
    }

    @Test
    void shouldThrowXsdValidationException_WhenNoRepository() throws Exception {
        assertThatCode(() -> {
            xmlLoader.loadConfigHolder(
                    configWithConfigRepos("  <config-repos>\n" + "    <config-repo pluginId=\"myplugin\">\n"
                            + "    </config-repo >\n" + "  </config-repos>\n"));
        }).isInstanceOf(XsdValidationException.class);
    }

    @Test
    void shouldThrowXsdValidationException_When2RepositoriesInSameConfigElement() throws Exception {
        assertThatCode(() -> {
            xmlLoader.loadConfigHolder(
                    configWithConfigRepos("  <config-repos>\n" + "    <config-repo pluginId=\"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"));
        }).isInstanceOf(XsdValidationException.class);
    }

    @Test
    void shouldFailValidation_WhenSameMaterialUsedBy2ConfigRepos() {
        assertThatCode(() -> {
            xmlLoader.loadConfigHolder(configWithConfigRepos(
                    "  <config-repos>\n" + "    <config-repo pluginId=\"myplugin\" id=\"id1\">\n"
                            + "      <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n"
                            + "    </config-repo >\n" + "    <config-repo pluginId=\"myotherplugin\" id=\"id2\">\n"
                            + "      <git url=\"https://github.com/tomzo/gocd-indep-config-part.git\" />\n"
                            + "    </config-repo >\n" + "  </config-repos>\n"));
        }).isInstanceOf(GoConfigInvalidException.class);
    }

    private ConfigRepoPartialPreprocessor findConfigRepoPartialPreprocessor() {
        List<GoConfigPreprocessor> preprocessors = MagicalGoConfigXmlLoader.PREPROCESSORS;
        for (GoConfigPreprocessor preprocessor : preprocessors) {
            if (preprocessor instanceof ConfigRepoPartialPreprocessor)
                return (ConfigRepoPartialPreprocessor) preprocessor;
        }
        return null;
    }

    @Test
    void shouldSetConfigOriginInCruiseConfig_AfterLoadingConfigFile() throws Exception {
        GoConfigHolder goConfigHolder = xmlLoader.loadConfigHolder(CONFIG, new MagicalGoConfigXmlLoader.Callback() {
            @Override
            public void call(CruiseConfig cruiseConfig) {
                cruiseConfig.setPartials(asList(new PartialConfig()));
            }
        });
        assertThat(goConfigHolder.config.getOrigin()).isEqualTo(new MergeConfigOrigin());
        assertThat(goConfigHolder.configForEdit.getOrigin()).isEqualTo(new FileConfigOrigin());
    }

    @Test
    void shouldSetConfigOriginInPipeline_AfterLoadingConfigFile() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(CONFIG).config;
        PipelineConfig pipelineConfig1 = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1"));
        assertThat(pipelineConfig1.getOrigin()).isEqualTo(new FileConfigOrigin());
    }

    @Test
    void shouldSetConfigOriginInEnvironment_AfterLoadingConfigFile() throws Exception {
        String content = configWithEnvironments(
                "<environments>" + "  <environment name='uat'>" + "    <agents>" + "      <physical uuid='1'/>"
                        + "      <physical uuid='2'/>" + "    </agents>" + "  </environment>" + "</environments>");
        EnvironmentsConfig environmentsConfig = xmlLoader.deserializeConfig(content).getEnvironments();
        EnvironmentConfig uat = environmentsConfig.get(0);
        assertThat(uat.getOrigin()).isEqualTo(new FileConfigOrigin());
    }

    @Test
    void shouldSupportMultipleAgentsFromSameBox() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader
                .loadConfigHolder(ConfigMigrator.migrate(WITH_MULTIPLE_LOCAL_AGENT_CONFIG)).config;
        assertThat(cruiseConfig.agents().size()).isEqualTo(2);
        assertThat(cruiseConfig.agents().get(0).getHostname())
                .isEqualTo(cruiseConfig.agents().get(1).getHostname());
        assertThat(cruiseConfig.agents().get(0).getIpAddress())
                .isEqualTo(cruiseConfig.agents().get(1).getIpAddress());
        assertThat(cruiseConfig.agents().get(0).getUuid()).isNotEqualTo(cruiseConfig.agents().get(1).getUuid());
    }

    @Test
    void shouldLoadAntBuilder() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.loadConfigHolder(CONFIG_WITH_ANT_BUILDER).config;
        JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true);

        assertThat(plan.tasks()).hasSize(1);
        AntTask builder = (AntTask) plan.tasks().first();
        assertThat(builder.getTarget()).isEqualTo("all");
        final ArtifactConfigs cardListArtifacts = cruiseConfig
                .jobConfigByName("pipeline1", "mingle", "cardlist", true).artifactConfigs();
        assertThat(cardListArtifacts.size()).isEqualTo(1);
        ArtifactConfig artifactConfigPlan = cardListArtifacts.get(0);
        assertThat(artifactConfigPlan.getArtifactType()).isEqualTo(ArtifactType.test);
    }

    @Test
    void shouldLoadNAntBuilder() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.deserializeConfig(CONFIG_WITH_NANT_AND_EXEC_BUILDER);
        JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true);
        BuildTask builder = (BuildTask) plan.tasks().findFirstByType(NantTask.class);
        assertThat(builder.getTarget()).isEqualTo("all");
    }

    @Test
    void shouldLoadExecBuilder() throws Exception {

        CruiseConfig cruiseConfig = xmlLoader.deserializeConfig(CONFIG_WITH_NANT_AND_EXEC_BUILDER);
        JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true);
        ExecTask builder = (ExecTask) plan.tasks().findFirstByType(ExecTask.class);
        assertThat(builder).isEqualTo(new ExecTask("ls", "-la", "workdir"));

        builder = (ExecTask) plan.tasks().get(2);
        assertThat(builder).isEqualTo(new ExecTask("ls", "", (String) null));
    }

    @Test
    void shouldLoadRakeBuilderWithEmptyOnCancel() throws Exception {

        CruiseConfig cruiseConfig = xmlLoader.deserializeConfig(CONFIG_WITH_NANT_AND_EXEC_BUILDER);
        JobConfig plan = cruiseConfig.jobConfigByName("pipeline1", "mingle", "cardlist", true);
        RakeTask builder = (RakeTask) plan.tasks().findFirstByType(RakeTask.class);
        assertThat(builder).isNotNull();
    }

    @Test
    void shouldRetainArtifactSourceThatIsNotWhitespace() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader
                .deserializeConfig(goConfigMigration.upgradeIfNecessary(configWithArtifactSourceAs("t ")));
        JobConfig plan = cruiseConfig.jobConfigByName("pipeline", "stage", "job", true);
        assertThat(plan.artifactConfigs().getBuiltInArtifactConfigs().get(0).getSource()).isEqualTo("t ");
    }

    @Test
    void shouldLoadBuildPlanFromXmlPartial() throws Exception {
        String buildXmlPartial = "<job name=\"functional\">\n" + "  <artifacts>\n"
                + "    <artifact type=\"build\" src=\"artifact1.xml\" dest=\"cruise-output\" />\n"
                + "  </artifacts>\n" + "</job>";
        JobConfig build = xmlLoader.fromXmlPartial(buildXmlPartial, JobConfig.class);
        assertThat(build.name()).isEqualTo(new CaseInsensitiveString("functional"));
        assertThat(build.artifactConfigs().size()).isEqualTo(1);
    }

    @Test
    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(buildXmlPartial, SvnMaterialConfig.class);
        Filter parsedFilter = svnMaterial.filter();
        Filter expectedFilter = new Filter();
        expectedFilter.add(new IgnoredFiles("x"));
        assertThat(parsedFilter).isEqualTo(expectedFilter);
    }

    @Test
    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(buildXmlPartial, HgMaterialConfig.class);
        Filter parsedFilter = hgMaterial.filter();
        Filter expectedFilter = new Filter();
        expectedFilter.add(new IgnoredFiles("x"));
        assertThat(parsedFilter).isEqualTo(expectedFilter);
    }

    @Test
    void shouldLoadMaterialWithAutoUpdate() throws Exception {
        MaterialConfig material = xmlLoader.fromXmlPartial(
                "<hg url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"false\"/>",
                HgMaterialConfig.class);
        assertThat(material.isAutoUpdate()).isFalse();
        material = xmlLoader.fromXmlPartial(
                "<git url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"false\"/>",
                GitMaterialConfig.class);
        assertThat(material.isAutoUpdate()).isFalse();
        material = xmlLoader.fromXmlPartial(
                "<svn url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"false\"/>",
                SvnMaterialConfig.class);
        assertThat(material.isAutoUpdate()).isFalse();
        material = xmlLoader.fromXmlPartial("<p4 port='localhost:1666' autoUpdate='false' ><view/></p4>",
                P4MaterialConfig.class);
        assertThat(material.isAutoUpdate()).isFalse();
    }

    @Test
    void autoUpdateShouldBeTrueByDefault() throws Exception {
        MaterialConfig hgMaterial = xmlLoader.fromXmlPartial("<hg url=\"file:///tmp/testSvnRepo/project1/trunk\"/>",
                HgMaterialConfig.class);
        assertThat(hgMaterial.isAutoUpdate()).isTrue();
    }

    @Test
    void autoUpdateShouldUnderstandTrue() throws Exception {
        MaterialConfig hgMaterial = xmlLoader.fromXmlPartial(
                "<hg url=\"file:///tmp/testSvnRepo/project1/trunk\" autoUpdate=\"true\"/>", HgMaterialConfig.class);
        assertThat(hgMaterial.isAutoUpdate()).isTrue();
    }

    @Test
    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
    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
    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
    void shouldLoadFromSvnPartial() throws Exception {
        String buildXmlPartial = "<svn url=\"http://foo.bar\" username=\"cruise\" password=\"password\" materialName=\"http___foo.bar\"/>";

        MaterialConfig materialConfig = xmlLoader.fromXmlPartial(buildXmlPartial, SvnMaterialConfig.class);
        MaterialConfig svnMaterial = MaterialConfigsMother.svnMaterialConfig("http://foo.bar", null, "cruise",
                "password", false, null);
        assertThat(materialConfig).isEqualTo(svnMaterial);
    }

    @Test
    void shouldLoadGetFromSvnPartialForDir() throws Exception {
        String buildXmlPartial = "<jobs>\n" + "  <job name=\"functional\">\n" + "     <tasks>\n"
                + "         <fetchartifact artifactOrigin='gocd' stage='dev' job='unit' srcdir='dist' dest='lib' />\n"
                + "      </tasks>\n" + "    </job>\n" + "</jobs>";

        JobConfigs jobs = xmlLoader.fromXmlPartial(buildXmlPartial, JobConfigs.class);
        JobConfig job = jobs.first();
        Tasks fetch = job.tasks();
        assertThat(fetch.size()).isEqualTo(1);
        FetchTask task = (FetchTask) fetch.first();
        assertThat(task.getStage()).isEqualTo(new CaseInsensitiveString("dev"));
        assertThat(task.getJob().toString()).isEqualTo("unit");
        assertThat(task.getSrc()).isEqualTo("dist");
        assertThat(task.getDest()).isEqualTo("lib");
    }

    @Test
    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(buildXmlPartial, JobConfigs.class);
        JobConfig job = jobs.first();
        Tasks tasks = job.tasks();
        assertThat(tasks.size()).isEqualTo(1);
        ExecTask execTask = (ExecTask) tasks.get(0);
        assertThat(execTask.cancelTask()).isInstanceOf(NullTask.class);
    }

    @Test
    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(gitPartial, GitMaterialConfig.class);
        assertThat(gitMaterial.getBranch()).isEqualTo(GitMaterialConfig.DEFAULT_BRANCH);
        Filter parsedFilter = gitMaterial.filter();
        Filter expectedFilter = new Filter();
        expectedFilter.add(new IgnoredFiles("x"));
        assertThat(parsedFilter).isEqualTo(expectedFilter);
    }

    @Test
    void shouldLoadShallowFlagFromGitPartial() throws Exception {
        String gitPartial = "<git url='file:///tmp/testGitRepo/project1' shallowClone=\"true\" />";
        GitMaterialConfig gitMaterial = xmlLoader.fromXmlPartial(gitPartial, GitMaterialConfig.class);
        assertThat(gitMaterial.isShallowClone()).isTrue();
    }

    @Test
    void shouldLoadBranchFromGitPartial() throws Exception {
        String gitPartial = "<git url='file:///tmp/testGitRepo/project1' branch='foo'/>";
        GitMaterialConfig gitMaterial = xmlLoader.fromXmlPartial(gitPartial, GitMaterialConfig.class);
        assertThat(gitMaterial.getBranch()).isEqualTo("foo");
    }

    @Test
    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(gitPartial, P4MaterialConfig.class);
        Filter parsedFilter = p4Material.filter();
        Filter expectedFilter = new Filter();
        expectedFilter.add(new IgnoredFiles("x"));
        assertThat(parsedFilter).isEqualTo(expectedFilter);
    }

    @Test
    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(stageXmlPartial, StageConfig.class);
        assertThat(stage.name()).isEqualTo(new CaseInsensitiveString("mingle"));
        assertThat(stage.allBuildPlans().size()).isEqualTo(1);
        assertThat(stage.jobConfigByInstanceName("functional", true)).isNotNull();
    }

    @Test
    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(stageXmlPartial, StageConfig.class);
        assertThat(stage.isArtifactCleanupProhibited()).isTrue();

        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(stageXmlPartial, StageConfig.class);
        assertThat(stage.isArtifactCleanupProhibited()).isFalse();

        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(stageXmlPartial, StageConfig.class);
        assertThat(stage.isArtifactCleanupProhibited()).isFalse();
    }

    @Test
    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(partialConfigWithPipeline, PartialConfig.class);
        assertThat(partialConfig.getGroups().size()).isEqualTo(1);
        PipelineConfig pipeline = partialConfig.getGroups().get(0).getPipelines().get(0);
        assertThat(pipeline.name()).isEqualTo(new CaseInsensitiveString("pipeline"));
        assertThat(pipeline.size()).isEqualTo(1);
        assertThat(pipeline.findBy(new CaseInsensitiveString("mingle")).jobConfigByInstanceName("functional", true))
                .isNotNull();
    }

    @Test
    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(partialConfigWithPipeline, PartialConfig.class);
        EnvironmentsConfig environmentsConfig = partialConfig.getEnvironments();
        assertThat(environmentsConfig.size()).isEqualTo(2);
        EnvironmentPipelineMatchers matchers = environmentsConfig.matchers();
        assertThat(matchers.size()).isEqualTo(2);
        ArrayList<String> uat_uuids = new ArrayList<String>() {
            {
                add("1");
                add("2");
            }
        };
        ArrayList<String> prod_uuids = new ArrayList<String>() {
            {
                add("2");
            }
        };
        assertThat(matchers).contains(new EnvironmentPipelineMatcher(new CaseInsensitiveString("uat"), uat_uuids,
                new EnvironmentPipelinesConfig()));
        assertThat(matchers).contains(new EnvironmentPipelineMatcher(new CaseInsensitiveString("prod"), prod_uuids,
                new EnvironmentPipelinesConfig()));
    }

    @Test
    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(pipelineXmlPartial, PipelineConfig.class);
        assertThat(pipeline.name()).isEqualTo(new CaseInsensitiveString("pipeline"));
        assertThat(pipeline.size()).isEqualTo(1);
        assertThat(pipeline.findBy(new CaseInsensitiveString("mingle")).jobConfigByInstanceName("functional", true))
                .isNotNull();
    }

    @Test
    void shouldBeAbleToExplicitlyLockAPipeline() throws Exception {
        String pipelineXmlPartial = "<pipeline name=\"pipeline\" lockBehavior=\"" + LOCK_VALUE_LOCK_ON_FAILURE
                + "\">\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(pipelineXmlPartial, PipelineConfig.class);

        assertThat(pipeline.hasExplicitLock()).isTrue();
        assertThat(pipeline.explicitLock()).isTrue();
    }

    @Test
    void shouldBeAbleToExplicitlyUnlockAPipeline() throws Exception {
        String pipelineXmlPartial = "<pipeline name=\"pipeline\" lockBehavior=\"" + PipelineConfig.LOCK_VALUE_NONE
                + "\">\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(pipelineXmlPartial, PipelineConfig.class);

        assertThat(pipeline.hasExplicitLock()).isTrue();
        assertThat(pipeline.explicitLock()).isFalse();
    }

    @Test
    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(pipelineXmlPartial, PipelineConfig.class);

        assertThat(pipeline.hasExplicitLock()).isFalse();
        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()).contains("There is no explicit lock on the pipeline 'pipeline'.");
        }
    }

    @Test
    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(pipelineWithP4MaterialXmlPartial, PipelineConfig.class);
        assertThat(pipeline.name()).isEqualTo(new CaseInsensitiveString("pipeline"));
        MaterialConfig material = pipeline.materialConfigs().first();
        assertThat(material).isInstanceOf(P4MaterialConfig.class);
        assertThat(((P4MaterialConfig) material).getUseTickets()).isTrue();
    }

    @Test
    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(stageXmlPartial, JobConfig.class);
            fail("Should not be able to load stage into jobConfig");
        } catch (Exception e) {
            assertThat(e.getMessage()).isEqualTo("Unable to parse element <stage> for class JobConfig");
        }
    }

    @Test
    void shouldThrowExceptionWhenCommandIsEmpty() throws Exception {
        String jobWithCommand = "<job name=\"functional\">\n" + "      <tasks>\n"
                + "        <exec command=\"\" arguments=\"\" />\n" + "      </tasks>\n" + "    </job>\n";
        String configWithInvalidCommand = withCommand(jobWithCommand);

        try {
            xmlLoader.deserializeConfig(configWithInvalidCommand);
            fail("Should not allow empty command");
        } catch (Exception e) {
            assertThat(e.getMessage())
                    .contains("Command is invalid. \"\" should conform to the pattern - \\S(.*\\S)?");
        }
    }

    @Test
    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 {
            xmlLoader.deserializeConfig(configXml);
            fail("Should not allow command with trailing spaces");
        } catch (Exception e) {
            assertThat(e.getMessage())
                    .contains("Command is invalid. \"bundle  \" should conform to the pattern - \\S(.*\\S)?");
        }
    }

    @Test
    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 {
            xmlLoader.deserializeConfig(configXml);
            fail("Should not allow command with trailing spaces");
        } catch (Exception e) {
            assertThat(e.getMessage())
                    .contains("Command is invalid. \"    bundle\" should conform to the pattern - \\S(.*\\S)?");
        }
    }

    @Test
    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 = withCommand(jobWithCommand);
        CruiseConfig cruiseConfig = xmlLoader.deserializeConfig(configWithCommand);
        Task task = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1")).first()
                .allBuildPlans().first().tasks().first();

        assertThat(task).isInstanceOf(ExecTask.class);
        assertThat(task).isEqualTo(new ExecTask("c:\\program files\\cmd.exe", "arguments", (String) null));
    }

    @Test
    void shouldLoadMingleConfigForPipeline() throws Exception {
        String configWithCommand = withMingleConfig(
                "<mingle baseUrl=\"https://foo.bar/baz\" projectIdentifier=\"cruise-performance\"/>");
        MingleConfig mingleConfig = xmlLoader.deserializeConfig(configWithCommand)
                .pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getMingleConfig();
        assertThat(mingleConfig).isEqualTo(new MingleConfig("https://foo.bar/baz", "cruise-performance"));

        configWithCommand = withMingleConfig(
                "<mingle baseUrl=\"https://foo.bar/baz\" projectIdentifier=\"cruise-performance\"><mqlGroupingConditions>foo = bar!=baz</mqlGroupingConditions></mingle>");
        mingleConfig = xmlLoader.deserializeConfig(configWithCommand)
                .pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getMingleConfig();
        assertThat(mingleConfig)
                .isEqualTo(new MingleConfig("https://foo.bar/baz", "cruise-performance", "foo = bar!=baz"));

        configWithCommand = withMingleConfig(
                "<mingle baseUrl=\"https://foo.bar/baz\" projectIdentifier=\"cruise-performance\"><mqlGroupingConditions/></mingle>");
        mingleConfig = xmlLoader.deserializeConfig(configWithCommand)
                .pipelineConfigByName(new CaseInsensitiveString("pipeline1")).getMingleConfig();
        assertThat(mingleConfig).isEqualTo(new MingleConfig("https://foo.bar/baz", "cruise-performance", ""));
    }

    private void shouldBeSvnMaterial(MaterialConfig material) {
        assertThat(material).isInstanceOf(SvnMaterialConfig.class);
        SvnMaterialConfig svnMaterial = (SvnMaterialConfig) material;
        assertThat(svnMaterial.getUrl()).isEqualTo("svnUrl");
        assertThat(svnMaterial.isCheckExternals()).isTrue();
    }

    private void shouldBeHgMaterial(MaterialConfig material) {
        assertThat(material).isInstanceOf(HgMaterialConfig.class);
        HgMaterialConfig hgMaterial = (HgMaterialConfig) material;
        assertThat(hgMaterial.getUrl()).isEqualTo("http://hgUrl.com");
        assertThat(hgMaterial.getUserName()).isEqualTo("username");
        assertThat(hgMaterial.getPassword()).isEqualTo("password");
    }

    private void shouldBeP4Material(MaterialConfig material) {
        assertThat(material).isInstanceOf(P4MaterialConfig.class);
        P4MaterialConfig p4Material = (P4MaterialConfig) material;
        assertThat(p4Material.getServerAndPort()).isEqualTo("localhost:1666");
        assertThat(p4Material.getUserName()).isEqualTo("cruise");
        assertThat(p4Material.getPassword()).isEqualTo("password");
        assertThat(p4Material.getView()).isEqualTo("//depot/dir1/... //lumberjack/...");
    }

    private void shouldBeGitMaterial(MaterialConfig material) {
        assertThat(material).isInstanceOf(GitMaterialConfig.class);
        GitMaterialConfig gitMaterial = (GitMaterialConfig) material;
        assertThat(gitMaterial.getUrl()).isEqualTo("git://username:password@gitUrl");
    }

    @Test
    void shouldNotAllowEmptyAuthInApproval() throws Exception {
        assertXsdFailureDuringLoad(STAGE_WITH_EMPTY_AUTH,
                "The content of element 'authorization' is not complete. One of '{user, role}' is expected.");
    }

    @Test
    void shouldNotAllowEmptyRoles() throws Exception {
        assertXsdFailureDuringLoad(CONFIG_WITH_EMPTY_ROLES,
                "The content of element 'roles' is not complete. One of '{baseRole}' is expected.");
    }

    @Test
    void shouldNotAllowEmptyUser() throws Exception {
        assertXsdFailureDuringLoad(CONFIG_WITH_EMPTY_USER,
                "Value '' with length = '0' is not facet-valid with respect to minLength '1' for type '#AnonType_userusersroleType'.");
    }

    @Test
    void shouldNotAllowDuplicateRoles() throws Exception {
        assertFailureDuringLoad(CONFIG_WITH_DUPLICATE_ROLE, GoConfigInvalidException.class,
                "Role names should be unique. Duplicate names found.");
    }

    @Test
    void shouldNotAllowDuplicateUsersInARole() throws Exception {
        assertFailureDuringLoad(CONFIG_WITH_DUPLICATE_USER, GoConfigInvalidException.class,
                "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
    void shouldLoadConfigurationFileWithComplexNonEmptyString() throws Exception {
        String customerXML = loadWithMigration(
                this.getClass().getResource("/data/p4_heavy_cruise_config.xml").getFile());
        assertThat(xmlLoader.deserializeConfig(customerXML)).isNotNull();
    }

    private String loadWithMigration(String file) throws Exception {
        String config = FileUtils.readFileToString(new File(file), UTF_8);
        return goConfigMigration.upgradeIfNecessary(config);
    }

    @Test
    void shouldNotAllowEmptyViewForPerforce() throws Exception {
        try {
            String p4XML = this.getClass().getResource("/data/p4-cruise-config-empty-view.xml").getFile();
            xmlLoader.loadConfigHolder(loadWithMigration(p4XML));
            fail("Should not accept p4 section with empty view.");
        } catch (Exception expected) {
            assertThat(expected.getMessage()).contains("P4 view cannot be empty.");
        }
    }

    @Test
    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(pipelineXmlPartial, PipelineConfig.class);
        assertThat(pipeline.materialConfigs().size()).isEqualTo(3);
        ScmMaterialConfig material = (ScmMaterialConfig) pipeline.materialConfigs().get(0);
        assertThat(material.getFolder()).isEqualTo("folder1");
    }

    @Test
    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
    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
    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
    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
    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
    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
    void shouldAllowPathStartWithDotSlash() throws Exception {
        String materials = "  <materials>\n" + "    <svn url=\"/hgrepo2\" dest=\"./folder3\" />\n"
                + "  </materials>\n";
        MagicalGoConfigXmlLoaderFixture.assertValid(materials);
    }

    @Test
    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 })
    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 })
    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
    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
    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
    void shouldLoadPipelinesWithGroupName() throws Exception {
        CruiseConfig config = xmlLoader.deserializeConfig(PIPELINE_GROUPS);
        assertThat(config.getGroups().first().getGroup()).isEqualTo("studios");
        assertThat(config.getGroups().get(1).getGroup()).isEqualTo("perfessionalservice");
    }

    @Test
    void shouldLoadTasksWithExecutionCondition() throws Exception {
        CruiseConfig config = xmlLoader.deserializeConfig(TASKS_WITH_CONDITION);
        JobConfig job = config.jobConfigByName("pipeline1", "mingle", "cardlist", true);

        assertThat(job.tasks().size()).isEqualTo(2);
        assertThat(job.tasks().findFirstByType(AntTask.class).getConditions().get(0))
                .isEqualTo(new RunIfConfig("failed"));

        RunIfConfigs conditions = job.tasks().findFirstByType(NantTask.class).getConditions();
        assertThat(conditions.get(0)).isEqualTo(new RunIfConfig("failed"));
        assertThat(conditions.get(1)).isEqualTo(new RunIfConfig("any"));
        assertThat(conditions.get(2)).isEqualTo(new RunIfConfig("passed"));
    }

    @Test
    void shouldLoadTasksWithOnCancel() throws Exception {
        CruiseConfig config = xmlLoader.deserializeConfig(TASKS_WITH_ON_CANCEL);
        JobConfig job = config.jobConfigByName("pipeline1", "mingle", "cardlist", true);

        Task task = job.tasks().findFirstByType(AntTask.class);
        assertThat(task.hasCancelTask()).isTrue();
        assertThat(task.cancelTask()).isEqualTo(new ExecTask("kill.rb", "", "utils"));

        Task task2 = job.tasks().findFirstByType(ExecTask.class);
        assertThat(task2.hasCancelTask()).isFalse();
    }

    @Test
    void shouldNotLoadTasksWithOnCancelTaskNested() throws Exception {
        try {
            xmlLoader.loadConfigHolder(TASKS_WITH_ON_CANCEL_NESTED);
            fail("Should not allow nesting of 'oncancel' within task inside oncancel");
        } catch (Exception expected) {
            assertThat(expected.getMessage()).isEqualTo("Cannot nest 'oncancel' within a cancel task");
            assertThat(expected instanceof GoConfigInvalidException).isTrue();
        }
    }

    @Test
    void shouldAllowBothCounterAndMaterialNameInLabelTemplate() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader
                .deserializeConfig(LABEL_TEMPLATE_WITH_LABEL_TEMPLATE("1.3.0-${COUNT}-${git}"));
        assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("cruise")).getLabelTemplate())
                .isEqualTo("1.3.0-${COUNT}-${git}");
    }

    @Test
    void shouldAllowBothCounterAndTruncatedGitMaterialInLabelTemplate() throws Exception {
        CruiseConfig cruiseConfig = xmlLoader.deserializeConfig(
                LABEL_TEMPLATE_WITH_LABEL_TEMPLATE("1.3.0-${COUNT}-${git[:7]}", CONFIG_SCHEMA_VERSION));
        assertThat(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("cruise")).getLabelTemplate())
                .isEqualTo("1.3.0-${COUNT}-${git[:7]}");
    }

    @Test
    void shouldAllowHashCharacterInLabelTemplate() throws Exception {
        GoConfigHolder goConfigHolder = xmlLoader.loadConfigHolder(
                LABEL_TEMPLATE_WITH_LABEL_TEMPLATE("1.3.0-${COUNT}-${git}##", CONFIG_SCHEMA_VERSION));
        assertThat(
                goConfigHolder.config.pipelineConfigByName(new CaseInsensitiveString("cruise")).getLabelTemplate())
                        .isEqualTo("1.3.0-${COUNT}-${git}#");
        assertThat(goConfigHolder.configForEdit.pipelineConfigByName(new CaseInsensitiveString("cruise"))
                .getLabelTemplate()).isEqualTo("1.3.0-${COUNT}-${git}##");
    }

    @Test
    void shouldLoadMaterialNameIfPresent() throws Exception {
        CruiseConfig config = xmlLoader.deserializeConfig(MATERIAL_WITH_NAME);
        MaterialConfigs materialConfigs = config.pipelineConfigByName(new CaseInsensitiveString("pipeline"))
                .materialConfigs();
        assertThat(materialConfigs.get(0).getName()).isEqualTo(new CaseInsensitiveString("svn"));
        assertThat(materialConfigs.get(1).getName()).isEqualTo(new CaseInsensitiveString("hg"));
    }

    @Test
    void shouldLoadPipelineWithTimer() throws Exception {
        CruiseConfig config = xmlLoader.deserializeConfig(PIPELINE_WITH_TIMER);
        PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString("pipeline"));
        assertThat(pipelineConfig.getTimer()).isEqualTo(new TimerConfig("0 15 10 ? * MON-FRI", false));
    }

    @Test
    void shouldLoadConfigWithEnvironment() throws Exception {
        String content = 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 = xmlLoader.loadConfigHolder(content).config.getEnvironments();
        EnvironmentPipelineMatchers matchers = environmentsConfig.matchers();
        assertThat(matchers.size()).isEqualTo(2);
        ArrayList<String> uat_uuids = new ArrayList<String>() {
            {
                add("1");
                add("2");
            }
        };
        ArrayList<String> prod_uuids = new ArrayList<String>() {
            {
                add("2");
            }
        };
        assertThat(matchers).contains(new EnvironmentPipelineMatcher(new CaseInsensitiveString("uat"), uat_uuids,
                new EnvironmentPipelinesConfig()));
        assertThat(matchers).contains(new EnvironmentPipelineMatcher(new CaseInsensitiveString("prod"), prod_uuids,
                new EnvironmentPipelinesConfig()));
    }

    @Test
    void shouldNotLoadConfigWithEmptyTemplates() throws Exception {
        String content = configWithTemplates("<templates>" + "</templates>");
        try {
            xmlLoader.loadConfigHolder(content);
            fail("Should not allow empty templates block");
        } catch (Exception expected) {
            assertThat(expected.getMessage()).contains(
                    "The content of element 'templates' is not complete. One of '{pipeline}' is expected.");
        }
    }

    @Test
    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()).contains(
                    "Pipeline 'pipeline1' does not have any stages configured. A pipeline must have at least one stage.");
        }
    }

    @Test
    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())
                    .contains("Pipeline 'pipeline1' refers to non-existent template 'abc'.");
        }
    }

    @Test
    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()).isEqualTo(1);
    }

    @Test
    void shouldAllowAdminInPipelineGroups() {
        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()).isEqualTo(CONFIG_SCHEMA_VERSION);
        assertThat(
                cruiseConfig.findGroup("first").isUserAnAdmin(new CaseInsensitiveString("foo"), new ArrayList<>()))
                        .isTrue();
    }

    @Test
    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()).isEqualTo(CONFIG_SCHEMA_VERSION);
        assertThat(cruiseConfig.findGroup("first").isUserAnAdmin(new CaseInsensitiveString("foo"),
                asList(new RoleConfig(new CaseInsensitiveString("bar"))))).isTrue();
    }

    @Test
    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()).isEqualTo("0");
    }

    @Test
    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()).isEqualTo("30");
    }

    @Test
    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()).isEqualTo("5");
    }

    @Test
    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())
                .isEqualTo(new ServerSiteUrlConfig("http://www.someurl.com/go"));
        assertThat(cruiseConfig.server().getSecureSiteUrl())
                .isEqualTo(new ServerSiteUrlConfig("https://www.someotherurl.com/go"));
    }

    @Test
    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()).isEqualTo(1.0);
        assertThat(cruiseConfig.server().getPurgeUpto()).isEqualTo(3.0);
    }

    @Test
    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()).isEqualTo(1.2);
        assertThat(cruiseConfig.server().getPurgeUpto()).isEqualTo(3.4);
    }

    @Test
    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()).isNull();
        assertThat(cruiseConfig.server().getPurgeUpto()).isNull();
    }

    @Test
    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()).contains(
                    "Cannot add stage 'badstage' to pipeline 'pipeline1', which already references template 'abc'.");
        }
    }

    @Test
    void shouldLoadConfigWithPipelineTemplate() throws Exception {
        String content = 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()).isEqualTo(1);
        assertThat(templates.get(0).size()).isEqualTo(1);
        assertThat(templates.get(0).get(0)).isEqualTo(StageConfigMother.custom("stage1", "job1"));
    }

    @Test
    void shouldLoadConfigWith2PipelineTemplates() throws Exception {
        String content = 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()).isEqualTo(2);
        assertThat(templates.get(0).name()).isEqualTo(new CaseInsensitiveString("erbshe"));
        assertThat(templates.get(1).name()).isEqualTo(new CaseInsensitiveString("erbshe2"));
    }

    @Test
    void shouldOnlySupportUniquePipelineTemplates() throws Exception {
        String content = 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 {
            xmlLoader.loadConfigHolder(content);
            fail("should not allow same template names");
        } catch (Exception expected) {
            assertThat(expected.getMessage())
                    .contains("Duplicate unique value [erbshe] declared for identity constraint");
        }
    }

    @Test
    void shouldNotAllowEmptyPipelineTemplates() throws Exception {
        String content = configWithTemplates(
                "<templates>" + "  <pipeline name='erbshe'>" + "  </pipeline>" + "</templates>");
        try {
            xmlLoader.loadConfigHolder(content);
            fail("should NotAllowEmptyPipelineTemplates");
        } catch (Exception expected) {
            assertThat(expected.getMessage()).contains(
                    "The content of element 'pipeline' is not complete. One of '{authorization, stage}' is expected");
        }
    }

    @Test
    void shouldNotAllowJobToHaveTheRunOnAllAgentsMarkerInItsName() throws Exception {
        String invalidJobName = format("%s-%s-%s", "invalid-name", RunOnAllAgentsJobTypeConfig.MARKER, 1);
        testForInvalidJobName(invalidJobName, RunOnAllAgentsJobTypeConfig.MARKER);
    }

    @Test
    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 = 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()).contains(
                    String.format("A job cannot have '%s' in it's name: %s because it is a reserved keyword",
                            marker, invalidJobName));
        }
    }

    @Test
    void shouldAllow_NonRunOnAllAgentJobToHavePartsOfTheRunOnAll_and_NonRunMultipleInstanceJobToHavePartsOfTheRunInstance_AgentsMarkerInItsName()
            throws Exception {
        String content = 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
    void shouldLoadLargeConfigFileInReasonableTime() throws Exception {
        String content = IOUtils.toString(getClass().getResourceAsStream("/data/big-cruise-config.xml"), UTF_8);
        //        long start = System.currentTimeMillis();
        GoConfigHolder configHolder = ConfigMigrator.loadWithMigration(content);
        //        assertThat(System.currentTimeMillis() - start, lessThan(new Long(2000)));
        assertThat(configHolder.config.schemaVersion()).isEqualTo(CONFIG_SCHEMA_VERSION);
    }

    @Test
    void shouldLoadConfigWithPipelinesMatchingUpWithPipelineDefinitionCaseInsensitively() throws Exception {
        String content = 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).isEqualTo(new EnvironmentPipelineMatcher(new CaseInsensitiveString("uat"),
                new ArrayList<>(), new EnvironmentPipelinesConfig(new CaseInsensitiveString("piPeline1"))));
    }

    @Test
    void shouldNotAllowConfigWithUnknownPipeline() throws Exception {
        String content = 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()).contains("Environment 'uat' refers to an unknown pipeline 'notpresent'.");
        }

    }

    @Test
    void shouldNotAllowDuplicatePipelineAcrossEnvironments() throws Exception {
        String content = 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()).contains("Associating pipeline(s) which is already part of uat environment");
        }
    }

    @Test
    void shouldNotAllowDuplicatePipelinesInASingleEnvironment() throws Exception {
        String content = 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()).contains("Cannot add pipeline 'Pipeline1' to the environment");
        }
    }

    @Test
    void shouldNotAllowConfigWithEnvironmentsWithSameNames() throws Exception {
        String content = configWithEnvironments("<environments>" + "  <environment name='uat' />"
                + "  <environment name='uat' />" + "</environments>");
        try {
            xmlLoader.loadConfigHolder(content);
            fail("Should not support 2 environments with the same same");
        } catch (Exception e) {
            assertThat(StringUtils.containsAny(e.getMessage(),
                    "Duplicate unique value [uat] declared for identity constraint of element \"environments\"",
                    "Duplicate unique value [uat] declared for identity constraint \"uniqueEnvironmentName\" of element \"environments\""))
                            .isTrue();
        }
    }

    @Test
    void shouldNotAllowConfigWithInvalidName() throws Exception {
        String content = configWithEnvironments(
                "<environments>" + "  <environment name='exclamation is invalid !' />" + "</environments>");
        try {
            xmlLoader.loadConfigHolder(content);
            fail("XSD should not allow invalid characters");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains(
                    "\"exclamation is invalid !\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
        }
    }

    @Test
    void shouldNotAllowConfigWithAbsentReferencedAgentUuid() throws Exception {
        String content = 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()).contains("Environment 'uat' has an invalid agent uuid 'missing'");
        }
    }

    @Test
    void shouldAllowConfigWithEmptyPipeline() throws Exception {
        String content = 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
    void shouldAllowConfigWithEmptyAgents() throws Exception {
        String content = 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
    void shouldNotAllowConfigWithDuplicateAgentUuidInEnvironment() throws Exception {
        String content = 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 {
            xmlLoader.loadConfigHolder(content);
            fail("XSD should not allow duplicate agent uuid in environment");
        } catch (Exception e) {
            assertThat(StringUtils.containsAny(e.getMessage(),
                    "Duplicate unique value [1] declared for identity constraint of element \"agents\".",
                    "Duplicate unique value [1] declared for identity constraint \"uniqueEnvironmentAgentsUuid\" of element \"agents\"."))
                            .isTrue();
        }
    }

    @Test
    void shouldNotAllowConfigWithEmptyEnvironmentsBlock() throws Exception {
        String content = configWithEnvironments("<environments>" + "</environments>");
        try {
            xmlLoader.loadConfigHolder(content);
            fail("XSD should not allow empty environments block");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains(
                    "The content of element 'environments' is not complete. One of '{environment}' is expected.");
        }
    }

    @Test
    void shouldAllowConfigWithNoAgentsAndNoPipelinesInEnvironment() throws Exception {
        String content = configWithEnvironments(
                "<environments>" + "  <environment name='uat' />" + "</environments>");
        CruiseConfig config = ConfigMigrator.loadWithMigration(content).config;
        assertThat(config.getEnvironments().size()).isEqualTo(1);
    }

    @Test
    void shouldAllowConfigWithEnvironmentReferencingDisabledAgent() throws Exception {
        String content = 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()).isEqualTo(1);
    }

    @Test
    void shouldSupportEnvironmentVariablesInEnvironment() throws Exception {
        String content = 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()).contains(element);
    }

    @Test
    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 = 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)).isEqualTo(element);
    }

    @Test
    void shouldAllowOnlyOneTimerOnAPipeline() throws Exception {
        String content = 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>",
                CONFIG_SCHEMA_VERSION);
        try {
            xmlLoader.loadConfigHolder(content);
            fail("XSD should not allow duplicate timer in pipeline");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains("Invalid content was found starting with element 'timer'.");
        }
    }

    @Test
    void shouldValidateTimerSpec() throws Exception {
        String content = 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()).contains("Invalid cron syntax");
        }
    }

    @Test
    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()).contains("'bad_value' is not a valid value for 'boolean'");
        }
    }

    @Test
    void shouldNotAllowIllegalValueForRunMultipleInstanceJob() throws Exception {
        try {
            loadJobWithRunMultipleInstance("-1");
            fail("should have failed as runOnAllAgents' value is not valid(boolean)");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains(
                    "'-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()).contains("'abcd' is not a valid value for 'integer'");
        }
    }

    @Test
    void shouldSupportEnvironmentVariablesInAJob() throws Exception {
        String content = 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")).isEqualTo(jobConfig);
    }

    @Test
    void shouldSupportEnvironmentVariablesInAPipeline() throws Exception {
        String content = 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())
                .contains(new EnvironmentVariableConfig("PIPELINE_VARIABLE", "pipeline variable"));
    }

    @Test
    void shouldSupportEnvironmentVariablesInAStage() throws Exception {
        String content = 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()).contains(new EnvironmentVariableConfig("STAGE_VARIABLE", "stage variable"));
    }

    @Test
    void shouldNotAllowDuplicateEnvironmentVariablesInAJob() throws Exception {
        String content = 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())
                    .contains("Environment Variable name 'JOB_VARIABLE' is not unique for job 'do-something'.");
        }
    }

    @Test
    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()).contains("Param name 'same-name' is not unique for pipeline 'dev'.");
        }
    }

    @Test
    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 {
            xmlLoader.loadConfigHolder(content);
            fail("Should not allow params in stage name");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains(
                    "\"stage#{command}ab\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
        }
    }

    @Test
    void shouldNotAllowDuplicateEnvironmentVariablesInAPipeline() throws Exception {
        String content = 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())
                    .contains("Variable name 'PIPELINE_VARIABLE' is not unique for pipeline 'pipeline1'.");
        }
    }

    @Test
    void shouldNotAllowDuplicateEnvironmentVariablesInAStage() throws Exception {
        String content = 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()).contains("Variable name 'STAGE_VARIABLE' is not unique for stage 'mingle'.");
        }
    }

    @Test
    void shouldNotAllowDuplicateEnvironmentVariablesInAnEnvironment() throws Exception {
        String content = 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()).contains("Variable name 'FOO' is not unique for environment 'uat'.");
        }
    }

    @Test
    void shouldAllowParamsInEnvironmentVariablesInAPipeline() throws Exception {
        String content = 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())
                .contains(new EnvironmentVariableConfig("param_name", "stage variable"));
    }

    @Test
    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).isEqualTo(jobConfig);
    }

    @Test
    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).isEqualTo(jobConfig);
    }

    @Test
    void shouldUnderstandEncryptedPasswordAttributeForSvnMaterial() throws Exception {
        String password = "abc";
        String encryptedPassword = new GoCipher().encrypt(password);
        String content = 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()).isEqualTo(encryptedPassword);
        assertThat(svnMaterialConfig.getPassword()).isEqualTo(password);

        CruiseConfig configForEdit = configHolder.configForEdit;
        svnMaterialConfig = (SvnMaterialConfig) configForEdit
                .pipelineConfigByName(new CaseInsensitiveString("pipeline1")).materialConfigs().get(0);
        assertThat(svnMaterialConfig.getEncryptedPassword()).isEqualTo(encryptedPassword);
        assertThat(svnMaterialConfig.getPassword()).isEqualTo("abc");
        assertThat(ReflectionUtil.getField(svnMaterialConfig, "password")).isNull();
    }

    @Test
    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")).isEqualTo(group);
    }

    private CruiseConfig loadJobWithRunOnAllAgents(String value) throws Exception {
        String content = 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 xmlLoader.loadConfigHolder(content).config;
    }

    private CruiseConfig loadJobWithRunMultipleInstance(String value) throws Exception {
        String content = 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 xmlLoader.loadConfigHolder(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 type=\"build\" src=\"artifact1.xml\" dest=\"cruise-output\" />\n"
                + "          </artifacts>\n" + "        </job>\n" + "      </jobs>\n" + "    </stage>\n"
                + "  </pipeline>\n" + "</pipelines>" + "</cruise>\n";

        return ConfigMigrator.loadWithMigration(pipelineXmlPartial).config;

    }

    @Test
    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()).isTrue();
    }

    //BUG: #5209
    @Test
    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()).isTrue();
    }

    private StageConfig stageWithAuth(String role) {
        StageConfig stage = stageWithJobResource("foo");
        stage.getApproval().getAuthConfig().add(new AdminRole(new CaseInsensitiveString(role)));
        return stage;
    }

    @Test
    void shouldAllowOnlyOneOfTrackingToolOrMingleConfigInSourceXml() {
        String content = 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 &gt; '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 {
            xmlLoader.loadConfigHolder(content);
            fail("Should not allow mingle config and tracking tool together");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains("Invalid content was found starting with element 'mingle'.");
        }
    }

    @Test
    void shouldAllowTFSMaterial() {
        String content = 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()).isEqualTo(1);
        TfsMaterialConfig materialConfig = (TfsMaterialConfig) materialConfigs.get(0);
        assertThat(materialConfig)
                .isEqualTo(tfs(new GoCipher(), UrlArgument.create("tfsurl"), "foo", "", "bar", "project-path"));
    }

    @Test
    void shouldAllowAnEnvironmentVariableToBeMarkedAsSecure_WithValueInItsOwnTag() throws Exception {
        String cipherText = new GoCipher().encrypt("plainText");
        String content = 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()).isEqualTo(1);
        EnvironmentVariableConfig environmentVariableConfig = variables.get(0);
        assertThat(environmentVariableConfig.getEncryptedValue()).isEqualTo(cipherText);
        assertThat(environmentVariableConfig.isSecure()).isTrue();
    }

    @Test
    void shouldMigrateEmptyEnvironmentVariable() throws Exception {
        String content = 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()).isEqualTo(1);
        EnvironmentVariableConfig environmentVariableConfig = variables.get(0);
        assertThat(environmentVariableConfig.getName()).isEqualTo("var_name");
        assertThat(environmentVariableConfig.getValue().isEmpty()).isTrue();
    }

    @Test
    void shouldAllowAnEnvironmentVariableToBeMarkedAsSecure_WithEncryptedValueInItsOwnTag() throws Exception {
        String value = "abc";
        String encryptedValue = new GoCipher().encrypt(value);
        String content = 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()).isEqualTo(1);
        EnvironmentVariableConfig environmentVariableConfig = variables.get(0);
        assertThat(environmentVariableConfig.getEncryptedValue()).isEqualTo(encryptedValue);
        assertThat(environmentVariableConfig.isSecure()).isTrue();
    }

    @Test
    void shouldNotAllowWorkspaceOwnerAndWorkspaceAsAttributesOnTfsMaterial() {
        String content = 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
    void shouldMigrateConfigToSplitUsernameAndDomainAsAttributeOnTfsMaterial() {
        String content = 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()).isEqualTo("username");
            assertThat(material.getDomain()).isEqualTo("domain");
        } catch (Exception e) {
            fail("Valid TFS tag for migration 51 and above");
        }
    }

    @Test
    void shouldAllowUserToSpecify_PathFromAncestor_forFetchArtifactFromAncestor() {
        String content = 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 artifactOrigin='gocd' 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)).isEqualTo(
                        new FetchTask(new CaseInsensitiveString("uppest_pipeline/up_pipeline/down_pipeline"),
                                new CaseInsensitiveString("uppest_stage"), new CaseInsensitiveString("uppest_job"),
                                "src", "dest"));
    }

    @Test
    void should_NOT_allowUserToSpecifyFetchStage_afterUpstreamStage() {
        String content = 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 artifactOrigin='gocd' 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()).contains(
                    "\"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
    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>";
        GoConfigHolder goConfigHolder = ConfigMigrator.loadWithMigration(content);
        assertThat(goConfigHolder.config.server().getCommandRepositoryLocation()).isEqualTo("default");
    }

    @Test
    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).isNotNull();
        assertThat(pipeline.materialConfigs().size()).isEqualTo(1);
        MaterialConfig material = pipeline.materialConfigs().get(0);
        assertThat(material).isInstanceOf(SvnMaterialConfig.class);
        assertThat(material.getUriForDisplay()).isEqualTo("file:///tmp/foo");
        assertThat(pipeline.size()).isEqualTo(1);
        assertThat(pipeline.get(0).getJobs().size()).isEqualTo(1);
    }

    @Test
    void shouldRegisterAllGoConfigValidators() {
        List<String> list = (List<String>) CollectionUtils.collect(MagicalGoConfigXmlLoader.VALIDATORS,
                o -> o.getClass().getCanonicalName());

        assertThat(list).contains(ArtifactDirValidator.class.getCanonicalName());
        assertThat(list).contains(EnvironmentAgentValidator.class.getCanonicalName());
        assertThat(list).contains(ServerIdImmutabilityValidator.class.getCanonicalName());
        assertThat(list).contains(CommandRepositoryLocationValidator.class.getCanonicalName());
        assertThat(list).contains(TokenGenerationKeyImmutabilityValidator.class.getCanonicalName());
    }

    @Test
    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()).isEqualTo(packageDefinition);
    }

    @Test
    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()).isEqualTo(packageDefinition);
        Configuration repoConfig = packageMaterialConfig.getPackageDefinition().getRepository().getConfiguration();
        assertThat(repoConfig.get(0).getConfigurationValue().getValue()).isEqualTo("value");
        assertThat(repoConfig.get(1).getEncryptedValue()).startsWith("AES:");
        assertThat(repoConfig.get(2).getEncryptedValue()).startsWith("AES:");
        Configuration packageConfig = packageMaterialConfig.getPackageDefinition().getConfiguration();
        assertThat(packageConfig.get(0).getConfigurationValue().getValue()).isEqualTo("value");
        assertThat(packageConfig.get(1).getEncryptedValue()).startsWith("AES:");
        assertThat(packageConfig.get(2).getEncryptedValue()).startsWith("AES:");
    }

    @Test
    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()).isEqualTo(packageRepository);
    }

    @Test
    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>";

        assertFailureDuringLoad(xml, GoConfigInvalidException.class,
                "Cannot save package or repo, found duplicate packages. [Repo Name: 'name-1', Package Name: 'name-1'], [Repo Name: 'name-2', Package Name: 'name-2']");
    }

    private 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>";
    private 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>";
    private 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>";
    private 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>";
    private 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>";
    private 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>";
    private 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>";
    private 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>";

    private final static String PACKAGE = "<package id='package-id' name='name'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>";
    private final static String PACKAGE_WITH_MISSING_ID = "<package name='name'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>";
    private 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>";
    private final static String PACKAGE_WITH_EMPTY_ID = "<package id='' name='name'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>";
    private final static String PACKAGE_WITH_MISSING_NAME = "<package id='id'><configuration><property><key>name</key><value>go-agent</value></property></configuration></package>";
    private 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>";
    private 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
    void shouldThrowXsdValidationWhenPackageRepositoryIdsAreDuplicate() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO, "") + withPackages(REPO, "") + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Duplicate unique value [repo-id] declared for identity constraint of element \"repositories\".",
                "Duplicate unique value [repo-id] declared for identity constraint \"uniqueRepositoryId\" of element \"repositories\".");
    }

    @Test
    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>";
        assertXsdFailureDuringLoad(xml,
                "Duplicate unique value [repo] declared for identity constraint of element \"repositories\".",
                "Duplicate unique value [repo] declared for identity constraint \"uniqueRepositoryName\" of element \"repositories\".");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageIdsAreDuplicate() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO, format("<packages>%s%s</packages>", PACKAGE, PACKAGE))
                + " </repositories></cruise>";

        assertXsdFailureDuringLoad(xml,
                "Duplicate unique value [package-id] declared for identity constraint of element \"cruise\".",
                "Duplicate unique value [package-id] declared for identity constraint \"uniquePackageId\" of element \"cruise\".");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageRepositoryIdIsEmpty() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_EMPTY_ID, "") + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Repo id is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageRepositoryIdIsInvalid() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_INVALID_ID, "") + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Repo id is invalid. \"id with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageRepositoryNameIsMissing() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_MISSING_NAME, "") + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml, "\"Name\" is required for Repository");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageRepositoryNameIsEmpty() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_EMPTY_NAME, "") + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Name is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageRepositoryNameIsInvalid() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_INVALID_NAME, "") + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Name is invalid. \"name with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    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()).isNotNull();
        assertThat(configHolder.config.getPackageRepositories().get(0).getPackages().get(0).getId()).isNotNull();
    }

    @Test
    void shouldThrowXsdValidationWhenPackageIdIsEmpty() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_EMPTY_ID, PACKAGE_WITH_EMPTY_ID) + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Repo id is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageIdIsInvalid() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_INVALID_ID, PACKAGE_WITH_INVALID_ID) + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Repo id is invalid. \"id with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageNameIsMissing() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_MISSING_NAME, PACKAGE_WITH_MISSING_NAME) + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml, "\"Name\" is required for Repository");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageNameIsEmpty() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_EMPTY_NAME, PACKAGE_WITH_EMPTY_NAME) + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Name is invalid. \"\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    void shouldThrowXsdValidationWhenPackageNameIsInvalid() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'><repositories>\n"
                + withPackages(REPO_WITH_INVALID_NAME, PACKAGE_WITH_INVALID_NAME) + " </repositories></cruise>";
        assertXsdFailureDuringLoad(xml,
                "Name is invalid. \"name with space\" should conform to the pattern - [a-zA-Z0-9_\\-]{1}[a-zA-Z0-9_\\-.]*");
    }

    @Test
    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()).isFalse();

        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()).isTrue();
    }

    @Test
    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())
                .isEqualTo("repo_name:pkg_name");
    }

    @Test
    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).isNotNull();
        assertThat(authorization.getAdminsConfig().getUsers()).isEmpty();
        assertThat(authorization.getAdminsConfig().getRoles()).isEmpty();
    }

    @Test
    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()).isEqualTo(1);
        assertThat(tasks.get(0) instanceof PluggableTask).isTrue();
        PluggableTask task = (PluggableTask) tasks.get(0);
        assertThat(task.getTaskType()).isEqualTo("pluggable_task_plugin_id_1");
        assertThat(task.getTypeForDisplay()).isEqualTo("Pluggable Task");
        final Configuration configuration = task.getConfiguration();
        assertThat(configuration.listOfConfigKeys().size()).isEqualTo(3);
        assertThat(configuration.listOfConfigKeys()).isEqualTo(asList("url", "username", "password"));
        Collection<String> values = CollectionUtils.collect(configuration.listOfConfigKeys(), o -> {
            ConfigurationProperty property = configuration.getProperty(o);
            return property.getConfigurationValue().getValue();
        });
        assertThat(new ArrayList<>(values)).isEqualTo(asList("http://fake-go-server", "godev", "password"));
    }

    @Test
    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();

        assertThat(task.getConfiguration().getProperty("username").isSecure()).isFalse();
        assertThat(task.getConfiguration().getProperty("password").isSecure()).isTrue();
    }

    @Test
    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()).isEqualTo(expectedViewConfig);
    }

    @Test
    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()).isTrue();
    }

    @Test
    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()).isFalse();
    }

    @Test
    void shouldSerializeJobElasticProfileId() throws Exception {
        String configWithJobElasticProfileId = "<cruise schemaVersion='119'>\n"
                + "  <elastic jobStarvationTimeout=\"10\">\n" + "    <profiles>\n"
                + "      <profile clusterProfileId='blah' id='unit-test' pluginId='aws'>\n" + "        <property>\n"
                + "          <key>instance-type</key>\n" + "          <value>m1.small</value>\n"
                + "        </property>\n" + "      </profile>\n" + "    </profiles>\n" + "    <clusterProfiles>"
                + "      <clusterProfile id=\"blah\" pluginId=\"aws\"/>" + "    </clusterProfiles>\n"
                + "  </elastic>\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).isEqualTo("unit-test");
    }

    @Test
    void shouldSerializeElasticAgentProfiles() throws Exception {
        String configWithElasticProfile = "<cruise schemaVersion='119'>\n"
                + "  <elastic jobStarvationTimeout=\"2\">\n" + "    <profiles>\n"
                + "      <profile clusterProfileId='blah' id=\"foo\" pluginId=\"docker\">\n"
                + "          <property>\n" + "           <key>USERNAME</key>\n" + "           <value>bob</value>\n"
                + "          </property>\n" + "      </profile>\n" + "    </profiles>\n" + "    <clusterProfiles>"
                + "      <clusterProfile id=\"blah\" pluginId=\"docker\"/>" + "    </clusterProfiles>\n"
                + "  </elastic>\n" + "</cruise>\n";

        CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configWithElasticProfile).configForEdit;

        assertThat(cruiseConfig.getElasticConfig().getJobStarvationTimeout()).isEqualTo(120000L);
        assertThat(cruiseConfig.getElasticConfig().getProfiles().size()).isEqualTo(1);

        ElasticProfile elasticProfile = cruiseConfig.getElasticConfig().getProfiles().find("foo");
        assertThat(elasticProfile).isNotNull();
        assertThat(elasticProfile.size()).isEqualTo(1);
        assertThat(elasticProfile.getProperty("USERNAME").getValue()).isEqualTo("bob");
    }

    @Test
    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 {
            xmlLoader.loadConfigHolder(configWithJobElasticProfile);
            fail("expected exception!");
        } catch (Exception e) {
            assertThat(e.getMessage()).isEqualTo(
                    "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
    void shouldGetConfigRepoPreprocessor() {
        MagicalGoConfigXmlLoader loader = new MagicalGoConfigXmlLoader(null, null);
        assertThat(loader.getPreprocessorOfType(
                ConfigRepoPartialPreprocessor.class) instanceof ConfigRepoPartialPreprocessor).isTrue();
        assertThat(loader.getPreprocessorOfType(ConfigParamPreprocessor.class) instanceof ConfigParamPreprocessor)
                .isTrue();
    }

    @Test
    void shouldMigrateEncryptedEnvironmentVariablesWithNewlineAndSpaces_XslMigrationFrom88To90() throws Exception {
        resetCipher.setupDESCipherFile();

        String plainText = "user-password!";
        // "user-password!" encrypted using the above key
        String encryptedValue = "mvcX9yrQsM4iPgm1tDxN1A==";
        String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue)
                .insert(2, "\r\n" + "                        ").toString();

        String content = 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()).isEqualTo(plainText);
        assertThat(config.allPipelines().get(0).getVariables().get(0).getEncryptedValue()).startsWith("AES:");
    }

    @Test
    void shouldMigrateEncryptedPluginPropertyValueWithNewlineAndSpaces_XslMigrationFrom88To90() throws Exception {
        resetCipher.setupDESCipherFile();

        String plainText = "user-password!";
        // "user-password!" encrypted using the above key
        String encryptedValue = "mvcX9yrQsM4iPgm1tDxN1A==";
        String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue)
                .insert(2, "\r\n" + "                        ").toString();

        String content = 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())
                .isEqualTo(plainText);
        assertThat(config.getSCMs().get(0).getConfiguration().getProperty("secureKey").getEncryptedValue())
                .startsWith("AES:");
        assertThat(config.getSCMs().get(0).getConfiguration().getProperty("plainTextKey").getValue())
                .isEqualTo("https://url/some_path");
    }

    @Test
    void shouldMigrateEncryptedMaterialPasswordWithNewlineAndSpaces_XslMigrationFrom88To90() throws Exception {
        resetCipher.setupDESCipherFile();

        String plainText = "user-password!";
        // "user-password!" encrypted using the above key
        String encryptedValue = "mvcX9yrQsM4iPgm1tDxN1A==";
        String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue)
                .insert(2, "\r\n" + "                        ").toString();

        String content = 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()).isEqualTo(plainText);
        assertThat(svnMaterialConfig.getEncryptedPassword()).startsWith("AES:");
        assertThat(svnMaterialConfig.getFilterAsString()).isEqualTo("**/*");
        TfsMaterialConfig tfs = (TfsMaterialConfig) materialConfigs.get(1);
        assertThat(tfs.getPassword()).isEqualTo(plainText);
        assertThat(tfs.getEncryptedPassword()).startsWith("AES:");
        assertThat(tfs.getUrl()).isEqualTo("tfsurl");
        P4MaterialConfig p4 = (P4MaterialConfig) materialConfigs.get(2);
        assertThat(p4.getPassword()).isEqualTo(plainText);
        assertThat(p4.getEncryptedPassword()).startsWith("AES:");
        assertThat(p4.getServerAndPort()).isEqualTo("host:9999");
    }

    @Test
    void shouldMigrateServerMailhostEncryptedPasswordWithNewlineAndSpaces_XslMigrationFrom88To90()
            throws Exception {
        resetCipher.setupDESCipherFile();

        String plainText = "user-password!";
        // "user-password!" encrypted using the above key
        String encryptedValue = "mvcX9yrQsM4iPgm1tDxN1A==";
        String encryptedValueWithWhitespaceAndNewline = new StringBuilder(encryptedValue)
                .insert(2, "\r\n" + "                        ").toString();

        String content = 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()).isEqualTo(plainText);
        assertThat(config.server().mailHost().getEncryptedPassword()).startsWith("AES:");
        assertThat(config.server().mailHost().getHostName()).isEqualTo("host");
    }

    @Test
    void shouldFailValidationForPipelineWithDuplicateStageNames() throws Exception {
        assertFailureDuringLoad(PIPELINES_WITH_DUPLICATE_STAGE_NAME, RuntimeException.class,
                "You have defined multiple stages called 'mingle'. Stage names are case-insensitive and must be unique.");
    }

    @Test
    void shouldThrowExceptionIfBuildPlansExistWithTheSameNameWithinAPipeline() throws Exception {
        assertXsdFailureDuringLoad(JOBS_WITH_SAME_NAME,
                "Duplicate unique value [unit] declared for identity constraint of element \"jobs\".",
                "Duplicate unique value [unit] declared for identity constraint \"uniqueJob\" of element \"jobs\".");
    }

    @Test
    void shouldThrowExceptionIfPipelineDoesNotContainAnyBuildPlans() throws Exception {
        assertXsdFailureDuringLoad(STAGE_WITH_NO_JOBS,
                "The content of element 'jobs' is not complete. One of '{job}' is expected.");
    }

    @Test
    void shouldAllowOnlyThreeValuesForLockBehavior() throws Exception {
        xmlLoader.loadConfigHolder(pipelineWithAttributes(
                "name=\"p1\" lockBehavior=\"" + LOCK_VALUE_LOCK_ON_FAILURE + "\"", CONFIG_SCHEMA_VERSION));
        xmlLoader.loadConfigHolder(pipelineWithAttributes(
                "name=\"p2\" lockBehavior=\"" + LOCK_VALUE_UNLOCK_WHEN_FINISHED + "\"", CONFIG_SCHEMA_VERSION));
        xmlLoader.loadConfigHolder(pipelineWithAttributes("name=\"p3\" lockBehavior=\"" + LOCK_VALUE_NONE + "\"",
                CONFIG_SCHEMA_VERSION));
        xmlLoader.loadConfigHolder(
                pipelineWithAttributes("name=\"pipelineWithNoLockBehaviorDefined\"", CONFIG_SCHEMA_VERSION));

        assertXsdFailureDuringLoad(
                pipelineWithAttributes("name=\"pipelineWithWrongLockBehavior\" lockBehavior=\"some-random-value\"",
                        CONFIG_SCHEMA_VERSION),
                "Value 'some-random-value' is not facet-valid with respect to enumeration '[lockOnFailure, unlockWhenFinished, none]'. It must be a value from the enumeration.");
    }

    @Test
    void shouldDisallowModificationOfTokenGenerationKeyWhileTheServerIsOnline() throws Exception {
        xmlLoader.loadConfigHolder(configWithTokenGenerationKey("something"));

        systemEnvironment.setProperty("go.enforce.server.immutability", "Y");
        assertFailureDuringLoad(configWithTokenGenerationKey("something-else"), RuntimeException.class,
                "The value of 'tokenGenerationKey' cannot be modified while the server is online. If you really want to make this change, you may do so while the server is offline. Please note: updating 'tokenGenerationKey' will invalidate all registration tokens issued to the agents so far.");
    }

    private String configWithTokenGenerationKey(final String key) {
        final ServerIdImmutabilityValidator serverIdImmutabilityValidator = (ServerIdImmutabilityValidator) MagicalGoConfigXmlLoader.VALIDATORS
                .stream().filter(new Predicate<GoConfigValidator>() {
                    @Override
                    public boolean test(GoConfigValidator goConfigValidator) {
                        return goConfigValidator instanceof ServerIdImmutabilityValidator;
                    }
                }).findFirst().orElse(null);
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><cruise schemaVersion=\"" + CONFIG_SCHEMA_VERSION
                + "\">\n" + "<server serverId=\"" + serverIdImmutabilityValidator.getServerId()
                + "\" tokenGenerationKey=\"" + key + "\"/>" + "<pipelines>\n" + "</pipelines>\n" + "</cruise>";
    }

    @Test
    void shouldDeserializeArtifactStores() {
        String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>" + "<artifactStores>\n"
                + "    <artifactStore pluginId=\"foo\" id=\"bar\">\n" + "        <property>\n"
                + "            <key>ACCESS_KEY</key>\n" + "            <value>dasdas</value>\n"
                + "        </property>\n" + "    </artifactStore>\n"
                + "    <artifactStore pluginId=\"bar\" id=\"foo\">\n" + "        <property>\n"
                + "            <key>SECRET_ACCESS_KEY</key>\n" + "            <value>$rrhsdhjf</value>\n"
                + "        </property>\n" + "    </artifactStore>\n" + "</artifactStores>" + "</cruise>";

        final CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configXml).configForEdit;
        assertThat(cruiseConfig.getArtifactStores()).hasSize(2);
        assertThat(cruiseConfig.getArtifactStores()).containsExactly(
                new ArtifactStore("bar", "foo", create("ACCESS_KEY", false, "dasdas")),
                new ArtifactStore("foo", "bar", create("SECRET_ACCESS_KEY", false, "$rrhsdhjf")));
    }

    @Test
    void shouldNotDeserializeArtifactStoreWhenIdIsNotDefined() {
        String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>" + "<artifactStores>\n"
                + "    <artifactStore pluginId=\"foo\">\n" + "        <property>\n"
                + "            <key>ACCESS_KEY</key>\n" + "            <value>dasdas</value>\n"
                + "        </property>\n" + "    </artifactStore>\n" + "</artifactStores>" + "</cruise>";

        try {
            xmlLoader.loadConfigHolder(configXml);
            fail("An exception was expected");
        } catch (Exception e) {
            assertThat(e.getMessage()).isEqualTo("\"Id\" is required for ArtifactStore");
        }
    }

    @Test
    void shouldNotDeserializeArtifactStoreWhenPluginIdIsNotDefined() {
        String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>" + "<artifactStores>\n"
                + "    <artifactStore id=\"foo\">\n" + "        <property>\n"
                + "            <key>ACCESS_KEY</key>\n" + "            <value>dasdas</value>\n"
                + "        </property>\n" + "    </artifactStore>\n" + "</artifactStores>" + "</cruise>";

        try {
            xmlLoader.loadConfigHolder(configXml);
            fail("An exception was expected");
        } catch (Exception e) {
            assertThat(e.getMessage()).isEqualTo("\"Plugin id\" is required for ArtifactStore");
        }
    }

    @Test
    void shouldDeserializePluggableArtifactConfig() {
        String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>" + "<artifactStores>\n"
                + "    <artifactStore pluginId=\"cd.go.s3\" id=\"s3\">\n" + "        <property>\n"
                + "            <key>ACCESS_KEY</key>\n" + "            <value>dasdas</value>\n"
                + "        </property>\n" + "    </artifactStore>\n"
                + "    <artifactStore pluginId=\"bar\" id=\"foo\">\n" + "        <property>\n"
                + "            <key>SECRET_ACCESS_KEY</key>\n" + "            <value>$rrhsdhjf</value>\n"
                + "        </property>\n" + "    </artifactStore>\n" + "</artifactStores>"
                + "<pipelines group=\"first\">\n" + "    <pipeline name=\"up42\">\n" + "      <materials>\n"
                + "        <git url=\"test-repo\" />\n" + "      </materials>\n"
                + "      <stage name=\"up42_stage\">\n" + "        <jobs>\n" + "          <job name=\"up42_job\">\n"
                + "            <tasks>\n" + "              <exec command=\"ls\">\n"
                + "                <runif status=\"passed\" />\n" + "              </exec>\n"
                + "            </tasks>\n" + "            <artifacts>\n"
                + "              <artifact id=\"installer\" storeId=\"s3\" type=\"external\">\n"
                + "               <configuration>" + "                <property>\n"
                + "                  <key>filename</key>\n" + "                  <value>foo.xml</value>\n"
                + "                </property>\n" + "               </configuration>"
                + "              </artifact>\n" + "            </artifacts>\n" + "          </job>\n"
                + "        </jobs>\n" + "      </stage>\n" + "    </pipeline>\n" + "  </pipelines>" + "</cruise>";

        final CruiseConfig cruiseConfig = ConfigMigrator.loadWithMigration(configXml).configForEdit;
        final ArtifactConfigs artifactConfigs = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("up42"))
                .getStage("up42_stage").getJobs().getJob(new CaseInsensitiveString("up42_job")).artifactConfigs();

        assertThat(artifactConfigs).hasSize(1);
        assertThat(artifactConfigs).containsExactly(
                new PluggableArtifactConfig("installer", "s3", create("filename", false, "foo.xml")));
    }

    @Test
    void shouldNotDeserializePluggableArtifactConfigWhenIdIsNotDefined() {
        String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>" + "<artifactStores>\n"
                + "    <artifactStore pluginId=\"cd.go.s3\" id=\"s3\">\n" + "        <property>\n"
                + "            <key>ACCESS_KEY</key>\n" + "            <value>dasdas</value>\n"
                + "        </property>\n" + "    </artifactStore>\n"
                + "    <artifactStore pluginId=\"bar\" id=\"foo\">\n" + "        <property>\n"
                + "            <key>SECRET_ACCESS_KEY</key>\n" + "            <value>$rrhsdhjf</value>\n"
                + "        </property>\n" + "    </artifactStore>\n" + "</artifactStores>"
                + "<pipelines group=\"first\">\n" + "    <pipeline name=\"up42\">\n" + "      <materials>\n"
                + "        <git url=\"test-repo\" />\n" + "      </materials>\n"
                + "      <stage name=\"up42_stage\">\n" + "        <jobs>\n" + "          <job name=\"up42_job\">\n"
                + "            <tasks>\n" + "              <exec command=\"ls\">\n"
                + "                <runif status=\"passed\" />\n" + "              </exec>\n"
                + "            </tasks>\n" + "            <artifacts>\n"
                + "              <artifact type=\"external\" storeId=\"s3\">\n" + "               <configuration>"
                + "                <property>\n" + "                  <key>filename</key>\n"
                + "                  <value>foo.xml</value>\n" + "                </property>\n"
                + "               </configuration>" + "              </artifact>\n" + "            </artifacts>\n"
                + "          </job>\n" + "        </jobs>\n" + "      </stage>\n" + "    </pipeline>\n"
                + "  </pipelines>" + "</cruise>";

        try {
            ConfigMigrator.loadWithMigration(configXml);
            fail("should fail");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains("\"Id\" is required for PluggableArtifact");
        }
    }

    @Test
    void shouldNotDeserializePluggableArtifactConfigWhenStoreIdIsNotDefined() {
        String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>" + "<artifactStores>\n"
                + "    <artifactStore pluginId=\"cd.go.s3\" id=\"s3\">\n" + "        <property>\n"
                + "            <key>ACCESS_KEY</key>\n" + "            <value>dasdas</value>\n"
                + "        </property>\n" + "    </artifactStore>\n"
                + "    <artifactStore pluginId=\"bar\" id=\"foo\">\n" + "        <property>\n"
                + "            <key>SECRET_ACCESS_KEY</key>\n" + "            <value>$rrhsdhjf</value>\n"
                + "        </property>\n" + "    </artifactStore>\n" + "</artifactStores>"
                + "<pipelines group=\"first\">\n" + "    <pipeline name=\"up42\">\n" + "      <materials>\n"
                + "        <git url=\"test-repo\" />\n" + "      </materials>\n"
                + "      <stage name=\"up42_stage\">\n" + "        <jobs>\n" + "          <job name=\"up42_job\">\n"
                + "            <tasks>\n" + "              <exec command=\"ls\">\n"
                + "                <runif status=\"passed\" />\n" + "              </exec>\n"
                + "            </tasks>\n" + "            <artifacts>\n"
                + "              <artifact type=\"external\" id=\"installer\">\n" + "               <configuration>"
                + "                <property>\n" + "                  <key>filename</key>\n"
                + "                  <value>foo.xml</value>\n" + "                </property>\n"
                + "               </configuration>" + "              </artifact>\n" + "            </artifacts>\n"
                + "          </job>\n" + "        </jobs>\n" + "      </stage>\n" + "    </pipeline>\n"
                + "  </pipelines>" + "</cruise>";

        try {
            ConfigMigrator.loadWithMigration(configXml);
            fail("An exception was expected");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains("\"Store id\" is required for PluggableArtifact");
        }
    }

    @Test
    void shouldNotDeserializePluggableArtifactConfigWhenStoreWithIdNotFound() {
        String configXml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>"
                + "<pipelines group=\"first\">\n" + "    <pipeline name=\"up42\">\n" + "      <materials>\n"
                + "        <git url=\"test-repo\" />\n" + "      </materials>\n"
                + "      <stage name=\"up42_stage\">\n" + "        <jobs>\n" + "          <job name=\"up42_job\">\n"
                + "            <tasks>\n" + "              <exec command=\"ls\">\n"
                + "                <runif status=\"passed\" />\n" + "              </exec>\n"
                + "            </tasks>\n" + "            <artifacts>\n"
                + "              <artifact type=\"external\" id=\"installer\" storeId=\"s3\">\n"
                + "               <configuration>" + "                <property>\n"
                + "                  <key>filename</key>\n" + "                  <value>foo.xml</value>\n"
                + "                </property>\n" + "               </configuration>"
                + "              </artifact>\n" + "            </artifacts>\n" + "          </job>\n"
                + "        </jobs>\n" + "      </stage>\n" + "    </pipeline>\n" + "  </pipelines>" + "</cruise>";

        try {
            ConfigMigrator.loadWithMigration(configXml);
            fail("An exception was expected");
        } catch (Exception e) {
            assertThat(e.getMessage()).contains("Artifact store with id `s3` does not exist.");
        }
    }

    @Test
    void shouldMigrateDESEncryptedEnvironmentVariables_XslMigrationFrom108To109() throws Exception {
        resetCipher.setupDESCipherFile();

        String clearText = "user-password!";
        // "user-password!" encrypted using the above key
        String desEncryptedPassword = "mvcX9yrQsM4iPgm1tDxN1A==";

        String content = configWithPipeline("" + "<pipeline name='some_pipeline'>" + "  <environmentvariables>"
                + "    <variable name='var_name' secure='true'>" + "      <encryptedValue>" + desEncryptedPassword
                + "</encryptedValue>" + "    </variable>" + "   </environmentvariables>" + "    <materials>"
                + "      <svn url='svnurl'/>" + "    </materials>" + "  <stage name='some_stage'>" + "    <jobs>"
                + "      <job name='some_job'>" + "      </job>" + "    </jobs>" + "  </stage>" + "</pipeline>",
                108);

        CruiseConfig config = ConfigMigrator.loadWithMigration(content).config;
        assertThat(config.allPipelines().get(0).getVariables().get(0).getValue()).isEqualTo(clearText);
        String encryptedValue = config.allPipelines().get(0).getVariables().get(0).getEncryptedValue();
        assertThat(encryptedValue).startsWith("AES:");
        assertThat(new AESEncrypter(new AESCipherProvider(systemEnvironment)).decrypt(encryptedValue))
                .isEqualTo("user-password!");
    }

    @Test
    void shouldRemoveEmptySCMPasswordAndEncryptedPasswordAttributes_XslMigrationFrom109To110() throws Exception {
        resetCipher.setupDESCipherFile();

        String content = configWithPipeline("<pipeline name='some_pipeline'>" + "    <materials>"
                + "      <svn url='svn1' username='bob' encryptedPassword='' dest='svn1'/>"
                + "      <svn url='svn2' username='bob' password='' dest='svn2'/>"
                + "      <tfs url='tfsurl1' username='user' domain='domain' encryptedPassword='' projectPath='path' dest='tfs1' />"
                + "      <tfs url='tfsurl2' username='user' domain='domain' password='' projectPath='path' dest='tfs2' />"
                + "      <p4 port='host:9999' username='user' encryptedPassword='' dest='perforce1'>"
                + "          <view><![CDATA[view]]></view>" + "        </p4>"
                + "      <p4 port='host:9999' username='user' password='' dest='perforce2'>"
                + "          <view><![CDATA[view]]></view>" + "        </p4>" + "    </materials>"
                + "  <stage name='some_stage'>" + "    <jobs>" + "      <job name='some_job'>" + "      </job>"
                + "    </jobs>" + "  </stage>" + "</pipeline>", 109);

        assertThat(XpathUtils.nodeExists(content, "//*[@password='']")).isTrue();
        assertThat(XpathUtils.nodeExists(content, "//*[@encryptedPassword='']")).isTrue();

        CruiseConfig config = ConfigMigrator.loadWithMigration(content).config;

        String xmlPartial = xmlWriter.toXmlPartial(config);

        assertThat(XpathUtils.nodeExists(xmlPartial, "//@password")).isFalse();
        assertThat(XpathUtils.nodeExists(xmlPartial, "//@encryptedPassword")).isFalse();
    }

    @Test
    void shouldMigrateDESEncryptedPluginPropertyValue_XslMigrationFrom108To109() throws Exception {
        resetCipher.setupDESCipherFile();

        String clearText = "user-password!";
        // "user-password!" encrypted using the above key
        String desEncryptedPassword = "mvcX9yrQsM4iPgm1tDxN1A==";

        String content = configWithPluggableScm(""
                + "  <scm id='f7c309f5-ea4d-41c5-9c43-95d79fa9ec7b' name='gocd-private'>"
                + "      <pluginConfiguration id='github.pr' version='1' />" + "      <configuration>"
                + "        <property>" + "          <key>plainTextKey</key>"
                + "          <value>https://url/some_path</value>" + "        </property>" + "        <property>"
                + "          <key>secureKey</key>" + "          <encryptedValue>" + desEncryptedPassword
                + "</encryptedValue>" + "        </property>" + "      </configuration>" + "    </scm>", 108);

        CruiseConfig config = ConfigMigrator.loadWithMigration(content).config;
        assertThat(config.getSCMs().get(0).getConfiguration().getProperty("secureKey").getValue())
                .isEqualTo(clearText);
        String encryptedValue = config.getSCMs().get(0).getConfiguration().getProperty("secureKey")
                .getEncryptedValue();

        assertThat(encryptedValue).startsWith("AES:");
        assertThat(new AESEncrypter(new AESCipherProvider(systemEnvironment)).decrypt(encryptedValue))
                .isEqualTo("user-password!");

        assertThat(config.getSCMs().get(0).getConfiguration().getProperty("plainTextKey").getValue())
                .isEqualTo("https://url/some_path");
    }

    @Test
    void shouldMigrateDESEncryptedMaterialPassword_XslMigrationFrom108To109() throws Exception {
        resetCipher.setupDESCipherFile();

        String clearText = "user-password!";
        // "user-password!" encrypted using the above key
        String desEncryptedPassword = "mvcX9yrQsM4iPgm1tDxN1A==";

        String content = configWithPipeline("<pipeline name='some_pipeline'>" + "    <materials>"
                + "      <svn url='asdsa' username='user' encryptedPassword='" + desEncryptedPassword
                + "' dest='svn'/>" + "      <tfs url='tfsurl' username='user' domain='domain' encryptedPassword='"
                + desEncryptedPassword + "' projectPath='path' dest='tfs' />"
                + "      <p4 port='host:9999' username='user' encryptedPassword='" + desEncryptedPassword
                + "' dest='perforce'>" + "          <view><![CDATA[view]]></view>" + "        </p4>"
                + "    </materials>" + "  <stage name='some_stage'>" + "    <jobs>" + "      <job name='some_job'>"
                + "      </job>" + "    </jobs>" + "  </stage>" + "</pipeline>", 108);

        CruiseConfig config = ConfigMigrator.loadWithMigration(content).config;
        MaterialConfigs materialConfigs = config.allPipelines().get(0).materialConfigs();
        SvnMaterialConfig svnMaterialConfig = (SvnMaterialConfig) materialConfigs.get(0);
        assertThat(svnMaterialConfig.getPassword()).isEqualTo(clearText);
        assertThat(svnMaterialConfig.getEncryptedPassword()).startsWith("AES:");
        TfsMaterialConfig tfs = (TfsMaterialConfig) materialConfigs.get(1);
        assertThat(tfs.getPassword()).isEqualTo(clearText);
        assertThat(tfs.getEncryptedPassword()).startsWith("AES:");
        assertThat(tfs.getUrl()).isEqualTo("tfsurl");
        P4MaterialConfig p4 = (P4MaterialConfig) materialConfigs.get(2);
        assertThat(p4.getPassword()).isEqualTo(clearText);
        assertThat(p4.getEncryptedPassword()).startsWith("AES:");
        assertThat(p4.getServerAndPort()).isEqualTo("host:9999");
    }

    @Test
    void shouldMigrateDESServerMailhostEncryptedPassword_XslMigrationFrom108To109() throws Exception {
        resetCipher.setupDESCipherFile();

        String clearText = "user-password!";
        // "user-password!" encrypted using the above key
        String desEncryptedPassword = "mvcX9yrQsM4iPgm1tDxN1A==";

        String content = config("<server artifactsdir='artifacts'>"
                + "    <mailhost hostname='host' port='25' username='user' encryptedPassword='"
                + desEncryptedPassword + "' tls='false' from='user@domain.com' admin='admin@domain.com' />"
                + "  </server>", 108);

        CruiseConfig config = ConfigMigrator.loadWithMigration(content).config;
        assertThat(config.server().mailHost().getPassword()).isEqualTo(clearText);
        assertThat(config.server().mailHost().getEncryptedPassword()).startsWith("AES:");
        assertThat(config.server().mailHost().getHostName()).isEqualTo("host");
    }

    @Test
    void shouldEncryptPluggablePublishArtifactProperties() throws Exception {
        PluginDescriptor pluginDescriptor = new GoPluginDescriptor("cd.go.artifact.docker.registry", "1.0", null,
                null, null, false);
        PluginConfiguration buildFile = new PluginConfiguration("BuildFile", new Metadata(false, false));
        PluginConfiguration image = new PluginConfiguration("Image", new Metadata(false, true));
        PluginConfiguration tag = new PluginConfiguration("Tag", new Metadata(false, false));
        PluginConfiguration fetchProperty = new PluginConfiguration("FetchProperty", new Metadata(false, true));
        PluginConfiguration fetchTag = new PluginConfiguration("Tag", new Metadata(false, false));
        PluginConfiguration registryUrl = new PluginConfiguration("RegistryURL", new Metadata(true, false));
        PluginConfiguration username = new PluginConfiguration("Username", new Metadata(false, false));
        PluginConfiguration password = new PluginConfiguration("Password", new Metadata(false, true));
        PluggableInstanceSettings storeConfigSettings = new PluggableInstanceSettings(
                asList(registryUrl, username, password));
        PluggableInstanceSettings publishArtifactSettings = new PluggableInstanceSettings(
                asList(buildFile, image, tag));
        PluggableInstanceSettings fetchArtifactSettings = new PluggableInstanceSettings(
                asList(fetchProperty, fetchTag));
        ArtifactPluginInfo artifactPluginInfo = new ArtifactPluginInfo(pluginDescriptor, storeConfigSettings,
                publishArtifactSettings, fetchArtifactSettings, null, new Capabilities());
        ArtifactMetadataStore.instance().setPluginInfo(artifactPluginInfo);

        String content = goConfigMigration.upgradeIfNecessary(IOUtils
                .toString(getClass().getResourceAsStream("/data/pluggable_artifacts_with_params.xml"), UTF_8));

        CruiseConfig config = xmlLoader.loadConfigHolder(content).configForEdit;
        PipelineConfig ancestor = config.pipelineConfigByName(new CaseInsensitiveString("ancestor"));
        PipelineConfig parent = config.pipelineConfigByName(new CaseInsensitiveString("parent"));
        PipelineConfig child = config.pipelineConfigByName(new CaseInsensitiveString("child"));

        Configuration ancestorPublishArtifactConfig = ancestor.get(0).getJobs().first().artifactConfigs()
                .getPluggableArtifactConfigs().get(0).getConfiguration();
        Configuration parentPublishArtifactConfig = parent.get(0).getJobs().first().artifactConfigs()
                .getPluggableArtifactConfigs().get(0).getConfiguration();
        Configuration childFetchArtifactFromAncestorConfig = ((FetchPluggableArtifactTask) child.get(0).getJobs()
                .first().tasks().get(0)).getConfiguration();
        Configuration childFetchArtifactFromParentConfig = ((FetchPluggableArtifactTask) child.get(0).getJobs()
                .first().tasks().get(1)).getConfiguration();
        ArtifactStore dockerhubStore = config.getArtifactStores().first();

        assertConfigProperty(ancestorPublishArtifactConfig, "Image", "IMAGE_SECRET", true);
        assertConfigProperty(ancestorPublishArtifactConfig, "Tag", "ancestor_tag_${GO_PIPELINE_COUNTER}", false);

        assertConfigProperty(parentPublishArtifactConfig, "Image", "IMAGE_SECRET", true);
        assertConfigProperty(parentPublishArtifactConfig, "Tag", "parent_tag_${GO_PIPELINE_COUNTER}", false);

        assertConfigProperty(childFetchArtifactFromAncestorConfig, "FetchProperty", "SECRET", true);
        assertConfigProperty(childFetchArtifactFromAncestorConfig, "Tag", "ancestor_tag", false);

        assertConfigProperty(childFetchArtifactFromParentConfig, "FetchProperty", "SECRET", true);
        assertConfigProperty(childFetchArtifactFromParentConfig, "Tag", "parent_tag", false);

        assertConfigProperty(dockerhubStore, "RegistryURL", "https://index.docker.io/v1/", false);
        assertConfigProperty(dockerhubStore, "Username", "docker-user", false);
        assertConfigProperty(dockerhubStore, "Password", "SECRET", true);
    }

    @Test
    void shouldLoadSecretConfigs() {
        String content = config(
                "<secretConfigs>" + "<secretConfig id=\"my_secret\" pluginId=\"gocd_file_based_plugin\">\n"
                        + "    <description>All secrets for env1</description>" + "    <configuration>"
                        + "       <property>\n" + "           <key>PasswordFilePath</key>\n"
                        + "           <value>/godata/config/password.properties</value>\n" + "       </property>\n"
                        + "    </configuration>" + "    <rules>\n"
                        + "        <deny action=\"refer\" type=\"pipeline_group\">my_group</deny>\n"
                        + "        <allow action=\"refer\" type=\"pipeline_group\">other_group</allow>  \n"
                        + "    </rules>\n" + "</secretConfig>" + "</secretConfigs>",
                116);

        CruiseConfig config = ConfigMigrator.load(content);
        SecretConfigs secretConfigs = config.getSecretConfigs();
        assertThat(secretConfigs.size()).isEqualTo(1);

        SecretConfig secretConfig = secretConfigs.first();
        assertThat(secretConfig.getId()).isEqualTo("my_secret");
        assertThat(secretConfig.getPluginId()).isEqualTo("gocd_file_based_plugin");
        assertThat(secretConfig.getDescription()).isEqualTo("All secrets for env1");

        Configuration configuration = secretConfig.getConfiguration();
        assertThat(configuration.size()).isEqualTo(1);
        assertThat(configuration.getProperty("PasswordFilePath").getValue())
                .isEqualTo("/godata/config/password.properties");

        Rules rules = secretConfig.getRules();
        assertThat(rules.size()).isEqualTo(2);
        assertThat(rules).containsExactly(new Deny("refer", "pipeline_group", "my_group"),
                new Allow("refer", "pipeline_group", "other_group"));

    }

    @Test
    void shouldNotAllowMoreThanOneOnCancelTaskWhenDefined() throws Exception {
        String xml = "<cruise schemaVersion='" + CONFIG_SCHEMA_VERSION + "'>\n"
                + "<server artifactsdir='artifactsDir' >" + "</server>" + "<pipelines>\n"
                + "<pipeline name='pipeline1' template='abc'>\n" + "    <materials>\n"
                + "      <svn url ='svnurl' username='foo' password='password'/>" + "    </materials>\n"
                + "</pipeline>\n" + "</pipelines>\n" + "<templates>\n" + "  <pipeline name='abc'>\n"
                + "    <stage name='stage1'>" + "      <jobs>" + "        <job name='job1'>" + "         <tasks>"
                + "             <exec command=\"rake\">\n" + "                 <arg>all_test</arg>\n"
                + "                 <oncancel>\n" + "                     <ant target='kill' />\n"
                + "                     <ant target='kill' />\n" + "                 </oncancel>\n"
                + "             </exec>" + "         </tasks>" + "        </job>" + "      </jobs>" + "    </stage>"
                + "  </pipeline>\n" + "</templates>\n" + "</cruise>";

        try {
            xmlLoader.loadConfigHolder(xml);
            fail("Should have failed with an exception");
        } catch (Exception e) {
            assertThat(e.getMessage()).isEqualTo(
                    "Invalid content was found starting with element 'ant'. No child element is expected at this point.");
        }
    }

    @Test
    void shouldLoadHgConfigWithBranchAttributePostSchemaVersion123() throws Exception {
        String content = config("<config-repos>" + "    <config-repo id=\"Test\" pluginId=\"cd.go.json\">"
                + "        <hg url=\"http://domain.com\" branch=\"feature\" />" + "     </config-repo>"
                + "</config-repos>" + "<pipelines group=\"first\">"
                + "    <pipeline name=\"Test\" template=\"test_template\">" + "      <materials>"
                + "          <hg url=\"http://domain.com\" branch=\"feature\" />" + "      </materials>"
                + "     </pipeline>" + "</pipelines>" + "<templates>" + "    <pipeline name=\"test_template\">"
                + "      <stage name=\"Functional\">" + "        <jobs>" + "          <job name=\"Functional\">"
                + "            <tasks>" + "              <exec command=\"echo\" args=\"Hello World!!!\" />"
                + "            </tasks>" + "           </job>" + "        </jobs>" + "      </stage>"
                + "    </pipeline>" + "</templates>", CONFIG_SCHEMA_VERSION);

        CruiseConfig config = xmlLoader.loadConfigHolder(content).config;

        PipelineConfig pipelineConfig = config.getPipelineConfigByName(new CaseInsensitiveString("Test"));
        assertThat(pipelineConfig.materialConfigs()).hasSize(1);
        assertThat(((HgMaterialConfig) pipelineConfig.materialConfigs().get(0)).getBranch()).isEqualTo("feature");

        assertThat(config.getConfigRepos()).hasSize(1);
        assertThat(((HgMaterialConfig) config.getConfigRepos().get(0).getMaterialConfig()).getBranch())
                .isEqualTo("feature");

    }

    @Test
    void shouldLoadRulesConfigWhereActionAndTypeHasWildcardForSchemaVersion124() throws Exception {
        String content = config(
                "<secretConfigs>\n" + " <secretConfig id=\"example\" pluginId=\"vault_based_plugin\">\n"
                        + "  <description>All secrets for env1</description>\n" + "  <configuration>\n"
                        + "   <property>\n" + "      <key>path</key>\n" + "     <value>secret/dev/teamA</value>\n"
                        + "   </property>\n" + "  </configuration>\n" + "  <rules>\n"
                        + "   <deny action=\"*\" type=\"environment\">up42</deny>  \n"
                        + "   <deny action=\"refer\" type=\"*\">up43</deny>  \n" + "  </rules>\n"
                        + " </secretConfig>\n" + "</secretConfigs>",
                124);

        CruiseConfig config = xmlLoader.loadConfigHolder(content).config;

        SecretConfig secretConfig = config.getSecretConfigs().find("example");

        assertThat(secretConfig.getRules().first().action()).isEqualTo("*");
        assertThat(secretConfig.getRules().get(1).type()).isEqualTo("*");
    }

    private void assertConfigProperty(Configuration configuration, String name, String plainTextValue,
            boolean shouldBeEncrypted) {
        assertThat(configuration.getProperty(name).getValue()).isEqualTo(plainTextValue);
        if (shouldBeEncrypted) {
            assertThat(configuration.getProperty(name).getEncryptedValue()).startsWith("AES");

        } else {
            assertThat(configuration.getProperty(name).getEncryptedValue()).isNull();
        }
    }

    private void assertXsdFailureDuringLoad(String configXML, String... expectedMessages) {
        assertFailureDuringLoad(configXML, XsdValidationException.class, expectedMessages);
    }

    private void assertFailureDuringLoad(String configXML, Class<?> expectedExceptionClass,
            String... expectedMessage) {
        try {
            xmlLoader.loadConfigHolder(configXML);
            fail("Should have failed with an exception of type: " + expectedExceptionClass.getSimpleName());
        } catch (Exception e) {
            String message = "\nExpected: " + expectedExceptionClass.getSimpleName() + "\nActual  : "
                    + e.getClass().getSimpleName() + " with message: " + e.getMessage();
            assertThat(e.getClass().equals(expectedExceptionClass)).as(message).isTrue();
            assertThat(StringUtils.containsAny(e.getMessage(), expectedMessage)).isTrue();
        }
    }

    private StageConfig stageWithJobResource(String resourceName) {
        StageConfig stage = StageConfigMother.custom("stage", "job");
        JobConfigs configs = stage.allBuildPlans();
        ResourceConfig resourceConfig = new ResourceConfig();
        resourceConfig.setName(resourceName);
        configs.get(0).resourceConfigs().add(resourceConfig);
        return stage;
    }
}