merge with branch feature/repository_config_v2_endpoint

This commit is contained in:
Philipp Czora
2018-08-08 15:41:29 +02:00
72 changed files with 2948 additions and 198 deletions

162
pom.xml
View File

@@ -121,28 +121,31 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mokito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.10.0</version>
<scope>test</scope>
</dependency>
@@ -154,6 +157,8 @@
<version>9aadeeb</version>
<!-- Don't ship this dependency with the app -->
<optional>true</optional>
<!-- Don't inherit this dependency! -->
<scope>provided</scope>
</dependency>
</dependencies>
@@ -182,6 +187,134 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-core-annotations</artifactId>
<version>${enunciate.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>de.otto.edison</groupId>
<artifactId>edison-hal</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<!-- rest api -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-guice</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-servlet-initializer</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>${jaxrs.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mokito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -194,6 +327,22 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.3</version>
</plugin>
<plugin>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-maven-plugin</artifactId>
<version>${enunciate.version}</version>
</plugin>
</plugins>
</pluginManagement>
@@ -294,7 +443,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
@@ -554,10 +702,12 @@
<servlet.version>3.0.1</servlet.version>
<jaxrs.version>2.0.1</jaxrs.version>
<resteasy.version>3.1.3.Final</resteasy.version>
<jersey-client.version>1.19.4</jersey-client.version>
<enunciate.version>2.9.1</enunciate.version>
<jackson.version>2.8.6</jackson.version>
<guice.version>4.0</guice.version>
<!-- event bus -->
<legman.version>1.3.0</legman.version>

View File

@@ -79,23 +79,47 @@
<artifactId>guice-throwingproviders</artifactId>
<version>${guice.version}</version>
</dependency>
<!-- rest api -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>${jaxrs.version}</version>
</dependency>
<!-- json -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- Hypermedia -->
<dependency>
<groupId>de.otto.edison</groupId>
<artifactId>edison-hal</artifactId>
</dependency>
<!-- (DTO) mapping -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
</dependency>
<!-- rest documentation -->
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-core-annotations</artifactId>
</dependency>
<!-- event bus -->

View File

@@ -5,12 +5,12 @@ import org.mapstruct.Mapping;
import java.time.Instant;
abstract class BaseMapper<T, D extends HalRepresentation> {
public abstract class BaseMapper<T, D extends HalRepresentation> {
@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);
}
}

View File

@@ -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<E, D extends HalRepresentation> {
private final String collectionName;
private final BaseMapper<E, D> mapper;
protected CollectionToDtoMapper(String collectionName, BaseMapper<E, D> mapper) {
this.collectionName = collectionName;
this.mapper = mapper;
}
public HalRepresentation map(Collection<E> collection) {
List<D> 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();
}

View File

@@ -23,12 +23,13 @@ import java.util.Arrays;
* .create();
* </pre>
*/
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<Call> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -38,7 +38,7 @@
<scope>provided</scope>
</dependency>
<!-- annotation processor -->
<!-- annotation processors -->
<dependency>
<groupId>sonia.scm</groupId>
@@ -46,6 +46,20 @@
<version>2.0.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- Annotation processor for DTO mappers-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<scope>provided</scope>
</dependency>
<!-- Annotation processor for getter, setters, constructors, etc. -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- test scope -->
@@ -56,6 +70,19 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -140,6 +167,96 @@
</build>
</profile>
<profile>
<id>doc</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-enunciate-configuration</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resource>
<directory>src/main/doc</directory>
<filtering>true</filtering>
<includes>
<include>**/enunciate.xml</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>docs</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
<configuration>
<configFile>${project.build.directory}/enunciate.xml</configFile>
<docsDir>${project.build.directory}</docsDir>
<docsSubdir>restdocs</docsSubdir>
</configuration>
<dependencies>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-top</artifactId>
<version>${enunciate.version}</version>
<exclusions>
<exclusion>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-swagger</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-lombok</artifactId>
<version>${enunciate.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/main/doc/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -9,9 +9,7 @@
<version>2.0.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-git-plugin</artifactId>
<version>2.0.0-SNAPSHOT</version>
<name>scm-git-plugin</name>
<packaging>smp</packaging>
<url>https://bitbucket.org/sdorra/scm-manager</url>
@@ -19,13 +17,6 @@
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>sonia.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
@@ -50,15 +41,6 @@
<version>2.6</version>
</dependency>
<!-- test scope -->
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-test</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- create test jar -->

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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<GitConfig, GitConfigDto> {
@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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,8 @@
package sonia.scm.web;
public class GitVndMediaType {
public static final String GIT_CONFIG = VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX;
private GitVndMediaType() {
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -9,9 +9,7 @@
<version>2.0.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-hg-plugin</artifactId>
<version>2.0.0-SNAPSHOT</version>
<name>scm-hg-plugin</name>
<packaging>smp</packaging>
<url>https://bitbucket.org/sdorra/scm-manager</url>
@@ -30,15 +28,6 @@
</exclusion>
</exclusions>
</dependency>
<!-- test scope -->
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-test</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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<String> paths;
public HgConfigInstallationsDto(Links links, List<String> paths) {
super(links);
this.paths = paths;
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<HgConfigPackageDto> 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;
}
}

View File

@@ -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<HgPackage> packages;
HgPackagesNonIterable(HgPackages hgPackages) {
this.packages = hgPackages.getPackages();
}
}
}

View File

@@ -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<HgConfigPackageResource> packagesResource;
private final Provider<HgConfigAutoConfigurationResource> autoconfigResource;
private final Provider<HgConfigInstallationsResource> installationsResource;
@Inject
public HgConfigResource(HgConfigDtoToHgConfigMapper dtoToConfigMapper, HgConfigToHgConfigDtoMapper configToDtoMapper,
HgRepositoryHandler repositoryHandler, Provider<HgConfigPackageResource> packagesResource,
Provider<HgConfigAutoConfigurationResource> autoconfigResource,
Provider<HgConfigInstallationsResource> 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();
}
}

View File

@@ -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<HgConfig, HgConfigDto> {
@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();
}
}

View File

@@ -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
{

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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() {
}
}

View File

@@ -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

View File

@@ -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<HgConfigAutoConfigurationResource> 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<HgConfig> configCaptor = ArgumentCaptor.forClass(HgConfig.class);
verify(repositoryHandler).doAutoConfiguration(configCaptor.capture());
return configCaptor.getValue();
}
}

