Java tutorial
/*************************GO-LICENSE-START********************************* * Copyright 2014 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. *************************GO-LICENSE-END***********************************/ package com.thoughtworks.go.config; import com.thoughtworks.go.config.exceptions.ConfigFileHasChangedException; import com.thoughtworks.go.config.exceptions.ConfigMergeException; import com.thoughtworks.go.config.materials.svn.SvnMaterialConfig; import com.thoughtworks.go.config.materials.tfs.TfsMaterialConfig; import com.thoughtworks.go.config.registry.ConfigElementImplementationRegistry; import com.thoughtworks.go.domain.GoConfigRevision; import com.thoughtworks.go.helper.ConfigFileFixture; import com.thoughtworks.go.helper.NoOpMetricsProbeService; import com.thoughtworks.go.helper.PipelineConfigMother; import com.thoughtworks.go.helper.StageConfigMother; import com.thoughtworks.go.metrics.service.MetricsProbeService; import com.thoughtworks.go.server.util.ServerVersion; import com.thoughtworks.go.serverhealth.ServerHealthService; import com.thoughtworks.go.service.ConfigRepository; import com.thoughtworks.go.util.*; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.hamcrest.core.Is; import org.joda.time.DateTime; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.security.GrantedAuthority; import org.springframework.security.context.SecurityContext; import org.springframework.security.context.SecurityContextHolder; import org.springframework.security.providers.UsernamePasswordAuthenticationToken; import org.springframework.security.userdetails.User; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Date; import java.util.List; import java.util.Vector; import static com.thoughtworks.go.helper.ConfigFileFixture.VALID_XML_3169; import static com.thoughtworks.go.util.GoConfigFileHelper.loadAndMigrate; import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.nullValue; import static org.hamcrest.core.StringContains.containsString; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class GoConfigDataSourceTest { private GoConfigDataSource dataSource; private GoConfigFileHelper configHelper; private SystemEnvironment systemEnvironment; private ConfigRepository configRepository; private TimeProvider timeProvider; private ConfigCache configCache = new ConfigCache(); private MetricsProbeService metricsProbeService = new NoOpMetricsProbeService(); private GoConfigFileDao goConfigFileDao; @Before public void setup() throws Exception { systemEnvironment = new SystemEnvironment(); configHelper = new GoConfigFileHelper(); configHelper.onSetUp(); configRepository = new ConfigRepository(systemEnvironment); configRepository.initialize(); timeProvider = mock(TimeProvider.class); when(timeProvider.currentTime()).thenReturn(new Date()); ServerVersion serverVersion = new ServerVersion(); ConfigElementImplementationRegistry registry = ConfigElementImplementationRegistryMother.withNoPlugins(); dataSource = new GoConfigDataSource(new GoConfigMigration(new GoConfigMigration.UpgradeFailedHandler() { public void handle(Exception e) { throw new RuntimeException(e); } }, configRepository, new TimeProvider(), configCache, registry, metricsProbeService), configRepository, systemEnvironment, timeProvider, configCache, serverVersion, registry, metricsProbeService, mock(ServerHealthService.class)); dataSource.upgradeIfNecessary(); CachedGoConfig cachedConfigService = new CachedGoConfig(dataSource, new ServerHealthService()); cachedConfigService.loadConfigIfNull(); goConfigFileDao = new GoConfigFileDao(cachedConfigService, metricsProbeService); configHelper.load(); } @After public void teardown() throws Exception { configHelper.onTearDown(); systemEnvironment.reset(SystemEnvironment.ENABLE_CONFIG_MERGE_FEATURE); } private static class UserAwarePipelineAddingCommand implements UpdateConfigCommand, UserAware { private final String pipelineName; private final String username; UserAwarePipelineAddingCommand(String pipelineName, String username) { this.pipelineName = pipelineName; this.username = username; } public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.addPipeline("my-grp", PipelineConfigMother.createPipelineConfig(pipelineName, "stage-other", "job-yet-another")); return cruiseConfig; } public ConfigModifyingUser user() { return new ConfigModifyingUser(username); } } @Test public void shouldUse_UserFromSession_asConfigModifyingUserWhenNoneGiven() throws GitAPIException, IOException { SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(new UsernamePasswordAuthenticationToken( new User("loser_boozer", "pass", true, true, true, true, new GrantedAuthority[] {}), null)); goConfigFileDao.updateMailHost(getMailHost("mailhost.local")); CruiseConfig cruiseConfig = goConfigFileDao.load(); GoConfigRevision revision = configRepository.getRevision(cruiseConfig.getMd5()); assertThat(revision.getUsername(), is("loser_boozer")); } @Test public void shouldVersionTheCruiseConfigXmlWhenSaved() throws Exception { CachedGoConfig cachedGoConfig = configHelper.getCachedGoConfig(); CruiseConfig configForEdit = cachedGoConfig.loadForEditing(); GoConfigHolder configHolder = new GoConfigHolder(cachedGoConfig.currentConfig(), configForEdit); Date loserChangedAt = new DateTime().plusDays(2).toDate(); when(timeProvider.currentTime()).thenReturn(loserChangedAt); GoConfigHolder afterFirstSave = dataSource .writeWithLock(new UserAwarePipelineAddingCommand("foo-pipeline", "loser"), configHolder) .getConfigHolder(); Date biggerLoserChangedAt = new DateTime().plusDays(4).toDate(); when(timeProvider.currentTime()).thenReturn(biggerLoserChangedAt); GoConfigHolder afterSecondSave = dataSource .writeWithLock(new UserAwarePipelineAddingCommand("bar-pipeline", "bigger_loser"), afterFirstSave) .getConfigHolder(); String expectedMd5 = afterFirstSave.config.getMd5(); GoConfigRevision firstRev = configRepository.getRevision(expectedMd5); assertThat(firstRev.getUsername(), is("loser")); assertThat(firstRev.getGoVersion(), is("N/A")); assertThat(firstRev.getMd5(), is(expectedMd5)); assertThat(firstRev.getTime(), is(loserChangedAt)); assertThat(firstRev.getSchemaVersion(), is(GoConstants.CONFIG_SCHEMA_VERSION)); assertThat(com.thoughtworks.go.config.ConfigMigrator.load(firstRev.getContent()), is(afterFirstSave.configForEdit)); CruiseConfig config = afterSecondSave.config; assertThat(config.hasPipelineNamed(new CaseInsensitiveString("bar-pipeline")), is(true)); expectedMd5 = config.getMd5(); GoConfigRevision secondRev = configRepository.getRevision(expectedMd5); assertThat(secondRev.getUsername(), is("bigger_loser")); assertThat(secondRev.getGoVersion(), is("N/A")); assertThat(secondRev.getMd5(), is(expectedMd5)); assertThat(secondRev.getTime(), is(biggerLoserChangedAt)); assertThat(secondRev.getSchemaVersion(), is(GoConstants.CONFIG_SCHEMA_VERSION)); assertThat(com.thoughtworks.go.config.ConfigMigrator.load(secondRev.getContent()), is(afterSecondSave.configForEdit)); } @Test public void shouldSaveTheCruiseConfigXml() throws Exception { File file = dataSource.fileLocation(); dataSource.write(ConfigMigrator.migrate(VALID_XML_3169), false); assertThat(FileUtils.readFileToString(file), containsString("http://hg-server/hg/connectfour")); } @Test public void shouldNotCorruptTheCruiseConfigXml() throws Exception { File file = dataSource.fileLocation(); String originalCopy = FileUtils.readFileToString(file); try { dataSource.write("abc", false); fail("Should not allow us to write an invalid config"); } catch (Exception e) { assertThat(e.getMessage(), containsString("Content is not allowed in prolog")); } assertThat(FileUtils.readFileToString(file), Is.is(originalCopy)); } @Test public void shouldLoadAsUser_Filesystem_WithMd5Sum() throws Exception { GoConfigHolder configHolder = goConfigFileDao.loadConfigHolder(); String md5 = DigestUtils.md5Hex(FileUtils.readFileToString(dataSource.fileLocation())); assertThat(configHolder.configForEdit.getMd5(), is(md5)); assertThat(configHolder.config.getMd5(), is(md5)); CruiseConfig forEdit = configHolder.configForEdit; forEdit.addPipeline("my-awesome-group", PipelineConfigMother.createPipelineConfig("pipeline-foo", "stage-bar", "job-baz")); FileOutputStream fos = new FileOutputStream(dataSource.fileLocation()); new MagicalGoConfigXmlWriter(configCache, ConfigElementImplementationRegistryMother.withNoPlugins(), metricsProbeService).write(forEdit, fos, false); configHolder = dataSource.load(); String xmlText = FileUtils.readFileToString(dataSource.fileLocation()); String secondMd5 = DigestUtils.md5Hex(xmlText); assertThat(configHolder.configForEdit.getMd5(), is(secondMd5)); assertThat(configHolder.config.getMd5(), is(secondMd5)); assertThat(configHolder.configForEdit.getMd5(), is(not(md5))); GoConfigRevision commitedVersion = configRepository.getRevision(secondMd5); assertThat(commitedVersion.getContent(), is(xmlText)); assertThat(commitedVersion.getUsername(), is(GoConfigDataSource.FILESYSTEM)); } @Test public void shouldEncryptSvnPasswordWhenConfigIsChangedViaFileSystem() throws Exception { String configContent = ConfigFileFixture.configWithPipeline( String.format("<pipeline name='pipeline1'>" + " <materials>" + " <svn url='svnurl' username='admin' password='%s'/>" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", "hello"), GoConstants.CONFIG_SCHEMA_VERSION); FileUtils.writeStringToFile(dataSource.fileLocation(), configContent); GoConfigHolder configHolder = dataSource.load(); PipelineConfig pipelineConfig = configHolder.config .pipelineConfigByName(new CaseInsensitiveString("pipeline1")); SvnMaterialConfig svnMaterialConfig = (SvnMaterialConfig) pipelineConfig.materialConfigs().get(0); assertThat(svnMaterialConfig.getEncryptedPassword(), is(not(nullValue()))); } @Test public void shouldEncryptTfsPasswordWhenConfigIsChangedViaFileSystem() throws Exception { String configContent = ConfigFileFixture.configWithPipeline(String.format("<pipeline name='pipeline1'>" + " <materials>" + " <tfs url='http://some.repo.local' username='username@domain' password='password' projectPath='$/project_path' />" + " </materials>" + " <stage name='mingle'>" + " <jobs>" + " <job name='do-something'>" + " </job>" + " </jobs>" + " </stage>" + "</pipeline>", "hello"), GoConstants.CONFIG_SCHEMA_VERSION); FileUtils.writeStringToFile(dataSource.fileLocation(), configContent); GoConfigHolder configHolder = dataSource.load(); PipelineConfig pipelineConfig = configHolder.config .pipelineConfigByName(new CaseInsensitiveString("pipeline1")); TfsMaterialConfig tfsMaterial = (TfsMaterialConfig) pipelineConfig.materialConfigs().get(0); assertThat(tfsMaterial.getEncryptedPassword(), is(not(nullValue()))); } @Test public void shouldNotReloadIfConfigDoesNotChange() throws Exception { LogFixture log = LogFixture.startListening(); dataSource.reloadIfModified(); GoConfigHolder loadedConfig = dataSource.load(); assertThat(log.getLog(), containsString("Config file changed at")); assertThat(loadedConfig, not(nullValue())); log.clear(); loadedConfig = dataSource.load(); assertThat(log.getLog(), not(containsString("Config file changed at"))); assertThat(loadedConfig, is(nullValue())); } @Test public void shouldUpdateFileAttributesIfFileContentsHaveNotChanged() throws Exception {//so that it doesn't have to do the file content checksum computation next time dataSource.reloadIfModified(); assertThat(dataSource.load(), not(nullValue())); GoConfigDataSource.ReloadIfModified reloadStrategy = (GoConfigDataSource.ReloadIfModified) ReflectionUtil .getField(dataSource, "reloadStrategy"); ReflectionUtil.setField(reloadStrategy, "lastModified", -1); ReflectionUtil.setField(reloadStrategy, "prevSize", -1); assertThat(dataSource.load(), is(nullValue())); assertThat((Long) ReflectionUtil.getField(reloadStrategy, "lastModified"), is(dataSource.fileLocation().lastModified())); assertThat((Long) ReflectionUtil.getField(reloadStrategy, "prevSize"), is(dataSource.fileLocation().length())); } @Test public void shouldBeAbleToConcurrentAccess() throws Exception { GoConfigFileHelper helper = new GoConfigFileHelper( loadAndMigrate(ConfigFileFixture.CONFIG_WITH_NANT_AND_EXEC_BUILDER)); final String xml = FileUtil.readContentFromFile(helper.getConfigFile()); final List<Exception> errors = new Vector<Exception>(); Thread thread1 = new Thread(new Runnable() { public void run() { for (int i = 0; i < 5; i++) { try { goConfigFileDao.updateMailHost(new MailHost("hostname", 9999, "user", "password", false, false, "from@local", "admin@local")); } catch (Exception e) { e.printStackTrace(); errors.add(e); } } } }, "Update-license"); Thread thread2 = new Thread(new Runnable() { public void run() { for (int i = 0; i < 5; i++) { try { dataSource.write(xml, false); } catch (Exception e) { e.printStackTrace(); errors.add(e); } } } }, "Modify-config"); thread1.start(); thread2.start(); thread1.join(); thread2.join(); assertThat(errors.size(), is(0)); } @Test public void shouldGetMergedConfig() throws Exception { configHelper.addMailHost(getMailHost("mailhost.local.old")); GoConfigHolder goConfigHolder = dataSource.forceLoad(dataSource.fileLocation()); CruiseConfig oldConfigForEdit = goConfigHolder.configForEdit; final String oldMD5 = oldConfigForEdit.getMd5(); MailHost oldMailHost = oldConfigForEdit.server().mailHost(); assertThat(oldMailHost.getHostName(), is("mailhost.local.old")); assertThat(oldMailHost.getHostName(), is(not("mailhost.local"))); goConfigFileDao.updateMailHost(getMailHost("mailhost.local")); goConfigHolder = dataSource.forceLoad(dataSource.fileLocation()); GoConfigDataSource.GoConfigSaveResult result = dataSource .writeWithLock(new NoOverwriteUpdateConfigCommand() { @Override public String unmodifiedMd5() { return oldMD5; } @Override public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception { cruiseConfig.addPipeline("g", PipelineConfigMother.pipelineConfig("p1", StageConfigMother.custom("s", "b"))); return cruiseConfig; } }, goConfigHolder); assertThat(result.getConfigHolder().config.server().mailHost().getHostName(), is("mailhost.local")); assertThat(result.getConfigHolder().config.hasPipelineNamed(new CaseInsensitiveString("p1")), is(true)); } @Test public void shouldPropagateConfigHasChangedException() throws Exception { String originalMd5 = dataSource.forceLoad(dataSource.fileLocation()).configForEdit.getMd5(); goConfigFileDao.updateConfig(configHelper.addPipelineCommand(originalMd5, "p1", "s1", "b1")); GoConfigHolder goConfigHolder = dataSource.forceLoad(dataSource.fileLocation()); try { dataSource.writeWithLock(configHelper.addPipelineCommand(originalMd5, "p2", "s", "b"), goConfigHolder); fail("Should throw ConfigFileHasChanged exception"); } catch (Exception e) { assertThat(e.getCause().getClass().getName(), e.getCause() instanceof ConfigMergeException, is(true)); } } @Test public void shouldThrowConfigMergeExceptionWhenConfigMergeFeatureIsTurnedOff() throws Exception { String firstMd5 = dataSource.forceLoad(dataSource.fileLocation()).configForEdit.getMd5(); goConfigFileDao.updateConfig(configHelper.addPipelineCommand(firstMd5, "p0", "s0", "b0")); String originalMd5 = dataSource.forceLoad(dataSource.fileLocation()).configForEdit.getMd5(); goConfigFileDao.updateConfig(configHelper.addPipelineCommand(originalMd5, "p1", "s1", "j1")); GoConfigHolder goConfigHolder = dataSource.forceLoad(dataSource.fileLocation()); systemEnvironment.set(SystemEnvironment.ENABLE_CONFIG_MERGE_FEATURE, Boolean.FALSE); try { dataSource.writeWithLock(configHelper.changeJobNameCommand(originalMd5, "p0", "s0", "b0", "j0"), goConfigHolder); fail("Should throw ConfigMergeException"); } catch (RuntimeException e) { ConfigMergeException cme = (ConfigMergeException) e.getCause(); assertThat(cme.getMessage(), is(ConfigFileHasChangedException.CONFIG_CHANGED_PLEASE_REFRESH)); } } @Test public void shouldGetConfigMergedStateWhenAMergerOccurs() throws Exception { configHelper.addMailHost(getMailHost("mailhost.local.old")); String originalMd5 = dataSource.forceLoad(dataSource.fileLocation()).configForEdit.getMd5(); configHelper.addMailHost(getMailHost("mailhost.local")); GoConfigHolder goConfigHolder = dataSource.forceLoad(dataSource.fileLocation()); GoConfigDataSource.GoConfigSaveResult goConfigSaveResult = dataSource .writeWithLock(configHelper.addPipelineCommand(originalMd5, "p1", "s", "b"), goConfigHolder); assertThat(goConfigSaveResult.getConfigSaveState(), is(ConfigSaveState.MERGED)); } private MailHost getMailHost(String hostName) { return new MailHost(hostName, 9999, "user", "password", true, false, "from@local", "admin@local"); } @Test public void shouldGetConfigUpdateStateWhenAnUpdateOccurs() throws Exception { String originalMd5 = dataSource.forceLoad(dataSource.fileLocation()).configForEdit.getMd5(); GoConfigHolder goConfigHolder = dataSource.forceLoad(dataSource.fileLocation()); GoConfigDataSource.GoConfigSaveResult goConfigSaveResult = dataSource .writeWithLock(configHelper.addPipelineCommand(originalMd5, "p1", "s", "b"), goConfigHolder); assertThat(goConfigSaveResult.getConfigSaveState(), is(ConfigSaveState.UPDATED)); } }