mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-14 22:13:59 +02:00
Use i18n for cli exception (#1989)
For scm exceptions, use the translations from plugins.json in the cli rest endpoint.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.cli;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import picocli.CommandLine;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.TransactionId;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.i18n.I18nCollector;
|
||||
import sonia.scm.store.StoreReadOnlyException;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CliExceptionHandlerTest {
|
||||
|
||||
private static final String PLUGIN_BUNDLE_EN = "{" +
|
||||
" \"errors\": {" +
|
||||
" \"AGR7UzkhA1\": {" +
|
||||
" \"displayName\": \"Not found\"," +
|
||||
" \"description\": \"The requested entity could not be found. It may have been deleted in another session.\"" +
|
||||
" }," +
|
||||
" \"3FSIYtBJw1\": {" +
|
||||
" \"displayName\": \"An entity could not be stored\"" +
|
||||
" }" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
private static final String PLUGIN_BUNDLE_DE = "{" +
|
||||
" \"errors\": {" +
|
||||
" \"3FSIYtBJw1\": {" +
|
||||
" \"displayName\": \"Ein Datensatz konnte nicht gespeichert werden\"," +
|
||||
" \"description\": \"Ein Datensatz konnte nicht gespeichert werden, da der entsprechende Speicher als schreibgeschützt markiert wurde. Weitere Hinweise finden sich im Log.\"" +
|
||||
" }" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
@Mock
|
||||
private I18nCollector i18nCollector;
|
||||
|
||||
@Mock
|
||||
private CommandLine commandLine;
|
||||
|
||||
private CliExceptionHandler handler;
|
||||
|
||||
private final ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
|
||||
private final PrintWriter stdErr = new PrintWriter(errorStream);
|
||||
|
||||
@BeforeEach
|
||||
void initErrorStream() {
|
||||
when(commandLine.getErr()).thenReturn(stdErr);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class EnglishLanguageTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUpHandler() {
|
||||
handler = new CliExceptionHandler(i18nCollector, "en");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintSimpleException() {
|
||||
int exitCode = callHandler(new NullPointerException("expected error"));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Error: expected error");
|
||||
assertThat(exitCode).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintTransactionIdIfPresent() {
|
||||
TransactionId.set("42");
|
||||
|
||||
callHandler(new NullPointerException("expected error"));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Transaction ID: 42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseTranslatedMessageFromExceptionWithContext() throws IOException {
|
||||
when(i18nCollector.findJson("en"))
|
||||
.thenReturn(Optional.of(new ObjectMapper().readTree(PLUGIN_BUNDLE_EN)));
|
||||
|
||||
callHandler(NotFoundException.notFound(new ContextEntry.ContextBuilder()));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Error: The requested entity could not be found. It may have been deleted in another session.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseExceptionMessageForExceptionWithContextWithoutCodeNode() throws IOException {
|
||||
when(i18nCollector.findJson("en"))
|
||||
.thenReturn(Optional.of(new ObjectMapper().readTree(PLUGIN_BUNDLE_EN)));
|
||||
|
||||
callHandler(new AlreadyExistsException(new Group("test", "hog")));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Error: group with id hog already exists");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseExceptionMessageForExceptionWithContextWithoutDescriptionNode() throws IOException {
|
||||
when(i18nCollector.findJson("en"))
|
||||
.thenReturn(Optional.of(new ObjectMapper().readTree(PLUGIN_BUNDLE_EN)));
|
||||
|
||||
callHandler(new StoreReadOnlyException("test"));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Error: Store is read only, could not write location test");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GermanLanguageTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUpHandler() {
|
||||
handler = new CliExceptionHandler(i18nCollector, "de");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintSimpleException() {
|
||||
int exitCode = callHandler(new NullPointerException("expected error"));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Fehler: expected error");
|
||||
assertThat(exitCode).isEqualTo(1);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldPrintTransactionIdIfPresent() {
|
||||
TransactionId.set("42");
|
||||
|
||||
callHandler(new NullPointerException("expected error"));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Transaktions-ID: 42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseTranslatedMessageFromExceptionWithContext() throws IOException {
|
||||
when(i18nCollector.findJson("de"))
|
||||
.thenReturn(Optional.of(new ObjectMapper().readTree(PLUGIN_BUNDLE_DE)));
|
||||
|
||||
callHandler(new StoreReadOnlyException("test"));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Fehler: Ein Datensatz konnte nicht gespeichert werden, da der entsprechende Speicher als schreibgeschützt markiert wurde. Weitere Hinweise finden sich im Log.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseFallbackExceptionMessageForExceptionWithContextWithoutLocalizedCodeNode() throws IOException {
|
||||
when(i18nCollector.findJson("en"))
|
||||
.thenReturn(Optional.of(new ObjectMapper().readTree(PLUGIN_BUNDLE_EN)));
|
||||
when(i18nCollector.findJson("de"))
|
||||
.thenReturn(Optional.of(new ObjectMapper().readTree(PLUGIN_BUNDLE_DE)));
|
||||
|
||||
callHandler(NotFoundException.notFound(new ContextEntry.ContextBuilder()));
|
||||
|
||||
assertThat(errorStream.toString()).contains("Fehler: The requested entity could not be found. It may have been deleted in another session.");
|
||||
}
|
||||
}
|
||||
|
||||
private int callHandler(Exception ex) {
|
||||
try {
|
||||
return handler.handleExecutionException(ex, commandLine, null);
|
||||
} finally {
|
||||
stdErr.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ package sonia.scm.cli;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
@@ -53,60 +54,103 @@ class CliProcessorTest {
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private CliContext context;
|
||||
|
||||
@Mock
|
||||
private CliExceptionHandlerFactory exceptionHandlerFactory;
|
||||
@Mock
|
||||
private CliExceptionHandler exceptionHandler;
|
||||
|
||||
@BeforeEach
|
||||
void setDefaultLocale() {
|
||||
when(context.getLocale()).thenReturn(Locale.ENGLISH);
|
||||
@Nested
|
||||
class ForDefaultLanguageTest {
|
||||
|
||||
@BeforeEach
|
||||
void setDefaultLocale() {
|
||||
when(context.getLocale()).thenReturn(Locale.ENGLISH);
|
||||
when(exceptionHandlerFactory.createExceptionHandler("en")).thenReturn(exceptionHandler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecutePingCommand() {
|
||||
when(registry.createCommandTree()).thenReturn(Collections.singleton(new RegisteredCommandNode("ping", PingCommand.class)));
|
||||
Injector injector = Guice.createInjector();
|
||||
CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory);
|
||||
|
||||
cliProcessor.execute(context, "ping");
|
||||
|
||||
verify(context.getStdout()).println("PONG");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecutePingCommandWithExitCode0() {
|
||||
when(registry.createCommandTree()).thenReturn(Collections.singleton(new RegisteredCommandNode("ping", PingCommand.class)));
|
||||
Injector injector = Guice.createInjector();
|
||||
CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory);
|
||||
|
||||
int exitCode = cliProcessor.execute(context, "ping");
|
||||
|
||||
assertThat(exitCode).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintCommandOne() {
|
||||
String result = executeHierarchyCommands("--help");
|
||||
|
||||
assertThat(result).contains("Commands:\n" +
|
||||
" one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintCommandTwo() {
|
||||
String result = executeHierarchyCommands("one", "--help");
|
||||
|
||||
assertThat(result).contains("Commands:\n" +
|
||||
" two");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintCommandThree() {
|
||||
String result = executeHierarchyCommands("one", "two", "--help");
|
||||
|
||||
assertThat(result).contains("Commands:\n" +
|
||||
" three");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecutePingCommand() {
|
||||
when(registry.createCommandTree()).thenReturn(Collections.singleton(new RegisteredCommandNode("ping", PingCommand.class)));
|
||||
Injector injector = Guice.createInjector();
|
||||
CliProcessor cliProcessor = new CliProcessor(registry, injector);
|
||||
@Nested
|
||||
class ForAnotherLanguageTest {
|
||||
|
||||
cliProcessor.execute(context, "ping");
|
||||
@Mock
|
||||
private CliExceptionHandler germanExceptionHandler;
|
||||
|
||||
verify(context.getStdout()).println("PONG");
|
||||
}
|
||||
@BeforeEach
|
||||
void setUpOtherLanguage() {
|
||||
when(exceptionHandlerFactory.createExceptionHandler("de")).thenReturn(germanExceptionHandler);
|
||||
when(context.getLocale()).thenReturn(Locale.GERMAN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecutePingCommandWithExitCode0() {
|
||||
when(registry.createCommandTree()).thenReturn(Collections.singleton(new RegisteredCommandNode("ping", PingCommand.class)));
|
||||
Injector injector = Guice.createInjector();
|
||||
CliProcessor cliProcessor = new CliProcessor(registry, injector);
|
||||
@Test
|
||||
void shouldUseResourceBundleFromAnnotationWithContextLocale() {
|
||||
String helpForThree = executeHierarchyCommands("one", "two", "three", "--help");
|
||||
|
||||
int exitCode = cliProcessor.execute(context, "ping");
|
||||
assertThat(helpForThree).contains("Dies ist meine App.");
|
||||
}
|
||||
|
||||
assertThat(exitCode).isZero();
|
||||
}
|
||||
@Test
|
||||
void shouldUseDefaultWithoutResourceBundle() {
|
||||
String helpForTwo = executeHierarchyCommands("one", "two", "--help");
|
||||
|
||||
@Test
|
||||
void shouldPrintCommandOne() {
|
||||
String result = executeHierachyCommands("--help");
|
||||
assertThat(helpForTwo).contains("Dies ist meine App.");
|
||||
}
|
||||
|
||||
assertThat(result).contains("Commands:\n" +
|
||||
" one");
|
||||
}
|
||||
@Test
|
||||
void shouldUseExceptionHandlerForOtherLanguage() {
|
||||
executeHierarchyCommands("one", "two", "--help");
|
||||
|
||||
@Test
|
||||
void shouldPrintCommandTwo() {
|
||||
String result = executeHierachyCommands("one","--help");
|
||||
|
||||
assertThat(result).contains("Commands:\n" +
|
||||
" two");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintCommandThree() {
|
||||
String result = executeHierachyCommands("one", "two","--help");
|
||||
|
||||
assertThat(result).contains("Commands:\n" +
|
||||
" three");
|
||||
verify(exceptionHandlerFactory).createExceptionHandler("de");
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String executeHierachyCommands(String... args) {
|
||||
private String executeHierarchyCommands(String... args) {
|
||||
RegisteredCommandNode one = new RegisteredCommandNode("one", RootCommand.class);
|
||||
RegisteredCommandNode two = new RegisteredCommandNode("two", SubCommand.class);
|
||||
RegisteredCommandNode three = new RegisteredCommandNode("three", SubSubCommand.class);
|
||||
@@ -118,31 +162,12 @@ class CliProcessorTest {
|
||||
when(context.getStdout()).thenReturn(new PrintWriter(baos));
|
||||
|
||||
Injector injector = Guice.createInjector();
|
||||
CliProcessor cliProcessor = new CliProcessor(registry, injector);
|
||||
CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory);
|
||||
|
||||
cliProcessor.execute(context, args);
|
||||
return baos.toString();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldUseResourceBundleFromAnnotationWithContextLocale() {
|
||||
when(context.getLocale()).thenReturn(Locale.GERMAN);
|
||||
|
||||
String helpForThree = executeHierachyCommands("one", "two", "three", "--help");
|
||||
|
||||
assertThat(helpForThree).contains("Dies ist meine App.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDefaultWithoutResourceBundle() {
|
||||
when(context.getLocale()).thenReturn(Locale.GERMAN);
|
||||
|
||||
String helpForTwo = executeHierachyCommands("one", "two", "--help");
|
||||
|
||||
assertThat(helpForTwo).contains("Dies ist meine App.");
|
||||
}
|
||||
|
||||
@CommandLine.Command(name = "one")
|
||||
static class RootCommand implements Runnable {
|
||||
|
||||
|
||||
225
scm-webapp/src/test/java/sonia/scm/i18n/I18nCollectorTest.java
Normal file
225
scm-webapp/src/test/java/sonia/scm/i18n/I18nCollectorTest.java
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.i18n;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.util.JsonMerger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class I18nCollectorTest {
|
||||
|
||||
private static final String GIT_PLUGIN_JSON = json(
|
||||
"{",
|
||||
"'scm-git-plugin': {",
|
||||
"'information': {",
|
||||
"'clone' : 'Clone',",
|
||||
"'create' : 'Create',",
|
||||
"'replace' : 'Push'",
|
||||
"}",
|
||||
"}",
|
||||
"}"
|
||||
);
|
||||
|
||||
private static final String HG_PLUGIN_JSON = json(
|
||||
"{",
|
||||
"'scm-hg-plugin': {",
|
||||
"'information': {",
|
||||
"'clone' : 'Clone',",
|
||||
"'create' : 'Create',",
|
||||
"'replace' : 'Push'",
|
||||
"}",
|
||||
"}",
|
||||
"}"
|
||||
);
|
||||
|
||||
private static final String SVN_PLUGIN_JSON = json(
|
||||
"{",
|
||||
"'scm-svn-plugin': {",
|
||||
"'information': {",
|
||||
"'checkout' : 'Checkout'",
|
||||
"}",
|
||||
"}",
|
||||
"}"
|
||||
);
|
||||
|
||||
private static final String[] ALL_PLUGIN_JSON = new String[]{
|
||||
GIT_PLUGIN_JSON, HG_PLUGIN_JSON, SVN_PLUGIN_JSON
|
||||
};
|
||||
|
||||
private static String json(String... parts) {
|
||||
return String.join("\n", parts).replaceAll("'", "\"");
|
||||
}
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider context;
|
||||
|
||||
@Mock
|
||||
private PluginLoader pluginLoader;
|
||||
|
||||
@Mock
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@Mock
|
||||
private Cache<String, JsonNode> cache;
|
||||
|
||||
private I18nCollector collector;
|
||||
|
||||
@BeforeEach
|
||||
void mockCache() {
|
||||
when(cacheManager.<String, JsonNode>getCache(I18nCollector.CACHE_NAME)).thenReturn(cache);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithoutResourcesTest {
|
||||
|
||||
@BeforeEach
|
||||
void mockClassLoader(@TempDir Path directory) throws IOException {
|
||||
mockUberClassLoader(directory);
|
||||
createCollector();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyForMissingLanguage() throws IOException {
|
||||
Optional<JsonNode> json = collector.findJson("de");
|
||||
assertThat(json).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithResourcesTest {
|
||||
|
||||
@BeforeEach
|
||||
void mockClassLoader(@TempDir Path directory) throws IOException {
|
||||
mockResources(directory);
|
||||
createCollector();
|
||||
}
|
||||
|
||||
@Test
|
||||
void inDevelopmentStageShouldNotUseCache() throws IOException {
|
||||
stage(Stage.DEVELOPMENT);
|
||||
|
||||
Optional<JsonNode> json = collector.findJson("de");
|
||||
|
||||
verifyJson(json);
|
||||
verify(cache, never()).get(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetFromCacheInProductionStage() throws IOException {
|
||||
stage(Stage.PRODUCTION);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode jsonNode = mapper.readTree(GIT_PLUGIN_JSON);
|
||||
when(cache.get("de")).thenReturn(jsonNode);
|
||||
|
||||
Optional<JsonNode> json = collector.findJson("de");
|
||||
assertThat(json).get().isSameAs(jsonNode);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreToCacheInProductionStage() throws IOException {
|
||||
stage(Stage.PRODUCTION);
|
||||
|
||||
Optional<JsonNode> json = collector.findJson("de");
|
||||
|
||||
verifyJson(json);
|
||||
verify(cache).put(eq("de"), any());
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyJson(Optional<JsonNode> json) {
|
||||
assertThat(json).isNotEmpty();
|
||||
assertThat(json.get().get("scm-svn-plugin")).isNotNull();
|
||||
assertThat(json.get().get("scm-git-plugin")).isNotNull();
|
||||
assertThat(json.get().get("scm-hg-plugin")).isNotNull();
|
||||
}
|
||||
|
||||
private void createCollector() {
|
||||
collector = new I18nCollector(context, pluginLoader, new JsonMerger(new ObjectMapper()), cacheManager);
|
||||
}
|
||||
|
||||
private void mockUberClassLoader(Path... directories) throws MalformedURLException {
|
||||
mockUberClassLoader(Arrays.asList(directories));
|
||||
}
|
||||
|
||||
private void mockUberClassLoader(Collection<Path> directories) throws MalformedURLException {
|
||||
List<URL> urls = new ArrayList<>();
|
||||
for (Path directory : directories) {
|
||||
urls.add(directory.toUri().toURL());
|
||||
}
|
||||
ClassLoader bootstrapLoader = ClassLoader.getSystemClassLoader().getParent();
|
||||
URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), bootstrapLoader);
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(classLoader);
|
||||
}
|
||||
|
||||
private void stage(Stage stage) {
|
||||
when(context.getStage()).thenReturn(stage);
|
||||
}
|
||||
|
||||
private void mockResources(Path directory) throws IOException {
|
||||
List<Path> directories = new ArrayList<>();
|
||||
for (int i = 0; i < ALL_PLUGIN_JSON.length; i++) {
|
||||
Path pluginDirectory = directory.resolve("plugin-" + i);
|
||||
Path file = pluginDirectory.resolve("locales/de/plugins.json");
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.write(file, ALL_PLUGIN_JSON[i].getBytes(StandardCharsets.UTF_8));
|
||||
directories.add(pluginDirectory);
|
||||
}
|
||||
mockUberClassLoader(directories);
|
||||
}
|
||||
}
|
||||
@@ -24,294 +24,94 @@
|
||||
|
||||
package sonia.scm.web.i18n;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.legman.EventBus;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.cache.Cache;
|
||||
import sonia.scm.cache.CacheManager;
|
||||
import sonia.scm.lifecycle.RestartEventFactory;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.util.JsonMerger;
|
||||
import sonia.scm.i18n.I18nCollector;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class I18nServletTest {
|
||||
|
||||
private static final String GIT_PLUGIN_JSON = json(
|
||||
"{",
|
||||
"'scm-git-plugin': {",
|
||||
private static final String GIT_PLUGIN =
|
||||
json(
|
||||
"{",
|
||||
"'scm-git-plugin': {",
|
||||
"'information': {",
|
||||
"'clone' : 'Clone',",
|
||||
"'create' : 'Create',",
|
||||
"'replace' : 'Push'",
|
||||
"'clone' : 'Clone',",
|
||||
"'create' : 'Create',",
|
||||
"'replace' : 'Push'",
|
||||
"}",
|
||||
"}",
|
||||
"}"
|
||||
);
|
||||
|
||||
private static final String HG_PLUGIN_JSON = json(
|
||||
"{",
|
||||
"'scm-hg-plugin': {",
|
||||
"'information': {",
|
||||
"'clone' : 'Clone',",
|
||||
"'create' : 'Create',",
|
||||
"'replace' : 'Push'",
|
||||
"}",
|
||||
"}",
|
||||
"}"
|
||||
);
|
||||
|
||||
private static final String SVN_PLUGIN_JSON = json(
|
||||
"{",
|
||||
"'scm-svn-plugin': {",
|
||||
"'information': {",
|
||||
"'checkout' : 'Checkout'",
|
||||
"}",
|
||||
"}",
|
||||
"}"
|
||||
);
|
||||
|
||||
private static final String[] ALL_PLUGIN_JSON = new String[]{
|
||||
GIT_PLUGIN_JSON, HG_PLUGIN_JSON, SVN_PLUGIN_JSON
|
||||
};
|
||||
"}"
|
||||
);
|
||||
|
||||
private static String json(String... parts) {
|
||||
return String.join("\n", parts).replaceAll("'", "\"");
|
||||
}
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider context;
|
||||
private I18nCollector collector;
|
||||
@InjectMocks
|
||||
private I18nServlet servlet;
|
||||
|
||||
@Mock
|
||||
private PluginLoader pluginLoader;
|
||||
|
||||
private HttpServletRequest request;
|
||||
@Mock
|
||||
private CacheManager cacheManager;
|
||||
private HttpServletResponse response;
|
||||
|
||||
@Mock
|
||||
private Cache<String, JsonNode> cache;
|
||||
|
||||
@Test
|
||||
void shouldNotHaveInvalidPluginsJsonFiles() throws Exception {
|
||||
URI uri = Objects.requireNonNull(getClass().getClassLoader().getResource("locales/en/plugins.json")).toURI();
|
||||
void shouldFailWith404OnMissingResources() throws IOException {
|
||||
String path = "/locales/de/plugins.json";
|
||||
when(request.getServletPath()).thenReturn(path);
|
||||
when(collector.findJson("de")).thenReturn(empty());
|
||||
|
||||
Path filePath = Paths.get(uri);
|
||||
Path translationRootPath = filePath.getParent().getParent();
|
||||
assertThat(translationRootPath).isDirectoryContaining("glob:**/en");
|
||||
servlet.doGet(request, response);
|
||||
|
||||
Files
|
||||
.list(translationRootPath)
|
||||
.filter(Files::isDirectory)
|
||||
.map(localePath -> localePath.resolve("plugins.json"))
|
||||
.forEach(this::validatePluginsJson);
|
||||
verify(response).setStatus(404);
|
||||
}
|
||||
|
||||
private void validatePluginsJson(Path path) {
|
||||
try {
|
||||
new ObjectMapper().readTree(path.toFile());
|
||||
} catch (IOException e) {
|
||||
fail("error while parsing translation file " + path, e);
|
||||
}
|
||||
@Test
|
||||
void shouldReturnJson() throws IOException {
|
||||
String path = "/locales/de/plugins.json";
|
||||
when(request.getServletPath()).thenReturn(path);
|
||||
when(collector.findJson("de")).thenReturn(of(new ObjectMapper().readTree(GIT_PLUGIN)));
|
||||
|
||||
String json = doGetString(servlet, request, response);
|
||||
|
||||
verifyHeaders(response);
|
||||
assertJson(json);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithCacheManager {
|
||||
private String doGetString(I18nServlet servlet, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PrintWriter writer = new PrintWriter(baos);
|
||||
when(response.getWriter()).thenReturn(writer);
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
when(cacheManager.<String, JsonNode>getCache(I18nServlet.CACHE_NAME)).thenReturn(cache);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWith404OnMissingResources(@TempDir Path directory) throws IOException {
|
||||
String path = "/locales/de/plugins.json";
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getServletPath()).thenReturn(path);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
mockUberClassLoader(directory);
|
||||
|
||||
createServlet().doGet(request, response);
|
||||
verify(response).setStatus(404);
|
||||
}
|
||||
|
||||
private void mockUberClassLoader(Path... directories) throws MalformedURLException {
|
||||
mockUberClassLoader(Arrays.asList(directories));
|
||||
}
|
||||
|
||||
private void mockUberClassLoader(Collection<Path> directories) throws MalformedURLException {
|
||||
List<URL> urls = new ArrayList<>();
|
||||
for (Path directory : directories) {
|
||||
urls.add(directory.toUri().toURL());
|
||||
}
|
||||
ClassLoader bootstrapLoader = ClassLoader.getSystemClassLoader().getParent();
|
||||
URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), bootstrapLoader);
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(classLoader);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWith500OnIOException(@TempDir Path directory) throws IOException {
|
||||
stage(Stage.DEVELOPMENT);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getServletPath()).thenReturn("/locales/de/plugins.json");
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
mockResource(directory, "locales/de/plugins.json", "invalid json");
|
||||
|
||||
createServlet().doGet(request, response);
|
||||
|
||||
verify(response).setStatus(500);
|
||||
}
|
||||
|
||||
private void mockResource(Path directory, String resourcePath, String content) throws IOException {
|
||||
Path file = directory.resolve(resourcePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.write(file, content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mockUberClassLoader(directory);
|
||||
}
|
||||
|
||||
private void stage(Stage stage) {
|
||||
when(context.getStage()).thenReturn(stage);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inDevelopmentStageShouldNotUseCache(@TempDir Path temp) throws IOException {
|
||||
stage(Stage.DEVELOPMENT);
|
||||
mockResources(temp, "locales/de/plugins.json");
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getServletPath()).thenReturn("/locales/de/plugins.json");
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
I18nServlet servlet = createServlet();
|
||||
String json = doGetString(servlet, request, response);
|
||||
|
||||
assertJson(json);
|
||||
verify(cache, never()).get(any());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private I18nServlet createServlet() {
|
||||
return new I18nServlet(context, pluginLoader, cacheManager, new JsonMerger(new ObjectMapper()));
|
||||
}
|
||||
|
||||
private String doGetString(I18nServlet servlet, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PrintWriter writer = new PrintWriter(baos);
|
||||
when(response.getWriter()).thenReturn(writer);
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
writer.flush();
|
||||
return baos.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
|
||||
private void mockResources(Path directory, String resourcePath) throws IOException {
|
||||
List<Path> directories = new ArrayList<>();
|
||||
for (int i = 0; i < ALL_PLUGIN_JSON.length; i++) {
|
||||
Path pluginDirectory = directory.resolve("plugin-" + i);
|
||||
Path file = pluginDirectory.resolve(resourcePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.write(file, ALL_PLUGIN_JSON[i].getBytes(StandardCharsets.UTF_8));
|
||||
directories.add(pluginDirectory);
|
||||
}
|
||||
mockUberClassLoader(directories);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetFromCacheInProductionStage() throws IOException {
|
||||
String path = "/locales/de/plugins.json";
|
||||
stage(Stage.PRODUCTION);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getServletPath()).thenReturn(path);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode jsonNode = mapper.readTree(GIT_PLUGIN_JSON);
|
||||
when(cache.get(path)).thenReturn(jsonNode);
|
||||
|
||||
I18nServlet servlet = createServlet();
|
||||
String json = doGetString(servlet, request, response);
|
||||
assertThat(json).contains("scm-git-plugin").doesNotContain("scm-hg-plugin");
|
||||
verifyHeaders(response);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreToCacheInProductionStage(@TempDir Path temp) throws IOException {
|
||||
String path = "/locales/de/plugins.json";
|
||||
mockResources(temp, "locales/de/plugins.json");
|
||||
stage(Stage.PRODUCTION);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getServletPath()).thenReturn(path);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
I18nServlet servlet = createServlet();
|
||||
String json = doGetString(servlet, request, response);
|
||||
|
||||
verify(cache).put(any(String.class), any(JsonNode.class));
|
||||
|
||||
verifyHeaders(response);
|
||||
assertJson(json);
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class WithDefaultClassLoader {
|
||||
|
||||
@BeforeEach
|
||||
void init() {
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(I18nServletTest.class.getClassLoader());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCleanCacheOnRestartEvent() {
|
||||
I18nServlet servlet = createServlet();
|
||||
EventBus eventBus = new EventBus("forTestingOnly");
|
||||
eventBus.register(servlet);
|
||||
eventBus.post(RestartEventFactory.create(I18nServlet.class, "Restart to reload the plugin resources"));
|
||||
|
||||
verify(cache).clear();
|
||||
}
|
||||
|
||||
}
|
||||
servlet.doGet(request, response);
|
||||
|
||||
writer.flush();
|
||||
return baos.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
|
||||
private void verifyHeaders(HttpServletResponse response) {
|
||||
@@ -323,8 +123,6 @@ class I18nServletTest {
|
||||
private void assertJson(String actual) {
|
||||
assertThat(actual)
|
||||
.isNotEmpty()
|
||||
.contains(CharMatcher.whitespace().removeFrom(GIT_PLUGIN_JSON.substring(1, GIT_PLUGIN_JSON.length() - 1)))
|
||||
.contains(CharMatcher.whitespace().removeFrom(HG_PLUGIN_JSON.substring(1, HG_PLUGIN_JSON.length() - 1)))
|
||||
.contains(CharMatcher.whitespace().removeFrom(SVN_PLUGIN_JSON.substring(1, SVN_PLUGIN_JSON.length() - 1)));
|
||||
.contains(CharMatcher.whitespace().removeFrom(GIT_PLUGIN.substring(1, GIT_PLUGIN.length() - 1)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user