Merge custom roles with migration

This commit is contained in:
René Pfeuffer
2019-05-15 15:01:17 +02:00
33 changed files with 1571 additions and 1140 deletions

View File

@@ -0,0 +1,141 @@
package sonia.scm.repository.xml;
import sonia.scm.SCMContextProvider;
import sonia.scm.repository.BasicRepositoryLocationResolver;
import sonia.scm.repository.InitialRepositoryLocationResolver;
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 static sonia.scm.ContextEntry.ContextBuilder.entity;
/**
* A Location Resolver for File based Repository Storage.
* <p>
* <b>WARNING:</b> The Locations provided with this class may not be used from the plugins to store any plugin specific files.
* <p>
* Please use the {@link sonia.scm.store.DataStoreFactory } and the {@link sonia.scm.store.DataStore} classes to store data<br>
* Please use the {@link sonia.scm.store.BlobStoreFactory } and the {@link sonia.scm.store.BlobStore} classes to store binary files<br>
* Please use the {@link sonia.scm.store.ConfigurationStoreFactory} and the {@link sonia.scm.store.ConfigurationStore} classes to store configurations
*
* @since 2.0.0
*/
public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocationResolver<Path> {
private static final String STORE_NAME = "repositories";
private final SCMContextProvider contextProvider;
private final InitialRepositoryLocationResolver initialRepositoryLocationResolver;
private final PathDatabase pathDatabase;
private final Map<String, Path> pathById;
private final Clock clock;
private Long creationTime;
private Long lastModified;
@Inject
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver) {
this(contextProvider, initialRepositoryLocationResolver, Clock.systemUTC());
}
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, Clock clock) {
super(Path.class);
this.contextProvider = contextProvider;
this.initialRepositoryLocationResolver = initialRepositoryLocationResolver;
this.pathById = new ConcurrentHashMap<>();
this.clock = clock;
this.creationTime = clock.millis();
pathDatabase = new PathDatabase(resolveStorePath());
read();
}
@Override
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
return repositoryId -> {
if (pathById.containsKey(repositoryId)) {
return (T) contextProvider.resolve(pathById.get(repositoryId));
} else {
return (T) create(repositoryId);
}
};
}
Path create(String repositoryId) {
Path path = initialRepositoryLocationResolver.getPath(repositoryId);
pathById.put(repositoryId, path);
writePathDatabase();
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) {
Path removedPath = pathById.remove(repositoryId);
writePathDatabase();
return contextProvider.resolve(removedPath);
}
void forAllPaths(BiConsumer<String, Path> consumer) {
pathById.forEach((id, path) -> consumer.accept(id, contextProvider.resolve(path)));
}
void updateModificationDate() {
this.writePathDatabase();
}
private void writePathDatabase() {
lastModified = clock.millis();
pathDatabase.write(creationTime, lastModified, pathById);
}
private void read() {
Path storePath = resolveStorePath();
// Files.exists is slow on java 8
if (storePath.toFile().exists()) {
pathDatabase.read(this::onLoadDates, this::onLoadRepository);
}
}
private void onLoadDates(Long creationTime, Long lastModified) {
this.creationTime = creationTime;
this.lastModified = lastModified;
}
public Long getCreationTime() {
return creationTime;
}
public Long getLastModified() {
return lastModified;
}
private void onLoadRepository(String id, Path repositoryPath) {
pathById.put(id, repositoryPath);
}
private Path resolveStorePath() {
return contextProvider.getBaseDirectory()
.toPath()
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
}

View File

