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:
Eduard Heimbuch
2021-02-25 13:01:03 +01:00
committed by GitHub
parent 367d7294b8
commit db2ce98721
53 changed files with 2698 additions and 237 deletions

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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");
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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));

View File

@@ -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));
}
}

View File

@@ -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);