diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index af4e65caf0..14fd4b4532 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.web; import javax.ws.rs.core.MediaType; @@ -71,13 +71,12 @@ public class VndMediaType { @SuppressWarnings("squid:S2068") public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX; public static final String PERMISSION_COLLECTION = PREFIX + "permissionCollection" + SUFFIX; - public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX; - public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX; public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX; public static final String ME = PREFIX + "me" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX; + public static final String ANNOTATE = PREFIX + "annotate" + SUFFIX; public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX; public static final String REPOSITORY_ROLE = PREFIX + "repositoryRole" + SUFFIX; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AnnotateResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AnnotateResource.java new file mode 100644 index 0000000000..43f8d87130 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AnnotateResource.java @@ -0,0 +1,95 @@ +/* + * 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 io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import java.io.IOException; + +public class AnnotateResource { + + private final RepositoryServiceFactory serviceFactory; + private final BlameResultToBlameDtoMapper mapper; + + @Inject + public AnnotateResource(RepositoryServiceFactory serviceFactory, BlameResultToBlameDtoMapper mapper) { + this.serviceFactory = serviceFactory; + this.mapper = mapper; + } + + /** + * Returns the content of a file with additional information for each line regarding the last commit that + * changed this line: The revision, the author, the date, and the description of the commit. + * + * @param namespace the namespace of the repository + * @param name the name of the repository + * @param revision the revision + * @param path The path of the file + */ + @GET + @Path("{revision}/{path: .*}") + @Produces(VndMediaType.ANNOTATE) + @Operation(summary = "File content by revision", description = "Returns the annotated file for the given revision in the repository.", tags = "Repository") + @ApiResponse(responseCode = "200", description = "success") + @ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials") + @ApiResponse(responseCode = "403", description = "not authorized, the current user has no privileges to read the repository") + @ApiResponse( + responseCode = "404", + description = "not found, no repository with the specified name available in the namespace", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + @ApiResponse( + responseCode = "500", + description = "internal server error", + content = @Content( + mediaType = VndMediaType.ERROR_TYPE, + schema = @Schema(implementation = ErrorDto.class) + )) + public BlameDto annotate( + @PathParam("namespace") String namespace, + @PathParam("name") String name, + @PathParam("revision") String revision, + @PathParam("path") String path + ) throws IOException { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { + return mapper.map(repositoryService.getBlameCommand().setRevision(revision).getBlameResult(path), namespaceAndName, revision, path); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java index ab065762e2..9e6f82a549 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseFileObjectDtoMapper.java @@ -21,7 +21,7 @@ * 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.annotations.VisibleForTesting; @@ -63,6 +63,7 @@ abstract class BaseFileObjectDtoMapper extends HalAppenderMapper implements Inst } else { links.self(resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path)); links.single(link("history", resourceLinks.fileHistory().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); + links.single(link("annotate", resourceLinks.annotate().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path))); } if (fileObject.isTruncated()) { links.single(link("proceed", resourceLinks.source().content(namespaceAndName.getNamespace(), namespaceAndName.getName(), browserResult.getRevision(), path) + "?offset=" + (offset + BrowseCommandRequest.DEFAULT_REQUEST_LIMIT))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameDto.java new file mode 100644 index 0000000000..8aec147a85 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameDto.java @@ -0,0 +1,42 @@ +/* + * 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 de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Data; + +import java.util.List; + +@Data +public class BlameDto extends HalRepresentation { + + private List blameLines; + + public BlameDto(Links links) { + super(links); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameLineDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameLineDto.java new file mode 100644 index 0000000000..29e3d785b6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameLineDto.java @@ -0,0 +1,41 @@ +/* + * 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 lombok.Data; + +import java.time.Instant; + +@Data +public class BlameLineDto { + + private PersonDto author; + private String code; + private String description; + private int lineNumber; + private String revision; + private Instant when; + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameResultToBlameDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameResultToBlameDtoMapper.java new file mode 100644 index 0000000000..4758d4ba97 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BlameResultToBlameDtoMapper.java @@ -0,0 +1,72 @@ +/* + * 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.annotations.VisibleForTesting; +import de.otto.edison.hal.Links; +import org.mapstruct.Mapper; +import org.mapstruct.ObjectFactory; +import sonia.scm.repository.BlameLine; +import sonia.scm.repository.BlameResult; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; + +import javax.inject.Inject; +import java.util.stream.Collectors; + +@Mapper +public abstract class BlameResultToBlameDtoMapper implements InstantAttributeMapper { + + @Inject + private ResourceLinks resourceLinks; + + BlameDto map(BlameResult result, NamespaceAndName namespaceAndName, String revision, String path) { + BlameDto dto = createDto(namespaceAndName, revision, path); + dto.setBlameLines(result.getBlameLines().stream().map(this::map).collect(Collectors.toList())); + return dto; + } + + abstract BlameLineDto map(BlameLine line); + + abstract PersonDto map(Person person); + + @ObjectFactory + BlameDto createDto(NamespaceAndName namespaceAndName, String revision, String path) { + return new BlameDto(Links.linkingTo() + .self( + resourceLinks.annotate() + .self( + namespaceAndName.getNamespace(), + namespaceAndName.getName(), + revision, + path)) + .build()); + } + + @VisibleForTesting + void setResourceLinks(ResourceLinks resourceLinks) { + this.resourceLinks = resourceLinks; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index d77533e01d..a45b52ddbe 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -73,6 +73,8 @@ public class MapperModule extends AbstractModule { bind(RepositoryToHalMapper.class).to(Mappers.getMapperClass(RepositoryToRepositoryDtoMapper.class)); + bind(BlameResultToBlameDtoMapper.class).to(Mappers.getMapperClass(BlameResultToBlameDtoMapper.class)); + // no mapstruct required bind(MeDtoFactory.class); bind(UIPluginDtoMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java index 96580a717c..9b779ebaa6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryBasedResourceProvider.java @@ -38,6 +38,7 @@ public class RepositoryBasedResourceProvider { private final Provider modificationsRootResource; private final Provider fileHistoryRootResource; private final Provider incomingRootResource; + private final Provider annotateResource; @Inject public RepositoryBasedResourceProvider( @@ -50,7 +51,7 @@ public class RepositoryBasedResourceProvider { Provider diffRootResource, Provider modificationsRootResource, Provider fileHistoryRootResource, - Provider incomingRootResource) { + Provider incomingRootResource, Provider annotateResource) { this.tagRootResource = tagRootResource; this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; @@ -61,6 +62,7 @@ public class RepositoryBasedResourceProvider { this.modificationsRootResource = modificationsRootResource; this.fileHistoryRootResource = fileHistoryRootResource; this.incomingRootResource = incomingRootResource; + this.annotateResource = annotateResource; } public TagRootResource getTagRootResource() { @@ -102,4 +104,8 @@ public class RepositoryBasedResourceProvider { public IncomingRootResource getIncomingRootResource() { return incomingRootResource.get(); } + + public AnnotateResource getAnnotateResource() { + return annotateResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 711e1b16c9..2ac4f0674f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -232,6 +232,11 @@ public class RepositoryResource { return resourceProvider.getIncomingRootResource(); } + @Path("annotate/") + public AnnotateResource annotate() { + return resourceProvider.getAnnotateResource(); + } + private Supplier loadBy(String namespace, String name) { NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); return () -> Optional.ofNullable(manager.get(namespaceAndName)).orElseThrow(() -> notFound(entity(namespaceAndName))); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 1b667602f0..48842edb92 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import sonia.scm.repository.NamespaceAndName; @@ -566,6 +566,22 @@ class ResourceLinks { } } + public AnnotateLinks annotate() { + return new AnnotateLinks(scmPathInfoStore.get()); + } + + static class AnnotateLinks { + private final LinkBuilder annotateLinkBuilder; + + AnnotateLinks(ScmPathInfo pathInfo) { + this.annotateLinkBuilder = new LinkBuilder(pathInfo, RepositoryRootResource.class, RepositoryResource.class, AnnotateResource.class); + } + + String self(String namespace, String name, String revision, String path) { + return annotateLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("annotate").parameters().method("annotate").parameters(revision, path).href(); + } + } + RepositoryVerbLinks repositoryVerbs() { return new RepositoryVerbLinks(scmPathInfoStore.get()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AnnotateResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AnnotateResourceTest.java new file mode 100644 index 0000000000..af64d4dad3 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AnnotateResourceTest.java @@ -0,0 +1,147 @@ +/* + * 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.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.repository.BlameLine; +import sonia.scm.repository.BlameResult; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.api.BlameCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.RestDispatcher; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AnnotateResourceTest extends RepositoryTestBase { + + public static final NamespaceAndName NAMESPACE_AND_NAME = new NamespaceAndName("space", "X"); + public static final String REVISION = "123"; + public static final String PATH = "some/file"; + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + @Mock + private BlameCommandBuilder blameCommandBuilder; + + private final RestDispatcher dispatcher = new RestDispatcher(); + private final MockHttpResponse response = new MockHttpResponse(); + + @BeforeEach + void initResource() { + BlameResultToBlameDtoMapperImpl mapper = new BlameResultToBlameDtoMapperImpl(); + mapper.setResourceLinks(ResourceLinksMock.createMock(URI.create("/"))); + annotateResource = new AnnotateResource(serviceFactory, mapper); + dispatcher.addSingletonResource(getRepositoryRootResource()); + } + + @BeforeEach + void initRepositoryService() { + when(serviceFactory.create(NAMESPACE_AND_NAME)).thenReturn(service); + when(service.getBlameCommand()).thenReturn(blameCommandBuilder); + } + + @BeforeEach + void initBlameCommand() throws IOException { + BlameLine line1 = new BlameLine( + 0, + "100", + System.currentTimeMillis(), + new Person("Arthur Dent", "arthur@hitchhiker.com"), + "first try", + "jump" + ); + BlameLine line2 = new BlameLine( + 1, + "42", + System.currentTimeMillis(), + new Person("Zaphod Beeblebrox", "zaphod@hitchhiker.com"), + "got it", + "heart of gold" + ); + BlameResult result = new BlameResult(asList(line1, line2)); + when(blameCommandBuilder.setRevision(REVISION)).thenReturn(blameCommandBuilder); + when(blameCommandBuilder.getBlameResult(PATH)).thenReturn(result); + } + + @Test + void test() throws URISyntaxException, UnsupportedEncodingException, JsonProcessingException { + MockHttpRequest request = MockHttpRequest + .get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + NAMESPACE_AND_NAME + "/annotate/" + REVISION + "/" + PATH); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(200); + + String content = response.getContentAsString(); + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(content); + JsonNode blameLines = jsonNode.get("blameLines"); + assertThat(blameLines.isArray()).isTrue(); + assertThat(jsonNode.get("_links").get("self").get("href").asText()) + .isEqualTo("/v2/repositories/space/X/annotate/123/some%2Ffile"); + + Iterator lineIterator = blameLines.iterator(); + JsonNode line1 = lineIterator.next(); + assertThat(line1.get("author").get("mail").asText()).isEqualTo("arthur@hitchhiker.com"); + assertThat(line1.get("author").get("name").asText()).isEqualTo("Arthur Dent"); + assertThat(line1.get("code").asText()).isEqualTo("jump"); + assertThat(line1.get("description").asText()).isEqualTo("first try"); + assertThat(line1.get("lineNumber").asInt()).isEqualTo(0); + assertThat(line1.get("revision").asText()).isEqualTo("100"); + + JsonNode line2 = lineIterator.next(); + assertThat(line2.get("author").get("mail").asText()).isEqualTo("zaphod@hitchhiker.com"); + assertThat(line2.get("author").get("name").asText()).isEqualTo("Zaphod Beeblebrox"); + assertThat(line2.get("code").asText()).isEqualTo("heart of gold"); + assertThat(line2.get("description").asText()).isEqualTo("got it"); + assertThat(line2.get("lineNumber").asInt()).isEqualTo(1); + assertThat(line2.get("revision").asText()).isEqualTo("42"); + + assertThat(lineIterator.hasNext()).isFalse(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java index 0d4dc09e10..bafb92fd62 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -45,6 +45,7 @@ abstract class RepositoryTestBase { FileHistoryRootResource fileHistoryRootResource; IncomingRootResource incomingRootResource; RepositoryCollectionResource repositoryCollectionResource; + AnnotateResource annotateResource; RepositoryRootResource getRepositoryRootResource() { @@ -58,8 +59,8 @@ abstract class RepositoryTestBase { of(diffRootResource), of(modificationsRootResource), of(fileHistoryRootResource), - of(incomingRootResource) - ); + of(incomingRootResource), + of(annotateResource)); return new RepositoryRootResource( of(new RepositoryResource( repositoryToDtoMapper, diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 39a9d2e30c..96cf705625 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -21,11 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import java.net.URI; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,47 +34,48 @@ public class ResourceLinksMock { public static ResourceLinks createMock(URI baseUri) { ResourceLinks resourceLinks = mock(ResourceLinks.class); - ScmPathInfo uriInfo = mock(ScmPathInfo.class); - when(uriInfo.getApiRestUri()).thenReturn(baseUri); + ScmPathInfo pathInfo = mock(ScmPathInfo.class); + when(pathInfo.getApiRestUri()).thenReturn(baseUri); - ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo); - when(resourceLinks.user()).thenReturn(userLinks); - when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks)); - when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo)); - when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(uriInfo)); - when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(uriInfo)); - when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); - when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); - when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(uriInfo)); - when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); - when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(uriInfo)); - when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo)); - when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); - when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); - when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); - when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(uriInfo)); - when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); - when(resourceLinks.repositoryPermission()).thenReturn(new ResourceLinks.RepositoryPermissionLinks(uriInfo)); - when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); - when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); - when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(uriInfo)); - when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(uriInfo)); - when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); - when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); - when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(uriInfo)); - when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(uriInfo)); - when(resourceLinks.pendingPluginCollection()).thenReturn(new ResourceLinks.PendingPluginCollectionLinks(uriInfo)); - when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(uriInfo)); - when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(uriInfo)); - when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); - when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo)); - when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo)); - when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); - when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); - when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(uriInfo)); - when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(uriInfo)); - when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(uriInfo)); - when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo)); + ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(pathInfo); + lenient().when(resourceLinks.user()).thenReturn(userLinks); + lenient().when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(pathInfo,userLinks)); + lenient().when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(pathInfo)); + lenient().when(resourceLinks.userPermissions()).thenReturn(new ResourceLinks.UserPermissionLinks(pathInfo)); + lenient().when(resourceLinks.autoComplete()).thenReturn(new ResourceLinks.AutoCompleteLinks(pathInfo)); + lenient().when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(pathInfo)); + lenient().when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(pathInfo)); + lenient().when(resourceLinks.groupPermissions()).thenReturn(new ResourceLinks.GroupPermissionLinks(pathInfo)); + lenient().when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(pathInfo)); + lenient().when(resourceLinks.incoming()).thenReturn(new ResourceLinks.IncomingLinks(pathInfo)); + lenient().when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(pathInfo)); + lenient().when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(pathInfo)); + lenient().when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(pathInfo)); + lenient().when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(pathInfo)); + lenient().when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(pathInfo)); + lenient().when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(pathInfo)); + lenient().when(resourceLinks.repositoryPermission()).thenReturn(new ResourceLinks.RepositoryPermissionLinks(pathInfo)); + lenient().when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(pathInfo)); + lenient().when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(pathInfo)); + lenient().when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(pathInfo)); + lenient().when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(pathInfo)); + lenient().when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(pathInfo)); + lenient().when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(pathInfo)); + lenient().when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.pendingPluginCollection()).thenReturn(new ResourceLinks.PendingPluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(pathInfo)); + lenient().when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(pathInfo)); + lenient().when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(pathInfo)); + lenient().when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(pathInfo)); + lenient().when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(pathInfo)); + lenient().when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(pathInfo)); + lenient().when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(pathInfo)); + lenient().when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(pathInfo)); + lenient().when(resourceLinks.repositoryRole()).thenReturn(new ResourceLinks.RepositoryRoleLinks(pathInfo)); + lenient().when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(pathInfo)); + lenient().when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(pathInfo)); + lenient().when(resourceLinks.annotate()).thenReturn(new ResourceLinks.AnnotateLinks(pathInfo)); return resourceLinks; }