diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java index 7166cb5255..bc394a5532 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryImportResource.java @@ -49,7 +49,6 @@ import sonia.scm.importexport.FullScmRepositoryImporter; import sonia.scm.importexport.RepositoryImportExportEncryption; import sonia.scm.importexport.RepositoryImportLoggerFactory; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.ImportFailedException; import sonia.scm.web.VndMediaType; @@ -206,7 +205,6 @@ public class RepositoryImportResource { @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type, MultipartFormDataInput input, @QueryParam("compressed") @DefaultValue("false") boolean compressed) { - RepositoryPermissions.create().check(); Repository repository = doImportFromBundle(type, input, compressed); return Response.created(URI.create(resourceLinks.repository().self(repository.getNamespace(), repository.getName()))).build(); @@ -256,7 +254,6 @@ public class RepositoryImportResource { public Response importFullRepository(@Context UriInfo uriInfo, @Pattern(regexp = "\\w{1,10}") @PathParam("type") String type, MultipartFormDataInput input) { - RepositoryPermissions.create().check(); Repository createdRepository = importFullRepositoryFromInput(input); return Response.created(URI.create(resourceLinks.repository().self(createdRepository.getNamespace(), createdRepository.getName()))).build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/importexport/FullScmRepositoryImporter.java b/scm-webapp/src/main/java/sonia/scm/importexport/FullScmRepositoryImporter.java index f3a3f5f02c..9286cb7d3e 100644 --- a/scm-webapp/src/main/java/sonia/scm/importexport/FullScmRepositoryImporter.java +++ b/scm-webapp/src/main/java/sonia/scm/importexport/FullScmRepositoryImporter.java @@ -34,6 +34,7 @@ import sonia.scm.ContextEntry; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.ImportFailedException; import javax.inject.Inject; @@ -74,6 +75,7 @@ public class FullScmRepositoryImporter { } public Repository importFromStream(Repository repository, InputStream inputStream, String password) { + RepositoryPermissions.create().check(); RepositoryImportLogger logger = startLogger(repository); try { if (inputStream.available() > 0) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MultiPartRequestBuilder.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MultiPartRequestBuilder.java new file mode 100644 index 0000000000..07cc2b896a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MultiPartRequestBuilder.java @@ -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 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 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())); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryImportResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryImportResourceTest.java new file mode 100644 index 0000000000..56b5bb3144 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryImportResourceTest.java @@ -0,0 +1,304 @@ +/* + * 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 parametersCaptor; + @Captor + private ArgumentCaptor 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"); + } + + 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; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index bf39e35aae..391deada94 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -24,8 +24,6 @@ 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.SubjectAware; import com.google.common.collect.ImmutableSet; @@ -37,14 +35,15 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +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.NotFoundException; import sonia.scm.PageResult; import sonia.scm.config.ScmConfiguration; -import sonia.scm.event.ScmEventBus; import sonia.scm.importexport.ExportFileExtensionResolver; import sonia.scm.importexport.ExportService; import sonia.scm.importexport.ExportStatus; @@ -59,14 +58,11 @@ import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceStrategy; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryHandler; -import sonia.scm.repository.RepositoryImportEvent; import sonia.scm.repository.RepositoryInitializer; import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.RepositoryType; import sonia.scm.repository.api.BundleCommandBuilder; import sonia.scm.repository.api.Command; -import sonia.scm.repository.api.ImportFailedException; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.user.User; @@ -76,21 +72,14 @@ import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MediaType; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.time.Instant; -import java.util.Collections; -import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.function.Predicate; import static java.util.Collections.singletonList; @@ -98,7 +87,6 @@ import static java.util.stream.Stream.of; import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; -import static javax.servlet.http.HttpServletResponse.SC_CREATED; 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_OK; @@ -115,7 +103,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.openMocks; @SubjectAware( username = "trillian", @@ -123,6 +110,7 @@ import static org.mockito.MockitoAnnotations.openMocks; configuration = "classpath:sonia/scm/repository/shiro.ini" ) @SuppressWarnings("UnstableApiUsage") +@RunWith(MockitoJUnitRunner.class) public class RepositoryRootResourceTest extends RepositoryTestBase { private static final String REALM = "AdminRealm"; @@ -151,8 +139,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Mock private Set strategies; @Mock - private ScmEventBus eventBus; - @Mock private FullScmRepositoryExporter fullScmRepositoryExporter; @Mock private RepositoryExportInformationToDtoMapper exportInformationToDtoMapper; @@ -182,9 +168,10 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @InjectMocks private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper; + private final MockHttpResponse response = new MockHttpResponse(); + @Before public void prepareEnvironment() throws IOException { - openMocks(this); super.repositoryToDtoMapper = repositoryToDtoMapper; super.dtoToRepositoryMapper = dtoToRepositoryMapper; super.manager = repositoryManager; @@ -194,8 +181,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { super.repositoryExportResource = new RepositoryExportResource(repositoryManager, serviceFactory, fullScmRepositoryExporter, repositoryImportExportEncryption, exportService, exportInformationToDtoMapper, fileExtensionResolver, resourceLinks); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(any(Repository.class))).thenReturn(service); - when(scmPathInfoStore.get()).thenReturn(uriInfo); - when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); doReturn(ImmutableSet.of(new CustomNamespaceStrategy()).iterator()).when(strategies).iterator(); SimplePrincipalCollection trillian = new SimplePrincipalCollection("trillian", REALM); trillian.add(new User("trillian"), REALM); @@ -213,7 +198,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { createRepository("space", "repo"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -226,7 +210,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -241,7 +224,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -256,7 +238,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?q=Rep"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -273,7 +254,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -290,7 +270,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space?q=Rep"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -310,7 +289,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") .contentType(VndMediaType.REPOSITORY) .content(repository); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -328,7 +306,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") .contentType(VndMediaType.REPOSITORY) .content(repository); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -347,7 +324,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") .contentType(VndMediaType.REPOSITORY) .content(repository); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -367,7 +343,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "wrong/repo") .contentType(VndMediaType.REPOSITORY) .content(repository); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -380,7 +355,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { createRepository("space", "repo"); MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -403,7 +377,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2) .contentType(VndMediaType.REPOSITORY) .content(repositoryJson); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -424,7 +397,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?initialize=true") .contentType(VndMediaType.REPOSITORY) .content(repositoryJson); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -454,7 +426,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2) .contentType(VndMediaType.REPOSITORY) .content(repositoryJson); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -473,7 +444,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(configuration.getNamespaceStrategy()).thenReturn("CustomNamespaceStrategy"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -495,7 +465,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/rename") .contentType(VndMediaType.REPOSITORY) .content(repository); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -503,82 +472,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { verify(repositoryManager).rename(repository1, "space", "x"); } - @Test - public void shouldImportRepositoryFromUrl() throws URISyntaxException, IOException { - ArgumentCaptor 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 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 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 event = ArgumentCaptor.forClass(RepositoryImportEvent.class); - verify(eventBus).post(event.capture()); - assertTrue(event.getValue().isFailed()); - } - @Test public void shouldMarkRepositoryAsArchived() throws Exception { String namespace = "space"; @@ -589,7 +482,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { MockHttpRequest request = MockHttpRequest .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/archive") .content(new byte[]{}); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -608,7 +500,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { MockHttpRequest request = MockHttpRequest .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/unarchive") .content(new byte[]{}); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -630,7 +521,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { MockHttpRequest request = MockHttpRequest .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -653,7 +543,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { MockHttpRequest request = MockHttpRequest .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/svn?compressed=true"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -670,12 +559,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(manager.get(new NamespaceAndName(namespace, name))).thenReturn(repository); mockRepositoryHandler(ImmutableSet.of(Command.BUNDLE)); - BundleCommandBuilder bundleCommandBuilder = mock(BundleCommandBuilder.class); - when(service.getBundleCommand()).thenReturn(bundleCommandBuilder); - MockHttpRequest request = MockHttpRequest .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/export/full"); - MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -891,7 +776,6 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(repositoryType.getSupportedCommands()).thenReturn(cmds); } - private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } @@ -919,48 +803,4 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { when(repositoryManager.get(id)).thenReturn(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 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 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())); - } } diff --git a/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryImporterTest.java b/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryImporterTest.java index 75f2277f6b..fc9f5e758f 100644 --- a/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryImporterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryImporterTest.java @@ -25,6 +25,9 @@ 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.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -64,6 +67,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -98,6 +102,8 @@ class FullScmRepositoryImporterTest { private RepositoryImportLogger logger; @Mock private RepositoryImportLoggerFactory loggerFactory; + @Mock + private Subject subject; @InjectMocks private EnvironmentCheckStep environmentCheckStep; @@ -140,6 +146,16 @@ class FullScmRepositoryImporterTest { lenient().when(loggerFactory.createLogger()).thenReturn(logger); } + @BeforeEach + void initSubject() { + ThreadContext.bind(subject); + } + + @BeforeEach + void cleanupSubject() { + ThreadContext.unbindSubject(); + } + @Test void shouldNotImportRepositoryIfFileNotExists(@TempDir Path temp) throws IOException { Path emptyFile = temp.resolve("empty"); @@ -162,6 +178,18 @@ class FullScmRepositoryImporterTest { ); } + @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 class WithValidEnvironment { @@ -214,6 +242,7 @@ class FullScmRepositoryImporterTest { assertThat(repository).isEqualTo(REPOSITORY); verify(storeImporter).importFromTarArchive(eq(REPOSITORY), any(InputStream.class), eq(logger)); + verify(repositoryManager).create(REPOSITORY); verify(repositoryManager).modify(REPOSITORY); verify(unbundleCommandBuilder).unbundle((InputStream) argThat(argument -> argument.getClass().equals(NoneClosingInputStream.class))); verify(workdirProvider, never()).createNewWorkdir(REPOSITORY.getId()); diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo-with-credentials.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo-with-credentials.json index 7b33dbdd06..e0dea12e07 100644 --- a/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo-with-credentials.json +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/import-repo-with-credentials.json @@ -1,6 +1,7 @@ { "namespace": "hitchhiker", "name": "HeartOfGold", + "type": "git", "importUrl": "https://scm-manager-org/scm/repo/secret/puzzle42", "username": "trillian", "password": "secret"