/* * Copyright 2017 ThoughtWorks, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.thoughtworks.go.server.service; import com.rits.cloning.Cloner; import com.thoughtworks.go.config.*; import com.thoughtworks.go.config.materials.MaterialConfigs; import com.thoughtworks.go.config.update.ConfigUpdateResponse; import com.thoughtworks.go.config.update.UpdateConfigFromUI; import com.thoughtworks.go.config.validation.GoConfigValidity; import com.thoughtworks.go.domain.JobIdentifier; 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.i18n.LocalizedMessage; import com.thoughtworks.go.i18n.Localizer; import com.thoughtworks.go.presentation.ConfigForEdit; import com.thoughtworks.go.security.GoCipher; import com.thoughtworks.go.server.dao.DatabaseAccessHelper; import com.thoughtworks.go.server.domain.Username; import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult; import com.thoughtworks.go.server.service.result.LocalizedOperationResult; import com.thoughtworks.go.service.ConfigRepository; import com.thoughtworks.go.util.GoConfigFileHelper; import com.thoughtworks.go.util.ReflectionUtil; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.io.ByteArrayOutputStream; import java.io.IOException; import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.sameInstance; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.assertThat; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:WEB-INF/applicationContext-global.xml", "classpath:WEB-INF/applicationContext-dataLocalAccess.xml", "classpath:WEB-INF/applicationContext-acegi-security.xml" }) public class GoConfigServiceIntegrationTest { @Autowired private SecurityService securityService; @Autowired private GoConfigDao goConfigDao; @Autowired private GoConfigService goConfigService; @Autowired private DatabaseAccessHelper dbHelper; @Autowired private Localizer localizer; @Autowired private ConfigRepository configRepo; @Autowired private CachedGoConfig cachedGoConfig; @Autowired private ServerConfigService serverConfigService; private GoConfigFileHelper configHelper; @Before public void setup() throws Exception { configHelper = new GoConfigFileHelper(); dbHelper.onSetUp(); configHelper.usingCruiseConfigDao(goConfigDao).initializeConfigFile(); configHelper.onSetUp(); goConfigService.forceNotifyListeners(); } @After public void tearDown() throws Exception { configHelper.onTearDown(); dbHelper.onTearDown(); } @Test public void shouldUnderstandGettingPipelineConfigForEdit() { PipelineConfig pipelineConfig = PipelineConfigMother.createPipelineConfig("my-pipeline", "my-stage", "my-build"); pipelineConfig.addParam(new ParamConfig("label-param", "param-value")); pipelineConfig.setLabelTemplate("${COUNT}-#{label-param}"); CruiseConfig config = configHelper.currentConfig(); config.addPipeline("defaultGroup", pipelineConfig); configHelper.writeConfigFile(config); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigForEdit<PipelineConfig> configForEdit = goConfigService.loadForEdit("my-pipeline", new Username(new CaseInsensitiveString("root")), result); assertThat(configForEdit.getProcessedConfig(), is(goConfigService.getCurrentConfig())); assertThat(configForEdit.getConfig().getLabelTemplate(), is("${COUNT}-#{label-param}")); assertThat(configForEdit.getCruiseConfig().getMd5(), is(goConfigService.configFileMd5())); configHelper.addPipeline("pipeline-foo", "stage-foo"); assertThat(configForEdit.getCruiseConfig().getMd5(), not(goConfigService.configFileMd5())); } @Test public void shouldOnlyAllowAdminsToGetPipelineConfig() { setupSecurity(); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigForEdit configForEdit = goConfigService.loadForEdit("my-pipeline", new Username(new CaseInsensitiveString("loser")), result); assertThat(configForEdit, is(nullValue())); assertThat(result.httpCode(), is(401)); assertThat(result.message(localizer), is("Unauthorized to edit my-pipeline pipeline.")); result = new HttpLocalizedOperationResult(); configForEdit = goConfigService.loadForEdit("my-pipeline", new Username(new CaseInsensitiveString("pipeline_admin")), result); assertThat(configForEdit, not(nullValue())); assertThat(result.isSuccessful(), is(true)); result = new HttpLocalizedOperationResult(); configForEdit = goConfigService.loadForEdit("my-pipeline", new Username(new CaseInsensitiveString("root")), result); assertThat(configForEdit, not(nullValue())); assertThat(result.isSuccessful(), is(true)); } @Test public void shouldReturn404WhenUserIsNotAnAdminAndTriesToLoadANonExistentPipeline() { setupSecurity(); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigForEdit configForEdit = goConfigService.loadForEdit("non-existent-pipeline", new Username(new CaseInsensitiveString("loser")), result); assertThat(configForEdit, is(nullValue())); assertThat(result.httpCode(), is(404)); assertThat(result.message(localizer), is("pipeline 'non-existent-pipeline' not found.")); } private void setupSecurity() { SecurityConfig securityConfig = new SecurityConfig(new LdapConfig(new GoCipher()), new PasswordFileConfig("foo"), false); securityConfig.adminsConfig().add(new AdminUser(new CaseInsensitiveString("root"))); configHelper.addSecurity(securityConfig); configHelper.addPipeline("my-pipeline", "my-stage"); configHelper.setAdminPermissionForGroup(BasicPipelineConfigs.DEFAULT_GROUP, "pipeline_admin"); } @Test public void shouldReturnWithA404WhenPipelineNotFound() { CruiseConfig config = configHelper.currentConfig(); configHelper.writeConfigFile(config); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); assertThat(goConfigService.loadForEdit("my-invalid-pipeline", new Username(new CaseInsensitiveString("root")), result), is(nullValue())); assertThat(result.isSuccessful(), is(false)); assertThat(result.httpCode(), is(404)); assertThat(result.message(localizer), is("pipeline 'my-invalid-pipeline' not found.")); } @Test public void shouldAlwaysReturnCloneOfCruiseConfigSoThatCachedCopyIsNotCorrupted() { configHelper.addPipeline("my-pipeline", "my-stage"); CruiseConfig cruiseConfig = configHelper.load(); cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("my-pipeline")).setLabelTemplate("foo-${COUNT}-bar"); configHelper.writeConfigFile(cruiseConfig); ConfigForEdit<PipelineConfig> config = goConfigService.loadForEdit("my-pipeline", new Username(new CaseInsensitiveString("root")), new HttpLocalizedOperationResult()); config.getConfig().setLabelTemplate("Foo-bar"); config.getCruiseConfig().pipelineConfigByName(new CaseInsensitiveString("my-pipeline")).setLabelTemplate("Foo-bar"); config.getProcessedConfig().pipelineConfigByName(new CaseInsensitiveString("my-pipeline")).setLabelTemplate("Foo-bar"); assertThat(goConfigService.getConfigForEditing().pipelineConfigByName(new CaseInsensitiveString("my-pipeline")).getLabelTemplate(), is("foo-${COUNT}-bar")); assertThat(goConfigService.currentCruiseConfig().pipelineConfigByName(new CaseInsensitiveString("my-pipeline")).getLabelTemplate(), is("foo-${COUNT}-bar")); } @Test public void shouldReturn401WhenAUserIsNotAnAdmin() throws IOException { configHelper.turnOnSecurity(); configHelper.addAdmins("hero"); configHelper.addTemplate("pipeline", "stage"); cachedGoConfig.forceReload(); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); goConfigService.loadCruiseConfigForEdit(new Username(new CaseInsensitiveString("loser")), result); assertThat(result.isSuccessful(), is(false)); assertThat(result.httpCode(), is(401)); assertThat(result.message(localizer), is("Unauthorized to edit configuration")); } @Test public void shouldReturnANewCopyOfConfigForEditWhenAUserIsATemplateAdmin() throws IOException { configHelper.turnOnSecurity(); configHelper.addAdmins("hero"); configHelper.addTemplate("pipeline", new Authorization(new AdminsConfig(new AdminUser(new CaseInsensitiveString("template-admin")))), "stage"); cachedGoConfig.forceReload(); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); CruiseConfig config = goConfigService.loadCruiseConfigForEdit(new Username(new CaseInsensitiveString("template-admin")), result); assertThat(result.isSuccessful(), is(true)); assertThat(goConfigService.getConfigForEditing(), is(config)); assertThat(goConfigService.getConfigForEditing(), not(sameInstance(config))); } @Test public void shouldReturnANewCopyOfConfigForEditWhenLoadingForEdit() throws IOException { configHelper.turnOnSecurity(); configHelper.addAdmins("hero"); configHelper.addTemplate("pipeline", "stage"); cachedGoConfig.forceReload(); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); CruiseConfig config = goConfigService.loadCruiseConfigForEdit(new Username(new CaseInsensitiveString("hero")), result); assertThat(result.isSuccessful(), is(true)); assertThat(goConfigService.getConfigForEditing(), is(config)); assertThat(goConfigService.getConfigForEditing(), not(sameInstance(config))); } @Test public void shouldLoadPipelineGroupForEdit() throws IOException { PipelineConfig pipelineConfig = PipelineConfigMother.createPipelineConfig("my-pipeline", "my-stage", "my-build"); pipelineConfig.addParam(new ParamConfig("label-param", "param-value")); pipelineConfig.setLabelTemplate("${COUNT}-#{label-param}"); CruiseConfig config = configHelper.currentConfig(); config.addPipeline("group_one", pipelineConfig); configHelper.writeConfigFile(config); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigForEdit<PipelineConfigs> configForEdit = goConfigService.loadGroupForEditing("group_one", new Username(new CaseInsensitiveString("root")), result); assertThat(configForEdit.getConfig().findBy(new CaseInsensitiveString("my-pipeline")).getLabelTemplate(), is("${COUNT}-#{label-param}")); assertThat(configForEdit.getCruiseConfig().getMd5(), is(goConfigService.configFileMd5())); configHelper.addPipeline("pipeline-foo", "stage-foo"); assertThat(configForEdit.getCruiseConfig().getMd5(), not(goConfigService.configFileMd5())); assertThat(result.isSuccessful(), is(true)); } @Test public void shouldCloneTheConfigObjectBeforeHandingOffForEdit() throws IOException { configHelper.addAdmins("hero"); configHelper.addPipelineWithGroup("group_one", "pipeline", "stage", "my_job"); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigForEdit<PipelineConfigs> groupForEdit = goConfigService.loadGroupForEditing("group_one", new Username(new CaseInsensitiveString("hero")), result); PipelineConfigs group = groupForEdit.getConfig(); group.getAuthorization().getAdminsConfig().add(new AdminUser(new CaseInsensitiveString("loser"))); PipelineConfigs groupFromProcessedConfigCopy = groupForEdit.getProcessedConfig().getGroups().findGroup("group_one"); groupFromProcessedConfigCopy.getAuthorization().getAdminsConfig().add(new AdminUser(new CaseInsensitiveString("loser"))); PipelineConfigs groupFromEditableConfigCopy = groupForEdit.getCruiseConfig().getGroups().findGroup("group_one"); groupFromEditableConfigCopy.getAuthorization().getAdminsConfig().add(new AdminUser(new CaseInsensitiveString("loser"))); AdminsConfig adminsConfig = goConfigService.getConfigForEditing().findGroup("group_one").getAuthorization().getAdminsConfig(); assertThat(adminsConfig.size(), is(0));//should not affect the global copy adminsConfig = goConfigService.currentCruiseConfig().findGroup("group_one").getAuthorization().getAdminsConfig(); assertThat(adminsConfig.size(), is(0)); group.setGroup("new-name"); assertThat(groupForEdit.getCruiseConfig().hasPipelineGroup("group_one"), is(true)); assertThat(groupForEdit.getProcessedConfig().hasPipelineGroup("group_one"), is(true)); assertThat(groupForEdit.getCruiseConfig().hasPipelineGroup("new-name"), is(false)); assertThat(groupForEdit.getProcessedConfig().hasPipelineGroup("new-name"), is(false)); } @Test public void shouldErrorOutWhenUserIsNotAuthorizedToLoadGroupForEdit() throws IOException { configHelper.turnOnSecurity(); configHelper.addAdmins("hero"); configHelper.addPipelineWithGroup("group_one", "pipeline", "stage", "my_job"); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigForEdit<PipelineConfigs> configForEdit = goConfigService.loadGroupForEditing("group_one", new Username(new CaseInsensitiveString("loser")), result); assertThat(result.isSuccessful(), is(false)); assertThat(result.message(localizer), is("Unauthorized to edit 'group_one' group.")); assertThat(configForEdit, is(nullValue())); } @Test public void shouldFailLoadingWhenGivenInvalidGroupName() throws IOException { configHelper.addPipelineWithGroup("group_one", "pipeline", "stage", "my_job"); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigForEdit<PipelineConfigs> configForEdit = goConfigService.loadGroupForEditing("group_foo", new Username(new CaseInsensitiveString("loser")), result); assertThat(result.isSuccessful(), is(false)); assertThat(result.message(localizer), is("Pipeline group named 'group_foo' does not exist.")); assertThat(configForEdit, is(nullValue())); } @Test public void shouldUpdateConfigFromUI() { configHelper.addPipeline("pipeline", "stage"); String md5 = goConfigService.getConfigForEditing().getMd5(); ConfigUpdateResponse response = goConfigService.updateConfigFromUI(new AddStageToPipelineCommand("secondStage"), md5, Username.ANONYMOUS, new HttpLocalizedOperationResult()); PipelineConfig config = goConfigService.getConfigForEditing().pipelineConfigByName(new CaseInsensitiveString("pipeline")); assertThat(config.size(), is(2)); assertThat(config.get(0).name(), is(new CaseInsensitiveString("stage"))); assertThat(config.get(1).name(), is(new CaseInsensitiveString("secondStage"))); assertThat(response.configAfterUpdate().hasStageConfigNamed(new CaseInsensitiveString("pipeline"), new CaseInsensitiveString("secondStage"), true), is(true)); } @Test public void shouldRespondWithNewConfigWhenSavedSuccessfully() { configHelper.addPipeline("pipeline", "stage"); String md5 = goConfigService.getConfigForEditing().getMd5(); UpdateConfigFromUI pipelineAndStageRename = new PipelineStageRenamingCommand(); ConfigUpdateResponse response = goConfigService.updateConfigFromUI(pipelineAndStageRename, md5, Username.ANONYMOUS, new HttpLocalizedOperationResult()); PipelineConfig pipeline = goConfigService.getConfigForEditing().pipelineConfigByName(new CaseInsensitiveString("new-pipeline")); StageConfig stage = pipeline.getStage(new CaseInsensitiveString("new-stage")); assertThat(pipeline, not(nullValue())); assertThat(stage, not(nullValue())); assertThat(((PipelineConfig) response.getNode()).name(), is(new CaseInsensitiveString("new-pipeline"))); assertThat(((StageConfig) response.getSubject()).name(), is(new CaseInsensitiveString("new-stage"))); assertThat(response.configAfterUpdate().hasStageConfigNamed(new CaseInsensitiveString("new-pipeline"), new CaseInsensitiveString("new-stage"), false), is(true)); } @Test public void shouldRespondWithLatestUnmodifiedConfigInCaseOfUnexpectedFailures() { configHelper.addPipeline("pipeline", "stage"); String md5 = goConfigService.getConfigForEditing().getMd5(); UpdateConfigFromUI pipelineAndStageRename = new PipelineStageRenamingCommand() { @Override public void update(Validatable pipelineNode) { throw new RuntimeException("Oh no!"); } }; ConfigUpdateResponse response = goConfigService.updateConfigFromUI(pipelineAndStageRename, md5, Username.ANONYMOUS, new HttpLocalizedOperationResult()); assertThat(goConfigService.getConfigForEditing().hasPipelineNamed(new CaseInsensitiveString("new-pipeline")), is(false)); assertThat(goConfigService.getConfigForEditing().hasPipelineNamed(new CaseInsensitiveString("pipeline")), is(true)); assertThat(((PipelineConfig) response.getNode()).name(), is(new CaseInsensitiveString("pipeline"))); assertThat(((StageConfig) response.getSubject()).name(), is(new CaseInsensitiveString("stage"))); assertThat(response.configAfterUpdate().hasStageConfigNamed(new CaseInsensitiveString("pipeline"), new CaseInsensitiveString("new-stage"), false), is(false)); assertThat(response.configAfterUpdate().hasStageConfigNamed(new CaseInsensitiveString("pipeline"), new CaseInsensitiveString("stage"), false), is(true)); } @Test public void shouldRespondWithModifiedConfigWhenSaveFailsBecauseOfValidationErrors() { configHelper.addPipeline("pipeline", "stage"); String md5 = goConfigService.getConfigForEditing().getMd5(); UpdateConfigFromUI pipelineAndStageRename = new PipelineStageRenamingCommand() {{ newPipelineName = "pipeline!@foo - bar"; }}; ConfigUpdateResponse response = goConfigService.updateConfigFromUI(pipelineAndStageRename, md5, Username.ANONYMOUS, new HttpLocalizedOperationResult()); assertThat(goConfigService.getConfigForEditing().hasPipelineNamed(new CaseInsensitiveString("pipeline!@foo - bar")), is(false)); assertThat(goConfigService.getConfigForEditing().hasPipelineNamed(new CaseInsensitiveString("pipeline")), is(true)); assertThat(((PipelineConfig) response.getNode()).name(), is(new CaseInsensitiveString("pipeline!@foo - bar"))); assertThat(((StageConfig) response.getSubject()).name(), is(new CaseInsensitiveString("new-stage"))); assertThat(response.configAfterUpdate().hasStageConfigNamed(new CaseInsensitiveString("pipeline!@foo - bar"), new CaseInsensitiveString("new-stage"), false), is(false)); } @Test public void shouldReturnAllErrorsAppliedOverEditedCopy() { configHelper.addPipeline("pipeline", "stage"); configHelper.addParamToPipeline("pipeline", "mingle_url", "http://foo.bar"); String md5 = goConfigService.getConfigForEditing().getMd5(); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigUpdateResponse response = goConfigService.updateConfigFromUI(new UpdateConfigFromUI() { public void checkPermission(CruiseConfig cruiseConfig, LocalizedOperationResult result) { } public Validatable node(CruiseConfig cruiseConfig) { return cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline")); } public Validatable updatedNode(CruiseConfig cruiseConfig) { return node(cruiseConfig); } public void update(Validatable pipeline) { PipelineConfig pipelineConfig = (PipelineConfig) pipeline; pipelineConfig.setMingleConfig(new MingleConfig("#{mingle_url}", "go")); } public Validatable subject(Validatable node) { return node; } public Validatable updatedSubject(Validatable updatedNode) { return subject(updatedNode); } }, md5, new Username(new CaseInsensitiveString("admin")), result); MingleConfig mingleConfig = ((PipelineConfig) response.getNode()).getMingleConfig(); assertThat(mingleConfig.errors().on(MingleConfig.BASE_URL), is("Should be a URL starting with https://")); assertThat(mingleConfig.getBaseUrl(), is("#{mingle_url}")); } @Test public void shouldReturnTheLatestConfigAsResultWhenThereIsAnMd5Conflict() { configHelper.addPipeline("pipeline", "stage"); String md5 = goConfigService.getConfigForEditing().getMd5(); goConfigService.updateConfigFromUI(new AddStageToPipelineCommand("secondStage"), md5, Username.ANONYMOUS, new HttpLocalizedOperationResult()); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigUpdateResponse response = goConfigService.updateConfigFromUI(new AddStageToPipelineCommand("thirdStage"), md5, Username.ANONYMOUS, result); assertFailedResult(result, "Save failed. Configuration file has been modified by someone else."); CruiseConfig expectedConfig = goConfigService.getConfigForEditing(); CruiseConfig modifiedConfig = new Cloner().deepClone(expectedConfig); ReflectionUtil.setField(modifiedConfig, "md5", expectedConfig.getMd5()); PipelineConfig expected = modifiedConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline")); expected.addStageWithoutValidityAssertion(StageConfigMother.custom("thirdStage", "job")); PipelineConfig actual = (PipelineConfig) response.getNode(); assertThat(response.configAfterUpdate(), is(expectedConfig)); assertThat(response.getCruiseConfig(), is(modifiedConfig)); assertThat(actual, is(expected)); assertFailedResult(result, "Save failed. Configuration file has been modified by someone else."); } @Test public void shouldReturnAResponseWithTheValidatedCruiseConfig() { HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); configHelper.addPipeline("pipeline", "stage"); String md5 = goConfigService.getConfigForEditing().getMd5(); ConfigUpdateResponse response = goConfigService.updateConfigFromUI(new AddStageToPipelineCommand("stage"), md5, Username.ANONYMOUS, result); CruiseConfig invalidConfig = response.getCruiseConfig(); assertThat(invalidConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline")).size(), is(2));//Make sure that the config returned is the duplicate on. PipelineConfig config = (PipelineConfig) response.getNode(); assertThat(config.size(), is(2)); assertStageError(config.get(1), "You have defined multiple stages called 'stage'. Stage names are case-insensitive and must be unique.", StageConfig.NAME); assertFailedResult(result, "Save failed, see errors below"); assertThat(response.configAfterUpdate().hasStageConfigNamed(new CaseInsensitiveString("pipeline"), new CaseInsensitiveString("secondStage"), true), is(false)); } @Test public void shouldNotUpdateConfigFromUI_whenUpdateMethodBombs() { final PipelineConfig pipelineConfig = configHelper.addPipeline("pipeline", "stage"); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); String md5 = goConfigService.getConfigForEditing().getMd5(); ConfigUpdateResponse response = goConfigService.updateConfigFromUI(new AddStageToPipelineCommand("secondStage") { public void update(Validatable node) { super.update(node); throw new RuntimeException("oops, foo bared!"); } }, md5, Username.ANONYMOUS, result); PipelineConfig config = goConfigService.getConfigForEditing().pipelineConfigByName(new CaseInsensitiveString("pipeline")); assertThat(config.size(), is(1)); assertThat(config.get(0).name(), is(new CaseInsensitiveString("stage"))); assertThat(response.getCruiseConfig(), is(goConfigService.getConfigForEditing())); assertThat(response.getNode(), is(pipelineConfig)); assertFailedResult(result, "Save failed. oops, foo bared!"); } @Test public void shouldNotUpdateConfigFromUIWhentheUserDoesNotHavePermissions() { final PipelineConfig pipelineConfig = configHelper.addPipeline("pipeline", "stage"); HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); String md5 = goConfigService.getConfigForEditing().getMd5(); final CruiseConfig[] configObtainedInCheckPermissions = new CruiseConfig[1]; ConfigUpdateResponse response = goConfigService.updateConfigFromUI(new AddStageToPipelineCommand("secondStage") { public void checkPermission(CruiseConfig cruiseConfig, LocalizedOperationResult result) { result.unauthorized(LocalizedMessage.string("UNAUTHORIZED_TO_EDIT_GROUP", "groupName"), null); configObtainedInCheckPermissions[0] = cruiseConfig; } }, md5, Username.ANONYMOUS, result); assertThat(configObtainedInCheckPermissions[0], is(goConfigService.getCurrentConfig())); PipelineConfig config = goConfigService.getConfigForEditing().pipelineConfigByName(new CaseInsensitiveString("pipeline")); assertThat(config.size(), is(1)); assertThat(config.get(0).name(), is(new CaseInsensitiveString("stage"))); assertThat(response.getCruiseConfig(), is(goConfigService.getConfigForEditing())); assertThat(response.getNode(), is(pipelineConfig)); assertThat(result.isSuccessful(), is(false)); assertThat(result.httpCode(), is(401)); assertThat(result.message(localizer), is("Unauthorized to edit 'groupName' group.")); } @Test public void shouldLoadConfigFileOnlyWhenModifiedOnDisk() throws InterruptedException { cachedGoConfig.forceReload(); Thread.sleep(1000); goConfigService.updateConfig(new UpdateConfigCommand() { public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.server().setArtifactsDir("foo"); return cruiseConfig; } }); CruiseConfig cruiseConfig = cachedGoConfig.loadForEditing(); cachedGoConfig.forceReload(); assertThat(cruiseConfig, sameInstance(cachedGoConfig.loadForEditing())); } @Test public void shouldReturnTheServerLevelJobTimeoutIfTheJobDoesNotHaveItConfigured() { configHelper.addPipeline("pipeline", "stage"); setJobTimeoutTo("30"); assertThat(goConfigService.getUnresponsiveJobTerminationThreshold(new JobIdentifier("pipeline", -1, "label", "stage", "-1", "unit")), is(30 * 60 * 1000L)); } @Test public void shouldReturnTrueIfTheJobDoesNotHaveTimeoutConfigured() { configHelper.addPipeline("pipeline", "stage"); assertThat(goConfigService.canCancelJobIfHung(new JobIdentifier("pipeline", -1, "label", "stage", "-1", "unit")), is(false)); } @Test public void shouldReturnFalseIfTheJobDoesNotHaveTimeoutConfiguredAndServerHasItSetToZero() { configHelper.addPipeline("pipeline", "stage"); setJobTimeoutTo("0"); assertThat(goConfigService.canCancelJobIfHung(new JobIdentifier("pipeline", -1, "label", "stage", "-1", "unit")), is(false)); } @Test public void shouldReturnFalseIfPipelineIsNotFoundForTheJob() { assertThat(goConfigService.canCancelJobIfHung(new JobIdentifier("recently_deleted_pipeline", -1, "label", "stage", "-1", "unit")), is(false)); } @Test public void shouldReturnTrueIfTheJobHasNonZeroTimeoutConfigured() { configHelper.addPipeline("pipeline", "stage"); CruiseConfig config = configHelper.currentConfig(); config.findJob("pipeline", "stage", "unit").setTimeout("10"); configHelper.writeConfigFile(config); assertThat(goConfigService.canCancelJobIfHung(new JobIdentifier("pipeline", -1, "label", "stage", "-1", "unit")), is(true)); } @Test public void shouldReturnFalseIfTheJobHasZeroTimeoutConfigured() { configHelper.addPipeline("pipeline", "stage"); CruiseConfig config = configHelper.currentConfig(); config.findJob("pipeline", "stage", "unit").setTimeout("0"); configHelper.writeConfigFile(config); assertThat(goConfigService.canCancelJobIfHung(new JobIdentifier("pipeline", -1, "label", "stage", "-1", "unit")), is(false)); } @Test public void shouldReturnTheJobLevelTimeoutIfTheJobHasItConfigured() { configHelper.addPipeline("pipeline", "stage"); CruiseConfig config = configHelper.currentConfig(); config.findJob("pipeline", "stage", "unit").setTimeout("10"); configHelper.writeConfigFile(config); setJobTimeoutTo("30"); assertThat(goConfigService.getUnresponsiveJobTerminationThreshold(new JobIdentifier("pipeline", -1, "label", "stage", "-1", "unit")), is(10 * 60 * 1000L)); } @Test public void shouldReturnTheDefaultTimeoutIfThePipelineIsNotRecentlyDeleted() { assertThat(goConfigService.getUnresponsiveJobTerminationThreshold(new JobIdentifier("recently_deleted_pipeline", -1, "label", "stage", "-1", "unit")), is(0L)); } @Test public void shouldThrowUpOnConfigSaveMergeConflict_ViaMergeFlow() throws Exception { // User 1 loads page CruiseConfig user1SeeingConfig = goConfigDao.loadForEditing(); String user1SeeingMd5 = user1SeeingConfig.getMd5(); // User 2 edits config configHelper.addPipelineWithGroup("defaultGroup", "user2_pipeline", "user2_stage", "user2_job"); CruiseConfig user2SeeingConfig = configHelper.load(); // User 1 edits old config new GoConfigMother().addPipelineWithGroup(user1SeeingConfig, "defaultGroup", "user1_pipeline", "user1_stage", "user1_job"); ByteArrayOutputStream os = new ByteArrayOutputStream(); configHelper.getXml(user1SeeingConfig, os); // User 1 saves edited config GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false); GoConfigValidity validity = saver.saveXml(os.toString(), user1SeeingMd5); assertThat(validity.isValid(), is(false)); assertThat(validity.isType(GoConfigValidity.VT_MERGE_OPERATION_ERROR), is(true)); assertThat(validity.errorMessage(), is("Configuration file has been modified by someone else.")); } @Test public void shouldThrowUpOnConfigSavePreValidationError_ViaMergeFlow() throws Exception { // User 1 loads page CruiseConfig user1SeeingConfig = goConfigDao.loadForEditing(); String user1SeeingMd5 = user1SeeingConfig.getMd5(); // User 2 edits config configHelper.addPipelineWithGroup("defaultGroup", "user2_pipeline", "user2_stage", "user2_job"); CruiseConfig user2SeeingConfig = configHelper.load(); // User 1 edits old config new GoConfigMother().addPipelineWithGroup(user1SeeingConfig, "defaultGroup", "user1_pipeline", "user1_stage", "user1_job"); ByteArrayOutputStream os = new ByteArrayOutputStream(); configHelper.getXml(user1SeeingConfig, os); // Introduce validation error on xml String xml = os.toString(); xml = xml.replace("user1_pipeline", "user1 pipeline"); // User 1 saves edited config GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false); GoConfigValidity validity = saver.saveXml(xml, user1SeeingMd5); assertThat(validity.isValid(), is(false)); // Pre throws VT_CONFLICT as user submitted xml is validated before attempting to save assertThat(validity.isType(GoConfigValidity.VT_CONFLICT), is(true)); assertThat(validity.errorMessage(), containsString("Name is invalid. \"user1 pipeline\"")); } @Test public void shouldThrowUpOnConfigSavePostValidationError_ViaMergeFlow() throws Exception { // User 1 adds a pipeline configHelper.addPipelineWithGroup("defaultGroup", "up_pipeline", "up_stage", "up_job"); configHelper.addPipelineWithGroup("anotherGroup", "random_pipeline", "random_stage", "random_job"); CruiseConfig user1SeeingConfig = configHelper.load(); String user1SeeingMd5 = user1SeeingConfig.getMd5(); // User 2 edits config configHelper.removePipeline("up_pipeline"); // User 1 edits old config MaterialConfigs materialConfigs = new MaterialConfigs(); materialConfigs.add(MaterialConfigsMother.dependencyMaterialConfig("up_pipeline", "up_stage")); new GoConfigMother().addPipelineWithGroup(user1SeeingConfig, "anotherGroup", "down_pipeline", materialConfigs, "down_stage", "down_job"); ByteArrayOutputStream os = new ByteArrayOutputStream(); configHelper.getXml(user1SeeingConfig, os); // User 1 saves edited config String xml = os.toString(); GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false); GoConfigValidity validity = saver.saveXml(xml, user1SeeingMd5); assertThat(validity.isValid(), is(false)); assertThat(validity.toString(), validity.isType(GoConfigValidity.VT_MERGE_POST_VALIDATION_ERROR), is(true)); assertThat(validity.errorMessage(), is("Pipeline \"up_pipeline\" does not exist. It is used from pipeline \"down_pipeline\".")); } @Test public void shouldThrowUpOnConfigSaveValidationError_ViaNormalFlow() throws Exception { // User 1 loads page CruiseConfig user1SeeingConfig = goConfigDao.loadForEditing(); String user1SeeingMd5 = user1SeeingConfig.getMd5(); // User 1 edits old config new GoConfigMother().addPipelineWithGroup(user1SeeingConfig, "defaultGroup", "user1_pipeline", "user1_stage", "user1_job"); ByteArrayOutputStream os = new ByteArrayOutputStream(); configHelper.getXml(user1SeeingConfig, os); // Introduce validation error on xml String xml = os.toString(); xml = xml.replace("user1_pipeline", "user1 pipeline"); // User 1 saves edited config GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false); GoConfigValidity validity = saver.saveXml(xml, user1SeeingMd5); assertThat(validity.isValid(), is(false)); assertThat(validity.isType(GoConfigValidity.VT_CONFLICT), is(true)); assertThat(validity.errorMessage(), containsString("Name is invalid. \"user1 pipeline\"")); } @Test public void shouldInternallyGetGoConfigInvalidExceptionOnValidationErrorAndFailWithATopLevelConfigError() throws Exception { String oldMd5 = goConfigService.getConfigForEditing().getMd5(); CruiseConfig user1SeeingConfig = configHelper.load(); // Setup a pipeline group in the config new GoConfigMother().addPipelineWithGroup(user1SeeingConfig, "defaultGroup", "user1_pipeline", "user1_stage", "user1_job"); ByteArrayOutputStream os = new ByteArrayOutputStream(); configHelper.getXml(user1SeeingConfig, os); GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false); saver.saveXml(os.toString(), oldMd5); CruiseConfig configBeforePipelineGroupWasAddedAtBeginning = configHelper.load(); String md5BeforeAddingGroupAtBeginning = configBeforePipelineGroupWasAddedAtBeginning.getMd5(); // User 1 edits config XML and adds a pipeline group before the first group in config String configXMLWithGroupAddedAtBeginning = os.toString().replace("</pipelines>", "</pipelines><pipelines group=\"first_group\"/>"); saver.saveXml(configXMLWithGroupAddedAtBeginning, md5BeforeAddingGroupAtBeginning); // User 2 adds another pipeline group, with the same name, through UI, but using the older MD5. HttpLocalizedOperationResult result = new HttpLocalizedOperationResult(); ConfigUpdateResponse response = goConfigService.updateConfigFromUI(new UpdateConfigFromUI() { public void checkPermission(CruiseConfig cruiseConfig, LocalizedOperationResult result) { } public Validatable node(CruiseConfig cruiseConfig) { return cruiseConfig; } public Validatable updatedNode(CruiseConfig cruiseConfig) { return node(cruiseConfig); } public void update(Validatable config) { CruiseConfig cruiseConfig = (CruiseConfig) config; MaterialConfigs materials = new MaterialConfigs(MaterialConfigsMother.mockMaterialConfigs("file:///tmp/foo")); new GoConfigMother().addPipelineWithGroup(cruiseConfig, "first_group", "up_pipeline", materials, "down_stage", "down_job"); } public Validatable subject(Validatable node) { return node; } public Validatable updatedSubject(Validatable updatedNode) { return subject(updatedNode); } }, md5BeforeAddingGroupAtBeginning, new Username(new CaseInsensitiveString("admin")), result); CruiseConfig config = response.getCruiseConfig(); assertThat(config.getMd5(), is(md5BeforeAddingGroupAtBeginning)); assertThat(result.isSuccessful(), is(false)); assertThat(result.httpCode(), is(SC_CONFLICT)); assertThat(result.message(localizer), is("Save failed. Duplicate unique value [first_group] declared for identity constraint \"uniquePipelines\" of element \"cruise\".")); } @Test public void shouldNotThrowUpOnConfigSaveWhenIndependentChangesAreMade_ViaMergeFlow() throws Exception { // Priming current configuration to add lines simulating the license section before removal for (int i = 0; i < 10; i++) { configHelper.addRole(new RoleConfig(new CaseInsensitiveString("admin_role_" + i), new RoleUser(new CaseInsensitiveString("admin_user_" + i)))); } // User 1 loads page CruiseConfig user1SeeingConfig = goConfigDao.loadForEditing(); String user1SeeingMd5 = user1SeeingConfig.getMd5(); // User 2 edits config configHelper.addPipelineWithGroup("defaultGroup", "user2_pipeline", "user2_stage", "user2_job"); CruiseConfig user2SeeingConfig = configHelper.load(); // User 1 edits old config to make an independent change new GoConfigMother().addRole(user1SeeingConfig, new RoleConfig(new CaseInsensitiveString("admin_role"), new RoleUser(new CaseInsensitiveString("admin_user")))); ByteArrayOutputStream os = new ByteArrayOutputStream(); configHelper.getXml(user1SeeingConfig, os); // User 1 saves edited config GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false); GoConfigValidity validity = saver.saveXml(os.toString(), user1SeeingMd5); assertThat(validity.errorMessage(), validity.isValid(), is(true)); } @Test public void shouldNotThrowUpOnConfigSave_ViaNormalFlow() throws Exception { // User 1 loads page CruiseConfig user1SeeingConfig = goConfigDao.loadForEditing(); // User 2 edits config configHelper.addPipelineWithGroup("defaultGroup", "user2_pipeline", "user2_stage", "user2_job"); CruiseConfig user2SeeingConfig = configHelper.load(); String user2SeeingMd5 = user2SeeingConfig.getMd5(); // User 1 edits new config new GoConfigMother().addPipelineWithGroup(user2SeeingConfig, "defaultGroup", "user1_pipeline", "user1_stage", "user1_job"); ByteArrayOutputStream os = new ByteArrayOutputStream(); configHelper.getXml(user2SeeingConfig, os); // User 1 saves edited config GoConfigService.XmlPartialSaver saver = goConfigService.fileSaver(false); GoConfigValidity validity = saver.saveXml(os.toString(), user2SeeingMd5); assertThat(validity.isValid(), is(true)); } private void setJobTimeoutTo(final String jobTimeout) { CruiseConfig config = configHelper.currentConfig(); config.server().setJobTimeout(jobTimeout); configHelper.writeConfigFile(config); } private void assertFailedResult(HttpLocalizedOperationResult result, final String message) { assertThat(result.isSuccessful(), is(false)); assertThat(result.message(localizer), is(message)); } private void assertStageError(StageConfig duplicatedStage, final String message, final String field) { assertThat(duplicatedStage.errors().isEmpty(), is(false)); assertThat(duplicatedStage.errors().on(field), is(message)); } private static class AddStageToPipelineCommand implements UpdateConfigFromUI { public String stageName; public AddStageToPipelineCommand(String stageName) { this.stageName = stageName; } public void checkPermission(CruiseConfig cruiseConfig, LocalizedOperationResult result) { } public Validatable node(CruiseConfig cruiseConfig) { return cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline")); } public Validatable updatedNode(CruiseConfig cruiseConfig) { return node(cruiseConfig); } public void update(Validatable node) { PipelineConfig pipeline = (PipelineConfig) node; pipeline.addStageWithoutValidityAssertion(StageConfigMother.custom(stageName, "job")); } public Validatable subject(Validatable node) { return node; } public Validatable updatedSubject(Validatable updatedNode) { return subject(updatedNode); } } private static class PipelineStageRenamingCommand implements UpdateConfigFromUI { protected String newPipelineName = "new-pipeline"; public void checkPermission(CruiseConfig cruiseConfig, LocalizedOperationResult result) { } public Validatable node(CruiseConfig cruiseConfig) { return cruiseConfig.pipelineConfigByName(new CaseInsensitiveString("pipeline")); } public Validatable updatedNode(CruiseConfig cruiseConfig) { return cruiseConfig.pipelineConfigByName(new CaseInsensitiveString(newPipelineName)); } public void update(Validatable pipelineNode) { PipelineConfig pipeline = (PipelineConfig) pipelineNode; ReflectionUtil.setField(pipeline, "name", new CaseInsensitiveString(newPipelineName)); ReflectionUtil.setField(pipeline.getStage(new CaseInsensitiveString("stage")), "name", new CaseInsensitiveString("new-stage")); } private StageConfig getStage(Validatable pipelineNode, String stageName) { PipelineConfig pipeline = (PipelineConfig) pipelineNode; return pipeline.getStage(new CaseInsensitiveString(stageName)); } public Validatable subject(Validatable pipelineNode) { return getStage(pipelineNode, "stage"); } public Validatable updatedSubject(Validatable pipelineNode) { return getStage(pipelineNode, "new-stage"); } } }