com.thoughtworks.go.server.service.GoConfigServiceTest.java Source code

Java tutorial

Introduction

Here is the source code for com.thoughtworks.go.server.service.GoConfigServiceTest.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.server.service;

import com.thoughtworks.go.config.*;
import com.thoughtworks.go.config.exceptions.*;
import com.thoughtworks.go.config.materials.MaterialConfigs;
import com.thoughtworks.go.config.materials.PluggableSCMMaterialConfig;
import com.thoughtworks.go.config.materials.dependency.DependencyMaterialConfig;
import com.thoughtworks.go.config.materials.git.GitMaterialConfig;
import com.thoughtworks.go.config.materials.svn.SvnMaterialConfig;
import static com.thoughtworks.go.helper.MaterialConfigsMother.svn;
import com.thoughtworks.go.config.registry.ConfigElementImplementationRegistry;
import com.thoughtworks.go.config.remote.RepoConfigOrigin;
import com.thoughtworks.go.config.rules.Allow;
import com.thoughtworks.go.config.rules.Rules;
import com.thoughtworks.go.config.update.ConfigUpdateResponse;
import com.thoughtworks.go.config.update.FullConfigUpdateCommand;
import com.thoughtworks.go.config.update.UiBasedConfigUpdateCommand;
import com.thoughtworks.go.config.update.UpdateConfigFromUI;
import com.thoughtworks.go.config.validation.GoConfigValidity;
import com.thoughtworks.go.domain.*;
import com.thoughtworks.go.domain.materials.MaterialConfig;
import com.thoughtworks.go.helper.GoConfigMother;
import com.thoughtworks.go.helper.MaterialConfigsMother;
import com.thoughtworks.go.helper.PipelineConfigMother;
import com.thoughtworks.go.helper.StageConfigMother;
import com.thoughtworks.go.listener.BaseUrlChangeListener;
import com.thoughtworks.go.listener.ConfigChangedListener;
import com.thoughtworks.go.security.GoCipher;
import com.thoughtworks.go.server.cache.GoCache;
import com.thoughtworks.go.server.dao.UserDao;
import com.thoughtworks.go.server.domain.PipelineConfigDependencyGraph;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.persistence.PipelineRepository;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.service.ConfigRepository;
import com.thoughtworks.go.util.*;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SystemUtils;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static com.thoughtworks.go.helper.ConfigFileFixture.configWith;
import static com.thoughtworks.go.helper.MaterialConfigsMother.git;
import static java.lang.String.format;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

public class GoConfigServiceTest {

    private GoConfigDao goConfigDao;
    private GoConfigService goConfigService;
    private PipelineRepository pipelineRepository;
    private static final String PIPELINE = "pipeline1";
    private static final String STAGE = "stage1";
    private static final String JOB = "Job1";
    private CruiseConfig cruiseConfig;
    private Clock clock;
    private GoCache goCache;
    private ConfigRepository configRepo;
    private InstanceFactory instanceFactory;
    private SystemEnvironment systemEnvironment;

    @Before
    public void setup() throws Exception {
        new SystemEnvironment().setProperty(SystemEnvironment.ENFORCE_SERVER_IMMUTABILITY, "N");

        configRepo = mock(ConfigRepository.class);
        goConfigDao = mock(GoConfigDao.class);
        pipelineRepository = mock(PipelineRepository.class);
        systemEnvironment = mock(SystemEnvironment.class);

        cruiseConfig = unchangedConfig();
        expectLoad(cruiseConfig);
        this.clock = mock(Clock.class);
        goCache = mock(GoCache.class);
        instanceFactory = mock(InstanceFactory.class);
        UserDao userDao = mock(UserDao.class);

        ConfigElementImplementationRegistry registry = ConfigElementImplementationRegistryMother.withNoPlugins();
        goConfigService = new GoConfigService(goConfigDao, pipelineRepository, this.clock,
                new GoConfigMigration(new TimeProvider(), registry), goCache, configRepo, registry, instanceFactory,
                mock(CachedGoPartials.class), systemEnvironment);
    }

    @Test
    public void shouldUnderstandIfAnEnvironmentVariableIsConfiguredForAPipeline() throws Exception {
        final PipelineConfigs newPipeline = new BasicPipelineConfigs();

        PipelineConfig otherPipeline = createPipelineConfig("pipeline_other", "stage_other", "plan_other");
        otherPipeline.setVariables(GoConfigFileHelper.env("OTHER_PIPELINE_LEVEL", "other pipeline"));
        otherPipeline.first().setVariables(GoConfigFileHelper.env("OTHER_STAGE_LEVEL", "other stage"));
        otherPipeline.first().jobConfigByConfigName(new CaseInsensitiveString("plan_other"))
                .setVariables(GoConfigFileHelper.env("OTHER_JOB_LEVEL", "other job"));

        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        pipelineConfig.setVariables(GoConfigFileHelper.env("PIPELINE_LEVEL", "pipeline value"));
        StageConfig stageConfig = pipelineConfig.first();
        stageConfig.setVariables(GoConfigFileHelper.env("STAGE_LEVEL", "stage value"));
        stageConfig.jobConfigByConfigName(new CaseInsensitiveString("plan"))
                .setVariables(GoConfigFileHelper.env("JOB_LEVEL", "job value"));

        newPipeline.add(pipelineConfig);
        newPipeline.add(otherPipeline);

        CruiseConfig cruiseConfig = new BasicCruiseConfig(newPipeline);
        EnvironmentConfig environmentConfig = cruiseConfig.addEnvironment("uat");
        environmentConfig.addPipeline(new CaseInsensitiveString("pipeline"));
        environmentConfig.addEnvironmentVariable("ENV_LEVEL", "env value");

        expectLoad(cruiseConfig);

        assertThat(goConfigService.hasVariableInScope("pipeline", "NOT_IN_SCOPE"), is(false));
        assertThat(goConfigService.hasVariableInScope("pipeline", "ENV_LEVEL"), is(true));
        assertThat(goConfigService.hasVariableInScope("pipeline", "PIPELINE_LEVEL"), is(true));
        assertThat(goConfigService.hasVariableInScope("pipeline", "STAGE_LEVEL"), is(true));
        assertThat(goConfigService.hasVariableInScope("pipeline", "JOB_LEVEL"), is(true));
        assertThat(goConfigService.hasVariableInScope("pipeline", "OTHER_PIPELINE_LEVEL"), is(false));
        assertThat(goConfigService.hasVariableInScope("pipeline", "OTHER_STAGE_LEVEL"), is(false));
        assertThat(goConfigService.hasVariableInScope("pipeline", "OTHER_JOB_LEVEL"), is(false));

        assertThat(goConfigService.hasVariableInScope("pipeline_other", "ENV_LEVEL"), is(false));
        assertThat(goConfigService.hasVariableInScope("pipeline_other", "OTHER_PIPELINE_LEVEL"), is(true));
        assertThat(goConfigService.hasVariableInScope("pipeline_other", "OTHER_STAGE_LEVEL"), is(true));
        assertThat(goConfigService.hasVariableInScope("pipeline_other", "OTHER_JOB_LEVEL"), is(true));
        assertThat(goConfigService.hasVariableInScope("pipeline_other", "NOT_IN_SCOPE"), is(false));
    }

    @Test
    public void shouldUnderstandIfAStageHasFetchMaterialsConfigured() throws Exception {
        PipelineConfig pipeline = createPipelineConfig("cruise", "dev", "test");
        StageConfig stage = pipeline.first();
        stage.setFetchMaterials(false);

        CruiseConfig cruiseConfig = new BasicCruiseConfig(new BasicPipelineConfigs(pipeline));
        expectLoad(cruiseConfig);

        assertThat(goConfigService.shouldFetchMaterials("cruise", "dev"), is(false));
    }

    private void expectLoad(final CruiseConfig result) throws Exception {
        when(goConfigDao.load()).thenReturn(result);
    }

    private void expectLoadForEditing(final CruiseConfig result) throws Exception {
        when(goConfigDao.loadForEditing()).thenReturn(result);
    }

    private CruiseConfig unchangedConfig() {
        return configWith(createPipelineConfig(PIPELINE, STAGE, JOB));
    }

