Add import protocol (#1558)

Adds a protocol for repository imports (either from an URL, a dump file or a SCM-Manager repository archive).
This protocol documents single steps of an import, the time and the user and is accessible via a dedicated REST
endpoint or a simple ui.

The id of the log is added to the repository imported event, so that plugins like the landingpage or mail can link to these logs.
This commit is contained in:
René Pfeuffer
2021-02-26 13:52:29 +01:00
committed by GitHub
parent ac404b3cdd
commit 0695ca3bac
41 changed files with 1808 additions and 480 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Added import protocols ([#1558](https://github.com/scm-manager/scm-manager/pull/1558))

View File

@@ -24,9 +24,7 @@
package sonia.scm.repository; package sonia.scm.repository;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import sonia.scm.HandlerEventType;
import sonia.scm.event.Event; import sonia.scm.event.Event;
/** /**
@@ -36,13 +34,15 @@ import sonia.scm.event.Event;
*/ */
@Event @Event
@Getter @Getter
@EqualsAndHashCode(callSuper = true) public class RepositoryImportEvent {
public class RepositoryImportEvent extends RepositoryEvent {
private final Repository item;
private final String logId;
private final boolean failed; private final boolean failed;
public RepositoryImportEvent(HandlerEventType eventType, Repository repository, boolean failed) { public RepositoryImportEvent(Repository item, boolean failed) {
super(eventType, repository); this.item = item;
this.logId = item.getId();
this.failed = failed; this.failed = failed;
} }
} }

View File

@@ -0,0 +1,60 @@
/*
* 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.store;
import java.util.HashMap;
import java.util.Map;
public class InMemoryBlobStoreFactory implements BlobStoreFactory {
private final Map<String, BlobStore> stores = new HashMap<>();
private final BlobStore fixedStore;
public InMemoryBlobStoreFactory() {
this(null);
}
public InMemoryBlobStoreFactory(BlobStore fixedStore) {
this.fixedStore = fixedStore;
}
@Override
public BlobStore getStore(StoreParameters storeParameters) {
if (fixedStore == null) {
return stores.computeIfAbsent(computeKey(storeParameters), key -> new InMemoryBlobStore());
} else {
return fixedStore;
}
}
private String computeKey(StoreParameters storeParameters) {
if (storeParameters.getRepositoryId() == null) {
return storeParameters.getName();
} else {
return storeParameters.getName() + "/" + storeParameters.getRepositoryId();
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
import { ApiResult, useRequiredIndexLink } from "./base";
import { useQuery } from "react-query";
import { apiClient } from "./apiclient";
export const useImportLog = (logId: string) : ApiResult<string> => {
const link = useRequiredIndexLink("importLog").replace("{logId}", logId);
return useQuery<string, Error>(["importLog", logId], () => apiClient.get(link).then(response => response.text()));
}

View File

@@ -43,6 +43,7 @@ export * from "./plugins";
export * from "./repository-roles"; export * from "./repository-roles";
export * from "./permissions"; export * from "./permissions";
export * from "./sources"; export * from "./sources";
export * from "./import";
export { default as ApiProvider } from "./ApiProvider"; export { default as ApiProvider } from "./ApiProvider";
export * from "./ApiProvider"; export * from "./ApiProvider";

View File

@@ -39,7 +39,7 @@ type Props = {
afterTitle?: ReactNode; afterTitle?: ReactNode;
subtitle?: string; subtitle?: string;
loading?: boolean; loading?: boolean;
error?: Error; error?: Error | null;
showContentOnError?: boolean; showContentOnError?: boolean;
children: ReactNode; children: ReactNode;
}; };

View File

@@ -121,5 +121,8 @@
}, },
"fileUpload": { "fileUpload": {
"label": "Datei hochladen" "label": "Datei hochladen"
},
"importLog": {
"title": "Importprotokoll"
} }
} }

View File

@@ -122,5 +122,8 @@
}, },
"fileUpload": { "fileUpload": {
"label": "Upload File" "label": "Upload File"
},
"importLog": {
"title": "Import Log"
} }
} }

View File

@@ -48,6 +48,7 @@ import Admin from "../admin/containers/Admin";
import Profile from "./Profile"; import Profile from "./Profile";
import NamespaceRoot from "../repos/namespaces/containers/NamespaceRoot"; import NamespaceRoot from "../repos/namespaces/containers/NamespaceRoot";
import ImportRepository from "../repos/containers/ImportRepository"; import ImportRepository from "../repos/containers/ImportRepository";
import ImportLog from "../repos/importlog/ImportLog";
type Props = { type Props = {
me: Me; me: Me;
@@ -96,6 +97,7 @@ class Main extends React.Component<Props> {
<ProtectedRoute exact path="/groups/:page" component={Groups} authenticated={authenticated} /> <ProtectedRoute exact path="/groups/:page" component={Groups} authenticated={authenticated} />
<ProtectedRoute path="/admin" component={Admin} authenticated={authenticated} /> <ProtectedRoute path="/admin" component={Admin} authenticated={authenticated} />
<ProtectedRoute path="/me" component={Profile} authenticated={authenticated} /> <ProtectedRoute path="/me" component={Profile} authenticated={authenticated} />
<ProtectedRoute path="/importlog/:logId" component={ImportLog} authenticated={authenticated} />
<ExtensionPoint <ExtensionPoint
name="main.route" name="main.route"
renderAll={true} renderAll={true}

View File

@@ -0,0 +1,45 @@
/*
* 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.
*/
import React, { FC } from "react";
import { useImportLog, useIndex } from "@scm-manager/ui-api";
import { useParams } from "react-router-dom";
import { Page } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
type Params = {
logId: string;
}
const ImportLog: FC = () => {
const {logId} = useParams<Params>();
const {isLoading, data, error} = useImportLog(logId);
const [t] = useTranslation("commons");
return <Page title={t("importLog.title")} loading={isLoading} error={error}>
<pre>{data ? data : null}</pre>
</Page>;
}
export default ImportLog;

View File

@@ -116,6 +116,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self())); builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self())); builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self())); builder.single(link("repositoryRoles", resourceLinks.repositoryRoleCollection().self()));
builder.single(link("importLog", resourceLinks.repository().importLog("IMPORT_LOG_ID").replace("IMPORT_LOG_ID", "{logId}")));
} else { } else {
builder.single(link("login", resourceLinks.authentication().jsonLogin())); builder.single(link("login", resourceLinks.authentication().jsonLogin()));
} }

View File

@@ -90,5 +90,6 @@ public class MapperModule extends AbstractModule {
bind(ApiKeyToApiKeyDtoMapper.class).to(Mappers.getMapperClass(ApiKeyToApiKeyDtoMapper.class)); bind(ApiKeyToApiKeyDtoMapper.class).to(Mappers.getMapperClass(ApiKeyToApiKeyDtoMapper.class));
bind(RepositoryExportInformationToDtoMapper.class).to(Mappers.getMapperClass(RepositoryExportInformationToDtoMapper.class)); bind(RepositoryExportInformationToDtoMapper.class).to(Mappers.getMapperClass(RepositoryExportInformationToDtoMapper.class));
bind(RepositoryImportDtoToRepositoryImportParametersMapper.class).to(Mappers.getMapperClass(RepositoryImportDtoToRepositoryImportParametersMapper.class));
} }
} }

View File