View File

@@ -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;
}
}

View File

@@ -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<HgConfigInstallationsResource> 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;
}
}

View File

@@ -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<String> 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());
}
}

View File

@@ -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<HgConfigPackageResource> 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<HgPackage> 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);
}
}

View File

@@ -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<HgPackage> createPackages() {
return Arrays.asList(createPackage(), new HgPackage());
}
}

View File

@@ -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<HgConfigPackageResource> packagesResource;
@Mock
private Provider<HgConfigAutoConfigurationResource> autoconfigResource;
@Mock
private Provider<HgConfigInstallationsResource> 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;
}
}

View File

@@ -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());
}
}

View File

@@ -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"));
}
}

View File

@@ -9,9 +9,7 @@
<version>2.0.0-SNAPSHOT</version>
</parent>
<groupId>sonia.scm.plugins</groupId>
<artifactId>scm-svn-plugin</artifactId>
<version>2.0.0-SNAPSHOT</version>
<name>scm-svn-plugin</name>
<packaging>smp</packaging>
<url>https://bitbucket.org/sdorra/scm-manager</url>
@@ -19,13 +17,6 @@
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>sonia.svnkit</groupId>
<artifactId>svnkit</artifactId>
@@ -44,15 +35,6 @@
<version>${svnkit.version}</version>
</dependency>
<!-- test scope -->
<dependency>
<groupId>sonia.scm</groupId>
<artifactId>scm-test</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- create test jar -->

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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<SvnConfig, SvnConfigDto> {
@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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, String> parameters = new HashMap<String, String>();
parameters.put(PARAMETER_SVN_PARENTPATH,

View File

@@ -0,0 +1,8 @@
package sonia.scm.web;
public class SvnVndMediaType {
public static final String SVN_CONFIG = VndMediaType.PREFIX + "svnConfig" + VndMediaType.SUFFIX;
private SvnVndMediaType() {
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -141,7 +141,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.3</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/scm-server-app.xml</descriptor>

View File

@@ -31,19 +31,20 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.sdorra</groupId>
<artifactId>shiro-unit</artifactId>
<scope>test</scope>
<!-- scm-test is test scoped in other modules and they might need shiro unit for their tests. -->
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>${mokito.version}</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@@ -14,5 +14,7 @@ export type Group = Collection & {
members: string[],
_embedded: {
members: Member[]
}
},
creationDate?: string,
lastModified?: string
};

View File

@@ -75,33 +75,15 @@
<artifactId>jjwt</artifactId>
<version>0.4</version>
</dependency>
<!-- json -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- json -->
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-base</artifactId>
@@ -117,48 +99,37 @@
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- rest api -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-guice</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-servlet-initializer</artifactId>
<version>${resteasy.version}</version>
</dependency>
<dependency>
<groupId>de.otto.edison</groupId>
<artifactId>edison-hal</artifactId>
<version>2.0.1</version>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-servlet-initializer</artifactId>
</dependency>
<!-- injection -->
@@ -168,7 +139,7 @@
<artifactId>guice-multibindings</artifactId>
<version>${guice.version}</version>
</dependency>
<!-- event bus -->
<dependency>
@@ -256,14 +227,6 @@
<version>${mustache.version}</version>
</dependency>
<!-- rest documentation -->
<dependency>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-core-annotations</artifactId>
<version>${enunciate.version}</version>
</dependency>
<!-- test scope -->
<dependency>
@@ -315,8 +278,7 @@
</dependency>
<!-- core plugins -->
<dependency>
<groupId>com.github.sdorra</groupId>
<artifactId>shiro-unit</artifactId>
@@ -387,20 +349,12 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
</dependency>
@@ -557,10 +511,8 @@
<scm.home>target/scm-it</scm.home>
<environment.profile>default</environment.profile>
<selenium.version>2.53.1</selenium.version>
<enunciate.version>2.9.1</enunciate.version>
<wagon.version>1.0</wagon.version>
<mustache.version>0.8.17</mustache.version>
<resteasy.version>3.1.3.Final</resteasy.version>
<netbeans.hint.deploy.server>Tomcat</netbeans.hint.deploy.server>
<sonar.issue.ignore.multicriteria>e1</sonar.issue.ignore.multicriteria>
<sonar.issue.ignore.multicriteria.e1.ruleKey>javascript:S3827</sonar.issue.ignore.multicriteria.e1.ruleKey>
@@ -687,29 +639,29 @@
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>selenium</id>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
@@ -734,7 +686,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
@@ -772,7 +724,7 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>selenium-maven-plugin</artifactId>
@@ -793,26 +745,25 @@
<phase>post-integration-test</phase>
<goals>
<goal>stop-server</goal>
</goals>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>doc</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>copy-enunciate-configuration</id>
@@ -822,7 +773,7 @@
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
<resources>
<resources>
<resource>
<directory>src/main/doc</directory>
<filtering>true</filtering>
@@ -830,16 +781,15 @@
<include>**/enunciate.xml</include>
</includes>
</resource>
</resources>
</configuration>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.webcohesion.enunciate</groupId>
<artifactId>enunciate-maven-plugin</artifactId>
<version>${enunciate.version}</version>
<executions>
<execution>
<goals>
@@ -872,11 +822,10 @@
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.3</version>
<configuration>
<descriptors>
<descriptor>src/main/doc/assembly.xml</descriptor>
@@ -891,7 +840,7 @@
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>

View File

@@ -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

View File

@@ -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.

View File

@@ -24,6 +24,7 @@ public class GroupDto extends HalRepresentation {
private List<String> members;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {
return super.add(links);
}

View File

@@ -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<ScmConfiguration, ConfigDto> {
@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());

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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

11
scm.iml
View File

@@ -12,10 +12,11 @@
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-library:1.3" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-all:1.10.19" level="project" />
<orderEntry type="library" name="Maven: com.github.cloudogu:ces-build-lib:9aadeeb" level="project" />
<orderEntry type="library" name="Maven: com.cloudbees:groovy-cps:1.21" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:11.0.1" level="project" />
<orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.groovy:groovy-all:2.4.11" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.10.0" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: com.github.cloudogu:ces-build-lib:9aadeeb" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: com.cloudbees:groovy-cps:1.21" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:guava:11.0.1" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
<orderEntry type="library" scope="PROVIDED" name="Maven: org.codehaus.groovy:groovy-all:2.4.11" level="project" />
</component>
</module>