From 56d323793e4720de41f31bc7f5a95b1ef63a1c49 Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Thu, 6 Sep 2018 11:33:39 +0200 Subject: [PATCH 1/3] file history endpoint --- .../ChangesetCollectionToDtoMapper.java | 11 +- .../FileHistoryCollectionToDtoMapper.java | 24 +++ .../v2/resources/FileHistoryRootResource.java | 78 +++++++ .../FileObjectToFileObjectDtoMapper.java | 3 + .../InternalRepositoryExceptionMapper.java | 15 ++ .../api/v2/resources/RepositoryResource.java | 10 +- .../scm/api/v2/resources/ResourceLinks.java | 17 ++ .../v2/resources/BranchRootResourceTest.java | 2 +- .../resources/ChangesetRootResourceTest.java | 4 +- .../v2/resources/FileHistoryResourceTest.java | 203 ++++++++++++++++++ .../resources/PermissionRootResourceTest.java | 2 +- .../resources/RepositoryRootResourceTest.java | 2 +- .../api/v2/resources/ResourceLinksMock.java | 1 + .../v2/resources/SourceRootResourceTest.java | 2 +- 14 files changed, 363 insertions(+), 11 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java index fcc4085486..3af3a1d15a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java @@ -6,11 +6,12 @@ import sonia.scm.repository.Repository; import javax.inject.Inject; import java.util.Optional; +import java.util.function.Supplier; public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper { private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; - private final ResourceLinks resourceLinks; + protected final ResourceLinks resourceLinks; @Inject public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { @@ -20,10 +21,14 @@ public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper pageResult, Repository repository) { - return super.map(pageNumber, pageSize, pageResult, createSelfLink(repository), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository)); + return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository)); } - private String createSelfLink(Repository repository) { + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, Supplier selfLinkSupplier) { + return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository)); + } + + protected String createSelfLink(Repository repository) { return resourceLinks.changeset().all(repository.getNamespace(), repository.getName()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java new file mode 100644 index 0000000000..692b2f57b1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java @@ -0,0 +1,24 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMapper { + + + @Inject + public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { + super(changesetToChangesetDtoMapper, resourceLinks); + } + + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, String revision, String path) { + return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, revision, path)); + } + + protected String createSelfLink(Repository repository, String revision, String path) { + return super.resourceLinks.fileHistory().self(repository.getNamespace(), repository.getName(), revision, path); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java new file mode 100644 index 0000000000..d56590b384 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -0,0 +1,78 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import lombok.extern.slf4j.Slf4j; +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +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.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.io.IOException; + +@Slf4j +public class FileHistoryRootResource { + + private final RepositoryServiceFactory serviceFactory; + + private final FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; + + + @Inject + public FileHistoryRootResource(RepositoryServiceFactory serviceFactory, FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper) { + this.serviceFactory = serviceFactory; + this.fileHistoryCollectionToDtoMapper = fileHistoryCollectionToDtoMapper; + } + + @GET + @Path("{revision}/{path: .*}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET_COLLECTION) + @TypeHint(CollectionDto.class) + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, + @PathParam("revision") String revision, + @PathParam("path") String path, + @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + log.info("Get changesets of the file {} and revision {}", path, revision); + Repository repository = repositoryService.getRepository(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setPagingStart(page) + .setPagingLimit(pageSize) + .setPath(path) + .setStartChangeset(revision) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null) { + PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); + return Response.ok(fileHistoryCollectionToDtoMapper.map(page, pageSize, pageResult, repository, revision, path)).build(); + } else { + String message = String.format("for the revision %s and the file %s there is no changesets", revision, path); + log.error(message); + throw new InternalRepositoryException(message); + } + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java index bc814c7e0c..0c5e61991a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java @@ -12,6 +12,8 @@ import sonia.scm.repository.SubRepository; import javax.inject.Inject; import java.net.URI; +import static de.otto.edison.hal.Link.link; + @Mapper public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper { @@ -30,6 +32,7 @@ public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper { + + public InternalRepositoryExceptionMapper() { + super(InternalRepositoryException.class, Response.Status.INTERNAL_SERVER_ERROR); + } +} 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 c47fc4f3d1..1585065b91 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 @@ -40,6 +40,7 @@ public class RepositoryResource { private final Provider contentResource; private final Provider permissionRootResource; private final Provider diffRootResource; + private final Provider fileHistoryRootResource; @Inject public RepositoryResource( @@ -50,7 +51,8 @@ public class RepositoryResource { Provider changesetRootResource, Provider sourceRootResource, Provider contentResource, Provider permissionRootResource, - Provider diffRootResource) { + Provider diffRootResource, + Provider fileHistoryRootResource) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; @@ -62,6 +64,7 @@ public class RepositoryResource { this.contentResource = contentResource; this.permissionRootResource = permissionRootResource; this.diffRootResource = diffRootResource; + this.fileHistoryRootResource = fileHistoryRootResource; } /** @@ -165,6 +168,11 @@ public class RepositoryResource { return changesetRootResource.get(); } + @Path("history/") + public FileHistoryRootResource history() { + return fileHistoryRootResource.get(); + } + @Path("sources/") public SourceRootResource sources() { return sourceRootResource.get(); 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 d51a462c19..5610b61dfc 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 @@ -307,6 +307,23 @@ class ResourceLinks { } } + public FileHistoryLinks fileHistory() { + return new FileHistoryLinks(uriInfoStore.get()); + } + + static class FileHistoryLinks { + private final LinkBuilder fileHistoryLinkBuilder; + + FileHistoryLinks(UriInfo uriInfo) { + fileHistoryLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, FileHistoryRootResource.class); + } + + String self(String namespace, String name, String changesetId, String path) { + return fileHistoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("history").parameters().method("getAll").parameters(changesetId, path).href(); + } + + } + public SourceLinks source() { return new SourceLinks(uriInfoStore.get()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index e23d3dc39b..0a209e905f 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -92,7 +92,7 @@ public class BranchRootResourceTest { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null, null)), null); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null, null, null)), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index 570830d651..44f9d7e7bc 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -81,7 +81,7 @@ public class ChangesetRootResourceTest { changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider .of(new RepositoryResource(null, null, null, null, null, - MockProvider.of(changesetRootResource), null, null, null, null)), null); + MockProvider.of(changesetRootResource), null, null, null, null, null)), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); @@ -125,7 +125,6 @@ public class ChangesetRootResourceTest { assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); - assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); } @Test @@ -155,7 +154,6 @@ public class ChangesetRootResourceTest { assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); - assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java new file mode 100644 index 0000000000..f89a92f124 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -0,0 +1,203 @@ +package sonia.scm.api.v2.resources; + +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.LogCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Instant; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +@Slf4j +public class FileHistoryResourceTest { + + public static final String FILE_HISTORY_PATH = "space/repo/history/"; + public static final String FILE_HISTORY_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + FILE_HISTORY_PATH; + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @Mock + private RepositoryServiceFactory serviceFactory; + + @Mock + private RepositoryService service; + + @Mock + private LogCommandBuilder logCommandBuilder; + + private FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; + + @InjectMocks + private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + + private FileHistoryRootResource fileHistoryRootResource; + + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + @Before + public void prepareEnvironment() throws Exception { + fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); + fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider + .of(new RepositoryResource(null, null, null, null, null, + null, null, null, null, null, MockProvider.of(fileHistoryRootResource))), null); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); + when(service.getLogCommand()).thenReturn(logCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGetFileHistory() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + Instant creationDate = Instant.now(); + String authorName = "name"; + String authorEmail = "em@i.l"; + String commit = "my branch commit"; + ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); + List changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } + + + @Test + public void shouldGet404OnMissingRepository() throws URISyntaxException, RepositoryNotFoundException { + when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(RepositoryNotFoundException.class); + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + "revision/a.txt") + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet404OnMissingRevision() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenThrow(RevisionNotFoundException.class); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet500OnInternalRepositoryException() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenThrow(InternalRepositoryException.class); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } + + @Test + public void shouldGet500OnNullChangesets() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(null); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 8d05c1f455..3f2ea9b317 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -138,7 +138,7 @@ public class PermissionRootResourceTest { permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource), null)), null); + .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource), null, null)), null); dispatcher = createDispatcher(repositoryRootResource); subjectThreadState.bind(); ThreadContext.bind(subject); 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 539d469445..48ca62089f 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 @@ -79,7 +79,7 @@ public class RepositoryRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null, null); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null, null, null); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); 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 f9ace9f8f6..018797a7a3 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 @@ -22,6 +22,7 @@ public class ResourceLinksMock { 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.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 52e445a801..5321219b18 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -1,7 +1,6 @@ package sonia.scm.api.v2.resources; import org.jboss.resteasy.core.Dispatcher; -import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; @@ -74,6 +73,7 @@ public class SourceRootResourceTest { MockProvider.of(sourceRootResource), null, null, + null, null)), null); dispatcher = createDispatcher(repositoryRootResource); From 8ec7677b754945d036713b5c41753193d0c6b2f0 Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Mon, 10 Sep 2018 09:51:59 +0200 Subject: [PATCH 2/3] integration tests for the file history endpoint --- .../sonia/scm/it/RepositoryAccessITCase.java | 37 ++- .../java/sonia/scm/it/RepositoryRequests.java | 245 ++++++++++++++++++ .../java/sonia/scm/it/RepositoryUtil.java | 4 +- .../v2/resources/FileHistoryRootResource.java | 14 + 4 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index a9139f18c8..e3603d258b 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -8,6 +8,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import sonia.scm.repository.Changeset; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; @@ -16,10 +17,12 @@ import java.io.IOException; import java.util.Collection; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; import static java.lang.Thread.sleep; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertNotNull; +import static sonia.scm.it.RestUtil.ADMIN_PASSWORD; +import static sonia.scm.it.RestUtil.ADMIN_USERNAME; import static sonia.scm.it.RestUtil.given; import static sonia.scm.it.ScmTypes.availableScmTypes; @@ -31,6 +34,7 @@ public class RepositoryAccessITCase { private final String repositoryType; private File folder; + private RepositoryRequests.AppliedRepositoryGetRequest repositoryGetRequest; public RepositoryAccessITCase(String repositoryType) { this.repositoryType = repositoryType; @@ -42,9 +46,15 @@ public class RepositoryAccessITCase { } @Before - public void initClient() { + public void init() { TestData.createDefault(); folder = tempFolder.getRoot(); + repositoryGetRequest = RepositoryRequests.start() + .given() + .url(TestData.getDefaultRepositoryUrl(repositoryType)) + .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) + .get() + .assertStatusCode(HttpStatus.SC_OK); } @Test @@ -152,5 +162,28 @@ public class RepositoryAccessITCase { assertThat(changesets).size().isBetween(2, 3); // svn has an implicit root revision '0' that is extra to the two commits } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindFileHistory() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + String fileName_1 = "a.txt"; + Changeset changeset_1 = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName_1, "a"); + + repositoryGetRequest + .usingRepositoryResponse() + .requestSources() + .usingSourcesResponse() + .requestFileHistory(fileName_1) + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .assertChangesets(changesets -> { + assertThat(changesets).hasSize(1); + assertThat(changesets.get(0)).containsEntry("id", changeset_1.getId()); + assertThat(changesets.get(0)).containsEntry("description", changeset_1.getDescription()); + } + ); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java b/scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java new file mode 100644 index 0000000000..62ce82aded --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java @@ -0,0 +1,245 @@ +package sonia.scm.it; + +import io.restassured.RestAssured; +import io.restassured.response.Response; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + + +/** + * Encapsulate rest requests of a repository in builder pattern + *

+ * A Get Request can be applied with the methods request*() + * These methods return a AppliedGet*Request object + * This object can be used to apply general assertions over the rest Assured response + * In the AppliedGet*Request classes there is a using*Response() method + * that return the *Response class containing specific operations related to the specific response + * the *Response class contains also the request*() method to apply the next GET request from a link in the response. + */ +public class RepositoryRequests { + + private String url; + private String username; + private String password; + + static RepositoryRequests start() { + return new RepositoryRequests(); + } + + Given given() { + return new Given(); + } + + + /** + * Apply a GET Request to the extracted url from the given link + * + * @param linkPropertyName the property name of link + * @param response the response containing the link + * @return the response of the GET request using the given link + */ + private Response getResponseFromLink(Response response, String linkPropertyName) { + return getResponse(response + .then() + .extract() + .path(linkPropertyName)); + } + + + /** + * Apply a GET Request to the given url and return the response. + * + * @param url the url of the GET request + * @return the response of the GET request using the given url + */ + private Response getResponse(String url) { + return RestAssured.given() + .auth().preemptive().basic(username, password) + .when() + .get(url); + } + + private void setUrl(String url) { + this.url = url; + } + + private void setUsername(String username) { + this.username = username; + } + + private void setPassword(String password) { + this.password = password; + } + + private String getUrl() { + return url; + } + + private String getUsername() { + return username; + } + + private String getPassword() { + return password; + } + + class Given { + + GivenUrl url(String url) { + setUrl(url); + return new GivenUrl(); + } + + } + + class GivenWithUrlAndAuth { + AppliedRepositoryGetRequest get() { + return new AppliedRepositoryGetRequest( + getResponse(url) + ); + } + } + + class AppliedGetRequest { + private Response response; + + public AppliedGetRequest(Response response) { + this.response = response; + } + + /** + * apply custom assertions to the actual response + * + * @param consumer consume the response in order to assert the content. the header, the payload etc.. + * @return the self object + */ + SELF assertResponse(Consumer consumer) { + consumer.accept(response); + return (SELF) this; + } + + /** + * special assertion of the status code + * + * @param expectedStatusCode the expected status code + * @return the self object + */ + SELF assertStatusCode(int expectedStatusCode) { + this.response.then().assertThat().statusCode(expectedStatusCode); + return (SELF) this; + } + + } + + class AppliedRepositoryGetRequest extends AppliedGetRequest { + + AppliedRepositoryGetRequest(Response response) { + super(response); + } + + RepositoryResponse usingRepositoryResponse() { + return new RepositoryResponse(super.response); + } + } + + class RepositoryResponse { + + private Response repositoryResponse; + + public RepositoryResponse(Response repositoryResponse) { + this.repositoryResponse = repositoryResponse; + } + + AppliedGetSourcesRequest requestSources() { + return new AppliedGetSourcesRequest(getResponseFromLink(repositoryResponse, "_links.sources.href")); + } + + AppliedGetChangesetsRequest requestChangesets(String fileName) { + return new AppliedGetChangesetsRequest(getResponseFromLink(repositoryResponse, "_links.changesets.href")); + } + } + + class AppliedGetChangesetsRequest extends AppliedGetRequest { + + AppliedGetChangesetsRequest(Response response) { + super(response); + } + + ChangesetsResponse usingChangesetsResponse() { + return new ChangesetsResponse(super.response); + } + } + + class ChangesetsResponse { + private Response changesetsResponse; + + public ChangesetsResponse(Response changesetsResponse) { + this.changesetsResponse = changesetsResponse; + } + + ChangesetsResponse assertChangesets(Consumer> changesetsConsumer) { + List changesets = changesetsResponse.then().extract().path("_embedded.changesets"); + changesetsConsumer.accept(changesets); + return this; + } + + AppliedGetDiffRequest requestDiff(String revision) { + return new AppliedGetDiffRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href")); + } + + } + + class AppliedGetSourcesRequest extends AppliedGetRequest { + + public AppliedGetSourcesRequest(Response sourcesResponse) { + super(sourcesResponse); + } + + SourcesResponse usingSourcesResponse() { + return new SourcesResponse(super.response); + } + } + + class SourcesResponse { + + private Response sourcesResponse; + + SourcesResponse(Response sourcesResponse) { + this.sourcesResponse = sourcesResponse; + } + + SourcesResponse assertRevision(Consumer assertRevision) { + String revision = sourcesResponse.then().extract().path("revision"); + assertRevision.accept(revision); + return this; + } + + SourcesResponse assertFiles(Consumer assertFiles) { + List files = sourcesResponse.then().extract().path("files"); + assertFiles.accept(files); + return this; + } + + AppliedGetChangesetsRequest requestFileHistory(String fileName) { + return new AppliedGetChangesetsRequest(getResponseFromLink(sourcesResponse, "files.find{it.name=='" + fileName + "'}._links.history.href")); + } + } + + class AppliedGetDiffRequest extends AppliedGetRequest { + + AppliedGetDiffRequest(Response response) { + super(response); + } + } + + class GivenUrl { + + GivenWithUrlAndAuth usernameAndPassword(String username, String password) { + setUsername(username); + setPassword(password); + return new GivenWithUrlAndAuth(); + } + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java index e49927b1b9..b2d0f44578 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -40,11 +40,11 @@ public class RepositoryUtil { return name; } - static void createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { + static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { File file = new File(repositoryClient.getWorkingCopy(), fileName); Files.write(content, file, Charsets.UTF_8); addWithParentDirectories(repositoryClient, file); - commit(repositoryClient, username, "added " + fileName); + return commit(repositoryClient, username, "added " + fileName); } private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java index d56590b384..118cc4167a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -40,6 +40,20 @@ public class FileHistoryRootResource { this.fileHistoryCollectionToDtoMapper = fileHistoryCollectionToDtoMapper; } + /** + * Get all changesets related to the given file starting with the given revision + * + * @param namespace the repository namespace + * @param name the repository name + * @param revision the revision + * @param path the path of the file + * @param page pagination + * @param pageSize pagination + * @return all changesets related to the given file starting with the given revision + * @throws IOException on io error + * @throws RevisionNotFoundException on missing revision + * @throws RepositoryNotFoundException on missing repository + */ @GET @Path("{revision}/{path: .*}") @StatusCodes({ From 2387c17e5e01480f49916bfdaa3cf435093a471d Mon Sep 17 00:00:00 2001 From: Philipp Czora Date: Wed, 12 Sep 2018 07:40:51 +0000 Subject: [PATCH 3/3] Close branch feature/file_history_endpoint_v2