/*
* 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.support.toggle;
import com.google.gson.Gson;
import com.googlecode.junit.ext.JunitExtRunner;
import com.thoughtworks.go.server.domain.support.toggle.FeatureToggle;
import com.thoughtworks.go.server.domain.support.toggle.FeatureToggles;
import com.thoughtworks.go.util.ListUtil;
import com.thoughtworks.go.util.SystemEnvironment;
import com.thoughtworks.go.util.TestFileUtil;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import java.io.File;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@RunWith(JunitExtRunner.class)
public class FeatureToggleRepositoryTest {
public static final String TEST_AVAILABLE_TOGGLES_PATH = "/available.test.toggles";
@Mock
private SystemEnvironment environment;
@Before
public void setUp() throws Exception {
initMocks(this);
}
@After
public void tearDown() throws Exception {
FileUtils.writeStringToFile(availableTogglesFile(), "");
TestFileUtil.cleanTempFiles();
}
@Test
public void shouldReadFeatureTogglesFromAvailableTogglesFile() throws Exception {
FeatureToggle featureToggle1 = new FeatureToggle("key1", "desc1", true);
FeatureToggle featureToggle2 = new FeatureToggle("key2", "desc2", false);
setupAvailableToggles(featureToggle1, featureToggle2);
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
assertThat(repository.availableToggles(), is(new FeatureToggles(featureToggle1, featureToggle2)));
}
@Test
public void shouldNotFailWhenSpecifiedAvailableTogglesFileIsNotFound() throws Exception {
setupAvailableToggleFileAs("a-non-existent-file");
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
assertThat(repository.availableToggles(), is(new FeatureToggles()));
}
@Test
public void shouldNotFailWhenContentOfAvailableTogglesFileIsInvalid() throws Exception {
setupAvailableToggleFileAs(TEST_AVAILABLE_TOGGLES_PATH);
FileUtils.writeStringToFile(availableTogglesFile(), "SOME-INVALID-CONTENT");
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
assertThat(repository.availableToggles(), is(new FeatureToggles()));
}
@Test
public void shouldReadFeatureTogglesFromUsersTogglesFile() throws Exception {
FeatureToggle featureToggle1 = new FeatureToggle("key1", "desc1", true);
FeatureToggle featureToggle2 = new FeatureToggle("key2", "desc2", false);
setupUserToggles(featureToggle1, featureToggle2);
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
assertThat(repository.userToggles(), is(new FeatureToggles(featureToggle1, featureToggle2)));
}
@Test
public void shouldNotFailWhenSpecifiedUserTogglesFileIsNotFound() throws Exception {
setupUserToggleFileAs(new File("a-non-existent-file"));
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
assertThat(repository.userToggles(), is(new FeatureToggles()));
}
@Test
public void shouldNotFailWhenContentOfUserTogglesFileIsInvalid() throws Exception {
File toggleFile = TestFileUtil.createTempFile("available.toggle.test");
FileUtils.writeStringToFile(toggleFile, "SOME-INVALID-CONTENT");
setupUserToggleFileAs(toggleFile);
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
assertThat(repository.userToggles(), is(new FeatureToggles()));
}
@Test
public void shouldAllowChangingValueOfAToggleWhenTheUserTogglesFileDoesNotExist() throws Exception {
File togglesDir = TestFileUtil.createTempFolder("toggles.dir");
File nonExistentUserToggleFile = new File(togglesDir, "a-non-existent-file");
setupUserToggleFileAs(nonExistentUserToggleFile);
setupAvailableToggles(new FeatureToggle("key1", "desc1", true));
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
repository.changeValueOfToggle("key1", false);
assertThat(repository.availableToggles(), is(new FeatureToggles(new FeatureToggle("key1", "desc1", true))));
assertThat(repository.userToggles(), is(new FeatureToggles(new FeatureToggle("key1", null, false))));
}
@Test
public void shouldAllowChangingValueOfAToggleWhenTheUserTogglesFileDoesExist() throws Exception {
setupAvailableToggles(new FeatureToggle("key1", "desc1", true), new FeatureToggle("key2", "desc2", true));
setupUserToggles(new FeatureToggle("key1", "desc1", true));
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
repository.changeValueOfToggle("key1", false);
assertThat(repository.availableToggles(), is(new FeatureToggles(new FeatureToggle("key1", "desc1", true), new FeatureToggle("key2", "desc2", true))));
assertThat(repository.userToggles(), is(new FeatureToggles(new FeatureToggle("key1", "desc1", false))));
}
@Test
public void shouldFailWhenUnableToWriteToUserTogglesFile_DuringChangingOfAToggleValue() throws Exception {
setupAvailableToggles(new FeatureToggle("key1", "desc1", true));
File userTogglesFile = setupUserToggles(new FeatureToggle("key1", "desc1", true));
userTogglesFile.setReadOnly();
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
try {
repository.changeValueOfToggle("key1", false);
fail("Should have failed to write");
} catch (RuntimeException e) {
assertThat(e.getMessage(), containsString(userTogglesFile.getPath()));
}
}
@Test
public void whileChangingAToggleValue_shouldNotPersist_ValueHasBeenChangedFlag() throws Exception {
String fieldForHasBeenChangedFlag = "hasBeenChangedFromDefault";
assertNotNull("This can never be null, but can throw an exception. If you've renamed the field mentioned above" +
"(in FeatureToggle class), please change it in this test too. Otherwise, this test can pass, wrongly.",
FeatureToggle.class.getDeclaredField(fieldForHasBeenChangedFlag));
setupAvailableToggles(new FeatureToggle("key1", "desc1", true));
File userTogglesFile = setupUserToggles(new FeatureToggle("key1", "desc1", false).withValueHasBeenChangedFlag(true));
FeatureToggleRepository repository = new FeatureToggleRepository(environment);
repository.changeValueOfToggle("key1", false);
assertThat(repository.userToggles(), is(new FeatureToggles(new FeatureToggle("key1", "desc1", false).withValueHasBeenChangedFlag(false))));
assertThat(FileUtils.readFileToString(userTogglesFile), containsString("key1"));
assertThat(FileUtils.readFileToString(userTogglesFile), containsString("desc1"));
assertThat(FileUtils.readFileToString(userTogglesFile), not(containsString(fieldForHasBeenChangedFlag)));
/* The first time the file is written, it is written by hand in this test. Force it to write again,
* so that the actual JSON write logic is used.
*/
repository.changeValueOfToggle("key1", true);
assertThat(repository.userToggles(), is(new FeatureToggles(new FeatureToggle("key1", "desc1", true).withValueHasBeenChangedFlag(false))));
assertThat(FileUtils.readFileToString(userTogglesFile), containsString("key1"));
assertThat(FileUtils.readFileToString(userTogglesFile), containsString("desc1"));
assertThat(FileUtils.readFileToString(userTogglesFile), not(containsString(fieldForHasBeenChangedFlag)));
}
@Test
public void ensureThatTheRealTogglesFileIsValid() throws Exception {
String realAvailableTogglesFilePath = new SystemEnvironment().get(SystemEnvironment.AVAILABLE_FEATURE_TOGGLES_FILE_PATH);
File realAvailableTogglesFile = new File(getClass().getResource(realAvailableTogglesFilePath).toURI());
String currentContentOfRealAvailableTogglesFile = FileUtils.readFileToString(realAvailableTogglesFile);
try {
new Gson().fromJson(currentContentOfRealAvailableTogglesFile, FeatureToggleRepository.FeatureToggleFileContentRepresentation.class);
} catch (Exception e) {
fail("Check contents of " + realAvailableTogglesFilePath + ". Contents should be valid and be equivalent" +
" to FeatureToggleRepository.FeatureToggleFileContentRepresentation.class. Contents were:\n" +
currentContentOfRealAvailableTogglesFile + "\n. Exception was: " + e.getMessage());
}
}
private void setupAvailableToggleFileAs(String fileResourcePath) {
when(environment.get(SystemEnvironment.AVAILABLE_FEATURE_TOGGLES_FILE_PATH)).thenReturn(fileResourcePath);
}
private void setupUserToggleFileAs(File file) {
when(environment.configDir()).thenReturn(file.getParentFile());
when(environment.get(SystemEnvironment.USER_FEATURE_TOGGLES_FILE_PATH_RELATIVE_TO_CONFIG_DIR)).thenReturn(file.getName());
}
private void setupAvailableToggles(FeatureToggle... toggles) throws Exception {
setupAvailableToggleFileAs(TEST_AVAILABLE_TOGGLES_PATH);
FileUtils.writeStringToFile(availableTogglesFile(), convertTogglesToJson(toggles));
}
private File setupUserToggles(FeatureToggle... toggles) throws Exception {
File toggleFile = TestFileUtil.createTempFile("user.toggle.test");
setupUserToggleFileAs(toggleFile);
FileUtils.writeStringToFile(toggleFile, convertTogglesToJson(toggles));
return toggleFile;
}
/* Write by hand to remove unnecessary coupling to actual write. */
private String convertTogglesToJson(FeatureToggle[] toggles) {
List<String> jsonContentForEachToggle = new ArrayList<>();
for (FeatureToggle toggle : toggles) {
jsonContentForEachToggle.add(MessageFormat.format(
"'{'\"key\": \"{0}\", \"description\": \"{1}\", \"value\": {2}'}'",
toggle.key(), toggle.description(), String.valueOf(toggle.isOn())));
}
return "{ \"version\": \"1\", \"toggles\": [" + ListUtil.join(jsonContentForEachToggle, ",").trim() + "]}";
}
private File availableTogglesFile() throws URISyntaxException {
return new File(getClass().getResource(TEST_AVAILABLE_TOGGLES_PATH).toURI());
}
}