@@ -80,8 +80,8 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.checkSupport; import static sonia.scm.importexport.RepositoryTypeSupportChecker.checkSupport;
import static sonia.scm.api.v2.resources.RepositoryTypeSupportChecker.type; import static sonia.scm.importexport.RepositoryTypeSupportChecker.type;
public class RepositoryExportResource { public class RepositoryExportResource {

View File

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

View File

@@ -27,10 +27,8 @@ package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content; 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.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.shiro.SecurityUtils;
import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl; import org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
import sonia.scm.HandlerEventType; import sonia.scm.importexport.FromBundleImporter;
import sonia.scm.Type; import sonia.scm.importexport.FromUrlImporter;
import sonia.scm.event.ScmEventBus;
import sonia.scm.importexport.FullScmRepositoryImporter; import sonia.scm.importexport.FullScmRepositoryImporter;
import sonia.scm.importexport.RepositoryImportExportEncryption; import sonia.scm.importexport.RepositoryImportExportEncryption;
import sonia.scm.repository.InternalRepositoryException; import sonia.scm.importexport.RepositoryImportLoggerFactory;
import sonia.scm.repository.Repository; 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.Command;
import sonia.scm.repository.api.ImportFailedException; 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.VndMediaType;
import sonia.scm.web.api.DtoValidator; import sonia.scm.web.api.DtoValidator;
@@ -71,55 +59,55 @@ import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue; import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
import static java.nio.charset.StandardCharsets.UTF_8; 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 { public class RepositoryImportResource {
private static final Logger logger = LoggerFactory.getLogger(RepositoryImportResource.class); private static final Logger logger = LoggerFactory.getLogger(RepositoryImportResource.class);
private final RepositoryManager manager;
private final RepositoryDtoToRepositoryMapper mapper; private final RepositoryDtoToRepositoryMapper mapper;
private final RepositoryServiceFactory serviceFactory;
private final ResourceLinks resourceLinks; private final ResourceLinks resourceLinks;
private final ScmEventBus eventBus;
private final FullScmRepositoryImporter fullScmRepositoryImporter; private final FullScmRepositoryImporter fullScmRepositoryImporter;
private final RepositoryImportExportEncryption repositoryImportExportEncryption; private final RepositoryImportExportEncryption repositoryImportExportEncryption;
private final RepositoryImportDtoToRepositoryImportParametersMapper importParametersMapper;
private final FromUrlImporter fromUrlImporter;
private final FromBundleImporter fromBundleImporter;
private final RepositoryImportLoggerFactory importLoggerFactory;
@Inject @Inject
public RepositoryImportResource(RepositoryManager manager, public RepositoryImportResource(RepositoryDtoToRepositoryMapper mapper,
RepositoryDtoToRepositoryMapper mapper,
RepositoryServiceFactory serviceFactory,
ResourceLinks resourceLinks, ResourceLinks resourceLinks,
ScmEventBus eventBus,
FullScmRepositoryImporter fullScmRepositoryImporter, FullScmRepositoryImporter fullScmRepositoryImporter,
RepositoryImportExportEncryption repositoryImportExportEncryption) { RepositoryImportDtoToRepositoryImportParametersMapper importParametersMapper,
this.manager = manager; RepositoryImportExportEncryption repositoryImportExportEncryption, FromUrlImporter fromUrlImporter,
FromBundleImporter fromBundleImporter,
RepositoryImportLoggerFactory importLoggerFactory) {
this.mapper = mapper; this.mapper = mapper;
this.serviceFactory = serviceFactory;
this.resourceLinks = resourceLinks; this.resourceLinks = resourceLinks;
this.eventBus = eventBus;
this.fullScmRepositoryImporter = fullScmRepositoryImporter; this.fullScmRepositoryImporter = fullScmRepositoryImporter;
this.repositoryImportExportEncryption = repositoryImportExportEncryption; this.repositoryImportExportEncryption = repositoryImportExportEncryption;
this.importParametersMapper = importParametersMapper;
this.fromUrlImporter = fromUrlImporter;
this.fromBundleImporter = fromBundleImporter;
this.importLoggerFactory = importLoggerFactory;
} }
/** /**
@@ -166,49 +154,13 @@ public class RepositoryImportResource {
public Response importFromUrl(@Context UriInfo uriInfo, public Response importFromUrl(@Context UriInfo uriInfo,
@Pattern(regexp = "\\w{1,10}") @PathParam("type") String type, @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type,
@Valid RepositoryImportResource.RepositoryImportFromUrlDto request) { @Valid RepositoryImportResource.RepositoryImportFromUrlDto request) {
RepositoryPermissions.create().check(); if (!type.equals(request.getType())) {
Type t = type(manager, type);
if (!t.getName().equals(request.getType())) {
throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST); 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(); 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<Repository> 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);
}
};
} }
/** /**
@@ -253,7 +205,6 @@ public class RepositoryImportResource {
@Pattern(regexp = "\\w{1,10}") @PathParam("type") String type, @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type,
MultipartFormDataInput input, MultipartFormDataInput input,
@QueryParam("compressed") @DefaultValue("false") boolean compressed) { @QueryParam("compressed") @DefaultValue("false") boolean compressed) {
RepositoryPermissions.create().check();
Repository repository = doImportFromBundle(type, input, compressed); Repository repository = doImportFromBundle(type, input, compressed);
return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build(); return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build();
@@ -303,11 +254,18 @@ public class RepositoryImportResource {
public Response importFullRepository(@Context UriInfo uriInfo, public Response importFullRepository(@Context UriInfo uriInfo,
@Pattern(regexp = "\\w{1,10}") @PathParam("type") String type, @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type,
MultipartFormDataInput input) { MultipartFormDataInput input) {
RepositoryPermissions.create().check();
Repository createdRepository = importFullRepositoryFromInput(input); Repository createdRepository = importFullRepositoryFromInput(input);
return Response.created(URI.create(resourceLinks.repository().self(createdRepository.getNamespace(), createdRepository.getName()))).build(); return Response.created(URI.create(resourceLinks.repository().self(createdRepository.getNamespace(), createdRepository.getName()))).build();
} }
@GET
@Path("log/{logId}")
@Produces(MediaType.TEXT_PLAIN)
public StreamingOutput getImportLog(@PathParam("logId") String logId) throws IOException {
importLoggerFactory.checkCanReadLog(logId);
return out -> importLoggerFactory.getLog(logId, out);
}
private Repository importFullRepositoryFromInput(MultipartFormDataInput input) { private Repository importFullRepositoryFromInput(MultipartFormDataInput input) {
Map<String, List<InputPart>> formParts = input.getFormDataMap(); Map<String, List<InputPart>> formParts = input.getFormDataMap();
InputStream inputStream = extractInputStream(formParts); InputStream inputStream = extractInputStream(formParts);
@@ -332,25 +290,13 @@ public class RepositoryImportResource {
inputStream = decryptInputStream(inputStream, repositoryDto.getPassword()); inputStream = decryptInputStream(inputStream, repositoryDto.getPassword());
} }
Type t = type(manager, type); if (!type.equals(repositoryDto.getType())) {
checkSupport(t, Command.UNBUNDLE); throw new WebApplicationException("type of import url and repository does not match", Response.Status.BAD_REQUEST);
}
Repository repository = mapper.map(repositoryDto); Repository repository = mapper.map(repositoryDto);
repository.setPermissions(singletonList(
new RepositoryPermission(SecurityUtils.getSubject().getPrincipal().toString(), "OWNER", false)
));
try { repository = fromBundleImporter.importFromBundle(compressed, inputStream, repository);
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; return repository;
} }
@@ -363,27 +309,6 @@ public class RepositoryImportResource {
} }
} }
@VisibleForTesting
Consumer<Repository> 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<String, List<InputPart>> formParts) { private RepositoryImportFromFileDto extractRepositoryDto(Map<String, List<InputPart>> formParts) {
RepositoryImportFromFileDto repositoryDto = extractFromInputPart(formParts.get("repository"), RepositoryImportFromFileDto.class); RepositoryImportFromFileDto repositoryDto = extractFromInputPart(formParts.get("repository"), RepositoryImportFromFileDto.class);
checkNotNull(repositoryDto, "repository data is required"); checkNotNull(repositoryDto, "repository data is required");

View File

@@ -377,6 +377,10 @@ class ResourceLinks {
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFullRepository").parameters(type).href(); return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("importFullRepository").parameters(type).href();
} }
String importLog(String importLogId) {
return repositoryImportLinkBuilder.method("getRepositoryImportResource").parameters().method("getImportLog").parameters(importLogId).href();
}
String archive(String namespace, String name) { String archive(String namespace, String name) {
return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("archive").parameters().href(); return repositoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("archive").parameters().href();
} }

View File

@@ -60,6 +60,7 @@ class EnvironmentCheckStep implements ImportStep {
if (!validEnvironment) { if (!validEnvironment) {
throw new IncompatibleEnvironmentForImportException(); throw new IncompatibleEnvironmentForImportException();
} }
state.getLogger().step("checked environment");
state.environmentChecked(); state.environmentChecked();
return true; return true;
} }

View File

@@ -0,0 +1,138 @@
/*
* 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.Files;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Type;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.ImportRepositoryHookEvent;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHookEvent;
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.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.util.IOUtil;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static java.util.Collections.singletonList;
import static sonia.scm.importexport.RepositoryImportLogger.ImportType.DUMP;
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;
private final WorkdirProvider workdirProvider;
private final RepositoryImportLoggerFactory loggerFactory;
@Inject
public FromBundleImporter(RepositoryManager manager, RepositoryServiceFactory serviceFactory, ScmEventBus eventBus, WorkdirProvider workdirProvider, RepositoryImportLoggerFactory loggerFactory) {
this.manager = manager;
this.serviceFactory = serviceFactory;
this.eventBus = eventBus;
this.workdirProvider = workdirProvider;
this.loggerFactory = loggerFactory;
}
public Repository importFromBundle(boolean compressed, InputStream inputStream, Repository repository) {
RepositoryPermissions.create().check();
Type t = type(manager, repository.getType());
checkSupport(t, Command.UNBUNDLE);
repository.setPermissions(singletonList(
new RepositoryPermission(SecurityUtils.getSubject().getPrincipal().toString(), "OWNER", false)
));
RepositoryImportLogger logger = loggerFactory.createLogger();
try {
repository = manager.create(repository, unbundleImport(inputStream, compressed, logger));
} catch (Exception e) {
logger.failed(e);
eventBus.post(new RepositoryImportEvent(repository, true));
throw e;
}
eventBus.post(new RepositoryImportEvent(repository, false));
return repository;
}
private Consumer<Repository> unbundleImport(InputStream inputStream, boolean compressed, RepositoryImportLogger logger) {
return repository -> {
logger.start(DUMP, repository);
File workdir = workdirProvider.createNewWorkdir(repository.getId());
try (RepositoryService service = serviceFactory.create(repository)) {
logger.step("writing temporary dump file");
File file = File.createTempFile("scm-import-", ".bundle", workdir);
long length = Files.asByteSink(file).writeFrom(inputStream);
LOG.info("copied {} bytes to temp, start bundle import", length);
logger.step("importing repository data from dump file");
runUnbundleCommand(compressed, service, file);
logger.finished();
} catch (IOException e) {
logger.failed(e);
throw new InternalRepositoryException(repository, "Failed to import from bundle", e);
} finally {
try {
IOUtil.delete(workdir);
} catch (IOException ex) {
LOG.warn("could not delete temporary file", ex);
}
}
};
}
private void runUnbundleCommand(boolean compressed, RepositoryService service, File file) throws IOException {
AtomicReference<RepositoryHookEvent> eventSink = new AtomicReference<>();
service.getUnbundleCommand()
.setCompressed(compressed)
.setPostEventSink(eventSink::set)
.unbundle(file);
RepositoryHookEvent repositoryHookEvent = eventSink.get();
if (repositoryHookEvent != null) {
eventBus.post(new ImportRepositoryHookEvent(repositoryHookEvent));
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* 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.base.Strings;
import lombok.Getter;
import lombok.Setter;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException;
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.ImportFailedException;
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.ContextEntry.ContextBuilder.noContext;
import static sonia.scm.importexport.RepositoryImportLogger.ImportType.URL;
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;
private final RepositoryImportLoggerFactory loggerFactory;
@Inject
public FromUrlImporter(RepositoryManager manager, RepositoryServiceFactory serviceFactory, ScmEventBus eventBus, RepositoryImportLoggerFactory loggerFactory) {
this.manager = manager;
this.serviceFactory = serviceFactory;
this.eventBus = eventBus;
this.loggerFactory = loggerFactory;
}
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)));
RepositoryImportLogger logger = loggerFactory.createLogger();
Repository createdRepository;
try {
createdRepository = manager.create(
repository,
pullChangesFromRemoteUrl(parameters, logger)
);
} catch (AlreadyExistsException e) {
throw e;
} catch (Exception e) {
if (logger.started()) {
logger.failed(e);
}
eventBus.post(new RepositoryImportEvent(repository, true));
throw new ImportFailedException(noContext(), "Could not import repository from url " + parameters.getImportUrl(), e);
}
eventBus.post(new RepositoryImportEvent(createdRepository, false));
return createdRepository;
}
private Consumer<Repository> pullChangesFromRemoteUrl(RepositoryImportParameters parameters, RepositoryImportLogger logger) {
return repository -> {
logger.start(URL, repository);
try (RepositoryService service = serviceFactory.create(repository)) {
PullCommandBuilder pullCommand = service.getPullCommand();
if (!Strings.isNullOrEmpty(parameters.getUsername()) && !Strings.isNullOrEmpty(parameters.getPassword())) {
logger.step("setting username and password for pull");
pullCommand
.withUsername(parameters.getUsername())
.withPassword(parameters.getPassword());
}
logger.step("pulling repository from " + parameters.getImportUrl());
pullCommand.pull(parameters.getImportUrl());
logger.finished();
} catch (IOException e) {
throw new InternalRepositoryException(repository, "Failed to import from remote url: " + e.getMessage(), e);
}
};
}
@Getter
@Setter
public static class RepositoryImportParameters {
private String importUrl;
private String username;
private String password;
}
}

View File

@@ -33,7 +33,9 @@ import org.slf4j.LoggerFactory;
import sonia.scm.ContextEntry; import sonia.scm.ContextEntry;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryImportEvent;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.ImportFailedException;
import javax.inject.Inject; import javax.inject.Inject;
@@ -42,8 +44,9 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import static java.util.Arrays.stream; import static java.util.Arrays.stream;
import static sonia.scm.util.Archives.createTarInputStream;
import static sonia.scm.ContextEntry.ContextBuilder.noContext; import static sonia.scm.ContextEntry.ContextBuilder.noContext;
import static sonia.scm.importexport.RepositoryImportLogger.ImportType.FULL;
import static sonia.scm.util.Archives.createTarInputStream;
public class FullScmRepositoryImporter { public class FullScmRepositoryImporter {
@@ -53,6 +56,7 @@ public class FullScmRepositoryImporter {
private final RepositoryManager repositoryManager; private final RepositoryManager repositoryManager;
private final RepositoryImportExportEncryption repositoryImportExportEncryption; private final RepositoryImportExportEncryption repositoryImportExportEncryption;
private final ScmEventBus eventBus; private final ScmEventBus eventBus;
private final RepositoryImportLoggerFactory loggerFactory;
@Inject @Inject
public FullScmRepositoryImporter(EnvironmentCheckStep environmentCheckStep, public FullScmRepositoryImporter(EnvironmentCheckStep environmentCheckStep,
@@ -61,15 +65,17 @@ public class FullScmRepositoryImporter {
RepositoryImportStep repositoryImportStep, RepositoryImportStep repositoryImportStep,
RepositoryManager repositoryManager, RepositoryManager repositoryManager,
RepositoryImportExportEncryption repositoryImportExportEncryption, RepositoryImportExportEncryption repositoryImportExportEncryption,
ScmEventBus eventBus RepositoryImportLoggerFactory loggerFactory,
) { ScmEventBus eventBus) {
this.repositoryManager = repositoryManager; this.repositoryManager = repositoryManager;
this.loggerFactory = loggerFactory;
this.repositoryImportExportEncryption = repositoryImportExportEncryption; this.repositoryImportExportEncryption = repositoryImportExportEncryption;
importSteps = new ImportStep[]{environmentCheckStep, metadataImportStep, storeImportStep, repositoryImportStep};
this.eventBus = eventBus; this.eventBus = eventBus;
importSteps = new ImportStep[]{environmentCheckStep, metadataImportStep, storeImportStep, repositoryImportStep};
} }
public Repository importFromStream(Repository repository, InputStream inputStream, String password) { public Repository importFromStream(Repository repository, InputStream inputStream, String password) {
RepositoryPermissions.create().check();
try { try {
if (inputStream.available() > 0) { if (inputStream.available() > 0) {
try ( try (
@@ -103,8 +109,17 @@ public class FullScmRepositoryImporter {
} }
} }
private RepositoryImportLogger startLogger(Repository repository) {
RepositoryImportLogger logger = loggerFactory.createLogger();
logger.start(FULL, repository);
return logger;
}
private Repository run(Repository repository, TarArchiveInputStream tais) throws IOException { private Repository run(Repository repository, TarArchiveInputStream tais) throws IOException {
ImportState state = new ImportState(repositoryManager.create(repository)); Repository createdRepository = repositoryManager.create(repository);
RepositoryImportLogger logger = startLogger(repository);
ImportState state = new ImportState(createdRepository, logger);
logger.repositoryCreated(state.getRepository());
try { try {
TarArchiveEntry tarArchiveEntry; TarArchiveEntry tarArchiveEntry;
while ((tarArchiveEntry = tais.getNextTarEntry()) != null) { while ((tarArchiveEntry = tais.getNextTarEntry()) != null) {
@@ -112,21 +127,28 @@ public class FullScmRepositoryImporter {
handle(tais, state, tarArchiveEntry); handle(tais, state, tarArchiveEntry);
} }
stream(importSteps).forEach(step -> step.finish(state)); stream(importSteps).forEach(step -> step.finish(state));
state.getLogger().finished();
return state.getRepository(); return state.getRepository();
} catch (RuntimeException | IOException e) {
state.getLogger().failed(e);
throw e;
} finally { } finally {
stream(importSteps).forEach(step -> step.cleanup(state)); stream(importSteps).forEach(step -> step.cleanup(state));
if (state.success()) { if (state.success()) {
// send all pending events on successful import // send all pending events on successful import
state.getPendingEvents().forEach(eventBus::post); state.getPendingEvents().forEach(eventBus::post);
eventBus.post(new RepositoryImportEvent(repository, false));
} else { } else {
// Delete the repository if any error occurs during the import // Delete the repository if any error occurs during the import
repositoryManager.delete(state.getRepository()); repositoryManager.delete(state.getRepository());
eventBus.post(new RepositoryImportEvent(repository, true));
} }
} }
} }
private void handle(TarArchiveInputStream tais, ImportState state, TarArchiveEntry currentEntry) { private void handle(TarArchiveInputStream tais, ImportState state, TarArchiveEntry currentEntry) {
state.getLogger().step("inspecting file " + currentEntry.getName());
for (ImportStep step : importSteps) { for (ImportStep step : importSteps) {
if (step.handle(currentEntry, state, tais)) { if (step.handle(currentEntry, state, tais)) {
return; return;

View File

@@ -36,6 +36,8 @@ import java.util.Optional;
class ImportState { class ImportState {
private final RepositoryImportLogger logger;
private Repository repository; private Repository repository;
private boolean environmentChecked; private boolean environmentChecked;
@@ -48,11 +50,8 @@ class ImportState {
private final List<Object> pendingEvents = new ArrayList<>(); private final List<Object> pendingEvents = new ArrayList<>();
ImportState(Repository repository) { ImportState(Repository repository, RepositoryImportLogger logger) {
this.repository = repository; this.logger = logger;
}
public void setRepository(Repository repository) {
this.repository = repository; this.repository = repository;
} }
@@ -104,6 +103,10 @@ class ImportState {
this.pendingEvents.add(event); this.pendingEvents.add(event);
} }
RepositoryImportLogger getLogger() {
return logger;
}
public Collection<Object> getPendingEvents() { public Collection<Object> getPendingEvents() {
return Collections.unmodifiableCollection(pendingEvents); return Collections.unmodifiableCollection(pendingEvents);
} }

View File

@@ -58,6 +58,7 @@ class MetadataImportStep implements ImportStep {
RepositoryMetadataXmlGenerator.RepositoryMetadata metadata = JAXB.unmarshal(new NoneClosingInputStream(inputStream), RepositoryMetadataXmlGenerator.RepositoryMetadata.class); RepositoryMetadataXmlGenerator.RepositoryMetadata metadata = JAXB.unmarshal(new NoneClosingInputStream(inputStream), RepositoryMetadataXmlGenerator.RepositoryMetadata.class);
if (metadata != null && metadata.getPermissions() != null) { if (metadata != null && metadata.getPermissions() != null) {
state.setPermissions(new HashSet<>(metadata.getPermissions())); state.setPermissions(new HashSet<>(metadata.getPermissions()));
state.getLogger().step("reading repository metadata with permissions");
} else { } else {
state.setPermissions(Collections.emptySet()); state.setPermissions(Collections.emptySet());
} }
@@ -69,6 +70,7 @@ class MetadataImportStep implements ImportStep {
@Override @Override
public void finish(ImportState state) { public void finish(ImportState state) {
LOG.trace("Saving permissions for imported repository"); LOG.trace("Saving permissions for imported repository");
state.getLogger().step("setting permissions for repository from import");
importRepositoryPermissions(state.getRepository(), state.getRepositoryPermissions()); importRepositoryPermissions(state.getRepository(), state.getRepositoryPermissions());
} }

View File

@@ -0,0 +1,131 @@
/*
* 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.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.Repository;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.user.User;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.time.Instant;
import static java.nio.charset.StandardCharsets.UTF_8;
class RepositoryImportLogger {
private static final Logger LOG = LoggerFactory.getLogger(RepositoryImportLogger.class);
private final BlobStore logStore;
private PrintWriter print;
private Blob blob;
RepositoryImportLogger(BlobStore logStore) {
this.logStore = logStore;
}
void start(ImportType importType, Repository repository) {
User user = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
blob = logStore.create(repository.getId());
OutputStream outputStream = getBlobOutputStream();
writeUser(user, outputStream);
print = new PrintWriter(outputStream);
print.printf("Import of repository %s/%s%n", repository.getNamespace(), repository.getName());
print.printf("Repository type: %s%n", repository.getType());
print.printf("Imported from: %s%n", importType);
print.printf("Imported by %s (%s)%n", user.getId(), user.getName());
print.println();
addLogEntry("import started");
}
private void writeUser(User user, OutputStream outputStream) {
try {
outputStream.write(user.getId().getBytes(UTF_8));
outputStream.write(0);
} catch (IOException e) {
LOG.warn("Could not write user to import log blob", e);
}
}
private OutputStream getBlobOutputStream() {
try {
return blob.getOutputStream();
} catch (IOException e) {
LOG.warn("Could not create logger for import; failed to get output stream from blob", e);
return new OutputStream() {
@Override
public void write(int b) {
// this is a dummy
}
};
}
}
public void finished() {
step("import finished successfully");
writeLog();
}
public void failed(Exception e) {
step("import failed (see next log entry)");
print.println(e.getMessage());
writeLog();
}
public void repositoryCreated(Repository createdRepository) {
step("created repository: " + createdRepository.getNamespaceAndName());
}
public void step(String message) {
addLogEntry(message);
}
private void writeLog() {
print.flush();
try {
blob.commit();
} catch (IOException e) {
LOG.warn("Could not commit blob with import log", e);
}
}
private void addLogEntry(String message) {
print.printf("%s - %s%n", Instant.now(), message);
}
public boolean started() {
return blob != null;
}
enum ImportType {
FULL, URL, DUMP
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.SecurityUtils;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import sonia.scm.NotFoundException;
import sonia.scm.store.BlobStore;
import sonia.scm.store.BlobStoreFactory;
import sonia.scm.util.IOUtil;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
public class RepositoryImportLoggerFactory {
private final BlobStoreFactory blobStoreFactory;
@Inject
RepositoryImportLoggerFactory(BlobStoreFactory blobStoreFactory) {
this.blobStoreFactory = blobStoreFactory;
}
RepositoryImportLogger createLogger() {
return new RepositoryImportLogger(blobStoreFactory.withName("imports").build());
}
public void checkCanReadLog(String logId) throws IOException {
try (InputStream blob = getBlob(logId)) {
// nothing to read
}
}
public void getLog(String logId, OutputStream out) throws IOException {
try (InputStream log = getBlob(logId)) {
IOUtil.copy(log, out);
}
}
private InputStream getBlob(String logId) throws IOException {
BlobStore importStore = blobStoreFactory.withName("imports").build();
InputStream log = importStore
.getOptional(logId).orElseThrow(() -> new NotFoundException("Log", logId))
.getInputStream();
checkPermission(log);
return log;
}
private void checkPermission(InputStream log) throws IOException {
Subject subject = SecurityUtils.getSubject();
String logUser = readUserFrom(log);
if (!subject.isPermitted("only:admin:allowed") && !subject.getPrincipal().toString().equals(logUser)) {
throw new AuthorizationException("not permitted");
}
}
private String readUserFrom(InputStream log) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int b;
while ((b = log.read()) > 0) {
buffer.write(b);
}
return new String(buffer.toByteArray(), UTF_8);
}
}

View File

@@ -62,12 +62,14 @@ class RepositoryImportStep implements ImportStep {
@Override @Override
public boolean handle(TarArchiveEntry currentEntry, ImportState state, InputStream inputStream) { public boolean handle(TarArchiveEntry currentEntry, ImportState state, InputStream inputStream) {
if (!currentEntry.isDirectory()) { if (!currentEntry.isDirectory() && !currentEntry.getName().contains("/")) {
if (state.isStoreImported()) { if (state.isStoreImported()) {
LOG.trace("Importing directly from tar stream (entry '{}')", currentEntry.getName()); LOG.trace("Importing directly from tar stream (entry '{}')", currentEntry.getName());
state.getLogger().step("directly importing repository data");
unbundleRepository(state, inputStream); unbundleRepository(state, inputStream);
} else { } else {
LOG.debug("Temporally storing tar entry '{}' in work dir", currentEntry.getName()); LOG.debug("Temporarily storing tar entry '{}' in work dir", currentEntry.getName());
state.getLogger().step("temporarily storing repository data for later import");
Path path = saveRepositoryDataFromTarArchiveEntry(state.getRepository(), inputStream); Path path = saveRepositoryDataFromTarArchiveEntry(state.getRepository(), inputStream);
state.setTemporaryRepositoryBundle(path); state.setTemporaryRepositoryBundle(path);
} }
@@ -90,6 +92,7 @@ class RepositoryImportStep implements ImportStep {
private void importFromTemporaryPath(ImportState state, Path path) { private void importFromTemporaryPath(ImportState state, Path path) {
LOG.debug("Importing repository from temporary location in work dir"); LOG.debug("Importing repository from temporary location in work dir");
state.getLogger().step("importing repository from temporary location");
try { try {
unbundleRepository(state, Files.newInputStream(path)); unbundleRepository(state, Files.newInputStream(path));
} catch (IOException e) { } catch (IOException e) {

View File

@@ -22,10 +22,11 @@
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.api.v2.resources; package sonia.scm.importexport;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.BadRequestException;
import sonia.scm.Type; import sonia.scm.Type;
import sonia.scm.repository.RepositoryHandler; import sonia.scm.repository.RepositoryHandler;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
@@ -36,7 +37,9 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Set; import java.util.Set;
class RepositoryTypeSupportChecker { import static sonia.scm.ContextEntry.ContextBuilder.noContext;
public class RepositoryTypeSupportChecker {
private RepositoryTypeSupportChecker() { private RepositoryTypeSupportChecker() {
} }
@@ -49,7 +52,7 @@ class RepositoryTypeSupportChecker {
* @param type repository type * @param type repository type
* @param cmd command * @param cmd command
*/ */
static void checkSupport(Type type, Command cmd) { public static void checkSupport(Type type, Command cmd) {
if (!(type instanceof RepositoryType)) { if (!(type instanceof RepositoryType)) {
logger.warn("type {} is not a repository type", type.getName()); logger.warn("type {} is not a repository type", type.getName());
throw new WebApplicationException(Response.Status.BAD_REQUEST); throw new WebApplicationException(Response.Status.BAD_REQUEST);
@@ -60,17 +63,28 @@ class RepositoryTypeSupportChecker {
logger.warn("type {} does not support this command {}", logger.warn("type {} does not support this command {}",
type.getName(), type.getName(),
cmd.name()); cmd.name());
throw new WebApplicationException(Response.Status.BAD_REQUEST); throw new IllegalTypeForImportException("type does not support command");
} }
} }
@SuppressWarnings("javasecurity:S5145") // the type parameter is validated in the resource to only contain valid characters (\w) @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); RepositoryHandler handler = manager.getHandler(type);
if (handler == null) { if (handler == null) {
logger.warn("no handler for type {} found", type); logger.warn("no handler for type {} found", type);
throw new WebApplicationException(Response.Status.NOT_FOUND); throw new IllegalTypeForImportException("unsupported repository type: " + type);
} }
return handler.getType(); return handler.getType();
} }
private static class IllegalTypeForImportException extends BadRequestException {
public IllegalTypeForImportException(String message) {
super(noContext(), message);
}
@Override
public String getCode() {
return "CISPvega31";
}
}
} }

View File

@@ -52,17 +52,18 @@ class StoreImportStep implements ImportStep {
public boolean handle(TarArchiveEntry entry, ImportState state, InputStream inputStream) { public boolean handle(TarArchiveEntry entry, ImportState state, InputStream inputStream) {
if (entry.getName().equals(STORE_DATA_FILE_NAME) && !entry.isDirectory()) { if (entry.getName().equals(STORE_DATA_FILE_NAME) && !entry.isDirectory()) {
LOG.trace("Importing store from tar"); LOG.trace("Importing store from tar");
state.getLogger().step("importing stores");
// Inside the repository tar archive stream is another tar archive. // Inside the repository tar archive stream is another tar archive.
// The nested tar archive is wrapped in another TarArchiveInputStream inside the storeImporter // The nested tar archive is wrapped in another TarArchiveInputStream inside the storeImporter
importStores(state.getRepository(), inputStream); importStores(state.getRepository(), inputStream, state.getLogger());
state.storeImported(); state.storeImported();
return true; return true;
} }
return false; return false;
} }
private void importStores(Repository repository, InputStream inputStream) { private void importStores(Repository repository, InputStream inputStream, RepositoryImportLogger logger) {
storeImporter.importFromTarArchive(repository, inputStream); storeImporter.importFromTarArchive(repository, inputStream, logger);
updateEngine.update(repository.getId()); updateEngine.update(repository.getId());
} }
} }

View File

@@ -47,13 +47,13 @@ public class TarArchiveRepositoryStoreImporter {
this.repositoryStoreImporter = repositoryStoreImporter; this.repositoryStoreImporter = repositoryStoreImporter;
} }
public void importFromTarArchive(Repository repository, InputStream inputStream) { public void importFromTarArchive(Repository repository, InputStream inputStream, RepositoryImportLogger logger) {
try (TarArchiveInputStream tais = new NoneClosingTarArchiveInputStream(inputStream)) { try (TarArchiveInputStream tais = new NoneClosingTarArchiveInputStream(inputStream)) {
ArchiveEntry entry = tais.getNextEntry(); ArchiveEntry entry = tais.getNextEntry();
while (entry != null) { while (entry != null) {
String[] entryPathParts = entry.getName().split(File.separator); String[] entryPathParts = entry.getName().split(File.separator);
validateStorePath(repository, entryPathParts); validateStorePath(repository, entryPathParts);
importStoreByType(repository, tais, entryPathParts); importStoreByType(repository, tais, entryPathParts, logger);
entry = tais.getNextEntry(); entry = tais.getNextEntry();
} }
} catch (IOException e) { } catch (IOException e) {
@@ -61,22 +61,26 @@ public class TarArchiveRepositoryStoreImporter {
} }
} }
private void importStoreByType(Repository repository, TarArchiveInputStream tais, String[] entryPathParts) { private void importStoreByType(Repository repository, TarArchiveInputStream tais, String[] entryPathParts, RepositoryImportLogger logger) {
String storeType = entryPathParts[1]; String storeType = entryPathParts[1];
String storeName = entryPathParts[2];
if (isDataStore(storeType)) { if (isDataStore(storeType)) {
logger.step("importing data store entry for store " + storeName);
repositoryStoreImporter repositoryStoreImporter
.doImport(repository) .doImport(repository)
.importStore(new StoreEntryMetaData(StoreType.DATA, entryPathParts[2])) .importStore(new StoreEntryMetaData(StoreType.DATA, entryPathParts[2]))
.importEntry(entryPathParts[3], tais); .importEntry(entryPathParts[3], tais);
} else if (isConfigStore(storeType)){ } else if (isConfigStore(storeType)){
logger.step("importing data store entry for store " + storeName);
repositoryStoreImporter repositoryStoreImporter
.doImport(repository) .doImport(repository)
.importStore(new StoreEntryMetaData(StoreType.CONFIG, "")) .importStore(new StoreEntryMetaData(StoreType.CONFIG, ""))
.importEntry(entryPathParts[2], tais); .importEntry(storeName, tais);
} else if(isBlobStore(storeType)) { } else if(isBlobStore(storeType)) {
logger.step("importing blob store entry for store " + storeName);
repositoryStoreImporter repositoryStoreImporter
.doImport(repository) .doImport(repository)
.importStore(new StoreEntryMetaData(StoreType.BLOB, entryPathParts[2])) .importStore(new StoreEntryMetaData(StoreType.BLOB, storeName))
.importEntry(entryPathParts[3], tais); .importEntry(entryPathParts[3], tais);
} }
} }

View File

@@ -338,6 +338,10 @@
"5GSO9ZkzX1": { "5GSO9ZkzX1": {
"displayName": "Inkompatible Umgebung", "displayName": "Inkompatible Umgebung",
"description": "Die Version dieses SCM-Managers oder eines der installierten Plugins ist zu alt für den Import des Dumps. Bitte installieren Sie die neuesten Versionen. Nähere Informationen finden sich im Log." "description": "Die Version dieses SCM-Managers oder eines der installierten Plugins ist zu alt für den Import des Dumps. Bitte installieren Sie die neuesten Versionen. Nähere Informationen finden sich im Log."
},
"CISPvega31": {
"displayName": "Ungültiger Repository-Typ für Import",
"description": "Der Import ist für den gegebenen Repository-Typen nicht möglich."
} }
}, },
"namespaceStrategies": { "namespaceStrategies": {

View File

@@ -338,6 +338,10 @@
"8YR7aawFW1": { "8YR7aawFW1": {
"displayName": "Wrong current password", "displayName": "Wrong current password",
"description": "The current password is wrong. Please try again." "description": "The current password is wrong. Please try again."
},
"CISPvega31": {
"displayName": "Illegal repository type for import",
"description": "The import is not possible for the given repository type."
} }
}, },
"namespaceStrategies": { "namespaceStrategies": {

View File

@@ -134,5 +134,6 @@ class IndexDtoGeneratorTest {
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(scmPathInfo)); when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(scmPathInfo));
when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(scmPathInfo)); when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(scmPathInfo));
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo))); when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo)));
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(scmPathInfo));
} }
} }

View File

@@ -0,0 +1,84 @@
/*
* 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 com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.resteasy.mock.MockHttpRequest;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.Map;
import java.util.UUID;
class MultiPartRequestBuilder {
/**
* This method is a slightly adapted copy of Lin Zaho's gist at https://gist.github.com/lin-zhao/9985191
*/
static void multipartRequest(MockHttpRequest request, Map<String, InputStream> files, RepositoryDto repository) throws IOException {
String boundary = UUID.randomUUID().toString();
request.contentType("multipart/form-data; boundary=" + boundary);
//Make sure this is deleted in afterTest()
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (OutputStreamWriter formWriter = new OutputStreamWriter(buffer)) {
formWriter.append("--").append(boundary);
for (Map.Entry<String, InputStream> entry : files.entrySet()) {
formWriter.append("\n");
formWriter.append(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"",
entry.getKey(), entry.getKey())).append("\n");
formWriter.append("Content-Type: application/octet-stream").append("\n\n");
InputStream stream = entry.getValue();
int b = stream.read();
while (b >= 0) {
formWriter.write(b);
b = stream.read();
}
stream.close();
formWriter.append("\n").append("--").append(boundary);
}
if (repository != null) {
formWriter.append("\n");
formWriter.append("Content-Disposition: form-data; name=\"repository\"").append("\n\n");
StringWriter repositoryWriter = new StringWriter();
new JsonFactory().createGenerator(repositoryWriter).setCodec(new ObjectMapper()).writeObject(repository);
formWriter.append(repositoryWriter.getBuffer().toString()).append("\n");
formWriter.append("--").append(boundary);
}
formWriter.append("--");
formWriter.flush();
}
request.setInputStream(new ByteArrayInputStream(buffer.toByteArray()));
}
}

View File

@@ -0,0 +1,305 @@
/*
* 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 com.google.common.io.Resources;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.api.v2.resources.RepositoryImportResource.RepositoryImportFromFileDto;
import sonia.scm.importexport.FromBundleImporter;
import sonia.scm.importexport.FromUrlImporter;
import sonia.scm.importexport.FullScmRepositoryImporter;
import sonia.scm.importexport.RepositoryImportExportEncryption;
import sonia.scm.importexport.RepositoryImportLoggerFactory;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonMap;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SuppressWarnings("UnstableApiUsage")
@RunWith(MockitoJUnitRunner.class)
public class RepositoryImportResourceTest extends RepositoryTestBase {
private final RestDispatcher dispatcher = new RestDispatcher();
@Mock
private FullScmRepositoryImporter fullScmRepositoryImporter;
@Mock
private FromUrlImporter fromUrlImporter;
@Mock
private FromBundleImporter fromBundleImporter;
@Mock
private RepositoryImportLoggerFactory importLoggerFactory;
@Mock
private RepositoryImportExportEncryption repositoryImportExportEncryption;
@Captor
private ArgumentCaptor<FromUrlImporter.RepositoryImportParameters> parametersCaptor;
@Captor
private ArgumentCaptor<Repository> repositoryCaptor;
private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@InjectMocks
private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper;
private final MockHttpResponse response = new MockHttpResponse();
@Before
public void prepareEnvironment() {
super.repositoryImportResource = new RepositoryImportResource(dtoToRepositoryMapper, resourceLinks, fullScmRepositoryImporter, new RepositoryImportDtoToRepositoryImportParametersMapperImpl(), repositoryImportExportEncryption, fromUrlImporter, fromBundleImporter, importLoggerFactory);
dispatcher.addSingletonResource(getRepositoryRootResource());
}
@Test
public void shouldImportRepositoryFromUrl() throws Exception {
when(fromUrlImporter.importFromUrl(parametersCaptor.capture(), repositoryCaptor.capture()))
.thenReturn(RepositoryTestData.createHeartOfGold());
URL url = Resources.getResource("sonia/scm/api/v2/import-repo.json");
byte[] importRequest = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/git/url")
.contentType(VndMediaType.REPOSITORY)
.content(importRequest);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(SC_CREATED);
assertThat(response.getOutputHeaders().get("Location")).asString().contains("/v2/repositories/hitchhiker/HeartOfGold");
assertThat(parametersCaptor.getValue().getImportUrl()).isEqualTo("https://scm-manager-org/scm/repo/secret/puzzle42");
assertThat(parametersCaptor.getValue().getUsername()).isNull();
assertThat(parametersCaptor.getValue().getPassword()).isNull();
assertThat(repositoryCaptor.getValue().getName()).isEqualTo("HeartOfGold");
assertThat(repositoryCaptor.getValue().getNamespace()).isEqualTo("hitchhiker");
}
@Test
public void shouldImportRepositoryFromUrlWithCredentials() throws Exception {
when(fromUrlImporter.importFromUrl(parametersCaptor.capture(), repositoryCaptor.capture()))
.thenReturn(RepositoryTestData.createHeartOfGold());
URL url = Resources.getResource("sonia/scm/api/v2/import-repo-with-credentials.json");
byte[] importRequest = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/git/url")
.contentType(VndMediaType.REPOSITORY)
.content(importRequest);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(SC_CREATED);
assertThat(parametersCaptor.getValue().getUsername()).isEqualTo("trillian");
assertThat(parametersCaptor.getValue().getPassword()).isEqualTo("secret");
}
@Test
public void shouldFailOnImportFromUrlWithDifferentTypes() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/import-repo.json");
byte[] importRequest = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/svn/url")
.contentType(VndMediaType.REPOSITORY)
.content(importRequest);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isNotEqualTo(SC_CREATED);
verify(fromUrlImporter, never()).importFromUrl(any(), any());
}
@Nested
class WithCorrectBundle {
@BeforeEach
void mockImporter() {
when(
fromBundleImporter.importFromBundle(
eq(false),
argThat(argument -> streamHasContent(argument, "svn-dump")),
argThat(repository -> repository.getName().equals("HeartOfGold"))
)
).thenReturn(RepositoryTestData.createHeartOfGold());
}
@Test
public void shouldImportRepositoryFromBundle() throws Exception {
RepositoryImportFromFileDto importDto = createBasicImportDto();
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/svn/bundle");
MultiPartRequestBuilder.multipartRequest(request, singletonMap("bundle", new ByteArrayInputStream("svn-dump".getBytes(UTF_8))), importDto);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(SC_CREATED);
assertThat(response.getOutputHeaders().get("Location")).asString().contains("/v2/repositories/hitchhiker/HeartOfGold");
verify(repositoryImportExportEncryption, never()).decrypt(any(), any());
}
@Test
public void shouldImportRepositoryFromEncryptedBundle() throws Exception {
when(repositoryImportExportEncryption.decrypt(any(), eq("hgt2g")))
.thenAnswer(invocation -> invocation.getArgument(0));
RepositoryImportFromFileDto importDto = createBasicImportDto();
importDto.setPassword("hgt2g");
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/svn/bundle");
MultiPartRequestBuilder.multipartRequest(request, singletonMap("bundle", new ByteArrayInputStream("svn-dump".getBytes(UTF_8))), importDto);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(SC_CREATED);
assertThat(response.getOutputHeaders().get("Location")).asString().contains("/v2/repositories/hitchhiker/HeartOfGold");
}
private RepositoryImportFromFileDto createBasicImportDto() {
RepositoryImportFromFileDto importDto = new RepositoryImportFromFileDto();
importDto.setName("HeartOfGold");
importDto.setNamespace("hitchhiker");
importDto.setType("svn");
return importDto;
}
}
@Test
public void shouldFailOnImportFromBundleWithDifferentTypes() throws Exception {
RepositoryDto repositoryDto = new RepositoryDto();
repositoryDto.setName("HeartOfGold");
repositoryDto.setNamespace("hitchhiker");
repositoryDto.setType("svn");
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/git/bundle");
MultiPartRequestBuilder.multipartRequest(request, Collections.singletonMap("bundle", new ByteArrayInputStream("svn-dump".getBytes(StandardCharsets.UTF_8))), repositoryDto);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isNotEqualTo(SC_CREATED);
verify(fromBundleImporter, never()).importFromBundle(any(Boolean.class), any(InputStream.class), any(Repository.class));
}
@Test
public void shouldImportFullRepository() throws Exception {
when(
fullScmRepositoryImporter.importFromStream(
argThat(repository -> repository.getName().equals("HeartOfGold")),
argThat(argument -> streamHasContent(argument, "svn-dump")),
isNull()
)
).thenReturn(RepositoryTestData.createHeartOfGold());
RepositoryDto repositoryDto = new RepositoryDto();
repositoryDto.setName("HeartOfGold");
repositoryDto.setNamespace("hitchhiker");
repositoryDto.setType("svn");
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/svn/full");
MultiPartRequestBuilder.multipartRequest(request, singletonMap("bundle", new ByteArrayInputStream("svn-dump".getBytes(UTF_8))), repositoryDto);
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(SC_CREATED);
assertThat(response.getOutputHeaders().get("Location")).asString().contains("/v2/repositories/hitchhiker/HeartOfGold");
}
@Test
public void shouldFindImportLog() throws Exception {
doAnswer(
invocation -> {
invocation.getArgument(1, OutputStream.class).write("some log".getBytes(UTF_8));
return null;
}
).when(importLoggerFactory).getLog(eq("42"), any(OutputStream.class));
MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/log/42");
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getContentAsString()).isEqualTo("some log");
verify(importLoggerFactory).checkCanReadLog("42");
}
private boolean streamHasContent(InputStream argument, String expectedContent) {
try {
byte[] data = new byte[expectedContent.length()];
argument.read(data);
return new String(data).equals(expectedContent);
} catch (IOException e) {
return false;
}
}
}

View File

@@ -24,8 +24,6 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@@ -37,38 +35,36 @@ import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor; import org.mockito.Captor;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.PageResult; import sonia.scm.PageResult;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus;
import sonia.scm.importexport.ExportFileExtensionResolver; import sonia.scm.importexport.ExportFileExtensionResolver;
import sonia.scm.importexport.ExportService; import sonia.scm.importexport.ExportService;
import sonia.scm.importexport.ExportStatus; import sonia.scm.importexport.ExportStatus;
import sonia.scm.importexport.FromBundleImporter;
import sonia.scm.importexport.FromUrlImporter;
import sonia.scm.importexport.FullScmRepositoryExporter; import sonia.scm.importexport.FullScmRepositoryExporter;
import sonia.scm.importexport.FullScmRepositoryImporter; import sonia.scm.importexport.FullScmRepositoryImporter;
import sonia.scm.importexport.RepositoryImportExportEncryption; import sonia.scm.importexport.RepositoryImportExportEncryption;
import sonia.scm.importexport.RepositoryImportLoggerFactory;
import sonia.scm.repository.CustomNamespaceStrategy; import sonia.scm.repository.CustomNamespaceStrategy;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.NamespaceStrategy; import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHandler; import sonia.scm.repository.RepositoryHandler;
import sonia.scm.repository.RepositoryImportEvent;
import sonia.scm.repository.RepositoryInitializer; import sonia.scm.repository.RepositoryInitializer;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.RepositoryType; import sonia.scm.repository.RepositoryType;
import sonia.scm.repository.api.BundleCommandBuilder; import sonia.scm.repository.api.BundleCommandBuilder;
import sonia.scm.repository.api.Command; 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.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory; 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.user.User;
import sonia.scm.web.RestDispatcher; import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
@@ -76,23 +72,14 @@ import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.time.Instant; import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
@@ -100,27 +87,22 @@ import static java.util.stream.Stream.of;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.RETURNS_SELF;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;
@SubjectAware( @SubjectAware(
username = "trillian", username = "trillian",
@@ -128,6 +110,7 @@ import static org.mockito.MockitoAnnotations.openMocks;
configuration = "classpath:sonia/scm/repository/shiro.ini" configuration = "classpath:sonia/scm/repository/shiro.ini"
) )
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
@RunWith(MockitoJUnitRunner.class)
public class RepositoryRootResourceTest extends RepositoryTestBase { public class RepositoryRootResourceTest extends RepositoryTestBase {
private static final String REALM = "AdminRealm"; private static final String REALM = "AdminRealm";
@@ -156,8 +139,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
@Mock @Mock
private Set<NamespaceStrategy> strategies; private Set<NamespaceStrategy> strategies;
@Mock @Mock
private ScmEventBus eventBus;
@Mock
private FullScmRepositoryExporter fullScmRepositoryExporter; private FullScmRepositoryExporter fullScmRepositoryExporter;
@Mock @Mock
private RepositoryExportInformationToDtoMapper exportInformationToDtoMapper; private RepositoryExportInformationToDtoMapper exportInformationToDtoMapper;
@@ -166,8 +147,14 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
@Mock @Mock
private RepositoryImportExportEncryption repositoryImportExportEncryption; private RepositoryImportExportEncryption repositoryImportExportEncryption;
@Mock @Mock
private FromUrlImporter fromUrlImporter;
@Mock
private FromBundleImporter fromBundleImporter;
@Mock
private ExportFileExtensionResolver fileExtensionResolver; private ExportFileExtensionResolver fileExtensionResolver;
@Mock @Mock
private RepositoryImportLoggerFactory importLoggerFactory;
@Mock
private ExportService exportService; private ExportService exportService;
@Captor @Captor
@@ -175,27 +162,25 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
private final URI baseUri = URI.create("/"); private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
private Repository repositoryMarkedAsExported;
@InjectMocks @InjectMocks
private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper;
@InjectMocks @InjectMocks
private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper; private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper;
private final MockHttpResponse response = new MockHttpResponse();
@Before @Before
public void prepareEnvironment() throws IOException { public void prepareEnvironment() throws IOException {
openMocks(this);
super.repositoryToDtoMapper = repositoryToDtoMapper; super.repositoryToDtoMapper = repositoryToDtoMapper;
super.dtoToRepositoryMapper = dtoToRepositoryMapper; super.dtoToRepositoryMapper = dtoToRepositoryMapper;
super.manager = repositoryManager; super.manager = repositoryManager;
RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks);
super.repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer); 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, importLoggerFactory);
super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter, repositoryImportExportEncryption, exportService, exportInformationToDtoMapper, fileExtensionResolver, resourceLinks); super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter, repositoryImportExportEncryption, exportService, exportInformationToDtoMapper, fileExtensionResolver, resourceLinks);
dispatcher.addSingletonResource(getRepositoryRootResource()); dispatcher.addSingletonResource(getRepositoryRootResource());
when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(scmPathInfoStore.get()).thenReturn(uriInfo);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y"));
doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator(); doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator();
SimplePrincipalCollection trillian = new SimplePrincipalCollection("trillian", REALM); SimplePrincipalCollection trillian = new SimplePrincipalCollection("trillian", REALM);
trillian.add(new User("trillian"), REALM); trillian.add(new User("trillian"), REALM);
@@ -213,7 +198,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
createRepository("space", "repo"); createRepository("space", "repo");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -226,7 +210,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -241,7 +224,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -256,7 +238,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?q=Rep"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?q=Rep");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -273,7 +254,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -290,7 +270,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space?q=Rep"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space?q=Rep");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -310,7 +289,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo")
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repository); .content(repository);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -328,7 +306,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo")
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repository); .content(repository);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -347,7 +324,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo")
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repository); .content(repository);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -367,7 +343,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "wrong/repo") .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "wrong/repo")
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repository); .content(repository);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -380,7 +355,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
createRepository("space", "repo"); createRepository("space", "repo");
MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -403,7 +377,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2) .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2)
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repositoryJson); .content(repositoryJson);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -424,7 +397,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?initialize=true") .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?initialize=true")
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repositoryJson); .content(repositoryJson);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -454,7 +426,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2) .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2)
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repositoryJson); .content(repositoryJson);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -473,7 +444,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy");
MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -495,7 +465,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/rename") .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/rename")
.contentType(VndMediaType.REPOSITORY) .contentType(VndMediaType.REPOSITORY)
.content(repository); .content(repository);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -503,202 +472,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
verify(repositoryManager).rename(repository1, "space", "x"); verify(repositoryManager).rename(repository1, "space", "x");
} }
@Test
public void shouldImportRepositoryFromUrl() throws URISyntaxException, IOException {
ArgumentCaptor<RepositoryImportEvent> captor = ArgumentCaptor.forClass(RepositoryImportEvent.class);
when(manager.getHandler("git")).thenReturn(repositoryHandler);
when(repositoryHandler.getType()).thenReturn(new RepositoryType("git", "git", ImmutableSet.of(Command.PULL)));
when(manager.create(any(Repository.class), any())).thenReturn(RepositoryTestData.create42Puzzle());
URL url = Resources.getResource("sonia/scm/api/v2/import-repo.json");
byte[] importRequest = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/git/url")
.contentType(VndMediaType.REPOSITORY)
.content(importRequest);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(SC_CREATED, response.getStatus());
verify(eventBus).post(captor.capture());
assertThat(captor.getValue().isFailed()).isFalse();
}
@Test
public void shouldFailOnImportRepositoryFromUrl() throws URISyntaxException, IOException {
ArgumentCaptor<RepositoryImportEvent> captor = ArgumentCaptor.forClass(RepositoryImportEvent.class);
when(manager.getHandler("git")).thenReturn(repositoryHandler);
when(repositoryHandler.getType()).thenReturn(new RepositoryType("git", "git", ImmutableSet.of(Command.PULL)));
doThrow(ImportFailedException.class).when(manager).create(any(Repository.class), any());
URL url = Resources.getResource("sonia/scm/api/v2/import-repo.json");
byte[] importRequest = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "import/git/url")
.contentType(VndMediaType.REPOSITORY)
.content(importRequest);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(500, response.getStatus());
verify(eventBus).post(captor.capture());
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<Repository> 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<Repository> 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<Repository> 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<RepositoryImportEvent> 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);
when(repositoryHandler.getType()).thenReturn(new RepositoryType("svn", "svn", ImmutableSet.of(Command.UNBUNDLE)));
doThrow(ImportFailedException.class).when(repositoryManager).create(any(), any());
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_INTERNAL_SERVER_ERROR, response.getStatus());
ArgumentCaptor<RepositoryImportEvent> event = ArgumentCaptor.forClass(RepositoryImportEvent.class);
verify(eventBus).post(event.capture());
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<Repository> 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<Repository> repositoryConsumer = repositoryImportResource.unbundleImport(in, false);
repositoryConsumer.accept(RepositoryTestData.createHeartOfGold("svn"));
verify(ubc, never()).setCompressed(true);
verify(ubc).unbundle(any(File.class));
}
@Test @Test
public void shouldMarkRepositoryAsArchived() throws Exception { public void shouldMarkRepositoryAsArchived() throws Exception {
String namespace = "space"; String namespace = "space";
@@ -709,7 +482,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/archive") .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/archive")
.content(new byte[]{}); .content(new byte[]{});
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -728,7 +500,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/unarchive") .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/unarchive")
.content(new byte[]{}); .content(new byte[]{});
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -746,11 +517,9 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class); BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder); when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
when(bundleCommandBuilder.getFileExtension()).thenReturn(".bundle");
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn"); .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -769,11 +538,9 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class); BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder); when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
when(bundleCommandBuilder.getFileExtension()).thenReturn(".bundle");
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn?compressed=true"); .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn?compressed=true");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -790,12 +557,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full"); .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response); dispatcher.invoke(request, response);
@@ -812,9 +575,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full") .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full")
.contentType(VndMediaType.REPOSITORY_EXPORT) .contentType(VndMediaType.REPOSITORY_EXPORT)
@@ -837,9 +597,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full") .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full")
.contentType(VndMediaType.REPOSITORY_EXPORT) .contentType(VndMediaType.REPOSITORY_EXPORT)
@@ -860,9 +617,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
when(exportService.isExporting(repository)).thenReturn(true); when(exportService.isExporting(repository)).thenReturn(true);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
@@ -877,19 +631,13 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
} }
@Test @Test
public void shouldDeleteRepositoryExport() throws URISyntaxException, IOException { public void shouldDeleteRepositoryExport() throws URISyntaxException {
String namespace = "space"; String namespace = "space";
String name = "repo"; String name = "repo";
Repository repository = createRepository(namespace, name, "svn"); Repository repository = createRepository(namespace, name, "svn");
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); 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 MockHttpRequest request = MockHttpRequest
.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export"); .delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export");
MockHttpResponse response = new MockHttpResponse(); MockHttpResponse response = new MockHttpResponse();
@@ -908,9 +656,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
when(exportService.isExporting(repository)).thenReturn(false); when(exportService.isExporting(repository)).thenReturn(false);
doThrow(NotFoundException.class).when(exportService).checkExportIsAvailable(repository); doThrow(NotFoundException.class).when(exportService).checkExportIsAvailable(repository);
@@ -932,9 +677,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
when(exportService.isExporting(repository)).thenReturn(true); when(exportService.isExporting(repository)).thenReturn(true);
MockHttpRequest request = MockHttpRequest MockHttpRequest request = MockHttpRequest
@@ -954,9 +696,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
when(exportService.isExporting(repository)).thenReturn(false); when(exportService.isExporting(repository)).thenReturn(false);
when(exportService.getData(repository)).thenReturn(new ByteArrayInputStream("content".getBytes())); when(exportService.getData(repository)).thenReturn(new ByteArrayInputStream("content".getBytes()));
when(exportService.getFileExtension(repository)).thenReturn("tar.gz.enc"); when(exportService.getFileExtension(repository)).thenReturn("tar.gz.enc");
@@ -979,9 +718,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository);
mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE));
BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class);
when(service.getBundleCommand()).thenReturn(bundleCommandBuilder);
RepositoryExportInformationDto dto = new RepositoryExportInformationDto(); RepositoryExportInformationDto dto = new RepositoryExportInformationDto();
dto.setExporterName("trillian"); dto.setExporterName("trillian");
dto.setCreated(Instant.ofEpochMilli(100)); dto.setCreated(Instant.ofEpochMilli(100));
@@ -1011,7 +747,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(repositoryType.getSupportedCommands()).thenReturn(cmds); when(repositoryType.getSupportedCommands()).thenReturn(cmds);
} }
private PageResult<Repository> createSingletonPageResult(Repository repository) { private PageResult<Repository> createSingletonPageResult(Repository repository) {
return new PageResult<>(singletonList(repository), 0); return new PageResult<>(singletonList(repository), 0);
} }
@@ -1039,48 +774,4 @@ public class RepositoryRootResourceTest extends RepositoryTestBase {
when(repositoryManager.get(id)).thenReturn(repository); when(repositoryManager.get(id)).thenReturn(repository);
return repository; return repository;
} }
/**
* This method is a slightly adapted copy of Lin Zaho's gist at https://gist.github.com/lin-zhao/9985191
*/
private void multipartRequest(MockHttpRequest request, Map<String, InputStream> files, RepositoryDto repository) throws IOException {
String boundary = UUID.randomUUID().toString();
request.contentType("multipart/form-data; boundary=" + boundary);
//Make sure this is deleted in afterTest()
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (OutputStreamWriter formWriter = new OutputStreamWriter(buffer)) {
formWriter.append("--").append(boundary);
for (Map.Entry<String, InputStream> entry : files.entrySet()) {
formWriter.append("\n");
formWriter.append(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"",
entry.getKey(), entry.getKey())).append("\n");
formWriter.append("Content-Type: application/octet-stream").append("\n\n");
InputStream stream = entry.getValue();
int b = stream.read();
while (b >= 0) {
formWriter.write(b);
b = stream.read();
}
stream.close();
formWriter.append("\n").append("--").append(boundary);
}
if (repository != null) {
formWriter.append("\n");
formWriter.append("Content-Disposition: form-data; name=\"repository\"").append("\n\n");
StringWriter repositoryWriter = new StringWriter();
new JsonFactory().createGenerator(repositoryWriter).setCodec(new ObjectMapper()).writeObject(repository);
formWriter.append(repositoryWriter.getBuffer().toString()).append("\n");
formWriter.append("--").append(boundary);
}
formWriter.append("--");
formWriter.flush();
}
request.setInputStream(new ByteArrayInputStream(buffer.toByteArray()));
}
} }

