Clone the repository
- git clone {repository._links.httpProtocol.href}
+ git clone {href}
Create a new repository
@@ -30,7 +33,7 @@ class ProtocolInformation extends React.Component {
git commit -m "added readme"
- git remote add origin {repository._links.httpProtocol.href}
+ git remote add origin {href}
git push -u origin master
@@ -39,7 +42,7 @@ class ProtocolInformation extends React.Component {
Push an existing repository
- git remote add origin {repository._links.httpProtocol.href}
+ git remote add origin {href}
git push -u origin master
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java
new file mode 100644
index 0000000000..665be19788
--- /dev/null
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java
@@ -0,0 +1,64 @@
+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 com.google.inject.util.Providers;
+import org.junit.Rule;
+import org.junit.Test;
+import sonia.scm.web.JsonEnricherContext;
+import sonia.scm.web.VndMediaType;
+
+import javax.ws.rs.core.MediaType;
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
+public class GitConfigInIndexResourceTest {
+
+ @Rule
+ public final ShiroRule shiroRule = new ShiroRule();
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ObjectNode root = objectMapper.createObjectNode();
+ private final GitConfigInIndexResource gitConfigInIndexResource;
+
+ public GitConfigInIndexResourceTest() {
+ root.put("_links", objectMapper.createObjectNode());
+ ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
+ pathInfoStore.set(() -> URI.create("/"));
+ gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
+ }
+
+ @Test
+ @SubjectAware(username = "admin", password = "secret")
+ public void admin() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ gitConfigInIndexResource.enrich(context);
+
+ assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText());
+ }
+
+ @Test
+ @SubjectAware(username = "readOnly", password = "secret")
+ public void user() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ gitConfigInIndexResource.enrich(context);
+
+ assertFalse(root.get("_links").iterator().hasNext());
+ }
+
+ @Test
+ public void anonymous() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ gitConfigInIndexResource.enrich(context);
+
+ assertFalse(root.get("_links").iterator().hasNext());
+ }
+}
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java
index d6e6ac98d8..78db8ae686 100644
--- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java
+++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java
@@ -1,3 +1,4 @@
+
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
@@ -33,14 +34,17 @@
package sonia.scm.repository.spi;
-//~--- non-JDK imports --------------------------------------------------------
-
+import com.google.common.io.Files;
import org.junit.Test;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitConstants;
import sonia.scm.repository.Modifications;
+import java.io.File;
+import java.io.IOException;
+
+import static java.nio.charset.Charset.defaultCharset;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -48,8 +52,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
-//~--- JDK imports ------------------------------------------------------------
-
/**
* Unit tests for {@link GitLogCommand}.
*
@@ -72,6 +74,8 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", result.getChangesets().get(1).getId());
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(2).getId());
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(3).getId());
+ assertEquals("master", result.getBranchName());
+ assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
// set default branch and fetch again
repository.setProperty(GitConstants.PROPERTY_DEFAULT_BRANCH, "test-branch");
@@ -79,10 +83,12 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
result = createCommand().getChangesets(new LogCommandRequest());
assertNotNull(result);
+ assertEquals("test-branch", result.getBranchName());
assertEquals(3, result.getTotal());
assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", result.getChangesets().get(0).getId());
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(1).getId());
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(2).getId());
+ assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
}
@Test
@@ -210,6 +216,32 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c2.getId());
}
+ @Test
+ public void shouldFindDefaultBranchFromHEAD() throws Exception {
+ setRepositoryHeadReference("ref: refs/heads/test-branch");
+
+ ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest());
+
+ assertEquals("test-branch", changesets.getBranchName());
+ }
+
+ @Test
+ public void shouldFindMasterBranchWhenHEADisNoRef() throws Exception {
+ setRepositoryHeadReference("592d797cd36432e591416e8b2b98154f4f163411");
+
+ ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest());
+
+ assertEquals("master", changesets.getBranchName());
+ }
+
+ private void setRepositoryHeadReference(String s) throws IOException {
+ Files.write(s, repositoryHeadReferenceFile(), defaultCharset());
+ }
+
+ private File repositoryHeadReferenceFile() {
+ return new File(repositoryDirectory, "HEAD");
+ }
+
private GitLogCommand createCommand()
{
return new GitLogCommand(createContext(), repository);
diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
index 36226edd7d..5d30a000f2 100644
--- a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
+++ b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
@@ -2,8 +2,10 @@
readOnly = secret, reader
writeOnly = secret, writer
readWrite = secret, readerWriter
+admin = secret, admin
[roles]
reader = configuration:read:git
writer = configuration:write:git
readerWriter = configuration:*:git
+admin = *
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java
new file mode 100644
index 0000000000..3de79b2f81
--- /dev/null
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java
@@ -0,0 +1,40 @@
+package sonia.scm.api.v2.resources;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import sonia.scm.config.ConfigurationPermissions;
+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.INDEX;
+
+@Extension
+public class HgConfigInIndexResource extends JsonEnricherBase {
+
+ private final Provider scmPathInfoStore;
+
+ @Inject
+ public HgConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) {
+ super(objectMapper);
+ this.scmPathInfoStore = scmPathInfoStore;
+ }
+
+ @Override
+ public void enrich(JsonEnricherContext context) {
+ if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
+ String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class)
+ .method("get")
+ .parameters()
+ .href();
+
+ JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl)));
+
+ addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode);
+ }
+ }
+}
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java
index 68d6913962..e9de7f7471 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/HgLogCommand.java
@@ -132,7 +132,11 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
List changesets = on(repository).rev(start + ":"
+ end).execute();
- result = new ChangesetPagingResult(total, changesets);
+ if (request.getBranch() == null) {
+ result = new ChangesetPagingResult(total, changesets);
+ } else {
+ result = new ChangesetPagingResult(total, changesets, request.getBranch());
+ }
}
else
{
diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java
index 6466eb6d11..89164a8d80 100644
--- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java
+++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/AbstractChangesetCommand.java
@@ -216,10 +216,7 @@ public abstract class AbstractChangesetCommand extends AbstractCommand
String branch = in.textUpTo('\n');
- if (!BRANCH_DEFAULT.equals(branch))
- {
- changeset.getBranches().add(branch);
- }
+ changeset.getBranches().add(branch);
String p1 = readId(in, changeset, PROPERTY_PARENT1_REVISION);
diff --git a/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js b/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js
index 28c1e53a07..03fc41450a 100644
--- a/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js
+++ b/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js
@@ -1,5 +1,6 @@
//@flow
import React from "react";
+import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types";
type Props = {
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component {
render() {
const { repository } = this.props;
- if (!repository._links.httpProtocol) {
+ const href = repositories.getProtocolLinkByType(repository, "http");
+ if (!href) {
return null;
}
return (
Clone the repository
- hg clone {repository._links.httpProtocol.href}
+ hg clone {href}
Create a new repository
@@ -26,7 +28,7 @@ class ProtocolInformation extends React.Component {
echo "[paths]" > .hg/hgrc
- echo "default = {repository._links.httpProtocol.href}" > .hg/hgrc
+ echo "default = {href}" > .hg/hgrc
echo "# {repository.name}" > README.md
@@ -44,7 +46,7 @@ class ProtocolInformation extends React.Component {
# add the repository url as default to your .hg/hgrc e.g:
- default = {repository._links.httpProtocol.href}
+ default = {href}
# push to remote repository
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java
new file mode 100644
index 0000000000..27ab74932c
--- /dev/null
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java
@@ -0,0 +1,64 @@
+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 com.google.inject.util.Providers;
+import org.junit.Rule;
+import org.junit.Test;
+import sonia.scm.web.JsonEnricherContext;
+import sonia.scm.web.VndMediaType;
+
+import javax.ws.rs.core.MediaType;
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
+public class HgConfigInIndexResourceTest {
+
+ @Rule
+ public final ShiroRule shiroRule = new ShiroRule();
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ObjectNode root = objectMapper.createObjectNode();
+ private final HgConfigInIndexResource hgConfigInIndexResource;
+
+ public HgConfigInIndexResourceTest() {
+ root.put("_links", objectMapper.createObjectNode());
+ ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
+ pathInfoStore.set(() -> URI.create("/"));
+ hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
+ }
+
+ @Test
+ @SubjectAware(username = "admin", password = "secret")
+ public void admin() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ hgConfigInIndexResource.enrich(context);
+
+ assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText());
+ }
+
+ @Test
+ @SubjectAware(username = "readOnly", password = "secret")
+ public void user() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ hgConfigInIndexResource.enrich(context);
+
+ assertFalse(root.get("_links").iterator().hasNext());
+ }
+
+ @Test
+ public void anonymous() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ hgConfigInIndexResource.enrich(context);
+
+ assertFalse(root.get("_links").iterator().hasNext());
+ }
+}
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java
index 99e9fc191a..29fc46ed57 100644
--- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java
+++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java
@@ -88,6 +88,21 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase
result.getChangesets().get(2).getId());
}
+ @Test
+ public void testGetDefaultBranchInfo() {
+ LogCommandRequest request = new LogCommandRequest();
+
+ request.setPath("a.txt");
+
+ ChangesetPagingResult result = createComamnd().getChangesets(request);
+
+ assertNotNull(result);
+ assertEquals(1,
+ result.getChangesets().get(0).getBranches().size());
+ assertEquals("default",
+ result.getChangesets().get(0).getBranches().get(0));
+ }
+
@Test
public void testGetAllWithLimit() {
LogCommandRequest request = new LogCommandRequest();
diff --git a/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
index fc08bb83ac..d8083a04c9 100644
--- a/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
+++ b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
@@ -2,8 +2,10 @@
readOnly = secret, reader
writeOnly = secret, writer
readWrite = secret, readerWriter
+admin = secret, admin
[roles]
reader = configuration:read:hg
writer = configuration:write:hg
readerWriter = configuration:*:hg
+admin = *
diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java
new file mode 100644
index 0000000000..5ee1de3169
--- /dev/null
+++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java
@@ -0,0 +1,40 @@
+package sonia.scm.api.v2.resources;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import sonia.scm.config.ConfigurationPermissions;
+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.INDEX;
+
+@Extension
+public class SvnConfigInIndexResource extends JsonEnricherBase {
+
+ private final Provider scmPathInfoStore;
+
+ @Inject
+ public SvnConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) {
+ super(objectMapper);
+ this.scmPathInfoStore = scmPathInfoStore;
+ }
+
+ @Override
+ public void enrich(JsonEnricherContext context) {
+ if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
+ String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class)
+ .method("get")
+ .parameters()
+ .href();
+
+ JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl)));
+
+ addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode);
+ }
+ }
+}
diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java
index 7cc78180ff..4352299ed5 100644
--- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java
+++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java
@@ -32,122 +32,45 @@
package sonia.scm.web;
-//~--- non-JDK imports --------------------------------------------------------
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import sonia.scm.filter.GZipFilter;
+import sonia.scm.filter.GZipFilterConfig;
+import sonia.scm.filter.GZipResponseWrapper;
import sonia.scm.repository.Repository;
import sonia.scm.repository.SvnRepositoryHandler;
import sonia.scm.repository.spi.ScmProviderHttpServlet;
+import sonia.scm.util.WebUtil;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
-//~--- JDK imports ------------------------------------------------------------
-
-/**
- *
- * @author Sebastian Sdorra
- */
-public class SvnGZipFilter extends GZipFilter implements ScmProviderHttpServlet
-{
+class SvnGZipFilter implements ScmProviderHttpServlet {
private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class);
private final SvnRepositoryHandler handler;
private final ScmProviderHttpServlet delegate;
- //~--- constructors ---------------------------------------------------------
+ private GZipFilterConfig config = new GZipFilterConfig();
- /**
- * Constructs ...
- *
- *
- * @param handler
- */
- public SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate)
- {
+ SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) {
this.handler = handler;
this.delegate = delegate;
- }
-
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param filterConfig
- *
- * @throws ServletException
- */
- @Override
- public void init(FilterConfig filterConfig) throws ServletException
- {
- super.init(filterConfig);
- getConfig().setBufferResponse(false);
- }
-
- /**
- * Method description
- *
- *
- * @param request
- * @param response
- * @param chain
- *
- * @throws IOException
- * @throws ServletException
- */
- @Override
- protected void doFilter(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain)
- throws IOException, ServletException
- {
- if (handler.getConfig().isEnabledGZip())
- {
- if (logger.isTraceEnabled())
- {
- logger.trace("encode svn request with gzip");
- }
-
- super.doFilter(request, response, chain);
- }
- else
- {
- if (logger.isTraceEnabled())
- {
- logger.trace("skip gzip encoding");
- }
-
- chain.doFilter(request, response);
- }
+ config.setBufferResponse(false);
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException {
- if (handler.getConfig().isEnabledGZip())
- {
- if (logger.isTraceEnabled())
- {
- logger.trace("encode svn request with gzip");
- }
-
- super.doFilter(request, response, (servletRequest, servletResponse) -> delegate.service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, repository));
- }
- else
- {
- if (logger.isTraceEnabled())
- {
- logger.trace("skip gzip encoding");
- }
-
+ if (handler.getConfig().isEnabledGZip() && WebUtil.isGzipSupported(request)) {
+ logger.trace("compress svn response with gzip");
+ GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response, config);
+ delegate.service(request, wrappedResponse, repository);
+ wrappedResponse.finishResponse();
+ } else {
+ logger.trace("skip gzip encoding");
delegate.service(request, response, repository);
}
}
diff --git a/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js b/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js
index ccff4118ba..0ba195887f 100644
--- a/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js
+++ b/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js
@@ -1,5 +1,6 @@
//@flow
import React from "react";
+import { repositories } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types";
type Props = {
@@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component {
render() {
const { repository } = this.props;
- if (!repository._links.httpProtocol) {
+ const href = repositories.getProtocolLinkByType(repository, "http");
+ if (!href) {
return null;
}
return (
Checkout the repository
- svn checkout {repository._links.httpProtocol.href}
+ svn checkout {href}
);
diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java
new file mode 100644
index 0000000000..8b87b57c6c
--- /dev/null
+++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java
@@ -0,0 +1,64 @@
+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 com.google.inject.util.Providers;
+import org.junit.Rule;
+import org.junit.Test;
+import sonia.scm.web.JsonEnricherContext;
+import sonia.scm.web.VndMediaType;
+
+import javax.ws.rs.core.MediaType;
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
+public class SvnConfigInIndexResourceTest {
+
+ @Rule
+ public final ShiroRule shiroRule = new ShiroRule();
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ObjectNode root = objectMapper.createObjectNode();
+ private final SvnConfigInIndexResource svnConfigInIndexResource;
+
+ public SvnConfigInIndexResourceTest() {
+ root.put("_links", objectMapper.createObjectNode());
+ ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
+ pathInfoStore.set(() -> URI.create("/"));
+ svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper);
+ }
+
+ @Test
+ @SubjectAware(username = "admin", password = "secret")
+ public void admin() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ svnConfigInIndexResource.enrich(context);
+
+ assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText());
+ }
+
+ @Test
+ @SubjectAware(username = "readOnly", password = "secret")
+ public void user() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ svnConfigInIndexResource.enrich(context);
+
+ assertFalse(root.get("_links").iterator().hasNext());
+ }
+
+ @Test
+ public void anonymous() {
+ JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root);
+
+ svnConfigInIndexResource.enrich(context);
+
+ assertFalse(root.get("_links").iterator().hasNext());
+ }
+}
diff --git a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
index 7e4233b540..fe84723e0a 100644
--- a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
+++ b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini
@@ -2,8 +2,10 @@
readOnly = secret, reader
writeOnly = secret, writer
readWrite = secret, readerWriter
+admin = secret, admin
[roles]
reader = configuration:read:svn
writer = configuration:write:svn
readerWriter = configuration:*:svn
+admin = *
diff --git a/scm-ui-components/package.json b/scm-ui-components/package.json
index 6fe782506e..73c20625bd 100644
--- a/scm-ui-components/package.json
+++ b/scm-ui-components/package.json
@@ -4,7 +4,8 @@
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap",
- "link": "lerna exec -- yarn link"
+ "link": "lerna exec -- yarn link",
+ "unlink": "lerna exec --no-bail -- yarn unlink"
},
"devDependencies": {
"lerna": "^3.2.1"
diff --git a/scm-ui-components/packages/ui-components/src/Help.js b/scm-ui-components/packages/ui-components/src/Help.js
new file mode 100644
index 0000000000..965d16f145
--- /dev/null
+++ b/scm-ui-components/packages/ui-components/src/Help.js
@@ -0,0 +1,39 @@
+//@flow
+import React from "react";
+import injectSheet from "react-jss";
+import classNames from "classnames";
+
+const styles = {
+ img: {
+ display: "block"
+ },
+ q: {
+ float: "left",
+ paddingLeft: "3px",
+ float: "right"
+ }
+};
+
+type Props = {
+ message: string,
+ classes: any
+};
+
+class Help extends React.Component {
+ render() {
+ const { message, classes } = this.props;
+ const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
+ return (
+
+
+
+ );
+ }
+}
+
+export default injectSheet(styles)(Help);
diff --git a/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js b/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js
new file mode 100644
index 0000000000..b5d049e68d
--- /dev/null
+++ b/scm-ui-components/packages/ui-components/src/LabelWithHelpIcon.js
@@ -0,0 +1,46 @@
+//@flow
+import React from "react";
+import { Help } from "./index";
+
+type Props = {
+ label: string,
+ helpText?: string
+};
+
+class LabelWithHelpIcon extends React.Component {
+ renderLabel = () => {
+ const label = this.props.label;
+ if (label) {
+ return ;
+ }
+ return "";
+ };
+
+ renderHelp = () => {
+ const helpText = this.props.helpText;
+ if (helpText) {
+ return (
+
+
+
+ );
+ } else return null;
+ };
+
+ renderLabelWithHelpIcon = () => {
+ if (this.props.label) {
+ return (
+
+
{this.renderLabel()}
+ {this.renderHelp()}
+
+ );
+ } else return null;
+ };
+
+ render() {
+ return this.renderLabelWithHelpIcon();
+ }
+}
+
+export default LabelWithHelpIcon;
diff --git a/scm-ui-components/packages/ui-components/src/apiclient.js b/scm-ui-components/packages/ui-components/src/apiclient.js
index 7bf3232260..0b57abeada 100644
--- a/scm-ui-components/packages/ui-components/src/apiclient.js
+++ b/scm-ui-components/packages/ui-components/src/apiclient.js
@@ -32,7 +32,7 @@ export function createUrl(url: string) {
if (url.indexOf("/") !== 0) {
urlWithStartingSlash = "/" + urlWithStartingSlash;
}
- return `${contextPath}/api/rest/v2${urlWithStartingSlash}`;
+ return `${contextPath}/api/v2${urlWithStartingSlash}`;
}
class ApiClient {
diff --git a/scm-ui-components/packages/ui-components/src/apiclient.test.js b/scm-ui-components/packages/ui-components/src/apiclient.test.js
index 7bbb3b0119..deb22a3b54 100644
--- a/scm-ui-components/packages/ui-components/src/apiclient.test.js
+++ b/scm-ui-components/packages/ui-components/src/apiclient.test.js
@@ -9,7 +9,7 @@ describe("create url", () => {
});
it("should add prefix for api", () => {
- expect(createUrl("/users")).toBe("/api/rest/v2/users");
- expect(createUrl("users")).toBe("/api/rest/v2/users");
+ expect(createUrl("/users")).toBe("/api/v2/users");
+ expect(createUrl("users")).toBe("/api/v2/users");
});
});
diff --git a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js
index 1770e07807..e5c04eb613 100644
--- a/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js
+++ b/scm-ui-components/packages/ui-components/src/forms/AddEntryToTableField.js
@@ -9,7 +9,8 @@ type Props = {
disabled: boolean,
buttonLabel: string,
fieldLabel: string,
- errorMessage: string
+ errorMessage: string,
+ helpText?: string
};
type State = {
@@ -25,7 +26,13 @@ class AddEntryToTableField extends React.Component {
}
render() {
- const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
+ const {
+ disabled,
+ buttonLabel,
+ fieldLabel,
+ errorMessage,
+ helpText
+ } = this.props;
return (
{
value={this.state.entryToAdd}
onReturnPressed={this.appendEntry}
disabled={disabled}
+ helpText={helpText}
/>
void,
- disabled?: boolean
+ disabled?: boolean,
+ helpText?: string
};
class Checkbox extends React.Component {
onCheckboxChange = (event: SyntheticInputEvent) => {
@@ -14,9 +16,20 @@ class Checkbox extends React.Component {
}
};
+ renderHelp = () => {
+ const helpText = this.props.helpText;
+ if (helpText) {
+ return (
+
+
+
+ );
+ } else return null;
+ };
+
render() {
return (
-
+
);
}
diff --git a/scm-ui-components/packages/ui-components/src/forms/InputField.js b/scm-ui-components/packages/ui-components/src/forms/InputField.js
index 6f87683939..79b71298f8 100644
--- a/scm-ui-components/packages/ui-components/src/forms/InputField.js
+++ b/scm-ui-components/packages/ui-components/src/forms/InputField.js
@@ -1,6 +1,7 @@
//@flow
import React from "react";
import classNames from "classnames";
+import { LabelWithHelpIcon } from "../index";
type Props = {
label?: string,
@@ -12,7 +13,8 @@ type Props = {
onReturnPressed?: () => void,
validationError: boolean,
errorMessage: string,
- disabled?: boolean
+ disabled?: boolean,
+ helpText?: string
};
class InputField extends React.Component
{
@@ -33,15 +35,6 @@ class InputField extends React.Component {
this.props.onChange(event.target.value);
};
- renderLabel = () => {
- const label = this.props.label;
- if (label) {
- return ;
- }
- return "";
- };
-
-
handleKeyPress = (event: SyntheticKeyboardEvent) => {
const onReturnPressed = this.props.onReturnPressed;
if (!onReturnPressed) {
@@ -60,7 +53,9 @@ class InputField extends React.Component {
value,
validationError,
errorMessage,
- disabled
+ disabled,
+ label,
+ helpText
} = this.props;
const errorView = validationError ? "is-danger" : "";
const helper = validationError ? (
@@ -70,7 +65,7 @@ class InputField extends React.Component {
);
return (
- {this.renderLabel()}
+
{
diff --git a/scm-ui-components/packages/ui-components/src/forms/Select.js b/scm-ui-components/packages/ui-components/src/forms/Select.js
index 184359cc11..880b375999 100644
--- a/scm-ui-components/packages/ui-components/src/forms/Select.js
+++ b/scm-ui-components/packages/ui-components/src/forms/Select.js
@@ -1,5 +1,7 @@
//@flow
import React from "react";
+import classNames from "classnames";
+import { LabelWithHelpIcon } from "../index";
export type SelectItem = {
value: string,
@@ -10,7 +12,9 @@ type Props = {
label?: string,
options: SelectItem[],
value?: SelectItem,
- onChange: string => void
+ onChange: string => void,
+ loading?: boolean,
+ helpText?: string
};
class Select extends React.Component
{
@@ -28,21 +32,18 @@ class Select extends React.Component {
this.props.onChange(event.target.value);
};
- renderLabel = () => {
- const label = this.props.label;
- if (label) {
- return ;
- }
- return "";
- };
-
render() {
- const { options, value } = this.props;
+ const { options, value, label, helpText, loading } = this.props;
+ const loadingClass = loading ? "is-loading" : "";
+
return (
- {this.renderLabel()}
-
+
+
);
diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js
index 0745312344..7656bc270c 100644
--- a/scm-ui/src/config/components/form/ProxySettings.js
+++ b/scm-ui/src/config/components/form/ProxySettings.js
@@ -42,6 +42,7 @@ class ProxySettings extends React.Component
{
label={t("proxy-settings.enable-proxy")}
onChange={this.handleEnableProxyChange}
disabled={!hasUpdatePermission}
+ helpText={t("help.enableProxyHelpText")}
/>
{
value={proxyPassword}
type="password"
disabled={!enableProxy || !hasUpdatePermission}
+ helpText={t("help.proxyPasswordHelpText")}
/>
{
removeLabel={t("admin-settings.remove-group-button")}
onRemove={this.removeEntry}
disabled={disabled}
+ helpText={t("help.adminGroupsHelpText")}
/>
);
}
diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js
index d1f35e8424..a077a6d5d2 100644
--- a/scm-ui/src/config/components/table/AdminUserTable.js
+++ b/scm-ui/src/config/components/table/AdminUserTable.js
@@ -22,6 +22,7 @@ class AdminUserTable extends React.Component {
removeLabel={t("admin-settings.remove-user-button")}
onRemove={this.removeEntry}
disabled={disabled}
+ helpText={t("help.adminUsersHelpText")}
/>
);
}
diff --git a/scm-ui/src/config/components/table/ArrayConfigTable.js b/scm-ui/src/config/components/table/ArrayConfigTable.js
index d724caa6ae..21d53261f4 100644
--- a/scm-ui/src/config/components/table/ArrayConfigTable.js
+++ b/scm-ui/src/config/components/table/ArrayConfigTable.js
@@ -1,21 +1,22 @@
//@flow
import React from "react";
-import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
+import { RemoveEntryOfTableButton, LabelWithHelpIcon } from "@scm-manager/ui-components";
type Props = {
items: string[],
label: string,
removeLabel: string,
onRemove: (string[], string) => void,
- disabled: boolean
+ disabled: boolean,
+ helpText: string
};
class ArrayConfigTable extends React.Component {
render() {
- const { label, disabled, removeLabel, items } = this.props;
+ const { label, disabled, removeLabel, items, helpText } = this.props;
return (
-
+
{items.map(item => {
diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js
index a7849ffdf2..d786810ad2 100644
--- a/scm-ui/src/config/components/table/ProxyExcludesTable.js
+++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js
@@ -22,6 +22,7 @@ class ProxyExcludesTable extends React.Component {
removeLabel={t("proxy-settings.remove-proxy-exclude-button")}
onRemove={this.removeEntry}
disabled={disabled}
+ helpText={t("help.proxyExcludesHelpText")}
/>
);
}
diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js
index 52cfd911ef..baff061a30 100644
--- a/scm-ui/src/config/modules/config.test.js
+++ b/scm-ui/src/config/modules/config.test.js
@@ -23,7 +23,7 @@ import reducer, {
getConfigUpdatePermission
} from "./config";
-const CONFIG_URL = "/api/rest/v2/config";
+const CONFIG_URL = "/api/v2/config";
const error = new Error("You have an error!");
@@ -51,8 +51,8 @@ const config = {
enabledXsrfProtection: true,
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
_links: {
- self: { href: "http://localhost:8081/api/rest/v2/config" },
- update: { href: "http://localhost:8081/api/rest/v2/config" }
+ self: { href: "http://localhost:8081/api/v2/config" },
+ update: { href: "http://localhost:8081/api/v2/config" }
}
};
@@ -80,8 +80,8 @@ const configWithNullValues = {
enabledXsrfProtection: true,
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
_links: {
- self: { href: "http://localhost:8081/api/rest/v2/config" },
- update: { href: "http://localhost:8081/api/rest/v2/config" }
+ self: { href: "http://localhost:8081/api/v2/config" },
+ update: { href: "http://localhost:8081/api/v2/config" }
}
};
@@ -135,7 +135,7 @@ describe("config fetch()", () => {
});
it("should successfully modify config", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/config", {
status: 204
});
@@ -150,7 +150,7 @@ describe("config fetch()", () => {
});
it("should call the callback after modifying config", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/config", {
status: 204
});
@@ -169,7 +169,7 @@ describe("config fetch()", () => {
});
it("should fail modifying config on HTTP 500", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/config", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/config", {
status: 500
});
diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js
index 8411326b53..9906091162 100644
--- a/scm-ui/src/createReduxStore.js
+++ b/scm-ui/src/createReduxStore.js
@@ -11,6 +11,7 @@ import groups from "./groups/modules/groups";
import auth from "./modules/auth";
import pending from "./modules/pending";
import failure from "./modules/failure";
+import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config";
import type { BrowserHistory } from "history/createBrowserHistory";
@@ -26,6 +27,7 @@ function createReduxStore(history: BrowserHistory) {
users,
repos,
repositoryTypes,
+ permissions,
groups,
auth,
config
diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js
index 1a29c0a433..4958fbf0fa 100644
--- a/scm-ui/src/groups/components/GroupForm.js
+++ b/scm-ui/src/groups/components/GroupForm.js
@@ -80,6 +80,7 @@ class GroupForm extends React.Component {
onChange={this.handleGroupNameChange}
value={group.name}
validationError={this.state.nameValidationError}
+ helpText={t("group-form.help.nameHelpText")}
/>
);
}
@@ -93,6 +94,7 @@ class GroupForm extends React.Component {
onChange={this.handleDescriptionChange}
value={group.description}
validationError={false}
+ helpText={t("group-form.help.descriptionHelpText")}
/>
{
const { t } = this.props;
return (
-
+
{this.props.members.map(member => {
diff --git a/scm-ui/src/groups/modules/groups.test.js b/scm-ui/src/groups/modules/groups.test.js
index 5a15f68017..191a2122e4 100644
--- a/scm-ui/src/groups/modules/groups.test.js
+++ b/scm-ui/src/groups/modules/groups.test.js
@@ -44,7 +44,7 @@ import reducer, {
MODIFY_GROUP_SUCCESS,
MODIFY_GROUP_FAILURE
} from "./groups";
-const GROUPS_URL = "/api/rest/v2/groups";
+const GROUPS_URL = "/api/v2/groups";
const error = new Error("You have an error!");
@@ -57,13 +57,13 @@ const humanGroup = {
members: ["userZaphod"],
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
+ href: "http://localhost:8081/api/v2/groups/humanGroup"
},
delete: {
- href: "http://localhost:8081/api/rest/v2/groups/humanGroup"
+ href: "http://localhost:8081/api/v2/groups/humanGroup"
},
update: {
- href:"http://localhost:8081/api/rest/v2/groups/humanGroup"
+ href:"http://localhost:8081/api/v2/groups/humanGroup"
}
},
_embedded: {
@@ -72,7 +72,7 @@ const humanGroup = {
name: "userZaphod",
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/users/userZaphod"
+ href: "http://localhost:8081/api/v2/users/userZaphod"
}
}
}
@@ -89,13 +89,13 @@ const emptyGroup = {
members: [],
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
+ href: "http://localhost:8081/api/v2/groups/emptyGroup"
},
delete: {
- href: "http://localhost:8081/api/rest/v2/groups/emptyGroup"
+ href: "http://localhost:8081/api/v2/groups/emptyGroup"
},
update: {
- href:"http://localhost:8081/api/rest/v2/groups/emptyGroup"
+ href:"http://localhost:8081/api/v2/groups/emptyGroup"
}
},
_embedded: {
@@ -108,16 +108,16 @@ const responseBody = {
pageTotal: 1,
_links: {
self: {
- href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
+ href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
},
first: {
- href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
+ href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
},
last: {
- href: "http://localhost:3000/api/rest/v2/groups/?page=0&pageSize=10"
+ href: "http://localhost:3000/api/v2/groups/?page=0&pageSize=10"
},
create: {
- href: "http://localhost:3000/api/rest/v2/groups/"
+ href: "http://localhost:3000/api/v2/groups/"
}
},
_embedded: {
@@ -244,7 +244,7 @@ describe("groups fetch()", () => {
});
it("should successfully modify group", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -259,7 +259,7 @@ describe("groups fetch()", () => {
});
it("should call the callback after modifying group", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -278,7 +278,7 @@ describe("groups fetch()", () => {
});
it("should fail modifying group on HTTP 500", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 500
});
@@ -293,7 +293,7 @@ describe("groups fetch()", () => {
});
it("should delete successfully group humanGroup", () => {
- fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
+ fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -308,7 +308,7 @@ describe("groups fetch()", () => {
});
it("should call the callback, after successful delete", () => {
- fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
+ fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 204
});
@@ -324,7 +324,7 @@ describe("groups fetch()", () => {
});
it("should fail to delete group humanGroup", () => {
- fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/groups/humanGroup", {
+ fetchMock.deleteOnce("http://localhost:8081/api/v2/groups/humanGroup", {
status: 500
});
diff --git a/scm-ui/src/modules/auth.test.js b/scm-ui/src/modules/auth.test.js
index 98691e85ac..3cea758566 100644
--- a/scm-ui/src/modules/auth.test.js
+++ b/scm-ui/src/modules/auth.test.js
@@ -78,7 +78,7 @@ describe("auth actions", () => {
});
it("should dispatch login success and dispatch fetch me", () => {
- fetchMock.postOnce("/api/rest/v2/auth/access_token", {
+ fetchMock.postOnce("/api/v2/auth/access_token", {
body: {
cookie: true,
grant_type: "password",
@@ -88,7 +88,7 @@ describe("auth actions", () => {
headers: { "content-type": "application/json" }
});
- fetchMock.getOnce("/api/rest/v2/me", {
+ fetchMock.getOnce("/api/v2/me", {
body: me,
headers: { "content-type": "application/json" }
});
@@ -106,7 +106,7 @@ describe("auth actions", () => {
});
it("should dispatch login failure", () => {
- fetchMock.postOnce("/api/rest/v2/auth/access_token", {
+ fetchMock.postOnce("/api/v2/auth/access_token", {
status: 400
});
@@ -120,7 +120,7 @@ describe("auth actions", () => {
});
it("should dispatch fetch me success", () => {
- fetchMock.getOnce("/api/rest/v2/me", {
+ fetchMock.getOnce("/api/v2/me", {
body: me,
headers: { "content-type": "application/json" }
});
@@ -141,7 +141,7 @@ describe("auth actions", () => {
});
it("should dispatch fetch me failure", () => {
- fetchMock.getOnce("/api/rest/v2/me", {
+ fetchMock.getOnce("/api/v2/me", {
status: 500
});
@@ -155,7 +155,7 @@ describe("auth actions", () => {
});
it("should dispatch fetch me unauthorized", () => {
- fetchMock.getOnce("/api/rest/v2/me", {
+ fetchMock.getOnce("/api/v2/me", {
status: 401
});
@@ -173,11 +173,11 @@ describe("auth actions", () => {
});
it("should dispatch logout success", () => {
- fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
+ fetchMock.deleteOnce("/api/v2/auth/access_token", {
status: 204
});
- fetchMock.getOnce("/api/rest/v2/me", {
+ fetchMock.getOnce("/api/v2/me", {
status: 401
});
@@ -194,7 +194,7 @@ describe("auth actions", () => {
});
it("should dispatch logout failure", () => {
- fetchMock.deleteOnce("/api/rest/v2/auth/access_token", {
+ fetchMock.deleteOnce("/api/v2/auth/access_token", {
status: 500
});
diff --git a/scm-ui/src/modules/failure.js b/scm-ui/src/modules/failure.js
index 67df22623b..49a48e7876 100644
--- a/scm-ui/src/modules/failure.js
+++ b/scm-ui/src/modules/failure.js
@@ -13,6 +13,20 @@ function extractIdentifierFromFailure(action: Action) {
return identifier;
}
+function removeAllEntriesOfIdentifierFromState(
+ state: Object,
+ payload: any,
+ identifier: string
+) {
+ const newState = {};
+ for (let failureType in state) {
+ if (failureType !== identifier && !failureType.startsWith(identifier)) {
+ newState[failureType] = state[failureType];
+ }
+ }
+ return newState;
+}
+
function removeFromState(state: Object, identifier: string) {
const newState = {};
for (let failureType in state) {
@@ -47,7 +61,9 @@ export default function reducer(
if (action.itemId) {
identifier += "/" + action.itemId;
}
- return removeFromState(state, identifier);
+ if (action.payload)
+ return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
+ else return removeFromState(state, identifier);
}
}
return state;
diff --git a/scm-ui/src/modules/pending.js b/scm-ui/src/modules/pending.js
index e83345aee6..306d8a157a 100644
--- a/scm-ui/src/modules/pending.js
+++ b/scm-ui/src/modules/pending.js
@@ -19,6 +19,20 @@ function removeFromState(state: Object, identifier: string) {
return newState;
}
+function removeAllEntriesOfIdentifierFromState(
+ state: Object,
+ payload: any,
+ identifier: string
+) {
+ const newState = {};
+ for (let childType in state) {
+ if (childType !== identifier && !childType.startsWith(identifier)) {
+ newState[childType] = state[childType];
+ }
+ }
+ return newState;
+}
+
function extractIdentifierFromPending(action: Action) {
const type = action.type;
let identifier = type.substring(0, type.length - PENDING_SUFFIX.length);
@@ -48,7 +62,10 @@ export default function reducer(
if (action.itemId) {
identifier += "/" + action.itemId;
}
- return removeFromState(state, identifier);
+ if (action.payload)
+ return removeAllEntriesOfIdentifierFromState(state, action.payload, identifier);
+ else
+ return removeFromState(state, identifier);
}
}
}
diff --git a/scm-ui/src/repos/components/PermissionsNavLink.js b/scm-ui/src/repos/components/PermissionsNavLink.js
new file mode 100644
index 0000000000..cb6d0e0723
--- /dev/null
+++ b/scm-ui/src/repos/components/PermissionsNavLink.js
@@ -0,0 +1,28 @@
+//@flow
+import React from "react";
+import { NavLink } from "@scm-manager/ui-components";
+import { translate } from "react-i18next";
+import type { Repository } from "@scm-manager/ui-types";
+
+type Props = {
+ permissionUrl: string,
+ t: string => string,
+ repository: Repository
+};
+
+class PermissionsNavLink extends React.Component {
+ hasPermissionsLink = () => {
+ return this.props.repository._links.permissions;
+ };
+ render() {
+ if (!this.hasPermissionsLink()) {
+ return null;
+ }
+ const { permissionUrl, t } = this.props;
+ return (
+
+ );
+ }
+}
+
+export default translate("repos")(PermissionsNavLink);
diff --git a/scm-ui/src/repos/components/PermissionsNavLink.test.js b/scm-ui/src/repos/components/PermissionsNavLink.test.js
new file mode 100644
index 0000000000..4aa67f989c
--- /dev/null
+++ b/scm-ui/src/repos/components/PermissionsNavLink.test.js
@@ -0,0 +1,39 @@
+import React from "react";
+import { mount, shallow } from "enzyme";
+import "../../tests/enzyme";
+import "../../tests/i18n";
+import ReactRouterEnzymeContext from "react-router-enzyme-context";
+import PermissionsNavLink from "./PermissionsNavLink";
+import EditNavLink from "./EditNavLink";
+
+describe("PermissionsNavLink", () => {
+ const options = new ReactRouterEnzymeContext();
+
+ it("should render nothing, if the modify link is missing", () => {
+ const repository = {
+ _links: {}
+ };
+
+ const navLink = shallow(
+ ,
+ options.get()
+ );
+ expect(navLink.text()).toBe("");
+ });
+
+ it("should render the navLink", () => {
+ const repository = {
+ _links: {
+ permissions: {
+ href: "/permissions"
+ }
+ }
+ };
+
+ const navLink = mount(
+ ,
+ options.get()
+ );
+ expect(navLink.text()).toBe("repository-root.permissions");
+ });
+});
diff --git a/scm-ui/src/repos/components/form/RepositoryForm.js b/scm-ui/src/repos/components/form/RepositoryForm.js
index 06833c7fd7..54abf8e08d 100644
--- a/scm-ui/src/repos/components/form/RepositoryForm.js
+++ b/scm-ui/src/repos/components/form/RepositoryForm.js
@@ -90,12 +90,14 @@ class RepositoryForm extends React.Component {
value={repository ? repository.contact : ""}
validationError={this.state.contactValidationError}
errorMessage={t("validation.contact-invalid")}
+ helpText={t("help.contactHelpText")}
/>
{
value={repository ? repository.name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.name-invalid")}
+ helpText={t("help.nameHelpText")}
/>
);
diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js
index 69efd4a857..b26d5a9e53 100644
--- a/scm-ui/src/repos/containers/RepositoryRoot.js
+++ b/scm-ui/src/repos/containers/RepositoryRoot.js
@@ -22,9 +22,11 @@ import { translate } from "react-i18next";
import RepositoryDetails from "../components/RepositoryDetails";
import DeleteNavAction from "../components/DeleteNavAction";
import Edit from "../containers/Edit";
+import Permissions from "../permissions/containers/Permissions";
import type { History } from "history";
import EditNavLink from "../components/EditNavLink";
+import PermissionsNavLink from "../components/PermissionsNavLink";
import ScmDiff from "./ScmDiff";
type Props = {
@@ -112,11 +114,24 @@ class RepositoryRoot extends React.Component {
/>
)}
/>
+ (
+
+ )}
+ />
diff --git a/scm-ui/src/repos/modules/repos.test.js b/scm-ui/src/repos/modules/repos.test.js
index ae85d6cb17..e3ec9d48ac 100644
--- a/scm-ui/src/repos/modules/repos.test.js
+++ b/scm-ui/src/repos/modules/repos.test.js
@@ -58,33 +58,33 @@ const hitchhikerPuzzle42: Repository = {
type: "svn",
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42"
+ href: "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42"
},
delete: {
- href: "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42"
+ href: "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42"
},
update: {
- href: "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42"
+ href: "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42"
},
permissions: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/permissions/"
},
tags: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/tags/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/tags/"
},
branches: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/branches/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/branches/"
},
changesets: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/changesets/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/changesets/"
},
sources: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/puzzle42/sources/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/puzzle42/sources/"
}
}
};
@@ -100,35 +100,35 @@ const hitchhikerRestatend: Repository = {
_links: {
self: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
},
delete: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
},
update: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
},
permissions: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/permissions/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend/permissions/"
},
tags: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/tags/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend/tags/"
},
branches: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/branches/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend/branches/"
},
changesets: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/changesets/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend/changesets/"
},
sources: {
href:
- "http://localhost:8081/api/rest/v2/repositories/hitchhiker/restatend/sources/"
+ "http://localhost:8081/api/v2/repositories/hitchhiker/restatend/sources/"
}
}
};
@@ -142,32 +142,32 @@ const slartiFjords: Repository = {
creationDate: "2018-07-31T08:59:05.653Z",
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords"
+ href: "http://localhost:8081/api/v2/repositories/slarti/fjords"
},
delete: {
- href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords"
+ href: "http://localhost:8081/api/v2/repositories/slarti/fjords"
},
update: {
- href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords"
+ href: "http://localhost:8081/api/v2/repositories/slarti/fjords"
},
permissions: {
href:
- "http://localhost:8081/api/rest/v2/repositories/slarti/fjords/permissions/"
+ "http://localhost:8081/api/v2/repositories/slarti/fjords/permissions/"
},
tags: {
- href: "http://localhost:8081/api/rest/v2/repositories/slarti/fjords/tags/"
+ href: "http://localhost:8081/api/v2/repositories/slarti/fjords/tags/"
},
branches: {
href:
- "http://localhost:8081/api/rest/v2/repositories/slarti/fjords/branches/"
+ "http://localhost:8081/api/v2/repositories/slarti/fjords/branches/"
},
changesets: {
href:
- "http://localhost:8081/api/rest/v2/repositories/slarti/fjords/changesets/"
+ "http://localhost:8081/api/v2/repositories/slarti/fjords/changesets/"
},
sources: {
href:
- "http://localhost:8081/api/rest/v2/repositories/slarti/fjords/sources/"
+ "http://localhost:8081/api/v2/repositories/slarti/fjords/sources/"
}
}
};
@@ -177,16 +177,16 @@ const repositoryCollection: RepositoryCollection = {
pageTotal: 1,
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
+ href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
first: {
- href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
+ href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
last: {
- href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
+ href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
create: {
- href: "http://localhost:8081/api/rest/v2/repositories/"
+ href: "http://localhost:8081/api/v2/repositories/"
}
},
_embedded: {
@@ -199,16 +199,16 @@ const repositoryCollectionWithNames: RepositoryCollection = {
pageTotal: 1,
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
+ href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
first: {
- href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
+ href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
last: {
- href: "http://localhost:8081/api/rest/v2/repositories/?page=0&pageSize=10"
+ href: "http://localhost:8081/api/v2/repositories/?page=0&pageSize=10"
},
create: {
- href: "http://localhost:8081/api/rest/v2/repositories/"
+ href: "http://localhost:8081/api/v2/repositories/"
}
},
_embedded: {
@@ -221,7 +221,7 @@ const repositoryCollectionWithNames: RepositoryCollection = {
};
describe("repos fetch", () => {
- const REPOS_URL = "/api/rest/v2/repositories";
+ const REPOS_URL = "/api/v2/repositories";
const SORT = "sortBy=namespaceAndName";
const REPOS_URL_WITH_SORT = REPOS_URL + "?" + SORT;
const mockStore = configureMockStore([thunk]);
@@ -293,7 +293,7 @@ describe("repos fetch", () => {
it("should append sortby parameter and successfully fetch repos from link", () => {
fetchMock.getOnce(
- "/api/rest/v2/repositories?one=1&sortBy=namespaceAndName",
+ "/api/v2/repositories?one=1&sortBy=namespaceAndName",
repositoryCollection
);
@@ -421,7 +421,7 @@ describe("repos fetch", () => {
it("should successfully delete repo slarti/fjords", () => {
fetchMock.delete(
- "http://localhost:8081/api/rest/v2/repositories/slarti/fjords",
+ "http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 204
}
@@ -448,7 +448,7 @@ describe("repos fetch", () => {
it("should successfully delete repo slarti/fjords and call the callback", () => {
fetchMock.delete(
- "http://localhost:8081/api/rest/v2/repositories/slarti/fjords",
+ "http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 204
}
@@ -468,7 +468,7 @@ describe("repos fetch", () => {
it("should disapatch failure on delete, if server returns status code 500", () => {
fetchMock.delete(
- "http://localhost:8081/api/rest/v2/repositories/slarti/fjords",
+ "http://localhost:8081/api/v2/repositories/slarti/fjords",
{
status: 500
}
diff --git a/scm-ui/src/repos/modules/repositoryTypes.test.js b/scm-ui/src/repos/modules/repositoryTypes.test.js
index 0842333484..840a5bdeb8 100644
--- a/scm-ui/src/repos/modules/repositoryTypes.test.js
+++ b/scm-ui/src/repos/modules/repositoryTypes.test.js
@@ -22,7 +22,7 @@ const git = {
displayName: "Git",
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositoryTypes/git"
+ href: "http://localhost:8081/api/v2/repositoryTypes/git"
}
}
};
@@ -32,7 +32,7 @@ const hg = {
displayName: "Mercurial",
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositoryTypes/hg"
+ href: "http://localhost:8081/api/v2/repositoryTypes/hg"
}
}
};
@@ -42,7 +42,7 @@ const svn = {
displayName: "Subversion",
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositoryTypes/svn"
+ href: "http://localhost:8081/api/v2/repositoryTypes/svn"
}
}
};
@@ -53,7 +53,7 @@ const collection = {
},
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/repositoryTypes"
+ href: "http://localhost:8081/api/v2/repositoryTypes"
}
}
};
@@ -97,7 +97,7 @@ describe("repository types caching", () => {
});
describe("repository types fetch", () => {
- const URL = "/api/rest/v2/repositoryTypes";
+ const URL = "/api/v2/repositoryTypes";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
diff --git a/scm-ui/src/repos/permissions/components/CreatePermissionForm.js b/scm-ui/src/repos/permissions/components/CreatePermissionForm.js
new file mode 100644
index 0000000000..595c27d8ef
--- /dev/null
+++ b/scm-ui/src/repos/permissions/components/CreatePermissionForm.js
@@ -0,0 +1,122 @@
+// @flow
+import React from "react";
+import { translate } from "react-i18next";
+import { Checkbox, InputField, SubmitButton } from "@scm-manager/ui-components";
+import TypeSelector from "./TypeSelector";
+import type {
+ PermissionCollection,
+ PermissionEntry
+} from "@scm-manager/ui-types";
+import * as validator from "./permissionValidation";
+
+type Props = {
+ t: string => string,
+ createPermission: (permission: PermissionEntry) => void,
+ loading: boolean,
+ currentPermissions: PermissionCollection
+};
+
+type State = {
+ name: string,
+ type: string,
+ groupPermission: boolean,
+ valid: boolean
+};
+
+class CreatePermissionForm extends React.Component {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ name: "",
+ type: "READ",
+ groupPermission: false,
+ valid: true
+ };
+ }
+
+ render() {
+ const { t, loading } = this.props;
+ const { name, type, groupPermission } = this.state;
+
+ return (
+
+
+ {t("permission.add-permission.add-permission-heading")}
+
+
+
+ );
+ }
+
+ submit = e => {
+ this.props.createPermission({
+ name: this.state.name,
+ type: this.state.type,
+ groupPermission: this.state.groupPermission
+ });
+ this.removeState();
+ e.preventDefault();
+ };
+
+ removeState = () => {
+ this.setState({
+ name: "",
+ type: "READ",
+ groupPermission: false,
+ valid: true
+ });
+ };
+
+ handleTypeChange = (type: string) => {
+ this.setState({
+ type: type
+ });
+ };
+
+ handleNameChange = (name: string) => {
+ this.setState({
+ name: name,
+ valid: validator.isPermissionValid(
+ name,
+ this.state.groupPermission,
+ this.props.currentPermissions
+ )
+ });
+ };
+ handleGroupPermissionChange = (groupPermission: boolean) => {
+ this.setState({
+ groupPermission: groupPermission,
+ valid: validator.isPermissionValid(
+ this.state.name,
+ groupPermission,
+ this.props.currentPermissions
+ )
+ });
+ };
+}
+
+export default translate("repos")(CreatePermissionForm);
diff --git a/scm-ui/src/repos/permissions/components/TypeSelector.js b/scm-ui/src/repos/permissions/components/TypeSelector.js
new file mode 100644
index 0000000000..9f94c11d54
--- /dev/null
+++ b/scm-ui/src/repos/permissions/components/TypeSelector.js
@@ -0,0 +1,40 @@
+// @flow
+import React from "react";
+import { translate } from "react-i18next";
+import {
+ Select
+} from "@scm-manager/ui-components";
+
+type Props = {
+ t: string => string,
+ handleTypeChange: string => void,
+ type: string,
+ loading?: boolean
+};
+
+class TypeSelector extends React.Component {
+ render() {
+ const { type, handleTypeChange, loading } = this.props;
+ const types = ["READ", "OWNER", "WRITE"];
+
+ return (
+
+ );
+ }
+
+ createSelectOptions(types: string[]) {
+ return types.map(type => {
+ return {
+ label: type,
+ value: type
+ };
+ });
+ }
+}
+
+export default translate("permissions")(TypeSelector);
diff --git a/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.js b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.js
new file mode 100644
index 0000000000..a3ba8616c9
--- /dev/null
+++ b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.js
@@ -0,0 +1,73 @@
+// @flow
+import React from "react";
+import { translate } from "react-i18next";
+import type { Permission } from "@scm-manager/ui-types";
+import { confirmAlert, DeleteButton } from "@scm-manager/ui-components";
+
+type Props = {
+ permission: Permission,
+ namespace: string,
+ repoName: string,
+ confirmDialog?: boolean,
+ t: string => string,
+ deletePermission: (
+ permission: Permission,
+ namespace: string,
+ repoName: string
+ ) => void,
+ loading: boolean
+};
+
+class DeletePermissionButton extends React.Component {
+ static defaultProps = {
+ confirmDialog: true
+ };
+
+ deletePermission = () => {
+ this.props.deletePermission(
+ this.props.permission,
+ this.props.namespace,
+ this.props.repoName
+ );
+ };
+
+ confirmDelete = () => {
+ const { t } = this.props;
+ confirmAlert({
+ title: t("permission.delete-permission-button.confirm-alert.title"),
+ message: t("permission.delete-permission-button.confirm-alert.message"),
+ buttons: [
+ {
+ label: t("permission.delete-permission-button.confirm-alert.submit"),
+ onClick: () => this.deletePermission()
+ },
+ {
+ label: t("permission.delete-permission-button.confirm-alert.cancel"),
+ onClick: () => null
+ }
+ ]
+ });
+ };
+
+ isDeletable = () => {
+ return this.props.permission._links.delete;
+ };
+
+ render() {
+ const { confirmDialog, loading, t } = this.props;
+ const action = confirmDialog ? this.confirmDelete : this.deletePermission;
+
+ if (!this.isDeletable()) {
+ return null;
+ }
+ return (
+
+ );
+ }
+}
+
+export default translate("repos")(DeletePermissionButton);
diff --git a/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.test.js b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.test.js
new file mode 100644
index 0000000000..0e722e78e8
--- /dev/null
+++ b/scm-ui/src/repos/permissions/components/buttons/DeletePermissionButton.test.js
@@ -0,0 +1,91 @@
+import React from "react";
+import { mount, shallow } from "enzyme";
+import "../../../../tests/enzyme";
+import "../../../../tests/i18n";
+import DeletePermissionButton from "./DeletePermissionButton";
+
+import { confirmAlert } from "@scm-manager/ui-components";
+jest.mock("@scm-manager/ui-components", () => ({
+ confirmAlert: jest.fn(),
+ DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
+}));
+
+describe("DeletePermissionButton", () => {
+ it("should render nothing, if the delete link is missing", () => {
+ const permission = {
+ _links: {}
+ };
+
+ const navLink = shallow(
+ {}}
+ />
+ );
+ expect(navLink.text()).toBe("");
+ });
+
+ it("should render the navLink", () => {
+ const permission = {
+ _links: {
+ delete: {
+ href: "/permission"
+ }
+ }
+ };
+
+ const navLink = mount(
+ {}}
+ />
+ );
+ expect(navLink.text()).not.toBe("");
+ });
+
+ it("should open the confirm dialog on button click", () => {
+ const permission = {
+ _links: {
+ delete: {
+ href: "/permission"
+ }
+ }
+ };
+
+ const button = mount(
+ {}}
+ />
+ );
+ button.find("button").simulate("click");
+
+ expect(confirmAlert.mock.calls.length).toBe(1);
+ });
+
+ it("should call the delete permission function with delete url", () => {
+ const permission = {
+ _links: {
+ delete: {
+ href: "/permission"
+ }
+ }
+ };
+
+ let calledUrl = null;
+ function capture(permission) {
+ calledUrl = permission._links.delete.href;
+ }
+
+ const button = mount(
+
+ );
+ button.find("button").simulate("click");
+
+ expect(calledUrl).toBe("/permission");
+ });
+});
diff --git a/scm-ui/src/repos/permissions/components/permissionValidation.js b/scm-ui/src/repos/permissions/components/permissionValidation.js
new file mode 100644
index 0000000000..b74ae40988
--- /dev/null
+++ b/scm-ui/src/repos/permissions/components/permissionValidation.js
@@ -0,0 +1,23 @@
+// @flow
+import { validation } from "@scm-manager/ui-components";
+import type {
+ PermissionCollection,
+} from "@scm-manager/ui-types";
+const isNameValid = validation.isNameValid;
+
+export { isNameValid };
+
+export const isPermissionValid = (name: string, groupPermission: boolean, permissions: PermissionCollection) => {
+ return isNameValid(name) && !currentPermissionIncludeName(name, groupPermission, permissions);
+};
+
+const currentPermissionIncludeName = (name: string, groupPermission: boolean, permissions: PermissionCollection) => {
+ for (let i = 0; i < permissions.length; i++) {
+ if (
+ permissions[i].name === name &&
+ permissions[i].groupPermission == groupPermission
+ )
+ return true;
+ }
+ return false;
+};
diff --git a/scm-ui/src/repos/permissions/components/permissionValidation.test.js b/scm-ui/src/repos/permissions/components/permissionValidation.test.js
new file mode 100644
index 0000000000..036375a348
--- /dev/null
+++ b/scm-ui/src/repos/permissions/components/permissionValidation.test.js
@@ -0,0 +1,66 @@
+//@flow
+import * as validator from "./permissionValidation";
+
+describe("permission validation", () => {
+ it("should return true if permission is valid and does not exist", () => {
+ const permissions = [];
+ const name = "PermissionName";
+ const groupPermission = false;
+
+ expect(
+ validator.isPermissionValid(name, groupPermission, permissions)
+ ).toBe(true);
+ });
+
+ it("should return true if permission is valid and does not exists with same group permission", () => {
+ const permissions = [
+ {
+ name: "PermissionName",
+ groupPermission: true,
+ type: "READ"
+ }
+ ];
+ const name = "PermissionName";
+ const groupPermission = false;
+
+ expect(
+ validator.isPermissionValid(name, groupPermission, permissions)
+ ).toBe(true);
+ });
+
+ it("should return false if permission is valid but exists", () => {
+ const permissions = [
+ {
+ name: "PermissionName",
+ groupPermission: false,
+ type: "READ"
+ }
+ ];
+ const name = "PermissionName";
+ const groupPermission = false;
+
+ expect(
+ validator.isPermissionValid(name, groupPermission, permissions)
+ ).toBe(false);
+ });
+
+ it("should return false if permission does not exist but is invalid", () => {
+ const permissions = [];
+ const name = "@PermissionName";
+ const groupPermission = false;
+
+ expect(
+ validator.isPermissionValid(name, groupPermission, permissions)
+ ).toBe(false);
+ });
+
+ it("should return false if permission is not valid and does not exist", () => {
+ const permissions = [];
+ const name = "@PermissionName";
+ const groupPermission = false;
+
+ expect(
+ validator.isPermissionValid(name, groupPermission, permissions)
+ ).toBe(false);
+ });
+});
diff --git a/scm-ui/src/repos/permissions/containers/Permissions.js b/scm-ui/src/repos/permissions/containers/Permissions.js
new file mode 100644
index 0000000000..d29359ed0b
--- /dev/null
+++ b/scm-ui/src/repos/permissions/containers/Permissions.js
@@ -0,0 +1,200 @@
+//@flow
+import React from "react";
+import { connect } from "react-redux";
+import { translate } from "react-i18next";
+import {
+ fetchPermissions,
+ getFetchPermissionsFailure,
+ isFetchPermissionsPending,
+ getPermissionsOfRepo,
+ hasCreatePermission,
+ createPermission,
+ isCreatePermissionPending,
+ getCreatePermissionFailure,
+ createPermissionReset,
+ getDeletePermissionsFailure,
+ getModifyPermissionsFailure,
+ modifyPermissionReset,
+ deletePermissionReset
+} from "../modules/permissions";
+import { Loading, ErrorPage } from "@scm-manager/ui-components";
+import type {
+ Permission,
+ PermissionCollection,
+ PermissionEntry
+} from "@scm-manager/ui-types";
+import SinglePermission from "./SinglePermission";
+import CreatePermissionForm from "../components/CreatePermissionForm";
+import type { History } from "history";
+
+type Props = {
+ namespace: string,
+ repoName: string,
+ loading: boolean,
+ error: Error,
+ permissions: PermissionCollection,
+ hasPermissionToCreate: boolean,
+ loadingCreatePermission: boolean,
+
+ //dispatch functions
+ fetchPermissions: (namespace: string, repoName: string) => void,
+ createPermission: (
+ permission: PermissionEntry,
+ namespace: string,
+ repoName: string,
+ callback?: () => void
+ ) => void,
+ createPermissionReset: (string, string) => void,
+ modifyPermissionReset: (string, string) => void,
+ deletePermissionReset: (string, string) => void,
+ // context props
+ t: string => string,
+ match: any,
+ history: History
+};
+
+class Permissions extends React.Component {
+ componentDidMount() {
+ const {
+ fetchPermissions,
+ namespace,
+ repoName,
+ modifyPermissionReset,
+ createPermissionReset,
+ deletePermissionReset
+ } = this.props;
+
+ createPermissionReset(namespace, repoName);
+ modifyPermissionReset(namespace, repoName);
+ deletePermissionReset(namespace, repoName);
+ fetchPermissions(namespace, repoName);
+ }
+
+ createPermission = (permission: Permission) => {
+ this.props.createPermission(
+ permission,
+ this.props.namespace,
+ this.props.repoName
+ );
+ };
+
+ render() {
+ const {
+ loading,
+ error,
+ permissions,
+ t,
+ namespace,
+ repoName,
+ loadingCreatePermission,
+ hasPermissionToCreate
+ } = this.props;
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (loading || !permissions) {
+ return ;
+ }
+
+ const createPermissionForm = hasPermissionToCreate ? (
+ this.createPermission(permission)}
+ loading={loadingCreatePermission}
+ currentPermissions={permissions}
+ />
+ ) : null;
+
+ return (
+
+
+
+
+ | {t("permission.name")} |
+
+ {t("permission.group-permission")}
+ |
+ {t("permission.type")} |
+
+
+
+ {permissions.map(permission => {
+ return (
+
+ );
+ })}
+
+
+ {createPermissionForm}
+
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ const namespace = ownProps.namespace;
+ const repoName = ownProps.repoName;
+ const error =
+ getFetchPermissionsFailure(state, namespace, repoName) ||
+ getCreatePermissionFailure(state, namespace, repoName) ||
+ getDeletePermissionsFailure(state, namespace, repoName) ||
+ getModifyPermissionsFailure(state, namespace, repoName);
+ const loading = isFetchPermissionsPending(state, namespace, repoName);
+ const permissions = getPermissionsOfRepo(state, namespace, repoName);
+ const loadingCreatePermission = isCreatePermissionPending(
+ state,
+ namespace,
+ repoName
+ );
+ const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
+ return {
+ namespace,
+ repoName,
+ error,
+ loading,
+ permissions,
+ hasPermissionToCreate,
+ loadingCreatePermission
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ fetchPermissions: (namespace: string, repoName: string) => {
+ dispatch(fetchPermissions(namespace, repoName));
+ },
+ createPermission: (
+ permission: PermissionEntry,
+ namespace: string,
+ repoName: string,
+ callback?: () => void
+ ) => {
+ dispatch(createPermission(permission, namespace, repoName, callback));
+ },
+ createPermissionReset: (namespace: string, repoName: string) => {
+ dispatch(createPermissionReset(namespace, repoName));
+ },
+ modifyPermissionReset: (namespace: string, repoName: string) => {
+ dispatch(modifyPermissionReset(namespace, repoName));
+ },
+ deletePermissionReset: (namespace: string, repoName: string) => {
+ dispatch(deletePermissionReset(namespace, repoName));
+ }
+ };
+};
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(translate("repos")(Permissions));
diff --git a/scm-ui/src/repos/permissions/containers/SinglePermission.js b/scm-ui/src/repos/permissions/containers/SinglePermission.js
new file mode 100644
index 0000000000..9426fbcd9f
--- /dev/null
+++ b/scm-ui/src/repos/permissions/containers/SinglePermission.js
@@ -0,0 +1,176 @@
+// @flow
+import React from "react";
+import type { Permission } from "@scm-manager/ui-types";
+import { translate } from "react-i18next";
+import {
+ modifyPermission,
+ isModifyPermissionPending,
+ deletePermission,
+ isDeletePermissionPending
+} from "../modules/permissions";
+import { connect } from "react-redux";
+import type { History } from "history";
+import { Checkbox } from "@scm-manager/ui-components";
+import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
+import TypeSelector from "../components/TypeSelector";
+
+type Props = {
+ submitForm: Permission => void,
+ modifyPermission: (Permission, string, string) => void,
+ permission: Permission,
+ t: string => string,
+ namespace: string,
+ repoName: string,
+ match: any,
+ history: History,
+ loading: boolean,
+ deletePermission: (Permission, string, string) => void,
+ deleteLoading: boolean
+};
+
+type State = {
+ permission: Permission
+};
+
+class SinglePermission extends React.Component {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ permission: {
+ name: "",
+ type: "READ",
+ groupPermission: false,
+ _links: {}
+ }
+ };
+ }
+
+ componentDidMount() {
+ const { permission } = this.props;
+ if (permission) {
+ this.setState({
+ permission: {
+ name: permission.name,
+ type: permission.type,
+ groupPermission: permission.groupPermission,
+ _links: permission._links
+ }
+ });
+ }
+ }
+
+ deletePermission = () => {
+ this.props.deletePermission(
+ this.props.permission,
+ this.props.namespace,
+ this.props.repoName
+ );
+ };
+
+ render() {
+ const { permission } = this.state;
+ const { loading, namespace, repoName } = this.props;
+ const typeSelector =
+ this.props.permission._links && this.props.permission._links.update ? (
+
+
+ |
+ ) : (
+ {permission.type} |
+ );
+
+ return (
+
+ | {permission.name} |
+
+
+ |
+ {typeSelector}
+
+
+ |
+
+ );
+ }
+
+ handleTypeChange = (type: string) => {
+ this.setState({
+ permission: {
+ ...this.state.permission,
+ type: type
+ }
+ });
+ this.modifyPermission(type);
+ };
+
+ modifyPermission = (type: string) => {
+ let permission = this.state.permission;
+ permission.type = type;
+ this.props.modifyPermission(
+ permission,
+ this.props.namespace,
+ this.props.repoName
+ );
+ };
+
+ createSelectOptions(types: string[]) {
+ return types.map(type => {
+ return {
+ label: type,
+ value: type
+ };
+ });
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ const permission = ownProps.permission;
+ const loading = isModifyPermissionPending(
+ state,
+ ownProps.namespace,
+ ownProps.repoName,
+ permission
+ );
+ const deleteLoading = isDeletePermissionPending(
+ state,
+ ownProps.namespace,
+ ownProps.repoName,
+ permission
+ );
+
+ return { loading, deleteLoading };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ modifyPermission: (
+ permission: Permission,
+ namespace: string,
+ repoName: string
+ ) => {
+ dispatch(modifyPermission(permission, namespace, repoName));
+ },
+ deletePermission: (
+ permission: Permission,
+ namespace: string,
+ repoName: string
+ ) => {
+ dispatch(deletePermission(permission, namespace, repoName));
+ }
+ };
+};
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(translate("repos")(SinglePermission));
diff --git a/scm-ui/src/repos/permissions/modules/permissions.js b/scm-ui/src/repos/permissions/modules/permissions.js
new file mode 100644
index 0000000000..86d78e7ae9
--- /dev/null
+++ b/scm-ui/src/repos/permissions/modules/permissions.js
@@ -0,0 +1,624 @@
+// @flow
+
+import { apiClient } from "@scm-manager/ui-components";
+import * as types from "../../../modules/types";
+import type { Action } from "@scm-manager/ui-components";
+import type {
+ PermissionCollection,
+ Permission,
+ PermissionEntry
+} from "@scm-manager/ui-types";
+import { isPending } from "../../../modules/pending";
+import { getFailure } from "../../../modules/failure";
+import { Dispatch } from "redux";
+
+export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
+export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
+ types.PENDING_SUFFIX
+}`;
+export const FETCH_PERMISSIONS_SUCCESS = `${FETCH_PERMISSIONS}_${
+ types.SUCCESS_SUFFIX
+}`;
+export const FETCH_PERMISSIONS_FAILURE = `${FETCH_PERMISSIONS}_${
+ types.FAILURE_SUFFIX
+}`;
+export const MODIFY_PERMISSION = "scm/permissions/MODFIY_PERMISSION";
+export const MODIFY_PERMISSION_PENDING = `${MODIFY_PERMISSION}_${
+ types.PENDING_SUFFIX
+}`;
+export const MODIFY_PERMISSION_SUCCESS = `${MODIFY_PERMISSION}_${
+ types.SUCCESS_SUFFIX
+}`;
+export const MODIFY_PERMISSION_FAILURE = `${MODIFY_PERMISSION}_${
+ types.FAILURE_SUFFIX
+}`;
+export const MODIFY_PERMISSION_RESET = `${MODIFY_PERMISSION}_${
+ types.RESET_SUFFIX
+}`;
+export const CREATE_PERMISSION = "scm/permissions/CREATE_PERMISSION";
+export const CREATE_PERMISSION_PENDING = `${CREATE_PERMISSION}_${
+ types.PENDING_SUFFIX
+}`;
+export const CREATE_PERMISSION_SUCCESS = `${CREATE_PERMISSION}_${
+ types.SUCCESS_SUFFIX
+}`;
+export const CREATE_PERMISSION_FAILURE = `${CREATE_PERMISSION}_${
+ types.FAILURE_SUFFIX
+}`;
+export const CREATE_PERMISSION_RESET = `${CREATE_PERMISSION}_${
+ types.RESET_SUFFIX
+}`;
+export const DELETE_PERMISSION = "scm/permissions/DELETE_PERMISSION";
+export const DELETE_PERMISSION_PENDING = `${DELETE_PERMISSION}_${
+ types.PENDING_SUFFIX
+}`;
+export const DELETE_PERMISSION_SUCCESS = `${DELETE_PERMISSION}_${
+ types.SUCCESS_SUFFIX
+}`;
+export const DELETE_PERMISSION_FAILURE = `${DELETE_PERMISSION}_${
+ types.FAILURE_SUFFIX
+}`;
+export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
+ types.RESET_SUFFIX
+}`;
+
+const REPOS_URL = "repositories";
+const PERMISSIONS_URL = "permissions";
+const CONTENT_TYPE = "application/vnd.scmm-permission+json";
+
+// fetch permissions
+
+export function fetchPermissions(namespace: string, repoName: string) {
+ return function(dispatch: any) {
+ dispatch(fetchPermissionsPending(namespace, repoName));
+ return apiClient
+ .get(`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`)
+ .then(response => response.json())
+ .then(permissions => {
+ dispatch(fetchPermissionsSuccess(permissions, namespace, repoName));
+ })
+ .catch(err => {
+ dispatch(fetchPermissionsFailure(namespace, repoName, err));
+ });
+ };
+}
+
+export function fetchPermissionsPending(
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: FETCH_PERMISSIONS_PENDING,
+ payload: {
+ namespace,
+ repoName
+ },
+ itemId: namespace + "/" + repoName
+ };
+}
+
+export function fetchPermissionsSuccess(
+ permissions: any,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: FETCH_PERMISSIONS_SUCCESS,
+ payload: permissions,
+ itemId: namespace + "/" + repoName
+ };
+}
+
+export function fetchPermissionsFailure(
+ namespace: string,
+ repoName: string,
+ error: Error
+): Action {
+ return {
+ type: FETCH_PERMISSIONS_FAILURE,
+ payload: {
+ namespace,
+ repoName,
+ error
+ },
+ itemId: namespace + "/" + repoName
+ };
+}
+
+// modify permission
+
+export function modifyPermission(
+ permission: Permission,
+ namespace: string,
+ repoName: string,
+ callback?: () => void
+) {
+ return function(dispatch: any) {
+ dispatch(modifyPermissionPending(permission, namespace, repoName));
+ return apiClient
+ .put(permission._links.update.href, permission, CONTENT_TYPE)
+ .then(() => {
+ dispatch(modifyPermissionSuccess(permission, namespace, repoName));
+ if (callback) {
+ callback();
+ }
+ })
+ .catch(cause => {
+ const error = new Error(
+ `failed to modify permission: ${cause.message}`
+ );
+ dispatch(
+ modifyPermissionFailure(permission, error, namespace, repoName)
+ );
+ });
+ };
+}
+
+export function modifyPermissionPending(
+ permission: Permission,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: MODIFY_PERMISSION_PENDING,
+ payload: permission,
+ itemId: createItemId(permission, namespace, repoName)
+ };
+}
+
+export function modifyPermissionSuccess(
+ permission: Permission,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: MODIFY_PERMISSION_SUCCESS,
+ payload: {
+ permission,
+ position: namespace + "/" + repoName
+ },
+ itemId: createItemId(permission, namespace, repoName)
+ };
+}
+
+export function modifyPermissionFailure(
+ permission: Permission,
+ error: Error,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: MODIFY_PERMISSION_FAILURE,
+ payload: { error, permission },
+ itemId: createItemId(permission, namespace, repoName)
+ };
+}
+
+function newPermissions(
+ oldPermissions: PermissionCollection,
+ newPermission: Permission
+) {
+ for (let i = 0; i < oldPermissions.length; i++) {
+ if (oldPermissions[i].name === newPermission.name) {
+ oldPermissions.splice(i, 1, newPermission);
+ return oldPermissions;
+ }
+ }
+}
+
+export function modifyPermissionReset(namespace: string, repoName: string) {
+ return {
+ type: MODIFY_PERMISSION_RESET,
+ payload: {
+ namespace,
+ repoName
+ },
+ itemId: namespace + "/" + repoName
+ };
+}
+
+// create permission
+export function createPermission(
+ permission: PermissionEntry,
+ namespace: string,
+ repoName: string,
+ callback?: () => void
+) {
+ return function(dispatch: Dispatch) {
+ dispatch(createPermissionPending(permission, namespace, repoName));
+ return apiClient
+ .post(
+ `${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`,
+ permission,
+ CONTENT_TYPE
+ )
+ .then(response => {
+ const location = response.headers.get("Location");
+ return apiClient.get(location);
+ })
+ .then(response => response.json())
+ .then(createdPermission => {
+ dispatch(
+ createPermissionSuccess(createdPermission, namespace, repoName)
+ );
+ if (callback) {
+ callback();
+ }
+ })
+ .catch(err =>
+ dispatch(
+ createPermissionFailure(
+ new Error(
+ `failed to add permission ${permission.name}: ${err.message}`
+ ),
+ namespace,
+ repoName
+ )
+ )
+ );
+ };
+}
+
+export function createPermissionPending(
+ permission: PermissionEntry,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: CREATE_PERMISSION_PENDING,
+ payload: permission,
+ itemId: namespace + "/" + repoName
+ };
+}
+
+export function createPermissionSuccess(
+ permission: PermissionEntry,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: CREATE_PERMISSION_SUCCESS,
+ payload: {
+ permission,
+ position: namespace + "/" + repoName
+ },
+ itemId: namespace + "/" + repoName
+ };
+}
+
+export function createPermissionFailure(
+ error: Error,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: CREATE_PERMISSION_FAILURE,
+ payload: error,
+ itemId: namespace + "/" + repoName
+ };
+}
+
+export function createPermissionReset(namespace: string, repoName: string) {
+ return {
+ type: CREATE_PERMISSION_RESET,
+ itemId: namespace + "/" + repoName
+ };
+}
+
+// delete permission
+
+export function deletePermission(
+ permission: Permission,
+ namespace: string,
+ repoName: string,
+ callback?: () => void
+) {
+ return function(dispatch: any) {
+ dispatch(deletePermissionPending(permission, namespace, repoName));
+ return apiClient
+ .delete(permission._links.delete.href)
+ .then(() => {
+ dispatch(deletePermissionSuccess(permission, namespace, repoName));
+ if (callback) {
+ callback();
+ }
+ })
+ .catch(cause => {
+ const error = new Error(
+ `could not delete permission ${permission.name}: ${cause.message}`
+ );
+ dispatch(
+ deletePermissionFailure(permission, namespace, repoName, error)
+ );
+ });
+ };
+}
+
+export function deletePermissionPending(
+ permission: Permission,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: DELETE_PERMISSION_PENDING,
+ payload: permission,
+ itemId: createItemId(permission, namespace, repoName)
+ };
+}
+
+export function deletePermissionSuccess(
+ permission: Permission,
+ namespace: string,
+ repoName: string
+): Action {
+ return {
+ type: DELETE_PERMISSION_SUCCESS,
+ payload: {
+ permission,
+ position: namespace + "/" + repoName
+ },
+ itemId: createItemId(permission, namespace, repoName)
+ };
+}
+
+export function deletePermissionFailure(
+ permission: Permission,
+ namespace: string,
+ repoName: string,
+ error: Error
+): Action {
+ return {
+ type: DELETE_PERMISSION_FAILURE,
+ payload: {
+ error,
+ permission
+ },
+ itemId: createItemId(permission, namespace, repoName)
+ };
+}
+
+export function deletePermissionReset(namespace: string, repoName: string) {
+ return {
+ type: DELETE_PERMISSION_RESET,
+ payload: {
+ namespace,
+ repoName
+ },
+ itemId: namespace + "/" + repoName
+ };
+}
+function deletePermissionFromState(
+ oldPermissions: PermissionCollection,
+ permission: Permission
+) {
+ let newPermission = [];
+ for (let i = 0; i < oldPermissions.length; i++) {
+ if (oldPermissions[i] !== permission) {
+ newPermission.push(oldPermissions[i]);
+ }
+ }
+ return newPermission;
+}
+
+function createItemId(
+ permission: Permission,
+ namespace: string,
+ repoName: string
+) {
+ let groupPermission = permission.groupPermission ? "@" : "";
+ return namespace + "/" + repoName + "/" + groupPermission + permission.name;
+}
+
+// reducer
+export default function reducer(
+ state: Object = {},
+ action: Action = { type: "UNKNOWN" }
+): Object {
+ if (!action.payload) {
+ return state;
+ }
+ switch (action.type) {
+ case FETCH_PERMISSIONS_SUCCESS:
+ return {
+ ...state,
+ [action.itemId]: {
+ entries: action.payload._embedded.permissions,
+ createPermission: action.payload._links.create ? true : false
+ }
+ };
+ case MODIFY_PERMISSION_SUCCESS:
+ const positionOfPermission = action.payload.position;
+ const newPermission = newPermissions(
+ state[action.payload.position].entries,
+ action.payload.permission
+ );
+ return {
+ ...state,
+ [positionOfPermission]: {
+ ...state[positionOfPermission],
+ entries: newPermission
+ }
+ };
+ case CREATE_PERMISSION_SUCCESS:
+ // return state;
+ const position = action.payload.position;
+ const permissions = state[action.payload.position].entries;
+ permissions.push(action.payload.permission);
+ return {
+ ...state,
+ [position]: {
+ ...state[position],
+ entries: permissions
+ }
+ };
+ case DELETE_PERMISSION_SUCCESS:
+ const permissionPosition = action.payload.position;
+ const new_Permissions = deletePermissionFromState(
+ state[action.payload.position].entries,
+ action.payload.permission
+ );
+ return {
+ ...state,
+ [permissionPosition]: {
+ ...state[permissionPosition],
+ entries: new_Permissions
+ }
+ };
+ default:
+ return state;
+ }
+}
+
+// selectors
+
+export function getPermissionsOfRepo(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ if (state.permissions && state.permissions[namespace + "/" + repoName]) {
+ const permissions = state.permissions[namespace + "/" + repoName].entries;
+ return permissions;
+ }
+}
+
+export function isFetchPermissionsPending(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
+}
+
+export function getFetchPermissionsFailure(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ return getFailure(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
+}
+
+export function isModifyPermissionPending(
+ state: Object,
+ namespace: string,
+ repoName: string,
+ permission: Permission
+) {
+ return isPending(
+ state,
+ MODIFY_PERMISSION,
+ createItemId(permission, namespace, repoName)
+ );
+}
+
+export function getModifyPermissionFailure(
+ state: Object,
+ namespace: string,
+ repoName: string,
+ permission: Permission
+) {
+ return getFailure(
+ state,
+ MODIFY_PERMISSION,
+ createItemId(permission, namespace, repoName)
+ );
+}
+
+export function hasCreatePermission(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ if (state.permissions && state.permissions[namespace + "/" + repoName])
+ return state.permissions[namespace + "/" + repoName].createPermission;
+ else return null;
+}
+
+export function isCreatePermissionPending(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
+}
+export function getCreatePermissionFailure(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ return getFailure(state, CREATE_PERMISSION, namespace + "/" + repoName);
+}
+
+export function isDeletePermissionPending(
+ state: Object,
+ namespace: string,
+ repoName: string,
+ permission: Permission
+) {
+ return isPending(
+ state,
+ DELETE_PERMISSION,
+ createItemId(permission, namespace, repoName)
+ );
+}
+
+export function getDeletePermissionFailure(
+ state: Object,
+ namespace: string,
+ repoName: string,
+ permission: Permission
+) {
+ return getFailure(
+ state,
+ DELETE_PERMISSION,
+ createItemId(permission, namespace, repoName)
+ );
+}
+
+export function getDeletePermissionsFailure(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ const permissions =
+ state.permissions && state.permissions[namespace + "/" + repoName]
+ ? state.permissions[namespace + "/" + repoName].entries
+ : null;
+ if (permissions == null) return undefined;
+ for (let i = 0; i < permissions.length; i++) {
+ if (
+ getDeletePermissionFailure(state, namespace, repoName, permissions[i])
+ ) {
+ return getFailure(
+ state,
+ DELETE_PERMISSION,
+ createItemId(permissions[i], namespace, repoName)
+ );
+ }
+ }
+ return null;
+}
+
+export function getModifyPermissionsFailure(
+ state: Object,
+ namespace: string,
+ repoName: string
+) {
+ const permissions =
+ state.permissions && state.permissions[namespace + "/" + repoName]
+ ? state.permissions[namespace + "/" + repoName].entries
+ : null;
+ if (permissions == null) return undefined;
+ for (let i = 0; i < permissions.length; i++) {
+ if (
+ getModifyPermissionFailure(state, namespace, repoName, permissions[i])
+ ) {
+ return getFailure(
+ state,
+ MODIFY_PERMISSION,
+ createItemId(permissions[i], namespace, repoName)
+ );
+ }
+ }
+ return null;
+}
diff --git a/scm-ui/src/repos/permissions/modules/permissions.test.js b/scm-ui/src/repos/permissions/modules/permissions.test.js
new file mode 100644
index 0000000000..e546f6cb00
--- /dev/null
+++ b/scm-ui/src/repos/permissions/modules/permissions.test.js
@@ -0,0 +1,769 @@
+// @flow
+import configureMockStore from "redux-mock-store";
+import thunk from "redux-thunk";
+import fetchMock from "fetch-mock";
+import reducer, {
+ fetchPermissions,
+ fetchPermissionsSuccess,
+ getPermissionsOfRepo,
+ isFetchPermissionsPending,
+ getFetchPermissionsFailure,
+ modifyPermission,
+ modifyPermissionSuccess,
+ getModifyPermissionFailure,
+ isModifyPermissionPending,
+ createPermission,
+ hasCreatePermission,
+ deletePermission,
+ deletePermissionSuccess,
+ getDeletePermissionFailure,
+ isDeletePermissionPending,
+ getModifyPermissionsFailure,
+ MODIFY_PERMISSION_FAILURE,
+ MODIFY_PERMISSION_PENDING,
+ FETCH_PERMISSIONS,
+ FETCH_PERMISSIONS_PENDING,
+ FETCH_PERMISSIONS_SUCCESS,
+ FETCH_PERMISSIONS_FAILURE,
+ MODIFY_PERMISSION_SUCCESS,
+ MODIFY_PERMISSION,
+ CREATE_PERMISSION_PENDING,
+ CREATE_PERMISSION_SUCCESS,
+ CREATE_PERMISSION_FAILURE,
+ DELETE_PERMISSION,
+ DELETE_PERMISSION_PENDING,
+ DELETE_PERMISSION_SUCCESS,
+ DELETE_PERMISSION_FAILURE,
+ CREATE_PERMISSION,
+ createPermissionSuccess,
+ getCreatePermissionFailure,
+ isCreatePermissionPending,
+ getDeletePermissionsFailure
+} from "./permissions";
+import type { Permission, PermissionCollection } from "@scm-manager/ui-types";
+
+const hitchhiker_puzzle42Permission_user_eins: Permission = {
+ name: "user_eins",
+ type: "READ",
+ groupPermission: false,
+ _links: {
+ self: {
+ href:
+ "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
+ },
+ delete: {
+ href:
+ "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
+ },
+ update: {
+ href:
+ "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
+ }
+ }
+};
+
+const hitchhiker_puzzle42Permission_user_zwei: Permission = {
+ name: "user_zwei",
+ type: "WRITE",
+ groupPermission: true,
+ _links: {
+ self: {
+ href:
+ "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
+ },
+ delete: {
+ href:
+ "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
+ },
+ update: {
+ href:
+ "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
+ }
+ }
+};
+
+const hitchhiker_puzzle42Permissions: PermissionCollection = [
+ hitchhiker_puzzle42Permission_user_eins,
+ hitchhiker_puzzle42Permission_user_zwei
+];
+
+const hitchhiker_puzzle42RepoPermissions = {
+ _embedded: {
+ permissions: hitchhiker_puzzle42Permissions
+ },
+ _links: {
+ create: {
+ href:
+ "http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions"
+ }
+ }
+};
+
+describe("permission fetch", () => {
+ const REPOS_URL = "/api/v2/repositories";
+ const mockStore = configureMockStore([thunk]);
+
+ afterEach(() => {
+ fetchMock.reset();
+ fetchMock.restore();
+ });
+
+ it("should successfully fetch permissions to repo hitchhiker/puzzle42", () => {
+ fetchMock.getOnce(
+ REPOS_URL + "/hitchhiker/puzzle42/permissions",
+ hitchhiker_puzzle42RepoPermissions
+ );
+
+ const expectedActions = [
+ {
+ type: FETCH_PERMISSIONS_PENDING,
+ payload: {
+ namespace: "hitchhiker",
+ repoName: "puzzle42"
+ },
+ itemId: "hitchhiker/puzzle42"
+ },
+ {
+ type: FETCH_PERMISSIONS_SUCCESS,
+ payload: hitchhiker_puzzle42RepoPermissions,
+ itemId: "hitchhiker/puzzle42"
+ }
+ ];
+
+ const store = mockStore({});
+ return store
+ .dispatch(fetchPermissions("hitchhiker", "puzzle42"))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+
+ it("should dispatch FETCH_PERMISSIONS_FAILURE, it the request fails", () => {
+ fetchMock.getOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
+ status: 500
+ });
+
+ const store = mockStore({});
+ return store
+ .dispatch(fetchPermissions("hitchhiker", "puzzle42"))
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0].type).toEqual(FETCH_PERMISSIONS_PENDING);
+ expect(actions[1].type).toEqual(FETCH_PERMISSIONS_FAILURE);
+ expect(actions[1].payload).toBeDefined();
+ });
+ });
+
+ it("should successfully modify user_eins permission", () => {
+ fetchMock.putOnce(
+ hitchhiker_puzzle42Permission_user_eins._links.update.href,
+ {
+ status: 204
+ }
+ );
+
+ let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
+ editedPermission.type = "OWNER";
+
+ const store = mockStore({});
+
+ return store
+ .dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42"))
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
+ expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS);
+ });
+ });
+
+ it("should successfully modify user_eins permission and call the callback", () => {
+ fetchMock.putOnce(
+ hitchhiker_puzzle42Permission_user_eins._links.update.href,
+ {
+ status: 204
+ }
+ );
+
+ let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
+ editedPermission.type = "OWNER";
+
+ const store = mockStore({});
+
+ let called = false;
+ const callback = () => {
+ called = true;
+ };
+
+ return store
+ .dispatch(
+ modifyPermission(editedPermission, "hitchhiker", "puzzle42", callback)
+ )
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
+ expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS);
+ expect(called).toBe(true);
+ });
+ });
+
+ it("should fail modifying on HTTP 500", () => {
+ fetchMock.putOnce(
+ hitchhiker_puzzle42Permission_user_eins._links.update.href,
+ {
+ status: 500
+ }
+ );
+
+ let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
+ editedPermission.type = "OWNER";
+
+ const store = mockStore({});
+
+ return store
+ .dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42"))
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
+ expect(actions[1].type).toEqual(MODIFY_PERMISSION_FAILURE);
+ expect(actions[1].payload).toBeDefined();
+ });
+ });
+
+ it("should add a permission successfully", () => {
+ // unmatched
+ fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
+ status: 204,
+ headers: {
+ location: "repositories/hitchhiker/puzzle42/permissions/user_eins"
+ }
+ });
+
+ fetchMock.getOnce(
+ REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins",
+ hitchhiker_puzzle42Permission_user_eins
+ );
+
+ const store = mockStore({});
+ return store
+ .dispatch(
+ createPermission(
+ hitchhiker_puzzle42Permission_user_eins,
+ "hitchhiker",
+ "puzzle42"
+ )
+ )
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING);
+ expect(actions[1].type).toEqual(CREATE_PERMISSION_SUCCESS);
+ });
+ });
+
+ it("should fail adding a permission on HTTP 500", () => {
+ fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
+ status: 500
+ });
+
+ const store = mockStore({});
+ return store
+ .dispatch(
+ createPermission(
+ hitchhiker_puzzle42Permission_user_eins,
+ "hitchhiker",
+ "puzzle42"
+ )
+ )
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING);
+ expect(actions[1].type).toEqual(CREATE_PERMISSION_FAILURE);
+ expect(actions[1].payload).toBeDefined();
+ });
+ });
+
+ it("should call the callback after permission successfully created", () => {
+ // unmatched
+ fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
+ status: 204,
+ headers: {
+ location: "repositories/hitchhiker/puzzle42/permissions/user_eins"
+ }
+ });
+
+ fetchMock.getOnce(
+ REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins",
+ hitchhiker_puzzle42Permission_user_eins
+ );
+ let callMe = "not yet";
+
+ const callback = () => {
+ callMe = "yeah";
+ };
+
+ const store = mockStore({});
+ return store
+ .dispatch(
+ createPermission(
+ hitchhiker_puzzle42Permission_user_eins,
+ "hitchhiker",
+ "puzzle42",
+ callback
+ )
+ )
+ .then(() => {
+ expect(callMe).toBe("yeah");
+ });
+ });
+ it("should delete successfully permission user_eins", () => {
+ fetchMock.deleteOnce(
+ hitchhiker_puzzle42Permission_user_eins._links.delete.href,
+ {
+ status: 204
+ }
+ );
+
+ const store = mockStore({});
+ return store
+ .dispatch(
+ deletePermission(
+ hitchhiker_puzzle42Permission_user_eins,
+ "hitchhiker",
+ "puzzle42"
+ )
+ )
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions.length).toBe(2);
+ expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING);
+ expect(actions[0].payload).toBe(
+ hitchhiker_puzzle42Permission_user_eins
+ );
+ expect(actions[1].type).toEqual(DELETE_PERMISSION_SUCCESS);
+ });
+ });
+
+ it("should call the callback, after successful delete", () => {
+ fetchMock.deleteOnce(
+ hitchhiker_puzzle42Permission_user_eins._links.delete.href,
+ {
+ status: 204
+ }
+ );
+
+ let called = false;
+ const callMe = () => {
+ called = true;
+ };
+
+ const store = mockStore({});
+ return store
+ .dispatch(
+ deletePermission(
+ hitchhiker_puzzle42Permission_user_eins,
+ "hitchhiker",
+ "puzzle42",
+ callMe
+ )
+ )
+ .then(() => {
+ expect(called).toBeTruthy();
+ });
+ });
+
+ it("should fail to delete permission", () => {
+ fetchMock.deleteOnce(
+ hitchhiker_puzzle42Permission_user_eins._links.delete.href,
+ {
+ status: 500
+ }
+ );
+
+ const store = mockStore({});
+ return store
+ .dispatch(
+ deletePermission(
+ hitchhiker_puzzle42Permission_user_eins,
+ "hitchhiker",
+ "puzzle42"
+ )
+ )
+ .then(() => {
+ const actions = store.getActions();
+ expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING);
+ expect(actions[0].payload).toBe(
+ hitchhiker_puzzle42Permission_user_eins
+ );
+ expect(actions[1].type).toEqual(DELETE_PERMISSION_FAILURE);
+ expect(actions[1].payload).toBeDefined();
+ });
+ });
+});
+
+describe("permissions reducer", () => {
+ it("should return empty object, if state and action is undefined", () => {
+ expect(reducer()).toEqual({});
+ });
+
+ it("should return the same state, if the action is undefined", () => {
+ const state = { x: true };
+ expect(reducer(state)).toBe(state);
+ });
+
+ it("should return the same state, if the action is unknown to the reducer", () => {
+ const state = { x: true };
+ expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state);
+ });
+
+ it("should store the permissions on FETCH_PERMISSION_SUCCESS", () => {
+ const newState = reducer(
+ {},
+ fetchPermissionsSuccess(
+ hitchhiker_puzzle42RepoPermissions,
+ "hitchhiker",
+ "puzzle42"
+ )
+ );
+
+ expect(newState["hitchhiker/puzzle42"].entries).toBe(
+ hitchhiker_puzzle42Permissions
+ );
+ });
+
+ it("should update permission", () => {
+ const oldState = {
+ "hitchhiker/puzzle42": {
+ entries: [hitchhiker_puzzle42Permission_user_eins]
+ }
+ };
+ let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins };
+ permissionEdited.type = "OWNER";
+ let expectedState = {
+ "hitchhiker/puzzle42": {
+ entries: [permissionEdited]
+ }
+ };
+ const newState = reducer(
+ oldState,
+ modifyPermissionSuccess(permissionEdited, "hitchhiker", "puzzle42")
+ );
+ expect(newState["hitchhiker/puzzle42"]).toEqual(
+ expectedState["hitchhiker/puzzle42"]
+ );
+ });
+
+ it("should remove permission from state when delete succeeds", () => {
+ const state = {
+ "hitchhiker/puzzle42": {
+ entries: [
+ hitchhiker_puzzle42Permission_user_eins,
+ hitchhiker_puzzle42Permission_user_zwei
+ ]
+ }
+ };
+
+ const expectedState = {
+ "hitchhiker/puzzle42": {
+ entries: [hitchhiker_puzzle42Permission_user_zwei]
+ }
+ };
+
+ const newState = reducer(
+ state,
+ deletePermissionSuccess(
+ hitchhiker_puzzle42Permission_user_eins,
+ "hitchhiker",
+ "puzzle42"
+ )
+ );
+ expect(newState["hitchhiker/puzzle42"]).toEqual(
+ expectedState["hitchhiker/puzzle42"]
+ );
+ });
+
+ it("should add permission", () => {
+ //changing state had to be removed because of errors
+ const oldState = {
+ "hitchhiker/puzzle42": {
+ entries: [hitchhiker_puzzle42Permission_user_eins]
+ }
+ };
+ let expectedState = {
+ "hitchhiker/puzzle42": {
+ entries: [
+ hitchhiker_puzzle42Permission_user_eins,
+ hitchhiker_puzzle42Permission_user_zwei
+ ]
+ }
+ };
+ const newState = reducer(
+ oldState,
+ createPermissionSuccess(
+ hitchhiker_puzzle42Permission_user_zwei,
+ "hitchhiker",
+ "puzzle42"
+ )
+ );
+ expect(newState["hitchhiker/puzzle42"]).toEqual(
+ expectedState["hitchhiker/puzzle42"]
+ );
+ });
+});
+
+describe("permissions selectors", () => {
+ const error = new Error("something goes wrong");
+
+ it("should return the permissions of one repository", () => {
+ const state = {
+ permissions: {
+ "hitchhiker/puzzle42": {
+ entries: hitchhiker_puzzle42Permissions
+ }
+ }
+ };
+
+ const repoPermissions = getPermissionsOfRepo(
+ state,
+ "hitchhiker",
+ "puzzle42"
+ );
+ expect(repoPermissions).toEqual(hitchhiker_puzzle42Permissions);
+ });
+
+ it("should return true, when fetch permissions is pending", () => {
+ const state = {
+ pending: {
+ [FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: true
+ }
+ };
+ expect(isFetchPermissionsPending(state, "hitchhiker", "puzzle42")).toEqual(
+ true
+ );
+ });
+
+ it("should return false, when fetch permissions is not pending", () => {
+ expect(isFetchPermissionsPending({}, "hitchiker", "puzzle42")).toEqual(
+ false
+ );
+ });
+
+ it("should return error when fetch permissions did fail", () => {
+ const state = {
+ failure: {
+ [FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: error
+ }
+ };
+ expect(getFetchPermissionsFailure(state, "hitchhiker", "puzzle42")).toEqual(
+ error
+ );
+ });
+
+ it("should return undefined when fetch permissions did not fail", () => {
+ expect(getFetchPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
+ undefined
+ );
+ });
+
+ it("should return true, when modify permission is pending", () => {
+ const state = {
+ pending: {
+ [MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true
+ }
+ };
+ expect(
+ isModifyPermissionPending(
+ state,
+ "hitchhiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toEqual(true);
+ });
+
+ it("should return false, when modify permission is not pending", () => {
+ expect(
+ isModifyPermissionPending(
+ {},
+ "hitchiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toEqual(false);
+ });
+
+ it("should return error when modify permission did fail", () => {
+ const state = {
+ failure: {
+ [MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
+ }
+ };
+ expect(
+ getModifyPermissionFailure(
+ state,
+ "hitchhiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toEqual(error);
+ });
+
+ it("should return undefined when modify permission did not fail", () => {
+ expect(
+ getModifyPermissionFailure(
+ {},
+ "hitchhiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toBe(undefined);
+ });
+
+ it("should return error when one of the modify permissions did fail", () => {
+ const state = {
+ permissions: {
+ "hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions }
+ },
+ failure: {
+ [MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
+ }
+ };
+ expect(
+ getModifyPermissionsFailure(state, "hitchhiker", "puzzle42")
+ ).toEqual(error);
+ });
+
+ it("should return undefined when no modify permissions did not fail", () => {
+ expect(getModifyPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
+ undefined
+ );
+ });
+
+ it("should return true, when createPermission is true", () => {
+ const state = {
+ permissions: {
+ ["hitchhiker/puzzle42"]: {
+ createPermission: true
+ }
+ }
+ };
+ expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toBe(true);
+ });
+
+ it("should return false, when createPermission is false", () => {
+ const state = {
+ permissions: {
+ ["hitchhiker/puzzle42"]: {
+ createPermission: false
+ }
+ }
+ };
+ expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toEqual(false);
+ });
+
+ it("should return true, when delete permission is pending", () => {
+ const state = {
+ pending: {
+ [DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true
+ }
+ };
+ expect(
+ isDeletePermissionPending(
+ state,
+ "hitchhiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toEqual(true);
+ });
+
+ it("should return false, when delete permission is not pending", () => {
+ expect(
+ isDeletePermissionPending(
+ {},
+ "hitchiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toEqual(false);
+ });
+
+ it("should return error when delete permission did fail", () => {
+ const state = {
+ failure: {
+ [DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
+ }
+ };
+ expect(
+ getDeletePermissionFailure(
+ state,
+ "hitchhiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toEqual(error);
+ });
+
+ it("should return undefined when delete permission did not fail", () => {
+ expect(
+ getDeletePermissionFailure(
+ {},
+ "hitchhiker",
+ "puzzle42",
+ hitchhiker_puzzle42Permission_user_eins
+ )
+ ).toBe(undefined);
+ });
+
+ it("should return error when one of the delete permissions did fail", () => {
+ const state = {
+ permissions: {
+ "hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions }
+ },
+ failure: {
+ [DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
+ }
+ };
+ expect(
+ getDeletePermissionsFailure(state, "hitchhiker", "puzzle42")
+ ).toEqual(error);
+ });
+
+ it("should return undefined when no delete permissions did not fail", () => {
+ expect(getDeletePermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
+ undefined
+ );
+ });
+
+ it("should return true, when create permission is pending", () => {
+ const state = {
+ pending: {
+ [CREATE_PERMISSION + "/hitchhiker/puzzle42"]: true
+ }
+ };
+ expect(isCreatePermissionPending(state, "hitchhiker", "puzzle42")).toEqual(
+ true
+ );
+ });
+
+ it("should return false, when create permissions is not pending", () => {
+ expect(isCreatePermissionPending({}, "hitchiker", "puzzle42")).toEqual(
+ false
+ );
+ });
+
+ it("should return error when create permissions did fail", () => {
+ const state = {
+ failure: {
+ [CREATE_PERMISSION + "/hitchhiker/puzzle42"]: error
+ }
+ };
+ expect(getCreatePermissionFailure(state, "hitchhiker", "puzzle42")).toEqual(
+ error
+ );
+ });
+
+ it("should return undefined when create permissions did not fail", () => {
+ expect(getCreatePermissionFailure({}, "hitchhiker", "puzzle42")).toBe(
+ undefined
+ );
+ });
+});
diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js
index 3e33dcffea..80ade5e070 100644
--- a/scm-ui/src/users/components/UserForm.js
+++ b/scm-ui/src/users/components/UserForm.js
@@ -97,6 +97,7 @@ class UserForm extends React.Component {
value={user ? user.name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.name-invalid")}
+ helpText={t("help.usernameHelpText")}
/>
);
}
@@ -109,6 +110,7 @@ class UserForm extends React.Component {
value={user ? user.displayName : ""}
validationError={this.state.displayNameValidationError}
errorMessage={t("validation.displayname-invalid")}
+ helpText={t("help.displayNameHelpText")}
/>
{
value={user ? user.mail : ""}
validationError={this.state.mailValidationError}
errorMessage={t("validation.mail-invalid")}
+ helpText={t("help.mailHelpText")}
/>
{
value={user ? user.password : ""}
validationError={this.state.validatePasswordError}
errorMessage={t("validation.password-invalid")}
+ helpText={t("help.passwordHelpText")}
/>
{
value={this.state ? this.state.validatePassword : ""}
validationError={this.state.passwordValidationError}
errorMessage={t("validation.passwordValidation-invalid")}
+ helpText={t("help.passwordConfirmHelpText")}
/>
{
return false;
};
export const isPasswordValid = (password: string) => {
- return password.length > 6 && password.length < 32;
+ return password.length >= 6 && password.length < 32;
};
diff --git a/scm-ui/src/users/modules/users.test.js b/scm-ui/src/users/modules/users.test.js
index c8c56f2ef5..c61d288c94 100644
--- a/scm-ui/src/users/modules/users.test.js
+++ b/scm-ui/src/users/modules/users.test.js
@@ -61,13 +61,13 @@ const userZaphod = {
properties: {},
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/users/zaphod"
+ href: "http://localhost:8081/api/v2/users/zaphod"
},
delete: {
- href: "http://localhost:8081/api/rest/v2/users/zaphod"
+ href: "http://localhost:8081/api/v2/users/zaphod"
},
update: {
- href: "http://localhost:8081/api/rest/v2/users/zaphod"
+ href: "http://localhost:8081/api/v2/users/zaphod"
}
}
};
@@ -84,13 +84,13 @@ const userFord = {
properties: {},
_links: {
self: {
- href: "http://localhost:8081/api/rest/v2/users/ford"
+ href: "http://localhost:8081/api/v2/users/ford"
},
delete: {
- href: "http://localhost:8081/api/rest/v2/users/ford"
+ href: "http://localhost:8081/api/v2/users/ford"
},
update: {
- href: "http://localhost:8081/api/rest/v2/users/ford"
+ href: "http://localhost:8081/api/v2/users/ford"
}
}
};
@@ -100,16 +100,16 @@ const responseBody = {
pageTotal: 1,
_links: {
self: {
- href: "http://localhost:3000/api/rest/v2/users/?page=0&pageSize=10"
+ href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
first: {
- href: "http://localhost:3000/api/rest/v2/users/?page=0&pageSize=10"
+ href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
last: {
- href: "http://localhost:3000/api/rest/v2/users/?page=0&pageSize=10"
+ href: "http://localhost:3000/api/v2/users/?page=0&pageSize=10"
},
create: {
- href: "http://localhost:3000/api/rest/v2/users/"
+ href: "http://localhost:3000/api/v2/users/"
}
},
_embedded: {
@@ -122,7 +122,7 @@ const response = {
responseBody
};
-const USERS_URL = "/api/rest/v2/users";
+const USERS_URL = "/api/v2/users";
const error = new Error("KAPUTT");
@@ -241,7 +241,7 @@ describe("users fetch()", () => {
});
it("successfully update user", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -255,7 +255,7 @@ describe("users fetch()", () => {
});
it("should call callback, after successful modified user", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -271,7 +271,7 @@ describe("users fetch()", () => {
});
it("should fail updating user on HTTP 500", () => {
- fetchMock.putOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
+ fetchMock.putOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 500
});
@@ -285,7 +285,7 @@ describe("users fetch()", () => {
});
it("should delete successfully user zaphod", () => {
- fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
+ fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -300,7 +300,7 @@ describe("users fetch()", () => {
});
it("should call the callback, after successful delete", () => {
- fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
+ fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 204
});
@@ -316,7 +316,7 @@ describe("users fetch()", () => {
});
it("should fail to delete user zaphod", () => {
- fetchMock.deleteOnce("http://localhost:8081/api/rest/v2/users/zaphod", {
+ fetchMock.deleteOnce("http://localhost:8081/api/v2/users/zaphod", {
status: 500
});
diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss
index c5559a8697..d2e5381a15 100644
--- a/scm-ui/styles/scm.scss
+++ b/scm-ui/styles/scm.scss
@@ -36,6 +36,7 @@ $blue: #33B2E8;
// 6. Import the rest of Bulma
@import "bulma/bulma";
+@import "bulma-tooltip/dist/css/bulma-tooltip";
// import at the end, because we need a lot of stuff from bulma/bulma
.box-link-shadow {
diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock
index dd761cc3c3..403f599ad6 100644
--- a/scm-ui/yarn.lock
+++ b/scm-ui/yarn.lock
@@ -1714,6 +1714,10 @@ builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+bulma-tooltip@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/bulma-tooltip/-/bulma-tooltip-2.0.2.tgz#cf0bf5ad2dc75492cbcbd4816e1a005314dc90ac"
+
bulma@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.1.tgz#73c2e3b2930c90cc272029cbd19918b493fca486"
@@ -5939,7 +5943,7 @@ node-sass-chokidar@^1.3.0:
sass-graph "^2.1.1"
stdout-stream "^1.4.0"
-node-sass@^4.9.2:
+node-sass@^4.9.2, node-sass@^4.9.3:
version "4.9.3"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.3.tgz#f407cf3d66f78308bb1e346b24fa428703196224"
dependencies:
diff --git a/scm-webapp/src/main/doc/enunciate.xml b/scm-webapp/src/main/doc/enunciate.xml
index 9bed97455a..6e7fc218aa 100644
--- a/scm-webapp/src/main/doc/enunciate.xml
+++ b/scm-webapp/src/main/doc/enunciate.xml
@@ -62,7 +62,7 @@
-
+
diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
index e9ec9e4a39..90764a7e00 100644
--- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
+++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java
@@ -152,7 +152,7 @@ public class ScmServletModule extends ServletModule
public static final String PATTERN_PLUGIN_SCRIPT = "/plugins/resources/js/*";
/** Field description */
- public static final String PATTERN_RESTAPI = "/api/rest/*";
+ public static final String PATTERN_RESTAPI = "/api/*";
/** Field description */
public static final String PATTERN_SCRIPT = "*.js";
diff --git a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java
index 059c990718..6b4a29961d 100644
--- a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java
+++ b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java
@@ -58,8 +58,10 @@ public class WebResourceServlet extends HttpServlet {
LOG.trace("try to load {}", uri);
URL url = webResourceLoader.getResource(uri);
if (url != null) {
+ LOG.trace("found {} -- serve as resource {}", uri, url);
serveResource(request, response, url);
} else {
+ LOG.trace("could not find {} -- dispatch", uri);
dispatch(request, response, uri);
}
}
@@ -79,6 +81,7 @@ public class WebResourceServlet extends HttpServlet {
private void serveResource(HttpServletRequest request, HttpServletResponse response, URL url) {
try {
+ LOG.debug("using sender to serve {}", request.getRequestURI());
sender.resource(url).send(request, response);
} catch (IOException ex) {
LOG.warn("failed to serve resource: {}", url);
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationConstraints.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationConstraints.java
new file mode 100644
index 0000000000..b136ee1bf7
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationConstraints.java
@@ -0,0 +1,14 @@
+package sonia.scm.api.v2;
+
+public final class ValidationConstraints {
+
+ private ValidationConstraints() {}
+
+ /**
+ * A user or group name should not start with @ or a whitespace
+ * and it not contains whitespaces
+ * and the characters: . - _ @ are allowed
+ */
+ public static final String USER_GROUP_PATTERN = "^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$";
+
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java
index afe8ad318b..8842d176fa 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java
@@ -12,12 +12,12 @@ public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToD
@Inject
public BranchChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
- super(changesetToChangesetDtoMapper);
+ super(changesetToChangesetDtoMapper, resourceLinks);
this.resourceLinks = resourceLinks;
}
public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, String branch) {
- return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, branch));
+ return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, branch), branch);
}
private String createSelfLink(Repository repository, String branch) {
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchReferenceDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchReferenceDto.java
new file mode 100644
index 0000000000..129d8d8bfa
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchReferenceDto.java
@@ -0,0 +1,19 @@
+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;
+
+@NoArgsConstructor @AllArgsConstructor @Getter @Setter
+public class BranchReferenceDto extends HalRepresentation {
+ private String name;
+
+ @Override
+ @SuppressWarnings("squid:S1185") // We want to have this method available in this package
+ protected HalRepresentation add(Links links) {
+ return super.add(links);
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java
index 24ee9b0ce1..87f3454e0f 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java
@@ -12,10 +12,13 @@ public class ChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapp
@Inject
public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
- super(changesetToChangesetDtoMapper);
+ super(changesetToChangesetDtoMapper, resourceLinks);
this.resourceLinks = resourceLinks;
}
+ public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, String branchName) {
+ return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository), branchName);
+ }
public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository) {
return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository));
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java
index e29a0a92b2..a34b7d44d6 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java
@@ -1,5 +1,6 @@
package sonia.scm.api.v2.resources;
+import de.otto.edison.hal.Links;
import sonia.scm.PageResult;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.Repository;
@@ -10,14 +11,28 @@ import java.util.function.Supplier;
class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper {
private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper;
+ private final ResourceLinks resourceLinks;
- ChangesetCollectionToDtoMapperBase(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper) {
+ ChangesetCollectionToDtoMapperBase(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
super("changesets");
this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper;
+ this.resourceLinks = resourceLinks;
}
CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, Supplier selfLinkSupplier) {
return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository));
}
-}
+ CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, Supplier selfLinkSupplier, String branchName) {
+ CollectionDto collectionDto = this.map(pageNumber, pageSize, pageResult, repository, selfLinkSupplier);
+ collectionDto.withEmbedded("branch", createBranchReferenceDto(repository, branchName));
+ return collectionDto;
+ }
+
+ private BranchReferenceDto createBranchReferenceDto(Repository repository, String branchName) {
+ BranchReferenceDto branchReferenceDto = new BranchReferenceDto();
+ branchReferenceDto.setName(branchName);
+ branchReferenceDto.add(Links.linkingTo().self(resourceLinks.branch().self(repository.getNamespaceAndName(), branchName)).build());
+ return branchReferenceDto;
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java
index 717b8d7198..396d055963 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java
@@ -65,7 +65,11 @@ public class ChangesetRootResource {
.getChangesets();
if (changesets != null && changesets.getChangesets() != null) {
PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal());
- return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build();
+ if (changesets.getBranchName() != null) {
+ return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, changesets.getBranchName())).build();
+ } else {
+ return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build();
+ }
} else {
return Response.ok().build();
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionDto.java
index c10e18267c..b59d697c2e 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionDto.java
@@ -15,4 +15,9 @@ class CollectionDto extends HalRepresentation {
CollectionDto(Links links, Embedded embedded) {
super(links, embedded);
}
+
+ @Override
+ protected HalRepresentation withEmbedded(String rel, HalRepresentation embeddedItem) {
+ return super.withEmbedded(rel, embeddedItem);
+ }
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java
index af7fb2ed83..57e5667c65 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java
@@ -13,7 +13,7 @@ public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMa
@Inject
public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) {
- super(changesetToChangesetDtoMapper);
+ super(changesetToChangesetDtoMapper, resourceLinks);
this.resourceLinks = resourceLinks;
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java
index b847412a33..760beab1da 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupDto.java
@@ -13,6 +13,8 @@ import java.time.Instant;
import java.util.List;
import java.util.Map;
+import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
+
@Getter @Setter @NoArgsConstructor
public class GroupDto extends HalRepresentation {
@@ -20,9 +22,8 @@ public class GroupDto extends HalRepresentation {
private String description;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified;
- @Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
+ @Pattern(regexp = USER_GROUP_PATTERN)
private String name;
- @NotEmpty
private String type;
private Map properties;
private List members;
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java
new file mode 100644
index 0000000000..9346420f58
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java
@@ -0,0 +1,16 @@
+package sonia.scm.api.v2.resources;
+
+import de.otto.edison.hal.HalRepresentation;
+import de.otto.edison.hal.Links;
+import lombok.Getter;
+
+@Getter
+public class IndexDto extends HalRepresentation {
+
+ private final String version;
+
+ IndexDto(String version, Links links) {
+ super(links);
+ this.version = version;
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java
new file mode 100644
index 0000000000..c8159f072c
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java
@@ -0,0 +1,50 @@
+package sonia.scm.api.v2.resources;
+
+import de.otto.edison.hal.Links;
+import org.apache.shiro.SecurityUtils;
+import sonia.scm.SCMContextProvider;
+import sonia.scm.config.ConfigurationPermissions;
+import sonia.scm.group.GroupPermissions;
+import sonia.scm.user.UserPermissions;
+
+import javax.inject.Inject;
+
+import static de.otto.edison.hal.Link.link;
+
+public class IndexDtoGenerator {
+
+ private final ResourceLinks resourceLinks;
+ private final SCMContextProvider scmContextProvider;
+
+ @Inject
+ public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) {
+ this.resourceLinks = resourceLinks;
+ this.scmContextProvider = scmContextProvider;
+ }
+
+ public IndexDto generate() {
+ Links.Builder builder = Links.linkingTo();
+ builder.self(resourceLinks.index().self());
+ builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self()));
+ if (SecurityUtils.getSubject().isAuthenticated()) {
+ builder.single(
+ link("me", resourceLinks.me().self()),
+ link("logout", resourceLinks.authentication().logout())
+ );
+ if (UserPermissions.list().isPermitted()) {
+ builder.single(link("users", resourceLinks.userCollection().self()));
+ }
+ if (GroupPermissions.list().isPermitted()) {
+ builder.single(link("groups", resourceLinks.groupCollection().self()));
+ }
+ if (ConfigurationPermissions.list().isPermitted()) {
+ builder.single(link("config", resourceLinks.config().self()));
+ }
+ builder.single(link("repositories", resourceLinks.repositoryCollection().self()));
+ } else {
+ builder.single(link("login", resourceLinks.authentication().jsonLogin()));
+ }
+
+ return new IndexDto(scmContextProvider.getVersion(), builder.build());
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java
new file mode 100644
index 0000000000..088558c7dc
--- /dev/null
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java
@@ -0,0 +1,29 @@
+package sonia.scm.api.v2.resources;
+
+import com.webcohesion.enunciate.metadata.rs.TypeHint;
+import sonia.scm.web.VndMediaType;
+
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+
+@Path(IndexResource.INDEX_PATH_V2)
+public class IndexResource {
+ public static final String INDEX_PATH_V2 = "v2/";
+
+ private final IndexDtoGenerator indexDtoGenerator;
+
+ @Inject
+ public IndexResource(IndexDtoGenerator indexDtoGenerator) {
+ this.indexDtoGenerator = indexDtoGenerator;
+ }
+
+ @GET
+ @Path("")
+ @Produces(VndMediaType.INDEX)
+ @TypeHint(IndexDto.class)
+ public IndexDto getIndex() {
+ return indexDtoGenerator.generate();
+ }
+}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java
index 581b2c24cd..82405a6ac2 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java
@@ -4,15 +4,20 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
+import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
-@Getter @Setter @ToString
+import javax.validation.constraints.Pattern;
+
+import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
+
+@Getter @Setter @ToString @NoArgsConstructor
public class PermissionDto extends HalRepresentation {
public static final String GROUP_PREFIX = "@";
- @JsonInclude(JsonInclude.Include.NON_NULL)
+ @Pattern(regexp = USER_GROUP_PATTERN)
private String name;
/**
@@ -28,9 +33,6 @@ public class PermissionDto extends HalRepresentation {
private boolean groupPermission = false;
- public PermissionDto() {
- }
-
public PermissionDto(String permissionName, boolean groupPermission) {
name = permissionName;
this.groupPermission = groupPermission;
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java
index b7f6df8c2d..af1ba3bf64 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java
@@ -16,6 +16,7 @@ import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
+import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
@@ -70,14 +71,15 @@ public class PermissionRootResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION)
@Path("")
- public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws Exception {
+ public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) throws AlreadyExistsException, NotFoundException {
log.info("try to add new permission: {}", permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
checkPermissionAlreadyExists(permission, repository);
repository.getPermissions().add(dtoToModelMapper.map(permission));
manager.modify(repository);
- return Response.created(URI.create(resourceLinks.permission().self(namespace, name, permission.getName()))).build();
+ String urlPermissionName = modelToDtoMapper.getUrlPermissionName(permission);
+ return Response.created(URI.create(resourceLinks.permission().self(namespace, name, urlPermissionName))).build();
}
@@ -156,7 +158,7 @@ public class PermissionRootResource {
public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName,
- PermissionDto permission) throws NotFoundException, AlreadyExistsException {
+ @Valid PermissionDto permission) throws NotFoundException, AlreadyExistsException {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Repository repository = load(namespace, name);
RepositoryPermissions.permissionWrite(repository).check();
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java
index 8ebe10eb6f..d6ab3721cf 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionToPermissionDtoMapper.java
@@ -41,9 +41,7 @@ public abstract class PermissionToPermissionDtoMapper {
*/
@AfterMapping
void appendLinks(@MappingTarget PermissionDto target, @Context Repository repository) {
- String permissionName = Optional.of(target.getName())
- .filter(p -> !target.isGroupPermission())
- .orElse(GROUP_PREFIX + target.getName());
+ String permissionName = getUrlPermissionName(target);
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.permission().self(repository.getNamespace(), repository.getName(), permissionName));
if (RepositoryPermissions.permissionWrite(repository).isPermitted()) {
@@ -52,4 +50,10 @@ public abstract class PermissionToPermissionDtoMapper {
}
target.add(linksBuilder.build());
}
+
+ public String getUrlPermissionName(PermissionDto permissionDto) {
+ return Optional.of(permissionDto.getName())
+ .filter(p -> !permissionDto.isGroupPermission())
+ .orElse(GROUP_PREFIX + permissionDto.getName());
+ }
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
index e978de443a..488577618c 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java
@@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName;
import javax.inject.Inject;
-import javax.ws.rs.core.UriInfo;
import java.net.URI;
class ResourceLinks {
@@ -441,7 +440,6 @@ class ResourceLinks {
}
}
-
public UIPluginLinks uiPlugin() {
return new UIPluginLinks(scmPathInfoStore.get());
}
@@ -473,4 +471,45 @@ class ResourceLinks {
return uiPluginCollectionLinkBuilder.method("plugins").parameters().method("getInstalledPlugins").parameters().href();
}
}
+
+ public AuthenticationLinks authentication() {
+ return new AuthenticationLinks(scmPathInfoStore.get());
+ }
+
+ static class AuthenticationLinks {
+ private final LinkBuilder loginLinkBuilder;
+
+ AuthenticationLinks(ScmPathInfo pathInfo) {
+ this.loginLinkBuilder = new LinkBuilder(pathInfo, AuthenticationResource.class);
+ }
+
+ String formLogin() {
+ return loginLinkBuilder.method("authenticateViaForm").parameters().href();
+ }
+
+ String jsonLogin() {
+ return loginLinkBuilder.method("authenticateViaJSONBody").parameters().href();
+ }
+
+ String logout() {
+ return loginLinkBuilder.method("logout").parameters().href();
+ }
+ }
+
+ public IndexLinks index() {
+ return new IndexLinks(scmPathInfoStore.get());
+ }
+
+ static class IndexLinks {
+ private final LinkBuilder indexLinkBuilder;
+
+ IndexLinks(ScmPathInfo pathInfo) {
+ indexLinkBuilder = new LinkBuilder(pathInfo, IndexResource.class);
+ }
+
+ String self() {
+ return indexLinkBuilder.method("getIndex").parameters().href();
+ }
+ }
+
}
diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java
index 4e4345445a..9dc5b850bd 100644
--- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java
+++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java
@@ -13,6 +13,8 @@ import javax.validation.constraints.Pattern;
import java.time.Instant;
import java.util.Map;
+import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
+
@NoArgsConstructor @Getter @Setter
public class UserDto extends HalRepresentation {
private boolean active;
@@ -24,7 +26,7 @@ public class UserDto extends HalRepresentation {
private Instant lastModified;
@NotEmpty @Email
private String mail;
- @Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
+ @Pattern(regexp = USER_GROUP_PATTERN)
private String name;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String password;
diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java
index de0d689c52..d97a5b050e 100644
--- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java
+++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java
@@ -84,7 +84,7 @@ public class SecurityFilter extends HttpFilter
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
- if (!SecurityRequests.isAuthenticationRequest(request))
+ if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request))
{
Subject subject = SecurityUtils.getSubject();
if (hasPermission(subject))
diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java
index 1b9abb058b..bf46eb2a6f 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java
@@ -56,6 +56,7 @@ import sonia.scm.plugin.Extension;
import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryDAO;
import sonia.scm.user.User;
+import sonia.scm.user.UserPermissions;
import sonia.scm.util.Util;
import java.util.List;
@@ -74,7 +75,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
// TODO move to util class
private static final String SEPARATOR = System.getProperty("line.separator", "\n");
-
+
/** Field description */
private static final String ADMIN_PERMISSION = "*";
@@ -88,7 +89,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
LoggerFactory.getLogger(DefaultAuthorizationCollector.class);
//~--- constructors ---------------------------------------------------------
-
+
/**
* Constructs ...
*
@@ -209,7 +210,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
String perm = permission.getType().getPermissionPrefix().concat(repository.getId());
if (logger.isTraceEnabled())
{
- logger.trace("add repository permission {} for user {} at repository {}",
+ logger.trace("add repository permission {} for user {} at repository {}",
perm, user.getName(), repository.getName());
}
@@ -254,6 +255,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
collectGlobalPermissions(builder, user, groups);
collectRepositoryPermissions(builder, user, groups);
+ builder.add(canReadOwnUser(user));
permissions = builder.build();
}
@@ -262,6 +264,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
return info;
}
+ private String canReadOwnUser(User user) {
+ return UserPermissions.read(user.getName()).asShiroString();
+ }
+
//~--- get methods ----------------------------------------------------------
private boolean isUserPermitted(User user, GroupNames groups,
@@ -272,7 +278,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|| ((!perm.isGroupPermission()) && user.getName().equals(perm.getName()));
//J+
}
-
+
@Subscribe
public void invalidateCache(AuthorizationChangedEvent event) {
if (event.isEveryUserAffected()) {
@@ -281,12 +287,12 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
invalidateCache();
}
}
-
+
private void invalidateUserCache(final String username) {
logger.info("invalidate cache for user {}, because of a received authorization event", username);
cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username));
}
-
+
private void invalidateCache() {
logger.info("invalidate cache, because of a received authorization event");
cache.clear();
diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java
index 81bb2092c9..49d03f598b 100644
--- a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java
+++ b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java
@@ -11,6 +11,7 @@ import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
public final class SecurityRequests {
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile(REST_API_PATH + "(?:/v2)?/auth/access_token");
+ private static final Pattern URI_INDEX_PATTERN = Pattern.compile(REST_API_PATH + "/v2/?");
private SecurityRequests() {}
@@ -23,4 +24,13 @@ public final class SecurityRequests {
return URI_LOGIN_PATTERN.matcher(uri).matches();
}
+ public static boolean isIndexRequest(HttpServletRequest request) {
+ String uri = request.getRequestURI().substring(request.getContextPath().length());
+ return isIndexRequest(uri);
+ }
+
+ public static boolean isIndexRequest(String uri) {
+ return URI_INDEX_PATTERN.matcher(uri).matches();
+ }
+
}
diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java
index d8fe469af9..c2444b43f5 100644
--- a/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java
+++ b/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java
@@ -99,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
throws IOException, ServletException
{
// skip filter on login resource
- if (SecurityRequests.isAuthenticationRequest(request))
+ if (SecurityRequests.isAuthenticationRequest(request) )
{
chain.doFilter(request, response);
}
diff --git a/scm-webapp/src/main/webapp/WEB-INF/web.xml b/scm-webapp/src/main/webapp/WEB-INF/web.xml
index b65f1f3c6b..8abba66aa3 100644
--- a/scm-webapp/src/main/webapp/WEB-INF/web.xml
+++ b/scm-webapp/src/main/webapp/WEB-INF/web.xml
@@ -59,7 +59,7 @@
resteasy.servlet.mapping.prefix
- /api/rest
+ /api
@@ -71,7 +71,7 @@
Resteasy
- /api/rest/*
+ /api/*
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/ValidationConstraints_IllegalCharactersTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/ValidationConstraints_IllegalCharactersTest.java
new file mode 100644
index 0000000000..c56f6195fe
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/ValidationConstraints_IllegalCharactersTest.java
@@ -0,0 +1,47 @@
+package sonia.scm.api.v2;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertFalse;
+import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN;
+
+@RunWith(Parameterized.class)
+public class ValidationConstraints_IllegalCharactersTest {
+
+ private static final List ACCEPTED_CHARS = asList('@', '_', '-', '.');
+
+ private final Pattern userGroupPattern=Pattern.compile(USER_GROUP_PATTERN);
+
+ private final String expression;
+
+ public ValidationConstraints_IllegalCharactersTest(String expression) {
+ this.expression = expression;
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection createParameters() {
+ return Stream.concat(IntStream.range(0x20, 0x2f).mapToObj(i -> (char) i), // chars before '0'
+ Stream.concat(IntStream.range(0x3a, 0x40).mapToObj(i -> (char) i), // chars between '9' and 'A'
+ Stream.concat(IntStream.range(0x5b, 0x60).mapToObj(i -> (char) i), // chars between 'Z' and 'a'
+ IntStream.range(0x7b, 0xff).mapToObj(i -> (char) i)))) // chars after 'z'
+ .filter(c -> !ACCEPTED_CHARS.contains(c))
+ .flatMap(c -> Stream.of("abc" + c + "xyz", "@" + c, c + "tail"))
+ .map(c -> new String[] {c})
+ .collect(Collectors.toList());
+ }
+
+ @Test
+ public void shouldNotAcceptSpecialCharacters() {
+ assertFalse(userGroupPattern.matcher(expression).matches());
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java
new file mode 100644
index 0000000000..69695279e6
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperTest.java
@@ -0,0 +1,61 @@
+package sonia.scm.api.v2.resources;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+import sonia.scm.PageResult;
+import sonia.scm.repository.Changeset;
+import sonia.scm.repository.Repository;
+
+import java.net.URI;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ChangesetCollectionToDtoMapperTest {
+
+ public static final Repository REPOSITORY = new Repository("", "git", "space", "name");
+ public static final Changeset CHANGESET = new Changeset();
+ private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper = mock(ChangesetToChangesetDtoMapper.class);
+
+ private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, ResourceLinksMock.createMock(URI.create("/")));
+
+ @Test
+ public void shouldMapCollectionEntries() {
+ ChangesetDto expectedChangesetDto = new ChangesetDto();
+ when(changesetToChangesetDtoMapper.map(CHANGESET, REPOSITORY)).thenReturn(expectedChangesetDto);
+
+ CollectionDto collectionDto = changesetCollectionToDtoMapper.map(0, 1, new PageResult<>(asList(CHANGESET), 1), REPOSITORY);
+
+ assertThat(collectionDto.getEmbedded().hasItem("changesets")).isTrue();
+ assertThat(collectionDto.getEmbedded().getItemsBy("changesets")).containsExactly(expectedChangesetDto);
+ assertThat(collectionDto.getEmbedded().hasItem("branch")).isFalse();
+ }
+
+ @Test
+ public void shouldNotEmbedBranchIfNotSpecified() {
+ ChangesetDto expectedChangesetDto = new ChangesetDto();
+ when(changesetToChangesetDtoMapper.map(CHANGESET, REPOSITORY)).thenReturn(expectedChangesetDto);
+
+ CollectionDto collectionDto = changesetCollectionToDtoMapper.map(0, 1, new PageResult<>(asList(CHANGESET), 1), REPOSITORY);
+
+ assertThat(collectionDto.getEmbedded().hasItem("branch")).isFalse();
+ }
+
+ @Test
+ public void shouldEmbedBranchIfSpecified() {
+ ChangesetDto expectedChangesetDto = new ChangesetDto();
+ when(changesetToChangesetDtoMapper.map(CHANGESET, REPOSITORY)).thenReturn(expectedChangesetDto);
+
+ CollectionDto collectionDto = changesetCollectionToDtoMapper.map(0, 1, new PageResult<>(asList(CHANGESET), 1), REPOSITORY, "someBranch");
+
+ assertThat(collectionDto.getEmbedded().hasItem("branch")).isTrue();
+ assertThat(collectionDto.getEmbedded().getItemsBy("branch"))
+ .hasSize(1)
+ .first().matches(b -> b.getLinks().getLinkBy("self").isPresent())
+ .extracting(b -> b.getLinks().getLinkBy("self").get().getHref()).first().isEqualTo("/v2/repositories/space/name/branches/someBranch");
+ assertThat(collectionDto.getEmbedded().getItemsBy("branch"))
+ .first().extracting("name").first().isEqualTo("someBranch");
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java
index f662542ff7..036579e9dd 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java
@@ -224,6 +224,64 @@ public class GroupRootResourceTest {
assertEquals("user1", createdGroup.getMembers().get(0));
}
+ @Test
+ public void shouldGet400OnCreatingNewGroupWithNotAllowedCharacters() throws URISyntaxException {
+ // the @ character at the begin of the name is not allowed
+ String groupJson = "{ \"name\": \"@grpname\", \"type\": \"admin\" }";
+ MockHttpRequest request = MockHttpRequest
+ .post("/" + GroupRootResource.GROUPS_PATH_V2)
+ .contentType(VndMediaType.GROUP)
+ .content(groupJson.getBytes());
+ MockHttpResponse response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+
+ // the whitespace at the begin of the name is not allowed
+ groupJson = "{ \"name\": \" grpname\", \"type\": \"admin\" }";
+ request = MockHttpRequest
+ .post("/" + GroupRootResource.GROUPS_PATH_V2)
+ .contentType(VndMediaType.GROUP)
+ .content(groupJson.getBytes());
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+
+ // the characters {[ are not allowed
+ groupJson = "{ \"name\": \"grp{name}\", \"type\": \"admin\" }";
+ request = MockHttpRequest
+ .post("/" + GroupRootResource.GROUPS_PATH_V2)
+ .contentType(VndMediaType.GROUP)
+ .content(groupJson.getBytes());
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+
+ groupJson = "{ \"name\": \"grp[name]\", \"type\": \"admin\" }";
+ request = MockHttpRequest
+ .post("/" + GroupRootResource.GROUPS_PATH_V2)
+ .contentType(VndMediaType.GROUP)
+ .content(groupJson.getBytes());
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+
+ groupJson = "{ \"name\": \"grp/name\", \"type\": \"admin\" }";
+ request = MockHttpRequest
+ .post("/" + GroupRootResource.GROUPS_PATH_V2)
+ .contentType(VndMediaType.GROUP)
+ .content(groupJson.getBytes());
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+
+ }
+
@Test
public void shouldFailForMissingContent() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java
new file mode 100644
index 0000000000..83697a3c4a
--- /dev/null
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java
@@ -0,0 +1,115 @@
+package sonia.scm.api.v2.resources;
+
+import com.github.sdorra.shiro.ShiroRule;
+import com.github.sdorra.shiro.SubjectAware;
+import org.assertj.core.api.Assertions;
+import org.junit.Rule;
+import org.junit.Test;
+import sonia.scm.SCMContextProvider;
+
+import java.net.URI;
+import java.util.Optional;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini")
+public class IndexResourceTest {
+
+ @Rule
+ public final ShiroRule shiroRule = new ShiroRule();
+
+ private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class);
+ private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider);
+ private final IndexResource indexResource = new IndexResource(indexDtoGenerator);
+
+ @Test
+ public void shouldRenderLoginUrlsForUnauthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent);
+ }
+
+ @Test
+ public void shouldRenderSelfLinkForUnauthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent);
+ }
+
+ @Test
+ public void shouldRenderUiPluginsLinkForUnauthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent);
+ }
+
+ @Test
+ @SubjectAware(username = "trillian", password = "secret")
+ public void shouldRenderSelfLinkForAuthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent);
+ }
+
+ @Test
+ @SubjectAware(username = "trillian", password = "secret")
+ public void shouldRenderUiPluginsLinkForAuthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent);
+ }
+
+ @Test
+ @SubjectAware(username = "trillian", password = "secret")
+ public void shouldRenderMeUrlForAuthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("me")).matches(Optional::isPresent);
+ }
+
+ @Test
+ @SubjectAware(username = "trillian", password = "secret")
+ public void shouldRenderLogoutUrlForAuthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("logout")).matches(Optional::isPresent);
+ }
+
+ @Test
+ @SubjectAware(username = "trillian", password = "secret")
+ public void shouldRenderRepositoriesForAuthenticatedRequest() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("repositories")).matches(Optional::isPresent);
+ }
+
+ @Test
+ @SubjectAware(username = "trillian", password = "secret")
+ public void shouldNotRenderAdminLinksIfNotAuthorized() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(o -> !o.isPresent());
+ Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(o -> !o.isPresent());
+ Assertions.assertThat(index.getLinks().getLinkBy("config")).matches(o -> !o.isPresent());
+ }
+
+ @Test
+ @SubjectAware(username = "dent", password = "secret")
+ public void shouldRenderAdminLinksIfAuthorized() {
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(Optional::isPresent);
+ Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(Optional::isPresent);
+ Assertions.assertThat(index.getLinks().getLinkBy("config")).matches(Optional::isPresent);
+ }
+
+ @Test
+ public void shouldGenerateVersion() {
+ when(scmContextProvider.getVersion()).thenReturn("v1");
+
+ IndexDto index = indexResource.getIndex();
+
+ Assertions.assertThat(index.getVersion()).isEqualTo("v1");
+ }
+}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java
index 563e9bdbc0..635d994763 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java
@@ -48,6 +48,7 @@ import java.util.stream.Stream;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.mockito.Matchers.any;
@@ -233,6 +234,35 @@ public class PermissionRootResourceTest extends RepositoryTestBase {
);
}
+
+ @Test
+ public void shouldGet400OnCreatingNewPermissionWithNotAllowedCharacters() throws URISyntaxException {
+ // the @ character at the begin of the name is not allowed
+ createUserWithRepository("user");
+ String permissionJson = "{ \"name\": \"@permission\", \"type\": \"OWNER\" }";
+ MockHttpRequest request = MockHttpRequest
+ .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
+ .content(permissionJson.getBytes())
+ .contentType(VndMediaType.PERMISSION);
+ MockHttpResponse response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+
+ // the whitespace at the begin opf the name is not allowed
+ permissionJson = "{ \"name\": \" permission\", \"type\": \"OWNER\" }";
+ request = MockHttpRequest
+ .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS)
+ .content(permissionJson.getBytes())
+ .contentType(VndMediaType.PERMISSION);
+ response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+ }
+
@Test
public void shouldGetCreatedPermissions() throws URISyntaxException {
createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE);
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java
index c70510fe39..eed46a1f6e 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java
@@ -34,6 +34,8 @@ public class ResourceLinksMock {
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo));
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo));
when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo));
+ when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo));
+ when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo));
return resourceLinks;
}
diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java
index 065a33313f..6ab1dc6aeb 100644
--- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java
@@ -98,6 +98,32 @@ public class UserRootResourceTest {
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}"));
}
+ @Test
+ public void shouldGet400OnCreatingNewUserWithNotAllowedCharacters() throws URISyntaxException {
+ // the @ character at the begin of the name is not allowed
+ String userJson = "{ \"name\": \"@user\", \"type\": \"db\" }";
+ MockHttpRequest request = MockHttpRequest
+ .post("/" + UserRootResource.USERS_PATH_V2)
+ .contentType(VndMediaType.USER)
+ .content(userJson.getBytes());
+ MockHttpResponse response = new MockHttpResponse();
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+
+ // the whitespace at the begin opf the name is not allowed
+ userJson = "{ \"name\": \" user\", \"type\": \"db\" }";
+ request = MockHttpRequest
+ .post("/" + UserRootResource.USERS_PATH_V2)
+ .contentType(VndMediaType.USER)
+ .content(userJson.getBytes());
+
+ dispatcher.invoke(request, response);
+
+ assertEquals(400, response.getStatus());
+ }
+
@Test
@SubjectAware(username = "unpriv")
public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException {
diff --git a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java
index fd217fd178..50aa6980f6 100644
--- a/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/filter/SecurityFilterTest.java
@@ -107,7 +107,7 @@ public class SecurityFilterTest {
*/
@Test
public void testDoOnAuthenticationUrlV1() throws IOException, ServletException {
- checkIfAuthenticationUrlIsPassedThrough("/scm/api/rest/auth/access_token");
+ checkIfAuthenticationUrlIsPassedThrough("/scm/api/auth/access_token");
}
/**
@@ -118,7 +118,7 @@ public class SecurityFilterTest {
*/
@Test
public void testDoOnAuthenticationUrlV2() throws IOException, ServletException {
- checkIfAuthenticationUrlIsPassedThrough("/scm/api/rest/v2/auth/access_token");
+ checkIfAuthenticationUrlIsPassedThrough("/scm/api/v2/auth/access_token");
}
private void checkIfAuthenticationUrlIsPassedThrough(String uri) throws IOException, ServletException {
diff --git a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java
index 75599770b3..1c1eb66417 100644
--- a/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java
+++ b/scm-webapp/src/test/java/sonia/scm/it/IntegrationTestUtil.java
@@ -78,7 +78,7 @@ public final class IntegrationTestUtil
public static final String BASE_URL = "http://localhost:8081/scm/";
/** scm-manager base url for the rest api */
- public static final String REST_BASE_URL = BASE_URL.concat("api/rest/v2/");
+ public static final String REST_BASE_URL = BASE_URL.concat("api/v2/");
//~--- constructors ---------------------------------------------------------
diff --git a/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java b/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java
index dc960c1b42..3c0e2eed52 100644
--- a/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java
+++ b/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java
@@ -37,6 +37,7 @@ package sonia.scm.it;
import com.sun.jersey.api.client.ClientResponse;
import de.otto.edison.hal.HalRepresentation;
+import org.junit.Assume;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import sonia.scm.api.rest.ObjectMapperProvider;
@@ -158,6 +159,7 @@ public class UserPermissionITCase extends AbstractPermissionITCaseBase
@Override
protected void checkGetAllResponse(ClientResponse response)
{
+ Assume.assumeTrue(credentials.getUsername() == null);
if (!credentials.isAnonymous())
{
assertNotNull(response);
diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java
index ed5689c9ce..5e7963ef1d 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java
@@ -57,6 +57,7 @@ import sonia.scm.repository.RepositoryTestData;
import sonia.scm.user.User;
import sonia.scm.user.UserTestData;
+import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
@@ -160,7 +161,8 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
- assertThat(authInfo.getStringPermissions(), hasSize(0));
+ assertThat(authInfo.getStringPermissions(), hasSize(1));
+ assertThat(authInfo.getStringPermissions(), contains("user:read:trillian"));
assertThat(authInfo.getObjectPermissions(), nullValue());
}
@@ -207,7 +209,7 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
assertThat(authInfo.getObjectPermissions(), nullValue());
- assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two"));
+ assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
}
/**
@@ -228,7 +230,7 @@ public class DefaultAuthorizationCollectorTest {
AuthorizationInfo authInfo = collector.collect();
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
assertThat(authInfo.getObjectPermissions(), nullValue());
- assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two"));
+ assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian"));
}
private void authenticate(User user, String group, String... groups) {
diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java
index d9d0e52a0c..0a02f3bd7f 100644
--- a/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java
+++ b/scm-webapp/src/test/java/sonia/scm/security/SecurityRequestsTest.java
@@ -21,7 +21,7 @@ public class SecurityRequestsTest {
@Test
public void testIsAuthenticationRequestWithContextPath() {
- when(request.getRequestURI()).thenReturn("/scm/api/rest/auth/access_token");
+ when(request.getRequestURI()).thenReturn("/scm/api/auth/access_token");
when(request.getContextPath()).thenReturn("/scm");
assertTrue(SecurityRequests.isAuthenticationRequest(request));
@@ -29,9 +29,9 @@ public class SecurityRequestsTest {
@Test
public void testIsAuthenticationRequest() throws Exception {
- assertTrue(SecurityRequests.isAuthenticationRequest("/api/rest/auth/access_token"));
- assertTrue(SecurityRequests.isAuthenticationRequest("/api/rest/v2/auth/access_token"));
- assertFalse(SecurityRequests.isAuthenticationRequest("/api/rest/repositories"));
- assertFalse(SecurityRequests.isAuthenticationRequest("/api/rest/v2/repositories"));
+ assertTrue(SecurityRequests.isAuthenticationRequest("/api/auth/access_token"));
+ assertTrue(SecurityRequests.isAuthenticationRequest("/api/v2/auth/access_token"));
+ assertFalse(SecurityRequests.isAuthenticationRequest("/api/repositories"));
+ assertFalse(SecurityRequests.isAuthenticationRequest("/api/v2/repositories"));
}
}