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 b4a5b0aa27..14efd0995b 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,19 +1,21 @@ package sonia.scm.repository.xml; -import com.google.common.annotations.VisibleForTesting; import sonia.scm.SCMContextProvider; import sonia.scm.repository.BasicRepositoryLocationResolver; import sonia.scm.repository.InitialRepositoryLocationResolver; -import sonia.scm.repository.Repository; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.store.StoreConstants; import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Clock; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; -import java.util.function.Consumer; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; /** * A Location Resolver for File based Repository Storage. @@ -75,7 +77,13 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation Path path = initialRepositoryLocationResolver.getPath(repositoryId); pathById.put(repositoryId, path); writePathDatabase(); - return contextProvider.resolve(path); + Path resolvedPath = contextProvider.resolve(path); + try { + Files.createDirectories(resolvedPath); + } catch (IOException e) { + throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e); + } + return resolvedPath; } Path remove(String repositoryId) { 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 2f20a46c5e..f8b517e18b 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 @@ -1,43 +1,172 @@ package sonia.scm.repository.xml; +import com.google.common.base.Charsets; 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.InjectMocks; +import org.junitpioneer.jupiter.TempDirectory; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.mockito.stubbing.Answer; import sonia.scm.SCMContextProvider; import sonia.scm.repository.InitialRepositoryLocationResolver; -import sonia.scm.repository.RepositoryDAO; -import sonia.scm.repository.RepositoryLocationResolver; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; +import java.time.Clock; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; -@ExtendWith({MockitoExtension.class}) +@ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) @MockitoSettings(strictness = Strictness.LENIENT) class PathBasedRepositoryLocationResolverTest { + private static final long CREATION_TIME = 42; + @Mock private SCMContextProvider contextProvider; @Mock private InitialRepositoryLocationResolver initialRepositoryLocationResolver; + @Mock + private Clock clock; + + private Path basePath; + private PathBasedRepositoryLocationResolver resolver; @BeforeEach - void beforeEach() { - when(contextProvider.resolve(any(Path.class))).then((Answer) invocationOnMock -> invocationOnMock.getArgument(0)); - resolver = new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver); + void beforeEach(@TempDirectory.TempDir Path temp) { + this.basePath = temp; + when(contextProvider.getBaseDirectory()).thenReturn(temp.toFile()); + when(contextProvider.resolve(any(Path.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(initialRepositoryLocationResolver.getPath(anyString())).thenAnswer(invocation -> temp.resolve(invocation.getArgument(0).toString())); + when(clock.millis()).thenReturn(CREATION_TIME); + resolver = createResolver(); } - // TODO implement tests + @Test + void shouldCreateInitialDirectory() { + Path path = resolver.forClass(Path.class).getLocation("newId"); + + assertThat(path).isEqualTo(basePath.resolve("newId")); + assertThat(path).isDirectory(); + } + + @Test + void shouldPersistInitialDirectory() { + resolver.forClass(Path.class).getLocation("newId"); + + String content = getXmlFileContent(); + + assertThat(content).contains("newId"); + assertThat(content).contains(basePath.resolve("newId").toString()); + } + + @Test + void shouldPersistWithCreationDate() { + long now = CREATION_TIME + 100; + when(clock.millis()).thenReturn(now); + + resolver.forClass(Path.class).getLocation("newId"); + + assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); + + String content = getXmlFileContent(); + assertThat(content).contains("creation-time=\"" + CREATION_TIME + "\""); + } + + @Test + void shouldUpdateWithModifiedDate() { + long now = CREATION_TIME + 100; + when(clock.millis()).thenReturn(now); + + resolver.forClass(Path.class).getLocation("newId"); + + assertThat(resolver.getCreationTime()).isEqualTo(CREATION_TIME); + assertThat(resolver.getLastModified()).isEqualTo(now); + + String content = getXmlFileContent(); + assertThat(content).contains("creation-time=\"" + CREATION_TIME + "\""); + assertThat(content).contains("last-modified=\"" + now + "\""); + } + + @Nested + class WithExistingData { + + private PathBasedRepositoryLocationResolver resolverWithExistingData; + + @BeforeEach + void createExistingDatabase() { + resolver.forClass(Path.class).getLocation("existingId_1"); + resolver.forClass(Path.class).getLocation("existingId_2"); + resolverWithExistingData = createResolver(); + } + + @Test + void shouldInitWithExistingData() { + Map foundRepositories = new HashMap<>(); + resolverWithExistingData.forAllPaths( + foundRepositories::put + ); + assertThat(foundRepositories) + .containsKeys("existingId_1", "existingId_2"); + } + + @Test + void shouldRemoveFromFile() { + resolverWithExistingData.remove("existingId_1"); + + assertThat(getXmlFileContent()).doesNotContain("existingId_1"); + } + + @Test + void shouldNotUpdateModificationDateForExistingDirectoryMapping() { + long now = CREATION_TIME + 100; + Path path = resolverWithExistingData.create(Path.class).getLocation("existingId_1"); + + assertThat(path).isEqualTo(basePath.resolve("existingId_1")); + + String content = getXmlFileContent(); + assertThat(content).doesNotContain("last-modified=\"" + now + "\""); + } + + @Test + void shouldNotCreateDirectoryForExistingMapping() throws IOException { + Files.delete(basePath.resolve("existingId_1")); + + Path path = resolverWithExistingData.create(Path.class).getLocation("existingId_1"); + + assertThat(path).doesNotExist(); + } + } + + private String getXmlFileContent() { + Path storePath = basePath.resolve("config").resolve("repositories.xml"); + + assertThat(storePath).isRegularFile(); + return content(storePath); + } + + private PathBasedRepositoryLocationResolver createResolver() { + return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, clock); + } + + private String content(Path storePath) { + try { + return new String(Files.readAllBytes(storePath), Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } }