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 73bcf10bd0..ecab36969c 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -19,6 +19,8 @@ public class VndMediaType { public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; + public static final String TAG = PREFIX + "tag" + SUFFIX; + public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; 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..0211647321 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -1,5 +1,7 @@ package sonia.scm.it; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import org.apache.http.HttpStatus; import org.junit.Assume; import org.junit.Before; @@ -8,18 +10,22 @@ 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; +import sonia.scm.web.VndMediaType; import java.io.File; 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; @@ -74,6 +80,88 @@ public class RepositoryAccessITCase { assertNotNull(branchName); } + @Test + public void shouldFindTags() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + + Assume.assumeTrue("There are no tags for " + repositoryType, repositoryClient.isCommandSupported(ClientCommand.TAG)); + + Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "a.txt", "a"); + + String tagName = "v1.0"; + String repositoryUrl = TestData.getDefaultRepositoryUrl(repositoryType); + String tagsUrl = given() + .when() + .get(repositoryUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + .path("_links.tags.href"); + + ExtractableResponse response = given(VndMediaType.TAG_COLLECTION, ADMIN_USERNAME, ADMIN_PASSWORD) + .when() + .get(tagsUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract(); + + assertThat(response).isNotNull(); + assertThat(response.body()).isNotNull(); + assertThat(response.body().asString()) + .isNotNull() + .isNotBlank() + ; + + RepositoryUtil.addTag(repositoryClient, changeset.getId(), tagName); + response = given(VndMediaType.TAG_COLLECTION, ADMIN_USERNAME, ADMIN_PASSWORD) + .when() + .get(tagsUrl) + .then() + .statusCode(HttpStatus.SC_OK) + .extract() + ; + + assertThat(response).isNotNull(); + assertThat(response.body()).isNotNull(); + assertThat(response.body().asString()) + .isNotNull() + .isNotBlank() + ; + assertThat(response.body().jsonPath().getString("_links.self.href")) + .as("assert tags self link") + .isNotNull() + .contains(repositoryUrl + "/tags/") + ; + assertThat(response.body().jsonPath().getList("_embedded.tags")) + .as("assert tag size") + .isNotNull() + .size() + .isGreaterThan(0) + ; + assertThat(response.body().jsonPath().getMap("_embedded.tags.find{it.name=='" + tagName + "'}")) + .as("assert tag name and revision") + .isNotNull() + .hasSize(3) + .containsEntry("name", tagName) + .containsEntry("revision", changeset.getId()) + ; + assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.self.href")) + .as("assert single tag self link") + .isNotNull() + .contains(String.format("%s/tags/%s", repositoryUrl, tagName)) + ; + assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.sources.href")) + .as("assert single tag source link") + .isNotNull() + .contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId())) + ; + assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changesets.href")) + .as("assert single tag changesets link") + .isNotNull() + .contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId())) + ; + } + @Test public void shouldReadContent() throws IOException, InterruptedException { RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); 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 ef2332074b..9fb1fcb236 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -5,6 +5,7 @@ import com.google.common.io.Files; import org.apache.http.HttpStatus; import sonia.scm.repository.Changeset; import sonia.scm.repository.Person; +import sonia.scm.repository.Tag; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClientFactory; @@ -36,11 +37,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 { @@ -64,4 +65,16 @@ public class RepositoryUtil { } return changeset; } + + static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException { + if (repositoryClient.isCommandSupported(ClientCommand.TAG)) { + Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN); + if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { + repositoryClient.getPushCommand().pushTags(); + } + return tag; + } + + return null; + } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java index 3b9d29abdf..8d65b54b06 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitPushCommand.java @@ -37,12 +37,12 @@ package sonia.scm.repository.client.spi; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.transport.CredentialsProvider; - import sonia.scm.repository.client.api.RepositoryClientException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +import java.util.function.Supplier; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -73,11 +73,20 @@ public class GitPushCommand implements PushCommand * @throws IOException */ @Override - public void push() throws IOException + public void push() throws IOException { + push(() -> git.push().setPushAll()); + } + + @Override + public void pushTags() throws IOException { + push(() -> git.push().setPushTags()); + } + + private void push(Supplier commandSupplier) throws RepositoryClientException { try { - org.eclipse.jgit.api.PushCommand cmd = git.push().setPushAll(); + org.eclipse.jgit.api.PushCommand cmd = commandSupplier.get(); if (credentialsProvider != null) { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java index ee03dc7458..e5e1f36155 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/client/spi/GitTagCommand.java @@ -35,22 +35,20 @@ package sonia.scm.repository.client.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Strings; - import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevWalk; - import sonia.scm.repository.GitUtil; import sonia.scm.repository.Tag; import sonia.scm.repository.client.api.RepositoryClientException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -119,7 +117,11 @@ public class GitTagCommand implements TagCommand ref = git.tag().setObjectId(revObject).call(); } - tag = new Tag(request.getName(), ref.getPeeledObjectId().toString()); + if (ref.isPeeled()) { + tag = new Tag(request.getName(), ref.getPeeledObjectId().toString()); + } else { + tag = new Tag(request.getName(), ref.getObjectId().toString()); + } } catch (GitAPIException ex) diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java index 73e0a1f9a4..263347ca00 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgPushCommand.java @@ -32,9 +32,10 @@ package sonia.scm.repository.client.spi; import com.aragost.javahg.Repository; import com.aragost.javahg.commands.ExecutionException; -import java.io.IOException; import sonia.scm.repository.client.api.RepositoryClientException; +import java.io.IOException; + /** * Mercurial implementation of the {@link PushCommand}. * @@ -63,5 +64,10 @@ public class HgPushCommand implements PushCommand throw new RepositoryClientException("push to repository failed", ex); } } - + + @Override + public void pushTags() throws IOException { + push(); + } + } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java index 3aa448bfca..639adf9a1b 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgTagCommand.java @@ -32,7 +32,6 @@ package sonia.scm.repository.client.spi; import com.aragost.javahg.Repository; import com.google.common.base.Strings; -import java.io.IOException; import sonia.scm.repository.Tag; /** @@ -51,13 +50,16 @@ public class HgTagCommand implements TagCommand } @Override - public Tag tag(TagRequest request) throws IOException + public Tag tag(TagRequest request) { String rev = request.getRevision(); if ( Strings.isNullOrEmpty(rev) ){ rev = repository.tip().getNode(); } - com.aragost.javahg.commands.TagCommand.on(repository).rev(rev).execute(request.getName()); + com.aragost.javahg.commands.TagCommand.on(repository) + .rev(rev) + .user(request.getUserName()) + .execute(request.getName()); return new Tag(request.getName(), rev); } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java index b3b7619e7b..06b59e4ba0 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/PushCommandBuilder.java @@ -36,13 +36,12 @@ package sonia.scm.repository.client.api; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.client.spi.PushCommand; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -88,6 +87,14 @@ public final class PushCommandBuilder command.push(); } + public void pushTags() throws IOException { + if (logger.isDebugEnabled()) { + logger.debug("push tag changes back to main repository"); + } + + command.pushTags(); + } + //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java b/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java index 8b42817aa8..1d153dee0e 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/TagCommandBuilder.java @@ -36,15 +36,14 @@ package sonia.scm.repository.client.api; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.repository.Tag; import sonia.scm.repository.client.spi.TagCommand; import sonia.scm.repository.client.spi.TagRequest; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -84,9 +83,10 @@ public final class TagCommandBuilder * * @throws IOException */ - public Tag tag(String name) throws IOException + public Tag tag(String name, String username) throws IOException { request.setName(name); + request.setUsername(username); if (logger.isDebugEnabled()) { diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java index 5e85ed0e5e..be8fd94718 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/PushCommand.java @@ -44,11 +44,7 @@ import java.io.IOException; public interface PushCommand { - /** - * Method description - * - * - * @throws IOException - */ - public void push() throws IOException; + void push() throws IOException; + + void pushTags() throws IOException; } diff --git a/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java b/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java index ff34309b6d..53abf0f8f8 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/spi/TagRequest.java @@ -91,6 +91,7 @@ public final class TagRequest { this.name = null; this.revision = null; + this.username = null; } /** @@ -106,6 +107,7 @@ public final class TagRequest return MoreObjects.toStringHelper(this) .add("revision", revision) .add("name", name) + .add("username", username) .toString(); //J+ } @@ -134,6 +136,10 @@ public final class TagRequest this.revision = revision; } + public void setUsername(String username) { + this.username = username; + } + //~--- get methods ---------------------------------------------------------- /** @@ -158,6 +164,10 @@ public final class TagRequest return revision; } + public String getUserName() { + return username; + } + //~--- fields --------------------------------------------------------------- /** Field description */ @@ -165,4 +175,6 @@ public final class TagRequest /** Field description */ private String revision; + + private String username; } 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..1d3e5e222e 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 @@ -218,8 +218,8 @@ class ResourceLinks { tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class); } - String self(String namespace, String name, String id) { - return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(id).href(); + String self(String namespace, String name, String tagName) { + return tagLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("tags").parameters().method("get").parameters(tagName).href(); } String all(String namespace, String name) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagNotFoundException.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagNotFoundException.java new file mode 100644 index 0000000000..b572d20c5b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagNotFoundException.java @@ -0,0 +1,7 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.NotFoundException; + +public class TagNotFoundException extends NotFoundException { + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java index 10b4607fbc..b3601f7709 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java @@ -1,27 +1,100 @@ package sonia.scm.api.v2.resources; -import javax.ws.rs.DefaultValue; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.Tag; +import sonia.scm.repository.Tags; +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.QueryParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.io.IOException; public class TagRootResource { + private final RepositoryServiceFactory serviceFactory; + private final TagCollectionToDtoMapper tagCollectionToDtoMapper; + private final TagToTagDtoMapper tagToTagDtoMapper; + + @Inject + public TagRootResource(RepositoryServiceFactory serviceFactory, + TagCollectionToDtoMapper tagCollectionToDtoMapper, + TagToTagDtoMapper tagToTagDtoMapper) { + this.serviceFactory = serviceFactory; + this.tagCollectionToDtoMapper = tagCollectionToDtoMapper; + this.tagToTagDtoMapper = tagToTagDtoMapper; + } + @GET @Path("") - public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - throw new UnsupportedOperationException(); + @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 tags"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.TAG_COLLECTION) + @TypeHint(CollectionDto.class) + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException, RepositoryNotFoundException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Tags tags = getTags(repositoryService); + if (tags != null && tags.getTags() != null) { + return Response.ok(tagCollectionToDtoMapper.map(namespace, name, tags.getTags())).build(); + } else { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Error on getting tag from repository.") + .build(); + } + } } + @GET - @Path("{id}") - public Response get(String id) { - throw new UnsupportedOperationException(); + @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 tags"), + @ResponseCode(code = 404, condition = "not found, no tag available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.TAG) + @TypeHint(TagDto.class) + @Path("{tagName}") + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("tagName") String tagName) throws IOException, RepositoryNotFoundException, TagNotFoundException { + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { + Tags tags = getTags(repositoryService); + if (tags != null && tags.getTags() != null) { + Tag tag = tags.getTags().stream() + .filter(t -> tagName.equals(t.getName())) + .findFirst() + .orElseThrow(TagNotFoundException::new); + return Response.ok(tagToTagDtoMapper.map(tag, namespaceAndName)).build(); + } else { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Error on getting tag from repository.") + .build(); + } + } } + private Tags getTags(RepositoryService repositoryService) throws IOException { + Repository repository = repositoryService.getRepository(); + RepositoryPermissions.read(repository).check(); + return repositoryService.getTagsCommand().getTags(); + } + + } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java index ada0fa2887..917b4b7789 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java @@ -11,6 +11,7 @@ import sonia.scm.repository.Tag; import javax.inject.Inject; +import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @Mapper @@ -25,7 +26,9 @@ public abstract class TagToTagDtoMapper { @AfterMapping void appendLinks(@MappingTarget TagDto target, @Context NamespaceAndName namespaceAndName) { Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())); + .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())) + .single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))) + .single(link("changesets", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))); target.add(linksBuilder.build()); } } 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..40aa61852a 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 @@ -58,7 +58,7 @@ public class ChangesetRootResourceTest { private RepositoryServiceFactory serviceFactory; @Mock - private RepositoryService service; + private RepositoryService repositoryService; @Mock private LogCommandBuilder logCommandBuilder; @@ -83,12 +83,12 @@ public class ChangesetRootResourceTest { .of(new RepositoryResource(null, null, null, null, null, MockProvider.of(changesetRootResource), 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); - when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); + when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); + when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); - when(service.getLogCommand()).thenReturn(logCommandBuilder); + when(repositoryService.getLogCommand()).thenReturn(logCommandBuilder); subjectThreadState.bind(); ThreadContext.bind(subject); when(subject.isPermitted(any(String.class))).thenReturn(true); 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 new file mode 100644 index 0000000000..029184e71f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -0,0 +1,201 @@ +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.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.Tag; +import sonia.scm.repository.Tags; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.TagsCommandBuilder; +import sonia.scm.web.VndMediaType; + +import java.net.URI; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(MockitoJUnitRunner.Silent.class) +public class TagRootResourceTest { + + public static final String TAG_PATH = "space/repo/tags/"; + public static final String TAG_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + TAG_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 repositoryService; + + @Mock + private TagsCommandBuilder tagsCommandBuilder; + + private TagCollectionToDtoMapper tagCollectionToDtoMapper; + + @InjectMocks + private TagToTagDtoMapperImpl tagToTagDtoMapper; + + private TagRootResource tagRootResource; + + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + @Before + public void prepareEnvironment() throws Exception { + tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper); + 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); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); + when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); + when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + when(repositoryService.getTagsCommand()).thenReturn(tagsCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGet404OnMissingTag() throws Exception { + Tags tags = new Tags(); + tags.setTags(Lists.emptyList()); + when(tagsCommandBuilder.getTags()).thenReturn(tags); + + MockHttpRequest request = MockHttpRequest + .get(TAG_URL + "not_existing_tag") + .accept(VndMediaType.TAG); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGetEmptyTagListOnMissingTags() throws Exception { + Tags tags = new Tags(); + tags.setTags(Lists.emptyList()); + when(tagsCommandBuilder.getTags()).thenReturn(tags); + + MockHttpRequest request = MockHttpRequest + .get(TAG_URL) + .accept(VndMediaType.TAG_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + assertThat(response).isNotNull(); + assertThat(response.getContentAsString()) + .isNotBlank() + .contains("_links") + ; + + } + + @Test + public void shouldGet500OnTagCommandError() throws Exception { + when(tagsCommandBuilder.getTags()).thenReturn(null); + + MockHttpRequest request = MockHttpRequest + .get(TAG_URL + "not_existing_tag") + .accept(VndMediaType.TAG); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + + request = MockHttpRequest + .get(TAG_URL) + .accept(VndMediaType.TAG_COLLECTION); + response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } + + + @Test + public void shouldGetTags() throws Exception { + Tags tags = new Tags(); + String tag1 = "v1.0"; + String revision1 = "revision_1234"; + String tag2 = "v2.0"; + String revision2 = "revision_12345"; + tags.setTags(Lists.newArrayList(new Tag(tag1, revision1), new Tag(tag2, revision2))); + when(tagsCommandBuilder.getTags()).thenReturn(tags); + + MockHttpRequest request = MockHttpRequest + .get(TAG_URL) + .accept(VndMediaType.TAG_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("the content: ", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag1))); + assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision1))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2))); + assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2))); + } + + @Test + public void shouldGetTag() throws Exception { + Tags tags = new Tags(); + String tag1 = "v1.0"; + String revision1 = "revision_1234"; + String tag2 = "v2.0"; + String revision2 = "revision_12345"; + tags.setTags(Lists.newArrayList(new Tag(tag1, revision1), new Tag(tag2, revision2))); + when(tagsCommandBuilder.getTags()).thenReturn(tags); + + MockHttpRequest request = MockHttpRequest + .get(TAG_URL + tag1) + .accept(VndMediaType.TAG); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag1))); + assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision1))); + + request = MockHttpRequest + .get(TAG_URL + tag2) + .accept(VndMediaType.TAG); + response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("the content: ", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", tag2))); + assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision2))); + } +}