@@ -33,22 +33,18 @@ package sonia.scm.repository.xml;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.inject.Singleton;
import sonia.scm.SCMContextProvider;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.PathBasedRepositoryDAO;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.store.StoreConstants;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Clock;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -57,82 +53,38 @@ import java.util.concurrent.ConcurrentHashMap;
* @author Sebastian Sdorra
*/
@Singleton
public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
public class XmlRepositoryDAO implements RepositoryDAO {
private static final String STORE_NAME = "repositories";
private final PathDatabase pathDatabase;
private final MetadataStore metadataStore = new MetadataStore();
private final SCMContextProvider context;
private final InitialRepositoryLocationResolver locationResolver;
private final PathBasedRepositoryLocationResolver repositoryLocationResolver;
private final FileSystem fileSystem;
private final Map<String, Path> pathById;
private final Map<String, Repository> byId;
private final Map<NamespaceAndName, Repository> byNamespaceAndName;
private final Clock clock;
private Long creationTime;
private Long lastModified;
@Inject
public XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem) {
this(context, locationResolver, fileSystem, Clock.systemUTC());
}
XmlRepositoryDAO(SCMContextProvider context, InitialRepositoryLocationResolver locationResolver, FileSystem fileSystem, Clock clock) {
this.context = context;
this.locationResolver = locationResolver;
public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem) {
this.repositoryLocationResolver = repositoryLocationResolver;
this.fileSystem = fileSystem;
this.clock = clock;
this.creationTime = clock.millis();
this.pathById = new ConcurrentHashMap<>();
this.byId = new ConcurrentHashMap<>();
this.byNamespaceAndName = new ConcurrentHashMap<>();
pathDatabase = new PathDatabase(resolveStorePath());
read();
init();
}
private void read() {
Path storePath = resolveStorePath();
// Files.exists is slow on java 8
if (storePath.toFile().exists()) {
pathDatabase.read(this::onLoadDates, this::onLoadRepository);
}
private void init() {
repositoryLocationResolver.forAllPaths((repositoryId, repositoryPath) -> {
Path metadataPath = resolveDataPath(repositoryPath);
Repository repository = metadataStore.read(metadataPath);
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
byId.put(repositoryId, repository);
});
}
private void onLoadDates(Long creationTime, Long lastModified) {
this.creationTime = creationTime;
this.lastModified = lastModified;
}
private void onLoadRepository(String id, Path repositoryPath) {
Path metadataPath = resolveMetadataPath(context.resolve(repositoryPath));
Repository repository = metadataStore.read(metadataPath);
byId.put(id, repository);
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
pathById.put(id, repositoryPath);
}
@VisibleForTesting
Path resolveStorePath() {
return context.getBaseDirectory()
.toPath()
.resolve(StoreConstants.CONFIG_DIRECTORY_NAME)
.resolve(STORE_NAME.concat(StoreConstants.FILE_EXTENSION));
}
@VisibleForTesting
Path resolveMetadataPath(Path repositoryPath) {
private Path resolveDataPath(Path repositoryPath) {
return repositoryPath.resolve(StoreConstants.REPOSITORY_METADATA.concat(StoreConstants.FILE_EXTENSION));
}
@@ -141,47 +93,27 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
return "xml";
}
@Override
public Long getCreationTime() {
return creationTime;
}
@Override
public Long getLastModified() {
return lastModified;
}
@Override
public void add(Repository repository) {
Repository clone = repository.clone();
Path repositoryPath = locationResolver.getPath(repository.getId());
Path resolvedPath = context.resolve(repositoryPath);
synchronized (this) {
Path repositoryPath = repositoryLocationResolver.create(repository.getId());
try {
fileSystem.create(resolvedPath.toFile());
Path metadataPath = resolveMetadataPath(resolvedPath);
metadataStore.write(metadataPath, repository);
synchronized (this) {
pathById.put(repository.getId(), repositoryPath);
byId.put(repository.getId(), clone);
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
writePathDatabase();
try {
Path metadataPath = resolveDataPath(repositoryPath);
metadataStore.write(metadataPath, repository);
} catch (Exception e) {
repositoryLocationResolver.remove(repository.getId());
throw new InternalRepositoryException(repository, "failed to create filesystem", e);
}
} catch (IOException e) {
throw new InternalRepositoryException(repository, "failed to create filesystem", e);
byId.put(repository.getId(), clone);
byNamespaceAndName.put(repository.getNamespaceAndName(), clone);
}
}
private void writePathDatabase() {
lastModified = clock.millis();
pathDatabase.write(creationTime, lastModified, pathById);
}
@Override
public boolean contains(Repository repository) {
@@ -224,12 +156,13 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
byNamespaceAndName.remove(prev.getNamespaceAndName());
}
byNamespaceAndName.put(clone.getNamespaceAndName(), clone);
writePathDatabase();
}
Path repositoryPath = context.resolve(getPath(repository.getId()));
Path metadataPath = resolveMetadataPath(repositoryPath);
Path repositoryPath = repositoryLocationResolver
.create(Path.class)
.getLocation(repository.getId());
Path metadataPath = resolveDataPath(repositoryPath);
repositoryLocationResolver.updateModificationDate();
metadataStore.write(metadataPath, clone);
}
@@ -241,14 +174,9 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
if (prev != null) {
byNamespaceAndName.remove(prev.getNamespaceAndName());
}
path = pathById.remove(repository.getId());
writePathDatabase();
path = repositoryLocationResolver.remove(repository.getId());
}
path = context.resolve(path);
try {
fileSystem.destroy(path.toFile());
} catch (IOException e) {
@@ -257,7 +185,12 @@ public class XmlRepositoryDAO implements PathBasedRepositoryDAO {
}
@Override
public Path getPath(String repositoryId) {
return pathById.get(repositoryId);
public Long getCreationTime() {
return repositoryLocationResolver.getCreationTime();
}
@Override
public Long getLastModified() {
return repositoryLocationResolver.getLastModified();
}
}

View File

@@ -40,6 +40,7 @@ import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.util.IOUtil;
import java.io.File;
import java.nio.file.Path;
//~--- JDK imports ------------------------------------------------------------
@@ -58,7 +59,7 @@ public abstract class FileBasedStoreFactory {
private RepositoryLocationResolver repositoryLocationResolver;
private Store store;
protected FileBasedStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, Store store) {
protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store) {
this.contextProvider = contextProvider;
this.repositoryLocationResolver = repositoryLocationResolver;
this.store = store;
@@ -92,7 +93,7 @@ public abstract class FileBasedStoreFactory {
* @return the store directory of a specific repository
*/
private File getStoreDirectory(Store store, Repository repository) {
return new File(repositoryLocationResolver.getPath(repository.getId()).toFile(), store.getRepositoryStoreDirectory());
return new File(repositoryLocationResolver.forClass(Path.class).getLocation(repository.getId()).toFile(), store.getRepositoryStoreDirectory());
}
/**

View File

@@ -65,7 +65,7 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS
* @param keyGenerator key generator
*/
@Inject
public FileBlobStoreFactory(SCMContextProvider contextProvider ,RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator) {
super(contextProvider, repositoryLocationResolver, Store.BLOB);
this.keyGenerator = keyGenerator;
}

View File

@@ -0,0 +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.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 sonia.scm.SCMContextProvider;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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(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(@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();
}
@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<String, Path> 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);
}
}
}

