diff --git a/pom.xml b/pom.xml index 788abaca36..9ec2963d79 100644 --- a/pom.xml +++ b/pom.xml @@ -121,28 +121,31 @@ junit junit - ${junit.version} test org.hamcrest hamcrest-core - ${hamcrest.version} test org.hamcrest hamcrest-library - ${hamcrest.version} test org.mockito mockito-all - ${mokito.version} + test + + + + org.assertj + assertj-core + 3.10.0 test @@ -154,6 +157,8 @@ 9aadeeb true + + provided @@ -182,6 +187,134 @@ true + + com.webcohesion.enunciate + enunciate-core-annotations + ${enunciate.version} + + + + org.mapstruct + mapstruct-jdk8 + ${org.mapstruct.version} + + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + provided + + + + de.otto.edison + edison-hal + 2.0.1 + + + + org.projectlombok + lombok + 1.16.18 + provided + + + + + + org.jboss.resteasy + resteasy-jaxrs + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-jaxb-provider + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-jackson2-provider + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-multipart-provider + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-guice + ${resteasy.version} + + + + org.jboss.resteasy + resteasy-servlet-initializer + ${resteasy.version} + + + + javax.ws.rs + javax.ws.rs-api + ${jaxrs.version} + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + junit + junit + ${junit.version} + test + + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + + org.hamcrest + hamcrest-library + ${hamcrest.version} + test + + + + org.mockito + mockito-all + ${mokito.version} + test + + + + org.assertj + assertj-core + 3.10.0 + test + @@ -194,6 +327,22 @@ maven-javadoc-plugin 3.0.1 + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + + org.apache.maven.plugins + maven-assembly-plugin + 2.3 + + + + com.webcohesion.enunciate + enunciate-maven-plugin + ${enunciate.version} + @@ -294,7 +443,6 @@ org.apache.maven.plugins maven-resources-plugin - 2.6 ${project.build.sourceEncoding} @@ -554,10 +702,12 @@ 3.0.1 2.0.1 + 3.1.3.Final 1.19.4 + 2.9.1 2.8.6 4.0 - + 1.3.0 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index f19ad7453e..17ca03b115 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -79,23 +79,47 @@ guice-throwingproviders ${guice.version} - + javax.ws.rs javax.ws.rs-api - ${jaxrs.version} + + + com.fasterxml.jackson.core jackson-core - ${jackson.version} + com.fasterxml.jackson.core jackson-databind - ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-annotations + + + + + de.otto.edison + edison-hal + + + + + org.mapstruct + mapstruct-jdk8 + + + + + com.webcohesion.enunciate + enunciate-core-annotations diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java similarity index 66% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java index db24463be8..e4cf8ecb5d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java @@ -5,12 +5,12 @@ import org.mapstruct.Mapping; import java.time.Instant; -abstract class BaseMapper { +public abstract class BaseMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - public abstract D map(T object); + public abstract D map(T modelObject); - Instant mapTime(Long epochMilli) { + protected Instant mapTime(Long epochMilli) { return epochMilli == null? null: Instant.ofEpochMilli(epochMilli); } } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java new file mode 100644 index 0000000000..523629ce3b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/CollectionToDtoMapper.java @@ -0,0 +1,32 @@ +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; + +public 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/LinkBuilder.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java similarity index 88% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java index a659d49004..6f6831b058 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java @@ -23,12 +23,13 @@ import java.util.Arrays; * .create(); * */ -class LinkBuilder { +@SuppressWarnings("WeakerAccess") // Non-public will result in IllegalAccessError for plugins +public class LinkBuilder { private final UriInfo uriInfo; private final Class[] classes; private final ImmutableList calls; - LinkBuilder(UriInfo uriInfo, Class... classes) { + public LinkBuilder(UriInfo uriInfo, Class... classes) { this(uriInfo, classes, ImmutableList.of()); } @@ -38,25 +39,24 @@ class LinkBuilder { this.calls = calls; } - Parameters method(String method) { + public Parameters method(String method) { if (calls.size() >= classes.length) { throw new IllegalStateException("no more classes for methods"); } return new Parameters(method); } - URI create() { + public URI create() { if (calls.size() < classes.length) { throw new IllegalStateException("not enough methods for all classes"); } URI baseUri = uriInfo.getBaseUri(); URI relativeUri = createRelativeUri(); - URI absoluteUri = baseUri.resolve(relativeUri); - return absoluteUri; + return baseUri.resolve(relativeUri); } - String href() { + public String href() { return create().toString(); } @@ -87,7 +87,7 @@ class LinkBuilder { return UriBuilder.fromResource(classes[0]); } - class Parameters { + public class Parameters { private final String method; @@ -95,7 +95,7 @@ class LinkBuilder { this.method = method; } - LinkBuilder parameters(String... parameters) { + public LinkBuilder parameters(String... parameters) { return LinkBuilder.this.add(method, parameters); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java similarity index 100% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java rename to scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java index 6e1db68f91..e94fabfa60 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java @@ -90,9 +90,10 @@ public class ScmConfiguration implements Configuration { /** * the logger for ScmConfiguration */ - private static final Logger logger = - LoggerFactory.getLogger(ScmConfiguration.class); + private static final Logger logger = LoggerFactory.getLogger(ScmConfiguration.class); + @SuppressWarnings("WeakerAccess") // This might be needed for permission checking + public static final String PERMISSION = "global"; @XmlElement(name = "admin-groups") @XmlJavaTypeAdapter(XmlSetStringAdapter.class) @@ -509,6 +510,6 @@ public class ScmConfiguration implements Configuration { @XmlTransient public String getId() { // Don't change this without migrating SCM permission configuration! - return "global"; + return PERMISSION; } } 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 41efc60cde..6563918192 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -10,8 +10,8 @@ public class VndMediaType { private static final String VERSION = "2"; private static final String TYPE = "application"; private static final String SUBTYPE_PREFIX = "vnd.scmm-"; - private static final String PREFIX = TYPE + "/" + SUBTYPE_PREFIX; - private static final String SUFFIX = "+json;v=" + VERSION; + public static final String PREFIX = TYPE + "/" + SUBTYPE_PREFIX; + public static final String SUFFIX = "+json;v=" + VERSION; public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; diff --git a/scm-plugins/pom.xml b/scm-plugins/pom.xml index 5b61882815..f9065babc2 100644 --- a/scm-plugins/pom.xml +++ b/scm-plugins/pom.xml @@ -38,7 +38,7 @@ provided - + sonia.scm @@ -46,6 +46,20 @@ 2.0.0-SNAPSHOT provided + + + + org.mapstruct + mapstruct-processor + provided + + + + + org.projectlombok + lombok + provided + @@ -56,6 +70,19 @@ test + + org.jboss.resteasy + resteasy-jaxrs + test + + + + org.jboss.resteasy + resteasy-jackson2-provider + test + + + @@ -140,6 +167,96 @@ + + doc + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-enunciate-configuration + compile + + copy-resources + + + ${project.build.directory} + + + src/main/doc + true + + **/enunciate.xml + + + + + + + + + + com.webcohesion.enunciate + enunciate-maven-plugin + + + + docs + + compile + + + + ${project.build.directory}/enunciate.xml + ${project.build.directory} + restdocs + + + + com.webcohesion.enunciate + enunciate-top + ${enunciate.version} + + + com.webcohesion.enunciate + enunciate-swagger + + + + + com.webcohesion.enunciate + enunciate-lombok + ${enunciate.version} + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + src/main/doc/assembly.xml + + + + + package + + single + + + + + + + + + diff --git a/scm-plugins/scm-git-plugin/pom.xml b/scm-plugins/scm-git-plugin/pom.xml index 7013c237cb..c6ae017d9e 100644 --- a/scm-plugins/scm-git-plugin/pom.xml +++ b/scm-plugins/scm-git-plugin/pom.xml @@ -9,9 +9,7 @@ 2.0.0-SNAPSHOT - sonia.scm.plugins scm-git-plugin - 2.0.0-SNAPSHOT scm-git-plugin smp https://bitbucket.org/sdorra/scm-manager @@ -19,13 +17,6 @@ - - javax.servlet - javax.servlet-api - ${servlet.version} - provided - - sonia.jgit org.eclipse.jgit @@ -50,15 +41,6 @@ 2.6 - - - - sonia.scm - scm-test - 2.0.0-SNAPSHOT - test - - diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java new file mode 100644 index 0000000000..19a014dcdf --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.File; + +@NoArgsConstructor +@Getter +@Setter +public class GitConfigDto extends HalRepresentation { + + private File repositoryDirectory; + private boolean disabled = false; + + private String gcExpression; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapper.java new file mode 100644 index 0000000000..74ba684a24 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapper.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import sonia.scm.repository.GitConfig; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class GitConfigDtoToGitConfigMapper { + public abstract GitConfig map(GitConfigDto dto); +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java new file mode 100644 index 0000000000..1384d73d9c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -0,0 +1,91 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.GitConfig; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.web.GitVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * RESTful Web Service Resource to manage the configuration of the git plugin. + */ +@Path(GitConfigResource.GIT_CONFIG_PATH_V2) +public class GitConfigResource { + + static final String GIT_CONFIG_PATH_V2 = "v2/config/git"; + private final GitConfigDtoToGitConfigMapper dtoToConfigMapper; + private final GitConfigToGitConfigDtoMapper configToDtoMapper; + private final GitRepositoryHandler repositoryHandler; + + @Inject + public GitConfigResource(GitConfigDtoToGitConfigMapper dtoToConfigMapper, GitConfigToGitConfigDtoMapper configToDtoMapper, + GitRepositoryHandler repositoryHandler) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.configToDtoMapper = configToDtoMapper; + this.repositoryHandler = repositoryHandler; + } + + /** + * Returns the git config. + */ + @GET + @Path("") + @Produces(GitVndMediaType.GIT_CONFIG) + @TypeHint(GitConfigDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:git\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get() { + + GitConfig config = repositoryHandler.getConfig(); + + if (config == null) { + config = new GitConfig(); + repositoryHandler.setConfig(config); + } + + ConfigurationPermissions.read(config).check(); + + return Response.ok(configToDtoMapper.map(config)).build(); + } + + /** + * Modifies the git config. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(GitVndMediaType.GIT_CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:git\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(GitConfigDto configDto) { + + GitConfig config = dtoToConfigMapper.map(configDto); + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.setConfig(config); + repositoryHandler.storeConfig(); + + return Response.noContent().build(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java new file mode 100644 index 0000000000..7163497487 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java @@ -0,0 +1,41 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.GitConfig; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper { + + @Inject + private UriInfoStore uriInfoStore; + + @AfterMapping + void appendLinks(GitConfig config, @MappingTarget GitConfigDto target) { + Links.Builder linksBuilder = linkingTo().self(self()); + if (ConfigurationPermissions.write(config).isPermitted()) { + linksBuilder.single(link("update", update())); + } + target.add(linksBuilder.build()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), GitConfigResource.class); + return linkBuilder.method("get").parameters().href(); + } + + private String update() { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), GitConfigResource.class); + return linkBuilder.method("update").parameters().href(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java index 2ec93ed83d..03f38b0086 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java @@ -48,7 +48,10 @@ import javax.xml.bind.annotation.XmlTransient; @XmlRootElement(name = "config") @XmlAccessorType(XmlAccessType.FIELD) public class GitConfig extends RepositoryConfig { - + + @SuppressWarnings("WeakerAccess") // This might be needed for permission checking + public static final String PERMISSION = "git"; + @XmlElement(name = "gc-expression") private String gcExpression; @@ -57,10 +60,14 @@ public class GitConfig extends RepositoryConfig { return gcExpression; } + public void setGcExpression(String gcExpression) { + this.gcExpression = gcExpression; + } + @Override @XmlTransient // Only for permission checks, don't serialize to XML public String getId() { // Don't change this without migrating SCM permission configuration! - return "git"; + return PERMISSION; } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java index 3d3442ce2a..bdad103c15 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java @@ -36,11 +36,11 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.servlet.ServletModule; - import org.eclipse.jgit.transport.ScmTransportProtocol; - +import org.mapstruct.factory.Mappers; +import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper; +import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper; import sonia.scm.plugin.Extension; - import sonia.scm.web.lfs.LfsBlobStoreFactory; /** @@ -73,6 +73,9 @@ public class GitServletModule extends ServletModule bind(LfsBlobStoreFactory.class); + bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass()); + bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass()); + // serlvelts and filters serve(PATTERN_GIT).with(ScmGitServlet.class); } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitVndMediaType.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitVndMediaType.java new file mode 100644 index 0000000000..8c81c6eefa --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitVndMediaType.java @@ -0,0 +1,8 @@ +package sonia.scm.web; + +public class GitVndMediaType { + public static final String GIT_CONFIG = VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX; + + private GitVndMediaType() { + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java new file mode 100644 index 0000000000..968b6b7f6f --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigDtoToGitConfigMapperTest.java @@ -0,0 +1,36 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.GitConfig; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@RunWith(MockitoJUnitRunner.class) +public class GitConfigDtoToGitConfigMapperTest { + + @InjectMocks + private GitConfigDtoToGitConfigMapperImpl mapper; + + @Test + public void shouldMapFields() { + GitConfigDto dto = createDefaultDto(); + GitConfig config = mapper.map(dto); + assertEquals("express", config.getGcExpression()); + assertEquals("repository/directory", config.getRepositoryDirectory().getPath()); + assertFalse(config.isDisabled()); + } + + private GitConfigDto createDefaultDto() { + GitConfigDto gitConfigDto = new GitConfigDto(); + gitConfigDto.setGcExpression("express"); + gitConfigDto.setDisabled(false); + gitConfigDto.setRepositoryDirectory(new File("repository/directory")); + return gitConfigDto; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java new file mode 100644 index 0000000000..42790ea7a4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java @@ -0,0 +1,160 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.GitConfig; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.web.GitVndMediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class GitConfigResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @InjectMocks + private GitConfigDtoToGitConfigMapperImpl dtoToConfigMapper; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private GitConfigToGitConfigDtoMapperImpl configToDtoMapper; + + @Mock + private GitRepositoryHandler repositoryHandler; + + @Before + public void prepareEnvironment() { + GitConfig gitConfig = createConfiguration(); + when(repositoryHandler.getConfig()).thenReturn(gitConfig); + GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); + dispatcher.getRegistry().addSingletonResource(gitConfigResource); + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetGitConfig() throws URISyntaxException, IOException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class); + + assertTrue(responseString.contains("\"disabled\":false")); + assertTrue(responseJson.get("repositoryDirectory").asText().endsWith("repository/directory")); + assertTrue(responseString.contains("\"gcExpression\":\"valid Git GC Cron Expression\"")); + assertTrue(responseString.contains("\"self\":{\"href\":\"/v2/config/git")); + assertTrue(responseString.contains("\"update\":{\"href\":\"/v2/config/git")); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException, IOException { + when(repositoryHandler.getConfig()).thenReturn(null); + + MockHttpResponse response = get(); + String responseString = response.getContentAsString(); + + assertTrue(responseString.contains("\"disabled\":false")); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetGitConfigWithoutUpdateLink() throws URISyntaxException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertFalse(response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config/git")); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:read:git]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfig() throws URISyntaxException { + MockHttpResponse response = put(); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException { + thrown.expectMessage("Subject does not have permission [configuration:write:git]"); + + put(); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + GitConfigResource.GIT_CONFIG_PATH_V2) + .contentType(GitVndMediaType.GIT_CONFIG) + .content("{\"disabled\":true}".getBytes()); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private GitConfig createConfiguration() { + GitConfig config = new GitConfig(); + config.setGcExpression("valid Git GC Cron Expression"); + config.setDisabled(false); + config.setRepositoryDirectory(new File("repository/directory")); + return config; + } + +} + diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java new file mode 100644 index 0000000000..51fded4839 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java @@ -0,0 +1,87 @@ +package sonia.scm.api.v2.resources; + +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.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.GitConfig; + +import java.io.File; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class GitConfigToGitConfigDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private GitConfigToGitConfigDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() { + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(GitConfigResource.GIT_CONFIG_PATH_V2); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapFields() { + GitConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:git")).thenReturn(true); + GitConfigDto dto = mapper.map(config); + + assertEquals("express", dto.getGcExpression()); + assertFalse(dto.isDisabled()); + assertEquals("repository/directory", dto.getRepositoryDirectory().getPath()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); + } + + @Test + public void shouldMapFieldsWithoutUpdate() { + GitConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:git")).thenReturn(false); + GitConfigDto dto = mapper.map(config); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertFalse(dto.getLinks().hasLink("update")); + } + + private GitConfig createConfiguration() { + GitConfig config = new GitConfig(); + config.setDisabled(false); + config.setRepositoryDirectory(new File("repository/directory")); + config.setGcExpression("express"); + return config; + } + +} diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini new file mode 100644 index 0000000000..36226edd7d --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -0,0 +1,9 @@ +[users] +readOnly = secret, reader +writeOnly = secret, writer +readWrite = secret, readerWriter + +[roles] +reader = configuration:read:git +writer = configuration:write:git +readerWriter = configuration:*:git diff --git a/scm-plugins/scm-hg-plugin/pom.xml b/scm-plugins/scm-hg-plugin/pom.xml index 67b6b3e684..d3381e3d22 100644 --- a/scm-plugins/scm-hg-plugin/pom.xml +++ b/scm-plugins/scm-hg-plugin/pom.xml @@ -9,9 +9,7 @@ 2.0.0-SNAPSHOT - sonia.scm.plugins scm-hg-plugin - 2.0.0-SNAPSHOT scm-hg-plugin smp https://bitbucket.org/sdorra/scm-manager @@ -30,15 +28,6 @@ - - - - - sonia.scm - scm-test - 2.0.0-SNAPSHOT - test - diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java new file mode 100644 index 0000000000..b265f2929d --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResource.java @@ -0,0 +1,76 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +public class HgConfigAutoConfigurationResource { + + private final HgRepositoryHandler repositoryHandler; + private final HgConfigDtoToHgConfigMapper dtoToConfigMapper; + + @Inject + public HgConfigAutoConfigurationResource(HgConfigDtoToHgConfigMapper dtoToConfigMapper, + HgRepositoryHandler repositoryHandler) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.repositoryHandler = repositoryHandler; + } + + /** + * Sets the default hg config and installs the hg binary. + */ + @PUT + @Path("") + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response autoConfiguration() { + return autoConfiguration(null); + } + + /** + * Modifies the hg config and installs the hg binary. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(HgVndMediaType.CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response autoConfiguration(HgConfigDto configDto) { + + HgConfig config; + + if (configDto != null) { + config = dtoToConfigMapper.map(configDto); + } else { + config = new HgConfig(); + } + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.doAutoConfiguration(config); + + return Response.noContent().build(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java new file mode 100644 index 0000000000..9fefc05ca4 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDto.java @@ -0,0 +1,31 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.File; + +@NoArgsConstructor +@Getter +@Setter +public class HgConfigDto extends HalRepresentation { + + private boolean disabled; + private File repositoryDirectory; + + private String encoding; + private String hgBinary; + private String pythonBinary; + private String pythonPath; + private boolean useOptimizedBytecode; + private boolean showRevisionInId; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapper.java new file mode 100644 index 0000000000..af3879013e --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapper.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import sonia.scm.repository.HgConfig; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class HgConfigDtoToHgConfigMapper { + public abstract HgConfig map(HgConfigDto dto); +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsDto.java new file mode 100644 index 0000000000..b60f7f5460 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsDto.java @@ -0,0 +1,21 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class HgConfigInstallationsDto extends HalRepresentation { + + private List paths; + + public HgConfigInstallationsDto(Links links, List paths) { + super(links); + this.paths = paths; + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java new file mode 100644 index 0000000000..8842d07569 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsResource.java @@ -0,0 +1,69 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.installer.HgInstallerFactory; +import sonia.scm.repository.HgConfig; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +public class HgConfigInstallationsResource { + + public static final String PATH_HG = "hg"; + public static final String PATH_PYTHON = "python"; + private final HgConfigInstallationsToDtoMapper hgConfigInstallationsToDtoMapper; + + @Inject + public HgConfigInstallationsResource(HgConfigInstallationsToDtoMapper hgConfigInstallationsToDtoMapper) { + this.hgConfigInstallationsToDtoMapper = hgConfigInstallationsToDtoMapper; + } + + /** + * Returns the hg installations. + */ + @GET + @Path(PATH_HG) + @Produces(HgVndMediaType.INSTALLATIONS) + @TypeHint(HalRepresentation.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public HalRepresentation getHgInstallations() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + return hgConfigInstallationsToDtoMapper.map( + HgInstallerFactory.createInstaller().getHgInstallations(), PATH_HG); + } + + /** + * Returns the python installations. + */ + @GET + @Path(PATH_PYTHON) + @Produces(HgVndMediaType.INSTALLATIONS) + @TypeHint(HalRepresentation.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public HalRepresentation getPythonInstallations() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + return hgConfigInstallationsToDtoMapper.map( + HgInstallerFactory.createInstaller().getPythonInstallations(), PATH_PYTHON); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java new file mode 100644 index 0000000000..d2f4aecf7e --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + + +import javax.inject.Inject; +import java.util.List; + +import static de.otto.edison.hal.Links.linkingTo; + +public class HgConfigInstallationsToDtoMapper { + + private UriInfoStore uriInfoStore; + + @Inject + public HgConfigInstallationsToDtoMapper(UriInfoStore uriInfoStore) { + this.uriInfoStore = uriInfoStore; + } + + public HgConfigInstallationsDto map(List installations, String path) { + return new HgConfigInstallationsDto(linkingTo().self(createSelfLink(path)).build(), installations); + } + + private String createSelfLink(String path) { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("getInstallationsResource").parameters().href() + '/' + path; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java new file mode 100644 index 0000000000..88a7de7ea0 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackageResource.java @@ -0,0 +1,96 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.SCMContext; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.installer.HgInstallerFactory; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackageReader; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +public class HgConfigPackageResource { + + private final HgPackageReader pkgReader; + private final AdvancedHttpClient client; + private final HgRepositoryHandler handler; + private final HgConfigPackagesToDtoMapper configPackageCollectionToDtoMapper; + + @Inject + public HgConfigPackageResource(HgPackageReader pkgReader, AdvancedHttpClient client, HgRepositoryHandler handler, + HgConfigPackagesToDtoMapper hgConfigPackagesToDtoMapper) { + this.pkgReader = pkgReader; + this.client = client; + this.handler = handler; + this.configPackageCollectionToDtoMapper = hgConfigPackagesToDtoMapper; + } + + /** + * Returns all mercurial packages. + */ + @GET + @Path("") + @Produces(HgVndMediaType.PACKAGES) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(HalRepresentation.class) + public HalRepresentation getPackages() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + return configPackageCollectionToDtoMapper.map(pkgReader.getPackages()); + } + + /** + * Installs a mercurial package + * + * @param pkgId Identifier of the package to install + */ + @PUT + @Path("{pkgId}") + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 404, condition = "no package found for id"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response installPackage(@PathParam("pkgId") String pkgId) { + Response response; + + ConfigurationPermissions.write(HgConfig.PERMISSION).check(); + + HgPackage pkg = pkgReader.getPackage(pkgId); + + if (pkg != null) { + if (HgInstallerFactory.createInstaller() + .installPackage(client, handler, SCMContext.getContext().getBaseDirectory(), pkg)) { + response = Response.noContent().build(); + } else { + response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } else { + response = Response.status(Response.Status.NOT_FOUND).build(); + } + + return response; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesDto.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesDto.java new file mode 100644 index 0000000000..959df12b61 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesDto.java @@ -0,0 +1,38 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@NoArgsConstructor +@Getter +@Setter +public class HgConfigPackagesDto extends HalRepresentation { + + private List packages; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } + + @NoArgsConstructor + @Getter + @Setter + public static class HgConfigPackageDto { + + private String arch; + private HgConfigDto hgConfigTemplate; + private String hgVersion; + private String id; + private String platform; + private String pythonVersion; + private long size; + private String url; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java new file mode 100644 index 0000000000..67d7e58dff --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java @@ -0,0 +1,59 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import lombok.Getter; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackages; + +import javax.inject.Inject; +import java.util.List; + +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class HgConfigPackagesToDtoMapper { + + @Inject + private UriInfoStore uriInfoStore; + + public HgConfigPackagesDto map(HgPackages hgpackages) { + return map(new HgPackagesNonIterable(hgpackages)); + } + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + /* Favor warning "Unmapped target property: "attributes", to packages[].hgConfigTemplate" + Over error "Unknown property "packages[].hgConfigTemplate.attributes" + @Mapping(target = "packages[].hgConfigTemplate.attributes", ignore = true) // Also not for nested DTOs + */ + protected abstract HgConfigPackagesDto map(HgPackagesNonIterable hgPackagesNonIterable); + + @AfterMapping + void appendLinks(@MappingTarget HgConfigPackagesDto target) { + Links.Builder linksBuilder = linkingTo().self(createSelfLink()); + target.add(linksBuilder.build()); + } + + private String createSelfLink() { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("getPackagesResource").parameters().href(); + } + + /** + * Unfortunately, HgPackages is iterable, HgConfigPackagesDto does not need to be iterable and MapStruct refuses to + * map an iterable to a non-iterable. So use this little non-iterable "proxy". + */ + @Getter + static class HgPackagesNonIterable { + private List packages; + + HgPackagesNonIterable(HgPackages hgPackages) { + this.packages = hgPackages.getPackages(); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java new file mode 100644 index 0000000000..e6a8f01238 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigResource.java @@ -0,0 +1,116 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * RESTful Web Service Resource to manage the configuration of the hg plugin. + */ +@Path(HgConfigResource.HG_CONFIG_PATH_V2) +public class HgConfigResource { + + static final String HG_CONFIG_PATH_V2 = "v2/config/hg"; + + private final HgConfigDtoToHgConfigMapper dtoToConfigMapper; + private final HgConfigToHgConfigDtoMapper configToDtoMapper; + private final HgRepositoryHandler repositoryHandler; + private final Provider packagesResource; + private final Provider autoconfigResource; + private final Provider installationsResource; + + @Inject + public HgConfigResource(HgConfigDtoToHgConfigMapper dtoToConfigMapper, HgConfigToHgConfigDtoMapper configToDtoMapper, + HgRepositoryHandler repositoryHandler, Provider packagesResource, + Provider autoconfigResource, + Provider installationsResource) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.configToDtoMapper = configToDtoMapper; + this.repositoryHandler = repositoryHandler; + this.packagesResource = packagesResource; + this.autoconfigResource = autoconfigResource; + this.installationsResource = installationsResource; + } + + /** + * Returns the hg config. + */ + @GET + @Path("") + @Produces(HgVndMediaType.CONFIG) + @TypeHint(HgConfigDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get() { + + ConfigurationPermissions.read(HgConfig.PERMISSION).check(); + + HgConfig config = repositoryHandler.getConfig(); + + if (config == null) { + config = new HgConfig(); + repositoryHandler.setConfig(config); + } + + return Response.ok(configToDtoMapper.map(config)).build(); + } + + /** + * Modifies the hg config. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(HgVndMediaType.CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:hg\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(HgConfigDto configDto) { + + HgConfig config = dtoToConfigMapper.map(configDto); + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.setConfig(config); + repositoryHandler.storeConfig(); + + return Response.noContent().build(); + } + + @Path("packages") + public HgConfigPackageResource getPackagesResource() { + return packagesResource.get(); + } + + @Path("auto-configuration") + public HgConfigAutoConfigurationResource getAutoConfigurationResource() { + return autoconfigResource.get(); + } + + @Path("installations") + public HgConfigInstallationsResource getInstallationsResource() { + return installationsResource.get(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java new file mode 100644 index 0000000000..98137aebd5 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java @@ -0,0 +1,41 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.HgConfig; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class HgConfigToHgConfigDtoMapper extends BaseMapper { + + @Inject + private UriInfoStore uriInfoStore; + + @AfterMapping + void appendLinks(HgConfig config, @MappingTarget HgConfigDto target) { + Links.Builder linksBuilder = linkingTo().self(self()); + if (ConfigurationPermissions.write(config).isPermitted()) { + linksBuilder.single(link("update", update())); + } + target.add(linksBuilder.build()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("get").parameters().href(); + } + + private String update() { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + return linkBuilder.method("update").parameters().href(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java index 7406505b5a..763977cfa3 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/installer/HgPackageInstaller.java @@ -162,6 +162,7 @@ public class HgPackageInstaller implements Runnable catch (IOException ex) { logger.error("could not downlaod file ".concat(pkg.getUrl()), ex); + file = null; } finally { diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java index 6438f49d4c..41b0f8d205 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/HgConfig.java @@ -48,6 +48,8 @@ import javax.xml.bind.annotation.XmlTransient; public class HgConfig extends RepositoryConfig { + public static final String PERMISSION = "hg"; + /** * Constructs ... * @@ -227,6 +229,6 @@ public class HgConfig extends RepositoryConfig @XmlTransient // Only for permission checks, don't serialize to XML public String getId() { // Don't change this without migrating SCM permission configuration! - return "hg"; + return PERMISSION; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java index 71d6f9f419..357995483d 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java @@ -36,7 +36,11 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.servlet.ServletModule; - +import org.mapstruct.factory.Mappers; +import sonia.scm.api.v2.resources.HgConfigDtoToHgConfigMapper; +import sonia.scm.api.v2.resources.HgConfigInstallationsToDtoMapper; +import sonia.scm.api.v2.resources.HgConfigPackagesToDtoMapper; +import sonia.scm.api.v2.resources.HgConfigToHgConfigDtoMapper; import sonia.scm.installer.HgPackageReader; import sonia.scm.plugin.Extension; import sonia.scm.repository.HgContext; @@ -70,6 +74,11 @@ public class HgServletModule extends ServletModule bind(HgHookManager.class); bind(HgPackageReader.class); + bind(HgConfigDtoToHgConfigMapper.class).to(Mappers.getMapper(HgConfigDtoToHgConfigMapper.class).getClass()); + bind(HgConfigToHgConfigDtoMapper.class).to(Mappers.getMapper(HgConfigToHgConfigDtoMapper.class).getClass()); + bind(HgConfigPackagesToDtoMapper.class).to(Mappers.getMapper(HgConfigPackagesToDtoMapper.class).getClass()); + bind(HgConfigInstallationsToDtoMapper.class); + // bind servlets serve(MAPPING_HOOK).with(HgHookCallbackServlet.class); diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgVndMediaType.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgVndMediaType.java new file mode 100644 index 0000000000..033c5e8361 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgVndMediaType.java @@ -0,0 +1,13 @@ +package sonia.scm.web; + + +public class HgVndMediaType { + private static final String PREFIX = VndMediaType.PREFIX + "hgConfig"; + + public static final String CONFIG = PREFIX + VndMediaType.SUFFIX; + public static final String PACKAGES = PREFIX + "-packages" + VndMediaType.SUFFIX; + public static final String INSTALLATIONS = PREFIX + "-installation" + VndMediaType.SUFFIX; + + private HgVndMediaType() { + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/configuration/shiro.ini new file mode 100644 index 0000000000..fc08bb83ac --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/resources/sonia/scm/configuration/shiro.ini @@ -0,0 +1,9 @@ +[users] +readOnly = secret, reader +writeOnly = secret, writer +readWrite = secret, readerWriter + +[roles] +reader = configuration:read:hg +writer = configuration:write:hg +readerWriter = configuration:*:hg diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java new file mode 100644 index 0000000000..4b66444bbe --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigAutoConfigurationResourceTest.java @@ -0,0 +1,123 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.net.URISyntaxException; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigAutoConfigurationResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @InjectMocks + private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper; + + @Mock + private HgRepositoryHandler repositoryHandler; + + @Mock + private Provider resourceProvider; + + @Before + public void prepareEnvironment() { + HgConfigAutoConfigurationResource resource = + new HgConfigAutoConfigurationResource(dtoToConfigMapper, repositoryHandler); + + when(resourceProvider.get()).thenReturn(resource); + dispatcher.getRegistry().addSingletonResource( + new HgConfigResource(null, null, null, null, + resourceProvider, null)); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldSetDefaultConfigAndInstallHg() throws Exception { + MockHttpResponse response = put(null); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + HgConfig actualConfig = captureConfig(); + assertFalse(actualConfig.isDisabled()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotSetDefaultConfigAndInstallHgWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put(null); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfigAndInstallHg() throws Exception { + MockHttpResponse response = put("{\"disabled\":true}"); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + HgConfig actualConfig = captureConfig(); + assertTrue(actualConfig.isDisabled()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigAndInstallHgWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put("{\"disabled\":true}"); + } + + private MockHttpResponse put(String content) throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/auto-configuration"); + + if (content != null) { + request + .contentType(HgVndMediaType.CONFIG) + .content(content.getBytes()); + } + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private HgConfig captureConfig() { + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(HgConfig.class); + verify(repositoryHandler).doAutoConfiguration(configCaptor.capture()); + return configCaptor.getValue(); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java new file mode 100644 index 0000000000..b95056892e --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigDtoToHgConfigMapperTest.java @@ -0,0 +1,49 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigDtoToHgConfigMapperTest { + + @InjectMocks + private HgConfigDtoToHgConfigMapperImpl mapper; + + @Test + public void shouldMapFields() { + HgConfigDto dto = createDefaultDto(); + HgConfig config = mapper.map(dto); + + assertTrue(config.isDisabled()); + assertEquals("repository/directory", config.getRepositoryDirectory().getPath()); + + assertEquals("ABC", config.getEncoding()); + assertEquals("/etc/hg", config.getHgBinary()); + assertEquals("/py", config.getPythonBinary()); + assertEquals("/etc/", config.getPythonPath()); + assertTrue(config.isShowRevisionInId()); + assertTrue(config.isUseOptimizedBytecode()); + } + + private HgConfigDto createDefaultDto() { + HgConfigDto configDto = new HgConfigDto(); + configDto.setDisabled(true); + configDto.setRepositoryDirectory(new File("repository/directory")); + configDto.setEncoding("ABC"); + configDto.setHgBinary("/etc/hg"); + configDto.setPythonBinary("/py"); + configDto.setPythonPath("/etc/"); + configDto.setShowRevisionInId(true); + configDto.setUseOptimizedBytecode(true); + + return configDto; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java new file mode 100644 index 0000000000..540a5b6757 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java @@ -0,0 +1,118 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.net.URI; +import java.net.URISyntaxException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigInstallationsResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private HgConfigInstallationsToDtoMapper mapper; + + @Mock + private Provider resourceProvider; + + + @Before + public void prepareEnvironment() { + HgConfigInstallationsResource resource = new HgConfigInstallationsResource(mapper); + + when(resourceProvider.get()).thenReturn(resource); + dispatcher.getRegistry().addSingletonResource( + new HgConfigResource(null, null, null, null, + null, resourceProvider)); + + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetHgInstallations() throws Exception { + MockHttpResponse response = get("hg"); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String contentAsString = response.getContentAsString(); + assertThat(contentAsString).contains("{\"paths\":["); + assertThat(contentAsString).contains("hg"); + assertThat(contentAsString).doesNotContain("python"); + + assertThat(contentAsString).contains("\"self\":{\"href\":\"/v2/config/hg/installations/hg"); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetHgInstallationsWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get("hg"); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetPythonInstallations() throws Exception { + MockHttpResponse response = get("python"); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String contentAsString = response.getContentAsString(); + assertThat(contentAsString).contains("{\"paths\":["); + assertThat(contentAsString).contains("python"); + + assertThat(contentAsString).contains("\"self\":{\"href\":\"/v2/config/hg/installations/python"); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetPythonInstallationsWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get("python"); + } + + private MockHttpResponse get(String path) throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + path); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java new file mode 100644 index 0000000000..34048f80b2 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigInstallationsToDtoMapperTest { + + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private HgConfigInstallationsToDtoMapper mapper; + + private URI expectedBaseUri; + + private String expectedPath = "path"; + + @Before + public void init() { + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + expectedPath); + } + + @Test + public void shouldMapFields() { + List installations = Arrays.asList("/hg", "/bin/hg"); + + HgConfigInstallationsDto dto = mapper.map(installations, expectedPath); + + assertThat(dto.getPaths()).isEqualTo(installations); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java new file mode 100644 index 0000000000..044897ad80 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java @@ -0,0 +1,207 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackageReader; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.HgConfigTests.createPackage; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigPackageResourceTest { + + public static final String URI = "/" + HgConfigResource.HG_CONFIG_PATH_V2 + "/packages"; + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = java.net.URI.create("/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private HgConfigPackagesToDtoMapperImpl mapper; + + @Mock + private HgRepositoryHandler repositoryHandler; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private HgPackageReader hgPackageReader; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private AdvancedHttpClient advancedHttpClient; + + @Mock + private Provider hgConfigPackageResourceProvider; + + @Mock + private HgPackage hgPackage; + + @Before + public void prepareEnvironment() { + setupResources(); + + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + + when(hgPackageReader.getPackages().getPackages()).thenReturn(createPackages()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetPackages() throws Exception { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class); + + JsonNode packages = responseJson.get("packages"); + assertThat(packages).isNotNull(); + assertThat(packages).hasSize(2); + + JsonNode package1 = packages.get(0); + assertThat(package1.get("_links")).isNull(); + + JsonNode hgConfigTemplate = package1.get("hgConfigTemplate"); + assertThat(hgConfigTemplate).isNotNull(); + assertThat(hgConfigTemplate.get("_links")).isNull(); + + assertThat(responseString).contains("\"_links\":{\"self\":{\"href\":\"/v2/config/hg/packages"); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetPackagesWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldInstallPackage() throws Exception { + String packgeId = "ourPackage"; + String url = "http://url"; + + setupPackageInstallation(packgeId, url); + when(advancedHttpClient.get(url).request().contentAsStream()) + .thenReturn(new ByteArrayInputStream("mockedFile".getBytes())); + + MockHttpResponse response = put(packgeId); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldHandleFailingInstallation() throws Exception { + String packgeId = "ourPackage"; + String url = "http://url"; + + setupPackageInstallation(packgeId, url); + when(advancedHttpClient.get(url).request().contentAsStream()) + .thenThrow(new IOException("mocked Exception")); + + MockHttpResponse response = put(packgeId); + assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldHandlePackagesThatAreNotFound() throws Exception { + String packageId = "this-package-does-not-ex"; + when(hgPackageReader.getPackage(packageId)).thenReturn(null); + MockHttpResponse response = put(packageId); + assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotInstallPackageWhenNotAuthorized() throws Exception { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put("don-t-care"); + } + + private List createPackages() { + return Arrays.asList(createPackage(), new HgPackage()); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get(URI); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put(String pckgId) throws URISyntaxException { + String packgeIdParam = ""; + if (pckgId != null) { + packgeIdParam = "/" + pckgId; + } + MockHttpRequest request = MockHttpRequest.put(URI + packgeIdParam); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private void setupResources() { + HgConfigPackageResource hgConfigPackageResource = + new HgConfigPackageResource(hgPackageReader, advancedHttpClient, repositoryHandler, mapper); + + when(hgConfigPackageResourceProvider.get()).thenReturn(hgConfigPackageResource); + dispatcher.getRegistry().addSingletonResource( + new HgConfigResource(null, null, null, + hgConfigPackageResourceProvider, null, null)); + } + + private void setupPackageInstallation(String packgeId, String url) throws IOException { + when(hgPackage.getId()).thenReturn(packgeId); + when(hgPackageReader.getPackage(packgeId)).thenReturn(hgPackage); + when(repositoryHandler.getConfig()).thenReturn(new HgConfig()); + when(hgPackage.getHgConfigTemplate()).thenReturn(new HgConfig()); + when(hgPackage.getUrl()).thenReturn(url); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java new file mode 100644 index 0000000000..671d9fb7e1 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java @@ -0,0 +1,69 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.installer.HgPackage; +import sonia.scm.installer.HgPackages; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.HgConfigTests.assertEqualsPackage; +import static sonia.scm.api.v2.resources.HgConfigTests.createPackage; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigPackagesToDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private HgConfigPackagesToDtoMapperImpl mapper; + + private URI expectedBaseUri; + + @Before + public void init() { + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/packages"); + } + + @Test + public void shouldMapFields() { + HgPackages hgPackages = new HgPackages(); + hgPackages.setPackages(createPackages()); + + HgConfigPackagesDto dto = mapper.map(hgPackages); + + assertThat(dto.getPackages()).hasSize(2); + + HgConfigPackagesDto.HgConfigPackageDto hgPackageDto1 = dto.getPackages().get(0); + assertEqualsPackage(hgPackageDto1); + + HgConfigPackagesDto.HgConfigPackageDto hgPackageDto2 = dto.getPackages().get(1); + // Just verify a random field + assertThat(hgPackageDto2.getId()).isNull(); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + } + + + private List createPackages() { + return Arrays.asList(createPackage(), new HgPackage()); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java new file mode 100644 index 0000000000..11a0fb55f9 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java @@ -0,0 +1,170 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.web.HgVndMediaType; + +import javax.inject.Provider; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class HgConfigResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @InjectMocks + private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private HgConfigToHgConfigDtoMapperImpl configToDtoMapper; + + @Mock + private HgRepositoryHandler repositoryHandler; + + @Mock + private Provider packagesResource; + + @Mock + private Provider autoconfigResource; + + @Mock + private Provider installationsResource; + + @Before + public void prepareEnvironment() { + HgConfig gitConfig = createConfiguration(); + when(repositoryHandler.getConfig()).thenReturn(gitConfig); + HgConfigResource gitConfigResource = + new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource, + autoconfigResource, installationsResource); + dispatcher.getRegistry().addSingletonResource(gitConfigResource); + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetHgConfig() throws URISyntaxException, IOException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class); + + assertTrue(responseString.contains("\"disabled\":false")); + assertTrue(responseJson.get("repositoryDirectory").asText().endsWith("repository/directory")); + assertTrue(responseString.contains("\"self\":{\"href\":\"/v2/config/hg")); + assertTrue(responseString.contains("\"update\":{\"href\":\"/v2/config/hg")); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetHgConfigEvenWhenItsEmpty() throws URISyntaxException { + when(repositoryHandler.getConfig()).thenReturn(null); + + MockHttpResponse response = get(); + String responseString = response.getContentAsString(); + + assertTrue(responseString.contains("\"disabled\":false")); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetHgConfigWithoutUpdateLink() throws URISyntaxException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertFalse(response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config/hg")); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:read:hg]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfig() throws URISyntaxException { + MockHttpResponse response = put(); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:write:hg]"); + + put(); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + HgConfigResource.HG_CONFIG_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + HgConfigResource.HG_CONFIG_PATH_V2) + .contentType(HgVndMediaType.CONFIG) + .content("{\"disabled\":true}".getBytes()); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private HgConfig createConfiguration() { + HgConfig config = new HgConfig(); + config.setDisabled(false); + config.setRepositoryDirectory(new File("repository/directory")); + return config; + } + +} + diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java new file mode 100644 index 0000000000..4167321344 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigTests.java @@ -0,0 +1,69 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.installer.HgPackage; +import sonia.scm.repository.HgConfig; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +class HgConfigTests { + + private HgConfigTests() { + } + + static HgConfig createConfiguration() { + HgConfig config = new HgConfig(); + config.setDisabled(true); + config.setRepositoryDirectory(new File("repository/directory")); + + config.setEncoding("ABC"); + config.setHgBinary("/etc/hg"); + config.setPythonBinary("/py"); + config.setPythonPath("/etc/"); + config.setShowRevisionInId(true); + config.setUseOptimizedBytecode(true); + + return config; + } + + static void assertEqualsConfiguration(HgConfigDto dto) { + assertTrue(dto.isDisabled()); + assertEquals("repository/directory", dto.getRepositoryDirectory().getPath()); + + assertEquals("ABC", dto.getEncoding()); + assertEquals("/etc/hg", dto.getHgBinary()); + assertEquals("/py", dto.getPythonBinary()); + assertEquals("/etc/", dto.getPythonPath()); + assertTrue(dto.isShowRevisionInId()); + assertTrue(dto.isUseOptimizedBytecode()); + } + + static HgPackage createPackage() { + HgPackage hgPackage= new HgPackage(); + hgPackage.setArch("arch"); + hgPackage.setId("1"); + hgPackage.setHgVersion("2"); + hgPackage.setPlatform("someOs"); + hgPackage.setPythonVersion("3"); + hgPackage.setSize(4); + hgPackage.setUrl("https://package"); + hgPackage.setHgConfigTemplate(createConfiguration()); + return hgPackage; + } + + static void assertEqualsPackage(HgConfigPackagesDto.HgConfigPackageDto dto) { + assertEquals("arch", dto.getArch()); + assertEquals("1", dto.getId()); + assertEquals("2", dto.getHgVersion()); + assertEquals("someOs", dto.getPlatform()); + assertEquals("3", dto.getPythonVersion()); + assertEquals(4, dto.getSize()); + assertEquals("https://package", dto.getUrl()); + + assertEqualsConfiguration(dto.getHgConfigTemplate()); + assertTrue(dto.getHgConfigTemplate().getLinks().isEmpty()); + } + +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java new file mode 100644 index 0000000000..a12e95926c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java @@ -0,0 +1,78 @@ +package sonia.scm.api.v2.resources; + +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.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.HgConfig; + +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.HgConfigTests.assertEqualsConfiguration; +import static sonia.scm.api.v2.resources.HgConfigTests.createConfiguration; + +@RunWith(MockitoJUnitRunner.class) +public class HgConfigToHgConfigDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private HgConfigToHgConfigDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() { + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapFields() { + HgConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:hg")).thenReturn(true); + HgConfigDto dto = mapper.map(config); + + assertEqualsConfiguration(dto); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); + } + + @Test + public void shouldMapFieldsWithoutUpdate() { + HgConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:hg")).thenReturn(false); + HgConfigDto dto = mapper.map(config); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertFalse(dto.getLinks().hasLink("update")); + } +} diff --git a/scm-plugins/scm-svn-plugin/pom.xml b/scm-plugins/scm-svn-plugin/pom.xml index 9d37319394..701569e620 100644 --- a/scm-plugins/scm-svn-plugin/pom.xml +++ b/scm-plugins/scm-svn-plugin/pom.xml @@ -9,9 +9,7 @@ 2.0.0-SNAPSHOT - sonia.scm.plugins scm-svn-plugin - 2.0.0-SNAPSHOT scm-svn-plugin smp https://bitbucket.org/sdorra/scm-manager @@ -19,13 +17,6 @@ - - javax.servlet - javax.servlet-api - ${servlet.version} - provided - - sonia.svnkit svnkit @@ -44,15 +35,6 @@ ${svnkit.version} - - - - sonia.scm - scm-test - 2.0.0-SNAPSHOT - test - - diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java new file mode 100644 index 0000000000..548944a49c --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDto.java @@ -0,0 +1,28 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import sonia.scm.repository.Compatibility; + +import java.io.File; + +@NoArgsConstructor +@Getter +@Setter +public class SvnConfigDto extends HalRepresentation { + + private boolean disabled; + private File repositoryDirectory; + + private boolean enabledGZip; + private Compatibility compatibility; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapper.java new file mode 100644 index 0000000000..f996c49248 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapper.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import sonia.scm.repository.SvnConfig; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class SvnConfigDtoToSvnConfigMapper { + public abstract SvnConfig map(SvnConfigDto dto); +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java new file mode 100644 index 0000000000..b12785dca9 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigResource.java @@ -0,0 +1,92 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.SvnConfig; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.web.SvnVndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +/** + * RESTful Web Service Resource to manage the configuration of the svn plugin. + */ +@Path(SvnConfigResource.SVN_CONFIG_PATH_V2) +public class SvnConfigResource { + + static final String SVN_CONFIG_PATH_V2 = "v2/config/svn"; + private final SvnConfigDtoToSvnConfigMapper dtoToConfigMapper; + private final SvnConfigToSvnConfigDtoMapper configToDtoMapper; + private final SvnRepositoryHandler repositoryHandler; + + @Inject + public SvnConfigResource(SvnConfigDtoToSvnConfigMapper dtoToConfigMapper, SvnConfigToSvnConfigDtoMapper configToDtoMapper, + SvnRepositoryHandler repositoryHandler) { + this.dtoToConfigMapper = dtoToConfigMapper; + this.configToDtoMapper = configToDtoMapper; + this.repositoryHandler = repositoryHandler; + } + + /** + * Returns the svn config. + */ + @GET + @Path("") + @Produces(SvnVndMediaType.SVN_CONFIG) + @TypeHint(SvnConfigDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:svn\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get() { + + SvnConfig config = repositoryHandler.getConfig(); + + if (config == null) { + config = new SvnConfig(); + repositoryHandler.setConfig(config); + } + + ConfigurationPermissions.read(config).check(); + + return Response.ok(configToDtoMapper.map(config)).build(); + } + + /** + * Modifies the svn config. + * + * @param configDto new configuration object + */ + @PUT + @Path("") + @Consumes(SvnVndMediaType.SVN_CONFIG) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:svn\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response update(SvnConfigDto configDto) { + + SvnConfig config = dtoToConfigMapper.map(configDto); + + ConfigurationPermissions.write(config).check(); + + repositoryHandler.setConfig(config); + repositoryHandler.storeConfig(); + + return Response.noContent().build(); + } + +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java new file mode 100644 index 0000000000..a71d75151d --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java @@ -0,0 +1,41 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.repository.SvnConfig; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper { + + @Inject + private UriInfoStore uriInfoStore; + + @AfterMapping + void appendLinks(SvnConfig config, @MappingTarget SvnConfigDto target) { + Links.Builder linksBuilder = linkingTo().self(self()); + if (ConfigurationPermissions.write(config).isPermitted()) { + linksBuilder.single(link("update", update())); + } + target.add(linksBuilder.build()); + } + + private String self() { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), SvnConfigResource.class); + return linkBuilder.method("get").parameters().href(); + } + + private String update() { + LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), SvnConfigResource.class); + return linkBuilder.method("update").parameters().href(); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java index 73b4f39219..5fe5c0815d 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/SvnConfig.java @@ -48,6 +48,9 @@ import javax.xml.bind.annotation.XmlTransient; public class SvnConfig extends RepositoryConfig { + @SuppressWarnings("WeakerAccess") // This might be needed for permission checking + public static final String PERMISSION = "svn"; + /** * Method description * @@ -112,6 +115,6 @@ public class SvnConfig extends RepositoryConfig @XmlTransient // Only for permission checks, don't serialize to XML public String getId() { // Don't change this without migrating SCM permission configuration! - return "svn"; + return PERMISSION; } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java index 813f7fb0e6..9b5c8ae556 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java @@ -36,15 +36,16 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.servlet.ServletModule; - +import org.mapstruct.factory.Mappers; +import sonia.scm.api.v2.resources.SvnConfigDtoToSvnConfigMapper; +import sonia.scm.api.v2.resources.SvnConfigToSvnConfigDtoMapper; import sonia.scm.plugin.Extension; -import sonia.scm.web.filter.AuthenticationFilter; - -//~--- JDK imports ------------------------------------------------------------ import java.util.HashMap; import java.util.Map; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -72,6 +73,9 @@ public class SvnServletModule extends ServletModule filter(PATTERN_SVN).through(SvnBasicAuthenticationFilter.class); filter(PATTERN_SVN).through(SvnPermissionFilter.class); + bind(SvnConfigDtoToSvnConfigMapper.class).to(Mappers.getMapper(SvnConfigDtoToSvnConfigMapper.class).getClass()); + bind(SvnConfigToSvnConfigDtoMapper.class).to(Mappers.getMapper(SvnConfigToSvnConfigDtoMapper.class).getClass()); + Map parameters = new HashMap(); parameters.put(PARAMETER_SVN_PARENTPATH, diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnVndMediaType.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnVndMediaType.java new file mode 100644 index 0000000000..1e294df1fe --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnVndMediaType.java @@ -0,0 +1,8 @@ +package sonia.scm.web; + +public class SvnVndMediaType { + public static final String SVN_CONFIG = VndMediaType.PREFIX + "svnConfig" + VndMediaType.SUFFIX; + + private SvnVndMediaType() { + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java new file mode 100644 index 0000000000..b111d0229f --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigDtoToSvnConfigMapperTest.java @@ -0,0 +1,42 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.Compatibility; +import sonia.scm.repository.SvnConfig; + +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class SvnConfigDtoToSvnConfigMapperTest { + + @InjectMocks + private SvnConfigDtoToSvnConfigMapperImpl mapper; + + @Test + public void shouldMapFields() { + SvnConfigDto dto = createDefaultDto(); + SvnConfig config = mapper.map(dto); + + assertTrue(config.isDisabled()); + assertEquals("repository/directory", config.getRepositoryDirectory().getPath()); + + assertEquals(Compatibility.PRE15, config.getCompatibility()); + assertTrue(config.isEnabledGZip()); + } + + private SvnConfigDto createDefaultDto() { + SvnConfigDto configDto = new SvnConfigDto(); + configDto.setDisabled(true); + configDto.setRepositoryDirectory(new File("repository/directory")); + configDto.setCompatibility(Compatibility.PRE15); + configDto.setEnabledGZip(true); + + return configDto; + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java new file mode 100644 index 0000000000..de4d654910 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java @@ -0,0 +1,158 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.SvnConfig; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.web.SvnVndMediaType; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +@SubjectAware( + configuration = "classpath:sonia/scm/configuration/shiro.ini", + password = "secret" +) +@RunWith(MockitoJUnitRunner.class) +public class SvnConfigResourceTest { + + @Rule + public ShiroRule shiro = new ShiroRule(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + + @InjectMocks + private SvnConfigDtoToSvnConfigMapperImpl dtoToConfigMapper; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private SvnConfigToSvnConfigDtoMapperImpl configToDtoMapper; + + @Mock + private SvnRepositoryHandler repositoryHandler; + + @Before + public void prepareEnvironment() { + SvnConfig gitConfig = createConfiguration(); + when(repositoryHandler.getConfig()).thenReturn(gitConfig); + SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); + dispatcher.getRegistry().addSingletonResource(gitConfigResource); + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetSvnConfig() throws URISyntaxException, IOException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + String responseString = response.getContentAsString(); + ObjectNode responseJson = new ObjectMapper().readValue(responseString, ObjectNode.class); + + assertTrue(responseString.contains("\"disabled\":false")); + assertTrue(responseJson.get("repositoryDirectory").asText().endsWith("repository/directory")); + assertTrue(responseString.contains("\"self\":{\"href\":\"/v2/config/svn")); + assertTrue(responseString.contains("\"update\":{\"href\":\"/v2/config/svn")); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldGetSvnConfigEvenWhenItsEmpty() throws URISyntaxException, IOException { + when(repositoryHandler.getConfig()).thenReturn(null); + + MockHttpResponse response = get(); + String responseString = response.getContentAsString(); + + assertTrue(responseString.contains("\"disabled\":false")); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldGetSvnConfigWithoutUpdateLink() throws URISyntaxException { + MockHttpResponse response = get(); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertFalse(response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config/svn")); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:read:svn]"); + + get(); + } + + @Test + @SubjectAware(username = "writeOnly") + public void shouldUpdateConfig() throws URISyntaxException { + MockHttpResponse response = put(); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + } + + @Test + @SubjectAware(username = "readOnly") + public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException { + thrown.expectMessage("Subject does not have permission [configuration:write:svn]"); + + put(); + } + + private MockHttpResponse get() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + SvnConfigResource.SVN_CONFIG_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private MockHttpResponse put() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.put("/" + SvnConfigResource.SVN_CONFIG_PATH_V2) + .contentType(SvnVndMediaType.SVN_CONFIG) + .content("{\"disabled\":true}".getBytes()); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + return response; + } + + private SvnConfig createConfiguration() { + SvnConfig config = new SvnConfig(); + config.setDisabled(false); + config.setRepositoryDirectory(new File("repository/directory")); + return config; + } + +} + diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java new file mode 100644 index 0000000000..de7b0ecd31 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java @@ -0,0 +1,95 @@ +package sonia.scm.api.v2.resources; + +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.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import sonia.scm.repository.Compatibility; +import sonia.scm.repository.SvnConfig; + +import java.io.File; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SvnConfigToSvnConfigDtoMapperTest { + + private URI baseUri = URI.create("http://example.com/base/"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private UriInfoStore uriInfoStore; + + @InjectMocks + private SvnConfigToSvnConfigDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + + @Before + public void init() { + when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + expectedBaseUri = baseUri.resolve(SvnConfigResource.SVN_CONFIG_PATH_V2); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapFields() { + SvnConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:svn")).thenReturn(true); + SvnConfigDto dto = mapper.map(config); + + assertTrue(dto.isDisabled()); + assertEquals("repository/directory", dto.getRepositoryDirectory().getPath()); + + assertEquals(Compatibility.PRE15, dto.getCompatibility()); + assertTrue(dto.isEnabledGZip()); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); + } + + @Test + public void shouldMapFieldsWithoutUpdate() { + SvnConfig config = createConfiguration(); + + when(subject.isPermitted("configuration:write:svn")).thenReturn(false); + SvnConfigDto dto = mapper.map(config); + + assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); + assertFalse(dto.getLinks().hasLink("update")); + } + + private SvnConfig createConfiguration() { + SvnConfig config = new SvnConfig(); + config.setDisabled(true); + config.setRepositoryDirectory(new File("repository/directory")); + + config.setCompatibility(Compatibility.PRE15); + config.setEnabledGZip(true); + + return config; + } + +} diff --git a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini new file mode 100644 index 0000000000..7e4233b540 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -0,0 +1,9 @@ +[users] +readOnly = secret, reader +writeOnly = secret, writer +readWrite = secret, readerWriter + +[roles] +reader = configuration:read:svn +writer = configuration:write:svn +readerWriter = configuration:*:svn diff --git a/scm-server/pom.xml b/scm-server/pom.xml index 64172d6673..c8d004cec1 100644 --- a/scm-server/pom.xml +++ b/scm-server/pom.xml @@ -141,7 +141,6 @@ org.apache.maven.plugins maven-assembly-plugin - 2.3 src/main/assembly/scm-server-app.xml diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 086552e864..9bc5019a1d 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -31,19 +31,20 @@ junit junit - ${junit.version} + compile com.github.sdorra shiro-unit - test + + compile org.mockito mockito-all - ${mokito.version} + compile diff --git a/scm-ui/src/groups/types/Group.js b/scm-ui/src/groups/types/Group.js index 420d724784..57cbcae3d8 100644 --- a/scm-ui/src/groups/types/Group.js +++ b/scm-ui/src/groups/types/Group.js @@ -14,5 +14,7 @@ export type Group = Collection & { members: string[], _embedded: { members: Member[] - } + }, + creationDate?: string, + lastModified?: string }; diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 054ce3d445..d0003c1f7c 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -75,33 +75,15 @@ jjwt 0.4 - - - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - + + com.fasterxml.jackson.module jackson-module-jaxb-annotations ${jackson.version} - + com.fasterxml.jackson.jaxrs jackson-jaxrs-base @@ -117,48 +99,37 @@ jackson-datatype-jsr310 ${jackson.version} + - + org.jboss.resteasy resteasy-jaxrs - ${resteasy.version} org.jboss.resteasy resteasy-jaxb-provider - ${resteasy.version} org.jboss.resteasy resteasy-jackson2-provider - ${resteasy.version} org.jboss.resteasy resteasy-multipart-provider - ${resteasy.version} org.jboss.resteasy resteasy-guice - ${resteasy.version} - - - - org.jboss.resteasy - resteasy-servlet-initializer - ${resteasy.version} - de.otto.edison - edison-hal - 2.0.1 + org.jboss.resteasy + resteasy-servlet-initializer @@ -168,7 +139,7 @@ guice-multibindings ${guice.version} - + @@ -256,14 +227,6 @@ ${mustache.version} - - - - com.webcohesion.enunciate - enunciate-core-annotations - ${enunciate.version} - - @@ -315,8 +278,7 @@ - - + com.github.sdorra shiro-unit @@ -387,20 +349,12 @@ org.projectlombok lombok - 1.16.18 provided - - org.mapstruct - mapstruct-jdk8 - ${org.mapstruct.version} - - org.mapstruct mapstruct-processor - ${org.mapstruct.version} provided @@ -557,10 +511,8 @@ target/scm-it default 2.53.1 - 2.9.1 1.0 0.8.17 - 3.1.3.Final Tomcat e1 javascript:S3827 @@ -687,29 +639,29 @@ - + - + selenium - + - + org.apache.httpcomponents httpclient 4.3.2 test - + - + - + org.apache.maven.plugins maven-failsafe-plugin @@ -734,7 +686,7 @@ - + org.eclipse.jetty jetty-maven-plugin @@ -772,7 +724,7 @@ - + org.codehaus.mojo selenium-maven-plugin @@ -793,26 +745,25 @@ post-integration-test stop-server - + - + - + - + doc - + - + org.apache.maven.plugins maven-resources-plugin - 2.6 copy-enunciate-configuration @@ -822,7 +773,7 @@ ${project.build.directory} - + src/main/doc true @@ -830,16 +781,15 @@ **/enunciate.xml - - + + - + com.webcohesion.enunciate enunciate-maven-plugin - ${enunciate.version} @@ -872,11 +822,10 @@ - + org.apache.maven.plugins maven-assembly-plugin - 2.3 src/main/doc/assembly.xml @@ -891,7 +840,7 @@ - + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java index 13d9ff5351..4c9620564b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigDto.java @@ -33,6 +33,7 @@ public class ConfigDto extends HalRepresentation { private String pluginUrl; private long loginAttemptLimitTimeout; private boolean enabledXsrfProtection; + private String defaultNamespaceStrategy; @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/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index 5dc479b17e..bf3a11fb9c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -14,9 +14,7 @@ import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; /** * RESTful Web Service Resource to manage the configuration. @@ -46,7 +44,7 @@ public class ConfigResource { @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 global config"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:read:global\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) public Response get() { @@ -61,19 +59,19 @@ public class ConfigResource { /** * Modifies the global scm config. * - * @param configDto new global scm configuration as DTO + * @param configDto new configuration object */ @PUT @Path("") @Consumes(VndMediaType.CONFIG) @StatusCodes({ - @ResponseCode(code = 201, condition = "update success"), + @ResponseCode(code = 204, condition = "update success"), @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), - @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to update the global config"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"configuration:write:global\" privilege"), @ResponseCode(code = 500, condition = "internal server error") }) @TypeHint(TypeHint.NO_CONTENT.class) - public Response update(ConfigDto configDto, @Context UriInfo uriInfo) { + public Response update(ConfigDto configDto) { // This *could* be moved to ScmConfiguration or ScmConfigurationUtil classes. // But to where to check? load() or store()? Leave it for now, SCMv1 legacy that can be cleaned up later. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java index 3944be81b8..cb05da6568 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java @@ -24,6 +24,7 @@ public class GroupDto extends HalRepresentation { private List members; @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package protected HalRepresentation add(Links links) { return super.add(links); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapper.java index 59e56feb11..ac9dba4e41 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapper.java @@ -15,13 +15,11 @@ import static de.otto.edison.hal.Links.linkingTo; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") @Mapper -public abstract class ScmConfigurationToConfigDtoMapper { +public abstract class ScmConfigurationToConfigDtoMapper extends BaseMapper { @Inject private ResourceLinks resourceLinks; - public abstract ConfigDto map(ScmConfiguration config); - @AfterMapping void appendLinks(ScmConfiguration config, @MappingTarget ConfigDto target) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.config().self()); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java index ad1e71368e..525d5814e1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigDtoToScmConfigurationMapperTest.java @@ -51,6 +51,7 @@ public class ConfigDtoToScmConfigurationMapperTest { assertEquals("https://plug.ins" , config.getPluginUrl()); assertEquals(40 , config.getLoginAttemptLimitTimeout()); assertTrue(config.isEnabledXsrfProtection()); + assertEquals("username", config.getDefaultNamespaceStrategy()); } private ConfigDto createDefaultDto() { @@ -75,6 +76,7 @@ public class ConfigDtoToScmConfigurationMapperTest { configDto.setPluginUrl("https://plug.ins"); configDto.setLoginAttemptLimitTimeout(40); configDto.setEnabledXsrfProtection(true); + configDto.setDefaultNamespaceStrategy("username"); return configDto; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 5365e9fbde..ff97ca332b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -21,9 +21,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.MockitoAnnotations.initMocks; @SubjectAware( @@ -72,7 +70,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "writeOnly") - public void shouldGetConfigOnlyWhenAuthorized() throws URISyntaxException { + public void shouldNotGetConfigWhenNotAuthorized() throws URISyntaxException { MockHttpRequest request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); MockHttpResponse response = new MockHttpResponse(); @@ -96,8 +94,7 @@ public class ConfigResourceTest { request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); response = new MockHttpResponse(); - dispatcher.invoke(request, response); - assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); @@ -105,7 +102,7 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readOnly") - public void shouldUpdateConfigOnlyWhenAuthorized() throws URISyntaxException, IOException { + public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException { URL url = Resources.getResource("sonia/scm/api/v2/config-test-update.json"); byte[] configJson = Resources.toByteArray(url); MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java index ff20516e5a..cdbd9ae344 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmConfigurationToConfigDtoMapperTest.java @@ -81,6 +81,7 @@ public class ScmConfigurationToConfigDtoMapperTest { assertEquals("pluginurl" , dto.getPluginUrl()); assertEquals(2 , dto.getLoginAttemptLimitTimeout()); assertTrue(dto.isEnabledXsrfProtection()); + assertEquals("username", dto.getDefaultNamespaceStrategy()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); @@ -120,6 +121,7 @@ public class ScmConfigurationToConfigDtoMapperTest { config.setPluginUrl("pluginurl"); config.setLoginAttemptLimitTimeout(2); config.setEnabledXsrfProtection(true); + config.setDefaultNamespaceStrategy("username"); return config; } diff --git a/scm-webapp/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-webapp/src/test/resources/sonia/scm/configuration/shiro.ini index 8647142b19..fc2eac14f1 100644 --- a/scm-webapp/src/test/resources/sonia/scm/configuration/shiro.ini +++ b/scm-webapp/src/test/resources/sonia/scm/configuration/shiro.ini @@ -4,6 +4,6 @@ writeOnly = secret, writer readWrite = secret, readerWriter [roles] -reader = configuration:read -writer = configuration:write -readerWriter = configuration:* +reader = configuration:read:global +writer = configuration:write:global +readerWriter = configuration:*:global diff --git a/scm.iml b/scm.iml index 4ed0077c7f..20f9f4b564 100644 --- a/scm.iml +++ b/scm.iml @@ -12,10 +12,11 @@ - - - - - + + + + + + \ No newline at end of file