diff --git a/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java b/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java index 36052d99e9..4c6a52b878 100644 --- a/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java +++ b/scm-core/src/main/java/sonia/scm/AlreadyExistsException.java @@ -1,4 +1,11 @@ package sonia.scm; public class AlreadyExistsException extends Exception { + + public AlreadyExistsException(String message) { + super(message); + } + + public AlreadyExistsException() { + } } diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 8a7ae642bd..0d8c14c61b 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -7,4 +7,9 @@ public class NotFoundException extends Exception { public NotFoundException() { } + + + public NotFoundException(String message) { + super(message); + } } 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 6184cc73e4..85764291a7 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,8 +10,10 @@ 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; @@ -20,6 +24,8 @@ 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,85 @@ 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..442856eefa 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java @@ -3,8 +3,11 @@ package sonia.scm.it; import com.google.common.base.Charsets; import com.google.common.io.Files; import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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; @@ -15,6 +18,8 @@ import java.util.UUID; public class RepositoryUtil { + private static final Logger LOG = LoggerFactory.getLogger(RepositoryUtil.class); + private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { @@ -36,11 +41,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 { @@ -58,10 +63,23 @@ public class RepositoryUtil { } static Changeset commit(RepositoryClient repositoryClient, String username, String message) throws IOException { + LOG.info("user: {} try to commit with message: {}", username, message); Changeset changeset = repositoryClient.getCommitCommand().commit(new Person(username, username + "@scm-manager.org"), message); if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { repositoryClient.getPushCommand().push(); } 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-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/TestData.java index ada67d588c..ae0e35004d 100644 --- a/scm-it/src/test/java/sonia/scm/it/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/TestData.java @@ -33,6 +33,7 @@ public class TestData { } public static void cleanup() { + LOG.info("start to clean up to integration tests"); cleanupRepositories(); cleanupGroups(); cleanupUsers(); @@ -43,6 +44,7 @@ public class TestData { } public static void createUser(String username, String password) { + LOG.info("create user with username: {}", username); given(VndMediaType.USER) .when() .content(" {\n" + @@ -64,6 +66,8 @@ public class TestData { public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { + String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); + LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl); given(VndMediaType.PERMISSION) .when() .content("{\n" + @@ -72,7 +76,7 @@ public class TestData { "\t\"groupPermission\": false\n" + "\t\n" + "}") - .post(TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType)) + .post(defaultPermissionUrl) .then() .statusCode(HttpStatus.SC_CREATED) ; @@ -114,6 +118,7 @@ public class TestData { private static void cleanupRepositories() { + LOG.info("clean up repository"); List repositories = given(VndMediaType.REPOSITORY_COLLECTION) .when() .get(createResourceUrl("repositories")) @@ -160,6 +165,7 @@ public class TestData { } private static void createDefaultRepositories() { + LOG.info("create default repositories"); for (String repositoryType : availableScmTypes()) { String url = given(VndMediaType.REPOSITORY) .body(repositoryJson(repositoryType)) @@ -171,6 +177,7 @@ public class TestData { .statusCode(HttpStatus.SC_CREATED) .extract() .header("location"); + LOG.info("a {} repository is created: {}", repositoryType, url); DEFAULT_REPOSITORIES.put(repositoryType, url); } } 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/rest/AlreadyExistsExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java index 28240482e5..13d341e1e6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/AlreadyExistsExceptionMapper.java @@ -11,6 +11,8 @@ import javax.ws.rs.ext.Provider; public class AlreadyExistsExceptionMapper implements ExceptionMapper { @Override public Response toResponse(AlreadyExistsException exception) { - return Response.status(Status.CONFLICT).build(); + return Response.status(Status.CONFLICT) + .entity(exception.getMessage()) + .build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java index 32b6a02bc6..4a8a5854f0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/IllegalArgumentExceptionMapper.java @@ -33,6 +33,8 @@ package sonia.scm.api.rest; //~--- JDK imports ------------------------------------------------------------ +import lombok.extern.slf4j.Slf4j; + import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; @@ -43,7 +45,7 @@ import javax.ws.rs.ext.Provider; * @author Sebastian Sdorra * @since 1.36 */ -@Provider +@Provider @Slf4j public class IllegalArgumentExceptionMapper implements ExceptionMapper { @@ -59,6 +61,7 @@ public class IllegalArgumentExceptionMapper @Override public Response toResponse(IllegalArgumentException exception) { + log.info("caught IllegalArgumentException -- mapping to bad request", exception); return Response.status(Status.BAD_REQUEST).build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java index fef7fc96c4..6f4978637e 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/StatusExceptionMapper.java @@ -90,6 +90,8 @@ public class StatusExceptionMapper logger.debug(msg.toString()); } - return Response.status(status).build(); + return Response.status(status) + .entity(exception.getMessage()) + .build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java deleted file mode 100644 index 4f8c6a6f3f..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java +++ /dev/null @@ -1,32 +0,0 @@ -package sonia.scm.api.v2.resources; - -import de.otto.edison.hal.HalRepresentation; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -import static de.otto.edison.hal.Embedded.embeddedBuilder; -import static de.otto.edison.hal.Links.linkingTo; - -abstract class CollectionToDtoMapper { - - private final String collectionName; - private final BaseMapper mapper; - - protected CollectionToDtoMapper(String collectionName, BaseMapper mapper) { - this.collectionName = collectionName; - this.mapper = mapper; - } - - public HalRepresentation map(Collection collection) { - List dtos = collection.stream().map(mapper::map).collect(Collectors.toList()); - return new HalRepresentation( - linkingTo().self(createSelfLink()).build(), - embeddedBuilder().with(collectionName, dtos).build() - ); - } - - protected abstract String createSelfLink(); - -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java index b184bc3934..581b2c24cd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -10,6 +10,8 @@ import lombok.ToString; @Getter @Setter @ToString public class PermissionDto extends HalRepresentation { + public static final String GROUP_PREFIX = "@"; + @JsonInclude(JsonInclude.Include.NON_NULL) private String name; @@ -21,11 +23,19 @@ public class PermissionDto extends HalRepresentation { * **/ @JsonInclude(JsonInclude.Include.NON_NULL) - private String type ; + private String type; private boolean groupPermission = false; + public PermissionDto() { + } + + public PermissionDto(String permissionName, boolean groupPermission) { + name = permissionName; + this.groupPermission = groupPermission; + } + @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 2b53192542..90e9b3daed 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -5,7 +5,6 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; import sonia.scm.AlreadyExistsException; import sonia.scm.NotFoundException; import sonia.scm.repository.NamespaceAndName; @@ -28,10 +27,14 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Response; import java.net.URI; import java.util.Optional; +import java.util.function.Predicate; + +import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; @Slf4j public class PermissionRootResource { + private PermissionDtoToPermissionMapper dtoToModelMapper; private PermissionToPermissionDtoMapper modelToDtoMapper; private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; @@ -101,7 +104,7 @@ public class PermissionRootResource { return Response.ok( repository.getPermissions() .stream() - .filter(permission -> permissionName.equals(permission.getName())) + .filter(filterPermission(permissionName)) .map(permission -> modelToDtoMapper.map(permission, repository)) .findFirst() .orElseThrow(NotFoundException::new) @@ -135,6 +138,7 @@ public class PermissionRootResource { /** * Update a permission to the user or group managed by the repository + * ignore the user input for groupPermission and take it from the path parameter (if the group prefix (@) exists it is a group permission) * * @param permission permission to modify * @param permissionName permission to modify @@ -152,15 +156,23 @@ public class PermissionRootResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName, - PermissionDto permission) throws NotFoundException { + PermissionDto permission) throws NotFoundException, AlreadyExistsException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); + String extractedPermissionName = getPermissionName(permissionName); + if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) { + throw new NotFoundException("the permission " + extractedPermissionName + " does not exist"); + } + permission.setGroupPermission(isGroupPermission(permissionName)); + if (!extractedPermissionName.equals(permission.getName())) { + checkPermissionAlreadyExists(permission, repository, "target permission " + permission.getName() + " already exists"); + } Permission existingPermission = repository.getPermissions() .stream() - .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .filter(filterPermission(permissionName)) .findFirst() - .orElseThrow(() -> new NotFoundException()); + .orElseThrow(NotFoundException::new); dtoToModelMapper.modify(existingPermission, permission); manager.modify(repository); log.info("the permission with name: {} is updated.", permissionName); @@ -186,11 +198,11 @@ public class PermissionRootResource { @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws NotFoundException { log.info("try to delete the permission with name: {}.", permissionName); - Repository repository = load(namespace, name); + Repository repository = load(namespace, name); RepositoryPermissions.modify(repository).check(); repository.getPermissions() .stream() - .filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName)) + .filter(filterPermission(permissionName)) .findFirst() .ifPresent(p -> repository.getPermissions().remove(p)) ; @@ -199,6 +211,22 @@ public class PermissionRootResource { return Response.noContent().build(); } + Predicate filterPermission(String permissionName) { + return permission -> getPermissionName(permissionName).equals(permission.getName()) + && + permission.isGroupPermission() == isGroupPermission(permissionName); + } + + private String getPermissionName(String permissionName) { + return Optional.of(permissionName) + .filter(p -> !isGroupPermission(permissionName)) + .orElse(permissionName.substring(1)); + } + + private boolean isGroupPermission(String permissionName) { + return permissionName.startsWith(GROUP_PREFIX); + } + /** * check if the actual user is permitted to manage the repository permissions @@ -219,16 +247,24 @@ public class PermissionRootResource { * * @param permission the searched permission * @param repository the repository to be inspected + * @param errorMessage error message * @throws AlreadyExistsException if the permission already exists in the repository */ - private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws AlreadyExistsException { - boolean isPermissionAlreadyExist = repository.getPermissions() - .stream() - .anyMatch(p -> p.getName().equals(permission.getName())); - if (isPermissionAlreadyExist) { - throw new AlreadyExistsException(); + private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository, String errorMessage) throws AlreadyExistsException { + if (isPermissionExist(permission, repository)) { + throw new AlreadyExistsException(errorMessage); } } + + private boolean isPermissionExist(PermissionDto permission, Repository repository) { + return repository.getPermissions() + .stream() + .anyMatch(p -> p.getName().equals(permission.getName()) && p.isGroupPermission() == permission.isGroupPermission()); + } + + private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws AlreadyExistsException { + checkPermissionAlreadyExists(permission, repository, "the permission " + permission.getName() + " already exist."); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java index 6938c9059c..8ebe10eb6f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java @@ -12,9 +12,11 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; import javax.inject.Inject; +import java.util.Optional; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; +import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; @Mapper public abstract class PermissionToPermissionDtoMapper { @@ -39,11 +41,14 @@ public abstract class PermissionToPermissionDtoMapper { */ @AfterMapping void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) { + String permissionName = Optional.of(target.getName()) + .filter(p -> !target.isGroupPermission()) + .orElse(GROUP_PREFIX + target.getName()); Links.Builder linksBuilder = linkingTo() - .self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), target.getName())); + .self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), permissionName)); if (RepositoryPermissions.permissionWrite(repository).isPermitted()) { - linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), target.getName()))); - linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), target.getName()))); + linksBuilder.single(link("update", resourceLinks.permission().update(repository.getNamespace(), repository.getName(), permissionName))); + linksBuilder.single(link("delete", resourceLinks.permission().delete(repository.getNamespace(), repository.getName(), permissionName))); } target.add(linksBuilder.build()); } 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 bff34e88a4..f72e9fdd6d 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/SourceRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java index 32a4105c46..874ce4f568 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import sonia.scm.NotFoundException; import sonia.scm.repository.BrowserResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.RepositoryNotFoundException; @@ -46,7 +47,7 @@ public class SourceRootResource { @GET @Produces(VndMediaType.SOURCE) @Path("{revision}/{path: .*}") - public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws RevisionNotFoundException, RepositoryNotFoundException, IOException { + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws NotFoundException, IOException { return getSource(namespace, name, path, revision); } 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/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index e5d6fb4296..8d05c1f455 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 @@ -39,6 +39,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -55,6 +56,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; +import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; @Slf4j @SubjectAware( @@ -253,7 +255,7 @@ public class PermissionRootResourceTest { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); Permission newPermission = TEST_PERMISSIONS.get(0); assertExpectedRequest(requestPOSTPermission - .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}") + .content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : false}") .expectedResponseStatus(409) ); } @@ -358,7 +360,10 @@ public class PermissionRootResourceTest { result.setName(permission.getName()); result.setGroupPermission(permission.isGroupPermission()); result.setType(permission.getType().name()); - String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permission.getName(); + String permissionName = Optional.of(permission.getName()) + .filter(p -> !permission.isGroupPermission()) + .orElse(GROUP_PREFIX + permission.getName()); + String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permissionName; if (PERMISSION_READ.equals(userPermission)) { result.add(linkingTo() .self(permissionHref) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java new file mode 100644 index 0000000000..31c2f0ec31 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapperTest.java @@ -0,0 +1,63 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.Repository; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.Silent.class) +@SubjectAware( + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +public class PermissionToPermissionDtoMapperTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + private final URI baseUri = URI.create("http://example.com/base/"); + + @SuppressWarnings("unused") // Is injected + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @InjectMocks + PermissionToPermissionDtoMapperImpl mapper; + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldMapGroupPermissionCorrectly() { + Repository repository = getDummyRepository(); + Permission permission = new Permission("42", PermissionType.OWNER, true); + + PermissionDto permissionDto = mapper.map(permission, repository); + + assertThat(permissionDto.getLinks().getLinkBy("self").isPresent()).isTrue(); + assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).contains("@42"); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldMapNonGroupPermissionCorrectly() { + Repository repository = getDummyRepository(); + Permission permission = new Permission("42", PermissionType.OWNER, false); + + PermissionDto permissionDto = mapper.map(permission, repository); + + assertThat(permissionDto.getLinks().getLinkBy("self").isPresent()).isTrue(); + assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).contains("42"); + assertThat(permissionDto.getLinks().getLinkBy("self").get().getHref()).doesNotContain("@"); + } + + private Repository getDummyRepository() { + return new Repository("repo", "git", "foo", "bar"); + } +} 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 9fb202158d..a59ed0c103 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 @@ -29,12 +29,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @RunWith(MockitoJUnitRunner.Silent.class) public class SourceRootResourceTest { - private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + private Dispatcher dispatcher; private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -75,9 +76,7 @@ public class SourceRootResourceTest { null, null)), null); - - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); - dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher = createDispatcher(repositoryRootResource); } @Test 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..92d11b3895 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -0,0 +1,199 @@ +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))); + } +}