View File

@@ -0,0 +1,186 @@
/*
* 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.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHandler;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.RepositoryType;
import sonia.scm.repository.api.Command;
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.repository.work.WorkdirProvider;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Path;
import java.util.function.Consumer;
import static java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
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 {
public static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold("svn");
@Mock
private RepositoryManager manager;
@Mock
private RepositoryHandler repositoryHandler;
@Mock
private RepositoryServiceFactory serviceFactory;
@Mock
private ScmEventBus eventBus;
@Mock
private WorkdirProvider workdirProvider;
@Mock
private RepositoryImportLoggerFactory loggerFactory;
@Mock
private RepositoryImportLogger logger;
@Mock(answer = Answers.RETURNS_SELF)
private UnbundleCommandBuilder unbundleCommandBuilder;
@Mock
private Subject subject;
@InjectMocks
private FromBundleImporter importer;
@BeforeEach
void mockSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void cleanupSubject() {
ThreadContext.unbindSubject();
}
@Nested
class WithPermission {
@BeforeEach
void initMocks(@TempDir Path temp) throws IOException {
when(subject.getPrincipal()).thenReturn("dent");
when(workdirProvider.createNewWorkdir(REPOSITORY.getId())).thenReturn(temp.toFile());
when(manager.create(eq(REPOSITORY), any())).thenAnswer(
invocation -> {
invocation.getArgument(1, Consumer.class).accept(REPOSITORY);
return REPOSITORY;
}
);
when(manager.getHandler("svn")).thenReturn(repositoryHandler);
RepositoryType repositoryType = mock(RepositoryType.class);
when(repositoryHandler.getType()).thenReturn(repositoryType);
when(repositoryType.getSupportedCommands()).thenReturn(singleton(Command.UNBUNDLE));
when(loggerFactory.createLogger()).thenReturn(logger);
when(unbundleCommandBuilder.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(unbundleCommandBuilder);
}
@Test
void shouldImportCompressedBundle() throws IOException {
URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump.gz");
InputStream in = new ByteArrayInputStream(Resources.toByteArray(dumpUrl));
importer.importFromBundle(true, in, REPOSITORY);
verify(unbundleCommandBuilder).setCompressed(true);
verify(unbundleCommandBuilder).unbundle(any(File.class));
}
@Test
void shouldImportNonCompressedBundle() throws IOException {
URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump");
InputStream in = new ByteArrayInputStream(Resources.toByteArray(dumpUrl));
importer.importFromBundle(false, in, REPOSITORY);
verify(unbundleCommandBuilder, never()).setCompressed(true);
verify(unbundleCommandBuilder).unbundle(any(File.class));
}
@Test
void shouldSetPermissionForCurrentUser() throws IOException {
URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump");
InputStream in = new ByteArrayInputStream(Resources.toByteArray(dumpUrl));
Repository createdRepository = importer.importFromBundle(false, in, REPOSITORY);
assertThat(createdRepository.getPermissions())
.hasSize(1);
RepositoryPermission permission = createdRepository.getPermissions().iterator().next();
assertThat(permission.getName()).isEqualTo("dent");
assertThat(permission.isGroupPermission()).isFalse();
assertThat(permission.getRole()).isEqualTo("OWNER");
}
}
@Test
void shouldFailWithoutPermission() throws IOException {
URL dumpUrl = Resources.getResource("sonia/scm/api/v2/svn.dump");
InputStream in = new ByteArrayInputStream(Resources.toByteArray(dumpUrl));
doThrow(new AuthorizationException()).when(subject).checkPermission("repository:create");
assertThrows(AuthorizationException.class, () -> importer.importFromBundle(false, in, REPOSITORY));
verify(manager, never()).create(any(), any());
}
}

View File

@@ -0,0 +1,186 @@
/*
* 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.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.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.RepositoryHandler;
import sonia.scm.repository.RepositoryImportEvent;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryTestData;
import sonia.scm.repository.RepositoryType;
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 java.util.Collections.singleton;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
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;
import static sonia.scm.repository.api.Command.PULL;
@ExtendWith(MockitoExtension.class)
class FromUrlImporterTest {
@Mock
private RepositoryManager manager;
@Mock
private RepositoryServiceFactory serviceFactory;
@Mock
private RepositoryService service;
@Mock
private ScmEventBus eventBus;
@Mock
private RepositoryImportLoggerFactory loggerFactory;
@Mock
private RepositoryImportLogger logger;
@Mock
private Subject subject;
@InjectMocks
private FromUrlImporter importer;
private final Repository repository = RepositoryTestData.createHeartOfGold("git");
@BeforeEach
void setUpMocks() {
when(serviceFactory.create(any(Repository.class))).thenReturn(service);
when(loggerFactory.createLogger()).thenReturn(logger);
when(manager.create(any(), any())).thenAnswer(
invocation -> {
Repository repository = invocation.getArgument(0, Repository.class);
Repository createdRepository = repository.clone();
createdRepository.setNamespace("created");
invocation.getArgument(1, Consumer.class).accept(createdRepository);
return createdRepository;
}
);
}
@BeforeEach
void setUpRepositoryType() {
RepositoryHandler repositoryHandler = mock(RepositoryHandler.class);
when(manager.getHandler(repository.getType())).thenReturn(repositoryHandler);
RepositoryType repositoryType = mock(RepositoryType.class);
when(repositoryHandler.getType()).thenReturn(repositoryType);
when(repositoryType.getSupportedCommands()).thenReturn(singleton(PULL));
}
@BeforeEach
void mockSubject() {
ThreadContext.bind(subject);
when(subject.getPrincipal()).thenReturn("trillian");
}
@AfterEach
void cleanupSubject() {
ThreadContext.unbindSubject();
}
@Test
void shouldPullChangesFromRemoteUrl() throws IOException {
PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF);
when(service.getPullCommand()).thenReturn(pullCommandBuilder);
FromUrlImporter.RepositoryImportParameters parameters = new FromUrlImporter.RepositoryImportParameters();
parameters.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
Repository createdRepository = importer.importFromUrl(parameters, repository);
assertThat(createdRepository.getNamespace()).isEqualTo("created");
verify(pullCommandBuilder).pull("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
verify(logger).finished();
verify(eventBus).post(argThat(
event -> {
assertThat(event).isInstanceOf(RepositoryImportEvent.class);
RepositoryImportEvent repositoryImportEvent = (RepositoryImportEvent) event;
assertThat(repositoryImportEvent.getItem().getNamespace()).isEqualTo("created");
assertThat(repositoryImportEvent.isFailed()).isFalse();
return true;
}
));
}
@Test
void shouldPullChangesFromRemoteUrlWithCredentials() {
PullCommandBuilder pullCommandBuilder = mock(PullCommandBuilder.class, RETURNS_SELF);
when(service.getPullCommand()).thenReturn(pullCommandBuilder);
FromUrlImporter.RepositoryImportParameters parameters = new FromUrlImporter.RepositoryImportParameters();
parameters.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
parameters.setUsername("trillian");
parameters.setPassword("secret");
importer.importFromUrl(parameters, 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(TestException.class).when(pullCommandBuilder).pull(anyString());
when(logger.started()).thenReturn(true);
FromUrlImporter.RepositoryImportParameters parameters = new FromUrlImporter.RepositoryImportParameters();
parameters.setImportUrl("https://scm-manager.org/scm/repo/scmadmin/scm-manager.git");
assertThrows(ImportFailedException.class, () -> importer.importFromUrl(parameters, repository));
verify(logger).failed(argThat(e -> e instanceof TestException));
verify(eventBus).post(argThat(
event -> {
assertThat(event).isInstanceOf(RepositoryImportEvent.class);
RepositoryImportEvent repositoryImportEvent = (RepositoryImportEvent) event;
assertThat(repositoryImportEvent.getItem()).isEqualTo(repository);
assertThat(repositoryImportEvent.isFailed()).isTrue();
return true;
}
));
}
private static class TestException extends RuntimeException {}
}

View File

@@ -25,6 +25,9 @@
package sonia.scm.importexport; package sonia.scm.importexport;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -40,6 +43,7 @@ import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.ImportRepositoryHookEvent; import sonia.scm.repository.ImportRepositoryHookEvent;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryHookEvent; import sonia.scm.repository.RepositoryHookEvent;
import sonia.scm.repository.RepositoryImportEvent;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermission; import sonia.scm.repository.RepositoryPermission;
import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.RepositoryTestData;
@@ -64,6 +68,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@@ -94,6 +100,14 @@ class FullScmRepositoryImporterTest {
private RepositoryImportExportEncryption repositoryImportExportEncryption; private RepositoryImportExportEncryption repositoryImportExportEncryption;
@Mock @Mock
private WorkdirProvider workdirProvider; private WorkdirProvider workdirProvider;
@Mock
private RepositoryImportLogger logger;
@Mock
private RepositoryImportLoggerFactory loggerFactory;
@Mock
private Subject subject;
@Mock
private ScmEventBus eventBus;
@InjectMocks @InjectMocks
private EnvironmentCheckStep environmentCheckStep; private EnvironmentCheckStep environmentCheckStep;
@@ -104,9 +118,6 @@ class FullScmRepositoryImporterTest {
@InjectMocks @InjectMocks
private RepositoryImportStep repositoryImportStep; private RepositoryImportStep repositoryImportStep;
@Mock
private ScmEventBus eventBus;
@Mock @Mock
private RepositoryHookEvent event; private RepositoryHookEvent event;
@@ -124,14 +135,25 @@ class FullScmRepositoryImporterTest {
repositoryImportStep, repositoryImportStep,
repositoryManager, repositoryManager,
repositoryImportExportEncryption, repositoryImportExportEncryption,
eventBus loggerFactory,
); eventBus);
} }
@BeforeEach @BeforeEach
void initRepositoryService() { void initRepositoryService() {
lenient().when(serviceFactory.create(REPOSITORY)).thenReturn(service); lenient().when(serviceFactory.create(REPOSITORY)).thenReturn(service);
lenient().when(service.getUnbundleCommand()).thenReturn(unbundleCommandBuilder); lenient().when(service.getUnbundleCommand()).thenReturn(unbundleCommandBuilder);
lenient().when(loggerFactory.createLogger()).thenReturn(logger);
}
@BeforeEach
void initSubject() {
ThreadContext.bind(subject);
}
@BeforeEach
void cleanupSubject() {
ThreadContext.unbindSubject();
} }
@Test @Test
@@ -154,6 +176,28 @@ class FullScmRepositoryImporterTest {
IncompatibleEnvironmentForImportException.class, IncompatibleEnvironmentForImportException.class,
() -> fullImporter.importFromStream(REPOSITORY, importStream, "") () -> fullImporter.importFromStream(REPOSITORY, importStream, "")
); );
verify(eventBus).post(argThat(
event -> {
assertThat(event).isInstanceOf(RepositoryImportEvent.class);
RepositoryImportEvent repositoryImportEvent = (RepositoryImportEvent) event;
assertThat(repositoryImportEvent.getItem()).isEqualTo(REPOSITORY);
assertThat(repositoryImportEvent.isFailed()).isTrue();
return true;
}
));
}
@Test
void shouldNotImportRepositoryWithoutPermission() throws IOException {
doThrow(AuthorizationException.class).when(subject).checkPermission("repository:create");
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
assertThrows(AuthorizationException.class, () -> fullImporter.importFromStream(REPOSITORY, stream, null));
verify(storeImporter, never()).importFromTarArchive(any(Repository.class), any(InputStream.class), any(RepositoryImportLogger.class));
verify(repositoryManager, never()).modify(any());
} }
@Nested @Nested
@@ -174,7 +218,7 @@ class FullScmRepositoryImporterTest {
Repository repository = fullImporter.importFromStream(REPOSITORY, stream, ""); Repository repository = fullImporter.importFromStream(REPOSITORY, stream, "");
assertThat(repository).isEqualTo(REPOSITORY); assertThat(repository).isEqualTo(REPOSITORY);
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class)); verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class), eq(logger));
verify(repositoryManager).modify(REPOSITORY); verify(repositoryManager).modify(REPOSITORY);
Collection<RepositoryPermission> updatedPermissions = REPOSITORY.getPermissions(); Collection<RepositoryPermission> updatedPermissions = REPOSITORY.getPermissions();
assertThat(updatedPermissions).hasSize(2); assertThat(updatedPermissions).hasSize(2);
@@ -192,6 +236,33 @@ class FullScmRepositoryImporterTest {
assertThat(workDirExists).isFalse(); assertThat(workDirExists).isFalse();
} }
@Test
void shouldSendImportedEventForImportedRepository() throws IOException {
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
when(unbundleCommandBuilder.setPostEventSink(any())).thenAnswer(
invocation -> {
invocation.getArgument(0, Consumer.class).accept(new RepositoryHookEvent(null, REPOSITORY, null));
return null;
}
);
ArgumentCaptor<Object> capturedEvents = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(capturedEvents.capture());
fullImporter.importFromStream(REPOSITORY, stream, null);
assertThat(capturedEvents.getAllValues()).hasSize(2);
assertThat(capturedEvents.getAllValues()).anyMatch(
event ->
event instanceof ImportRepositoryHookEvent &&
((ImportRepositoryHookEvent) event).getRepository().equals(REPOSITORY)
);
assertThat(capturedEvents.getAllValues()).anyMatch(
event ->
event instanceof RepositoryImportEvent &&
((RepositoryImportEvent) event).getItem().equals(REPOSITORY)
);
}
@Test @Test
void shouldTriggerUpdateForImportedRepository() throws IOException { void shouldTriggerUpdateForImportedRepository() throws IOException {
InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream(); InputStream stream = Resources.getResource("sonia/scm/repository/import/scm-import.tar.gz").openStream();
@@ -207,7 +278,8 @@ class FullScmRepositoryImporterTest {
Repository repository = fullImporter.importFromStream(REPOSITORY, stream, ""); Repository repository = fullImporter.importFromStream(REPOSITORY, stream, "");
assertThat(repository).isEqualTo(REPOSITORY); assertThat(repository).isEqualTo(REPOSITORY);
verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class)); verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class), eq(logger));
verify(repositoryManager).create(REPOSITORY);
verify(repositoryManager).modify(REPOSITORY); verify(repositoryManager).modify(REPOSITORY);
verify(unbundleCommandBuilder).unbundle((InputStream) argThat(argument -> argument.getClass().equals(NoneClosingInputStream.class))); verify(unbundleCommandBuilder).unbundle((InputStream) argThat(argument -> argument.getClass().equals(NoneClosingInputStream.class)));
verify(workdirProvider, never()).createNewWorkdir(REPOSITORY.getId()); verify(workdirProvider, never()).createNewWorkdir(REPOSITORY.getId());

View File

@@ -0,0 +1,136 @@
/*
* 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.apache.shiro.authz.AuthorizationException;
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 sonia.scm.NotFoundException;
import sonia.scm.store.Blob;
import sonia.scm.store.InMemoryBlobStore;
import sonia.scm.store.InMemoryBlobStoreFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class RepositoryImportLoggerFactoryTest {
private final Subject subject = mock(Subject.class);
private final InMemoryBlobStore store = new InMemoryBlobStore();
private final RepositoryImportLoggerFactory factory = new RepositoryImportLoggerFactory(new InMemoryBlobStoreFactory(store));
@BeforeEach
void initSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void cleanupSubject() {
ThreadContext.unbindSubject();
}
@Test
void shouldReadLogForExportingUser() throws IOException {
when(subject.getPrincipal()).thenReturn("dent");
createLog();
ByteArrayOutputStream out = new ByteArrayOutputStream();
factory.getLog("42", out);
assertLogReadCorrectly(out);
}
@Test
void shouldReadLogForAdmin() throws IOException {
when(subject.getPrincipal()).thenReturn("trillian");
when(subject.isPermitted(anyString())).thenReturn(true);
createLog();
ByteArrayOutputStream out = new ByteArrayOutputStream();
factory.getLog("42", out);
assertLogReadCorrectly(out);
}
private void assertLogReadCorrectly(ByteArrayOutputStream out) {
assertThat(out).asString().contains(
"Import of repository hitchhiker/HeartOfGold",
"Repository type: git",
"Imported from: URL",
"Imported by dent (Arthur Dent)",
"",
"Thu Feb 25 11:11:07 CET 2021 - import started",
"Thu Feb 25 11:11:07 CET 2021 - pulling repository from https://github.com/scm-manager/scm-manager",
"Thu Feb 25 11:11:08 CET 2021 - import finished successfully"
);
}
@Test
void shouldThrowNotFoundExceptionForMissingLog() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertThrows(NotFoundException.class, () -> factory.getLog("42", out));
}
@Test
void shouldFailWithoutPermission() throws IOException {
when(subject.getPrincipal()).thenReturn("trillian");
createLog();
doThrow(AuthorizationException.class).when(subject).checkPermission("only:admin:allowed");
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertThrows(AuthorizationException.class, () -> factory.getLog("42", out));
}
@SuppressWarnings("UnstableApiUsage")
private void createLog() throws IOException {
Blob blob = store.create("42");
try (OutputStream outputStream = blob.getOutputStream()) {
Resources.copy(
Resources.getResource("sonia/scm/importexport/importLog.blob"),
outputStream);
}
blob.commit();
}
}

View File

@@ -53,6 +53,8 @@ class TarArchiveRepositoryStoreImporterTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private RepositoryStoreImporter repositoryStoreImporter; private RepositoryStoreImporter repositoryStoreImporter;
@Mock
private RepositoryImportLogger logger;
@InjectMocks @InjectMocks
private TarArchiveRepositoryStoreImporter tarArchiveRepositoryStoreImporter; private TarArchiveRepositoryStoreImporter tarArchiveRepositoryStoreImporter;
@@ -60,20 +62,20 @@ class TarArchiveRepositoryStoreImporterTest {
@Test @Test
void shouldDoNothingIfNoEntries() { void shouldDoNothingIfNoEntries() {
ByteArrayInputStream bais = new ByteArrayInputStream("".getBytes()); ByteArrayInputStream bais = new ByteArrayInputStream("".getBytes());
tarArchiveRepositoryStoreImporter.importFromTarArchive(repository, bais); tarArchiveRepositoryStoreImporter.importFromTarArchive(repository, bais, logger);
verify(repositoryStoreImporter, never()).doImport(any(Repository.class)); verify(repositoryStoreImporter, never()).doImport(any(Repository.class));
} }
@Test @Test
void shouldImportEachEntry() throws IOException { void shouldImportEachEntry() throws IOException {
InputStream inputStream = Resources.getResource("sonia/scm/repository/import/scm-metadata.tar").openStream(); InputStream inputStream = Resources.getResource("sonia/scm/repository/import/scm-metadata.tar").openStream();
tarArchiveRepositoryStoreImporter.importFromTarArchive(repository, inputStream); tarArchiveRepositoryStoreImporter.importFromTarArchive(repository, inputStream, logger);
verify(repositoryStoreImporter, times(2)).doImport(repository); verify(repositoryStoreImporter, times(2)).doImport(repository);
} }
@Test @Test
void shouldThrowImportFailedExceptionIfInvalidStorePath() throws IOException { void shouldThrowImportFailedExceptionIfInvalidStorePath() throws IOException {
InputStream inputStream = Resources.getResource("sonia/scm/repository/import/scm-metadata_invalid.tar").openStream(); InputStream inputStream = Resources.getResource("sonia/scm/repository/import/scm-metadata_invalid.tar").openStream();
assertThrows(ImportFailedException.class, () -> tarArchiveRepositoryStoreImporter.importFromTarArchive(repository, inputStream)); assertThrows(ImportFailedException.class, () -> tarArchiveRepositoryStoreImporter.importFromTarArchive(repository, inputStream, logger));
} }
} }

View File

@@ -1,6 +1,7 @@
{ {
"namespace": "hitchhiker", "namespace": "hitchhiker",
"name": "HeartOfGold", "name": "HeartOfGold",
"type": "git",
"importUrl": "https://scm-manager-org/scm/repo/secret/puzzle42", "importUrl": "https://scm-manager-org/scm/repo/secret/puzzle42",
"username": "trillian", "username": "trillian",
"password": "secret" "password": "secret"