    @Test
    public void shouldGetAllStagesWithOne() throws Exception {
        final PipelineConfigs newPipeline = new BasicPipelineConfigs();
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        newPipeline.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(newPipeline));
        assertThat(goConfigService.stageConfigNamed("pipeline", "name"),
                is(pipelineConfig.findBy(new CaseInsensitiveString("name"))));
    }

    @Test
    public void shouldTellIfAnUSerIsGroupAdministrator() throws Exception {
        final PipelineConfigs newPipeline = new BasicPipelineConfigs();
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        newPipeline.add(pipelineConfig);
        newPipeline.setAuthorization(
                new Authorization(new AdminsConfig(new AdminUser(new CaseInsensitiveString("dawg")))));
        expectLoad(new BasicCruiseConfig(newPipeline));
        final Username dawg = new Username(new CaseInsensitiveString("dawg"));
        assertThat(goConfigService.isGroupAdministrator(dawg.getUsername()), is(true));
    }

    @Test
    public void shouldTellIfAnEnvironmentExists() throws Exception {
        BasicEnvironmentConfig first = new BasicEnvironmentConfig(new CaseInsensitiveString("first"));
        BasicEnvironmentConfig second = new BasicEnvironmentConfig(new CaseInsensitiveString("second"));
        CruiseConfig config = new BasicCruiseConfig();
        config.addEnvironment(first);
        config.addEnvironment(second);
        expectLoad(config);

        assertThat(goConfigService.hasEnvironmentNamed(new CaseInsensitiveString("first")), is(true));
        assertThat(goConfigService.hasEnvironmentNamed(new CaseInsensitiveString("second")), is(true));
        assertThat(goConfigService.hasEnvironmentNamed(new CaseInsensitiveString("SECOND")), is(true));
        assertThat(goConfigService.hasEnvironmentNamed(new CaseInsensitiveString("fourth")), is(false));
    }

    @Test
    public void shouldTellIfOnlyKnownUsersAreAllowedToLogin() throws Exception {
        CruiseConfig config = new BasicCruiseConfig();
        config.server().security().setAllowOnlyKnownUsersToLogin(true);
        expectLoad(config);

        assertThat(goConfigService.isOnlyKnownUserAllowedToLogin(), is(true));
    }

    @Test
    public void shouldTellIfAnAgentExists() throws Exception {
        CruiseConfig config = new BasicCruiseConfig();
        config.agents().add(new AgentConfig("uuid"));
        expectLoad(config);

        assertThat(goConfigService.hasAgent("uuid"), is(true));
        assertThat(goConfigService.hasAgent("doesnt-exist"), is(false));
    }

    @Test
    public void shouldReturnTrueIfStageHasTestsAndFalseIfItDoesnt() throws Exception {
        PipelineConfigs newPipelines = new BasicPipelineConfigs();
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        pipelineConfig.add(StageConfigMother.stageConfigWithArtifact("stage1", "job1", ArtifactType.test));
        pipelineConfig.add(StageConfigMother.stageConfigWithArtifact("stage2", "job2", ArtifactType.build));
        newPipelines.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(newPipelines));
        assertThat(goConfigService.stageHasTests("pipeline", "stage1"), is(true));
        assertThat(goConfigService.stageHasTests("pipeline", "stage2"), is(false));
    }

    @Test
    public void shouldGetCommentRenderer() throws Exception {
        PipelineConfigs newPipeline = new BasicPipelineConfigs();
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        pipelineConfig.setTrackingTool(new TrackingTool("link", "regex"));
        newPipeline.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(newPipeline));
        assertEquals(goConfigService.getCommentRendererFor("pipeline"), new TrackingTool("link", "regex"));

        pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        pipelineConfig.setMingleConfig(new MingleConfig("baseUrl", "projIdentifier", "mql"));
        newPipeline = new BasicPipelineConfigs();
        newPipeline.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(newPipeline));
        assertEquals(goConfigService.getCommentRendererFor("pipeline"),
                new MingleConfig("baseUrl", "projIdentifier", "mql"));

        pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        newPipeline = new BasicPipelineConfigs();
        newPipeline.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(newPipeline));
        assertEquals(goConfigService.getCommentRendererFor("pipeline"), new TrackingTool());
    }

    @Test
    public void shouldUnderstandIfAPipelineIsLockable() throws Exception {
        PipelineConfigs group = new BasicPipelineConfigs();
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        group.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(group));
        assertThat(goConfigService.isLockable("pipeline"), is(false));

        pipelineConfig.lockExplicitly();
        expectLoad(new BasicCruiseConfig(group));
        assertThat(goConfigService.isLockable("pipeline"), is(true));
    }

    @Test
    public void shouldRememberValidityWhenCruiseConfigLoaderHasInvalidConfigFile() throws Exception {
        GoConfigService service = goConfigServiceWithInvalidStatus();
        assertThat(service.checkConfigFileValid().isValid(), is(false));
        assertThat(((GoConfigValidity.InvalidGoConfig) service.checkConfigFileValid()).errorMessage(),
                is("JDom exception"));
    }

    @Test
    public void shouldNotHaveErrorMessageWhenConfigFileValid() {
        when(goConfigDao.checkConfigFileValid()).thenReturn(GoConfigValidity.valid());
        GoConfigValidity configValidity = goConfigService.checkConfigFileValid();
        assertThat(configValidity.isValid(), is(true));
    }

    private CruiseConfig configWithPipeline() {
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "stage", "first");
        pipelineConfig.addMaterialConfig(MaterialConfigsMother.hgMaterialConfig());
        CruiseConfig config = configWith(pipelineConfig);
        config.server().setArtifactsDir("/var/logs");
        return config;
    }

    @Test
    public void shouldReturnInvalidWhenWholeConfigIsInvalidAndShouldUpgrade() throws Exception {
        CruiseConfig config = configWithPipeline();
        when(goConfigDao.loadForEditing()).thenReturn(config);
        String configContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                + "<cruise xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" schemaVersion=\"14\">\n"
                + "<server artifactsdir='artifactsDir'/><unknown/></cruise>";
        GoConfigValidity validity = goConfigService.fileSaver(true).saveXml(configContent, "md5");
        assertThat(((GoConfigValidity.InvalidGoConfig) validity).errorMessage(),
                is("Cruise config file with version 14 is invalid. Unable to upgrade."));
    }

    @Test
    public void shouldReturnInvalidWhenWholeConfigIsInvalid() throws Exception {
        CruiseConfig config = configWithPipeline();
        when(goConfigDao.loadForEditing()).thenReturn(config);
        String configContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                + "<cruise xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" schemaVersion=\""
                + GoConstants.CONFIG_SCHEMA_VERSION + "\">\n"
                + "<server artifactsdir='artifactsDir'/><unknown/></cruise>";
        GoConfigValidity validity = goConfigService.fileSaver(false).saveXml(configContent, "md5");
        assertThat(((GoConfigValidity.InvalidGoConfig) validity).errorMessage(),
                containsString("Invalid content was found starting with element 'unknown'"));
    }

    @Test
    public void shouldReturnvariablesForAPipeline() {
        EnvironmentConfig env = cruiseConfig.addEnvironment("environment");
        env.addEnvironmentVariable("foo", "env-fooValue");
        env.addEnvironmentVariable("bar", "env-barValue");
        env.addPipeline(new CaseInsensitiveString(PIPELINE));
        PipelineConfig pipeline = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString(PIPELINE));
        pipeline.addEnvironmentVariable("foo", "pipeline-fooValue");
        pipeline.addEnvironmentVariable("blah", "pipeline-blahValue");

        EnvironmentVariablesConfig variables = goConfigService.variablesFor(PIPELINE);
        assertThat(variables.size(), is(3));
        assertThat(variables,
                hasItems(new EnvironmentVariableConfig("foo", "pipeline-fooValue"),
                        new EnvironmentVariableConfig("bar", "env-barValue"),
                        new EnvironmentVariableConfig("blah", "pipeline-blahValue")));
    }

    @Test
    public void shouldReturnvariablesForAPipelineNotInAnEnvironment() {
        PipelineConfig pipeline = cruiseConfig.pipelineConfigByName(new CaseInsensitiveString(PIPELINE));
        pipeline.addEnvironmentVariable("foo", "pipeline-fooValue");
        pipeline.addEnvironmentVariable("blah", "pipeline-blahValue");
        EnvironmentVariablesConfig variables = goConfigService.variablesFor(PIPELINE);
        assertThat(variables.size(), is(2));
        assertThat(variables, hasItems(new EnvironmentVariableConfig("foo", "pipeline-fooValue"),
                new EnvironmentVariableConfig("blah", "pipeline-blahValue")));
    }

    private PipelineConfig pipelineWithTemplate() {
        PipelineConfig pipeline = PipelineConfigMother.pipelineConfig("pipeline");
        pipeline.clear();
        pipeline.setTemplateName(new CaseInsensitiveString("foo"));
        PipelineTemplateConfig template = new PipelineTemplateConfig(new CaseInsensitiveString("foo"),
                StageConfigMother.custom("stage", "job"));
        pipeline.usingTemplate(template);
        return pipeline;
    }

    @Test
    public void shouldNotThrowExceptionWhenUpgradeFailsForConfigFileUpdate() throws Exception {
        expectLoadForEditing(configWith(createPipelineConfig("pipeline", "stage", "build")));
        GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(true);
        GoConfigValidity validity = saver.saveXml("some_junk", "junk_md5");
        assertThat(validity.isValid(), is(false));
        assertThat(((GoConfigValidity.InvalidGoConfig) validity).errorMessage(),
                is("Error on line 1: Content is not allowed in prolog."));
    }

    @Test
    public void shouldProvideDetailsWhenXmlConfigDomIsInvalid() throws Exception {
        expectLoadForEditing(configWith(createPipelineConfig("pipeline", "stage", "build")));
        GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false);
        String configContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
                + "<cruise xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\" schemaVersion=\""
                + GoConstants.CONFIG_SCHEMA_VERSION + "\">\n" + "<server artifactsdir='artifactsDir></cruise>";
        GoConfigValidity validity = saver.saveXml(configContent, "junk_md5");
        assertThat(validity.isValid(), is(false));
        assertThat(((GoConfigValidity.InvalidGoConfig) validity).errorMessage(), is(
                "Invalid Configuration - Error on line 3: The value of attribute \"artifactsdir\" associated with an element type \"server\" must not contain the '<' character."));
    }

    @Test
    public void xmlPartialSaverShouldReturnTheRightXMLThroughAsXml() throws Exception {
        expectLoadForEditing(new GoConfigMother().defaultCruiseConfig());
        GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(true);
        assertThat(saver.asXml(),
                containsString(String.format("schemaVersion=\"%s\"", GoConstants.CONFIG_SCHEMA_VERSION)));
        assertThat(saver.asXml(), containsString("xsi:noNamespaceSchemaLocation=\"cruise-config.xsd\""));
    }

    @Test
    public void shouldRegisterListenerWithTheConfigDAO() throws Exception {
        final ConfigChangedListener listener = mock(ConfigChangedListener.class);
        goConfigService.register(listener);
        verify(goConfigDao).registerListener(listener);
    }

    private CruiseConfig configWithAgents(AgentConfig... agentConfigs) {
        CruiseConfig cruiseConfig = unchangedConfig();
        cruiseConfig.agents().addAll(Arrays.asList(agentConfigs));
        return cruiseConfig;
    }

    @Test
    public void shouldFixJobNameCase() throws Exception {
        expectLoad(unchangedConfig());
        JobConfigIdentifier translated = goConfigService.translateToActualCase(
                new JobConfigIdentifier(PIPELINE.toUpperCase(), STAGE.toUpperCase(), JOB.toUpperCase()));
        assertThat(translated, is(new JobConfigIdentifier(PIPELINE, STAGE, JOB)));
    }

    @Test
    public void shouldNotLoseUUIDWhenRunOnAllAgents() throws Exception {
        expectLoad(unchangedConfigWithRunOnAllAgents());
        JobConfigIdentifier translated = goConfigService
                .translateToActualCase(new JobConfigIdentifier(PIPELINE.toUpperCase(), STAGE.toUpperCase(),
                        RunOnAllAgents.CounterBasedJobNameGenerator.appendMarker(JOB.toUpperCase(), 2)));
        assertThat(translated, is(new JobConfigIdentifier(PIPELINE, STAGE,
                RunOnAllAgents.CounterBasedJobNameGenerator.appendMarker(JOB, 2))));
    }

    @Test
    public void shouldNotBeInstanceOfWhenRunOnAllAgentsWithMissingAgent() throws Exception {
        expectLoad(unchangedConfigWithRunOnAllAgents());
        String missingJobName = JOB + "-missing";
        try {
            goConfigService.translateToActualCase(new JobConfigIdentifier(PIPELINE, STAGE, missingJobName));
            fail("Should not be able to find job with missing agent");
        } catch (RecordNotFoundException expected) {
            assertThat(expected.getMessage(),
                    is(format("Job '%s' not found in pipeline '%s' stage '%s'", missingJobName, PIPELINE, STAGE)));
        }
    }

    private CruiseConfig unchangedConfigWithRunOnAllAgents() {
        PipelineConfig pipelineConfig = createPipelineConfig(PIPELINE, STAGE, JOB);
        pipelineConfig.get(0).jobConfigByConfigName(new CaseInsensitiveString(JOB)).setRunOnAllAgents(true);
        return configWith(pipelineConfig);
    }

    @Test
    public void shouldThrowJobNotFoundExceptionWhenJobDoesNotExist() throws Exception {
        expectLoad(unchangedConfig());
        try {
            goConfigService.translateToActualCase(new JobConfigIdentifier(PIPELINE, STAGE, "invalid-job"));
            fail("should throw exception if job does not exist");
        } catch (Exception e) {
            assertThat(e, instanceOf(RecordNotFoundException.class));
            assertThat(e.getMessage(), containsString("invalid-job"));
        }
    }

    @Test
    public void shouldThrowStageNotFoundExceptionWhenStageDoesNotExist() throws Exception {
        expectLoad(unchangedConfig());
        try {
            goConfigService.translateToActualCase(new JobConfigIdentifier(PIPELINE, "invalid-stage", JOB));
            fail("should throw exception if stage does not exist");
        } catch (Exception e) {
            assertThat(e, instanceOf(StageNotFoundException.class));
            assertThat(e.getMessage(), containsString("invalid-stage"));
        }
    }

    @Test
    public void shouldThrowRecordNotFoundExceptionWhenStageDoesNotExist() throws Exception {
        expectLoad(unchangedConfig());
        try {
            goConfigService.translateToActualCase(new JobConfigIdentifier("invalid-pipeline", STAGE, JOB));
            fail("should throw exception if pipeline does not exist");
        } catch (Exception e) {
            assertThat(e, instanceOf(RecordNotFoundException.class));
            assertThat(e.getMessage(), containsString("invalid-pipeline"));
        }
    }

    @Test
    public void shouldThrowIfCruiseHasNoReadPermissionOnArtifactsDir() throws Exception {
        if (SystemUtils.IS_OS_WINDOWS) {
            return;
        }

        File artifactsDir = FileUtil.createTempFolder();
        artifactsDir.setReadable(false, false);
        cruiseConfig.setServerConfig(new ServerConfig(artifactsDir.getAbsolutePath(), new SecurityConfig()));
        expectLoad(cruiseConfig);

        try {
            goConfigService.initialize();
            fail("should throw when cruise has no read permission on artifacts dir "
                    + artifactsDir.getAbsolutePath());
        } catch (Exception e) {
            assertThat(e.getMessage(),
                    is("Cruise does not have read permission on " + artifactsDir.getAbsolutePath()));
        } finally {
            FileUtils.deleteQuietly(artifactsDir);
        }

    }

    @Test
    public void shouldThrowIfCruiseHasNoWritePermissionOnArtifactsDir() throws Exception {
        if (SystemUtils.IS_OS_WINDOWS) {
            return;
        }
        File artifactsDir = FileUtil.createTempFolder();
        artifactsDir.setWritable(false, false);
        cruiseConfig.setServerConfig(new ServerConfig(artifactsDir.getAbsolutePath(), new SecurityConfig()));
        expectLoad(cruiseConfig);

        try {
            goConfigService.initialize();
            fail("should throw when cruise has no write permission on artifacts dir "
                    + artifactsDir.getAbsolutePath());
        } catch (Exception e) {
            assertThat(e.getMessage(),
                    is("Cruise does not have write permission on " + artifactsDir.getAbsolutePath()));
        } finally {
            FileUtils.deleteQuietly(artifactsDir);
        }

    }

    @Test
    public void shouldFindMaterialByPipelineUniqueFingerprint() throws Exception {
        SvnMaterialConfig svnMaterialConfig = svn("repo", null, null, false);
        svnMaterialConfig.setName(new CaseInsensitiveString("foo"));
        cruiseConfig = configWith(GoConfigMother.createPipelineConfigWithMaterialConfig(svnMaterialConfig));
        when(goConfigDao.load()).thenReturn(cruiseConfig);

        assertThat(goConfigService.findMaterial(new CaseInsensitiveString("pipeline"),
                svnMaterialConfig.getPipelineUniqueFingerprint()), is(svnMaterialConfig));
        assertThat(goConfigService.findMaterial(new CaseInsensitiveString("piPelIne"),
                svnMaterialConfig.getPipelineUniqueFingerprint()), is(svnMaterialConfig));
    }

    @Test
    public void shouldReturnNullIfNoMaterialMatches() throws Exception {
        DependencyMaterialConfig dependencyMaterialConfig = new DependencyMaterialConfig(
                new CaseInsensitiveString("upstream-pipeline"), new CaseInsensitiveString("upstream-stage"));
        cruiseConfig = configWith(GoConfigMother.createPipelineConfigWithMaterialConfig(dependencyMaterialConfig));
        when(goConfigDao.load()).thenReturn(cruiseConfig);

        assertThat(goConfigService.findMaterial(new CaseInsensitiveString("pipeline"), "missing"), is(nullValue()));
    }

    @Test
    public void shouldFindMaterialConfigBasedOnFingerprint() throws Exception {
        SvnMaterialConfig expected = svn("repo", null, null, false);
        cruiseConfig = configWith(GoConfigMother.createPipelineConfigWithMaterialConfig(expected));
        when(goConfigDao.load()).thenReturn(cruiseConfig);

        MaterialConfig actual = goConfigService.materialForPipelineWithFingerprint("pipeline",
                expected.getFingerprint());
        assertThat(actual, is(expected));
    }

    @Test
    public void shouldThrowExceptionWhenUnableToFindMaterialBasedOnFingerprint() throws Exception {
        SvnMaterialConfig svnMaterialConfig = svn("repo", null, null, false);
        cruiseConfig = configWith(GoConfigMother.createPipelineConfigWithMaterialConfig(svnMaterialConfig));
        when(goConfigDao.load()).thenReturn(cruiseConfig);

        try {
            goConfigService.materialForPipelineWithFingerprint("pipeline", "bad-fingerprint");
            fail("Shouldn't be able to find material with incorrect fingerprint");
        } catch (Exception expected) {
            assertThat(expected.getMessage(),
                    is("Pipeline [pipeline] does not have a material with fingerprint [bad-fingerprint]"));
        }
    }

    @Test
    public void shouldReturnDependentPiplinesForAGivenPipeline() throws Exception {
        PipelineConfig up = createPipelineConfig("blahPipeline", "blahStage");
        up.addMaterialConfig(MaterialConfigsMother.hgMaterialConfig());
        PipelineConfig down1 = GoConfigMother.createPipelineConfigWithMaterialConfig("down1",
                new DependencyMaterialConfig(new CaseInsensitiveString("blahPipeline"),
                        new CaseInsensitiveString("blahStage")));
        PipelineConfig down2 = GoConfigMother.createPipelineConfigWithMaterialConfig("down2",
                new DependencyMaterialConfig(new CaseInsensitiveString("blahPipeline"),
                        new CaseInsensitiveString("blahStage")));
        when(goConfigDao.load())
                .thenReturn(configWith(up, down1, down2,
                        GoConfigMother.createPipelineConfigWithMaterialConfig("otherPipeline",
                                new DependencyMaterialConfig(new CaseInsensitiveString("someotherpipeline"),
                                        new CaseInsensitiveString("blahStage")))));

        assertThat(goConfigService.downstreamPipelinesOf("blahPipeline"), is(Arrays.asList(down1, down2)));
    }

    @Test
    public void shouldReturnUpstreamDependencyGraphForAGivenPipeline() throws Exception {
        PipelineConfig current = GoConfigMother.createPipelineConfigWithMaterialConfig("current",
                new DependencyMaterialConfig(new CaseInsensitiveString("up1"), new CaseInsensitiveString("first")),
                new DependencyMaterialConfig(new CaseInsensitiveString("up2"), new CaseInsensitiveString("first")));
        PipelineConfig up1 = GoConfigMother.createPipelineConfigWithMaterialConfig("up1",
                new DependencyMaterialConfig(new CaseInsensitiveString("uppest"),
                        new CaseInsensitiveString("first")));
        PipelineConfig up2 = GoConfigMother.createPipelineConfigWithMaterialConfig("up2",
                new DependencyMaterialConfig(new CaseInsensitiveString("uppest"),
                        new CaseInsensitiveString("first")));
        PipelineConfig uppest = GoConfigMother.createPipelineConfigWithMaterialConfig("uppest",
                MaterialConfigsMother.hgMaterialConfig());
        when(goConfigDao.load()).thenReturn(configWith(current, up1, up2, uppest));
        assertThat(goConfigService.upstreamDependencyGraphOf("current"),
                is(new PipelineConfigDependencyGraph(current,
                        new PipelineConfigDependencyGraph(up1, new PipelineConfigDependencyGraph(uppest)),
                        new PipelineConfigDependencyGraph(up2, new PipelineConfigDependencyGraph(uppest)))));
        /*
                     uppest
                    /     \
                   up1   up2
                    \     /
                    current
        */
    }

    @Test
    public void shouldDetermineIfStageExistsInCurrentConfig() throws Exception {
        PipelineConfigs pipelineConfigs = new BasicPipelineConfigs();
        pipelineConfigs.add(createPipelineConfig("pipeline", "stage", "job"));
        expectLoad(new BasicCruiseConfig(pipelineConfigs));
        assertThat(goConfigService.stageExists("pipeline", "randomstage"), is(false));
        assertThat(goConfigService.stageExists("pipeline", "stage"), is(true));
    }

    @Test
    public void shouldRegisterBaseUrlChangeListener() throws Exception {
        CruiseConfig cruiseConfig = new GoConfigMother().cruiseConfigWithOnePipelineGroup();
        when(goConfigDao.load()).thenReturn(cruiseConfig);
        goConfigService.initialize();
        verify(goConfigDao).registerListener(any(BaseUrlChangeListener.class));
    }

    @Test
    public void getConfigAtVersion_shouldFetchRequiredVersion() throws Exception {
        GoConfigRevision revision = new GoConfigRevision("v1", "md5-1", "loser", "100.3.9.1", new TimeProvider());
        when(configRepo.getRevision("md5-1")).thenReturn(revision);
        assertThat(goConfigService.getConfigAtVersion("md5-1"), is(revision));
    }

    @Test
    public void getNotThrowUpWhenRevisionIsNotFound() throws Exception {
        when(configRepo.getRevision("md5-1")).thenThrow(new IllegalArgumentException("did not find the revision"));
        try {
            assertThat(goConfigService.getConfigAtVersion("md5-1"), is(nullValue()));
        } catch (Exception e) {
            fail("should not have thrown up");
        }

    }

    @Test
    public void shouldReturnListOfUpstreamPipelineConfigValidForFetchArtifact() {
        PipelineConfig unrelatedPipeline = PipelineConfigMother.pipelineConfig("some.random.pipeline");
        PipelineConfig upstream = PipelineConfigMother.createPipelineConfig("upstream", "upstream.stage",
                "upstream.job");
        upstream.add(StageConfigMother.stageConfig("upstream.stage.2"));
        upstream.add(StageConfigMother.stageConfig("upstream.stage.3"));
        PipelineConfig downstream = PipelineConfigMother.createPipelineConfig("pipeline", "stage.1", "jobs");
        downstream.add(StageConfigMother.stageConfig("stage.2"));
        downstream.add(StageConfigMother.stageConfig("current.stage"));

        downstream.addMaterialConfig(new DependencyMaterialConfig(new CaseInsensitiveString("upstream"),
                new CaseInsensitiveString("upstream.stage.2")));

        CruiseConfig cruiseConfig = configWith(upstream, downstream, unrelatedPipeline);
        when(goConfigDao.load()).thenReturn(cruiseConfig);

        List<PipelineConfig> fetchablePipelines = goConfigService.pipelinesForFetchArtifacts("pipeline");

        assertThat(fetchablePipelines.size(), is(2));
        assertThat(fetchablePipelines, hasItem(upstream));
        assertThat(fetchablePipelines, hasItem(downstream));
    }

    @Test
    public void uiBasedUpdateCommandShouldReturnTheConfigPassedByUpdateOperation() {
        UiBasedConfigUpdateCommand command = new UiBasedConfigUpdateCommand("md5", null, null, null) {
            public boolean canContinue(CruiseConfig cruiseConfig) {
                return true;
            }
        };
        CruiseConfig after = new BasicCruiseConfig();
        command.afterUpdate(after);
        assertThat(command.configAfter(), sameInstance(after));
    }

    @Test
    public void shouldUseInstanceFactoryToCreateAStageInstanceForTheSpecifiedPipelineStageCombination()
            throws Exception {
        PipelineConfig pipelineConfig = PipelineConfigMother.createPipelineConfig("foo-pipeline", "foo-stage",
                "foo-job");
        DefaultSchedulingContext schedulingContext = new DefaultSchedulingContext("loser");
        String md5 = "foo-md5";

        CruiseConfig config = mock(BasicCruiseConfig.class);
        when(config.pipelineConfigByName(new CaseInsensitiveString("foo-pipeline"))).thenReturn(pipelineConfig);
        when(config.getMd5()).thenReturn(md5);
        when(goConfigDao.load()).thenReturn(config);

        goConfigService.scheduleStage("foo-pipeline", "foo-stage", schedulingContext);

        verify(instanceFactory).createStageInstance(pipelineConfig, new CaseInsensitiveString("foo-stage"),
                schedulingContext, md5, clock);
    }

    @Test
    public void shouldReturnFalseIfMD5DoesNotMatch() {
        String staleMd5 = "oldmd5";
        when(goConfigDao.md5OfConfigFile()).thenReturn("newmd5");
        assertThat(goConfigService.doesMd5Match(staleMd5), is(false));
    }

    @Test
    public void shouldReturnTrueifMd5Matches() {
        String staleMd5 = "md5";
        when(goConfigDao.md5OfConfigFile()).thenReturn("md5");
        assertThat(goConfigService.doesMd5Match(staleMd5), is(true));
    }

    @Test
    public void shouldThrowExceptionIfGroupDoesNotExist_WhenUserIsAdmin() {
        CaseInsensitiveString adminName = new CaseInsensitiveString("admin");
        GoConfigMother mother = new GoConfigMother();
        mother.enableSecurityWithPasswordFilePlugin(cruiseConfig);
        cruiseConfig.server().security().adminsConfig().add(new AdminUser(adminName));
        String groupName = String.format("group_%s", UUID.randomUUID());
        try {
            goConfigService.isUserAdminOfGroup(adminName, groupName);
            fail("Should fail since group does not exist");
        } catch (Exception e) {
            assertThat(e, is(instanceOf(RecordNotFoundException.class)));
        }
    }

    @Test
    public void shouldThrowExceptionIfGroupDoesNotExist_WhenUserIsNonAdmin() {
        CaseInsensitiveString adminName = new CaseInsensitiveString("admin");
        String groupName = String.format("group_%s", UUID.randomUUID());
        GoConfigMother mother = new GoConfigMother();
        mother.enableSecurityWithPasswordFilePlugin(cruiseConfig);
        cruiseConfig.server().security().adminsConfig().add(new AdminUser(adminName));
        try {
            goConfigService.isUserAdminOfGroup(new CaseInsensitiveString("foo"), groupName);
            fail("Should fail since group does not exist");
        } catch (Exception e) {
            assertThat(e, is(instanceOf(RecordNotFoundException.class)));
        }
    }

    @Test
    public void shouldReturnTrueIfUserIsTheAdminForGroup() {
        CaseInsensitiveString adminName = new CaseInsensitiveString("admin");
        String groupName = String.format("group_%s", UUID.randomUUID());
        GoConfigMother mother = new GoConfigMother();
        mother.enableSecurityWithPasswordFilePlugin(cruiseConfig);
        cruiseConfig.server().security().adminsConfig().add(new AdminUser(adminName));
        mother.addPipelineWithGroup(cruiseConfig, groupName, "pipeline", "stage");
        mother.addAdminUserForPipelineGroup(cruiseConfig, "user", groupName);
        assertThat(goConfigService.isUserAdminOfGroup(new CaseInsensitiveString("user"), groupName), is(true));
    }

    @Test
    public void shouldReturnValidOnUpdateXml() throws Exception {
        String groupName = "group_name";
        String md5 = "md5";
        cruiseConfig = new BasicCruiseConfig();
        expectLoad(cruiseConfig);
        new GoConfigMother().addPipelineWithGroup(cruiseConfig, groupName, "pipeline_name", "stage_name",
                "job_name");
        expectLoadForEditing(cruiseConfig);
        when(goConfigDao.md5OfConfigFile()).thenReturn(md5);

        GoConfigService.XmlPartialSaver partialSaver = goConfigService.groupSaver(groupName);
        String renamedGroupName = "renamed_group_name";
        GoConfigValidity validity = partialSaver.saveXml(groupXml(renamedGroupName), md5);
        assertThat(validity.isValid(), Matchers.is(true));

        ArgumentCaptor<FullConfigUpdateCommand> commandArgCaptor = ArgumentCaptor
                .forClass(FullConfigUpdateCommand.class);
        verify(goConfigDao).updateFullConfig(commandArgCaptor.capture());

        FullConfigUpdateCommand command = commandArgCaptor.getValue();
        CruiseConfig updatedConfig = command.configForEdit();
        PipelineConfigs group = updatedConfig.findGroup(renamedGroupName);
        PipelineConfig pipeline = group.findBy(new CaseInsensitiveString("new_name"));
        assertThat(pipeline.name(), is(new CaseInsensitiveString("new_name")));
        assertThat(pipeline.getLabelTemplate(), is("${COUNT}-#{foo}"));
        assertThat(pipeline.materialConfigs().first(), is(instanceOf(SvnMaterialConfig.class)));
        assertThat(pipeline.materialConfigs().first().getUriForDisplay(), is("file:///tmp/foo"));
    }

    @Test
    public void shouldIgnoreXmlEntitiesAndReplaceThemWithEmptyString_DuringPipelineGroupPartialSave()
            throws Exception {
        ArgumentCaptor<FullConfigUpdateCommand> commandArgumentCaptor = ArgumentCaptor
                .forClass(FullConfigUpdateCommand.class);
        File targetFile = TempFiles.createUniqueFile("somefile");
        FileUtils.writeStringToFile(targetFile, "CONTENTS_OF_FILE", StandardCharsets.UTF_8);

        cruiseConfig = new BasicCruiseConfig();
        expectLoad(cruiseConfig);
        new GoConfigMother().addPipelineWithGroup(cruiseConfig, "group_name", "pipeline1", "stage_name",
                "job_name");
        expectLoadForEditing(cruiseConfig);
        when(goConfigDao.md5OfConfigFile()).thenReturn("md5");
        when(goConfigDao.updateFullConfig(commandArgumentCaptor.capture())).thenReturn(null);

        GoConfigService.XmlPartialSaver partialSaver = goConfigService.groupSaver("group_name");
        GoConfigValidity validity = partialSaver.saveXml(groupXmlWithEntity(targetFile.getAbsolutePath()), "md5");

        PipelineConfigs group = commandArgumentCaptor.getValue().configForEdit().findGroup("group_name");
        PipelineConfig pipeline = group.findBy(new CaseInsensitiveString("pipeline1"));
        assertThat(validity.isValid(), Matchers.is(true));

        String entityValue = pipeline.getParams().getParamNamed("foo").getValue();
        assertThat(entityValue, not(containsString("CONTENTS_OF_FILE")));
        assertThat(entityValue, isEmptyString());
    }

    @Test
    public void shouldUpdateXmlUsingNewFlowIfEnabled() throws Exception {
        String groupName = "group_name";
        String md5 = "md5";
        cruiseConfig = new BasicCruiseConfig();
        ArgumentCaptor<FullConfigUpdateCommand> commandArgumentCaptor = ArgumentCaptor
                .forClass(FullConfigUpdateCommand.class);

        expectLoad(cruiseConfig);
        new GoConfigMother().addPipelineWithGroup(cruiseConfig, groupName, "pipeline_name", "stage_name",
                "job_name");
        expectLoadForEditing(cruiseConfig);
        when(goConfigDao.md5OfConfigFile()).thenReturn(md5);
        when(goConfigDao.updateFullConfig(commandArgumentCaptor.capture())).thenReturn(null);

        GoConfigService.XmlPartialSaver partialSaver = goConfigService.groupSaver(groupName);
        String renamedGroupName = "renamed_group_name";

        GoConfigValidity validity = partialSaver.saveXml(groupXml(renamedGroupName), md5);

        assertThat(validity.isValid(), Matchers.is(true));
        CruiseConfig updatedConfig = commandArgumentCaptor.getValue().configForEdit();

        PipelineConfigs group = updatedConfig.findGroup(renamedGroupName);
        PipelineConfig pipeline = group.findBy(new CaseInsensitiveString("new_name"));
        assertThat(pipeline.name(), is(new CaseInsensitiveString("new_name")));
        assertThat(pipeline.getLabelTemplate(), is("${COUNT}-#{foo}"));
        assertThat(pipeline.materialConfigs().first(), is(instanceOf(SvnMaterialConfig.class)));
        assertThat(pipeline.materialConfigs().first().getUriForDisplay(), is("file:///tmp/foo"));
    }

    @Test
    public void shouldReturnInvalidWhenPipelineGroupPartialIsInvalid() throws Exception {
        String groupName = "group_name";
        String md5 = "md5";
        cruiseConfig = new BasicCruiseConfig();
        expectLoad(cruiseConfig);
        new GoConfigMother().addPipelineWithGroup(cruiseConfig, groupName, "pipeline_name", "stage_name",
                "job_name");
        expectLoadForEditing(cruiseConfig);
        when(goConfigDao.md5OfConfigFile()).thenReturn(md5);

        String pipelineGroupContent = groupXmlWithInvalidElement(groupName);
        GoConfigValidity validity = goConfigService.groupSaver(groupName).saveXml(pipelineGroupContent, "md5");
        assertThat(validity.isValid(), Matchers.is(false));
        assertThat(((GoConfigValidity.InvalidGoConfig) validity).errorMessage(),
                containsString("Invalid content was found starting with element 'unknown'"));
        verify(goConfigDao, never()).updateConfig(any(UpdateConfigCommand.class));
    }

    @Test
    public void shouldReturnInvalidWhenPipelineGroupPartialHasInvalidAttributeValue() throws Exception {
        String groupName = "group_name";
        String md5 = "md5";
        cruiseConfig = new BasicCruiseConfig();
        expectLoad(cruiseConfig);
        new GoConfigMother().addPipelineWithGroup(cruiseConfig, groupName, "pipeline_name", "stage_name",
                "job_name");
        expectLoadForEditing(cruiseConfig);
        when(goConfigDao.md5OfConfigFile()).thenReturn(md5);

        String pipelineGroupContent = groupXmlWithInvalidAttributeValue(groupName);
        GoConfigValidity validity = goConfigService.groupSaver(groupName).saveXml(pipelineGroupContent, "md5");
        assertThat(validity.isValid(), Matchers.is(false));
        assertThat(((GoConfigValidity.InvalidGoConfig) validity).errorMessage(),
                containsString("Name is invalid. \"pipeline@$^\""));
        verify(goConfigDao, never()).updateConfig(any(UpdateConfigCommand.class));
    }

    @Test
    public void shouldReturnInvalidWhenPipelineGroupPartialXmlIsInvalid() throws Exception {
        String groupName = "group_name";
        String md5 = "md5";
        cruiseConfig = new BasicCruiseConfig();
        expectLoad(cruiseConfig);
        new GoConfigMother().addPipelineWithGroup(cruiseConfig, groupName, "pipeline_name", "stage_name",
                "job_name");
        expectLoadForEditing(cruiseConfig);
        when(goConfigDao.md5OfConfigFile()).thenReturn(md5);

        GoConfigValidity validity = goConfigService.groupSaver(groupName).saveXml("<foobar>", "md5");
        assertThat(validity.isValid(), Matchers.is(false));
        assertThat(((GoConfigValidity.InvalidGoConfig) validity).errorMessage(),
                containsString("XML document structures must start and end within the same entity"));
        verify(goConfigDao, never()).updateConfig(any(UpdateConfigCommand.class));
    }

    @Test
    public void shouldFindConfigChangesForGivenConfigMd5() throws Exception {
        goConfigService.configChangesFor("md5-5", "md5-4", new HttpLocalizedOperationResult());
        verify(configRepo).configChangesFor("md5-5", "md5-4");
    }

    @Test
    public void shouldUpdateResultAsConfigRevisionNotFoundWhenConfigChangeIsNotFound() throws Exception {
        HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
        when(configRepo.configChangesFor("md5-5", "md5-4")).thenThrow(new IllegalArgumentException("something"));
        goConfigService.configChangesFor("md5-5", "md5-4", result);
        assertThat(result.isSuccessful(), is(false));
        assertThat(result.httpCode(), is(SC_BAD_REQUEST));
        assertThat(result.message(), is("Historical configuration is not available for this stage run."));
    }

    @Test
    public void shouldUpdateResultAsCouldNotRetrieveConfigDiffWhenGenericExceptionOccurs() throws Exception {
        HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
        when(configRepo.configChangesFor("md5-5", "md5-4")).thenThrow(new RuntimeException("something"));
        goConfigService.configChangesFor("md5-5", "md5-4", result);
        assertThat(result.isSuccessful(), is(false));
        assertThat(result.httpCode(), is(SC_INTERNAL_SERVER_ERROR));
        assertThat(result.message(), is("Could not retrieve config changes for this revision."));
    }

    @Test
    public void shouldReturnWasMergedInConfigUpdateResponse_WhenConfigIsMerged() {
        when(goConfigDao.updateConfig(org.mockito.ArgumentMatchers.<UpdateConfigCommand>any()))
                .thenReturn(ConfigSaveState.MERGED);
        ConfigUpdateResponse configUpdateResponse = goConfigService.updateConfigFromUI(
                mock(UpdateConfigFromUI.class), "md5", new Username(new CaseInsensitiveString("user")),
                new HttpLocalizedOperationResult());
        assertThat(configUpdateResponse.wasMerged(), is(true));
    }

    @Test
    public void shouldReturnNotMergedInConfigUpdateResponse_WhenConfigIsUpdated() {
        when(goConfigDao.updateConfig(org.mockito.ArgumentMatchers.<UpdateConfigCommand>any()))
                .thenReturn(ConfigSaveState.UPDATED);
        ConfigUpdateResponse configUpdateResponse = goConfigService.updateConfigFromUI(
                mock(UpdateConfigFromUI.class), "md5", new Username(new CaseInsensitiveString("user")),
                new HttpLocalizedOperationResult());
        assertThat(configUpdateResponse.wasMerged(), is(false));
    }

    @Test
    public void shouldReturnNotMergedInConfigUpdateResponse_WhenConfigUpdateFailed() throws Exception {
        when(goConfigDao.updateConfig(org.mockito.ArgumentMatchers.<UpdateConfigCommand>any()))
                .thenThrow(new ConfigFileHasChangedException());
        expectLoadForEditing(cruiseConfig);
        ConfigUpdateResponse configUpdateResponse = goConfigService.updateConfigFromUI(
                mock(UpdateConfigFromUI.class), "md5", new Username(new CaseInsensitiveString("user")),
                new HttpLocalizedOperationResult());
        assertThat(configUpdateResponse.wasMerged(), is(false));
    }

    @Test
    public void badConfigShouldContainOldMD5_WhenConfigUpdateFailed() {
        when(goConfigDao.updateConfig(org.mockito.ArgumentMatchers.<UpdateConfigCommand>any()))
                .thenThrow(new RuntimeException(getGoConfigInvalidException()));
        ConfigUpdateResponse configUpdateResponse = goConfigService.updateConfigFromUI(
                mock(UpdateConfigFromUI.class), "old-md5", new Username(new CaseInsensitiveString("user")),
                new HttpLocalizedOperationResult());
        assertThat(configUpdateResponse.wasMerged(), is(false));
        assertThat(configUpdateResponse.getCruiseConfig().getMd5(), is("old-md5"));
    }

    @Test
    public void configShouldContainOldMD5_WhenConfigMergeFailed() {
        when(goConfigDao.loadForEditing()).thenReturn(new BasicCruiseConfig());
        when(goConfigDao.updateConfig(org.mockito.ArgumentMatchers.<UpdateConfigCommand>any()))
                .thenThrow(new ConfigFileHasChangedException());
        ConfigUpdateResponse configUpdateResponse = goConfigService.updateConfigFromUI(
                mock(UpdateConfigFromUI.class), "old-md5", new Username(new CaseInsensitiveString("user")),
                new HttpLocalizedOperationResult());
        assertThat(configUpdateResponse.wasMerged(), is(false));
        assertThat(configUpdateResponse.getCruiseConfig().getMd5(), is("old-md5"));
    }

    @Test
    public void shouldReturnConfigStateFromDaoLayer_WhenUpdatingServerConfig() {
        ConfigSaveState expectedSaveState = ConfigSaveState.MERGED;
        when(goConfigDao.updateConfig(org.mockito.ArgumentMatchers.<UpdateConfigCommand>any()))
                .thenReturn(expectedSaveState);
        ConfigSaveState configSaveState = goConfigService.updateServerConfig(new MailHost(new GoCipher()), true,
                "md5", null, null, null, null, "http://site", "https://site", "location");
        assertThat(configSaveState, is(expectedSaveState));
    }

    @Test
    public void shouldDelegateToConfig_getAllPipelinesInGroup() throws Exception {
        CruiseConfig cruiseConfig = mock(BasicCruiseConfig.class);
        when(goConfigDao.loadForEditing()).thenReturn(cruiseConfig);
        goConfigService.getAllPipelinesForEditInGroup("group");
        verify(cruiseConfig).pipelines("group");
    }

    @Test
    public void pipelineEditableViaUI_shouldReturnFalseWhenPipelineIsRemote() throws Exception {
        PipelineConfigs group = new BasicPipelineConfigs();
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        pipelineConfig.setOrigin(new RepoConfigOrigin());
        group.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(group));
        assertThat(goConfigService.isPipelineEditable("pipeline"), is(false));
    }

    @Test
    public void pipelineEditableViaUI_shouldReturnTrueWhenPipelineIsLocal() throws Exception {
        PipelineConfigs group = new BasicPipelineConfigs();
        PipelineConfig pipelineConfig = createPipelineConfig("pipeline", "name", "plan");
        group.add(pipelineConfig);
        expectLoad(new BasicCruiseConfig(group));
        assertThat(goConfigService.isPipelineEditable("pipeline"), is(true));
    }

    @Test
    public void shouldTellIfAnUserIsAdministrator() throws Exception {
        final Username user = new Username(new CaseInsensitiveString("user"));
        expectLoad(mock(BasicCruiseConfig.class));
        goConfigService.isAdministrator(user.getUsername());
        verify(goConfigDao.load()).isAdministrator(user.getUsername().toString());
    }

    @Test
    public void shouldBeAbleToListAllDependencyMaterialConfigs() {
        BasicCruiseConfig config = mock(BasicCruiseConfig.class);
        DependencyMaterialConfig dependencyMaterialConfig = MaterialConfigsMother.dependencyMaterialConfig();
        SvnMaterialConfig svnMaterialConfig = MaterialConfigsMother.svnMaterialConfig();
        PluggableSCMMaterialConfig pluggableSCMMaterialConfig = MaterialConfigsMother.pluggableSCMMaterialConfig();
        HashSet<MaterialConfig> materialConfigs = new HashSet<>(
                Arrays.asList(dependencyMaterialConfig, svnMaterialConfig, pluggableSCMMaterialConfig));

        when(goConfigService.getCurrentConfig()).thenReturn(config);
        when(config.getAllUniqueMaterialsBelongingToAutoPipelinesAndConfigRepos()).thenReturn(materialConfigs);

        Set<DependencyMaterialConfig> schedulableDependencyMaterials = goConfigService
                .getSchedulableDependencyMaterials();

        assertThat(schedulableDependencyMaterials.size(), is(1));
        assertTrue(schedulableDependencyMaterials.contains(dependencyMaterialConfig));
    }

    @Test
    public void shouldBeAbleToListAllSCMMaterialConfigs() {
        BasicCruiseConfig config = mock(BasicCruiseConfig.class);
        DependencyMaterialConfig dependencyMaterialConfig = MaterialConfigsMother.dependencyMaterialConfig();
        SvnMaterialConfig svnMaterialConfig = MaterialConfigsMother.svnMaterialConfig();
        PluggableSCMMaterialConfig pluggableSCMMaterialConfig = MaterialConfigsMother.pluggableSCMMaterialConfig();
        HashSet<MaterialConfig> materialConfigs = new HashSet<>(
                Arrays.asList(dependencyMaterialConfig, svnMaterialConfig, pluggableSCMMaterialConfig));

        when(goConfigService.getCurrentConfig()).thenReturn(config);
        when(config.getAllUniqueMaterialsBelongingToAutoPipelinesAndConfigRepos()).thenReturn(materialConfigs);

        Set<MaterialConfig> schedulableDependencyMaterials = goConfigService.getSchedulableSCMMaterials();

        assertThat(schedulableDependencyMaterials.size(), is(2));
        assertTrue(schedulableDependencyMaterials.contains(svnMaterialConfig));
        assertTrue(schedulableDependencyMaterials.contains(pluggableSCMMaterialConfig));
    }

    @Test
    public void shouldBeAbleToEditAnExistentLocalPipelineWithAdminPrivileges() throws Exception {
        CruiseConfig cruiseConfig = mock(CruiseConfig.class);
        PipelineConfig pipeline = new PipelineConfig();
        pipeline.setName("pipeline1");
        pipeline.setOrigin(null);

        when(goConfigDao.load()).thenReturn(cruiseConfig);
        when(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1"))).thenReturn(pipeline);
        when(cruiseConfig.getGroups())
                .thenReturn(new GoConfigMother().cruiseConfigWithOnePipelineGroup().getGroups());
        when(cruiseConfig.isAdministrator("admin_user")).thenReturn(true);

        assertTrue(goConfigService.canEditPipeline("pipeline1", new Username("admin_user")));
    }

    @Test
    public void shouldNotBeAbleToEditANonExistentPipeline() throws Exception {
        CruiseConfig cruiseConfig = mock(CruiseConfig.class);

        when(goConfigDao.load()).thenReturn(cruiseConfig);
        when(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("non_existing_pipeline")))
                .thenThrow(new RecordNotFoundException(EntityType.Pipeline, "non_existing_pipeline"));

        assertFalse(goConfigService.canEditPipeline("non_existing_pipeline", null));
    }

    @Test
    public void shouldNotBeAbleToEditPipelineIfUserDoesNotHaveSufficientPermissions() throws Exception {
        CruiseConfig cruiseConfig = mock(CruiseConfig.class);
        PipelineConfig pipeline = new PipelineConfig();
        pipeline.setName("pipeline1");

        when(goConfigDao.load()).thenReturn(cruiseConfig);
        when(cruiseConfig.isSecurityEnabled()).thenReturn(true);
        when(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1"))).thenReturn(pipeline);
        BasicCruiseConfig basicCruiseConfig = new GoConfigMother().cruiseConfigWithOnePipelineGroup();
        when(cruiseConfig.getGroups()).thenReturn(basicCruiseConfig.getGroups());
        when(cruiseConfig.findGroup("group1")).thenReturn(mock(PipelineConfigs.class));
        when(cruiseConfig.isAdministrator("view_user")).thenReturn(false);
        when(cruiseConfig.server()).thenReturn(new ServerConfig());

        assertFalse(goConfigService.canEditPipeline("pipeline1", new Username("view_user")));
    }

    @Test
    public void shouldNotAllowEditOfConfigRepoPipelines() throws Exception {
        CruiseConfig cruiseConfig = mock(CruiseConfig.class);

        when(goConfigDao.load()).thenReturn(cruiseConfig);
        PipelineConfig pipeline = new PipelineConfig();
        pipeline.setName("pipeline1");
        pipeline.setOrigin(new RepoConfigOrigin());
        when(cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline1"))).thenReturn(pipeline);
        when(cruiseConfig.getGroups())
                .thenReturn(new GoConfigMother().cruiseConfigWithOnePipelineGroup().getGroups());
        when(cruiseConfig.isAdministrator("admin_user")).thenReturn(true);

        assertFalse(goConfigService.canEditPipeline("pipeline1", new Username("admin_user")));
    }

    @Test
    public void shouldIncludeAllLocalPipelinesWithSpecificFingerprint() throws Exception {
        cruiseConfig = new BasicCruiseConfig();
        expectLoad(cruiseConfig);
        PipelineConfig pipelineConfig = new GoConfigMother().addPipelineWithGroup(cruiseConfig, "group",
                "pipeline_name", "stage_name", "job_name");

        GitMaterialConfig gitMaterialConfig = git("https://foo");
        MaterialConfigs materialConfigs = new MaterialConfigs(gitMaterialConfig);
        pipelineConfig.setMaterialConfigs(materialConfigs);

        List<CaseInsensitiveString> pipelineNames = goConfigService
                .pipelinesWithMaterial(gitMaterialConfig.getFingerprint());

        assertThat(pipelineNames, contains(new CaseInsensitiveString("pipeline_name")));
    }

    @Test
    public void shouldIncludeAllRemotePipelinesWithSpecificFingerprint() throws Exception {
        cruiseConfig = new BasicCruiseConfig();
        expectLoad(cruiseConfig);
        PipelineConfig pipelineConfig = new GoConfigMother().addPipelineWithGroup(cruiseConfig, "group",
                "pipeline_name", "stage_name", "job_name");

        GitMaterialConfig gitMaterialConfig = git("https://foo");
        MaterialConfigs materialConfigs = new MaterialConfigs(gitMaterialConfig);
        pipelineConfig.setMaterialConfigs(materialConfigs);
        pipelineConfig.setOrigin(new RepoConfigOrigin());

        List<CaseInsensitiveString> pipelineNames = goConfigService
                .pipelinesWithMaterial(gitMaterialConfig.getFingerprint());

        assertThat(pipelineNames, contains(new CaseInsensitiveString("pipeline_name")));
    }

    @Test
    public void shouldReturnEmptyWhenThereAreNoPipelinesWithGivenFingerprint() {
        List<CaseInsensitiveString> pipelineNames = goConfigService.pipelinesWithMaterial("fingerprint");

        assertThat(pipelineNames.isEmpty(), is(true));
    }

    @Test
    public void shouldFindGroupByPipelineName() throws Exception {
        GoConfigMother configMother = new GoConfigMother();
        BasicCruiseConfig config = GoConfigMother.defaultCruiseConfig();
        configMother.addPipelineWithGroup(config, "group1", "pipeline1", "stage1", "job1");
        configMother.addPipelineWithGroup(config, "group1", "pipeline2", "stage1", "job1");
        configMother.addPipelineWithGroup(config, "group2", "pipeline3", "stage1", "job1");

        expectLoad(config);
        assertThat(goConfigService.findGroupByPipeline(new CaseInsensitiveString("pipeline1")).getGroup(),
                is("group1"));
        assertThat(goConfigService.findGroupByPipeline(new CaseInsensitiveString("pipeline2")).getGroup(),
                is("group1"));
        assertThat(goConfigService.findGroupByPipeline(new CaseInsensitiveString("pipeline3")).getGroup(),
                is("group2"));
    }

    @Test
    public void shouldFindPipelineByPipelineName() throws Exception {
        GoConfigMother configMother = new GoConfigMother();
        BasicCruiseConfig config = GoConfigMother.defaultCruiseConfig();
        configMother.addPipelineWithGroup(config, "group1", "pipeline1", "stage1", "job1");
        configMother.addPipelineWithGroup(config, "group1", "pipeline2", "stage1", "job1");
        configMother.addPipelineWithGroup(config, "group2", "pipeline3", "stage1", "job1");

        expectLoad(config);
        assertThat(goConfigService.findPipelineByName(new CaseInsensitiveString("pipeline1")).name().toString(),
                is("pipeline1"));
        assertThat(goConfigService.findPipelineByName(new CaseInsensitiveString("pipeline2")).name().toString(),
                is("pipeline2"));
        assertThat(goConfigService.findPipelineByName(new CaseInsensitiveString("pipeline3")).name().toString(),
                is("pipeline3"));
    }

    @Test
    public void shouldReturnNullIfNoPipelineExistByPipelineName() throws Exception {
        GoConfigMother configMother = new GoConfigMother();
        BasicCruiseConfig config = GoConfigMother.defaultCruiseConfig();
        configMother.addPipelineWithGroup(config, "group1", "pipeline1", "stage1", "job1");
        configMother.addPipelineWithGroup(config, "group1", "pipeline2", "stage1", "job1");
        configMother.addPipelineWithGroup(config, "group2", "pipeline3", "stage1", "job1");

        expectLoad(config);
        assertThat(goConfigService.findPipelineByName(new CaseInsensitiveString("invalid")), is(nullValue()));
    }

    @Test
    public void shouldReturnSecretConfigBySecretConfigId() throws Exception {
        Rules rules = new Rules(new Allow("refer", "pipeline_group", "default"));
        SecretConfig secretConfig = new SecretConfig("secret_config_id", "plugin_id", rules);
        GoConfigMother configMother = new GoConfigMother();
        CruiseConfig config = GoConfigMother.configWithSecretConfig(secretConfig);
        configMother.addPipelineWithGroup(config, "default", "pipeline1", "stage1", "job1");

        expectLoad(config);
        assertThat(goConfigService.getSecretConfigById("secret_config_id"), is(secretConfig));
    }

    @Test
    public void shouldReturnNullIfNoSecretConfigExistBySecretConfigId() throws Exception {
        GoConfigMother configMother = new GoConfigMother();
        BasicCruiseConfig config = GoConfigMother.defaultCruiseConfig();
        configMother.addPipelineWithGroup(config, "group1", "pipeline1", "stage1", "job1");

        expectLoad(config);
        assertThat(goConfigService.getSecretConfigById("invalid"), is(nullValue()));
    }

    @Test
    public void shouldReturnAllPipelinesForASuperAdmin() throws Exception {
        GoConfigMother configMother = new GoConfigMother();
        BasicCruiseConfig config = GoConfigMother.defaultCruiseConfig();
        GoConfigMother.enableSecurityWithPasswordFilePlugin(config);
        GoConfigMother.addUserAsSuperAdmin(config, "superadmin");
        PipelineConfig pipelineConfig = configMother.addPipelineWithGroup(config, "group1", "p1", "s1", "j1");
        when(goConfigDao.loadForEditing()).thenReturn(config);
        expectLoad(config);

        List<PipelineConfig> pipelines = goConfigService
                .getAllPipelineConfigsForEditForUser(new Username("superadmin"));

        assertThat(pipelines, contains(pipelineConfig));
    }

    @Test
    public void shouldReturnSpecificPipelinesForAGroupAdmin() throws Exception {
        GoConfigMother configMother = new GoConfigMother();
        BasicCruiseConfig config = GoConfigMother.defaultCruiseConfig();
        GoConfigMother.enableSecurityWithPasswordFilePlugin(config);
        GoConfigMother.addUserAsSuperAdmin(config, "superadmin");

        PipelineConfig pipelineConfig1 = configMother.addPipelineWithGroup(config, "group1", "p1", "s1", "j1");
        PipelineConfig pipelineConfig2 = configMother.addPipelineWithGroup(config, "group2", "p2", "s1", "j1");

        configMother.addAdminUserForPipelineGroup(config, "groupAdmin", "group1");

        when(goConfigDao.loadForEditing()).thenReturn(config);
        expectLoad(config);

        List<PipelineConfig> pipelines = goConfigService
                .getAllPipelineConfigsForEditForUser(new Username("groupAdmin"));

        assertThat(pipelines, contains(pipelineConfig1));
        assertThat(pipelines, not(contains(pipelineConfig2)));
    }

    private PipelineConfig createPipelineConfig(String pipelineName, String stageName, String... buildNames) {
        PipelineConfig pipeline = new PipelineConfig(new CaseInsensitiveString(pipelineName),
                new MaterialConfigs());
        pipeline.add(new StageConfig(new CaseInsensitiveString(stageName), jobConfigs(buildNames)));
        return pipeline;
    }

    private JobConfigs jobConfigs(String... buildNames) {
        JobConfigs jobConfigs = new JobConfigs();
        for (String buildName : buildNames) {
            jobConfigs.add(new JobConfig(buildName));
        }
        return jobConfigs;
    }

    private GoConfigService goConfigServiceWithInvalidStatus() {
        goConfigDao = mock(GoConfigDao.class, "badCruiseConfigManager");
        when(goConfigDao.checkConfigFileValid()).thenReturn(GoConfigValidity.invalid("JDom exception"));
        return new GoConfigService(goConfigDao, pipelineRepository, new SystemTimeClock(),
                mock(GoConfigMigration.class), goCache, null,
                ConfigElementImplementationRegistryMother.withNoPlugins(), instanceFactory, null, null);
    }

    private GoConfigInvalidException getGoConfigInvalidException() {
        ConfigErrors configErrors = new ConfigErrors();
        configErrors.add("command", "command cannot be empty");
        AllConfigErrors list = new AllConfigErrors();
        list.add(configErrors);
        return new GoConfigInvalidException(new BasicCruiseConfig(), list.asString());
    }

    private String groupXml(final String groupName) {
        return "<pipelines group=\"" + groupName + "\">\n"
                + "  <pipeline name=\"new_name\" labeltemplate=\"${COUNT}-#{foo}\">\n" + "     <params>\n"
                + "      <param name=\"foo\">test</param>\n" + "    </params>" + "    <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>";

    }

    private String groupXmlWithEntity(String filePathToReferToInEntity) {
        return "<!DOCTYPE foo [  \n" + "<!ELEMENT param ANY >\n" + "<!ENTITY myentity SYSTEM \"file://"
                + filePathToReferToInEntity + "\" >]>" + "<pipelines group=\"group_name\">\n"
                + "  <pipeline name=\"pipeline1\" labeltemplate=\"${COUNT}-#{foo}\">\n" + "     <params>\n"
                + "      <param name=\"foo\">&myentity;</param>\n" + "    </params>" + "    <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>";

    }

    private String groupXmlWithInvalidElement(final String groupName) {
        return "<pipelines group='" + groupName + "'>" + "  <unknown/>" + "<pipeline name='pipeline'>\n"
                + "    <materials>\n" + "         <svn url ='svnurl' dest='a'/>\n" + "    </materials>\n"
                + "  <stage name='firstStage'>" + "     <jobs>" + "         <job name='jobName'/>" + "      </jobs>"
                + "  </stage>" + "</pipeline>" + "</pipelines>";
    }

    private String groupXmlWithInvalidAttributeValue(final String groupName) {
        return "<pipelines group='" + groupName + "'>" + "<pipeline name='pipeline@$^'>\n" + "    <materials>\n"
                + "         <svn url ='svnurl' dest='a'/>\n" + "    </materials>\n" + "  <stage name='firstStage'>"
                + "     <jobs>" + "         <job name='jobName'/>" + "      </jobs>" + "  </stage>" + "</pipeline>"
                + "</pipelines>";
    }

}