Introduce config rest endpoint for default git branch

This commit is contained in:
René Pfeuffer
2018-12-12 14:12:55 +01:00
parent 8ecb64a8aa
commit bc8c776821
10 changed files with 278 additions and 14 deletions

View File

@@ -9,13 +9,17 @@ import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.web.GitVndMediaType;
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.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
/**
* RESTful Web Service Resource to manage the configuration of the git plugin.
*/
@@ -26,13 +30,15 @@ public class GitConfigResource {
private final GitConfigDtoToGitConfigMapper dtoToConfigMapper;
private final GitConfigToGitConfigDtoMapper configToDtoMapper;
private final GitRepositoryHandler repositoryHandler;
private final Provider<GitRepositoryConfigResource> gitRepositoryConfigResource;
@Inject
public GitConfigResource(GitConfigDtoToGitConfigMapper dtoToConfigMapper, GitConfigToGitConfigDtoMapper configToDtoMapper,
GitRepositoryHandler repositoryHandler) {
GitRepositoryHandler repositoryHandler, Provider<GitRepositoryConfigResource> gitRepositoryConfigResource) {
this.dtoToConfigMapper = dtoToConfigMapper;
this.configToDtoMapper = configToDtoMapper;
this.repositoryHandler = repositoryHandler;
this.gitRepositoryConfigResource = gitRepositoryConfigResource;
}
/**
@@ -88,4 +94,9 @@ public class GitConfigResource {
return Response.noContent().build();
}
@Path("{namespace}/{name}")
public GitRepositoryConfigResource getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) {
return gitRepositoryConfigResource.get();
}
}

View File

@@ -0,0 +1,24 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@SuppressWarnings("squid:S2160") // there is no proper semantic for equals on this dto
public class GitRepositoryConfigDto extends HalRepresentation {
private String defaultBranch;
@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,43 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import sonia.scm.plugin.Extension;
import sonia.scm.web.JsonEnricherBase;
import sonia.scm.web.JsonEnricherContext;
import javax.inject.Inject;
import javax.inject.Provider;
import static java.util.Collections.singletonMap;
import static sonia.scm.web.VndMediaType.REPOSITORY;
@Extension
public class GitRepositoryConfigEnricher extends JsonEnricherBase {
private final Provider<ScmPathInfoStore> scmPathInfoStore;
@Inject
public GitRepositoryConfigEnricher(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
super(objectMapper);
this.scmPathInfoStore = scmPathInfoStore;
}
@Override
public void enrich(JsonEnricherContext context) {
if (resultHasMediaType(REPOSITORY, context)) {
JsonNode repositoryNode = context.getResponseEntity();
String namespace = repositoryNode.get("namespace").asText();
String name = repositoryNode.get("name").asText();
String newPullRequest = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
.method("getRepositoryConfig")
.parameters(namespace, name)
.href();
JsonNode newPullRequestNode = createObject(singletonMap("href", value(newPullRequest)));
addPropertyNode(repositoryNode.get("_links"), "configuration", newPullRequestNode);
}
}
}

View File

@@ -0,0 +1,52 @@
package sonia.scm.api.v2.resources;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.web.GitVndMediaType;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
public class GitRepositoryConfigResource {
private final GitRepositoryConfigToGitRepositoryConfigDtoMapper repositoryConfigToDtoMapper;
private final RepositoryManager repositoryManager;
private final ConfigurationStoreFactory configurationStoreFactory;
@Inject
public GitRepositoryConfigResource(GitRepositoryConfigToGitRepositoryConfigDtoMapper repositoryConfigToDtoMapper, RepositoryManager repositoryManager, ConfigurationStoreFactory configurationStoreFactory) {
this.repositoryConfigToDtoMapper = repositoryConfigToDtoMapper;
this.repositoryManager = repositoryManager;
this.configurationStoreFactory = configurationStoreFactory;
}
@GET
@Path("/")
@Produces(GitVndMediaType.GIT_REPOSITORY_CONFIG)
public Response getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
Repository repository = repositoryManager.get(namespaceAndName);
if (repository == null) {
throw notFound(entity(namespaceAndName));
}
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = configurationStoreFactory.withType(GitRepositoryConfig.class).withName("gitConfig").forRepository(repository).build();
GitRepositoryConfig config = repositoryConfigStore.get();
if (config == null) {
config = new GitRepositoryConfig();
}
GitRepositoryConfigDto dto = repositoryConfigToDtoMapper.map(config, repository);
return Response.ok(dto).build();
}
}

View File

@@ -0,0 +1,45 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryPermissions;
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 GitRepositoryConfigToGitRepositoryConfigDtoMapper {
@Inject
private ScmPathInfoStore scmPathInfoStore;
public abstract GitRepositoryConfigDto map(GitRepositoryConfig config, @Context Repository repository);
@AfterMapping
void appendLinks(GitRepositoryConfig config, @MappingTarget GitRepositoryConfigDto target, @Context Repository repository) {
Links.Builder linksBuilder = linkingTo().self(self());
if (RepositoryPermissions.modify(repository).isPermitted()) {
linksBuilder.single(link("update", update()));
}
target.add(linksBuilder.build());
}
private String self() {
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class);
return linkBuilder.method("get").parameters().href();
}
private String update() {
LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), GitConfigResource.class);
return linkBuilder.method("update").parameters().href();
}
}

View File

@@ -0,0 +1,14 @@
package sonia.scm.repository;
public class GitRepositoryConfig {
private String defaultBranch;
public String getDefaultBranch() {
return defaultBranch;
}
public void setDefaultBranch(String defaultBranch) {
this.defaultBranch = defaultBranch;
}
}

View File

@@ -40,6 +40,7 @@ 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.api.v2.resources.GitRepositoryConfigToGitRepositoryConfigDtoMapper;
import sonia.scm.plugin.Extension;
import sonia.scm.repository.GitWorkdirFactory;
import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
@@ -65,6 +66,7 @@ public class GitServletModule extends ServletModule
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
bind(GitRepositoryConfigToGitRepositoryConfigDtoMapper.class).to(Mappers.getMapper(GitRepositoryConfigToGitRepositoryConfigDtoMapper.class).getClass());
bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
}

View File

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

View File

@@ -1,7 +1,5 @@
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;
@@ -18,18 +16,25 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
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 com.google.inject.util.Providers.of;
import static junit.framework.TestCase.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@SubjectAware(
@@ -55,30 +60,45 @@ public class GitConfigResourceTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private ScmPathInfoStore scmPathInfoStore;
@Mock
private RepositoryManager repositoryManager;
@InjectMocks
private GitConfigToGitConfigDtoMapperImpl configToDtoMapper;
@InjectMocks
private GitRepositoryConfigToGitRepositoryConfigDtoMapperImpl repositoryConfigToDtoMapper;
@Mock
private GitRepositoryHandler repositoryHandler;
@Mock(answer = Answers.CALLS_REAL_METHODS)
private ConfigurationStoreFactory configurationStoreFactory;
@Mock
private ConfigurationStore configurationStore;
@Before
public void prepareEnvironment() {
GitConfig gitConfig = createConfiguration();
when(repositoryHandler.getConfig()).thenReturn(gitConfig);
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler);
GitRepositoryConfigResource gitRepositoryConfigResource = new GitRepositoryConfigResource(repositoryConfigToDtoMapper, repositoryManager, configurationStoreFactory);
GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, of(gitRepositoryConfigResource));
dispatcher.getRegistry().addSingletonResource(gitConfigResource);
when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri);
}
@Before
public void initConfigStore() {
when(configurationStoreFactory.getStore(any())).thenReturn(configurationStore);
}
@Test
@SubjectAware(username = "readWrite")
public void shouldGetGitConfig() throws URISyntaxException, IOException {
public void shouldGetGitConfig() throws URISyntaxException {
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(responseString.contains("\"gcExpression\":\"valid Git GC Cron Expression\""));
@@ -88,7 +108,7 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "readWrite")
public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException, IOException {
public void shouldGetGitConfigEvenWhenItsEmpty() throws URISyntaxException {
when(repositoryHandler.getConfig()).thenReturn(null);
MockHttpResponse response = get();
@@ -124,12 +144,64 @@ public class GitConfigResourceTest {
@Test
@SubjectAware(username = "readOnly")
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException {
public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException {
thrown.expectMessage("Subject does not have permission [configuration:write:git]");
put();
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldReadDefaultRepositoryConfig() throws URISyntaxException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertThat(response.getContentAsString())
.contains("\"defaultBranch\":null")
.contains("self")
.contains("update");
}
@Test
@SubjectAware(username = "readOnly")
public void shouldNotHaveUpdateLinkForReadOnlyUser() throws URISyntaxException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertThat(response.getContentAsString())
.contains("\"defaultBranch\":null")
.contains("self")
.doesNotContain("update");
}
@Test
@SubjectAware(username = "writeOnly")
public void shouldReadStoredRepositoryConfig() throws URISyntaxException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig();
gitRepositoryConfig.setDefaultBranch("test");
when(configurationStore.get()).thenReturn(gitRepositoryConfig);
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2 + "/space/X");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertThat(response.getContentAsString())
.contains("\"defaultBranch\":\"test\"");
}
private MockHttpResponse get() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/" + GitConfigResource.GIT_CONFIG_PATH_V2);
MockHttpResponse response = new MockHttpResponse();
@@ -153,6 +225,4 @@ public class GitConfigResourceTest {
config.setDisabled(false);
return config;
}
}

View File

@@ -1,6 +1,6 @@
[users]
readOnly = secret, reader
writeOnly = secret, writer
readOnly = secret, reader, repoRead
writeOnly = secret, writer, repoWrite
readWrite = secret, readerWriter
admin = secret, admin
@@ -9,3 +9,5 @@ reader = configuration:read:git
writer = configuration:write:git
readerWriter = configuration:*:git
admin = *
repoRead = repository:read:*
repoWrite = repository:modify:*