mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-21 05:56:53 +01:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user