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 399dd26035..fc095828b3 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -37,6 +37,7 @@ public class RepositoryAccessITCase { private final String repositoryType; private File folder; + private RepositoryRequests.AppliedRepositoryGetRequest repositoryGetRequest; public RepositoryAccessITCase(String repositoryType) { this.repositoryType = repositoryType; @@ -48,9 +49,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 @@ -281,5 +288,29 @@ public class RepositoryAccessITCase { .contains("diff"); } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindFileHistory() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a"); + repositoryGetRequest + .usingRepositoryResponse() + .requestSources() + .usingSourcesResponse() + .requestSelf("folder") + .usingSourcesResponse() + .requestSelf("subfolder") + .usingSourcesResponse() + .requestFileHistory("a.txt") + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .assertChangesets(changesets -> { + assertThat(changesets).hasSize(1); + assertThat(changesets.get(0)).containsEntry("id", changeset.getId()); + assertThat(changesets.get(0)).containsEntry("description", changeset.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..41fa2a6a3d --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryRequests.java @@ -0,0 +1,249 @@ +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, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href")); + } + + AppliedGetSourcesRequest requestSelf(String fileName) { + return new AppliedGetSourcesRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.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 442856eefa..0e2eb24ca9 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -43,6 +43,7 @@ public class RepositoryUtil { static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { File file = new File(repositoryClient.getWorkingCopy(), fileName); + Files.createParentDirs(file); Files.write(content, file, Charsets.UTF_8); addWithParentDirectories(repositoryClient, file); return commit(repositoryClient, username, "added " + fileName); @@ -53,7 +54,6 @@ public class RepositoryUtil { String thisName = file.getName(); String path; if (!repositoryClient.getWorkingCopy().equals(parent)) { - addWithParentDirectories(repositoryClient, parent); path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName; } else { path = thisName; 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..118cc4167a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -0,0 +1,92 @@ +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 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({ + @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 fdcc5c56ca..01085958f8 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 @@ -11,6 +11,8 @@ import sonia.scm.repository.SubRepository; import javax.inject.Inject; +import static de.otto.edison.hal.Link.link; + @Mapper public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper { @@ -29,6 +31,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 f72e9fdd6d..3d61083033 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 @@ -15,6 +15,10 @@ class ResourceLinks { this.uriInfoStore = uriInfoStore; } + // we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F' + private static String addPath(String sourceWithPath, String path) { + return URI.create(sourceWithPath).resolve(path).toASCIIString(); + } GroupLinks group() { return new GroupLinks(uriInfoStore.get()); @@ -307,6 +311,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 addPath(fileHistoryLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("history").parameters().method("getAll").parameters(changesetId, "").href(), path); + } + + } + public SourceLinks source() { return new SourceLinks(uriInfoStore.get()); } @@ -338,10 +359,7 @@ class ResourceLinks { return addPath(sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("content").parameters().method("get").parameters(revision, "").href(), path); } - // we have to add the file path using URI, so that path separators (aka '/') will not be encoded as '%2F' - private String addPath(String sourceWithPath, String path) { - return URI.create(sourceWithPath).resolve(path).toASCIIString(); - } + } public PermissionLinks permission() { return new PermissionLinks(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 40aa61852a..a7bad96534 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(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); @@ -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/DiffResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java index 0daeb07b7c..2613292e0b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java @@ -65,7 +65,7 @@ public class DiffResourceTest { diffRootResource = new DiffRootResource(serviceFactory); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider .of(new RepositoryResource(null, null, null, null, null, - null, null, null, null, MockProvider.of(diffRootResource))), null); + null, null, null, null, MockProvider.of(diffRootResource),null)), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); 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 2c4893b362..5f35ce9cf2 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 @@ -73,6 +73,7 @@ public class SourceRootResourceTest { MockProvider.of(sourceRootResource), null, null, + null, null)), null); dispatcher = createDispatcher(repositoryRootResource); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java index 92d11b3895..ad4b396101 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -74,7 +74,7 @@ public class TagRootResourceTest { tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider .of(new RepositoryResource(null, null, null, MockProvider.of(tagRootResource), null, - null, null, null, null, null)), null); + null, null, null, null, null, null)), null); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService);