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