Add queryable store with SQLite implementation

This adds the new "queryable store" API, that allows complex
queries and is backed by SQLite. This new API can be used
for entities annotated with the new QueryableType annotation.
This commit is contained in:
Rene Pfeuffer
2025-04-01 16:18:04 +02:00
parent d5362d634b
commit ada575d871
235 changed files with 10154 additions and 252 deletions

View File

@@ -0,0 +1,257 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
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.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.Spy;
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.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryLocationResolver.RepositoryLocationResolverInstance;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.DownForMaintenanceContext;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.UpAfterMaintenanceContext;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.time.Clock;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.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 final FileSystem fileSystem = new DefaultFileSystem();
private Path basePath;
private PathBasedRepositoryLocationResolver resolver;
@BeforeEach
void beforeEach(@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).createLocation("newId");
assertThat(path).isEqualTo(basePath.resolve("newId"));
assertThat(path).isDirectory();
}
@Test
void shouldFailIfDirectoryExists() throws IOException {
Files.createDirectories(basePath.resolve("newId"));
RepositoryLocationResolverInstance<Path> resolverInstance = resolver.forClass(Path.class);
assertThatThrownBy(() -> resolverInstance.createLocation("newId"))
.isInstanceOf(RepositoryLocationResolver.RepositoryStorageException.class);
}
@Test
void shouldPersistInitialDirectory() {
resolver.forClass(Path.class).createLocation("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).createLocation("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).createLocation("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;
@Spy
private PathBasedRepositoryLocationResolver.MaintenanceCallback maintenanceCallback;
@BeforeEach
void createExistingDatabase() {
resolver.forClass(Path.class).createLocation("existingId_1");
resolver.forClass(Path.class).createLocation("existingId_2");
resolverWithExistingData = createResolver();
resolverWithExistingData.registerMaintenanceCallback(maintenanceCallback);
}
@Test
void shouldInitWithExistingData() {
Map<String, Path> foundRepositories = new HashMap<>();
resolverWithExistingData.forClass(Path.class).forAllLocations(
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();
}
@Test
void shouldModifyLocation() throws IOException {
Path oldPath = resolverWithExistingData.create(Path.class).getLocation("existingId_1");
Path newPath = basePath.resolve("modified_location");
resolverWithExistingData.create(Path.class).modifyLocation("existingId_1", newPath);
assertThat(newPath).exists();
assertThat(oldPath).doesNotExist();
assertThat(resolverWithExistingData.create(Path.class).getLocation("existingId_1")).isEqualTo(newPath);
verify(maintenanceCallback).downForMaintenance(new DownForMaintenanceContext("existingId_1"));
verify(maintenanceCallback).upAfterMaintenance(new UpAfterMaintenanceContext("existingId_1", newPath));
}
@Test
void shouldModifyLocationAndKeepOld() throws IOException {
Path oldPath = resolverWithExistingData.create(Path.class).getLocation("existingId_1");
Path newPath = basePath.resolve("modified_location");
resolverWithExistingData.create(Path.class).modifyLocationAndKeepOld("existingId_1", newPath);
assertThat(newPath).exists();
assertThat(oldPath).exists();
assertThat(resolverWithExistingData.create(Path.class).getLocation("existingId_1")).isEqualTo(newPath);
verify(maintenanceCallback).downForMaintenance(new DownForMaintenanceContext("existingId_1"));
verify(maintenanceCallback).upAfterMaintenance(new UpAfterMaintenanceContext("existingId_1", newPath));
}
@Test
void shouldHandleErrorOnModifyLocation() throws IOException {
Path oldPath = resolverWithExistingData.create(Path.class).getLocation("existingId_1");
Path newPath = basePath.resolve("thou").resolve("shall").resolve("not").resolve("move").resolve("here");
Files.createDirectories(newPath);
Files.setPosixFilePermissions(newPath, Set.of(PosixFilePermission.OWNER_READ));
assertThatThrownBy(() -> resolverWithExistingData.create(Path.class).modifyLocationAndKeepOld("existingId_1", newPath))
.isInstanceOf(RepositoryLocationResolver.RepositoryStorageException.class);
assertThat(newPath).exists();
assertThat(oldPath).exists();
assertThat(resolverWithExistingData.create(Path.class).getLocation("existingId_1")).isEqualTo(oldPath);
}
}
private String getXmlFileContent() {
Path storePath = basePath.resolve("config").resolve("repository-paths.xml");
assertThat(storePath).isRegularFile();
return content(storePath);
}
private PathBasedRepositoryLocationResolver createResolver() {
return new PathBasedRepositoryLocationResolver(contextProvider, initialRepositoryLocationResolver, fileSystem, 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

@@ -0,0 +1,110 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.xml;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
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.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryExportingCheck;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static java.util.Collections.emptySet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class XmlRepositoryDAOSynchronizationTest {
private static final int CREATION_COUNT = 100;
private static final long TIMEOUT = 10L;
@Mock
private SCMContextProvider provider;
@Mock
private RepositoryExportingCheck repositoryExportingCheck;
private FileSystem fileSystem;
private PathBasedRepositoryLocationResolver resolver;
private XmlRepositoryDAO repositoryDAO;
@BeforeEach
void setUpObjectUnderTest(@TempDir Path path) {
when(provider.getBaseDirectory()).thenReturn(path.toFile());
when(provider.resolve(any())).then(ic -> {
Path args = ic.getArgument(0);
return path.resolve(args);
});
fileSystem = new DefaultFileSystem();
resolver = new PathBasedRepositoryLocationResolver(
provider, new InitialRepositoryLocationResolver(emptySet()), fileSystem
);
repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck);
}
@Test
@Timeout(TIMEOUT)
void shouldCreateALotOfRepositoriesInSerial() {
for (int i=0; i<CREATION_COUNT; i++) {
repositoryDAO.add(new Repository("repo_" + i, "git", "sync_it", "repo_" + i));
}
assertCreated();
}
private void assertCreated() {
XmlRepositoryDAO assertionDao = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck);
assertThat(assertionDao.getAll()).hasSize(CREATION_COUNT);
}
@Test
@Timeout(TIMEOUT)
void shouldCreateALotOfRepositoriesInParallel() throws InterruptedException {
ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
final XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck);
for (int i=0; i<CREATION_COUNT; i++) {
executors.submit(create(repositoryDAO, i));
}
executors.shutdown();
executors.awaitTermination(TIMEOUT, TimeUnit.SECONDS);
assertCreated();
}
private Runnable create(XmlRepositoryDAO repositoryDAO, int index) {
return () -> repositoryDAO.add(new Repository("repo_" + index, "git", "sync_it", "repo_" + index));
}
}

View File

@@ -0,0 +1,498 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
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.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.io.DefaultFileSystem;
import sonia.scm.io.FileSystem;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryExportingCheck;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.DownForMaintenanceContext;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.UpAfterMaintenanceContext;
import sonia.scm.store.StoreReadOnlyException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
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})
@MockitoSettings(strictness = Strictness.LENIENT)
class XmlRepositoryDAOTest {
private final Repository REPOSITORY = createRepository("42");
@Mock
private PathBasedRepositoryLocationResolver locationResolver;
private Consumer<BiConsumer<String, Path>> triggeredOnForAllLocations = none -> {};
@Mock
private RepositoryExportingCheck repositoryExportingCheck;
private final FileSystem fileSystem = new DefaultFileSystem();
private XmlRepositoryDAO dao;
@BeforeEach
void createDAO(@TempDir Path basePath) {
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) {
}
@Override
public void forAllLocations(BiConsumer<String, Path> consumer) {
triggeredOnForAllLocations.accept(consumer);
}
}
);
when(locationResolver.create(any(Repository.class))).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation.getArgument(0, Repository.class).getId()));
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation.getArgument(0, String.class)));
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
}
private static Path createMockedRepoPath(Path basePath, String repositoryId) {
Path resolvedPath = basePath.resolve(repositoryId);
try {
Files.createDirectories(resolvedPath);
} catch (IOException e) {
fail(e);
}
return resolvedPath;
}
@Nested
class WithEmptyDatabase {
@BeforeEach
void createDAO() {
dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
}
@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 shouldNotModifyArchivedRepository() {
REPOSITORY.setArchived(true);
dao.add(REPOSITORY);
Repository heartOfGold = createRepository("42");
heartOfGold.setArchived(true);
assertThrows(StoreReadOnlyException.class, () -> dao.modify(heartOfGold));
}
@Test
void shouldNotModifyExportingRepository() {
when(repositoryExportingCheck.isExporting(REPOSITORY)).thenReturn(true);
dao.add(REPOSITORY);
Repository heartOfGold = createRepository("42");
assertThrows(StoreReadOnlyException.class, () -> dao.modify(heartOfGold));
}
@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 shouldNotRemoveArchivedRepository() {
REPOSITORY.setArchived(true);
dao.add(REPOSITORY);
assertThat(dao.contains("42")).isTrue();
assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY));
}
@Test
void shouldNotRemoveExportingRepository() {
when(repositoryExportingCheck.isExporting(REPOSITORY)).thenReturn(true);
dao.add(REPOSITORY);
assertThat(dao.contains("42")).isTrue();
assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY));
}
@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>")
.containsSubsequence("vogons", "<verb>delete</verb>");
}
@Test
void shouldUpdateRepositoryPathDatabse() {
dao.add(REPOSITORY);
verify(locationResolver, never()).updateModificationDate();
dao.modify(REPOSITORY);
verify(locationResolver).updateModificationDate();
}
@Test
void shouldGetAllWithCorrectSorting() {
dao.add(createRepository("banana1", "banana", "red"));
dao.add(createRepository("banana2", "banana.venezuela", "red"));
Collection<Repository> repositories = dao.getAll();
assertThat(repositories)
.hasSize(2)
.extracting("id").containsExactly("banana1", "banana2");
}
private String getXmlFileContent(String id) {
Path storePath = metadataFile(id);
assertThat(storePath).isRegularFile();
return content(storePath);
}
private Path metadataFile(String id) {
return locationResolver.create(id).resolve("metadata.xml");
}
private String content(Path storePath) {
try {
return Files.readString(storePath, Charsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Nested
class WithExistingRepositories {
private Path repositoryPath;
@Captor
private ArgumentCaptor<PathBasedRepositoryLocationResolver.MaintenanceCallback> callbackArgumentCaptor;
@BeforeEach
void createMetadataFileForRepository(@TempDir Path basePath) throws IOException {
repositoryPath = basePath.resolve("existing");
prepareRepositoryPath(repositoryPath);
}
@Test
void shouldReadExistingRepositoriesFromPathDatabase() {
// given
mockExistingPath();
// when
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
// then
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
}
@Test
void shouldRefreshWithExistingRepositoriesFromPathDatabase() {
// given
mockExistingPath();
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
// when
dao.refresh();
// then
verify(locationResolver).refresh();
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
}
@Test
void shouldHandleMaintenanceEvents() {
doNothing().when(locationResolver).registerMaintenanceCallback(callbackArgumentCaptor.capture());
mockExistingPath();
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
callbackArgumentCaptor.getValue().downForMaintenance(new DownForMaintenanceContext("existing"));
assertThat(dao.contains("existing")).isFalse();
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isFalse();
assertThat(dao.getAll()).isEmpty();
callbackArgumentCaptor.getValue().upAfterMaintenance(new UpAfterMaintenanceContext("existing", repositoryPath));
assertThat(dao.contains("existing")).isTrue();
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
assertThat(dao.getAll()).hasSize(1);
}
private void mockExistingPath() {
triggeredOnForAllLocations = consumer -> consumer.accept("existing", repositoryPath);
}
}
@Nested
class WithDuplicateRepositories {
private Path repositoryPath;
private Path duplicateRepositoryPath;
@BeforeEach
void createMetadataFileForRepository(@TempDir Path basePath) throws IOException {
repositoryPath = basePath.resolve("existing");
duplicateRepositoryPath = basePath.resolve("duplicate");
prepareRepositoryPath(repositoryPath);
prepareRepositoryPath(duplicateRepositoryPath);
}
@Test
void shouldRenameDuplicateRepositories() {
mockExistingPath();
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
assertThat(dao.contains(new NamespaceAndName("space", "existing-existing2-DUPLICATE"))).isTrue();
}
private void mockExistingPath() {
triggeredOnForAllLocations = consumer -> {
consumer.accept("existing", repositoryPath);
consumer.accept("existing2", duplicateRepositoryPath);
};
}
}
private void prepareRepositoryPath(Path repositoryPath) throws IOException {
Files.createDirectories(repositoryPath);
URL metadataUrl = Resources.getResource("sonia/scm/store/repositoryDaoMetadata.xml");
Files.copy(metadataUrl.openStream(), repositoryPath.resolve("metadata.xml"));
}
private Repository createRepository(String id, String namespace, String name) {
return new Repository(id, "xml", namespace, name);
}
private Repository createRepository(String id) {
return createRepository(id, "space", id);
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store;
import org.junit.jupiter.api.BeforeEach;
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.repository.RepositoryLocationResolver;
import sonia.scm.update.StoreUpdateStepUtilFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class)
class FileStoreUpdateStepUtilFactoryTest {
@Mock
private RepositoryLocationResolver locationResolver;
@Mock
private RepositoryLocationResolver.RepositoryLocationResolverInstance locationResolverInstance;
@Mock
private SCMContextProvider contextProvider;
@InjectMocks
private FileStoreUpdateStepUtilFactory factory;
private Path globalPath;
private Path repositoryPath;
@BeforeEach
void initPaths(@TempDir Path temp) throws IOException {
globalPath = temp.resolve("global");
Files.createDirectories(globalPath);
lenient().doReturn(globalPath.toFile()).when(contextProvider).getBaseDirectory();
repositoryPath = temp.resolve("repo");
Files.createDirectories(repositoryPath);
lenient().doReturn(true).when(locationResolver).supportsLocationType(Path.class);
lenient().doReturn(locationResolverInstance).when(locationResolver).forClass(Path.class);
lenient().doReturn(repositoryPath).when(locationResolverInstance).getLocation("repo-id");
}
@Test
void shouldMoveGlobalDataDirectory() throws IOException {
Path dataPath = globalPath.resolve("var").resolve("data");
Files.createDirectories(dataPath.resolve("something"));
Files.createFile(dataPath.resolve("something").resolve("some.file"));
StoreUpdateStepUtilFactory.StoreUpdateStepUtil util =
factory
.forType(StoreType.DATA)
.forName("something")
.build();
util.renameStore("new-name");
assertThat(dataPath.resolve("new-name").resolve("some.file")).exists();
assertThat(dataPath.resolve("something")).doesNotExist();
}
@Test
void shouldMoveRepositoryDataDirectory() throws IOException {
Path dataPath = repositoryPath.resolve("store").resolve("data");
Files.createDirectories(dataPath.resolve("something"));
Files.createFile(dataPath.resolve("something").resolve("some.file"));
StoreUpdateStepUtilFactory.StoreUpdateStepUtil util =
factory
.forType(StoreType.DATA)
.forName("something")
.forRepository("repo-id")
.build();
util.renameStore("new-name");
assertThat(dataPath.resolve("new-name").resolve("some.file")).exists();
assertThat(dataPath.resolve("something")).doesNotExist();
}
@Test
void shouldHandleMissingMoveGlobalDataDirectory() throws IOException {
StoreUpdateStepUtilFactory.StoreUpdateStepUtil util =
factory
.forType(StoreType.DATA)
.forName("something")
.build();
util.renameStore("new-name");
assertThat(globalPath.resolve("new-name")).doesNotExist();
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryTestData;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class RepositoryStoreImporterTest {
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private RepositoryLocationResolver locationResolver;
@InjectMocks
private RepositoryStoreImporter repositoryStoreImporter;
@Test
void shouldImportStore() {
StoreEntryImporterFactory storeEntryImporterFactory = repositoryStoreImporter.doImport(REPOSITORY);
assertThat(storeEntryImporterFactory).isInstanceOf(StoreEntryImporterFactory.class);
}
}

View File

@@ -0,0 +1,161 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.store.StoreException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static sonia.scm.CopyOnWrite.withTemporaryFile;
class CopyOnWriteTest {
@Test
void shouldCreateNewFile(@TempDir Path tempDir) {
Path expectedFile = tempDir.resolve("toBeCreated.txt");
withTemporaryFile(file -> {
try (OutputStream os = new FileOutputStream(file.toFile())) {
os.write("great success".getBytes());
}
}, expectedFile);
assertThat(expectedFile).hasContent("great success");
}
@Test
void shouldOverwriteExistingFile(@TempDir Path tempDir) throws IOException {
Path expectedFile = tempDir.resolve("toBeOverwritten.txt");
Files.createFile(expectedFile);
withTemporaryFile(file -> {
try (OutputStream os = new FileOutputStream(file.toFile())) {
os.write("great success".getBytes());
}
}, expectedFile);
assertThat(expectedFile).hasContent("great success");
}
@Test
void shouldFailForDirectory(@TempDir Path tempDir) {
assertThrows(IllegalArgumentException.class, () -> withTemporaryFile(file -> {
try (OutputStream os = new FileOutputStream(file.toFile())) {
os.write("should not be written".getBytes());
}
}, tempDir));
}
@Test
void shouldFailForMissingDirectory() {
assertThrows(IllegalArgumentException.class, () -> withTemporaryFile(file -> {
try (OutputStream os = new FileOutputStream(file.toFile())) {
os.write("should not be written".getBytes());
}
}, Paths.get("someFile")));
}
@Test
void shouldKeepBackupIfTemporaryFileCouldNotBeWritten(@TempDir Path tempDir) throws IOException {
Path unchangedOriginalFile = tempDir.resolve("notToBeDeleted.txt");
try (OutputStream unchangedOriginalOs = new FileOutputStream(unchangedOriginalFile.toFile())) {
unchangedOriginalOs.write("this should be kept".getBytes());
}
assertThrows(
StoreException.class,
() -> withTemporaryFile(
file -> {
throw new IOException("test");
},
unchangedOriginalFile));
assertThat(unchangedOriginalFile).hasContent("this should be kept");
}
@Test
void shouldDeleteTemporaryFileIfFileCouldNotBeWritten(@TempDir Path tempDir) throws IOException {
Path unchangedOriginalFile = tempDir.resolve("target.txt");
assertThrows(
StoreException.class,
() -> withTemporaryFile(
file -> {
throw new IOException("test");
},
unchangedOriginalFile));
assertThat(tempDir).isEmptyDirectory();
}
@Test
void shouldNotWrapRuntimeExceptions(@TempDir Path tempDir) throws IOException {
Path someFile = tempDir.resolve("something.txt");
assertThrows(
NullPointerException.class,
() -> withTemporaryFile(
file -> {
throw new NullPointerException("test");
},
someFile));
}
@Test
void shouldKeepBackupIfTemporaryFileIsMissing(@TempDir Path tempDir) throws IOException {
Path backedUpFile = tempDir.resolve("notToBeDeleted.txt");
try (OutputStream backedUpOs = new FileOutputStream(backedUpFile.toFile())) {
backedUpOs.write("this should be kept".getBytes());
}
assertThrows(
StoreException.class,
() -> withTemporaryFile(
Files::delete,
backedUpFile));
assertThat(backedUpFile).hasContent("this should be kept");
}
@Test
void shouldDeleteExistingFile(@TempDir Path tempDir) throws IOException {
Path expectedFile = tempDir.resolve("toBeReplaced.txt");
try (OutputStream expectedOs = new FileOutputStream(expectedFile.toFile())) {
expectedOs.write("this should be removed".getBytes());
}
withTemporaryFile(file -> {
try (OutputStream os = new FileOutputStream(file.toFile())) {
os.write("overwritten".getBytes()) ;
}
}, expectedFile);
assertThat(Files.list(tempDir)).hasSize(1);
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.cache.MapCache;
import java.io.File;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class DataFileCacheTest {
@Nested
class WithActivatedCache {
private final MapCache<File, Object> backingCache = new MapCache<>();
private final DataFileCache dataFileCache = new DataFileCache(backingCache, true);
@Test
void shouldReturnCachedData() {
File file = new File("/some.string");
backingCache.put(file, "some string");
DataFileCache.DataFileCacheInstance instance = dataFileCache.instanceFor(String.class);
Object result = instance.get(file, () -> {
throw new RuntimeException("should not be read");
});
assertThat(result).isSameAs("some string");
}
@Test
void shouldReadDataIfNotCached() {
File file = new File("/some.string");
DataFileCache.DataFileCacheInstance instance = dataFileCache.instanceFor(String.class);
Object result = instance.get(file, () -> "some string");
assertThat(result).isSameAs("some string");
assertThat(backingCache.get(file)).isSameAs("some string");
}
@Test
void shouldReadDataAnewIfOfDifferentType() {
File file = new File("/some.string");
backingCache.put(file, 42);
DataFileCache.DataFileCacheInstance instance = dataFileCache.instanceFor(String.class);
Object result = instance.get(file, () -> "some string");
assertThat(result).isSameAs("some string");
assertThat(backingCache.get(file)).isSameAs("some string");
}
@Test
void shouldRemoveOutdatedDataIfOfDifferentType() {
File file = new File("/some.string");
backingCache.put(file, 42);
DataFileCache.DataFileCacheInstance instance = dataFileCache.instanceFor(String.class);
Object result = instance.get(file, () -> null);
assertThat(result).isNull();
assertThat(backingCache.get(file)).isNull();
}
@Test
void shouldCacheNewData() {
File file = new File("/some.string");
DataFileCache.DataFileCacheInstance instance = dataFileCache.instanceFor(String.class);
instance.put(file, "some string");
assertThat(backingCache.get(file)).isSameAs("some string");
}
@Test
void shouldRemoveDataFromCache() {
File file = new File("/some.string");
backingCache.put(file, "some string");
DataFileCache.DataFileCacheInstance instance = dataFileCache.instanceFor(String.class);
instance.remove(file);
assertThat(backingCache.get(file)).isNull();
}
}
@Nested
class WithDeactivatedCache {
private final DataFileCache dataFileCache = new DataFileCache(null, false);
@Test
void shouldReadData() {
File file = new File("/some.string");
DataFileCache.DataFileCacheInstance instance = dataFileCache.instanceFor(String.class);
Object result = instance.get(file, () -> "some string");
assertThat(result).isSameAs("some string");
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
class ExportableBlobFileStoreTest {
@Test
void shouldIgnoreStoreIfExcludedStore() {
Path dir = Paths.get("test/path/repository-export");
ExportableBlobFileStore exportableBlobFileStore = new ExportableBlobFileStore(dir);
Path file = Paths.get(dir.toString(), "some.blob");
boolean result = exportableBlobFileStore.shouldIncludeFile(file);
assertThat(result).isFalse();
}
@Test
void shouldIgnoreStoreIfNotBlob() {
Path dir = Paths.get("test/path/any-store");
ExportableBlobFileStore exportableBlobFileStore = new ExportableBlobFileStore(dir);
Path file = Paths.get(dir.toString(), "some.unblob");
boolean result = exportableBlobFileStore.shouldIncludeFile(file);
assertThat(result).isFalse();
}
@Test
void shouldIncludeStore() {
Path dir = Paths.get("test/path/any-blob-store");
ExportableBlobFileStore exportableBlobFileStore = new ExportableBlobFileStore(dir);
Path file = Paths.get(dir.toString(), "some.blob");
boolean result = exportableBlobFileStore.shouldIncludeFile(file);
assertThat(result).isTrue();
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
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.store.ExportableStore;
import sonia.scm.store.Exporter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
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 ExportableFileStoreTest {
@Mock
Exporter exporter;
@Test
void shouldNotPutContentIfNoFilesExists(@TempDir Path temp) throws IOException {
Path dataStoreDir = temp.resolve("some-store");
Files.createDirectories(dataStoreDir);
ExportableStore exportableFileStore = new ExportableDataFileStore(dataStoreDir);
exportableFileStore.export(exporter);
verify(exporter, never()).put(anyString(), anyLong());
}
@Test
void shouldPutContentIntoExporterForDataStore(@TempDir Path temp) throws IOException {
createFile(temp, "data", "trace", "first.xml");
createFile(temp, "data", "trace", "second.xml");
ByteArrayOutputStream os = new ByteArrayOutputStream();
ExportableStore exportableFileStore = new ExportableDataFileStore(temp.resolve("data").resolve("trace"));
when(exporter.put(anyString(), anyLong())).thenReturn(os);
exportableFileStore.export(exporter);
verify(exporter).put(eq("first.xml"), anyLong());
verify(exporter).put(eq("second.xml"), anyLong());
assertThat(os.toString()).isNotBlank();
}
@Test
void shouldPutContentIntoExporterForConfigStore(@TempDir Path temp) throws IOException {
createFile(temp, "config", "", "first.xml");
ByteArrayOutputStream os = new ByteArrayOutputStream();
ExportableStore exportableConfigFileStore = new ExportableConfigFileStore(temp.resolve("config").resolve("first.xml"));
when(exporter.put(anyString(), anyLong())).thenReturn(os);
exportableConfigFileStore.export(exporter);
verify(exporter).put(eq("first.xml"), anyLong());
assertThat(os.toString()).isNotBlank();
}
@Test
void shouldPutContentIntoExporterForBlobStore(@TempDir Path temp) throws IOException {
createFile(temp, "blob", "assets", "first.blob");
ByteArrayOutputStream os = new ByteArrayOutputStream();
Exporter exporter = mock(Exporter.class);
ExportableStore exportableBlobFileStore = new ExportableBlobFileStore(temp.resolve("blob").resolve("assets"));
when(exporter.put(anyString(), anyLong())).thenReturn(os);
exportableBlobFileStore.export(exporter);
verify(exporter).put(eq("first.blob"), anyLong());
assertThat(os.toString()).isNotBlank();
}
@Test
void shouldSkipFilteredBlobFiles(@TempDir Path temp) throws IOException {
createFile(temp, "blob", "security", "second.xml");
ByteArrayOutputStream os = new ByteArrayOutputStream();
Exporter exporter = mock(Exporter.class);
ExportableStore exportableBlobFileStore = new ExportableBlobFileStore(temp.resolve("blob").resolve("security"));
exportableBlobFileStore.export(exporter);
verify(exporter, never()).put(anyString(), anyLong());
assertThat(os.toString()).isBlank();
}
private File createFile(Path temp, String type, String name, String fileName) throws IOException {
Path path = name != null ? temp.resolve(type).resolve(name) : temp.resolve(type);
new File(path.toUri()).mkdirs();
File file = new File(path.toFile(), fileName);
if (!file.exists()) {
file.createNewFile();
}
FileWriter source = new FileWriter(file);
source.write("something");
source.close();
return file;
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.store.StoreEntryMetaData;
import sonia.scm.store.StoreType;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class FileBasedStoreEntryImporterFactoryTest {
@Test
void shouldCreateStoreEntryImporterForDataStore(@TempDir Path temp) {
FileBasedStoreEntryImporterFactory factory = new FileBasedStoreEntryImporterFactory(temp);
FileBasedStoreEntryImporter dataImporter = (FileBasedStoreEntryImporter) factory.importStore(new StoreEntryMetaData(StoreType.DATA, "hitchhiker"));
assertThat(dataImporter.getDirectory()).isEqualTo(temp.resolve("store").resolve("data").resolve("hitchhiker"));
}
@Test
void shouldCreateStoreEntryImporterForConfigStore(@TempDir Path temp) {
FileBasedStoreEntryImporterFactory factory = new FileBasedStoreEntryImporterFactory(temp);
FileBasedStoreEntryImporter configImporter = (FileBasedStoreEntryImporter) factory.importStore(new StoreEntryMetaData(StoreType.CONFIG, ""));
assertThat(configImporter.getDirectory()).isEqualTo(temp.resolve("store").resolve("config"));
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class FileBasedStoreEntryImporterTest {
@Test
void shouldCreateFileFromInputStream(@TempDir Path temp) {
FileBasedStoreEntryImporter importer = new FileBasedStoreEntryImporter(temp);
String fileName = "testStore.xml";
importer.importEntry(fileName, new ByteArrayInputStream("testdata".getBytes()));
assertThat(Files.exists(temp.resolve(fileName))).isTrue();
assertThat(temp.resolve(fileName)).hasContent("testdata");
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import com.google.common.io.ByteStreams;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import sonia.scm.AbstractTestBase;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryReadOnlyChecker;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.security.UUIDKeyGenerator;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.store.BlobStoreFactory;
import sonia.scm.store.EntryAlreadyExistsStoreException;
import sonia.scm.store.StoreReadOnlyException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class FileBlobStoreTest extends AbstractTestBase
{
private final Repository repository = RepositoryTestData.createHeartOfGold();
private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
private BlobStore store;
@BeforeEach
void createBlobStore()
{
store = createBlobStoreFactory()
.withName("test")
.forRepository(repository)
.build();
}
@Test
void testClear()
{
store.create("1");
store.create("2");
store.create("3");
assertNotNull(store.get("1"));
assertNotNull(store.get("2"));
assertNotNull(store.get("3"));
store.clear();
assertNull(store.get("1"));
assertNull(store.get("2"));
assertNull(store.get("3"));
}
@Test
void testContent() throws IOException
{
Blob blob = store.create();
write(blob, "Hello");
assertEquals("Hello", read(blob));
blob = store.get(blob.getId());
assertEquals("Hello", read(blob));
write(blob, "Other Text");
assertEquals("Other Text", read(blob));
blob = store.get(blob.getId());
assertEquals("Other Text", read(blob));
}
@Test
void testCreateAlreadyExistingEntry()
{
assertNotNull(store.create("1"));
assertThrows(EntryAlreadyExistsStoreException.class, () -> store.create("1"));
}
@Test
void testCreateWithId()
{
Blob blob = store.create("1");
assertNotNull(blob);
blob = store.get("1");
assertNotNull(blob);
}
@Test
void testCreateWithoutId()
{
Blob blob = store.create();
assertNotNull(blob);
String id = blob.getId();
assertNotNull(id);
blob = store.get(id);
assertNotNull(blob);
}
@Test
void testGet()
{
Blob blob = store.get("1");
assertNull(blob);
blob = store.create("1");
assertNotNull(blob);
blob = store.get("1");
assertNotNull(blob);
}
@Test
void testGetAll()
{
store.create("1");
store.create("2");
store.create("3");
List<Blob> all = store.getAll();
assertNotNull(all);
assertFalse(all.isEmpty());
assertEquals(3, all.size());
boolean c1 = false;
boolean c2 = false;
boolean c3 = false;
for (Blob b : all)
{
if ("1".equals(b.getId()))
{
c1 = true;
}
else if ("2".equals(b.getId()))
{
c2 = true;
}
else if ("3".equals(b.getId()))
{
c3 = true;
}
}
assertTrue(c1);
assertTrue(c2);
assertTrue(c3);
}
@Nested
class WithArchivedRepository {
@BeforeEach
void setRepositoryArchived() {
store.create("1"); // store for test must not be empty
when(readOnlyChecker.isReadOnly(repository.getId())).thenReturn(true);
createBlobStore();
}
@Test
void shouldNotClear() {
assertThrows(StoreReadOnlyException.class, () -> store.clear());
}
@Test
void shouldNotRemove() {
assertThrows(StoreReadOnlyException.class, () -> store.remove("1"));
}
}
private String read(Blob blob) throws IOException
{
InputStream input = blob.getInputStream();
byte[] bytes = ByteStreams.toByteArray(input);
input.close();
return new String(bytes);
}
private void write(Blob blob, String content) throws IOException
{
OutputStream output = blob.getOutputStream();
output.write(content.getBytes());
output.close();
blob.commit();
}
protected BlobStoreFactory createBlobStoreFactory()
{
return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), readOnlyChecker);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.TempDirRepositoryLocationResolver;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
class FileNamespaceUpdateIteratorTest {
private TempDirRepositoryLocationResolver locationResolver;
@BeforeEach
void initLocationResolver(@TempDir Path tempDir) throws IOException {
locationResolver = new TempDirRepositoryLocationResolver(tempDir.toFile());
Files.write(tempDir.resolve("metadata.xml"), asList(
"<repositories>",
" <namespace>hitchhike</namespace>",
"</repositories>"
));
}
@Test
void shouldFindNamespaces() {
Collection<String> foundNamespaces = new ArrayList<>();
new FileNamespaceUpdateIterator(locationResolver)
.forEachNamespace(foundNamespaces::add);
assertThat(foundNamespaces).containsExactly("hitchhike");
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.store.ExportableStore;
import sonia.scm.store.StoreType;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class FileStoreExporterTest {
private static final Repository REPOSITORY = RepositoryTestData.create42Puzzle();
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private RepositoryLocationResolver resolver;
@InjectMocks
private FileStoreExporter fileStoreExporter;
private Path storePath;
@BeforeEach
void setUpStorePath(@TempDir Path temp) {
storePath = temp.resolve("store");
when(resolver.forClass(Path.class).getLocation(REPOSITORY.getId())).thenReturn(temp);
}
@Test
void shouldReturnEmptyList(@TempDir Path temp) {
when(resolver.forClass(Path.class).getLocation(REPOSITORY.getId())).thenReturn(temp);
List<ExportableStore> exportableStores = fileStoreExporter.listExportableStores(REPOSITORY);
assertThat(exportableStores).isEmpty();
}
@Test
void shouldReturnConfigStores() throws IOException {
createFile(StoreType.CONFIG.getValue(), "config.xml")
.withContent("<?xml version=\"1.0\" ?>", "<data>", "some arbitrary content", "</data>");
List<ExportableStore> exportableStores = fileStoreExporter.listExportableStores(REPOSITORY);
assertThat(exportableStores).hasSize(1);
assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.CONFIG))).hasSize(1);
}
@Test
void shouldReturnConfigEntryStores() throws IOException {
createFile(StoreType.CONFIG.getValue(), "config-entry.xml")
.withContent("<?xml version=\"1.0\" ?>", "<configuration type=\"config-entry\">", "</configuration>");
List<ExportableStore> exportableStores = fileStoreExporter.listExportableStores(REPOSITORY);
assertThat(exportableStores).hasSize(1);
assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.CONFIG_ENTRY))).hasSize(1);
}
@Test
void shouldReturnDataStores() throws IOException {
createFile(StoreType.DATA.getValue(), "ci", "data.xml");
createFile(StoreType.DATA.getValue(), "jenkins", "data.xml");
List<ExportableStore> exportableStores = fileStoreExporter.listExportableStores(REPOSITORY);
assertThat(exportableStores).hasSize(2);
assertThat(exportableStores.stream().filter(e -> e.getMetaData().getType().equals(StoreType.DATA))).hasSize(2);
}
private FileWriter createFile(String... names) throws IOException {
Path file = Arrays.stream(names).map(Paths::get).reduce(Path::resolve).map(storePath::resolve).orElse(storePath);
Files.createDirectories(file.getParent());
if (!Files.exists(file)) {
Files.createFile(file);
}
return new FileWriter(file);
}
private static class FileWriter {
private final Path file;
private FileWriter(Path file) {
this.file = file;
}
void withContent(String... content) throws IOException {
Files.write(file, Arrays.asList(content));
}
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import com.google.common.io.Closeables;
import com.google.common.io.Resources;
import org.junit.Test;
import sonia.scm.security.AssignedPermission;
import sonia.scm.security.UUIDKeyGenerator;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import sonia.scm.store.ConfigurationEntryStoreTestBase;
import sonia.scm.store.StoreObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class JAXBConfigurationEntryStoreTest
extends ConfigurationEntryStoreTestBase
{
private static final String RESOURCE_FIXED =
"sonia/scm/store/fixed.format.xml";
private static final String RESOURCE_WRONG =
"sonia/scm/store/wrong.format.xml";
@Test
public void testLoad() throws IOException
{
ConfigurationEntryStore<AssignedPermission> store =
createPermissionStore(RESOURCE_FIXED);
AssignedPermission a1 = store.get("3ZOHKUePB3");
assertEquals("tuser", a1.getName());
AssignedPermission a2 = store.get("7COHL2j1G1");
assertEquals("tuser2", a2.getName());
AssignedPermission a3 = store.get("A0OHL3Qqw2");
assertEquals("tuser3", a3.getName());
}
@Test
public void testLoadWrongFormat() throws IOException
{
ConfigurationEntryStore<AssignedPermission> store =
createPermissionStore(RESOURCE_WRONG);
AssignedPermission a1 = store.get("3ZOHKUePB3");
assertEquals("tuser", a1.getName());
AssignedPermission a2 = store.get("7COHL2j1G1");
assertEquals("tuser2", a2.getName());
AssignedPermission a3 = store.get("A0OHL3Qqw2");
assertEquals("tuser3", a3.getName());
}
/**
* Method description
*
*
* @throws IOException
*/
@Test
public void testStoreAndLoad() throws IOException
{
String name = UUID.randomUUID().toString();
ConfigurationEntryStore<AssignedPermission> store = createPermissionStore(RESOURCE_FIXED, name);
store.put("a45", new AssignedPermission("tuser4", "repository:create"));
store = createConfigurationStoreFactory()
.withType(AssignedPermission.class)
.withName(name)
.build();
AssignedPermission ap = store.get("a45");
assertNotNull(ap);
assertEquals("tuser4", ap.getName());
assertEquals("repository:create", ap.getPermission().getValue());
}
@Test
public void shouldStoreAndLoadInRepository() throws IOException
{
repoStore.put("abc", new StoreObject("abc_value"));
StoreObject storeObject = repoStore.get("abc");
assertNotNull(storeObject);
assertEquals("abc_value", storeObject.getValue());
}
@Override
protected ConfigurationEntryStoreFactory createConfigurationStoreFactory()
{
return new JAXBConfigurationEntryStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), null, new StoreCacheFactory(new StoreCacheConfigProvider(false)));
}
private void copy(String resource, String name) throws IOException
{
URL url = Resources.getResource(resource);
File confdir = new File(contextProvider.getBaseDirectory(), "config");
File file = new File(confdir, name.concat(".xml"));
OutputStream output = null;
try
{
output = new FileOutputStream(file);
Resources.copy(url, output);
}
finally
{
Closeables.close(output, true);
}
}
private ConfigurationEntryStore<AssignedPermission> createPermissionStore(
String resource)
throws IOException
{
return createPermissionStore(resource, null);
}
/**
* Method description
*
*
* @param resource
* @param name
*
* @return
*
* @throws IOException
*/
private ConfigurationEntryStore<AssignedPermission> createPermissionStore(
String resource, String name)
throws IOException
{
if (name == null)
{
name = UUID.randomUUID().toString();
}
copy(resource, name);
return createConfigurationStoreFactory()
.withType(AssignedPermission.class)
.withName(name)
.build();
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.Test;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryReadOnlyChecker;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.StoreObject;
import sonia.scm.store.StoreReadOnlyException;
import sonia.scm.store.StoreTestBase;
import static java.util.Collections.emptySet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link JAXBConfigurationStore}.
*
*/
public class JAXBConfigurationStoreTest extends StoreTestBase {
private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
@Override
protected JAXBConfigurationStoreFactory createStoreFactory() {
return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker, emptySet(), new StoreCacheFactory(new StoreCacheConfigProvider(false)));
}
@Test
public void shouldStoreAndLoadInRepository() {
Repository repository = new Repository("id", "git", "ns", "n");
ConfigurationStore<StoreObject> store = createStoreFactory()
.withType(StoreObject.class)
.withName("test")
.forRepository(repository)
.build();
store.set(new StoreObject("value"));
StoreObject storeObject = store.get();
assertNotNull(storeObject);
assertEquals("value", storeObject.getValue());
}
@Test
public void shouldNotWriteArchivedRepository() {
Repository repository = new Repository("id", "git", "ns", "n");
when(readOnlyChecker.isReadOnly("id")).thenReturn(true);
ConfigurationStore<StoreObject> store = createStoreFactory()
.withType(StoreObject.class)
.withName("test")
.forRepository(repository)
.build();
StoreObject storeObject = new StoreObject("value");
assertThrows(RuntimeException.class, () -> store.set(storeObject));
}
@Test
public void shouldDeleteConfigStore() {
Repository repository = new Repository("id", "git", "ns", "n");
ConfigurationStore<StoreObject> store = createStoreFactory()
.withType(StoreObject.class)
.withName("test")
.forRepository(repository)
.build();
store.set(new StoreObject("value"));
store.delete();
StoreObject storeObject = store.get();
assertThat(storeObject).isNull();
}
@Test
public void shouldNotDeleteStoreForArchivedRepository() {
Repository repository = new Repository("id", "git", "ns", "n");
when(readOnlyChecker.isReadOnly("id")).thenReturn(false);
ConfigurationStore<StoreObject> store = createStoreFactory()
.withType(StoreObject.class)
.withName("test")
.forRepository(repository)
.build();
store.set(new StoreObject());
when(readOnlyChecker.isReadOnly("id")).thenReturn(true);
assertThrows(StoreReadOnlyException.class, store::delete);
assertThat(store.getOptional()).isPresent();
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import org.junit.Test;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryReadOnlyChecker;
import sonia.scm.security.UUIDKeyGenerator;
import sonia.scm.store.DataStore;
import sonia.scm.store.DataStoreFactory;
import sonia.scm.store.DataStoreTestBase;
import sonia.scm.store.StoreObject;
import sonia.scm.store.StoreReadOnlyException;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class JAXBDataStoreTest extends DataStoreTestBase {
private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
@Override
protected DataStoreFactory createDataStoreFactory() {
return new JAXBDataStoreFactory(
contextProvider,
repositoryLocationResolver,
new UUIDKeyGenerator(),
readOnlyChecker,
new DataFileCache(null, false),
new StoreCacheFactory(new StoreCacheConfigProvider(false))
);
}
@Override
protected <T> DataStore<T> getDataStore(Class<T> type, Repository repository) {
return createDataStoreFactory()
.withType(type)
.withName("test")
.forRepository(repository)
.build();
}
@Override
protected <T> DataStore<T> getDataStore(Class<T> type) {
return createDataStoreFactory()
.withType(type)
.withName("test")
.build();
}
@Test
public void shouldStoreAndLoadInRepository() {
repoStore.put("abc", new StoreObject("abc_value"));
StoreObject storeObject = repoStore.get("abc");
assertNotNull(storeObject);
assertEquals("abc_value", storeObject.getValue());
}
@Test(expected = StoreReadOnlyException.class)
public void shouldNotStoreForReadOnlyRepository() {
when(readOnlyChecker.isReadOnly(repository.getId())).thenReturn(true);
getDataStore(StoreObject.class, repository).put("abc", new StoreObject("abc_value"));
}
@Test
public void testGetAllWithNonXmlFile() throws IOException {
StoreObject obj1 = new StoreObject("test-1");
store.put("1", obj1);
new File(getTempDirectory(), "var/data/test/no-xml").createNewFile();
Map<String, StoreObject> map = store.getAll();
assertEquals(obj1, map.get("1"));
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
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.io.DefaultFileSystem;
import sonia.scm.repository.InitialRepositoryLocationResolver;
import sonia.scm.repository.RepositoryLocationResolver;
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
import sonia.scm.update.PropertyFileAccess;
import sonia.scm.util.IOUtil;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.util.Collections.emptySet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class)
class JAXBPropertyFileAccessTest {
public static final String REPOSITORY_ID = "repoId";
public static final String STORE_NAME = "test";
@Mock
SCMContextProvider contextProvider;
RepositoryLocationResolver locationResolver;
JAXBPropertyFileAccess fileAccess;
@TempDir
private Path tempDir;
@BeforeEach
void initTempDir() {
lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString()));
locationResolver = new PathBasedRepositoryLocationResolver(contextProvider, new InitialRepositoryLocationResolver(emptySet()), new DefaultFileSystem());
fileAccess = new JAXBPropertyFileAccess(contextProvider, locationResolver);
}
@Test
void shouldRenameGlobalConfigFile() throws IOException {
Path baseDirectory = contextProvider.getBaseDirectory().toPath();
Path configDirectory = baseDirectory.resolve(StoreConstants.CONFIG_DIRECTORY_NAME);
Files.createDirectories(configDirectory);
Path oldPath = configDirectory.resolve("old" + StoreConstants.FILE_EXTENSION);
Files.createFile(oldPath);
fileAccess.renameGlobalConfigurationFrom("old").to("new");
Path newPath = configDirectory.resolve("new" + StoreConstants.FILE_EXTENSION);
assertThat(oldPath).doesNotExist();
assertThat(newPath).exists();
}
@Nested
class ForExistingRepository {
@BeforeEach
void createRepositoryLocation() {
locationResolver.forClass(Path.class).createLocation(REPOSITORY_ID);
}
@Test
void shouldMoveStoreFileToRepositoryBasedLocation() throws IOException {
createV1StoreFile("myStore.xml");
fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID);
assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).exists();
}
@Test
void shouldMoveAllStoreFilesToRepositoryBasedLocations() throws IOException {
locationResolver.forClass(Path.class).createLocation("repoId2");
createV1StoreFile(REPOSITORY_ID + ".xml");
createV1StoreFile("repoId2.xml");
PropertyFileAccess.StoreFileTools statisticStoreAccess = fileAccess.forStoreName(STORE_NAME);
statisticStoreAccess.forStoreFiles(statisticStoreAccess::moveAsRepositoryStore);
assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId.xml")).exists();
assertThat(tempDir.resolve("repositories").resolve("repoId2").resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId2.xml")).exists();
}
}
private void createV1StoreFile(String name) throws IOException {
Path v1Dir = tempDir.resolve("var").resolve("data").resolve(STORE_NAME);
IOUtil.mkdirs(v1Dir.toFile());
Files.createFile(v1Dir.resolve(name));
}
@Nested
class ForMissingRepository {
@Test
void shouldIgnoreStoreFile() throws IOException {
createV1StoreFile("myStore.xml");
fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID);
assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).doesNotExist();
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.file;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.store.TypedStoreParameters;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TypedStoreContextTest {
@Test
void shouldMarshallAndUnmarshall(@TempDir Path tempDir) {
TypedStoreContext<Sample> context = context();
File file = tempDir.resolve("test.xml").toFile();
context.marshal(new Sample("awesome"), file);
Sample sample = context.unmarshal(file);
assertThat(sample.value).isEqualTo("awesome");
}
@Test
void shouldWorkWithMarshallerAndUnmarshaller(@TempDir Path tempDir) {
TypedStoreContext<Sample> context = context();
File file = tempDir.resolve("test.xml").toFile();
context.withMarshaller(marshaller -> {
marshaller.marshal(new Sample("wow"), file);
});
AtomicReference<Sample> ref = new AtomicReference<>();
context.withUnmarshaller(unmarshaller -> {
Sample sample = (Sample) unmarshaller.unmarshal(file);
ref.set(sample);
});
assertThat(ref.get().value).isEqualTo("wow");
}
@Test
void shouldSetContextClassLoader() {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader classLoader = new URLClassLoader(new URL[0], contextClassLoader);
TypedStoreParameters<Sample> params = params(Sample.class);
when(params.getClassLoader()).thenReturn(Optional.of(classLoader));
TypedStoreContext<Sample> context = TypedStoreContext.of(params);
AtomicReference<ClassLoader> ref = new AtomicReference<>();
context.withMarshaller(marshaller -> {
ref.set(Thread.currentThread().getContextClassLoader());
});
assertThat(ref.get()).isSameAs(classLoader);
assertThat(Thread.currentThread().getContextClassLoader()).isSameAs(contextClassLoader);
}
@Test
void shouldConfigureAdapter(@TempDir Path tempDir) {
TypedStoreParameters<SampleWithAdapter> params = params(SampleWithAdapter.class);
when(params.getAdapters()).thenReturn(Collections.singleton(new AppendingAdapter("!")));
TypedStoreContext<SampleWithAdapter> context = TypedStoreContext.of(params);
File file = tempDir.resolve("test.xml").toFile();
context.marshal(new SampleWithAdapter("awesome"), file);
SampleWithAdapter sample = context.unmarshal(file);
// one ! should be added for marshal and one for unmarshal
assertThat(sample.value).isEqualTo("awesome!!");
}
@SuppressWarnings("unchecked")
private <T> TypedStoreContext<T> context() {
return TypedStoreContext.of(params((Class<T>) Sample.class));
}
@SuppressWarnings("unchecked")
private <T> TypedStoreParameters<T> params(Class<T> type) {
TypedStoreParameters<T> params = mock(TypedStoreParameters.class);
when(params.getType()).thenReturn(type);
return params;
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public static class Sample {
private String value;
public Sample() {
}
public Sample(String value) {
this.value = value;
}
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public static class SampleWithAdapter {
@XmlJavaTypeAdapter(AppendingAdapter.class)
private String value;
public SampleWithAdapter() {
}
public SampleWithAdapter(String value) {
this.value = value;
}
}
public static class AppendingAdapter extends XmlAdapter<String, String> {
private final String suffix;
public AppendingAdapter(String suffix) {
this.suffix = suffix;
}
@Override
public String unmarshal(String v) {
return v + suffix;
}
@Override
public String marshal(String v) {
return v + suffix;
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import sonia.scm.plugin.QueryableTypeDescriptor;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
public class QueryableTypeDescriptorTestData {
static QueryableTypeDescriptor createDescriptor(String[] t) {
return createDescriptor("com.cloudogu.space.to.be.Spaceship", t);
}
static QueryableTypeDescriptor createDescriptor(String clazz, String[] t) {
QueryableTypeDescriptor descriptor = mock(QueryableTypeDescriptor.class);
lenient().when(descriptor.getTypes()).thenReturn(t);
lenient().when(descriptor.getClazz()).thenReturn(clazz);
lenient().when(descriptor.getName()).thenReturn("");
return descriptor;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import lombok.Getter;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.QueryableTypeDescriptor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings("java:S115") // we do not heed enum naming conventions for better readability in the test
class SQLiteIdentifiersTest {
@Nested
class Sanitize {
@Getter
private enum BadName {
OneToOne("examplename or 1=1"),
BatchedSQLStatement("105; DROP TABLE Classes"),
CommentOut("--"),
CommentOutWithContent("spaceship'--"),
BlindIfInjection("iif(count(*)>2,\"True\",\"False\")"),
VersionRequest("splite_version()"),
InnocentNameWithSpace("Traumschiff Enterprise");
BadName(String name) {
this.name = name;
}
private final String name;
}
@Getter
private enum GoodName {
Alphabetical("spaceship"),
AlphabeticalWithUnderscore("spaceship_STORE"),
Alphanumerical("rollerCoaster2000"),
AlphanumericalWithUnderscore("rollerCoaster2000_STORE");
GoodName(String name) {
this.name = name;
}
private final String name;
}
@ParameterizedTest
@EnumSource(BadName.class)
void shouldBlockSuspiciousNames(BadName name) {
assertThatThrownBy(() -> SQLiteIdentifiers.sanitize(name.getName()));
}
@ParameterizedTest
@EnumSource(GoodName.class)
void shouldPassCorrectNames(GoodName name) {
String outputName = SQLiteIdentifiers.sanitize(name.getName());
assertThat(outputName).isEqualTo(name.getName());
}
}
@Nested
class ComputeTableName {
@Mock
QueryableTypeDescriptor typeDescriptor;
void setUp(String clazzName, String name) {
lenient().when(typeDescriptor.getClazz()).thenReturn(clazzName);
lenient().when(typeDescriptor.getName()).thenReturn(name);
}
@Test
void shouldReturnCorrectTableNameIncludingPath() {
setUp("sonia.scm.store.sqlite.Spaceship", null);
String output = SQLiteIdentifiers.computeTableName(typeDescriptor);
assertThat(output).isEqualTo("sonia_scm_store_sqlite_Spaceship_STORE");
}
@Test
void shouldReturnTableNameEscapingUnderscores() {
setUp("sonia.scm.store.sqlite.Spaceship_One", null);
String output = SQLiteIdentifiers.computeTableName(typeDescriptor);
assertThat(output).isEqualTo("sonia_scm_store_sqlite_Spaceship__One_STORE");
}
@Test
void shouldReturnCorrectNameWithName() {
setUp("sonia.scm.store.sqlite.Spaceship", "TraumschiffEnterprise");
String output = SQLiteIdentifiers.computeTableName(typeDescriptor);
assertThat(output).isEqualTo("TraumschiffEnterprise_STORE");
}
}
@Nested
class ComputeColumnIdentifier {
@Test
void shouldReturnIdOnlyWithNullValue() {
assertThat(SQLiteIdentifiers.computeColumnIdentifier(null)).isEqualTo("ID");
}
@Test
void shouldReturnCombinedNameWithGivenClassName() {
assertThat(SQLiteIdentifiers.computeColumnIdentifier("sonia.scm.store.sqlite.Spaceship.class")).isEqualTo("Spaceship_ID");
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.repository.Repository;
import sonia.scm.store.QueryableMaintenanceStore;
import sonia.scm.user.User;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@Slf4j
class SQLiteParallelizationTest {
private String connectionString;
@BeforeEach
void init(@TempDir Path path) {
connectionString = "jdbc:sqlite:" + path.toString() + "/test.db";
}
@Test
void shouldTestParallelPutOperations() throws InterruptedException, ExecutionException, SQLException {
int numThreads = 100;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<Future<?>> futures = new ArrayList<>();
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
for (int i = 0; i < numThreads; i++) {
final String userId = "user-" + i;
final String userName = "User" + i;
futures.add(executor.submit(() -> {
try {
store.transactional(() -> {
store.put(userId, new User(userName));
return true;
});
} catch (Exception e) {
fail("Error storing user", e);
}
}));
}
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
int count = actualCount();
assertEquals(numThreads, count, "All threads should have been successfully saved");
}
@Test
void shouldWriteMultipleRowsConcurrently() throws InterruptedException, ExecutionException, SQLException {
int numThreads = 100;
int rowsPerThread = 50;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<Future<?>> futures = new ArrayList<>();
StoreTestBuilder testStoreBuilder = new StoreTestBuilder(connectionString, Repository.class.getName());
QueryableMaintenanceStore<User> store = testStoreBuilder.forMaintenanceWithSubIds("42");
for (int i = 0; i < numThreads; i++) {
final int threadIndex = i;
futures.add(executor.submit(() -> {
List<QueryableMaintenanceStore.Row> rows = new ArrayList<>();
try {
for (int j = 1; j <= rowsPerThread; j++) {
QueryableMaintenanceStore.Row<User> row = new QueryableMaintenanceStore.Row<>(
new String[]{String.valueOf(threadIndex)},
"user-" + threadIndex + "-" + j,
new User("User" + threadIndex + "-" + j, "User " + threadIndex + "-" + j,
"user" + threadIndex + "-" + j + "@example.com")
);
rows.add(row);
}
store.writeAll(rows);
} catch (Exception e) {
fail("Error writing rows", e);
}
}));
}
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
int expectedCount = numThreads * rowsPerThread;
int count = actualCount();
assertEquals(expectedCount, count, "Exactly " + expectedCount + " entries should have been saved");
}
private int actualCount() throws SQLException {
int count;
try (Connection conn = DriverManager.getConnection(connectionString);
PreparedStatement stmt = conn.prepareStatement("SELECT COUNT(*) FROM sonia_scm_user_User_STORE");
ResultSet rs = stmt.executeQuery()) {
rs.next();
count = rs.getInt(1);
}
return count;
}
}

View File

@@ -0,0 +1,270 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.user.User;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class SQLiteQueryableMutableStoreTest {
private Connection connection;
private String connectionString;
@BeforeEach
void init(@TempDir Path path) throws SQLException {
connectionString = "jdbc:sqlite:" + path.toString() + "/test.db";
connection = DriverManager.getConnection(connectionString);
}
@Nested
class Put {
@Test
void shouldPutObjectWithoutParent() throws SQLException {
new StoreTestBuilder(connectionString).withIds().put("tricia", new User("trillian"));
ResultSet resultSet = connection
.createStatement()
.executeQuery("SELECT json_extract(u.payload, '$.name') as name FROM sonia_scm_user_User_STORE u WHERE ID = 'tricia'");
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getString("name")).isEqualTo("trillian");
}
@Test
void shouldOverwriteExistingObject() throws SQLException {
new StoreTestBuilder(connectionString).withIds().put("tricia", new User("Trillian"));
new StoreTestBuilder(connectionString).withIds().put("tricia", new User("McMillan"));
ResultSet resultSet = connection
.createStatement()
.executeQuery("SELECT json_extract(u.payload, '$.name') as name FROM sonia_scm_user_User_STORE u WHERE ID = 'tricia'");
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getString("name")).isEqualTo("McMillan");
}
@Test
void shouldPutObjectWithSingleParent() throws SQLException {
new StoreTestBuilder(connectionString, "sonia.Group").withIds("42")
.put("tricia", new User("trillian"));
ResultSet resultSet = connection
.createStatement()
.executeQuery("SELECT json_extract(u.payload, '$.name') as name FROM sonia_scm_user_User_STORE u WHERE ID = 'tricia' and GROUP_ID = '42'");
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getString("name")).isEqualTo("trillian");
}
@Test
void shouldPutObjectWithMultipleParents() throws SQLException {
new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42")
.put("tricia", new User("trillian"));
ResultSet resultSet = connection
.createStatement()
.executeQuery("""
SELECT json_extract(u.payload, '$.name') as name
FROM sonia_scm_user_User_STORE u
WHERE ID = 'tricia'
AND GROUP_ID = '42'
AND COMPANY_ID = 'cloudogu'
""");
assertThat(resultSet.next()).isTrue();
assertThat(resultSet.getString("name")).isEqualTo("trillian");
}
@Test
void shouldRollback() throws SQLException {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.transactional(() -> {
store.put("tricia", new User("trillian"));
return false;
});
ResultSet resultSet = connection
.createStatement()
.executeQuery("SELECT * FROM sonia_scm_user_User_STORE");
assertThat(resultSet.next()).isFalse();
}
@Test
void shouldDisableAutoCommit() throws SQLException {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.transactional(() -> {
store.put("tricia", new User("trillian"));
try {
ResultSet resultSet = connection
.createStatement()
.executeQuery("SELECT * FROM sonia_scm_user_User_STORE");
assertThat(resultSet.next()).isFalse();
} catch (SQLException e) {
throw new RuntimeException(e);
}
return true;
});
ResultSet resultSet = connection
.createStatement()
.executeQuery("SELECT * FROM sonia_scm_user_User_STORE");
assertThat(resultSet.next()).isTrue();
}
}
@Nested
class Get {
@Test
void shouldGetObjectWithoutParent() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian"));
User tricia = store.get("tricia");
assertThat(tricia)
.isNotNull()
.extracting("name")
.isEqualTo("trillian");
}
@Test
void shouldReturnForNotExistingValue() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User earth = store.get("earth");
assertThat(earth)
.isNull();
}
@Test
void shouldGetObjectWithSingleParent() {
new StoreTestBuilder(connectionString, new String[]{"sonia.Group"}).withIds("1337").put("tricia", new User("McMillan"));
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Group").withIds("42");
store.put("tricia", new User("trillian"));
User tricia = store.get("tricia");
assertThat(tricia)
.isNotNull()
.extracting("name")
.isEqualTo("trillian");
}
@Test
void shouldGetObjectWithMultipleParents() {
new StoreTestBuilder(connectionString, new String[]{"sonia.Company", "sonia.Group"}).withIds("cloudogu", "1337").put("tricia", new User("McMillan"));
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
store.put("tricia", new User("trillian"));
User tricia = store.get("tricia");
assertThat(tricia)
.isNotNull()
.extracting("name")
.isEqualTo("trillian");
}
@Test
void shouldGetAllForSingleEntry() {
new StoreTestBuilder(connectionString, new String[]{"sonia.Company", "sonia.Group"}).withIds("cloudogu", "1337").put("tricia", new User("McMillan"));
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
store.put("tricia", new User("trillian"));
Map<String, User> users = store.getAll();
assertThat(users).hasSize(1);
assertThat(users.get("tricia"))
.isNotNull()
.extracting("name")
.isEqualTo("trillian");
}
@Test
void shouldGetAllForMultipleEntries() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
store.put("dent", new User("arthur"));
store.put("tricia", new User("trillian"));
Map<String, User> users = store.getAll();
assertThat(users).hasSize(2);
assertThat(users.get("tricia"))
.isNotNull()
.extracting("name")
.isEqualTo("trillian");
assertThat(users.get("dent"))
.isNotNull()
.extracting("name")
.isEqualTo("arthur");
}
}
@Nested
class Clear {
@Test
void shouldClear() {
SQLiteQueryableMutableStore<User> uneffectedStore = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "1337");
uneffectedStore.put("tricia", new User("McMillan"));
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
store.put("tricia", new User("trillian"));
store.clear();
assertThat(store.getAll()).isEmpty();
assertThat(uneffectedStore.getAll()).hasSize(1);
}
}
@Nested
class Remove {
@Test
void shouldRemove() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Company", "sonia.Group").withIds("cloudogu", "42");
store.put("dent", new User("arthur"));
store.put("tricia", new User("trillian"));
store.remove("dent");
assertThat(store.getAll()).containsOnlyKeys("tricia");
}
}
@AfterEach
void closeDB() throws SQLException {
connection.close();
}
}

View File

@@ -0,0 +1,908 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.group.Group;
import sonia.scm.repository.Repository;
import sonia.scm.store.Conditions;
import sonia.scm.store.LeafCondition;
import sonia.scm.store.Operator;
import sonia.scm.store.QueryableMaintenanceStore;
import sonia.scm.store.QueryableMaintenanceStore.MaintenanceIterator;
import sonia.scm.store.QueryableMaintenanceStore.MaintenanceStoreEntry;
import sonia.scm.store.QueryableStore;
import sonia.scm.user.User;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SuppressWarnings({"resource", "unchecked"})
class SQLiteQueryableStoreTest {
private String connectionString;
@BeforeEach
void init(@TempDir Path path) {
connectionString = "jdbc:sqlite:" + path.toString() + "/test.db";
}
@Nested
class FindAll {
@Nested
class QueryClassTypes {
@Test
void shouldWorkWithEnums() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Space Shuttle", Range.SOLAR_SYSTEM));
store.put(new Spaceship("Heart Of Gold", Range.INTER_GALACTIC));
List<Spaceship> all = store
.query(SPACESHIP_RANGE_ENUM_QUERY_FIELD.eq(Range.SOLAR_SYSTEM))
.findAll();
assertThat(all).hasSize(1);
}
@Test
void shouldWorkWithLongs() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User trillian = new User("trillian", "McMillan", "tricia@hog.org");
trillian.setCreationDate(10000000000L);
store.put(trillian);
User arthur = new User("arthur", "Dent", "arthur@hog.org");
arthur.setCreationDate(9999999999L);
store.put(arthur);
List<User> all = store.query(
CREATION_DATE_QUERY_FIELD.lessOrEquals(9999999999L)
)
.findAll();
assertThat(all)
.extracting("name")
.containsExactly("arthur");
}
@Test
void shouldWorkWithIntegers() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User trillian = new User("trillian", "McMillan", "tricia@hog.org");
trillian.setCreationDate(42L);
store.put(trillian);
User arthur = new User("arthur", "Dent", "arthur@hog.org");
arthur.setCreationDate(23L);
store.put(arthur);
List<User> all = store.query(
CREATION_DATE_AS_INTEGER_QUERY_FIELD.less(40)
)
.findAll();
assertThat(all)
.extracting("name")
.containsExactly("arthur");
}
@Test
void shouldWorkWithNumberCollection() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User trillian = new User("trillian", "McMillan", "tricia@hog.org");
trillian.setActive(true);
store.put(trillian);
User arthur = new User("arthur", "Dent", "arthur@hog.org");
arthur.setActive(false);
store.put(arthur);
List<User> all = store.query(
ACTIVE_QUERY_FIELD.isTrue()
)
.findAll();
assertThat(all)
.extracting("name")
.containsExactly("trillian");
}
@Test
void shouldCountAndWorkWithNumberCollection() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User trillian = new User("trillian", "McMillan", "tricia@hog.org");
trillian.setActive(true);
store.put(trillian);
User arthur = new User("arthur", "Dent", "arthur@hog.org");
arthur.setActive(false);
store.put(arthur);
long count = store.query(
ACTIVE_QUERY_FIELD.isTrue()
)
.count();
assertThat(count).isEqualTo(1);
}
}
@Nested
class QueryFeatures {
@Test
void shouldHandleCollections() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Spaceshuttle", "Buzz", "Anndre"));
store.put(new Spaceship("Heart Of Gold", "Trillian", "Arthur", "Ford", "Zaphod", "Marvin"));
List<Spaceship> result = store.query(
SPACESHIP_CREW_QUERY_FIELD.contains("Marvin")
).findAll();
assertThat(result).hasSize(1);
}
@Test
void shouldCountAndHandleCollections() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Spaceshuttle", "Buzz", "Anndre"));
store.put(new Spaceship("Heart Of Gold", "Trillian", "Arthur", "Ford", "Zaphod", "Marvin"));
long result = store.query(
SPACESHIP_CREW_QUERY_FIELD.contains("Marvin")
).count();
assertThat(result).isEqualTo(1);
}
@Test
void shouldCountWithoutConditions() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Spaceshuttle", "Buzz", "Anndre"));
store.put(new Spaceship("Heart Of Gold", "Trillian", "Arthur", "Ford", "Zaphod", "Marvin"));
long result = store.query().count();
assertThat(result).isEqualTo(2);
}
@Test
void shouldHandleCollectionSize() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Spaceshuttle", "Buzz", "Anndre"));
store.put(new Spaceship("Heart of Gold", "Trillian", "Arthur", "Ford", "Zaphod", "Marvin"));
store.put(new Spaceship("MillenniumFalcon"));
List<Spaceship> onlyEmpty = store.query(
SPACESHIP_CREW_SIZE_QUERY_FIELD.isEmpty()
).findAll();
assertThat(onlyEmpty).hasSize(1);
assertThat(onlyEmpty.get(0).getName()).isEqualTo("MillenniumFalcon");
List<Spaceship> exactlyTwoCrewMates = store.query(
SPACESHIP_CREW_SIZE_QUERY_FIELD.eq(2L)
).findAll();
assertThat(exactlyTwoCrewMates).hasSize(1);
assertThat(exactlyTwoCrewMates.get(0).getName()).isEqualTo("Spaceshuttle");
List<Spaceship> moreThanTwoCrewMates = store.query(
SPACESHIP_CREW_SIZE_QUERY_FIELD.greater(2L)
).findAll();
assertThat(moreThanTwoCrewMates).hasSize(1);
assertThat(moreThanTwoCrewMates.get(0).getName()).isEqualTo("Heart of Gold");
}
@Test
void shouldHandleMap() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Spaceshuttle", Map.of("moon", true, "earth", true)));
store.put(new Spaceship("Heart of Gold", Map.of("vogon", true, "earth", true)));
store.put(new Spaceship("MillenniumFalcon", Map.of("dagobah", false)));
List<Spaceship> keyResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsKey("vogon")
).findAll();
assertThat(keyResult).hasSize(1);
assertThat(keyResult.get(0).getName()).isEqualTo("Heart of Gold");
List<Spaceship> valueResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsValue(false)
).findAll();
assertThat(valueResult).hasSize(1);
assertThat(valueResult.get(0).getName()).isEqualTo("MillenniumFalcon");
}
@Test
void shouldCountAndHandleMap() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Spaceshuttle", Map.of("moon", true, "earth", true)));
store.put(new Spaceship("Heart of Gold", Map.of("vogon", true, "earth", true)));
store.put(new Spaceship("MillenniumFalcon", Map.of("dagobah", false)));
long keyResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsKey("vogon")
).count();
assertThat(keyResult).isEqualTo(1);
long valueResult = store.query(
SPACESHIP_DESTINATIONS_QUERY_FIELD.containsValue(false)
).count();
assertThat(valueResult).isEqualTo(1);
}
@Test
void shouldHandleMapSize() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
store.put(new Spaceship("Spaceshuttle", Map.of("moon", true, "earth", true)));
store.put(new Spaceship("Heart of Gold", Map.of("vogon", true, "earth", true, "dagobah", true)));
store.put(new Spaceship("MillenniumFalcon", Map.of()));
List<Spaceship> onlyEmpty = store.query(
SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD.isEmpty()
).findAll();
assertThat(onlyEmpty).hasSize(1);
assertThat(onlyEmpty.get(0).getName()).isEqualTo("MillenniumFalcon");
List<Spaceship> exactlyTwoDestinations = store.query(
SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD.eq(2L)
).findAll();
assertThat(exactlyTwoDestinations).hasSize(1);
assertThat(exactlyTwoDestinations.get(0).getName()).isEqualTo("Spaceshuttle");
List<Spaceship> moreThanTwoDestinations = store.query(
SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD.greater(2L)
).findAll();
assertThat(moreThanTwoDestinations).hasSize(1);
assertThat(moreThanTwoDestinations.get(0).getName()).isEqualTo("Heart of Gold");
}
@Test
void shouldRetrieveTime() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
Spaceship spaceshuttle = new Spaceship("Spaceshuttle", Range.SOLAR_SYSTEM);
spaceshuttle.setInServiceSince(Instant.parse("1981-04-12T10:00:00Z"));
store.put(spaceshuttle);
Spaceship falcon = new Spaceship("Falcon9", Range.SOLAR_SYSTEM);
falcon.setInServiceSince(Instant.parse("2015-12-21T10:00:00Z"));
store.put(falcon);
List<Spaceship> resultEqOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.eq(Instant.parse("2015-12-21T10:00:00Z"))).findAll();
assertThat(resultEqOperator).hasSize(1);
assertThat(resultEqOperator.get(0).getName()).isEqualTo("Falcon9");
List<Spaceship> resultBeforeOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.before(Instant.parse("2000-12-21T10:00:00Z"))).findAll();
assertThat(resultBeforeOperator).hasSize(1);
assertThat(resultBeforeOperator.get(0).getName()).isEqualTo("Spaceshuttle");
List<Spaceship> resultAfterOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.after(Instant.parse("2000-01-01T00:00:00Z"))).findAll();
assertThat(resultAfterOperator).hasSize(1);
assertThat(resultAfterOperator.get(0).getName()).isEqualTo("Falcon9");
List<Spaceship> resultBetweenOperator = store.query(
SPACESHIP_INSERVICE_QUERY_FIELD.between(Instant.parse("1980-04-12T10:00:00Z"), Instant.parse("2016-12-21T10:00:00Z"))).findAll();
assertThat(resultBetweenOperator).hasSize(2);
}
@Test
void shouldLimitQuery() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put(new User("trillian", "McMillan", "tricia@hog.org"));
store.put(new User("arthur", "Dent", "arthur@hog.org"));
store.put(new User("zaphod", "Beeblebrox", "zaphod@hog.org"));
store.put(new User("marvin", "Marvin", "marvin@hog.org"));
List<User> all = store.query()
.findAll(1, 2);
assertThat(all)
.extracting("name")
.containsExactly("arthur", "zaphod");
}
@Test
void shouldOrderResults() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put(new User("trillian", "McMillan", "tricia@hog.org"));
store.put(new User("arthur", "Dent", "arthur@hog.org"));
store.put(new User("zaphod", "Beeblebrox Head 1", "zaphod1@hog.org"));
store.put(new User("zaphod", "Beeblebrox Head 2", "zaphod2@hog.org"));
store.put(new User("marvin", "Marvin", "marvin@hog.org"));
List<User> all = store.query()
.orderBy(USER_NAME_QUERY_FIELD, QueryableStore.Order.ASC)
.orderBy(DISPLAY_NAME_QUERY_FIELD, QueryableStore.Order.DESC)
.findAll();
assertThat(all)
.extracting("mail")
.containsExactly("arthur@hog.org", "marvin@hog.org", "tricia@hog.org", "zaphod2@hog.org", "zaphod1@hog.org");
}
}
@Nested
class QueryLogicalHandling {
@Test
void shouldQueryForId() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("1", new User("trillian", "Tricia", "tricia@hog.org"));
store.put("2", new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put("3", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
ID_QUERY_FIELD.eq("1")
)
.findAll();
assertThat(all)
.extracting("displayName")
.containsExactly("Tricia");
}
@Test
void shouldQueryForParents() {
new StoreTestBuilder(connectionString, Group.class.getName())
.withIds("42")
.put("tricia", new User("trillian", "Tricia", "tricia@hog.org"));
new StoreTestBuilder(connectionString, Group.class.getName())
.withIds("1337")
.put("tricia", new User("trillian", "Trillian McMillan", "tricia@hog.org"));
SQLiteQueryableStore<User> store = new StoreTestBuilder(connectionString, Group.class.getName()).withIds();
List<User> all = store.query(
GROUP_QUERY_FIELD.eq("42")
)
.findAll();
assertThat(all)
.extracting("displayName")
.containsExactly("Tricia");
}
@Test
void shouldHandleContainsCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian", "Tricia", "tricia@hog.org"));
store.put("McMillan", new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
USER_NAME_QUERY_FIELD.contains("ri")
)
.findAll();
assertThat(all).hasSize(2);
}
@Test
void shouldHandleIsNullCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian", null, "tricia@hog.org"));
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
DISPLAY_NAME_QUERY_FIELD.isNull()
)
.findAll();
assertThat(all)
.extracting("name")
.containsExactly("trillian");
}
@Test
void shouldHandleNotNullCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian", null, "tricia@hog.org"));
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
Conditions.not(DISPLAY_NAME_QUERY_FIELD.isNull())
)
.findAll();
assertThat(all)
.extracting("name")
.containsExactly("arthur");
}
@Test
void shouldHandleOr() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian", "Tricia", "tricia@hog.org"));
store.put("McMillan", new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
Conditions.or(
DISPLAY_NAME_QUERY_FIELD.eq("Tricia"),
DISPLAY_NAME_QUERY_FIELD.eq("Trillian McMillan")
)
)
.findAll();
assertThat(all)
.extracting("name")
.containsExactly("trillian", "trillian");
}
@Test
void shouldHandleOrWithMultipleStores() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Group").withIds("CoolGroup");
User tricia = new User("trillian", "Tricia", "tricia@hog.org");
User mcmillan = new User("trillian", "Trillian McMillan", "mcmillan@gmail.com");
User dent = new User("arthur", "Arthur Dent", "arthur@hog.org");
store.put("tricia", tricia);
store.put("McMillan", mcmillan);
store.put("dent", dent);
SQLiteQueryableMutableStore<User> parallelStore = new StoreTestBuilder(connectionString, "sonia.Group").withIds("LameGroup");
parallelStore.put("tricia", new User("trillian", "Trillian IAMINAPARALLELSTORE McMillan", "mcmillan@gmail.com"));
List<User> result = store.query(
Conditions.or(
new LeafCondition<>(new QueryableStore.StringQueryField<>("mail"), Operator.EQ, "arthur@hog.org"),
new LeafCondition<>(new QueryableStore.StringQueryField<>("mail"), Operator.EQ, "mcmillan@gmail.com"))
).findAll();
assertThat(result).containsExactlyInAnyOrder(dent, mcmillan);
}
@Test
void shouldHandleGroup() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Group")
.withIds("42");
store.put("tricia", new User("trillian", "Tricia", "tricia@hog.org"));
new StoreTestBuilder(connectionString, "sonia.Group")
.withIds("1337")
.put("tricia", new User("trillian", "Trillian McMillan", "tricia@hog.org"));
List<User> all = store.query().findAll();
assertThat(all)
.extracting("displayName")
.containsExactly("Tricia");
}
@Test
void shouldHandleGroupWithCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Group")
.withIds("42");
store
.put("tricia", new User("trillian", "Tricia", "tricia@hog.org"));
new StoreTestBuilder(connectionString, "sonia.Group")
.withIds("1337")
.put("tricia", new User("trillian", "Trillian McMillan", "tricia@hog.org"));
List<User> all = store.query(USER_NAME_QUERY_FIELD.eq("trillian")).findAll();
assertThat(all)
.extracting("displayName")
.containsExactly("Tricia");
}
@Test
void shouldHandleInArrayCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put(new User("trillian", "McMillan", "tricia@hog.org"));
store.put(new User("arthur", "Dent", "arthur@hog.org"));
store.put(new User("zaphod", "Beeblebrox", "zaphod@hog.org"));
List<User> all = store.query(
USER_NAME_QUERY_FIELD.in("trillian", "arthur")
)
.findAll();
assertThat(all)
.extracting("name")
.containsExactly("trillian", "arthur");
}
}
@Test
void shouldFindAllObjectsWithoutParentWithoutConditions() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian"));
List<User> all = store.query().findAll();
assertThat(all).hasSize(1);
}
@Test
void shouldFindAllObjectsWithoutParentWithCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian"));
store.put("dent", new User("arthur"));
List<User> all = store.query(USER_NAME_QUERY_FIELD.eq("trillian")).findAll();
assertThat(all).hasSize(1);
}
@Test
void shouldFindAllObjectsWithOneParentAndMultipleConditions() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, "sonia.Group").withIds("CoolGroup");
User tricia = new User("trillian", "Tricia", "tricia@hog.org");
User mcmillan = new User("trillian", "Trillian McMillan", "mcmillan@gmail.com");
User dent = new User("arthur", "Arthur Dent", "arthur@hog.org");
store.put("tricia", tricia);
store.put("McMillan", mcmillan);
store.put("dent", dent);
List<User> result = store.query(
Conditions.or(
new LeafCondition<>(new QueryableStore.StringQueryField<>("mail"), Operator.EQ, "arthur@hog.org"),
new LeafCondition<>(new QueryableStore.StringQueryField<>("mail"), Operator.EQ, "mcmillan@gmail.com"))
).findAll();
assertThat(result).containsExactlyInAnyOrder(dent, mcmillan);
}
@Test
void shouldFindAllObjectsWithoutParentWithMultipleConditions() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put("tricia", new User("trillian", "Tricia", "tricia@hog.org"));
store.put("McMillan", new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put("dent", new User("arthur", "Arthur Dent", "arthur@hog.org"));
List<User> all = store.query(
USER_NAME_QUERY_FIELD.eq("trillian"),
DISPLAY_NAME_QUERY_FIELD.eq("Tricia")
)
.findAll();
assertThat(all).hasSize(1);
}
@Test
void shouldReturnIds() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString, Spaceship.class.getName())
.withIds("hog");
store.put("tricia", new User("trillian", "Tricia", "tricia@hog.org"));
List<QueryableStore.Result<User>> results = store
.query()
.withIds()
.findAll();
assertThat(results).hasSize(1);
QueryableStore.Result<User> result = results.get(0);
assertThat(result.getParentId(Spaceship.class)).contains("hog");
assertThat(result.getId()).isEqualTo("tricia");
assertThat(result.getEntity().getName()).isEqualTo("trillian");
}
}
@Nested
class FindOne {
@Test
void shouldReturnEmptyOptionalIfNoResultFound() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
assertThat(store.query(SPACESHIP_NAME_QUERY_FIELD.eq("Heart Of Gold")).findOne()).isEmpty();
}
@Test
void shouldReturnOneResultIfOneIsGiven() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
Spaceship expectedShip = new Spaceship("Heart Of Gold", Range.INNER_GALACTIC);
store.put(expectedShip);
Spaceship ship = store.query(SPACESHIP_NAME_QUERY_FIELD.eq("Heart Of Gold")).findOne().get();
assertThat(ship).isEqualTo(expectedShip);
}
@Test
void shouldThrowErrorIfMoreThanOneResultIsSaved() {
SQLiteQueryableMutableStore<Spaceship> store = new StoreTestBuilder(connectionString).forClassWithIds(Spaceship.class);
Spaceship expectedShip = new Spaceship("Heart Of Gold", Range.INNER_GALACTIC);
Spaceship localShip = new Spaceship("Heart Of Gold", Range.SOLAR_SYSTEM);
store.put(expectedShip);
store.put(localShip);
assertThatThrownBy(() -> store.query(SPACESHIP_NAME_QUERY_FIELD.eq("Heart Of Gold")).findOne().get())
.isInstanceOf(QueryableStore.TooManyResultsException.class);
}
}
@Nested
class FindFirst {
@Test
void shouldFindFirst() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User expectedUser = new User("trillian", "Tricia", "tricia@hog.org");
store.put("1", expectedUser);
store.put("2", new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put("3", new User("arthur", "Arthur Dent", "arthur@hog.org"));
Optional<User> user = store.query(
USER_NAME_QUERY_FIELD.eq("trillian")
)
.findFirst();
assertThat(user).isEqualTo(Optional.of(expectedUser));
}
@Test
void shouldFindFirstWithMatchingCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User expectedUser = new User("trillian", "Trillian McMillan", "mcmillan-alternate@gmail.com");
store.put("1", new User("trillian", "Tricia", "tricia@hog.org"));
store.put("2", new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put("3", expectedUser);
store.put("4", new User("arthur", "Arthur Dent", "arthur@hog.org"));
Optional<User> user = store.query(
USER_NAME_QUERY_FIELD.eq("trillian"),
MAIL_QUERY_FIELD.eq("mcmillan-alternate@gmail.com")
)
.findFirst();
assertThat(user).isEqualTo(Optional.of(expectedUser));
}
@Test
void shouldFindFirstWithMatchingLogicalCondition() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User expectedUser = new User("trillian", "Trillian McMillan", "mcmillan@gmail.com");
store.put("1", new User("trillian-old", "Tricia", "tricia@hog.org"));
store.put("2", new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put("3", expectedUser);
store.put("4", new User("arthur", "Arthur Dent", "arthur@hog.org"));
store.put("5", new User("arthur", "Trillian McMillan", "mcmillan@gmail.com"));
Optional<User> user = store.query(
Conditions.and(
Conditions.and(
DISPLAY_NAME_QUERY_FIELD.eq("Trillian McMillan"),
MAIL_QUERY_FIELD.eq("mcmillan@gmail.com")
),
Conditions.not(
ID_QUERY_FIELD.eq("1")
)
)
).findFirst();
assertThat(user).isEqualTo(Optional.of(expectedUser));
}
@Test
void shouldReturnEmptyOptionalIfNoResultFound() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
Optional<User> user = store.query(
USER_NAME_QUERY_FIELD.eq("dave")
)
.findFirst();
assertThat(user).isEmpty();
}
}
@Nested
class ForMaintenance {
@Test
void shouldUpdateRawJson() throws Exception {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
User user = new User("trillian", "Trillian McMillan", "mcmillan@gmail.com");
store.put("1", user);
try (MaintenanceIterator<User> iterator = store.iterateAll()) {
assertThat(iterator.hasNext()).isTrue();
MaintenanceStoreEntry<User> entry = iterator.next();
assertThat(entry.getId()).isEqualTo("1");
User userFromIterator = entry.get();
userFromIterator.setName("dent");
entry.update(userFromIterator);
assertThat(iterator.hasNext()).isFalse();
}
User changedUser = store.get("1");
assertThat(changedUser.getName()).isEqualTo("dent");
}
@Test
void shouldUpdateRawJsonForItemWithParent() throws Exception {
SQLiteQueryableMutableStore<User> subStore = new StoreTestBuilder(connectionString, Group.class.getName()).withIds("hitchhiker");
User user = new User("trillian", "Trillian McMillan", "mcmillan@gmail.com");
subStore.put("1", user);
QueryableMaintenanceStore<User> maintenanceStore = new StoreTestBuilder(connectionString, Group.class.getName()).forMaintenanceWithSubIds();
try (MaintenanceIterator<User> iterator = maintenanceStore.iterateAll()) {
assertThat(iterator.hasNext()).isTrue();
MaintenanceStoreEntry<User> entry = iterator.next();
assertThat(entry.getId()).isEqualTo("1");
User userFromIterator = entry.get();
userFromIterator.setName("dent");
entry.update(userFromIterator);
assertThat(iterator.hasNext()).isFalse();
}
User changedUser = subStore.get("1");
assertThat(changedUser.getName()).isEqualTo("dent");
}
@Test
void shouldRemoveFromIteratorWithoutParent() {
SQLiteQueryableMutableStore<User> store = new StoreTestBuilder(connectionString).withIds();
store.put(new User("trillian", "Trillian McMillan", "mcmillan@gmail.com"));
store.put(new User("dent", "Arthur Dent", "dent@gmail.com"));
for (MaintenanceIterator<User> iter = store.iterateAll(); iter.hasNext(); ) {
MaintenanceStoreEntry<User> next = iter.next();
if (next.get().getName().equals("dent")) {
iter.remove();
}
}
assertThat(store.getAll())
.values()
.extracting("name")
.containsExactly("trillian");
}
@Test
void shouldRemoveFromIteratorWithParents() {
StoreTestBuilder testStoreBuilder = new StoreTestBuilder(connectionString, Repository.class.getName(), Group.class.getName());
SQLiteQueryableMutableStore<User> hogStore = testStoreBuilder.withIds("42", "hog");
hogStore.put("trisha", new User("trillian", "Trillian McMillan", "mcmillan@hog.com"));
hogStore.put("dent", new User("dent", "Arthur Dent", "dent@hog.com"));
SQLiteQueryableMutableStore<User> earthStore = testStoreBuilder.withIds("42", "earth");
earthStore.put("dent", new User("dent", "Arthur Dent", "dent@gmail.com"));
QueryableMaintenanceStore<User> store = testStoreBuilder.forMaintenanceWithSubIds("42");
for (MaintenanceIterator<User> iter = store.iterateAll(); iter.hasNext(); ) {
MaintenanceStoreEntry<User> next = iter.next();
if (next.get().getName().equals("dent") && next.getParentId(Group.class).get().equals("hog")) {
iter.remove();
}
}
assertThat(testStoreBuilder.withIds("42", "hog").getAll())
.values()
.extracting("name")
.containsExactly("trillian");
assertThat(testStoreBuilder.withIds("42", "earth").getAll())
.values()
.extracting("name")
.containsExactly("dent");
}
@Test
void shouldReadAll() {
StoreTestBuilder testStoreBuilder = new StoreTestBuilder(connectionString, Repository.class.getName());
SQLiteQueryableMutableStore<User> hogStore = testStoreBuilder.withIds("42");
hogStore.put("trisha", new User("trillian", "Trillian McMillan", "mcmillan@hog.com"));
hogStore.put("dent", new User("dent", "Arthur Dent", "dent@hog.com"));
QueryableMaintenanceStore<User> store = testStoreBuilder.forMaintenanceWithSubIds("42");
Collection<QueryableMaintenanceStore.Row<User>> rows = store.readAll();
assertThat(rows)
.extracting("id")
.containsExactlyInAnyOrder("dent", "trisha");
assertThat(rows)
.extracting(QueryableMaintenanceStore.Row::getParentIds)
.allSatisfy(strings -> assertThat(strings).containsExactly("42"));
assertThat(rows)
.extracting(QueryableMaintenanceStore.Row::getValue)
.extracting("name")
.containsExactlyInAnyOrder("trillian", "dent");
}
@Test
void shouldWriteAllForNewParent() {
StoreTestBuilder testStoreBuilder = new StoreTestBuilder(connectionString, Repository.class.getName());
QueryableMaintenanceStore<User> store = testStoreBuilder.forMaintenanceWithSubIds("42");
store.writeAll(
List.of(
new QueryableMaintenanceStore.Row<>(new String[]{"23"}, "trisha", new User("trillian", "Trillian McMillan", "trisha@hog.com"))
)
);
SQLiteQueryableMutableStore<User> hogStore = testStoreBuilder.withIds("42");
Collection<QueryableMaintenanceStore.Row<User>> allValues = hogStore.readAll();
assertThat(allValues)
.extracting("value")
.extracting("name")
.containsExactly("trillian");
}
@Test
void shouldWriteRawForNewParent() {
StoreTestBuilder testStoreBuilder = new StoreTestBuilder(connectionString, Repository.class.getName());
QueryableMaintenanceStore<User> store = testStoreBuilder.forMaintenanceWithSubIds("42");
store.writeRaw(
List.of(
new QueryableMaintenanceStore.RawRow(new String[]{"23"}, "trisha", "{ \"name\": \"trillian\", \"displayName\": \"Trillian McMillan\", \"mail\": \"mcmillan@hog.com\" }")
)
);
SQLiteQueryableMutableStore<User> hogStore = testStoreBuilder.withIds("42");
Collection<QueryableMaintenanceStore.Row<User>> allValues = hogStore.readAll();
assertThat(allValues)
.extracting("value")
.extracting("name")
.containsExactly("trillian");
}
}
private static final QueryableStore.IdQueryField<User> ID_QUERY_FIELD =
new QueryableStore.IdQueryField<>();
private static final QueryableStore.IdQueryField<User> GROUP_QUERY_FIELD =
new QueryableStore.IdQueryField<>(Group.class);
private static final QueryableStore.StringQueryField<User> USER_NAME_QUERY_FIELD =
new QueryableStore.StringQueryField<>("name");
private static final QueryableStore.StringQueryField<User> DISPLAY_NAME_QUERY_FIELD =
new QueryableStore.StringQueryField<>("displayName");
private static final QueryableStore.StringQueryField<User> MAIL_QUERY_FIELD =
new QueryableStore.StringQueryField<>("mail");
private static final QueryableStore.NumberQueryField<User, Long> CREATION_DATE_QUERY_FIELD =
new QueryableStore.NumberQueryField<>("creationDate");
private static final QueryableStore.NumberQueryField<User, Integer> CREATION_DATE_AS_INTEGER_QUERY_FIELD =
new QueryableStore.NumberQueryField<>("creationDate");
private static final QueryableStore.BooleanQueryField<User> ACTIVE_QUERY_FIELD =
new QueryableStore.BooleanQueryField<>("active");
enum Range {
SOLAR_SYSTEM, INNER_GALACTIC, INTER_GALACTIC
}
private static final QueryableStore.StringQueryField<Spaceship> SPACESHIP_NAME_QUERY_FIELD =
new QueryableStore.StringQueryField<>("name");
private static final QueryableStore.EnumQueryField<Spaceship, Range> SPACESHIP_RANGE_ENUM_QUERY_FIELD =
new QueryableStore.EnumQueryField<>("range");
private static final QueryableStore.CollectionQueryField<Spaceship> SPACESHIP_CREW_QUERY_FIELD =
new QueryableStore.CollectionQueryField<>("crew");
private static final QueryableStore.CollectionSizeQueryField<Spaceship> SPACESHIP_CREW_SIZE_QUERY_FIELD =
new QueryableStore.CollectionSizeQueryField<>("crew");
private static final QueryableStore.MapQueryField<Spaceship> SPACESHIP_DESTINATIONS_QUERY_FIELD =
new QueryableStore.MapQueryField<>("destinations");
private static final QueryableStore.MapSizeQueryField<Spaceship> SPACESHIP_DESTINATIONS_SIZE_QUERY_FIELD =
new QueryableStore.MapSizeQueryField<>("destinations");
private static final QueryableStore.InstantQueryField<Spaceship> SPACESHIP_INSERVICE_QUERY_FIELD =
new QueryableStore.InstantQueryField<>("inServiceSince");
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
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.plugin.ExtensionProcessor;
import sonia.scm.plugin.PluginLoader;
import sonia.scm.plugin.QueryableTypeDescriptor;
import sonia.scm.store.QueryableType;
import java.util.Collection;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class SQLiteStoreMetaDataProviderTest {
@Mock
private PluginLoader pluginLoader;
@Mock
private ExtensionProcessor extensionProcessor;
@Mock
private QueryableTypeDescriptor descriptor1;
@Mock
private QueryableTypeDescriptor descriptor2;
private SQLiteStoreMetaDataProvider metaDataProvider;
@BeforeEach
void setUp() {
when(descriptor1.getTypes()).thenReturn(new String[]{"sonia.scm.store.sqlite.TestParent1.class"});
when(descriptor1.getClazz()).thenReturn("sonia.scm.store.sqlite.TestChildWithOneParent");
when(descriptor2.getTypes()).thenReturn(new String[]{"sonia.scm.store.sqlite.TestParent1.class", "sonia.scm.store.sqlite.TestParent2.class"});
when(descriptor2.getClazz()).thenReturn("sonia.scm.store.sqlite.TestChildWithTwoParent");
when(extensionProcessor.getQueryableTypes()).thenReturn(List.of(descriptor1, descriptor2));
when(pluginLoader.getUberClassLoader()).thenReturn(this.getClass().getClassLoader());
when(pluginLoader.getExtensionProcessor()).thenReturn(extensionProcessor);
metaDataProvider = new SQLiteStoreMetaDataProvider(pluginLoader);
}
@Test
void testInitializeType() {
Collection<Class<?>> parent1Types = metaDataProvider.getTypesWithParent(TestParent1.class);
assertThat(parent1Types)
.extracting("name")
.containsExactly(
"sonia.scm.store.sqlite.TestChildWithOneParent",
"sonia.scm.store.sqlite.TestChildWithTwoParent"
);
Collection<Class<?>> parent2Types = metaDataProvider.getTypesWithParent(TestParent1.class, TestParent2.class);
assertThat(parent2Types)
.extracting("name")
.containsExactly("sonia.scm.store.sqlite.TestChildWithTwoParent");
}
}
class TestParent1 {
}
class TestParent2 {
}
@QueryableType(TestParent1.class)
class TestChildWithOneParent {
}
@QueryableType({TestParent1.class, TestParent2.class})
class TestChildWithTwoParent {
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.EqualsAndHashCode;
import sonia.scm.store.QueryableType;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@QueryableType
@EqualsAndHashCode
class Spaceship {
String name;
SQLiteQueryableStoreTest.Range range;
Collection<String> crew;
Map<String, Boolean> destinations;
Instant inServiceSince;
public Spaceship() {
}
public Spaceship(String name, SQLiteQueryableStoreTest.Range range) {
this.name = name;
this.range = range;
}
public Spaceship(String name, String... crew) {
this.name = name;
this.crew = Arrays.asList(crew);
}
public Spaceship(String name, Map<String, Boolean> destinations) {
this.name = name;
this.destinations = destinations;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public SQLiteQueryableStoreTest.Range getRange() {
return range;
}
public void setRange(SQLiteQueryableStoreTest.Range range) {
this.range = range;
}
public Collection<String> getCrew() {
return crew;
}
public void setCrew(Collection<String> crew) {
this.crew = crew;
}
public Map<String, Boolean> getDestinations() {
return destinations;
}
public void setDestinations(Map<String, Boolean> destinations) {
this.destinations = destinations;
}
public Instant getInServiceSince() {
return inServiceSince;
}
public void setInServiceSince(Instant inServiceSince) {
this.inServiceSince = inServiceSince;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import sonia.scm.security.UUIDKeyGenerator;
import sonia.scm.store.QueryableMaintenanceStore;
import sonia.scm.store.QueryableStore;
import sonia.scm.user.User;
import java.util.List;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS;
import static sonia.scm.store.sqlite.QueryableTypeDescriptorTestData.createDescriptor;
class StoreTestBuilder {
private final ObjectMapper mapper = getObjectMapper();
private static ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(WRITE_DATES_AS_TIMESTAMPS, true);
objectMapper.configure(WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
private final String connectionString;
private final String[] parentClasses;
StoreTestBuilder(String connectionString, String... parentClasses) {
this.connectionString = connectionString;
this.parentClasses = parentClasses;
}
SQLiteQueryableMutableStore<User> withIds(String... ids) {
return forClassWithIds(User.class, ids);
}
QueryableStore<User> withSubIds(String... ids) {
if (ids.length > parentClasses.length) {
throw new IllegalArgumentException("id length should be at most " + parentClasses.length);
}
return createStoreFactory(User.class).getReadOnly(User.class, ids);
}
QueryableMaintenanceStore<User> forMaintenanceWithSubIds(String... ids) {
if (ids.length > parentClasses.length) {
throw new IllegalArgumentException("id length should be at most " + parentClasses.length);
}
return createStoreFactory(User.class).getForMaintenance(User.class, ids);
}
<T> SQLiteQueryableMutableStore<T> forClassWithIds(Class<T> clazz, String... ids) {
return createStoreFactory(clazz).getMutable(clazz, ids);
}
private <T> SQLiteQueryableStoreFactory createStoreFactory(Class<T> clazz) {
return new SQLiteQueryableStoreFactory(
connectionString,
mapper,
new UUIDKeyGenerator(),
List.of(createDescriptor(clazz.getName(), parentClasses))
);
}
}

View File

@@ -0,0 +1,151 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.store.sqlite;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.plugin.QueryableTypeDescriptor;
import sonia.scm.store.StoreException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.when;
import static sonia.scm.store.sqlite.QueryableTypeDescriptorTestData.createDescriptor;
@ExtendWith(MockitoExtension.class)
class TableCreatorTest {
private final Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");;
private final TableCreator tableCreator = new TableCreator(connection);
TableCreatorTest() throws SQLException {
}
@Test
void shouldCreateTableWithoutParents() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[0]);
tableCreator.initializeTable(descriptor);
assertThat(getColumns("com_cloudogu_space_to_be_Spaceship_STORE"))
.containsEntry("ID", "TEXT")
.containsEntry("payload", "JSONB");
}
@Test
void shouldCreateNamedTableWithoutParents() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[0]);
when(descriptor.getName()).thenReturn("ships");
tableCreator.initializeTable(descriptor);
assertThat(getColumns("ships_STORE"))
.containsEntry("ID", "TEXT")
.containsEntry("payload", "JSONB");
}
@Test
void shouldCreateTableWithSingleParent() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[]{"sonia.scm.repo.Repository.class"});
tableCreator.initializeTable(descriptor);
assertThat(getColumns("com_cloudogu_space_to_be_Spaceship_STORE"))
.containsEntry("Repository_ID", "TEXT")
.containsEntry("ID", "TEXT")
.containsEntry("payload", "JSONB");
}
@Test
void shouldCreateTableWithMultipleParents() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[]{"sonia.scm.repo.Repository.class", "sonia.scm.user.User"});
tableCreator.initializeTable(descriptor);
assertThat(getColumns("com_cloudogu_space_to_be_Spaceship_STORE"))
.containsEntry("Repository_ID", "TEXT")
.containsEntry("User_ID", "TEXT")
.containsEntry("ID", "TEXT")
.containsEntry("payload", "JSONB");
}
@Test
void shouldFailIfTableExistsWithoutIdColumn() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[0]);
try {
connection.createStatement().execute("CREATE TABLE com_cloudogu_space_to_be_Spaceship_STORE (payload JSONB)");
tableCreator.initializeTable(descriptor);
fail("exception expected");
} catch (StoreException e) {
assertThat(e.getMessage()).contains("does not contain ID column");
}
}
@Test
void shouldFailIfTableExistsWithoutPayloadColumn() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[0]);
try {
connection.createStatement().execute("CREATE TABLE com_cloudogu_space_to_be_Spaceship_STORE (ID TEXT)");
tableCreator.initializeTable(descriptor);
fail("exception expected");
} catch (StoreException e) {
assertThat(e.getMessage()).contains("does not contain payload column");
}
}
@Test
void shouldFailIfTableExistsWithoutParentColumn() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[]{"sonia.scm.repo.Repository.class"});
try {
connection.createStatement().execute("CREATE TABLE com_cloudogu_space_to_be_Spaceship_STORE (ID TEXT, payload JSONB)");
tableCreator.initializeTable(descriptor);
fail("exception expected");
} catch (StoreException e) {
assertThat(e.getMessage()).contains("does not contain column Repository_ID");
}
}
@Test
void shouldFailIfTableExistsWithTooManyParentColumns() throws SQLException {
QueryableTypeDescriptor descriptor = createDescriptor(new String[]{"sonia.scm.repo.Repository.class"});
try {
connection.createStatement().execute("CREATE TABLE com_cloudogu_space_to_be_Spaceship_STORE (ID TEXT, Repository_ID, User_ID, payload JSONB)");
tableCreator.initializeTable(descriptor);
fail("exception expected");
} catch (StoreException e) {
assertThat(e.getMessage()).contains("but has too many columns");
}
}
private Map<String, String> getColumns(String expectedTableName) throws SQLException {
ResultSet resultSet = connection.createStatement().executeQuery("PRAGMA table_info("+ expectedTableName +")");
Map<String, String> columns = new LinkedHashMap<>();
while (resultSet.next()) {
columns.put(resultSet.getString("name"), resultSet.getString("type"));
}
return columns;
}
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.update.xml;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import sonia.scm.SCMContextProvider;
import sonia.scm.Stage;
import sonia.scm.repository.RepositoryReadOnlyChecker;
import sonia.scm.security.KeyGenerator;
import sonia.scm.store.file.JAXBConfigurationEntryStoreFactory;
import sonia.scm.store.file.StoreCacheConfigProvider;
import sonia.scm.store.file.StoreCacheFactory;
import sonia.scm.update.RepositoryV1PropertyReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.mockito.Mockito.mock;
class XmlV1PropertyDAOTest {
public static final String PROPERTIES = "<?xml version=\"1.0\" ?>\n" +
"<configuration>\n" +
" <entry>\n" +
" <key>9ZQKlvI401</key>\n" +
" <value>\n" +
" <item></item>\n" +
" <item>\n" +
" <key>redmine.url</key>\n" +
" <value>https://redmine.example.net/projects/r6</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>redmine.auto-close-username-transformer</key>\n" +
" <value>{0}</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>redmine.auto-close</key>\n" +
" </item>\n" +
" </value>\n" +
" </entry>\n" +
" <entry>\n" +
" <key>1wPsrHPM81</key>\n" +
" <value>\n" +
" <item>\n" +
" <key>notify.contact.repository</key>\n" +
" <value>true</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>redmine.auto-close-username-transformer</key>\n" +
" <value>fixed, fix, closed, close, resolved, resolve</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>redmine.url</key>\n" +
" <value>https://redmine.example.net/projects/a2</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>redmine.update-issues</key>\n" +
" <value>true</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>notify.email.per.push</key>\n" +
" <value>true</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>notify.use.author.as.from.address</key>\n" +
" <value>true</value>\n" +
" </item>\n" +
" <item>\n" +
" <key>redmine.auto-close</key>\n" +
" <value>true</value>\n" +
" </item>\n" +
" </value>\n" +
" </entry>\n" +
" <entry>\n" +
" <key>WlDszQtZj4</key>\n" +
" <value></value>\n" +
" </entry>\n" +
"</configuration>";
/**
* This is a test for https://github.com/scm-manager/scm-manager/issues/1237
*/
@Test
void shouldReadItemsWithEmptyValues(@TempDir Path temp) throws IOException {
Path configPath = temp.resolve("config");
Files.createDirectories(configPath);
Path propFile = configPath.resolve("repository-properties-v1.xml");
Files.write(propFile, PROPERTIES.getBytes());
RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
XmlV1PropertyDAO dao = new XmlV1PropertyDAO(new JAXBConfigurationEntryStoreFactory(new SimpleContextProvider(temp), null, new SimpleKeyGenerator(), readOnlyChecker, new StoreCacheFactory(new StoreCacheConfigProvider(false))));
dao.getProperties(new RepositoryV1PropertyReader())
.forEachEntry((key, prop) -> {
if (key.equals("9ZQKlvI401")) {
Assertions.assertThat(prop.getBoolean("redmine.auto-close")).isNotPresent();
} else if (key.equals("1wPsrHPM81")) {
Assertions.assertThat(prop.getBoolean("redmine.auto-close")).isPresent().get().isEqualTo(true);
}
});
}
private static class SimpleContextProvider implements SCMContextProvider {
private final Path temp;
public SimpleContextProvider(Path temp) {
this.temp = temp;
}
@Override
public File getBaseDirectory() {
return temp.toFile();
}
@Override
public Path resolve(Path path) {
return null;
}
@Override
public Stage getStage() {
return null;
}
@Override
public Throwable getStartupError() {
return null;
}
@Override
public String getVersion() {
return null;
}
}
private static class SimpleKeyGenerator implements KeyGenerator {
@Override
public String createKey() {
return "" + System.nanoTime();
}
}
}