diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 0e3c56944c..06dbcff502 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -90,5 +90,6 @@ public class MapperModule extends AbstractModule { bind(ApiKeyToApiKeyDtoMapper.class).to(Mappers.getMapperClass(ApiKeyToApiKeyDtoMapper.class)); bind(RepositoryExportInformationToDtoMapper.class).to(Mappers.getMapperClass(RepositoryExportInformationToDtoMapper.class)); + bind(RepositoryImportDtoToRepositoryImportParametersMapper.class).to(Mappers.getMapperClass(RepositoryImportDtoToRepositoryImportParametersMapper.class)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java index 6485e40393..a8ed757954 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java @@ -80,8 +80,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static sonia.scm.ContextEntry.ContextBuilder.entity; -import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.checkSupport; -import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.type; +import static sonia.scm.importexport.RepositoryTypeSupportChecker.checkSupport; +import static sonia.scm.importexport.RepositoryTypeSupportChecker.type; public class RepositoryExportResource { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportDtoToRepositoryImportParametersMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportDtoToRepositoryImportParametersMapper.java new file mode 100644 index 0000000000..3c73d97c9b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportDtoToRepositoryImportParametersMapper.java @@ -0,0 +1,33 @@ +/* + * 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.mapstruct.Mapper; +import sonia.scm.importexport.FromUrlImporter; + +@Mapper +public interface RepositoryImportDtoToRepositoryImportParametersMapper { + FromUrlImporter.RepositoryImportParameters map(RepositoryImportResource.RepositoryImportFromUrlDto dto); +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java index b87142c3a7..2aa982b64e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java @@ -27,10 +27,8 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.io.ByteSource; -import com.google.common.io.Files; import com.google.inject.Inject; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -39,30 +37,20 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.apache.shiro.SecurityUtils; import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.ContextEntry; -import sonia.scm.HandlerEventType; -import sonia.scm.Type; -import sonia.scm.event.ScmEventBus; +import sonia.scm.importexport.FromBundleImporter; +import sonia.scm.importexport.FromUrlImporter; import sonia.scm.importexport.FullScmRepositoryImporter; import sonia.scm.importexport.RepositoryImportExportEncryption; -import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryImportEvent; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.ImportFailedException; -import sonia.scm.repository.api.PullCommandBuilder; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.repository.api.RepositoryServiceFactory; -import sonia.scm.util.IOUtil; import sonia.scm.web.VndMediaType; import sonia.scm.web.api.DtoValidator; @@ -80,46 +68,40 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Collections.singletonList; -import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.checkSupport; -import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.type; public class RepositoryImportResource { private static final Logger logger = LoggerFactory.getLogger(RepositoryImportResource.class); - private final RepositoryManager manager; private final RepositoryDtoToRepositoryMapper mapper; - private final RepositoryServiceFactory serviceFactory; private final ResourceLinks resourceLinks; - private final ScmEventBus eventBus; private final FullScmRepositoryImporter fullScmRepositoryImporter; private final RepositoryImportExportEncryption repositoryImportExportEncryption; + private final RepositoryImportDtoToRepositoryImportParametersMapper importParametersMapper; + private final FromUrlImporter fromUrlImporter; + private final FromBundleImporter fromBundleImporter; @Inject - public RepositoryImportResource(RepositoryManager manager, - RepositoryDtoToRepositoryMapper mapper, - RepositoryServiceFactory serviceFactory, + public RepositoryImportResource(RepositoryDtoToRepositoryMapper mapper, ResourceLinks resourceLinks, - ScmEventBus eventBus, FullScmRepositoryImporter fullScmRepositoryImporter, - RepositoryImportExportEncryption repositoryImportExportEncryption) { - this.manager = manager; + RepositoryImportDtoToRepositoryImportParametersMapper importParametersMapper, + RepositoryImportExportEncryption repositoryImportExportEncryption, FromUrlImporter fromUrlImporter, + FromBundleImporter fromBundleImporter) { this.mapper = mapper; - this.serviceFactory = serviceFactory; this.resourceLinks = resourceLinks; - this.eventBus = eventBus; this.fullScmRepositoryImporter = fullScmRepositoryImporter; this.repositoryImportExportEncryption = repositoryImportExportEncryption; + this.importParametersMapper = importParametersMapper; + this.fromUrlImporter = fromUrlImporter; + this.fromBundleImporter = fromBundleImporter; } /** @@ -166,49 +148,13 @@ public class RepositoryImportResource { public Response importFromUrl(@Context UriInfo uriInfo, @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type, @Valid RepositoryImportResource.RepositoryImportFromUrlDto request) { - RepositoryPermissions.create().check(); - - Type t = type(manager, type); - if (!t.getName().equals(request.getType())) { + if (!type.equals(request.getType())) { throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST); } - checkSupport(t, Command.PULL); - logger.info("start {} import for external url {}", type, request.getImportUrl()); + Repository repository = fromUrlImporter.importFromUrl(importParametersMapper.map(request), mapper.map(request)); - Repository repository = mapper.map(request); - repository.setPermissions(singletonList(new RepositoryPermission(SecurityUtils.getSubject().getPrincipal().toString(), "OWNER", false))); - - try { - repository = manager.create( - repository, - pullChangesFromRemoteUrl(request) - ); - eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, false)); - - return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build(); - } catch (Exception e) { - eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, true)); - throw e; - } - } - - @VisibleForTesting - Consumer pullChangesFromRemoteUrl(RepositoryImportFromUrlDto request) { - return repository -> { - try (RepositoryService service = serviceFactory.create(repository)) { - PullCommandBuilder pullCommand = service.getPullCommand(); - if (!Strings.isNullOrEmpty(request.getUsername()) && !Strings.isNullOrEmpty(request.getPassword())) { - pullCommand - .withUsername(request.getUsername()) - .withPassword(request.getPassword()); - } - - pullCommand.pull(request.getImportUrl()); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "Failed to import from remote url", e); - } - }; + return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build(); } /** @@ -332,25 +278,13 @@ public class RepositoryImportResource { inputStream = decryptInputStream(inputStream, repositoryDto.getPassword()); } - Type t = type(manager, type); - checkSupport(t, Command.UNBUNDLE); + if (!type.equals(repositoryDto.getType())) { + throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST); + } Repository repository = mapper.map(repositoryDto); - repository.setPermissions(singletonList( - new RepositoryPermission(SecurityUtils.getSubject().getPrincipal().toString(), "OWNER", false) - )); - try { - repository = manager.create( - repository, - unbundleImport(inputStream, compressed) - ); - eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, false)); - - } catch (Exception e) { - eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, true)); - throw e; - } + repository = fromBundleImporter.importFromBundle(compressed, inputStream, repository); return repository; } @@ -363,27 +297,6 @@ public class RepositoryImportResource { } } - @VisibleForTesting - Consumer unbundleImport(InputStream inputStream, boolean compressed) { - return repository -> { - File file = null; - try (RepositoryService service = serviceFactory.create(repository)) { - file = File.createTempFile("scm-import-", ".bundle"); - long length = Files.asByteSink(file).writeFrom(inputStream); - logger.info("copied {} bytes to temp, start bundle import", length); - service.getUnbundleCommand().setCompressed(compressed).unbundle(file); - } catch (IOException e) { - throw new InternalRepositoryException(repository, "Failed to import from bundle", e); - } finally { - try { - IOUtil.delete(file); - } catch (IOException ex) { - logger.warn("could not delete temporary file", ex); - } - } - }; - } - private RepositoryImportFromFileDto extractRepositoryDto(Map> formParts) { RepositoryImportFromFileDto repositoryDto = extractFromInputPart(formParts.get("repository"), RepositoryImportFromFileDto.class); checkNotNull(repositoryDto, "repository data is required"); diff --git a/scm-webapp/src/main/java/sonia/scm/importexport/FromBundleImporter.java b/scm-webapp/src/main/java/sonia/scm/importexport/FromBundleImporter.java new file mode 100644 index 0000000000..c01d06887b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/importexport/FromBundleImporter.java @@ -0,0 +1,109 @@ +/* + * 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.annotations.VisibleForTesting; +import com.google.common.io.Files; +import org.apache.shiro.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.HandlerEventType; +import sonia.scm.Type; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryImportEvent; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.function.Consumer; + +import static java.util.Collections.singletonList; +import static sonia.scm.importexport.RepositoryTypeSupportChecker.checkSupport; +import static sonia.scm.importexport.RepositoryTypeSupportChecker.type; + +public class FromBundleImporter { + + private static final Logger LOG = LoggerFactory.getLogger(FromBundleImporter.class); + + private final RepositoryManager manager; + private final RepositoryServiceFactory serviceFactory; + private final ScmEventBus eventBus; + + @Inject + public FromBundleImporter(RepositoryManager manager, RepositoryServiceFactory serviceFactory, ScmEventBus eventBus) { + this.manager = manager; + this.serviceFactory = serviceFactory; + this.eventBus = eventBus; + } + + public Repository importFromBundle(boolean compressed, InputStream inputStream, Repository repository) { + Type t = type(manager, repository.getType()); + checkSupport(t, Command.UNBUNDLE); + + repository.setPermissions(singletonList( + new RepositoryPermission(SecurityUtils.getSubject().getPrincipal().toString(), "OWNER", false) + )); + + try { + repository = manager.create(repository, unbundleImport(inputStream, compressed)); + eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, false)); + } catch (Exception e) { + eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, true)); + throw e; + } + + return repository; + } + + @VisibleForTesting + Consumer unbundleImport(InputStream inputStream, boolean compressed) { + return repository -> { + File file = null; + try (RepositoryService service = serviceFactory.create(repository)) { + file = File.createTempFile("scm-import-", ".bundle"); + long length = Files.asByteSink(file).writeFrom(inputStream); + LOG.info("copied {} bytes to temp, start bundle import", length); + service.getUnbundleCommand().setCompressed(compressed).unbundle(file); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "Failed to import from bundle", e); + } finally { + try { + IOUtil.delete(file); + } catch (IOException ex) { + LOG.warn("could not delete temporary file", ex); + } + } + }; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/importexport/FromUrlImporter.java b/scm-webapp/src/main/java/sonia/scm/importexport/FromUrlImporter.java new file mode 100644 index 0000000000..2377124731 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/importexport/FromUrlImporter.java @@ -0,0 +1,119 @@ +/* + * 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.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import lombok.Getter; +import lombok.Setter; +import org.apache.shiro.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.HandlerEventType; +import sonia.scm.Type; +import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryImportEvent; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.PullCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.function.Consumer; + +import static java.util.Collections.singletonList; +import static sonia.scm.importexport.RepositoryTypeSupportChecker.checkSupport; +import static sonia.scm.importexport.RepositoryTypeSupportChecker.type; + +public class FromUrlImporter { + + private static final Logger LOG = LoggerFactory.getLogger(FromUrlImporter.class); + + private final RepositoryManager manager; + private final RepositoryServiceFactory serviceFactory; + private final ScmEventBus eventBus; + + @Inject + public FromUrlImporter(RepositoryManager manager, RepositoryServiceFactory serviceFactory, ScmEventBus eventBus) { + this.manager = manager; + this.serviceFactory = serviceFactory; + this.eventBus = eventBus; + } + + public Repository importFromUrl(RepositoryImportParameters parameters, Repository repository) { + Type t = type(manager, repository.getType()); + RepositoryPermissions.create().check(); + checkSupport(t, Command.PULL); + + LOG.info("start {} import for external url {}", repository.getType(), parameters.getImportUrl()); + + repository.setPermissions(singletonList(new RepositoryPermission(SecurityUtils.getSubject().getPrincipal().toString(), "OWNER", false))); + + try { + repository = manager.create( + repository, + pullChangesFromRemoteUrl(parameters) + ); + eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, false)); + } catch (Exception e) { + eventBus.post(new RepositoryImportEvent(HandlerEventType.MODIFY, repository, true)); + throw e; + } + return repository; + } + + @VisibleForTesting + Consumer pullChangesFromRemoteUrl(RepositoryImportParameters parameters) { + return repository -> { + try (RepositoryService service = serviceFactory.create(repository)) { + PullCommandBuilder pullCommand = service.getPullCommand(); + if (!Strings.isNullOrEmpty(parameters.getUsername()) && !Strings.isNullOrEmpty(parameters.getPassword())) { + pullCommand + .withUsername(parameters.getUsername()) + .withPassword(parameters.getPassword()); + } + + pullCommand.pull(parameters.getImportUrl()); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "Failed to import from remote url", e); + } + }; + } + + @Getter + @Setter + public static class RepositoryImportParameters { + private String importUrl; + private String username; + private String password; + + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeSupportChecker.java b/scm-webapp/src/main/java/sonia/scm/importexport/RepositoryTypeSupportChecker.java similarity index 92% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeSupportChecker.java rename to scm-webapp/src/main/java/sonia/scm/importexport/RepositoryTypeSupportChecker.java index 11759ea0c8..fc9345427c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryTypeSupportChecker.java +++ b/scm-webapp/src/main/java/sonia/scm/importexport/RepositoryTypeSupportChecker.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package sonia.scm.api.v2.resources; +package sonia.scm.importexport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,7 +36,7 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import java.util.Set; -class RepositoryTypeSupportChecker { +public class RepositoryTypeSupportChecker { private RepositoryTypeSupportChecker() { } @@ -49,7 +49,7 @@ class RepositoryTypeSupportChecker { * @param type repository type * @param cmd command */ - static void checkSupport(Type type, Command cmd) { + public static void checkSupport(Type type, Command cmd) { if (!(type instanceof RepositoryType)) { logger.warn("type {} is not a repository type", type.getName()); throw new WebApplicationException(Response.Status.BAD_REQUEST); @@ -65,7 +65,7 @@ class RepositoryTypeSupportChecker { } @SuppressWarnings("javasecurity:S5145") // the type parameter is validated in the resource to only contain valid characters (\w) - static Type type(RepositoryManager manager, String type) { + public static Type type(RepositoryManager manager, String type) { RepositoryHandler handler = manager.getHandler(type); if (handler == null) { logger.warn("no handler for type {} found", type); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index da00c043ff..413f108a66 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -48,6 +48,8 @@ import sonia.scm.event.ScmEventBus; import sonia.scm.importexport.ExportFileExtensionResolver; import sonia.scm.importexport.ExportService; import sonia.scm.importexport.ExportStatus; +import sonia.scm.importexport.FromBundleImporter; +import sonia.scm.importexport.FromUrlImporter; import sonia.scm.importexport.FullScmRepositoryExporter; import sonia.scm.importexport.FullScmRepositoryImporter; import sonia.scm.importexport.RepositoryImportExportEncryption; @@ -64,11 +66,8 @@ import sonia.scm.repository.RepositoryType; import sonia.scm.repository.api.BundleCommandBuilder; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.ImportFailedException; -import sonia.scm.repository.api.PullCommandBuilder; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; -import sonia.scm.repository.api.UnbundleCommandBuilder; -import sonia.scm.repository.api.UnbundleResponse; import sonia.scm.user.User; import sonia.scm.web.RestDispatcher; import sonia.scm.web.VndMediaType; @@ -77,7 +76,6 @@ import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MediaType; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -92,7 +90,6 @@ 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 static java.util.Collections.singletonList; @@ -107,13 +104,10 @@ import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; 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.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -166,6 +160,10 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Mock private RepositoryImportExportEncryption repositoryImportExportEncryption; @Mock + private FromUrlImporter fromUrlImporter; + @Mock + private FromBundleImporter fromBundleImporter; + @Mock private ExportFileExtensionResolver fileExtensionResolver; @Mock private ExportService exportService; @@ -175,7 +173,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - private Repository repositoryMarkedAsExported; @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; @@ -190,7 +187,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { 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, repositoryImportExportEncryption); + super.repositoryImportResource = new RepositoryImportResource(dtoToRepositoryMapper, resourceLinks, fullScmRepositoryImporter, new RepositoryImportDtoToRepositoryImportParametersMapperImpl(), repositoryImportExportEncryption, fromUrlImporter, fromBundleImporter); super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter, repositoryImportExportEncryption, exportService, exportInformationToDtoMapper, fileExtensionResolver, resourceLinks); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(any(Repository.class))).thenReturn(service); @@ -551,88 +548,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertThat(captor.getValue().isFailed()).isTrue(); } - @Test - public void shouldPullChangesFromRemoteUrl() throws IOException { - PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF); - when(service.getPullCommand()).thenReturn(pullCommandBuilder); - - Repository repository = RepositoryTestData.createHeartOfGold(); - 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 repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportFromUrlDto); - repositoryConsumer.accept(repository); - - verify(pullCommandBuilder).pull("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git"); - } - - @Test - public void shouldPullChangesFromRemoteUrlWithCredentials() { - PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF); - when(service.getPullCommand()).thenReturn(pullCommandBuilder); - - Repository repository = RepositoryTestData.createHeartOfGold(); - 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 repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportFromUrlDto); - repositoryConsumer.accept(repository); - - verify(pullCommandBuilder).withUsername("trillian"); - verify(pullCommandBuilder).withPassword("secret"); - } - - @Test - public void shouldThrowImportFailedEvent() throws IOException { - PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF); - when(service.getPullCommand()).thenReturn(pullCommandBuilder); - doThrow(ImportFailedException.class).when(pullCommandBuilder).pull(anyString()); - - Repository repository = RepositoryTestData.createHeartOfGold(); - 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 repositoryConsumer = repositoryImportResource.pullChangesFromRemoteUrl(repositoryImportFromUrlDto); - assertThrows(ImportFailedException.class, () -> repositoryConsumer.accept(repository)); - } - - @Test - public void shouldImportRepositoryFromBundle() throws IOException, URISyntaxException { - when(manager.getHandler("svn")).thenReturn(repositoryHandler); - when(repositoryHandler.getType()).thenReturn(new RepositoryType("svn", "svn", ImmutableSet.of(Command.UNBUNDLE))); - when(repositoryManager.create(any(), any())).thenReturn(RepositoryTestData.createHeartOfGold()); - - RepositoryDto repositoryDto = new RepositoryDto(); - repositoryDto.setName("HeartOfGold"); - repositoryDto.setNamespace("hitchhiker"); - repositoryDto.setType("svn"); - - URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump"); - byte[] svnDump = Resources.toByteArray(dumpUrl); - - MockHttpRequest request = MockHttpRequest - .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/svn/bundle"); - MockHttpResponse response = new MockHttpResponse(); - - multipartRequest(request, Collections.singletonMap("bundle", new ByteArrayInputStream(svnDump)), repositoryDto); - - dispatcher.invoke(request, response); - - assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); - assertEquals("/v2/repositories/hitchhiker/HeartOfGold", response.getOutputHeaders().get("Location").get(0).toString()); - ArgumentCaptor event = ArgumentCaptor.forClass(RepositoryImportEvent.class); - verify(eventBus).post(event.capture()); - assertFalse(event.getValue().isFailed()); - } - @Test public void shouldThrowFailedEventOnImportRepositoryFromBundle() throws IOException, URISyntaxException { when(manager.getHandler("svn")).thenReturn(repositoryHandler); @@ -661,44 +576,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertTrue(event.getValue().isFailed()); } - @Test - public void shouldImportCompressedBundle() throws IOException { - URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump.gz"); - byte[] svnDump = Resources.toByteArray(dumpUrl); - - UnbundleCommandBuilder ubc = mock(UnbundleCommandBuilder.class, RETURNS_SELF); - when(ubc.unbundle(any(File.class))).thenReturn(new UnbundleResponse(42)); - RepositoryService service = mock(RepositoryService.class); - when(serviceFactory.create(any(Repository.class))).thenReturn(service); - when(service.getUnbundleCommand()).thenReturn(ubc); - InputStream in = new ByteArrayInputStream(svnDump); - - Consumer repositoryConsumer = repositoryImportResource.unbundleImport(in, true); - repositoryConsumer.accept(RepositoryTestData.createHeartOfGold("svn")); - - verify(ubc).setCompressed(true); - verify(ubc).unbundle(any(File.class)); - } - - @Test - public void shouldImportNonCompressedBundle() throws IOException { - URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump"); - byte[] svnDump = Resources.toByteArray(dumpUrl); - - UnbundleCommandBuilder ubc = mock(UnbundleCommandBuilder.class, RETURNS_SELF); - when(ubc.unbundle(any(File.class))).thenReturn(new UnbundleResponse(21)); - RepositoryService service = mock(RepositoryService.class); - when(serviceFactory.create(any(Repository.class))).thenReturn(service); - when(service.getUnbundleCommand()).thenReturn(ubc); - InputStream in = new ByteArrayInputStream(svnDump); - - Consumer repositoryConsumer = repositoryImportResource.unbundleImport(in, false); - repositoryConsumer.accept(RepositoryTestData.createHeartOfGold("svn")); - - verify(ubc, never()).setCompressed(true); - verify(ubc).unbundle(any(File.class)); - } - @Test public void shouldMarkRepositoryAsArchived() throws Exception { String namespace = "space"; diff --git a/scm-webapp/src/test/java/sonia/scm/importexport/FromBundleImporterTest.java b/scm-webapp/src/test/java/sonia/scm/importexport/FromBundleImporterTest.java new file mode 100644 index 0000000000..a70f2b51ca --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/importexport/FromBundleImporterTest.java @@ -0,0 +1,108 @@ +/* + * 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.Resources; +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.event.ScmEventBus; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.UnbundleCommandBuilder; +import sonia.scm.repository.api.UnbundleResponse; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.function.Consumer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@SuppressWarnings("UnstableApiUsage") +class FromBundleImporterTest { + + @Mock + private RepositoryManager manager; + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private ScmEventBus eventBus; + + @InjectMocks + private FromBundleImporter importer; + + @Test + void shouldImportCompressedBundle() throws IOException { + URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump.gz"); + byte[] svnDump = Resources.toByteArray(dumpUrl); + + UnbundleCommandBuilder ubc = mock(UnbundleCommandBuilder.class, RETURNS_SELF); + when(ubc.unbundle(any(File.class))).thenReturn(new UnbundleResponse(42)); + RepositoryService service = mock(RepositoryService.class); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getUnbundleCommand()).thenReturn(ubc); + InputStream in = new ByteArrayInputStream(svnDump); + + Consumer repositoryConsumer = importer.unbundleImport(in, true); + repositoryConsumer.accept(RepositoryTestData.createHeartOfGold("svn")); + + verify(ubc).setCompressed(true); + verify(ubc).unbundle(any(File.class)); + } + + @Test + void shouldImportNonCompressedBundle() throws IOException { + URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump"); + byte[] svnDump = Resources.toByteArray(dumpUrl); + + UnbundleCommandBuilder ubc = mock(UnbundleCommandBuilder.class, RETURNS_SELF); + when(ubc.unbundle(any(File.class))).thenReturn(new UnbundleResponse(21)); + RepositoryService service = mock(RepositoryService.class); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getUnbundleCommand()).thenReturn(ubc); + InputStream in = new ByteArrayInputStream(svnDump); + + Consumer repositoryConsumer = importer.unbundleImport(in, false); + repositoryConsumer.accept(RepositoryTestData.createHeartOfGold("svn")); + + verify(ubc, never()).setCompressed(true); + verify(ubc).unbundle(any(File.class)); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/importexport/FromUrlImporterTest.java b/scm-webapp/src/test/java/sonia/scm/importexport/FromUrlImporterTest.java new file mode 100644 index 0000000000..50a6224e46 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/importexport/FromUrlImporterTest.java @@ -0,0 +1,120 @@ +/* + * 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.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.event.ScmEventBus; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryTestData; +import sonia.scm.repository.api.ImportFailedException; +import sonia.scm.repository.api.PullCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import java.io.IOException; +import java.util.function.Consumer; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FromUrlImporterTest { + + @Mock + private RepositoryManager manager; + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + @Mock + private ScmEventBus eventBus; + + @InjectMocks + private FromUrlImporter importer; + + @BeforeEach + void setUpMocks() { + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + } + + @Test + void shouldPullChangesFromRemoteUrl() throws IOException { + PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF); + when(service.getPullCommand()).thenReturn(pullCommandBuilder); + + Repository repository = RepositoryTestData.createHeartOfGold(); + FromUrlImporter.RepositoryImportParameters parameters = new FromUrlImporter.RepositoryImportParameters(); + parameters.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git"); + + Consumer repositoryConsumer = importer.pullChangesFromRemoteUrl(parameters); + repositoryConsumer.accept(repository); + + verify(pullCommandBuilder).pull("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git"); + } + + @Test + void shouldPullChangesFromRemoteUrlWithCredentials() { + PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF); + when(service.getPullCommand()).thenReturn(pullCommandBuilder); + + Repository repository = RepositoryTestData.createHeartOfGold(); + FromUrlImporter.RepositoryImportParameters parameters = new FromUrlImporter.RepositoryImportParameters(); + parameters.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git"); + parameters.setUsername("trillian"); + parameters.setPassword("secret"); + + Consumer repositoryConsumer = importer.pullChangesFromRemoteUrl(parameters); + repositoryConsumer.accept(repository); + + verify(pullCommandBuilder).withUsername("trillian"); + verify(pullCommandBuilder).withPassword("secret"); + } + + @Test + void shouldThrowImportFailedEvent() throws IOException { + PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF); + when(service.getPullCommand()).thenReturn(pullCommandBuilder); + doThrow(ImportFailedException.class).when(pullCommandBuilder).pull(anyString()); + + Repository repository = RepositoryTestData.createHeartOfGold(); + FromUrlImporter.RepositoryImportParameters parameters = new FromUrlImporter.RepositoryImportParameters(); + parameters.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git"); + + Consumer repositoryConsumer = importer.pullChangesFromRemoteUrl(parameters); + assertThrows(ImportFailedException.class, () -> repositoryConsumer.accept(repository)); + } +}