diff --git a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java b/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java
index 244bc8f03c..9aab8d18ae 100644
--- a/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java
+++ b/scm-core/src/main/java/sonia/scm/boot/RestartEvent.java
@@ -33,14 +33,16 @@ package sonia.scm.boot;
//~--- non-JDK imports --------------------------------------------------------
-import sonia.scm.Stage;
import sonia.scm.event.Event;
/**
* This event can be used to force a restart of the webapp context. The restart
* event is useful during plugin development, because we don't have to restart
- * the whole server, to see our changes. The restart event can only be used in
- * stage {@link Stage#DEVELOPMENT}.
+ * the whole server, to see our changes. The restart event could also be used
+ * to install or upgrade plugins.
+ *
+ * But the restart event should be used carefully, because the whole context
+ * will be restarted and that process could take some time.
*
* @author Sebastian Sdorra
* @since 2.0.0
diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
index bc8df673d0..1b7da51c4c 100644
--- a/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
+++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryLocationResolver.java
@@ -13,8 +13,27 @@ public abstract class RepositoryLocationResolver {
return create(type);
}
- @FunctionalInterface
public interface RepositoryLocationResolverInstance {
+
+ /**
+ * Get the existing location for the repository.
+ * @param repositoryId The id of the repository.
+ * @throws IllegalStateException when there is no known location for the given repository.
+ */
T getLocation(String repositoryId);
+
+ /**
+ * Create a new location for the new repository.
+ * @param repositoryId The id of the new repository.
+ * @throws IllegalStateException when there already is a location for the given repository registered.
+ */
+ T createLocation(String repositoryId);
+
+ /**
+ * Set the location of a new repository.
+ * @param repositoryId The id of the new repository.
+ * @throws IllegalStateException when there already is a location for the given repository registered.
+ */
+ void setLocation(String repositoryId, T location);
}
}
diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java
index 9c1fa590cc..8c3afbd90f 100644
--- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java
+++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java
@@ -161,10 +161,17 @@ public class DefaultCipherHandler implements CipherHandler {
* @return decrypted value
*/
public String decode(char[] plainKey, String value) {
- String result = null;
-
+ Base64.Decoder decoder = Base64.getUrlDecoder();
try {
- byte[] encodedInput = Base64.getUrlDecoder().decode(value);
+ return decode(plainKey, value, decoder);
+ } catch (IllegalArgumentException e) {
+ return decode(plainKey, value, Base64.getDecoder());
+ }
+ }
+
+ private String decode(char[] plainKey, String value, Base64.Decoder decoder) {
+ try {
+ byte[] encodedInput = decoder.decode(value);
byte[] salt = new byte[SALT_LENGTH];
byte[] encoded = new byte[encodedInput.length - SALT_LENGTH];
@@ -180,12 +187,10 @@ public class DefaultCipherHandler implements CipherHandler {
byte[] decoded = cipher.doFinal(encoded);
- result = new String(decoded, ENCODING);
+ return new String(decoded, ENCODING);
} catch (IOException | GeneralSecurityException ex) {
throw new CipherException("could not decode string", ex);
}
-
- return result;
}
@Override
diff --git a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java
index 8d95131ee6..3ea7737b4e 100644
--- a/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java
+++ b/scm-core/src/main/java/sonia/scm/security/PermissionDescriptor.java
@@ -100,7 +100,7 @@ public class PermissionDescriptor implements Serializable
@Override
public int hashCode()
{
- return value.hashCode();
+ return value == null? -1: value.hashCode();
}
/**
diff --git a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java
index 972ed95a2d..51ff906b8f 100644
--- a/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java
+++ b/scm-core/src/test/java/sonia/scm/util/ValidationUtilTest.java
@@ -147,6 +147,10 @@ public class ValidationUtilTest
public void testIsRepositoryNameValid() {
String[] validPaths = {
"scm",
+ "scm-",
+ "scm_",
+ "s_cm",
+ "s-cm",
"s",
"sc",
".hiddenrepo",
@@ -206,7 +210,8 @@ public class ValidationUtilTest
"a/..b",
"scm/main",
"scm/plugins/git-plugin",
- "scm/plugins/git-plugin"
+ "_scm",
+ "-scm"
};
for (String path : validPaths) {
diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java
index bfb59f9f35..81cf167071 100644
--- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java
+++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolver.java
@@ -1,6 +1,7 @@
package sonia.scm.repository.xml;
import sonia.scm.SCMContextProvider;
+import sonia.scm.io.FileSystem;
import sonia.scm.repository.BasicRepositoryLocationResolver;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.InternalRepositoryException;
@@ -8,8 +9,6 @@ import sonia.scm.store.StoreConstants;
import javax.inject.Inject;
import javax.inject.Singleton;
-import java.io.IOException;
-import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.util.Map;
@@ -36,6 +35,7 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
private final SCMContextProvider contextProvider;
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
+ private final FileSystem fileSystem;
private final PathDatabase pathDatabase;
private final Map pathById;
@@ -46,14 +46,15 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
private Long lastModified;
@Inject
- public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver) {
- this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
+ public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem) {
+ this(contextProvider, initialRepositoryLocationResolver, fileSystem, Clock.systemUTC());
}
- PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
+ PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem, Clock clock) {
super(Path.class);
this.contextProvider = contextProvider;
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
+ this.fileSystem = fileSystem;
this.pathById = new ConcurrentHashMap<>();
this.clock = clock;
@@ -66,23 +67,43 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
@Override
protected RepositoryLocationResolverInstance create(Class type) {
- return repositoryId -> {
- if (pathById.containsKey(repositoryId)) {
- return (T) contextProvider.resolve(pathById.get(repositoryId));
- } else {
- return (T) create(repositoryId);
+ return new RepositoryLocationResolverInstance() {
+ @Override
+ public T getLocation(String repositoryId) {
+ if (pathById.containsKey(repositoryId)) {
+ return (T) contextProvider.resolve(pathById.get(repositoryId));
+ } else {
+ throw new IllegalStateException("location for repository " + repositoryId + " does not exist");
+ }
+ }
+
+ @Override
+ public T createLocation(String repositoryId) {
+ if (pathById.containsKey(repositoryId)) {
+ throw new IllegalStateException("location for repository " + repositoryId + " already exists");
+ } else {
+ return (T) create(repositoryId);
+ }
+ }
+
+ @Override
+ public void setLocation(String repositoryId, T location) {
+ if (pathById.containsKey(repositoryId)) {
+ throw new IllegalStateException("location for repository " + repositoryId + " already exists");
+ } else {
+ PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath());
+ }
}
};
}
Path create(String repositoryId) {
Path path = initialRepositoryLocationResolver.getPath(repositoryId);
- pathById.put(repositoryId, path);
- writePathDatabase();
+ setLocation(repositoryId, path);
Path resolvedPath = contextProvider.resolve(path);
try {
- Files.createDirectories(resolvedPath);
- } catch (IOException e) {
+ fileSystem.create(resolvedPath.toFile());
+ } catch (Exception e) {
throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e);
}
return resolvedPath;
@@ -141,6 +162,11 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
+ private void setLocation(String repositoryId, Path repositoryBasePath) {
+ pathById.put(repositoryId, repositoryBasePath);
+ writePathDatabase();
+ }
+
public void refresh() {
this.read();
}
diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java
index 5f758f124a..941775d6ea 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/PathBasedRepositoryLocationResolverTest.java
@@ -11,6 +11,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.SCMContextProvider;
+import sonia.scm.io.DefaultFileSystem;
+import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import java.io.IOException;
@@ -41,6 +43,8 @@ class PathBasedRepositoryLocationResolverTest {
@Mock
private Clock clock;
+ private final FileSystem fileSystem = new DefaultFileSystem();
+
private Path basePath;
private PathBasedRepositoryLocationResolver resolver;
@@ -57,7 +61,7 @@ class PathBasedRepositoryLocationResolverTest {
@Test
void shouldCreateInitialDirectory() {
- Path path = resolver.forClass(Path.class).getLocation("newId");
+ Path path = resolver.forClass(Path.class).createLocation("newId");
assertThat(path).isEqualTo(basePath.resolve("newId"));
assertThat(path).isDirectory();
@@ -65,7 +69,7 @@ class PathBasedRepositoryLocationResolverTest {
@Test
void shouldPersistInitialDirectory() {
- resolver.forClass(Path.class).getLocation("newId");
+ resolver.forClass(Path.class).createLocation("newId");
String content = getXmlFileContent();
@@ -78,7 +82,7 @@ class PathBasedRepositoryLocationResolverTest {
long now = CREATION_TIME + 100;
when(clock.millis()).thenReturn(now);
- resolver.forClass(Path.class).getLocation("newId");
+ resolver.forClass(Path.class).createLocation("newId");
assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME);
@@ -91,7 +95,7 @@ class PathBasedRepositoryLocationResolverTest {
long now = CREATION_TIME + 100;
when(clock.millis()).thenReturn(now);
- resolver.forClass(Path.class).getLocation("newId");
+ resolver.forClass(Path.class).createLocation("newId");
assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME);
assertThat(resolver.getLastModified()).isEqualTo(now);
@@ -108,8 +112,8 @@ class PathBasedRepositoryLocationResolverTest {
@BeforeEach
void createExistingDatabase() {
- resolver.forClass(Path.class).getLocation("existingId_1");
- resolver.forClass(Path.class).getLocation("existingId_2");
+ resolver.forClass(Path.class).createLocation("existingId_1");
+ resolver.forClass(Path.class).createLocation("existingId_2");
resolverWithExistingData = createResolver();
}
@@ -159,7 +163,7 @@ class PathBasedRepositoryLocationResolverTest {
}
private PathBasedRepositoryLocationResolver createResolver() {
- return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, clock);
+ return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, fileSystem, clock);
}
private String content(Path storePath) {
diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
index d77bfb3c63..bdf28310e1 100644
--- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
+++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOTest.java
@@ -17,6 +17,7 @@ import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
+import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryPermission;
import java.io.IOException;
@@ -53,7 +54,23 @@ class XmlRepositoryDAOTest {
@BeforeEach
void createDAO(@TempDirectory.TempDir Path basePath) {
- when(locationResolver.create(Path.class)).thenReturn(locationResolver::create);
+ when(locationResolver.create(Path.class)).thenReturn(
+ new RepositoryLocationResolver.RepositoryLocationResolverInstance() {
+ @Override
+ public Path getLocation(String repositoryId) {
+ return locationResolver.create(repositoryId);
+ }
+
+ @Override
+ public Path createLocation(String repositoryId) {
+ return locationResolver.create(repositoryId);
+ }
+
+ @Override
+ public void setLocation(String repositoryId, Path location) {
+ }
+ }
+ );
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation));
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
}
diff --git a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java
index 1dfc22e15a..acffe6c769 100644
--- a/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java
+++ b/scm-test/src/main/java/sonia/scm/TempDirRepositoryLocationResolver.java
@@ -1,7 +1,6 @@
package sonia.scm;
import sonia.scm.repository.BasicRepositoryLocationResolver;
-import sonia.scm.repository.RepositoryLocationResolver;
import java.io.File;
import java.nio.file.Path;
@@ -16,6 +15,21 @@ public class TempDirRepositoryLocationResolver extends BasicRepositoryLocationRe
@Override
protected RepositoryLocationResolverInstance create(Class type) {
- return repositoryId -> (T) tempDirectory.toPath();
+ return new RepositoryLocationResolverInstance() {
+ @Override
+ public T getLocation(String repositoryId) {
+ return (T) tempDirectory.toPath();
+ }
+
+ @Override
+ public T createLocation(String repositoryId) {
+ return (T) tempDirectory.toPath();
+ }
+
+ @Override
+ public void setLocation(String repositoryId, T location) {
+ throw new UnsupportedOperationException("not implemented for tests");
+ }
+ };
}
}
diff --git a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java
index 063681bbc4..615169b58c 100644
--- a/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java
+++ b/scm-test/src/main/java/sonia/scm/repository/SimpleRepositoryHandlerTestBase.java
@@ -34,6 +34,7 @@ package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import org.junit.Test;
+import org.mockito.stubbing.Answer;
import sonia.scm.AbstractTestBase;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
@@ -82,11 +83,12 @@ public abstract class SimpleRepositoryHandlerTestBase extends AbstractTestBase {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.create(any())).thenReturn(instanceMock);
when(locationResolver.supportsLocationType(any())).thenReturn(true);
-
- when(instanceMock.getLocation(anyString())).then(ic -> {
+ Answer
+{{/content}}
+
+{{$script}}
+
+{{/script}}
+
+{{/layout}}
diff --git a/scm-webapp/src/main/resources/templates/repository-migration.mustache b/scm-webapp/src/main/resources/templates/repository-migration.mustache
new file mode 100644
index 0000000000..9c76438667
--- /dev/null
+++ b/scm-webapp/src/main/resources/templates/repository-migration.mustache
@@ -0,0 +1,104 @@
+{{< layout}}
+
+{{$title}}SCM-Manager Migration{{/title}}
+
+{{$content}}
+ You have migrated from SCM-Manager v1 to SCM-Manager v2.
+
+
+ To migrate the existing repositories you have to specify a namespace and a name for each on them
+ as well as a migration strategy.
+
+
+
+ The strategies are the following:
+
+
+
+ {{#strategies}}
+
+ | {{name}} |
+ {{description}} |
+
+ {{/strategies}}
+
+
+
+
+ {{#validationErrorsFound}}
+ Please correct the invalid namespaces or names below and try again.
+
+ {{/validationErrorsFound}}
+
+
+{{/content}}
+
+{{$script}}
+
+{{/script}}
+
+{{/ layout}}
diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java
new file mode 100644
index 0000000000..fed4a3b6c8
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java
@@ -0,0 +1,224 @@
+package sonia.scm.update;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.update.repository.MigrationStrategy;
+import sonia.scm.update.repository.MigrationStrategyDao;
+import sonia.scm.update.repository.V1Repository;
+import sonia.scm.update.repository.XmlRepositoryV1UpdateStep;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class MigrationWizardServletTest {
+
+ @Mock
+ XmlRepositoryV1UpdateStep updateStep;
+ @Mock
+ MigrationStrategyDao migrationStrategyDao;
+
+ @Mock
+ HttpServletRequest request;
+ @Mock
+ HttpServletResponse response;
+
+ String renderedTemplateName;
+ Map renderedModel;
+
+ MigrationWizardServlet servlet;
+
+ @BeforeEach
+ void initServlet() {
+ servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao) {
+ @Override
+ void respondWithTemplate(HttpServletResponse resp, Map model, String templateName) {
+ renderedTemplateName = templateName;
+ renderedModel = model;
+ }
+ };
+ }
+
+ @Test
+ void shouldUseRepositoryTypeAsNamespaceForNamesWithSingleElement() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "simple"))
+ );
+
+ servlet.doGet(request, response);
+
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("namespace")
+ .contains("git");
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("name")
+ .contains("simple");
+ }
+
+ @Test
+ void shouldUseDirectoriesForNamespaceAndNameForNamesWithTwoElements() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "two/dirs"))
+ );
+
+ servlet.doGet(request, response);
+
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("namespace")
+ .contains("two");
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("name")
+ .contains("dirs");
+ }
+
+ @Test
+ void shouldUseDirectoriesForNamespaceAndConcatenatedNameForNamesWithMoreThanTwoElements() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "more/than/two/dirs"))
+ );
+
+ servlet.doGet(request, response);
+
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("namespace")
+ .contains("more");
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("name")
+ .contains("than_two_dirs");
+ }
+
+ @Test
+ void shouldUseTypeAndNameAsPath() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "name"))
+ );
+
+ servlet.doGet(request, response);
+
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("path")
+ .contains("git/name");
+ }
+
+ @Test
+ void shouldKeepId() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "name"))
+ );
+
+ servlet.doGet(request, response);
+
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("id")
+ .contains("id");
+ }
+
+ @Test
+ void shouldNotBeInvalidAtFirstRequest() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "name"))
+ );
+
+ servlet.doGet(request, response);
+
+ assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(false);
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("namespaceInvalid")
+ .contains(false);
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("nameInvalid")
+ .contains(false);
+ }
+
+ @Test
+ void shouldValidateNamespaceAndNameOnPost() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "name"))
+ );
+ doReturn("invalid namespace").when(request).getParameter("namespace-id");
+ doReturn("invalid name").when(request).getParameter("name-id");
+ doReturn("COPY").when(request).getParameter("strategy-id");
+
+ servlet.doPost(request, response);
+
+ assertThat(renderedModel.get("validationErrorsFound")).isEqualTo(true);
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("namespaceInvalid")
+ .contains(true);
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("nameInvalid")
+ .contains(true);
+ }
+
+ @Test
+ void shouldKeepSelectedMigrationStrategy() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "name"))
+ );
+
+ doReturn("we need an").when(request).getParameter("namespace-id");
+ doReturn("error for this test").when(request).getParameter("name-id");
+ doReturn("INLINE").when(request).getParameter("strategy-id");
+
+ servlet.doPost(request, response);
+
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("selectedStrategy")
+ .contains(MigrationStrategy.INLINE);
+ }
+
+ @Test
+ void shouldUseCopyWithoutMigrationStrategy() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "name"))
+ );
+
+ doReturn("we need an").when(request).getParameter("namespace-id");
+ doReturn("error for this test").when(request).getParameter("name-id");
+ doReturn("").when(request).getParameter("strategy-id");
+
+ servlet.doPost(request, response);
+
+ assertThat(renderedModel.get("repositories"))
+ .asList()
+ .extracting("selectedStrategy")
+ .contains(MigrationStrategy.COPY);
+ }
+
+ @Test
+ void shouldStoreValidMigration() {
+ when(updateStep.getRepositoriesWithoutMigrationStrategies()).thenReturn(
+ Collections.singletonList(new V1Repository("id", "git", "name"))
+ );
+ doReturn("namespace").when(request).getParameter("namespace-id");
+ doReturn("name").when(request).getParameter("name-id");
+ doReturn("COPY").when(request).getParameter("strategy-id");
+
+ servlet.doPost(request, response);
+
+ verify(migrationStrategyDao).set("id", MigrationStrategy.COPY, "namespace", "name");
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java
index b40283ae79..d3f18a1ff7 100644
--- a/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/update/repository/CopyMigrationStrategyTest.java
@@ -43,18 +43,18 @@ class CopyMigrationStrategyTest {
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
- when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
+ when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
- Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
+ Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
}
@Test
void shouldCopyDataDirectory(@TempDirectory.TempDir Path tempDir) {
- Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
+ Path target = new CopyMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir
.resolve("repositories")
diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java
index 6abddae3fb..2b97c1d79b 100644
--- a/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/update/repository/InlineMigrationStrategyTest.java
@@ -7,11 +7,14 @@ import org.junitpioneer.jupiter.TempDirectory;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
+import sonia.scm.repository.RepositoryLocationResolver;
+import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import java.io.IOException;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(TempDirectory.class)
@@ -20,9 +23,14 @@ class InlineMigrationStrategyTest {
@Mock
SCMContextProvider contextProvider;
+ @Mock
+ PathBasedRepositoryLocationResolver locationResolver;
+ @Mock
+ RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance;
@BeforeEach
void mockContextProvider(@TempDirectory.TempDir Path tempDir) {
+ when(locationResolver.forClass(Path.class)).thenReturn(locationResolverInstance);
when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
}
@@ -33,13 +41,14 @@ class InlineMigrationStrategyTest {
@Test
void shouldUseExistingDirectory(@TempDirectory.TempDir Path tempDir) {
- Path target = new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
+ Path target = new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target).isEqualTo(resolveOldDirectory(tempDir));
+ verify(locationResolverInstance).setLocation("b4f-a9f0-49f7-ad1f-37d3aae1c55f", target);
}
@Test
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
- new InlineMigrationStrategy(contextProvider).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
+ new InlineMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
assertThat(resolveOldDirectory(tempDir).resolve("data")).exists();
}
diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java
index e3fd4457b6..5829f5b98f 100644
--- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyDaoTest.java
@@ -11,8 +11,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.JAXBConfigurationStoreFactory;
-import sonia.scm.update.repository.MigrationStrategy;
-import sonia.scm.update.repository.MigrationStrategyDao;
import javax.xml.bind.JAXBException;
import java.nio.file.Path;
@@ -37,23 +35,31 @@ class MigrationStrategyDaoTest {
}
@Test
- void shouldReturnEmptyOptionalWhenStoreIsEmpty() throws JAXBException {
+ void shouldReturnEmptyOptionalWhenStoreIsEmpty() {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
- Optional strategy = dao.get("any");
+ Optional entry = dao.get("any");
- Assertions.assertThat(strategy).isEmpty();
+ Assertions.assertThat(entry).isEmpty();
}
@Test
- void shouldReturnNewValue() throws JAXBException {
+ void shouldReturnNewValue() {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
- dao.set("id", INLINE);
+ dao.set("id", INLINE, "space", "name");
- Optional strategy = dao.get("id");
+ Optional entry = dao.get("id");
- Assertions.assertThat(strategy).contains(INLINE);
+ Assertions.assertThat(entry)
+ .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy)
+ .contains(INLINE);
+ Assertions.assertThat(entry)
+ .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace)
+ .contains("space");
+ Assertions.assertThat(entry)
+ .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName)
+ .contains("name");
}
@Nested
@@ -62,16 +68,24 @@ class MigrationStrategyDaoTest {
void initExistingDatabase() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
- dao.set("id", INLINE);
+ dao.set("id", INLINE, "space", "name");
}
@Test
void shouldFindExistingValue() throws JAXBException {
MigrationStrategyDao dao = new MigrationStrategyDao(storeFactory);
- Optional strategy = dao.get("id");
+ Optional entry = dao.get("id");
- Assertions.assertThat(strategy).contains(INLINE);
+ Assertions.assertThat(entry)
+ .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getDataMigrationStrategy)
+ .contains(INLINE);
+ Assertions.assertThat(entry)
+ .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewNamespace)
+ .contains("space");
+ Assertions.assertThat(entry)
+ .map(RepositoryMigrationPlan.RepositoryMigrationEntry::getNewName)
+ .contains("name");
}
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java
index e0018f584f..c04c9477bb 100644
--- a/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java
+++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MigrationStrategyMock.java
@@ -3,10 +3,13 @@ package sonia.scm.update.repository;
import com.google.inject.Injector;
import sonia.scm.update.repository.MigrationStrategy.Instance;
+import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
+import static java.util.Optional.of;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -20,6 +23,13 @@ class MigrationStrategyMock {
.thenAnswer(
invocationOnMock -> mocks.computeIfAbsent(invocationOnMock.getArgument(0), key -> mock((Class) key))
);
+
+ for (MigrationStrategy strategy : MigrationStrategy.values()) {
+ MigrationStrategy.Instance strategyMock = mock(strategy.getImplementationClass());
+ when(strategyMock.migrate(any(), any(), any())).thenReturn(of(Paths.get("")));
+ lenient().when(mock.getInstance((Class) strategy.getImplementationClass())).thenReturn(strategyMock);
+ }
+
return mock;
}
}
diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java
index b55315d85f..fa58eb8ea1 100644
--- a/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/update/repository/MoveMigrationStrategyTest.java
@@ -40,18 +40,18 @@ class MoveMigrationStrategyTest {
void mockLocationResolver(@TempDirectory.TempDir Path tempDir) {
RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class);
when(locationResolver.forClass(Path.class)).thenReturn(instanceMock);
- when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
+ when(instanceMock.createLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0)));
}
@Test
void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) {
- Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
+ Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f"));
}
@Test
void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) {
- Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git");
+ Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git").get();
assertThat(target.resolve("data")).exists();
Path originalDataDir = tempDir
.resolve("repositories")
diff --git a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java
index 2102d296ea..6c977a3cb6 100644
--- a/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/update/repository/XmlRepositoryV1UpdateStepTest.java
@@ -11,6 +11,7 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Answer;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.xml.XmlRepositoryDAO;
@@ -33,11 +34,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static sonia.scm.update.repository.MigrationStrategy.COPY;
-import static sonia.scm.update.repository.MigrationStrategy.INLINE;
import static sonia.scm.update.repository.MigrationStrategy.MOVE;
@ExtendWith(MockitoExtension.class)
@@ -89,9 +89,14 @@ class XmlRepositoryV1UpdateStepTest {
@BeforeEach
void createMigrationPlan() {
- lenient().when(migrationStrategyDao.get("3b91caa5-59c3-448f-920b-769aaa56b761")).thenReturn(of(MOVE));
- lenient().when(migrationStrategyDao.get("c1597b4f-a9f0-49f7-ad1f-37d3aae1c55f")).thenReturn(of(COPY));
- lenient().when(migrationStrategyDao.get("454972da-faf9-4437-b682-dc4a4e0aa8eb")).thenReturn(of(INLINE));
+ Answer