mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-22 06:26:56 +01:00
Feature/import export encryption (#1533)
Add option to encrypt repository exports with a password and add possibility to decrypt them on repository import. Also make the repository export asynchronous. This implies that the repository export will be created on the server and can be downloaded multiple times. The repository export will be deleted automatically 10 days after creation.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.importexport.ExportService;
|
||||
import sonia.scm.importexport.ExportStatus;
|
||||
import sonia.scm.importexport.RepositoryExportInformation;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RepositoryExportInformationToDtoMapperTest {
|
||||
|
||||
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/scm/api/"));
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private ExportService exportService;
|
||||
|
||||
private RepositoryExportInformationToDtoMapperImpl mapper;
|
||||
|
||||
@BeforeEach
|
||||
void initResourceLinks() {
|
||||
mapper = new RepositoryExportInformationToDtoMapperImpl();
|
||||
mapper.setResourceLinks(resourceLinks);
|
||||
mapper.setExportService(exportService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMapToInfoDtoWithLinks() {
|
||||
when(exportService.isExporting(REPOSITORY)).thenReturn(false);
|
||||
when(exportService.getExportInformation(REPOSITORY).getStatus()).thenReturn(ExportStatus.FINISHED);
|
||||
|
||||
String exporterName = "trillian";
|
||||
Instant now = Instant.now();
|
||||
RepositoryExportInformation info = new RepositoryExportInformation(exporterName, now, true, true, false, ExportStatus.FINISHED);
|
||||
|
||||
RepositoryExportInformationDto dto = mapper.map(info, REPOSITORY);
|
||||
|
||||
assertThat(dto.getExporterName()).isEqualTo(exporterName);
|
||||
assertThat(dto.getCreated()).isEqualTo(now);
|
||||
assertThat(dto.isCompressed()).isTrue();
|
||||
assertThat(dto.isWithMetadata()).isTrue();
|
||||
assertThat(dto.isEncrypted()).isFalse();
|
||||
assertThat(dto.getStatus()).isEqualTo(ExportStatus.FINISHED);
|
||||
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/scm/api/v2/repositories/hitchhiker/HeartOfGold/export/info");
|
||||
assertThat(dto.getLinks().getLinkBy("download").get().getHref()).isEqualTo("/scm/api/v2/repositories/hitchhiker/HeartOfGold/export/download");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAppendDownloadLink() {
|
||||
when(exportService.isExporting(REPOSITORY)).thenReturn(true);
|
||||
|
||||
String exporterName = "trillian";
|
||||
Instant now = Instant.now();
|
||||
RepositoryExportInformation info = new RepositoryExportInformation(exporterName, now, true, true, false, ExportStatus.EXPORTING);
|
||||
|
||||
RepositoryExportInformationDto dto = mapper.map(info, REPOSITORY);
|
||||
|
||||
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/scm/api/v2/repositories/hitchhiker/HeartOfGold/export/info");
|
||||
assertThat(dto.getLinks().getLinkBy("download")).isNotPresent();
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,16 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.importexport.ExportFileExtensionResolver;
|
||||
import sonia.scm.importexport.ExportService;
|
||||
import sonia.scm.importexport.ExportStatus;
|
||||
import sonia.scm.importexport.FullScmRepositoryExporter;
|
||||
import sonia.scm.importexport.FullScmRepositoryImporter;
|
||||
import sonia.scm.importexport.RepositoryImportExportEncryption;
|
||||
import sonia.scm.repository.CustomNamespaceStrategy;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.NamespaceStrategy;
|
||||
@@ -82,16 +87,17 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.stream.Stream.of;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
|
||||
@@ -108,7 +114,6 @@ import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.RETURNS_SELF;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -155,7 +160,15 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
@Mock
|
||||
private FullScmRepositoryExporter fullScmRepositoryExporter;
|
||||
@Mock
|
||||
private RepositoryExportInformationToDtoMapper exportInformationToDtoMapper;
|
||||
@Mock
|
||||
private FullScmRepositoryImporter fullScmRepositoryImporter;
|
||||
@Mock
|
||||
private RepositoryImportExportEncryption repositoryImportExportEncryption;
|
||||
@Mock
|
||||
private ExportFileExtensionResolver fileExtensionResolver;
|
||||
@Mock
|
||||
private ExportService exportService;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Predicate<Repository>> filterCaptor;
|
||||
@@ -170,15 +183,15 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper;
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() {
|
||||
public void prepareEnvironment() throws IOException {
|
||||
openMocks(this);
|
||||
super.repositoryToDtoMapper = repositoryToDtoMapper;
|
||||
super.dtoToRepositoryMapper = dtoToRepositoryMapper;
|
||||
super.manager = repositoryManager;
|
||||
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
|
||||
super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer);
|
||||
super.repositoryImportResource = new RepositoryImportResource(repositoryManager, dtoToRepositoryMapper, serviceFactory, resourceLinks, eventBus, fullScmRepositoryImporter);
|
||||
super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter);
|
||||
super.repositoryImportResource = new RepositoryImportResource(repositoryManager, dtoToRepositoryMapper, serviceFactory, resourceLinks, eventBus, fullScmRepositoryImporter, repositoryImportExportEncryption);
|
||||
super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter, repositoryImportExportEncryption, exportService, exportInformationToDtoMapper, fileExtensionResolver, resourceLinks);
|
||||
dispatcher.addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
|
||||
when(scmPathInfoStore.get()).thenReturn(uriInfo);
|
||||
@@ -191,6 +204,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
.principals(trillian)
|
||||
.authenticated(true)
|
||||
.buildSubject());
|
||||
when(repositoryImportExportEncryption.optionallyEncrypt(any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -543,12 +557,12 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
when(service.getPullCommand()).thenReturn(pullCommandBuilder);
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryImportResource.RepositoryImportDto repositoryImportDto = new RepositoryImportResource.RepositoryImportDto();
|
||||
repositoryImportDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportDto.setNamespace("scmadmin");
|
||||
repositoryImportDto.setName("scm-manager");
|
||||
RepositoryImportResource.RepositoryImportFromUrlDto repositoryImportFromUrlDto = new RepositoryImportResource.RepositoryImportFromUrlDto();
|
||||
repositoryImportFromUrlDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportFromUrlDto.setNamespace("scmadmin");
|
||||
repositoryImportFromUrlDto.setName("scm-manager");
|
||||
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportDto);
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportFromUrlDto);
|
||||
repositoryConsumer.accept(repository);
|
||||
|
||||
verify(pullCommandBuilder).pull("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
@@ -560,14 +574,14 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
when(service.getPullCommand()).thenReturn(pullCommandBuilder);
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryImportResource.RepositoryImportDto repositoryImportDto = new RepositoryImportResource.RepositoryImportDto();
|
||||
repositoryImportDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportDto.setNamespace("scmadmin");
|
||||
repositoryImportDto.setName("scm-manager");
|
||||
repositoryImportDto.setUsername("trillian");
|
||||
repositoryImportDto.setPassword("secret");
|
||||
RepositoryImportResource.RepositoryImportFromUrlDto repositoryImportFromUrlDto = new RepositoryImportResource.RepositoryImportFromUrlDto();
|
||||
repositoryImportFromUrlDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportFromUrlDto.setNamespace("scmadmin");
|
||||
repositoryImportFromUrlDto.setName("scm-manager");
|
||||
repositoryImportFromUrlDto.setUsername("trillian");
|
||||
repositoryImportFromUrlDto.setPassword("secret");
|
||||
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportDto);
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportFromUrlDto);
|
||||
repositoryConsumer.accept(repository);
|
||||
|
||||
verify(pullCommandBuilder).withUsername("trillian");
|
||||
@@ -581,12 +595,12 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
doThrow(ImportFailedException.class).when(pullCommandBuilder).pull(anyString());
|
||||
|
||||
Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryImportResource.RepositoryImportDto repositoryImportDto = new RepositoryImportResource.RepositoryImportDto();
|
||||
repositoryImportDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportDto.setNamespace("scmadmin");
|
||||
repositoryImportDto.setName("scm-manager");
|
||||
RepositoryImportResource.RepositoryImportFromUrlDto repositoryImportFromUrlDto = new RepositoryImportResource.RepositoryImportFromUrlDto();
|
||||
repositoryImportFromUrlDto.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
|
||||
repositoryImportFromUrlDto.setNamespace("scmadmin");
|
||||
repositoryImportFromUrlDto.setName("scm-manager");
|
||||
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportDto);
|
||||
Consumer<Repository> repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportFromUrlDto);
|
||||
assertThrows(ImportFailedException.class, () -> repositoryConsumer.accept(repository));
|
||||
}
|
||||
|
||||
@@ -732,6 +746,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
when(bundleCommandBuilder.getFileExtension()).thenReturn(".bundle");
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn");
|
||||
@@ -754,6 +769,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
when(bundleCommandBuilder.getFileExtension()).thenReturn(".bundle");
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn?compressed=true");
|
||||
@@ -785,7 +801,206 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
assertEquals(SC_OK, response.getStatus());
|
||||
assertEquals("application/x-gzip", response.getOutputHeaders().get("Content-Type").get(0).toString());
|
||||
verify(fullScmRepositoryExporter).export(eq(repository), any(OutputStream.class));
|
||||
verify(fullScmRepositoryExporter).export(eq(repository), any(OutputStream.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldExportFullRepositoryWithPassword() throws URISyntaxException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full")
|
||||
.contentType(VndMediaType.REPOSITORY_EXPORT)
|
||||
.content("{\"password\": \"hitchhiker\"}".getBytes());
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_OK, response.getStatus());
|
||||
assertEquals("application/x-gzip", response.getOutputHeaders().get("Content-Type").get(0).toString());
|
||||
verify(fullScmRepositoryExporter).export(eq(repository), any(OutputStream.class), any());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldExportFullRepositoryAsyncWithPassword() throws URISyntaxException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full")
|
||||
.contentType(VndMediaType.REPOSITORY_EXPORT)
|
||||
.content("{\"password\": \"hitchhiker\", \"async\":\"true\"}".getBytes());
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_ACCEPTED, response.getStatus());
|
||||
assertEquals("/v2/repositories/space/repo/export/download", response.getOutputHeaders().getFirst("SCM-Export-Download"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnConflictIfRepositoryAlreadyExporting() throws URISyntaxException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
when(exportService.isExporting(repository)).thenReturn(true);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full")
|
||||
.contentType(VndMediaType.REPOSITORY_EXPORT)
|
||||
.content("{\"password\": \"hitchhiker\", \"async\":\"true\"}".getBytes());
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_CONFLICT, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeleteRepositoryExport() throws URISyntaxException, IOException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
when(exportService.isExporting(repository)).thenReturn(false);
|
||||
when(exportService.getData(repository)).thenReturn(new ByteArrayInputStream("".getBytes()));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_NO_CONTENT, response.getStatus());
|
||||
verify(exportService).clear(repository.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnNotFoundIfExportDoesNotExist() throws URISyntaxException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
when(exportService.isExporting(repository)).thenReturn(false);
|
||||
doThrow(NotFoundException.class).when(exportService).checkExportIsAvailable(repository);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/download");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_NOT_FOUND, response.getStatus());
|
||||
verify(exportService).checkExportIsAvailable(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnConflictIfExportIsStillExporting() throws URISyntaxException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
when(exportService.isExporting(repository)).thenReturn(true);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/download");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_CONFLICT, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDownloadRepositoryExportIfReady() throws URISyntaxException, IOException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
when(exportService.isExporting(repository)).thenReturn(false);
|
||||
when(exportService.getData(repository)).thenReturn(new ByteArrayInputStream("content".getBytes()));
|
||||
when(exportService.getFileExtension(repository)).thenReturn("tar.gz.enc");
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/download");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(SC_OK, response.getStatus());
|
||||
verify(exportService).getData(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReturnExportInfo() throws URISyntaxException, IOException {
|
||||
String namespace = "space";
|
||||
String name = "repo";
|
||||
Repository repository = createRepository(namespace, name, "svn");
|
||||
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
|
||||
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
|
||||
|
||||
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
|
||||
|
||||
RepositoryExportInformationDto dto = new RepositoryExportInformationDto();
|
||||
dto.setExporterName("trillian");
|
||||
dto.setCreated(Instant.ofEpochMilli(100));
|
||||
dto.setStatus(ExportStatus.EXPORTING);
|
||||
when(exportInformationToDtoMapper.map(any(), eq(repository))).thenReturn(dto);
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/info");
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(
|
||||
"{\"exporterName\":\"trillian\",\"created\":0.100000000,\"withMetadata"
|
||||
+ "\":false,\"compressed\":false,\"encrypted\":false,\"status\":\"EXPORTING\"}",
|
||||
response.getContentAsString()
|
||||
);
|
||||
assertEquals(SC_OK, response.getStatus());
|
||||
verify(exportService).getExportInformation(repository);
|
||||
}
|
||||
|
||||
private void mockRepositoryHandler(Set<Command> cmds) {
|
||||
|
||||
@@ -285,23 +285,19 @@ public class RepositoryToRepositoryDtoMapperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateExportLink() {
|
||||
public void shouldCreateExportLinks() {
|
||||
Repository repository = createTestRepository();
|
||||
repository.setType("svn");
|
||||
RepositoryDto dto = mapper.map(repository);
|
||||
assertEquals(
|
||||
"http://example.com/base/v2/repositories/testspace/test/export/svn",
|
||||
dto.getLinks().getLinkBy("export").get().getHref());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateFullExportLink() {
|
||||
Repository repository = createTestRepository();
|
||||
repository.setType("svn");
|
||||
RepositoryDto dto = mapper.map(repository);
|
||||
assertEquals(
|
||||
"http://example.com/base/v2/repositories/testspace/test/export/full",
|
||||
dto.getLinks().getLinkBy("fullExport").get().getHref());
|
||||
assertEquals(
|
||||
"http://example.com/base/v2/repositories/testspace/test/export/info",
|
||||
dto.getLinks().getLinkBy("exportInfo").get().getHref());
|
||||
}
|
||||
|
||||
private ScmProtocol mockProtocol(String type, String protocol) {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.api.BundleCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.spi.BundleCommand;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ExportFileExtensionResolverTest {
|
||||
|
||||
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
@Mock
|
||||
private RepositoryServiceFactory serviceFactory;
|
||||
@Mock
|
||||
private RepositoryService service;
|
||||
@Mock
|
||||
private BundleCommandBuilder bundleCommand;
|
||||
|
||||
@InjectMocks
|
||||
private ExportFileExtensionResolver resolver;
|
||||
|
||||
@Test
|
||||
void shouldResolveWithMetadata() {
|
||||
String result = resolver.resolve(REPOSITORY, true, false, false);
|
||||
|
||||
assertThat(result).isEqualTo("tar.gz");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveWithMetadataAndEncrypted() {
|
||||
String result = resolver.resolve(REPOSITORY, true, false, true);
|
||||
|
||||
assertThat(result).isEqualTo("tar.gz.enc");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class withRepositoryService {
|
||||
|
||||
@BeforeEach
|
||||
void initBundleCommand() {
|
||||
when(serviceFactory.create(REPOSITORY)).thenReturn(service);
|
||||
when(service.getBundleCommand()).thenReturn(bundleCommand);
|
||||
when(bundleCommand.getFileExtension()).thenReturn("dump");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveDump() {
|
||||
String result = resolver.resolve(REPOSITORY, false, false, false);
|
||||
|
||||
assertThat(result).isEqualTo("dump");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveDump_Compressed() {
|
||||
String result = resolver.resolve(REPOSITORY, false, true, false);
|
||||
|
||||
assertThat(result).isEqualTo("dump.gz");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveDump_Encrypted() {
|
||||
String result = resolver.resolve(REPOSITORY, false, false, true);
|
||||
|
||||
assertThat(result).isEqualTo("dump.enc");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveDump_Compressed_Encrypted() {
|
||||
String result = resolver.resolve(REPOSITORY, false, true, true);
|
||||
|
||||
assertThat(result).isEqualTo("dump.gz.enc");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.NotFoundException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.store.BlobStoreFactory;
|
||||
import sonia.scm.store.DataStore;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.store.InMemoryBlobStore;
|
||||
import sonia.scm.store.InMemoryDataStore;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.importexport.ExportService.STORE_NAME;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ExportServiceTest {
|
||||
|
||||
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private BlobStoreFactory blobStoreFactory;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private DataStoreFactory dataStoreFactory;
|
||||
|
||||
@Mock
|
||||
private ExportFileExtensionResolver resolver;
|
||||
|
||||
private BlobStore blobStore;
|
||||
private DataStore<RepositoryExportInformation> dataStore;
|
||||
|
||||
@Mock
|
||||
private Subject subject;
|
||||
|
||||
@InjectMocks
|
||||
private ExportService exportService;
|
||||
|
||||
@BeforeEach
|
||||
void initMocks() {
|
||||
ThreadContext.bind(subject);
|
||||
PrincipalCollection principalCollection = mock(PrincipalCollection.class);
|
||||
lenient().when(subject.getPrincipals()).thenReturn(principalCollection);
|
||||
lenient().when(principalCollection.oneByType(User.class)).thenReturn(
|
||||
new User("trillian", "Trillian", "trillian@hitchhiker.org")
|
||||
);
|
||||
|
||||
blobStore = new InMemoryBlobStore();
|
||||
when(blobStoreFactory.withName(STORE_NAME).forRepository(REPOSITORY.getId()).build())
|
||||
.thenReturn(blobStore);
|
||||
|
||||
dataStore = new InMemoryDataStore<>();
|
||||
when(dataStoreFactory.withType(RepositoryExportInformation.class).withName(STORE_NAME).build())
|
||||
.thenReturn(dataStore);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
ThreadContext.unbindSubject();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldClearStoreIfEntryAlreadyExists() throws IOException {
|
||||
//Old content blob
|
||||
blobStore.create(REPOSITORY.getId());
|
||||
|
||||
String newContent = "Scm-Manager-Export";
|
||||
OutputStream os = exportService.store(REPOSITORY, true, true, true);
|
||||
os.write(newContent.getBytes());
|
||||
os.flush();
|
||||
os.close();
|
||||
|
||||
// Only new blob should exist
|
||||
List<Blob> blobs = blobStore.getAll();
|
||||
assertThat(blobs).hasSize(1);
|
||||
|
||||
//Verify content
|
||||
byte[] bytes = new byte[18];
|
||||
exportService.getData(REPOSITORY).read(bytes);
|
||||
assertThat(new String(bytes)).isEqualTo(newContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldShowCorrectExportStatus() {
|
||||
doNothing().when(subject).checkPermission("repository:export:" + REPOSITORY.getId());
|
||||
exportService.store(REPOSITORY, false, false, false);
|
||||
assertThat(exportService.isExporting(REPOSITORY)).isTrue();
|
||||
|
||||
exportService.setExportFinished(REPOSITORY);
|
||||
assertThat(exportService.isExporting(REPOSITORY)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOnlyClearRepositoryExports() {
|
||||
doNothing().when(subject).checkPermission("repository:export:" + REPOSITORY.getId());
|
||||
Repository hvpt = RepositoryTestData.createHappyVerticalPeopleTransporter();
|
||||
dataStore.put(hvpt.getId(), new RepositoryExportInformation());
|
||||
|
||||
blobStore.create(REPOSITORY.getId());
|
||||
dataStore.put(REPOSITORY.getId(), new RepositoryExportInformation());
|
||||
|
||||
exportService.clear(REPOSITORY.getId());
|
||||
|
||||
assertThat(dataStore.get(REPOSITORY.getId())).isNull();
|
||||
assertThat(dataStore.get(hvpt.getId())).isNotNull();
|
||||
assertThat(blobStore.getAll()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetExportInformation() {
|
||||
doNothing().when(subject).checkPermission("repository:export:" + REPOSITORY.getId());
|
||||
exportService.store(REPOSITORY, true, true, false);
|
||||
RepositoryExportInformation exportInformation = exportService.getExportInformation(REPOSITORY);
|
||||
|
||||
assertThat(exportInformation.getExporterName()).isEqualTo("trillian");
|
||||
assertThat(exportInformation.getCreated()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowNotFoundException() {
|
||||
assertThrows(NotFoundException.class, () -> exportService.getExportInformation(REPOSITORY));
|
||||
assertThrows(NotFoundException.class, () -> exportService.getFileExtension(REPOSITORY));
|
||||
assertThrows(NotFoundException.class, () -> exportService.getData(REPOSITORY));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveFileExtension() {
|
||||
doNothing().when(subject).checkPermission("repository:export:" + REPOSITORY.getId());
|
||||
String extension = "tar.gz.enc";
|
||||
RepositoryExportInformation info = new RepositoryExportInformation();
|
||||
dataStore.put(REPOSITORY.getId(), info);
|
||||
|
||||
when(resolver.resolve(REPOSITORY, false, false, false)).thenReturn(extension);
|
||||
|
||||
String fileExtension = exportService.getFileExtension(REPOSITORY);
|
||||
|
||||
assertThat(fileExtension).isEqualTo(extension);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOnlyCleanupUnfinishedExports() {
|
||||
blobStore.create(REPOSITORY.getId());
|
||||
RepositoryExportInformation info = new RepositoryExportInformation();
|
||||
info.setStatus(ExportStatus.EXPORTING);
|
||||
dataStore.put(
|
||||
REPOSITORY.getId(),
|
||||
info
|
||||
);
|
||||
|
||||
Repository finishedExport = RepositoryTestData.createHappyVerticalPeopleTransporter();
|
||||
BlobStore finishedExportBlobStore = new InMemoryBlobStore();
|
||||
Blob finishedExportBlob = finishedExportBlobStore.create(finishedExport.getId());
|
||||
RepositoryExportInformation finishedExportInfo = new RepositoryExportInformation();
|
||||
finishedExportInfo.setStatus(ExportStatus.FINISHED);
|
||||
dataStore.put(
|
||||
finishedExport.getId(),
|
||||
finishedExportInfo
|
||||
);
|
||||
when(blobStoreFactory.withName(STORE_NAME).forRepository(finishedExport.getId()).build())
|
||||
.thenReturn(finishedExportBlobStore);
|
||||
|
||||
exportService.cleanupUnfinishedExports();
|
||||
|
||||
assertThat(blobStore.getAll()).isEmpty();
|
||||
assertThat(dataStore.get(REPOSITORY.getId()).getStatus()).isEqualTo(ExportStatus.INTERRUPTED);
|
||||
assertThat(finishedExportBlobStore.get(finishedExport.getId())).isEqualTo(finishedExportBlob);
|
||||
assertThat(dataStore.get(finishedExport.getId()).getStatus()).isEqualTo(ExportStatus.FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOnlyCleanupOutdatedExports() {
|
||||
blobStore.create(REPOSITORY.getId());
|
||||
Instant now = Instant.now();
|
||||
RepositoryExportInformation newExportInfo = new RepositoryExportInformation();
|
||||
newExportInfo.setCreated(now);
|
||||
dataStore.put(REPOSITORY.getId(), newExportInfo);
|
||||
|
||||
Repository oldExportRepo = RepositoryTestData.createHappyVerticalPeopleTransporter();
|
||||
BlobStore oldExportBlobStore = new InMemoryBlobStore();
|
||||
oldExportBlobStore.create(oldExportRepo.getId());
|
||||
RepositoryExportInformation oldExportInfo = new RepositoryExportInformation();
|
||||
Instant old = Instant.now().minus(11, ChronoUnit.DAYS);
|
||||
oldExportInfo.setCreated(old);
|
||||
dataStore.put(oldExportRepo.getId(), oldExportInfo);
|
||||
when(blobStoreFactory.withName(STORE_NAME).forRepository(oldExportRepo.getId()).build())
|
||||
.thenReturn(oldExportBlobStore);
|
||||
|
||||
exportService.cleanupOutdatedExports();
|
||||
|
||||
assertThat(blobStore.getAll()).hasSize(1);
|
||||
assertThat(oldExportBlobStore.getAll()).isEmpty();
|
||||
assertThat(dataStore.get(REPOSITORY.getId()).getCreated()).isEqualTo(now);
|
||||
assertThat(dataStore.get(oldExportRepo.getId())).isNull();
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,11 @@ import java.util.function.Supplier;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FullScmRepositoryExporterTest {
|
||||
@@ -74,18 +78,21 @@ class FullScmRepositoryExporterTest {
|
||||
private WorkdirProvider workdirProvider;
|
||||
@Mock
|
||||
private RepositoryExportingCheck repositoryExportingCheck;
|
||||
@Mock
|
||||
private RepositoryImportExportEncryption repositoryImportExportEncryption;
|
||||
|
||||
@InjectMocks
|
||||
private FullScmRepositoryExporter exporter;
|
||||
|
||||
private Collection<Path> workDirsCreated = new ArrayList<>();
|
||||
private final Collection<Path> workDirsCreated = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
void initRepoService() {
|
||||
void initRepoService() throws IOException {
|
||||
when(serviceFactory.create(REPOSITORY)).thenReturn(repositoryService);
|
||||
when(environmentGenerator.generate()).thenReturn(new byte[0]);
|
||||
when(metadataGenerator.generate(REPOSITORY)).thenReturn(new byte[0]);
|
||||
when(repositoryExportingCheck.withExportingLock(any(), any())).thenAnswer(invocation -> invocation.getArgument(1, Supplier.class).get());
|
||||
when(repositoryImportExportEncryption.optionallyEncrypt(any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,7 +102,7 @@ class FullScmRepositoryExporterTest {
|
||||
when(repositoryService.getRepository()).thenReturn(REPOSITORY);
|
||||
when(workdirProvider.createNewWorkdir(anyString())).thenAnswer(invocation -> createWorkDir(temp, invocation.getArgument(0, String.class)));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
exporter.export(REPOSITORY, baos);
|
||||
exporter.export(REPOSITORY, baos, "");
|
||||
|
||||
verify(storeExporter, times(1)).export(eq(REPOSITORY), any(OutputStream.class));
|
||||
verify(environmentGenerator, times(1)).generate();
|
||||
|
||||
@@ -85,6 +85,8 @@ class FullScmRepositoryImporterTest {
|
||||
@Mock
|
||||
private UpdateEngine updateEngine;
|
||||
@Mock
|
||||
private RepositoryImportExportEncryption repositoryImportExportEncryption;
|
||||
@Mock
|
||||
private WorkdirProvider workdirProvider;
|
||||
|
||||
@InjectMocks
|
||||
@@ -100,13 +102,20 @@ class FullScmRepositoryImporterTest {
|
||||
|
||||
@BeforeEach
|
||||
void initTestObject() {
|
||||
fullImporter = new FullScmRepositoryImporter(environmentCheckStep, metadataImportStep, storeImportStep, repositoryImportStep, repositoryManager);
|
||||
fullImporter = new FullScmRepositoryImporter(
|
||||
environmentCheckStep,
|
||||
metadataImportStep,
|
||||
storeImportStep,
|
||||
repositoryImportStep,
|
||||
repositoryManager,
|
||||
repositoryImportExportEncryption);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initRepositoryService() {
|
||||
void initRepositoryService() throws IOException {
|
||||
lenient().when(serviceFactory.create(REPOSITORY)).thenReturn(service);
|
||||
lenient().when(service.getUnbundleCommand()).thenReturn(unbundleCommandBuilder);
|
||||
lenient().when(repositoryImportExportEncryption.decrypt(any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,7 +125,7 @@ class FullScmRepositoryImporterTest {
|
||||
FileInputStream inputStream = new FileInputStream(emptyFile.toFile());
|
||||
assertThrows(
|
||||
ImportFailedException.class,
|
||||
() -> fullImporter.importFromStream(REPOSITORY, inputStream)
|
||||
() -> fullImporter.importFromStream(REPOSITORY, inputStream, "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -127,7 +136,7 @@ class FullScmRepositoryImporterTest {
|
||||
InputStream importStream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
|
||||
assertThrows(
|
||||
IncompatibleEnvironmentForImportException.class,
|
||||
() -> fullImporter.importFromStream(REPOSITORY, importStream)
|
||||
() -> fullImporter.importFromStream(REPOSITORY, importStream, "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +155,7 @@ class FullScmRepositoryImporterTest {
|
||||
void shouldImportScmRepositoryArchiveWithWorkDir() throws IOException {
|
||||
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
|
||||
|
||||
Repository repository = fullImporter.importFromStream(REPOSITORY, stream);
|
||||
Repository repository = fullImporter.importFromStream(REPOSITORY, stream, "");
|
||||
|
||||
assertThat(repository).isEqualTo(REPOSITORY);
|
||||
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class));
|
||||
@@ -161,7 +170,7 @@ class FullScmRepositoryImporterTest {
|
||||
void shouldNotExistWorkDirAfterRepositoryImportIsFinished(@TempDir Path temp) throws IOException {
|
||||
when(workdirProvider.createNewWorkdir(REPOSITORY.getId())).thenReturn(temp.toFile());
|
||||
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
|
||||
fullImporter.importFromStream(REPOSITORY, stream);
|
||||
fullImporter.importFromStream(REPOSITORY, stream, "");
|
||||
|
||||
boolean workDirExists = Files.exists(temp);
|
||||
assertThat(workDirExists).isFalse();
|
||||
@@ -171,7 +180,7 @@ class FullScmRepositoryImporterTest {
|
||||
void shouldTriggerUpdateForImportedRepository() throws IOException {
|
||||
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
|
||||
|
||||
fullImporter.importFromStream(REPOSITORY, stream);
|
||||
fullImporter.importFromStream(REPOSITORY, stream, "");
|
||||
|
||||
verify(updateEngine).update(REPOSITORY.getId());
|
||||
}
|
||||
@@ -179,7 +188,7 @@ class FullScmRepositoryImporterTest {
|
||||
@Test
|
||||
void shouldImportRepositoryDirectlyWithoutCopyInWorkDir() throws IOException {
|
||||
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import-stores-before-repository.tar.gz").openStream();
|
||||
Repository repository = fullImporter.importFromStream(REPOSITORY, stream);
|
||||
Repository repository = fullImporter.importFromStream(REPOSITORY, stream, "");
|
||||
|
||||
assertThat(repository).isEqualTo(REPOSITORY);
|
||||
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class));
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.importexport;
|
||||
|
||||
import com.google.common.io.ByteSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class RepositoryImportExportEncryptionTest {
|
||||
|
||||
private final RepositoryImportExportEncryption encryption = new RepositoryImportExportEncryption();
|
||||
|
||||
@Test
|
||||
void shouldNotEncryptWithoutPassword() throws IOException {
|
||||
String content = "my content";
|
||||
String secret = "";
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
OutputStream os = encryption.optionallyEncrypt(baos, secret);
|
||||
os.write(content.getBytes());
|
||||
os.flush();
|
||||
|
||||
assertThat(os).hasToString(content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotDecryptWithoutPassword() throws IOException {
|
||||
String content = "my content";
|
||||
String secret = "";
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(content.getBytes());
|
||||
|
||||
InputStream is = encryption.optionallyDecrypt(bais, secret);
|
||||
|
||||
ByteSource byteSource = new ByteSource() {
|
||||
@Override
|
||||
public InputStream openStream() {
|
||||
return is;
|
||||
}
|
||||
};
|
||||
|
||||
String result = byteSource.asCharSource(StandardCharsets.UTF_8).read();
|
||||
|
||||
assertThat(result).isEqualTo(content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEncryptAndDecryptContentWithPassword() throws IOException {
|
||||
String content = "my content";
|
||||
String secret = "secretPassword";
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
OutputStream os = encryption.optionallyEncrypt(baos, secret);
|
||||
os.write(content.getBytes());
|
||||
os.flush();
|
||||
os.close();
|
||||
|
||||
assertThat(baos.toString()).isNotEqualTo(content);
|
||||
|
||||
InputStream is = encryption.optionallyDecrypt(new ByteArrayInputStream(baos.toByteArray()), secret);
|
||||
ByteSource byteSource = new ByteSource() {
|
||||
@Override
|
||||
public InputStream openStream() {
|
||||
return is;
|
||||
}
|
||||
};
|
||||
String result = byteSource.asCharSource(StandardCharsets.UTF_8).read();
|
||||
|
||||
assertThat(result).isEqualTo(content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailOnDecryptIfNotEncrypted() {
|
||||
String content = "my content";
|
||||
String secret = "secretPassword";
|
||||
|
||||
ByteArrayInputStream notEncryptedStream = new ByteArrayInputStream(content.getBytes());
|
||||
|
||||
assertThrows(IOException.class, () -> encryption.optionallyDecrypt(notEncryptedStream, secret));
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
@@ -39,6 +39,7 @@ import sonia.scm.SCMContext;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.group.GroupManager;
|
||||
import sonia.scm.importexport.ExportService;
|
||||
import sonia.scm.security.AnonymousMode;
|
||||
import sonia.scm.security.PermissionAssigner;
|
||||
import sonia.scm.security.PermissionDescriptor;
|
||||
@@ -82,6 +83,9 @@ class SetupContextListenerTest {
|
||||
@Mock
|
||||
private GroupManager groupManager;
|
||||
|
||||
@Mock
|
||||
private ExportService exportService;
|
||||
|
||||
@Mock
|
||||
private PermissionAssigner permissionAssigner;
|
||||
|
||||
@@ -209,6 +213,13 @@ class SetupContextListenerTest {
|
||||
verify(groupManager, never()).create(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCleanupUnfinishedRepositoryExports() {
|
||||
setupContextListener.contextInitialized(null);
|
||||
|
||||
verify(exportService).cleanupUnfinishedExports();
|
||||
}
|
||||
|
||||
private void verifyAdminPermissionsAssigned() {
|
||||
ArgumentCaptor<String> usernameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<Collection<PermissionDescriptor>> permissionCaptor = ArgumentCaptor.forClass(Collection.class);
|
||||
|
||||
Reference in New Issue
Block a user