View File

@@ -2,376 +2,309 @@ package sonia.scm.repository.xml;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
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.junitpioneer.jupiter.TempDirectory;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
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 sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Clock;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith({MockitoExtension.class, TempDirectory.class})
@MockitoSettings(strictness = Strictness.LENIENT)
class XmlRepositoryDAOTest {
@Mock
private SCMContextProvider context;
private final Repository REPOSITORY = createRepository("42");
@Mock
private InitialRepositoryLocationResolver locationResolver;
private PathBasedRepositoryLocationResolver locationResolver;
@Captor
private ArgumentCaptor<BiConsumer<String, Path>> forAllCaptor;
private FileSystem fileSystem = new DefaultFileSystem();
private XmlRepositoryDAO dao;
private Path baseDirectory;
private AtomicLong atomicClock;
@BeforeEach
void createDAO(@TempDirectory.TempDir Path baseDirectory) {
this.baseDirectory = baseDirectory;
this.atomicClock = new AtomicLong();
when(locationResolver.getPath("42")).thenReturn(Paths.get("repos", "42"));
when(locationResolver.getPath("42+1")).thenReturn(Paths.get("repos", "puzzle"));
when(context.getBaseDirectory()).thenReturn(baseDirectory.toFile());
when(context.resolve(any(Path.class))).then(ic -> {
Path path = ic.getArgument(0);
return baseDirectory.resolve(path);
});
dao = createDAO();
void createDAO(@TempDirectory.TempDir Path basePath) {
when(locationResolver.create(Path.class)).thenReturn(locationResolver::create);
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation));
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
}
private XmlRepositoryDAO createDAO() {
Clock clock = mock(Clock.class);
when(clock.millis()).then(ic -> atomicClock.incrementAndGet());
private Path createMockedRepoPath(@TempDirectory.TempDir Path basePath, InvocationOnMock invocation) {
Path resolvedPath = basePath.resolve(invocation.getArgument(0).toString());
try {
Files.createDirectories(resolvedPath);
} catch (IOException e) {
fail(e);
}
return resolvedPath;
}
return new XmlRepositoryDAO(context, locationResolver, fileSystem, clock);
@Nested
class WithEmptyDatabase {
@BeforeEach
void createDAO() {
dao = new XmlRepositoryDAO(locationResolver, fileSystem);
}
@Test
void shouldReturnXmlType() {
assertThat(dao.getType()).isEqualTo("xml");
}
@Test
void shouldReturnCreationTimeOfLocationResolver() {
long now = 42L;
when(locationResolver.getCreationTime()).thenReturn(now);
assertThat(dao.getCreationTime()).isEqualTo(now);
}
@Test
void shouldReturnLasModifiedOfLocationResolver() {
long now = 42L;
when(locationResolver.getLastModified()).thenReturn(now);
assertThat(dao.getLastModified()).isEqualTo(now);
}
@Test
void shouldReturnTrueForEachContainsMethod() {
dao.add(REPOSITORY);
assertThat(dao.contains(REPOSITORY)).isTrue();
assertThat(dao.contains(REPOSITORY.getId())).isTrue();
assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isTrue();
}
@Test
void shouldPersistRepository() {
dao.add(REPOSITORY);
String content = getXmlFileContent(REPOSITORY.getId());
assertThat(content).contains("<id>42</id>");
}
@Test
void shouldDeleteDataFile() {
dao.add(REPOSITORY);
dao.delete(REPOSITORY);
assertThat(metadataFile(REPOSITORY.getId())).doesNotExist();
}
@Test
void shouldModifyRepository() {
dao.add(REPOSITORY);
Repository changedRepository = REPOSITORY.clone();
changedRepository.setContact("change");
dao.modify(changedRepository);
String content = getXmlFileContent(REPOSITORY.getId());
assertThat(content).contains("change");
}
@Test
void shouldReturnFalseForEachContainsMethod() {
assertThat(dao.contains(REPOSITORY)).isFalse();
assertThat(dao.contains(REPOSITORY.getId())).isFalse();
assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse();
}
@Test
void shouldReturnNullForEachGetMethod() {
assertThat(dao.get("42")).isNull();
assertThat(dao.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isNull();
}
@Test
void shouldReturnRepository() {
dao.add(REPOSITORY);
assertThat(dao.get("42")).isEqualTo(REPOSITORY);
assertThat(dao.get(new NamespaceAndName("space", "42"))).isEqualTo(REPOSITORY);
}
@Test
void shouldNotReturnTheSameInstance() {
dao.add(REPOSITORY);
Repository repository = dao.get("42");
assertThat(repository).isNotSameAs(REPOSITORY);
}
@Test
void shouldReturnAllRepositories() {
dao.add(REPOSITORY);
Repository secondRepository = createRepository("23");
dao.add(secondRepository);
Collection<Repository> repositories = dao.getAll();
assertThat(repositories)
.containsExactlyInAnyOrder(REPOSITORY, secondRepository);
}
@Test
void shouldModifyRepositoryTwice() {
REPOSITORY.setDescription("HeartOfGold");
dao.add(REPOSITORY);
assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold");
Repository heartOfGold = createRepository("42");
heartOfGold.setDescription("Heart of Gold");
dao.modify(heartOfGold);
assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold");
}
@Test
void shouldRemoveRepository() {
dao.add(REPOSITORY);
assertThat(dao.contains("42")).isTrue();
dao.delete(REPOSITORY);
assertThat(dao.contains("42")).isFalse();
assertThat(dao.contains(REPOSITORY.getNamespaceAndName())).isFalse();
Path storePath = metadataFile(REPOSITORY.getId());
assertThat(storePath).doesNotExist();
}
@Test
void shouldRenameTheRepository() {
dao.add(REPOSITORY);
REPOSITORY.setNamespace("hg2tg");
REPOSITORY.setName("hog");
dao.modify(REPOSITORY);
Repository repository = dao.get("42");
assertThat(repository.getNamespace()).isEqualTo("hg2tg");
assertThat(repository.getName()).isEqualTo("hog");
assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue();
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
String content = getXmlFileContent(REPOSITORY.getId());
assertThat(content).contains("<name>hog</name>");
}
@Test
void shouldDeleteRepositoryEvenWithChangedNamespace() {
dao.add(REPOSITORY);
REPOSITORY.setNamespace("hg2tg");
REPOSITORY.setName("hog");
dao.delete(REPOSITORY);
assertThat(dao.contains(new NamespaceAndName("space", "42"))).isFalse();
}
@Test
void shouldRemoveRepositoryDirectoryAfterDeletion() {
dao.add(REPOSITORY);
Path path = locationResolver.create(REPOSITORY.getId());
assertThat(path).isDirectory();
dao.delete(REPOSITORY);
assertThat(path).doesNotExist();
}
@Test
void shouldPersistPermissions() {
REPOSITORY.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", singletonList("delete"), true)));
dao.add(REPOSITORY);
String content = getXmlFileContent(REPOSITORY.getId());
assertThat(content).containsSubsequence("trillian", "<verb>read</verb>", "<verb>write</verb>");
assertThat(content).containsSubsequence("vogons", "<verb>delete</verb>");
}
@Test
void shouldUpdateRepositoryPathDatabse() {
dao.add(REPOSITORY);
verify(locationResolver, never()).updateModificationDate();
dao.modify(REPOSITORY);
verify(locationResolver).updateModificationDate();
}
}
@Test
void shouldReturnXmlType() {
assertThat(dao.getType()).isEqualTo("xml");
void shouldReadExistingRepositoriesFromPathDatabase(@TempDirectory.TempDir Path basePath) throws IOException {
doNothing().when(locationResolver).forAllPaths(forAllCaptor.capture());
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem);
Path repositoryPath = basePath.resolve("existing");
Files.createDirectories(repositoryPath);
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
forAllCaptor.getValue().accept("existing", repositoryPath);
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
}
@Test
void shouldReturnCreationTimeAfterCreation() {
long now = atomicClock.get();
assertThat(dao.getCreationTime()).isEqualTo(now);
}
private String getXmlFileContent(String id) {
Path storePath = metadataFile(id);
@Test
void shouldNotReturnLastModifiedAfterCreation() {
assertThat(dao.getLastModified()).isNull();
}
@Test
void shouldReturnTrueForEachContainsMethod() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
assertThat(dao.contains(heartOfGold)).isTrue();
assertThat(dao.contains(heartOfGold.getId())).isTrue();
assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isTrue();
}
private Repository createHeartOfGold() {
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
heartOfGold.setId("42");
return heartOfGold;
}
@Test
void shouldReturnFalseForEachContainsMethod() {
Repository heartOfGold = createHeartOfGold();
assertThat(dao.contains(heartOfGold)).isFalse();
assertThat(dao.contains(heartOfGold.getId())).isFalse();
assertThat(dao.contains(heartOfGold.getNamespaceAndName())).isFalse();
}
@Test
void shouldReturnNullForEachGetMethod() {
assertThat(dao.get("42")).isNull();
assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isNull();
}
@Test
void shouldReturnRepository() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
assertThat(dao.get("42")).isEqualTo(heartOfGold);
assertThat(dao.get(new NamespaceAndName("hitchhiker","HeartOfGold"))).isEqualTo(heartOfGold);
}
@Test
void shouldNotReturnTheSameInstance() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Repository repository = dao.get("42");
assertThat(repository).isNotSameAs(heartOfGold);
}
@Test
void shouldReturnAllRepositories() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Repository puzzle = createPuzzle();
dao.add(puzzle);
Collection<Repository> repositories = dao.getAll();
assertThat(repositories).containsExactlyInAnyOrder(heartOfGold, puzzle);
}
private Repository createPuzzle() {
Repository puzzle = RepositoryTestData.create42Puzzle();
puzzle.setId("42+1");
return puzzle;
}
@Test
void shouldModifyRepository() {
Repository heartOfGold = createHeartOfGold();
heartOfGold.setDescription("HeartOfGold");
dao.add(heartOfGold);
assertThat(dao.get("42").getDescription()).isEqualTo("HeartOfGold");
heartOfGold = createHeartOfGold();
heartOfGold.setDescription("Heart of Gold");
dao.modify(heartOfGold);
assertThat(dao.get("42").getDescription()).isEqualTo("Heart of Gold");
}
@Test
void shouldRemoveRepository() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
assertThat(dao.contains("42")).isTrue();
dao.delete(heartOfGold);
assertThat(dao.contains("42")).isFalse();
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
}
@Test
void shouldUpdateLastModifiedAfterEachWriteOperation() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Long firstLastModified = dao.getLastModified();
assertThat(firstLastModified).isNotNull();
Repository puzzle = createPuzzle();
dao.add(puzzle);
Long lastModifiedAdded = dao.getLastModified();
assertThat(lastModifiedAdded).isGreaterThan(firstLastModified);
heartOfGold.setDescription("Heart of Gold");
dao.modify(heartOfGold);
Long lastModifiedModified = dao.getLastModified();
assertThat(lastModifiedModified).isGreaterThan(lastModifiedAdded);
dao.delete(puzzle);
Long lastModifiedRemoved = dao.getLastModified();
assertThat(lastModifiedRemoved).isGreaterThan(lastModifiedModified);
}
@Test
void shouldRenameTheRepository() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
heartOfGold.setNamespace("hg2tg");
heartOfGold.setName("hog");
dao.modify(heartOfGold);
Repository repository = dao.get("42");
assertThat(repository.getNamespace()).isEqualTo("hg2tg");
assertThat(repository.getName()).isEqualTo("hog");
assertThat(dao.contains(new NamespaceAndName("hg2tg", "hog"))).isTrue();
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
}
@Test
void shouldDeleteRepositoryEvenWithChangedNamespace() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
heartOfGold.setNamespace("hg2tg");
heartOfGold.setName("hog");
dao.delete(heartOfGold);
assertThat(dao.contains(new NamespaceAndName("hitchhiker", "HeartOfGold"))).isFalse();
}
@Test
void shouldReturnThePathForTheRepository() {
Path repositoryPath = Paths.get("r", "42");
when(locationResolver.getPath("42")).thenReturn(repositoryPath);
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path path = dao.getPath("42");
assertThat(path).isEqualTo(repositoryPath);
}
@Test
void shouldCreateTheDirectoryForTheRepository() {
Path repositoryPath = Paths.get("r", "42");
when(locationResolver.getPath("42")).thenReturn(repositoryPath);
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path path = getAbsolutePathFromDao("42");
assertThat(path).isDirectory();
}
@Test
void shouldRemoveRepositoryDirectoryAfterDeletion() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path path = getAbsolutePathFromDao(heartOfGold.getId());
assertThat(path).isDirectory();
dao.delete(heartOfGold);
assertThat(path).doesNotExist();
}
private Path getAbsolutePathFromDao(String id) {
return context.resolve(dao.getPath(id));
}
@Test
void shouldCreateRepositoryPathDatabase() throws IOException {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path storePath = dao.resolveStorePath();
assertThat(storePath).isRegularFile();
String content = content(storePath);
assertThat(content).contains(heartOfGold.getId());
assertThat(content).contains(dao.getPath(heartOfGold.getId()).toString());
return content(storePath);
}
private String content(Path storePath) throws IOException {
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
private Path metadataFile(String id) {
return locationResolver.create(id).resolve("metadata.xml");
}
@Test
void shouldStoreRepositoryMetadataAfterAdd() throws IOException {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId());
Path metadataPath = dao.resolveMetadataPath(repositoryDirectory);
assertThat(metadataPath).isRegularFile();
String content = content(metadataPath);
assertThat(content).contains(heartOfGold.getName());
assertThat(content).contains(heartOfGold.getNamespace());
assertThat(content).contains(heartOfGold.getDescription());
private String content(Path storePath) {
try {
return new String(Files.readAllBytes(storePath), Charsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
void shouldUpdateRepositoryMetadataAfterModify() throws IOException {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
heartOfGold.setDescription("Awesome Spaceship");
dao.modify(heartOfGold);
Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId());
Path metadataPath = dao.resolveMetadataPath(repositoryDirectory);
String content = content(metadataPath);
assertThat(content).contains("Awesome Spaceship");
}
@Test
void shouldPersistPermissions() throws IOException {
Repository heartOfGold = createHeartOfGold();
heartOfGold.setPermissions(asList(new RepositoryPermission("trillian", asList("read", "write"), false), new RepositoryPermission("vogons", Collections.singletonList("delete"), true)));
dao.add(heartOfGold);
Path repositoryDirectory = getAbsolutePathFromDao(heartOfGold.getId());
Path metadataPath = dao.resolveMetadataPath(repositoryDirectory);
String content = content(metadataPath);
System.out.println(content);
assertThat(content).containsSubsequence("trillian", "<verb>read</verb>", "<verb>write</verb>");
assertThat(content).containsSubsequence("vogons", "<verb>delete</verb>");
}
@Test
void shouldReadPathDatabaseAndMetadataOfRepositories() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
// reload data
dao = createDAO();
heartOfGold = dao.get("42");
assertThat(heartOfGold.getName()).isEqualTo("HeartOfGold");
Path path = getAbsolutePathFromDao(heartOfGold.getId());
assertThat(path).isDirectory();
}
@Test
void shouldReadCreationTimeAndLastModifedDateFromDatabase() {
Repository heartOfGold = createHeartOfGold();
dao.add(heartOfGold);
Long creationTime = dao.getCreationTime();
Long lastModified = dao.getLastModified();
// reload data
dao = createDAO();
assertThat(dao.getCreationTime()).isEqualTo(creationTime);
assertThat(dao.getLastModified()).isEqualTo(lastModified);
private static Repository createRepository(String id) {
return new Repository(id, "xml", "space", id);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<repositories>
<properties/>
<id>existing</id>
<namespace>space</namespace>
<name>existing</name>
<public>false</public>
<archived>false</archived>
<type>xml</type>
</repositories>