merge with 2.0.0-m3

This commit is contained in:
Sebastian Sdorra
2018-10-17 15:54:25 +02:00
161 changed files with 5951 additions and 10483 deletions

2
Jenkinsfile vendored
View File

@@ -125,7 +125,7 @@ boolean isMainBranch() {
boolean waitForQualityGateWebhookToBeCalled() {
boolean isQualityGateSucceeded = true
timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example
timeout(time: 5, unit: 'MINUTES') { // Needed when there is no webhook for example
def qGate = waitForQualityGate()
echo "SonarQube Quality Gate status: ${qGate.status}"
if (qGate.status != 'OK') {

View File

@@ -188,15 +188,14 @@
<scope>test</scope>
</dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId>
<version>${ssp.version}</version>
</dependency>
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId>
<version>${ssp.version}</version>
<optional>true</optional>
@@ -765,7 +764,7 @@
<jetty.maven.version>9.2.10.v20150310</jetty.maven.version>
<!-- security libraries -->
<ssp.version>967c8fd521</ssp.version>
<ssp.version>1.1.0</ssp.version>
<shiro.version>1.4.0</shiro.version>
<!-- repostitory libraries -->

View File

@@ -94,6 +94,12 @@
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>test</scope>
</dependency>
<!-- json -->
<dependency>
@@ -160,14 +166,13 @@
<scope>provided</scope>
</dependency>
<!-- TODO replace by proper version from maven central (group: com.github.sdorra) once its there. -->
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-lib</artifactId>
</dependency>
<dependency>
<groupId>com.github.sdorra.shiro-static-permissions</groupId>
<groupId>com.github.sdorra</groupId>
<artifactId>ssp-processor</artifactId>
<optional>true</optional>
</dependency>

View File

@@ -114,4 +114,5 @@ public interface GenericDAO<T>
* @return all items
*/
public Collection<T> getAll();
}

View File

@@ -47,6 +47,9 @@ public interface Manager<T extends ModelObject>
extends HandlerBase<T>, LastModifiedAware
{
int DEFAULT_LIMIT = 5;
/**
* Reloads a object from store and overwrites all changes.
*

View File

@@ -0,0 +1,15 @@
package sonia.scm;
/**
* This is a reduced form of a model object.
* It can be used as search result to avoid returning the whole object properties.
*
* @author Mohamed Karray
*/
public interface ReducedModelObject {
String getId();
String getDisplayName();
}

View File

@@ -4,11 +4,11 @@ import java.net.URI;
public interface ScmPathInfo {
String REST_API_PATH = "/api/rest";
String REST_API_PATH = "/api";
URI getApiRestUri();
default URI getRootUri() {
return getApiRestUri().resolve("../..");
return getApiRestUri().resolve("..");
}
}

View File

@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
@StaticPermissions(
value = "configuration",
permissions = {"read", "write"},
globalPermissions = {}
globalPermissions = {"list"}
)
public interface Configuration extends PermissionObject {
}

View File

@@ -1,125 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.filter;
//~--- non-JDK imports --------------------------------------------------------
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.Priority;
import sonia.scm.util.WebUtil;
import sonia.scm.web.filter.HttpFilter;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Filter for gzip encoding.
*
* @author Sebastian Sdorra
* @since 1.15
*/
@Priority(Filters.PRIORITY_PRE_BASEURL)
@WebElement(value = Filters.PATTERN_RESOURCE_REGEX, regex = true)
public class GZipFilter extends HttpFilter
{
/**
* the logger for GZipFilter
*/
private static final Logger logger =
LoggerFactory.getLogger(GZipFilter.class);
//~--- get methods ----------------------------------------------------------
/**
* Return the configuration for the gzip filter.
*
*
* @return gzip filter configuration
*/
public GZipFilterConfig getConfig()
{
return config;
}
//~--- methods --------------------------------------------------------------
/**
* Encodes the response, if the request has support for gzip encoding.
*
*
* @param request http request
* @param response http response
* @param chain filter chain
*
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilter(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
if (WebUtil.isGzipSupported(request))
{
if (logger.isTraceEnabled())
{
logger.trace("compress output with gzip");
}
GZipResponseWrapper wrappedResponse = new GZipResponseWrapper(response,
config);
chain.doFilter(request, wrappedResponse);
wrappedResponse.finishResponse();
}
else
{
chain.doFilter(request, response);
}
}
//~--- fields ---------------------------------------------------------------
/** gzip filter configuration */
private GZipFilterConfig config = new GZipFilterConfig();
}

View File

@@ -0,0 +1,24 @@
package sonia.scm.filter;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.util.WebUtil;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
@Provider
@Slf4j
public class GZipResponseFilter implements ContainerResponseFilter {
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
if (WebUtil.isGzipSupported(requestContext::getHeaderString)) {
log.trace("compress output with gzip");
GZIPOutputStream wrappedResponse = new GZIPOutputStream(responseContext.getEntityStream());
responseContext.getHeaders().add("Content-Encoding", "gzip");
responseContext.setEntityStream(wrappedResponse);
}
}
}

View File

@@ -42,6 +42,7 @@ import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject;
import sonia.scm.ReducedModelObject;
import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil;
@@ -55,16 +56,16 @@ import java.util.List;
/**
* Organizes users into a group for easier permissions management.
*
*
* TODO for 2.0: Use a set instead of a list for members
*
* @author Sebastian Sdorra
*/
@StaticPermissions("group")
@StaticPermissions(value = "group", globalPermissions = {"create", "list", "autocomplete"})
@XmlRootElement(name = "groups")
@XmlAccessorType(XmlAccessType.FIELD)
public class Group extends BasicPropertiesAware
implements ModelObject, PermissionObject
implements ModelObject, PermissionObject, ReducedModelObject
{
/** Field description */
@@ -309,6 +310,11 @@ public class Group extends BasicPropertiesAware
return name;
}
@Override
public String getDisplayName() {
return description;
}
/**
* Returns a timestamp of the last modified date of this group.
*

View File

@@ -61,4 +61,14 @@ public interface GroupManager
* @return all groups assigned to the given member
*/
public Collection<Group> getGroupsForMember(String member);
/**
* Returns a {@link java.util.Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<Group> autocomplete(String filter);
}

View File

@@ -109,6 +109,11 @@ public class GroupManagerDecorator
return decorated.getGroupsForMember(member);
}
@Override
public Collection<Group> autocomplete(String filter) {
return decorated.autocomplete(filter);
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -82,6 +82,22 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
{
this.total = total;
this.changesets = changesets;
this.branchName = null;
}
/**
* Constructs a new changeset paging result for a specific branch.
*
*
* @param total total number of changesets
* @param changesets current list of fetched changesets
* @param branchName branch name this result was created for
*/
public ChangesetPagingResult(int total, List<Changeset> changesets, String branchName)
{
this.total = total;
this.changesets = changesets;
this.branchName = branchName;
}
//~--- methods --------------------------------------------------------------
@@ -158,6 +174,7 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
return MoreObjects.toStringHelper(this)
.add("changesets", changesets)
.add("total", total)
.add("branch", branchName)
.toString();
//J+
}
@@ -186,37 +203,35 @@ public class ChangesetPagingResult implements Iterable<Changeset>, Serializable
return total;
}
//~--- set methods ----------------------------------------------------------
/**
* Sets the current list of changesets.
*
*
* @param changesets current list of changesets
*/
public void setChangesets(List<Changeset> changesets)
void setChangesets(List<Changeset> changesets)
{
this.changesets = changesets;
}
/**
* Sets the total number of changesets
*
*
* @param total total number of changesets
*/
public void setTotal(int total)
void setTotal(int total)
{
this.total = total;
}
void setBranchName(String branchName) {
this.branchName = branchName;
}
/**
* Returns the branch name this result was created for. This can either be an explicit branch ("give me all
* changesets for branch xyz") or an implicit one ("give me the changesets for the default").
*/
public String getBranchName() {
return branchName;
}
//~--- fields ---------------------------------------------------------------
/** current list of changesets */
@XmlElement(name = "changeset")
@XmlElementWrapper(name = "changesets")
private List<Changeset> changesets;
/** total number of changesets */
private int total;
private String branchName;
}

View File

@@ -64,7 +64,7 @@ import java.util.List;
)
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "repositories")
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject {
public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{
private static final long serialVersionUID = 3486560714961909711L;

View File

@@ -70,6 +70,12 @@ public class SearchRequest
this.ignoreCase = ignoreCase;
}
public SearchRequest(String query, boolean ignoreCase, int maxResults) {
this.query = query;
this.ignoreCase = ignoreCase;
this.maxResults = maxResults;
}
//~--- get methods ----------------------------------------------------------
/**

View File

@@ -41,6 +41,7 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import sonia.scm.BasicPropertiesAware;
import sonia.scm.ModelObject;
import sonia.scm.ReducedModelObject;
import sonia.scm.util.Util;
import sonia.scm.util.ValidationUtil;
@@ -55,11 +56,10 @@ import java.security.Principal;
*
* @author Sebastian Sdorra
*/
@StaticPermissions("user")
@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"})
@XmlRootElement(name = "users")
@XmlAccessorType(XmlAccessType.FIELD)
public class
User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
{
/** Field description */

View File

@@ -39,6 +39,7 @@ import sonia.scm.Manager;
import sonia.scm.search.Searchable;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.function.Consumer;
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
@@ -90,5 +91,13 @@ public interface UserManager
return getDefaultType().equals(user.getType());
}
/**
* Returns a {@link java.util.Collection} of filtered objects
*
* @param filter the searched string
* @return filtered object from the store
*/
Collection<User> autocomplete(String filter);
}

View File

@@ -121,6 +121,11 @@ public class UserManagerDecorator extends ManagerDecorator<User>
return decorated.getDefaultType();
}
@Override
public Collection<User> autocomplete(String filter) {
return decorated.autocomplete(filter);
}
//~--- fields ---------------------------------------------------------------
/** Field description */

View File

@@ -49,6 +49,7 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -266,7 +267,12 @@ public final class WebUtil
*/
public static boolean isGzipSupported(HttpServletRequest request)
{
String enc = request.getHeader(HEADER_ACCEPTENCODING);
return isGzipSupported(request::getHeader);
}
public static boolean isGzipSupported(Function<String, String> headerResolver)
{
String enc = headerResolver.apply(HEADER_ACCEPTENCODING);
return (enc != null) && enc.contains("gzip");
}

View File

@@ -0,0 +1,40 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Map;
public abstract class JsonEnricherBase implements JsonEnricher {
private final ObjectMapper objectMapper;
protected JsonEnricherBase(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) {
return mediaType.equals(context.getResponseMediaType().toString());
}
protected JsonNode value(Object object) {
return objectMapper.convertValue(object, JsonNode.class);
}
protected ObjectNode createObject() {
return objectMapper.createObjectNode();
}
protected ObjectNode createObject(Map<String, Object> values) {
ObjectNode object = createObject();
values.forEach((key, value) -> object.set(key, value(value)));
return object;
}
protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) {
((ObjectNode) parent).set(newKey, child);
}
}

View File

@@ -15,8 +15,10 @@ public class VndMediaType {
public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX;
public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION;
public static final String INDEX = PREFIX + "index" + SUFFIX;
public static final String USER = PREFIX + "user" + SUFFIX;
public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;

View File

@@ -114,7 +114,7 @@ public class InitializingHttpScmProtocolWrapperTest {
}
private OngoingStubbing<ScmPathInfo> mockSetPathInfo() {
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/"));
return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/"));
}
}

View File

@@ -0,0 +1,51 @@
package sonia.scm.web;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
import javax.ws.rs.core.MediaType;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
public class JsonEnricherBaseTest {
private ObjectMapper objectMapper = new ObjectMapper();
private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper);
@Test
public void testResultHasMediaType() {
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null);
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue();
assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse();
}
@Test
public void testAppendLink() {
ObjectNode root = objectMapper.createObjectNode();
ObjectNode links = objectMapper.createObjectNode();
root.set("_links", links);
JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root);
enricher.enrich(context);
assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link");
}
private static class TestJsonEnricher extends JsonEnricherBase {
public TestJsonEnricher(ObjectMapper objectMapper) {
super(objectMapper);
}
@Override
public void enrich(JsonEnricherContext context) {
JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link")));
addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode);
}
}
}

View File

@@ -35,17 +35,20 @@ package sonia.scm.xml;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableList;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.GenericDAO;
import sonia.scm.ModelObject;
import sonia.scm.group.xml.XmlGroupDAO;
//~--- JDK imports ------------------------------------------------------------
import sonia.scm.store.ConfigurationStore;
import sonia.scm.util.AssertUtil;
import java.util.Collection;
import sonia.scm.store.ConfigurationStore;
import java.util.stream.Collectors;
//~--- JDK imports ------------------------------------------------------------
/**
*

View File

@@ -0,0 +1,73 @@
package sonia.scm.it;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
public class AutoCompleteITCase {
public static final String CREATED_USER_PREFIX = "user_";
public static final String CREATED_GROUP_PREFIX = "group_";
@Before
public void init() {
TestData.cleanup();
}
@Test
public void adminShouldAutoComplete() {
shouldAutocomplete(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN);
}
@Test
public void userShouldAutoComplete() {
String username = "nonAdmin";
String password = "pass";
TestData.createUser(username, password, false, "xml", "email@e.de");
shouldAutocomplete(username, password);
}
public void shouldAutocomplete(String username, String password) {
createUsers();
createGroups();
ScmRequests.start()
.requestIndexResource(username, password)
.assertStatusCode(200)
.requestAutoCompleteGroups("group*")
.assertStatusCode(200)
.assertAutoCompleteResults(assertAutoCompleteResult(CREATED_GROUP_PREFIX))
.returnToPrevious()
.requestAutoCompleteUsers("user*")
.assertStatusCode(200)
.assertAutoCompleteResults(assertAutoCompleteResult(CREATED_USER_PREFIX));
}
@SuppressWarnings("unchecked")
private Consumer<List<Map>> assertAutoCompleteResult(String id) {
return autoCompleteDtos -> {
IntStream.range(0, 5).forEach(i -> {
assertThat(autoCompleteDtos).as("return maximum 5 entries").hasSize(5);
assertThat(autoCompleteDtos.get(i)).containsEntry("id", id + (i + 1));
assertThat(autoCompleteDtos.get(i)).containsEntry("displayName", id + (i + 1));
});
};
}
private void createUsers() {
IntStream.range(0, 6).forEach(i -> TestData.createUser(CREATED_USER_PREFIX + (i + 1), "pass", false, "xml", CREATED_USER_PREFIX + (i + 1) + "@scm-manager.org"));
}
private void createGroups() {
IntStream.range(0, 6).forEach(i -> TestData.createGroup(CREATED_GROUP_PREFIX + (i + 1), CREATED_GROUP_PREFIX + (i + 1)));
}
}

View File

@@ -0,0 +1,48 @@
package sonia.scm.it;
import io.restassured.RestAssured;
import org.apache.http.HttpStatus;
import org.junit.Test;
import sonia.scm.it.utils.RestUtil;
import sonia.scm.web.VndMediaType;
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
import static sonia.scm.it.utils.RestUtil.given;
public class IndexITCase {
@Test
public void shouldLinkEverythingForAdmin() {
given(VndMediaType.INDEX)
.when()
.get(RestUtil.createResourceUrl(""))
.then()
.statusCode(HttpStatus.SC_OK)
.body(
"_links.repositories.href", matchesPattern(".+/repositories/"),
"_links.users.href", matchesPattern(".+/users/"),
"_links.groups.href", matchesPattern(".+/groups/"),
"_links.config.href", matchesPattern(".+/config"),
"_links.gitConfig.href", matchesPattern(".+/config/git"),
"_links.hgConfig.href", matchesPattern(".+/config/hg"),
"_links.svnConfig.href", matchesPattern(".+/config/svn")
);
}
@Test
public void shouldCreateLoginLinksForAnonymousAccess() {
RestAssured.given() // do not specify user credentials
.when()
.get(RestUtil.createResourceUrl(""))
.then()
.statusCode(HttpStatus.SC_OK)
.body(
"_links.login.href", matchesPattern(".+/auth/.+")
);
}
}

View File

@@ -20,12 +20,9 @@ public class MeITCase {
String newPassword = TestData.USER_SCM_ADMIN + "1";
// admin change the own password
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.getMeResource()
.requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.requestMe()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo("xml"))
@@ -33,12 +30,9 @@ public class MeITCase {
.assertStatusCode(204);
// assert password is changed -> login with the new Password than undo changes
ScmRequests.start()
.given()
.url(TestData.getUserUrl(TestData.USER_SCM_ADMIN))
.usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword)
.getMeResource()
.requestIndexResource(TestData.USER_SCM_ADMIN, newPassword)
.requestMe()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
.requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
.assertStatusCode(204);
@@ -49,31 +43,14 @@ public class MeITCase {
String newUser = "user";
String password = "pass";
String type = "not XML Type";
TestData.createUser(newUser, password, true, type);
TestData.createUser(newUser, password, true, type, "user@scm-manager.org");
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.requestIndexResource(newUser, password)
.requestMe()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(403);
}
}

View File

@@ -87,13 +87,13 @@ public class PermissionsITCase {
@Before
public void prepareEnvironment() {
TestData.createDefault();
TestData.createUser(USER_READ, USER_PASS);
TestData.createNotAdminUser(USER_READ, USER_PASS);
TestData.createUserPermission(USER_READ, PermissionType.READ, repositoryType);
TestData.createUser(USER_WRITE, USER_PASS);
TestData.createNotAdminUser(USER_WRITE, USER_PASS);
TestData.createUserPermission(USER_WRITE, PermissionType.WRITE, repositoryType);
TestData.createUser(USER_OWNER, USER_PASS);
TestData.createNotAdminUser(USER_OWNER, USER_PASS);
TestData.createUserPermission(USER_OWNER, PermissionType.OWNER, repositoryType);
TestData.createUser(USER_OTHER, USER_PASS);
TestData.createNotAdminUser(USER_OTHER, USER_PASS);
createdPermissions = 3;
}

View File

@@ -36,6 +36,7 @@ package sonia.scm.it;
import org.apache.http.HttpStatus;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

View File

@@ -44,7 +44,7 @@ public class RepositoryAccessITCase {
private final String repositoryType;
private File folder;
private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
private ScmRequests.RepositoryResponse<ScmRequests.IndexResponse> repositoryResponse;
public RepositoryAccessITCase(String repositoryType) {
this.repositoryType = repositoryType;
@@ -59,17 +59,13 @@ public class RepositoryAccessITCase {
public void init() {
TestData.createDefault();
folder = tempFolder.getRoot();
repositoryGetRequest = ScmRequests.start()
.given()
.url(TestData.getDefaultRepositoryUrl(repositoryType))
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.getRepositoryResource()
String namespace = ADMIN_USERNAME;
String repo = TestData.getDefaultRepoName(repositoryType);
repositoryResponse =
ScmRequests.start()
.requestIndexResource(ADMIN_USERNAME, ADMIN_PASSWORD)
.requestRepository(namespace, repo)
.assertStatusCode(HttpStatus.SC_OK);
ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.getMeResource();
}
@Test
@@ -306,17 +302,12 @@ public class RepositoryAccessITCase {
public void shouldFindFileHistory() throws IOException {
RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder);
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a");
repositoryGetRequest
.usingRepositoryResponse()
repositoryResponse
.requestSources()
.usingSourcesResponse()
.requestSelf("folder")
.usingSourcesResponse()
.requestSelf("subfolder")
.usingSourcesResponse()
.requestFileHistory("a.txt")
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.assertChangesets(changesets -> {
assertThat(changesets).hasSize(1);
assertThat(changesets.get(0)).containsEntry("id", changeset.getId());
@@ -332,14 +323,11 @@ public class RepositoryAccessITCase {
String fileName = "a.txt";
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a");
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
repositoryResponse
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(addedFiles -> assertThat(addedFiles)
.hasSize(1)
@@ -359,14 +347,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName);
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
repositoryResponse
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertRemoved(removedFiles -> assertThat(removedFiles)
.hasSize(1)
@@ -386,14 +371,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content");
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
repositoryResponse
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertModified(modifiedFiles -> assertThat(modifiedFiles)
.hasSize(1)
@@ -423,14 +405,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
repositoryResponse
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(a -> assertThat(a)
.hasSize(1)
@@ -463,14 +442,11 @@ public class RepositoryAccessITCase {
Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles);
String revision = changeset.getId();
repositoryGetRequest
.usingRepositoryResponse()
repositoryResponse
.requestChangesets()
.assertStatusCode(HttpStatus.SC_OK)
.usingChangesetsResponse()
.requestModifications(revision)
.assertStatusCode(HttpStatus.SC_OK)
.usingModificationsResponse()
.assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision))
.assertAdded(a -> assertThat(a)
.hasSize(3)

View File

@@ -19,57 +19,48 @@ public class UserITCase {
public void adminShouldChangeOwnPassword() {
String newUser = "user";
String password = "pass";
TestData.createUser(newUser, password, true, "xml");
TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org");
String newPassword = "new_password";
// admin change the own password
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, password)
.getUserResource()
.requestIndexResource(newUser, password)
.assertStatusCode(200)
.requestUser(newUser)
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
.assertStatusCode(204);
// assert password is changed -> login with the new Password
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, newPassword)
.getUserResource()
.requestIndexResource(newUser, newPassword)
.assertStatusCode(200)
.usingUserResponse()
.requestUser(newUser)
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull);
}
@Test
public void adminShouldChangePasswordOfOtherUser() {
String newUser = "user";
String password = "pass";
TestData.createUser(newUser, password, true, "xml");
TestData.createUser(newUser, password, true, "xml", "user@scm-manager.org");
String newPassword = "new_password";
// admin change the password of the user
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))// the admin get the user object
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.getUserResource()
.requestIndexResource(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.assertStatusCode(200)
.requestUser(newUser)
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
.assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
.assertStatusCode(204);
// assert password is changed
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, newPassword)
.getUserResource()
.requestIndexResource(newUser, newPassword)
.assertStatusCode(200)
.requestUser(newUser)
.assertStatusCode(200);
}
@@ -80,34 +71,15 @@ public class UserITCase {
String newUser = "user";
String password = "pass";
String type = "not XML Type";
TestData.createUser(newUser, password, true, type);
TestData.createUser(newUser, password, true, type, "user@scm-manager.org");
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.requestIndexResource(newUser, password)
.assertStatusCode(200)
.requestUser(newUser)
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(403);
}
}

View File

@@ -24,6 +24,6 @@ public class RegExMatcher extends BaseMatcher<String> {
@Override
public boolean matches(Object o) {
return Pattern.compile(pattern).matcher(o.toString()).matches();
return o != null && Pattern.compile(pattern).matcher(o.toString()).matches();
}
}

View File

@@ -10,7 +10,7 @@ import static java.net.URI.create;
public class RestUtil {
public static final URI BASE_URL = create("http://localhost:8081/scm/");
public static final URI REST_BASE_URL = BASE_URL.resolve("api/rest/v2/");
public static final URI REST_BASE_URL = BASE_URL.resolve("api/v2/");
public static URI createResourceUrl(String path) {
return REST_BASE_URL.resolve(path);

View File

@@ -4,7 +4,6 @@ import io.restassured.RestAssured;
import io.restassured.response.Response;
import sonia.scm.web.VndMediaType;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@@ -33,8 +32,10 @@ public class ScmRequests {
return new ScmRequests();
}
public Given given() {
return new Given();
public IndexResponse requestIndexResource(String username, String password) {
setUsername(username);
setPassword(password);
return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
}
@@ -46,24 +47,46 @@ public class ScmRequests {
* @return the response of the GET request using the given link
*/
private Response applyGETRequestFromLink(Response response, String linkPropertyName) {
return applyGETRequest(response
.then()
.extract()
.path(linkPropertyName));
return applyGETRequestFromLinkWithParams(response, linkPropertyName, "");
}
/**
* Apply a GET Request to the extracted url from the given link
*
* @param linkPropertyName the property name of link
* @param response the response containing the link
* @param params query params eg. ?q=xyz&count=12 or path params eg. namespace/name
* @return the response of the GET request using the given link
*/
private Response applyGETRequestFromLinkWithParams(Response response, String linkPropertyName, String params) {
return applyGETRequestWithQueryParams(response
.then()
.extract()
.path(linkPropertyName), params);
}
/**
* Apply a GET Request to the given <code>url</code> and return the response.
*
* @param url the url of the GET request
* @param params query params eg. ?q=xyz&count=12 or path params eg. namespace/name
* @return the response of the GET request using the given <code>url</code>
*/
private Response applyGETRequestWithQueryParams(String url, String params) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.get(url + params);
}
/**
* Apply a GET Request to the given <code>url</code> and return the response.
*
* @param url the url of the GET request
* @return the response of the GET request using the given <code>url</code>
*/
**/
private Response applyGETRequest(String url) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.get(url);
return applyGETRequestWithQueryParams(url, "");
}
@@ -101,11 +124,6 @@ public class ScmRequests {
.put(url);
}
private void setUrl(String url) {
this.url = url;
}
private void setUsername(String username) {
this.username = username;
}
@@ -114,296 +132,185 @@ public class ScmRequests {
this.password = password;
}
private String getUrl() {
return url;
}
private String getUsername() {
return username;
}
public class IndexResponse extends ModelResponse<IndexResponse, IndexResponse> {
public static final String LINK_AUTOCOMPLETE_USERS = "_links.autocomplete.find{it.name=='users'}.href";
public static final String LINK_AUTOCOMPLETE_GROUPS = "_links.autocomplete.find{it.name=='groups'}.href";
public static final String LINK_REPOSITORIES = "_links.repositories.href";
private static final String LINK_ME = "_links.me.href";
private static final String LINK_USERS = "_links.users.href";
private String getPassword() {
return password;
}
public class Given {
public GivenUrl url(String url) {
setUrl(url);
return new GivenUrl();
public IndexResponse(Response response) {
super(response, null);
}
public GivenUrl url(URI url) {
setUrl(url.toString());
return new GivenUrl();
public AutoCompleteResponse<IndexResponse> requestAutoCompleteUsers(String q) {
return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_USERS, "?q=" + q), this);
}
public AutoCompleteResponse<IndexResponse> requestAutoCompleteGroups(String q) {
return new AutoCompleteResponse<>(applyGETRequestFromLinkWithParams(response, LINK_AUTOCOMPLETE_GROUPS, "?q=" + q), this);
}
public RepositoryResponse<IndexResponse> requestRepository(String namespace, String name) {
return new RepositoryResponse<>(applyGETRequestFromLinkWithParams(response, LINK_REPOSITORIES, namespace + "/" + name), this);
}
public MeResponse<IndexResponse> requestMe() {
return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this);
}
public UserResponse<IndexResponse> requestUser(String username) {
return new UserResponse<>(applyGETRequestFromLinkWithParams(response, LINK_USERS, username), this);
}
}
public class GivenWithUrlAndAuth {
public AppliedMeRequest getMeResource() {
return new AppliedMeRequest(applyGETRequest(url));
public class RepositoryResponse<PREV extends ModelResponse> extends ModelResponse<RepositoryResponse<PREV>, PREV> {
public static final String LINKS_SOURCES = "_links.sources.href";
public static final String LINKS_CHANGESETS = "_links.changesets.href";
public RepositoryResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
public AppliedUserRequest getUserResource() {
return new AppliedUserRequest(applyGETRequest(url));
public SourcesResponse<RepositoryResponse> requestSources() {
return new SourcesResponse<>(applyGETRequestFromLink(response, LINKS_SOURCES), this);
}
public AppliedRepositoryRequest getRepositoryResource() {
return new AppliedRepositoryRequest(
applyGETRequest(url)
);
}
}
public class AppliedRequest<SELF extends AppliedRequest> {
private Response response;
public AppliedRequest(Response response) {
this.response = response;
}
/**
* apply custom assertions to the actual response
*
* @param consumer consume the response in order to assert the content. the header, the payload etc..
* @return the self object
*/
public SELF assertResponse(Consumer<Response> consumer) {
consumer.accept(response);
return (SELF) this;
}
/**
* special assertion of the status code
*
* @param expectedStatusCode the expected status code
* @return the self object
*/
public SELF assertStatusCode(int expectedStatusCode) {
this.response.then().assertThat().statusCode(expectedStatusCode);
return (SELF) this;
public ChangesetsResponse<RepositoryResponse> requestChangesets() {
return new ChangesetsResponse<>(applyGETRequestFromLink(response, LINKS_CHANGESETS), this);
}
}
public class AppliedRepositoryRequest extends AppliedRequest<AppliedRepositoryRequest> {
public class ChangesetsResponse<PREV extends ModelResponse> extends ModelResponse<ChangesetsResponse<PREV>, PREV> {
public AppliedRepositoryRequest(Response response) {
super(response);
}
public RepositoryResponse usingRepositoryResponse() {
return new RepositoryResponse(super.response);
}
}
public class RepositoryResponse {
private Response repositoryResponse;
public RepositoryResponse(Response repositoryResponse) {
this.repositoryResponse = repositoryResponse;
}
public AppliedSourcesRequest requestSources() {
return new AppliedSourcesRequest(applyGETRequestFromLink(repositoryResponse, "_links.sources.href"));
}
public AppliedChangesetsRequest requestChangesets() {
return new AppliedChangesetsRequest(applyGETRequestFromLink(repositoryResponse, "_links.changesets.href"));
}
}
public class AppliedChangesetsRequest extends AppliedRequest<AppliedChangesetsRequest> {
public AppliedChangesetsRequest(Response response) {
super(response);
}
public ChangesetsResponse usingChangesetsResponse() {
return new ChangesetsResponse(super.response);
}
}
public class ChangesetsResponse {
private Response changesetsResponse;
public ChangesetsResponse(Response changesetsResponse) {
this.changesetsResponse = changesetsResponse;
public ChangesetsResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
public ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
List<Map> changesets = response.then().extract().path("_embedded.changesets");
changesetsConsumer.accept(changesets);
return this;
}
public AppliedDiffRequest requestDiff(String revision) {
return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
public DiffResponse<ChangesetsResponse> requestDiff(String revision) {
return new DiffResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"), this);
}
public AppliedModificationsRequest requestModifications(String revision) {
return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
public ModificationsResponse<ChangesetsResponse> requestModifications(String revision) {
return new ModificationsResponse<>(applyGETRequestFromLink(response, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"), this);
}
}
public class AppliedSourcesRequest extends AppliedRequest<AppliedSourcesRequest> {
public AppliedSourcesRequest(Response sourcesResponse) {
super(sourcesResponse);
}
public class SourcesResponse<PREV extends ModelResponse> extends ModelResponse<SourcesResponse<PREV>, PREV> {
public SourcesResponse usingSourcesResponse() {
return new SourcesResponse(super.response);
}
}
public class SourcesResponse {
private Response sourcesResponse;
public SourcesResponse(Response sourcesResponse) {
this.sourcesResponse = sourcesResponse;
public SourcesResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
public SourcesResponse assertRevision(Consumer<String> assertRevision) {
String revision = sourcesResponse.then().extract().path("revision");
String revision = response.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public SourcesResponse assertFiles(Consumer<List> assertFiles) {
List files = sourcesResponse.then().extract().path("files");
List files = response.then().extract().path("files");
assertFiles.accept(files);
return this;
}
public AppliedChangesetsRequest requestFileHistory(String fileName) {
return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
public ChangesetsResponse<SourcesResponse> requestFileHistory(String fileName) {
return new ChangesetsResponse<>(applyGETRequestFromLink(response, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"), this);
}
public AppliedSourcesRequest requestSelf(String fileName) {
return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
public SourcesResponse<SourcesResponse> requestSelf(String fileName) {
return new SourcesResponse<>(applyGETRequestFromLink(response, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"), this);
}
}
public class AppliedDiffRequest extends AppliedRequest<AppliedDiffRequest> {
public class ModificationsResponse<PREV extends ModelResponse> extends ModelResponse<ModificationsResponse<PREV>, PREV> {
public AppliedDiffRequest(Response response) {
super(response);
}
}
public class GivenUrl {
public GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
setUsername(username);
setPassword(password);
return new GivenWithUrlAndAuth();
}
}
public class AppliedModificationsRequest extends AppliedRequest<AppliedModificationsRequest> {
public AppliedModificationsRequest(Response response) {
super(response);
public ModificationsResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
public ModificationsResponse usingModificationsResponse() {
return new ModificationsResponse(super.response);
}
}
public class ModificationsResponse {
private Response resource;
public ModificationsResponse(Response resource) {
this.resource = resource;
}
public ModificationsResponse assertRevision(Consumer<String> assertRevision) {
String revision = resource.then().extract().path("revision");
public ModificationsResponse<PREV> assertRevision(Consumer<String> assertRevision) {
String revision = response.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
List<String> added = resource.then().extract().path("added");
public ModificationsResponse<PREV> assertAdded(Consumer<List<String>> assertAdded) {
List<String> added = response.then().extract().path("added");
assertAdded.accept(added);
return this;
}
public ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
List<String> removed = resource.then().extract().path("removed");
public ModificationsResponse<PREV> assertRemoved(Consumer<List<String>> assertRemoved) {
List<String> removed = response.then().extract().path("removed");
assertRemoved.accept(removed);
return this;
}
public ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
List<String> modified = resource.then().extract().path("modified");
public ModificationsResponse<PREV> assertModified(Consumer<List<String>> assertModified) {
List<String> modified = response.then().extract().path("modified");
assertModified.accept(modified);
return this;
}
}
public class AppliedMeRequest extends AppliedRequest<AppliedMeRequest> {
public AppliedMeRequest(Response response) {
super(response);
}
public MeResponse usingMeResponse() {
return new MeResponse(super.response);
}
}
public class MeResponse extends UserResponse<MeResponse> {
public class MeResponse<PREV extends ModelResponse> extends UserResponse<PREV> {
public MeResponse(Response response) {
super(response);
}
public AppliedChangePasswordRequest requestChangePassword(String oldPassword, String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, "_links.password.href", VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)));
public MeResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
}
public class UserResponse<SELF extends UserResponse> extends ModelResponse<SELF> {
public class UserResponse<PREV extends ModelResponse> extends ModelResponse<UserResponse<PREV>, PREV> {
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
public UserResponse(Response response) {
super(response);
public UserResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
public SELF assertPassword(Consumer<String> assertPassword) {
public UserResponse<PREV> assertPassword(Consumer<String> assertPassword) {
return super.assertSingleProperty(assertPassword, "password");
}
public SELF assertType(Consumer<String> assertType) {
public UserResponse<PREV> assertType(Consumer<String> assertType) {
return assertSingleProperty(assertType, "type");
}
public SELF assertAdmin(Consumer<Boolean> assertAdmin) {
public UserResponse<PREV> assertAdmin(Consumer<Boolean> assertAdmin) {
return assertSingleProperty(assertAdmin, "admin");
}
public SELF assertPasswordLinkDoesNotExists() {
public UserResponse<PREV> assertPasswordLinkDoesNotExists() {
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
}
public SELF assertPasswordLinkExists() {
public UserResponse<PREV> assertPasswordLinkExists() {
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
}
public AppliedChangePasswordRequest requestChangePassword(String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword)));
public ChangePasswordResponse<UserResponse> requestChangePassword(String newPassword) {
return requestChangePassword(null, newPassword);
}
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
}
}
@@ -412,12 +319,18 @@ public class ScmRequests {
/**
* encapsulate standard assertions over model properties
*/
public class ModelResponse<SELF extends ModelResponse> {
public class ModelResponse<SELF extends ModelResponse<SELF, PREV>, PREV extends ModelResponse> {
protected PREV previousResponse;
protected Response response;
public ModelResponse(Response response) {
public ModelResponse(Response response, PREV previousResponse) {
this.response = response;
this.previousResponse = previousResponse;
}
public PREV returnToPrevious() {
return previousResponse;
}
public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) {
@@ -441,25 +354,45 @@ public class ScmRequests {
assertProperties.accept(properties);
return (SELF) this;
}
/**
* special assertion of the status code
*
* @param expectedStatusCode the expected status code
* @return the self object
*/
public SELF assertStatusCode(int expectedStatusCode) {
this.response.then().assertThat().statusCode(expectedStatusCode);
return (SELF) this;
}
}
public class AppliedChangePasswordRequest extends AppliedRequest<AppliedChangePasswordRequest> {
public class AutoCompleteResponse<PREV extends ModelResponse> extends ModelResponse<AutoCompleteResponse<PREV>, PREV> {
public AppliedChangePasswordRequest(Response response) {
super(response);
public AutoCompleteResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
public AutoCompleteResponse<PREV> assertAutoCompleteResults(Consumer<List<Map>> checker) {
List<Map> result = response.then().extract().path("");
checker.accept(result);
return this;
}
}
public class AppliedUserRequest extends AppliedRequest<AppliedUserRequest> {
public AppliedUserRequest(Response response) {
super(response);
public class DiffResponse<PREV extends ModelResponse> extends ModelResponse<DiffResponse<PREV>, PREV> {
public DiffResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
}
public UserResponse usingUserResponse() {
return new UserResponse(super.response);
public class ChangePasswordResponse<PREV extends ModelResponse> extends ModelResponse<ChangePasswordResponse<PREV>, PREV> {
public ChangePasswordResponse(Response response, PREV previousResponse) {
super(response, previousResponse);
}
}
}

View File

@@ -46,11 +46,11 @@ public class TestData {
return DEFAULT_REPOSITORIES.get(repositoryType);
}
public static void createUser(String username, String password) {
createUser(username, password, false, "xml");
public static void createNotAdminUser(String username, String password) {
createUser(username, password, false, "xml", "user1@scm-manager.org");
}
public static void createUser(String username, String password, boolean isAdmin, String type) {
public static void createUser(String username, String password, boolean isAdmin, String type, final String email) {
LOG.info("create user with username: {}", username);
String admin = isAdmin ? "true" : "false";
given(VndMediaType.USER)
@@ -61,7 +61,7 @@ public class TestData {
.append(" \"admin\": ").append(admin).append(",\n")
.append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
.append(" \"displayName\": \"").append(username).append("\",\n")
.append(" \"mail\": \"user1@scm-manager.org\",\n")
.append(" \"mail\": \"" + email + "\",\n")
.append(" \"name\": \"").append(username).append("\",\n")
.append(" \"password\": \"").append(password).append("\",\n")
.append(" \"type\": \"").append(type).append("\"\n")
@@ -71,6 +71,16 @@ public class TestData {
.statusCode(HttpStatus.SC_CREATED)
;
}
public static void createGroup(String groupName, String desc) {
LOG.info("create group with group name: {} and description {}", groupName, desc);
given(VndMediaType.GROUP)
.when()
.content(getGroupJson(groupName,desc))
.post(getGroupsUrl())
.then()
.statusCode(HttpStatus.SC_CREATED)
;
}
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
@@ -193,28 +203,31 @@ public class TestData {
return JSON_BUILDER
.add("contact", "zaphod.beeblebrox@hitchhiker.com")
.add("description", "Heart of Gold")
.add("name", "HeartOfGold-" + repositoryType)
.add("name", getDefaultRepoName(repositoryType))
.add("archived", false)
.add("type", repositoryType)
.build().toString();
}
public static URI getMeUrl() {
return RestUtil.createResourceUrl("me/");
public static String getDefaultRepoName(String repositoryType) {
return "HeartOfGold-" + repositoryType;
}
public static String getGroupJson(String groupname , String desc) {
return JSON_BUILDER
.add("name", groupname)
.add("description", desc)
.build().toString();
}
public static URI getGroupsUrl() {
return RestUtil.createResourceUrl("groups/");
}
public static URI getUsersUrl() {
return RestUtil.createResourceUrl("users/");
}
public static URI getUserUrl(String username) {
return getUsersUrl().resolve(username);
}
public static String createPasswordChangeJson(String oldPassword, String newPassword) {
return JSON_BUILDER
.add("oldPassword", oldPassword)
@@ -225,4 +238,5 @@ public class TestData {
public static void main(String[] args) {
cleanup();
}
}

View File

@@ -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 GitConfigInIndexResource extends JsonEnricherBase {
private final Provider<ScmPathInfoStore> scmPathInfoStore;
@Inject
public GitConfigInIndexResource(Provider<ScmPathInfoStore> scmPathInfoStore, ObjectMapper objectMapper) {
super(objectMapper);
this.scmPathInfoStore = scmPathInfoStore;
}
@Override
public void enrich(JsonEnricherContext context) {
if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) {
String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class)
.method("get")
.parameters()
.href();
JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl)));
addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode);
}
}
}

View File

@@ -39,7 +39,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -51,8 +50,8 @@ import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
@@ -130,27 +129,9 @@ public class GitChangesetConverter implements Closeable
*
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit) throws IOException
public Changeset createChangeset(RevCommit commit)
{
List<String> branches = Lists.newArrayList();
Set<Ref> refs = repository.getAllRefsByPeeledObjectId().get(commit.getId());
if (Util.isNotEmpty(refs))
{
for (Ref ref : refs)
{
String branch = GitUtil.getBranch(ref);
if (branch != null)
{
branches.add(branch);
}
}
}
return createChangeset(commit, branches);
return createChangeset(commit, Collections.emptyList());
}
/**
@@ -165,7 +146,6 @@ public class GitChangesetConverter implements Closeable
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit, String branch)
throws IOException
{
return createChangeset(commit, Lists.newArrayList(branch));
}
@@ -183,7 +163,6 @@ public class GitChangesetConverter implements Closeable
* @throws IOException
*/
public Changeset createChangeset(RevCommit commit, List<String> branches)
throws IOException
{
String id = commit.getId().name();
List<String> parentList = null;

View File

@@ -63,8 +63,11 @@ import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static java.util.Optional.of;
//~--- JDK imports ------------------------------------------------------------
/**
@@ -345,12 +348,11 @@ public final class GitUtil
*
* @throws IOException
*/
public static ObjectId getBranchId(org.eclipse.jgit.lib.Repository repo,
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
String branchName)
throws IOException
{
ObjectId branchId = null;
Ref ref = null;
if (!branchName.startsWith(REF_HEAD))
{
branchName = PREFIX_HEADS.concat(branchName);
@@ -360,24 +362,19 @@ public final class GitUtil
try
{
Ref ref = repo.findRef(branchName);
ref = repo.findRef(branchName);
if (ref != null)
{
branchId = ref.getObjectId();
}
else if (logger.isWarnEnabled())
if (ref == null)
{
logger.warn("could not find branch for {}", branchName);
}
}
catch (IOException ex)
{
logger.warn("error occured during resolve of branch id", ex);
}
return branchId;
return ref;
}
/**
@@ -499,68 +496,48 @@ public final class GitUtil
return ref;
}
/**
* Method description
*
*
* @param repo
*
* @return
*
* @throws IOException
*/
public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo)
throws IOException
{
ObjectId id = null;
String head = null;
Map<String, Ref> refs = repo.getAllRefs();
public static ObjectId getRepositoryHead(org.eclipse.jgit.lib.Repository repo) {
return getRepositoryHeadRef(repo).map(Ref::getObjectId).orElse(null);
}
for (Map.Entry<String, Ref> e : refs.entrySet())
{
String key = e.getKey();
public static Optional<Ref> getRepositoryHeadRef(org.eclipse.jgit.lib.Repository repo) {
Optional<Ref> foundRef = findMostAppropriateHead(repo.getAllRefs());
if (REF_HEAD.equals(key))
{
head = REF_HEAD;
id = e.getValue().getObjectId();
break;
}
else if (key.startsWith(REF_HEAD_PREFIX))
{
id = e.getValue().getObjectId();
head = key.substring(REF_HEAD_PREFIX.length());
if (REF_MASTER.equals(head))
{
break;
}
if (foundRef.isPresent()) {
if (logger.isDebugEnabled()) {
logger.debug("use {}:{} as repository head for directory {}",
foundRef.map(GitUtil::getBranch).orElse(null),
foundRef.map(Ref::getObjectId).map(ObjectId::name).orElse(null),
repo.getDirectory());
}
} else {
logger.warn("could not find repository head in directory {}", repo.getDirectory());
}
if (id == null)
{
id = repo.resolve(Constants.HEAD);
return foundRef;
}
private static Optional<Ref> findMostAppropriateHead(Map<String, Ref> refs) {
Ref refHead = refs.get(REF_HEAD);
if (refHead != null && refHead.isSymbolic() && isBranch(refHead.getTarget().getName())) {
return of(refHead.getTarget());
}
if (logger.isDebugEnabled())
{
if ((head != null) && (id != null))
{
logger.debug("use {}:{} as repository head", head, id.name());
}
else if (id != null)
{
logger.debug("use {} as repository head", id.name());
}
else
{
logger.warn("could not find repository head");
}
Ref master = refs.get(REF_HEAD_PREFIX + REF_MASTER);
if (master != null) {
return of(master);
}
return id;
Ref develop = refs.get(REF_HEAD_PREFIX + "develop");
if (develop != null) {
return of(develop);
}
return refs.entrySet()
.stream()
.filter(e -> e.getKey().startsWith(REF_HEAD_PREFIX))
.map(Map.Entry::getValue)
.findFirst();
}
/**
@@ -648,7 +625,7 @@ public final class GitUtil
return tagName;
}
/**
* Method description
*

View File

@@ -34,19 +34,20 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.eclipse.jgit.lib.Repository;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.GitConstants;
import sonia.scm.repository.GitUtil;
import java.io.IOException;
import java.util.Optional;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
@@ -97,27 +98,29 @@ public class AbstractGitCommand
}
return commit;
}
protected ObjectId getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
ObjectId head;
if ( Strings.isNullOrEmpty(requestedBranch) ) {
head = getDefaultBranch(gitRepository);
} else {
head = GitUtil.getBranchId(gitRepository, requestedBranch);
}
return head;
}
protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException {
ObjectId head;
String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH);
if (!Strings.isNullOrEmpty(defaultBranchName)) {
head = GitUtil.getBranchId(gitRepository, defaultBranchName);
Ref ref = getBranchOrDefault(gitRepository, null);
if (ref == null) {
return null;
} else {
logger.trace("no default branch configured, use repository head as default");
head = GitUtil.getRepositoryHead(gitRepository);
return ref.getObjectId();
}
}
protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
if ( Strings.isNullOrEmpty(requestedBranch) ) {
String defaultBranchName = repository.getProperty(GitConstants.PROPERTY_DEFAULT_BRANCH);
if (!Strings.isNullOrEmpty(defaultBranchName)) {
return GitUtil.getBranchId(gitRepository, defaultBranchName);
} else {
logger.trace("no default branch configured, use repository head as default");
Optional<Ref> repositoryHeadRef = GitUtil.getRepositoryHeadRef(gitRepository);
return repositoryHeadRef.orElse(null);
}
} else {
return GitUtil.getBranchId(gitRepository, requestedBranch);
}
return head;
}
//~--- fields ---------------------------------------------------------------

View File

@@ -39,6 +39,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -170,8 +171,8 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
GitChangesetConverter converter = null;
RevWalk revWalk = null;
try (org.eclipse.jgit.lib.Repository gr = open()) {
if (!gr.getAllRefs().isEmpty()) {
try (org.eclipse.jgit.lib.Repository repository = open()) {
if (!repository.getAllRefs().isEmpty()) {
int counter = 0;
int start = request.getPagingStart();
@@ -188,18 +189,18 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
ObjectId startId = null;
if (!Strings.isNullOrEmpty(request.getStartChangeset())) {
startId = gr.resolve(request.getStartChangeset());
startId = repository.resolve(request.getStartChangeset());
}
ObjectId endId = null;
if (!Strings.isNullOrEmpty(request.getEndChangeset())) {
endId = gr.resolve(request.getEndChangeset());
endId = repository.resolve(request.getEndChangeset());
}
revWalk = new RevWalk(gr);
revWalk = new RevWalk(repository);
converter = new GitChangesetConverter(gr, revWalk);
converter = new GitChangesetConverter(repository, revWalk);
if (!Strings.isNullOrEmpty(request.getPath())) {
revWalk.setTreeFilter(
@@ -207,13 +208,13 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
PathFilter.create(request.getPath()), TreeFilter.ANY_DIFF));
}
ObjectId head = getBranchOrDefault(gr, request.getBranch());
Ref branch = getBranchOrDefault(repository,request.getBranch());
if (head != null) {
if (branch != null) {
if (startId != null) {
revWalk.markStart(revWalk.lookupCommit(startId));
} else {
revWalk.markStart(revWalk.lookupCommit(head));
revWalk.markStart(revWalk.lookupCommit(branch.getObjectId()));
}
Iterator<RevCommit> iterator = revWalk.iterator();
@@ -234,10 +235,14 @@ public class GitLogCommand extends AbstractGitCommand implements LogCommand
}
}
changesets = new ChangesetPagingResult(counter, changesetList);
if (branch != null) {
changesets = new ChangesetPagingResult(counter, changesetList, GitUtil.getBranch(branch.getName()));
} else {
changesets = new ChangesetPagingResult(counter, changesetList);
}
} else if (logger.isWarnEnabled()) {
logger.warn("the repository {} seems to be empty",
repository.getName());
this.repository.getName());
changesets = new ChangesetPagingResult(0, Collections.EMPTY_LIST);
}

View File

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

View File

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

View File

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

View File

@@ -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> scmPathInfoStore;
@Inject
public HgConfigInIndexResource(Provider<ScmPathInfoStore> 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);
}
}
}

View File

@@ -132,7 +132,11 @@ public class HgLogCommand extends AbstractCommand implements LogCommand
List<Changeset> 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
{

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> scmPathInfoStore;
@Inject
public SvnConfigInIndexResource(Provider<ScmPathInfoStore> 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);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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 || true"
},
"devDependencies": {
"lerna": "^3.2.1"

View File

@@ -19,7 +19,8 @@
"flow-bin": "^0.79.1",
"flow-typed": "^2.5.1",
"jest": "^23.5.0",
"raf": "^3.4.0"
"raf": "^3.4.0",
"react-router-enzyme-context": "^1.2.0"
},
"dependencies": {
"classnames": "^2.2.6",

View File

@@ -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<Props> {
render() {
const { message, classes } = this.props;
const multiline = message.length > 60 ? "is-tooltip-multiline" : "";
return (
<div
className={classNames("tooltip is-tooltip-right", multiline, classes.q)}
data-tooltip={message}
>
<i
className={classNames("fa fa-question has-text-info", classes.img)}
/>
</div>
);
}
}
export default injectSheet(styles)(Help);

View File

@@ -0,0 +1,46 @@
//@flow
import React from "react";
import { Help } from "./index";
type Props = {
label: string,
helpText?: string
};
class LabelWithHelpIcon extends React.Component<Props> {
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
renderHelp = () => {
const helpText = this.props.helpText;
if (helpText) {
return (
<div className="control columns is-vcentered">
<Help message={helpText} />
</div>
);
} else return null;
};
renderLabelWithHelpIcon = () => {
if (this.props.label) {
return (
<div className="field is-grouped">
<div className="control">{this.renderLabel()}</div>
{this.renderHelp()}
</div>
);
} else return null;
};
render() {
return this.renderLabelWithHelpIcon();
}
}
export default LabelWithHelpIcon;

View File

@@ -3,10 +3,13 @@ import React from "react";
import { mount, shallow } from "enzyme";
import "./tests/enzyme";
import "./tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
import Paginator from "./Paginator";
describe("paginator rendering tests", () => {
const options = new ReactRouterEnzymeContext();
const dummyLink = {
href: "https://dummy"
};
@@ -18,7 +21,10 @@ describe("paginator rendering tests", () => {
_links: {}
};
const paginator = shallow(<Paginator collection={collection} />);
const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(7);
for (let button of buttons) {
@@ -37,7 +43,10 @@ describe("paginator rendering tests", () => {
}
};
const paginator = shallow(<Paginator collection={collection} />);
const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(5);
@@ -73,7 +82,10 @@ describe("paginator rendering tests", () => {
}
};
const paginator = shallow(<Paginator collection={collection} />);
const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(6);
@@ -112,7 +124,10 @@ describe("paginator rendering tests", () => {
}
};
const paginator = shallow(<Paginator collection={collection} />);
const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(5);
@@ -148,7 +163,10 @@ describe("paginator rendering tests", () => {
}
};
const paginator = shallow(<Paginator collection={collection} />);
const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(6);
@@ -189,7 +207,10 @@ describe("paginator rendering tests", () => {
}
};
const paginator = shallow(<Paginator collection={collection} />);
const paginator = shallow(
<Paginator collection={collection} />,
options.get()
);
const buttons = paginator.find("Button");
expect(buttons.length).toBe(7);
@@ -244,7 +265,8 @@ describe("paginator rendering tests", () => {
};
const paginator = mount(
<Paginator collection={collection} onPageChange={callMe} />
<Paginator collection={collection} onPageChange={callMe} />,
options.get()
);
paginator.find("Button.pagination-previous").simulate("click");

View File

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

View File

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

View File

@@ -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<Props, State> {
}
render() {
const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
const {
disabled,
buttonLabel,
fieldLabel,
errorMessage,
helpText
} = this.props;
return (
<div className="field">
<InputField
@@ -36,6 +43,7 @@ class AddEntryToTableField extends React.Component<Props, State> {
value={this.state.entryToAdd}
onReturnPressed={this.appendEntry}
disabled={disabled}
helpText={helpText}
/>
<AddButton
label={buttonLabel}

View File

@@ -1,11 +1,13 @@
//@flow
import React from "react";
import { Help } from "../index";
type Props = {
label?: string,
checked: boolean,
onChange?: boolean => void,
disabled?: boolean
disabled?: boolean,
helpText?: string
};
class Checkbox extends React.Component<Props> {
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
@@ -14,9 +16,20 @@ class Checkbox extends React.Component<Props> {
}
};
renderHelp = () => {
const helpText = this.props.helpText;
if (helpText) {
return (
<div className="control columns is-vcentered">
<Help message={helpText} />
</div>
);
} else return null;
};
render() {
return (
<div className="field">
<div className="field is-grouped">
<div className="control">
<label className="checkbox" disabled={this.props.disabled}>
<input
@@ -28,6 +41,7 @@ class Checkbox extends React.Component<Props> {
{this.props.label}
</label>
</div>
{this.renderHelp()}
</div>
);
}

View File

@@ -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<Props> {
@@ -33,15 +35,6 @@ class InputField extends React.Component<Props> {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
const onReturnPressed = this.props.onReturnPressed;
if (!onReturnPressed) {
@@ -60,7 +53,9 @@ class InputField extends React.Component<Props> {
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<Props> {
);
return (
<div className="field">
{this.renderLabel()}
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control">
<input
ref={input => {

View File

@@ -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<Props> {
@@ -28,21 +32,18 @@ class Select extends React.Component<Props> {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
render() {
const { options, value } = this.props;
const { options, value, label, helpText, loading } = this.props;
const loadingClass = loading ? "is-loading" : "";
return (
<div className="field">
{this.renderLabel()}
<div className="control select">
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className={classNames(
"control select",
loadingClass
)}>
<select
ref={input => {
this.field = input;

View File

@@ -1,5 +1,6 @@
//@flow
import React from "react";
import { LabelWithHelpIcon } from "../index";
export type SelectItem = {
value: string,
@@ -10,7 +11,8 @@ type Props = {
label?: string,
placeholder?: SelectItem[],
value?: string,
onChange: string => void
onChange: string => void,
helpText?: string
};
class Textarea extends React.Component<Props> {
@@ -20,20 +22,12 @@ class Textarea extends React.Component<Props> {
this.props.onChange(event.target.value);
};
renderLabel = () => {
const label = this.props.label;
if (label) {
return <label className="label">{label}</label>;
}
return "";
};
render() {
const { placeholder, value } = this.props;
const { placeholder, value, label, helpText } = this.props;
return (
<div className="field">
{this.renderLabel()}
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control">
<textarea
className="textarea"

View File

@@ -17,11 +17,11 @@ export { default as Notification } from "./Notification.js";
export { default as Paginator } from "./Paginator.js";
export { default as LinkPaginator } from "./LinkPaginator.js";
export { default as ProtectedRoute } from "./ProtectedRoute.js";
export { default as Help } from "./Help.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
export * from "./buttons";
export * from "./forms";
export * from "./layout";

View File

@@ -1,5 +1,5 @@
// @flow
const nameRegex = /^([A-z0-9.\-_@]|[^ ]([A-z0-9.\-_@ ]*[A-z0-9.\-_@]|[^\s])?)$/;
const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/;
export const isNameValid = (name: string) => {
return nameRegex.test(name);

View File

@@ -5,6 +5,7 @@ describe("test name validation", () => {
it("should return false", () => {
// invalid names taken from ValidationUtilTest.java
const invalidNames = [
"@test",
" test 123",
" test 123 ",
"test 123 ",
@@ -35,10 +36,9 @@ describe("test name validation", () => {
"Test123-git",
"Test_user-123.git",
"test@scm-manager.de",
"test 123",
"test123",
"tt",
"t",
"valid_name",
"another1",
"stillValid",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
//@flow
import type { Links } from "./hal";
export type Permission = {
name: string,
type: string,
groupPermission: boolean,
_links?: Links
};
export type PermissionEntry = {
name: string,
type: string,
groupPermission: boolean
}
export type PermissionCollection = Permission[];

View File

@@ -16,3 +16,5 @@ export type { Changeset } from "./Changesets";
export type { Tag } from "./Tags";
export type { Config } from "./Config";
export type { Permission, PermissionEntry, PermissionCollection } from "./RepositoryPermissions";

View File

@@ -48,6 +48,16 @@
<script>bootstrap</script>
</configuration>
</execution>
<execution>
<id>unlink</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<script>unlink</script>
</configuration>
</execution>
<execution>
<id>link</id>
<phase>package</phase>

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
"@fortawesome/fontawesome-free": "^5.3.1",
"@scm-manager/ui-extensions": "^0.0.7",
"bulma": "^0.7.1",
"bulma-tooltip": "^2.0.2",
"classnames": "^2.2.5",
"font-awesome": "^4.7.0",
"history": "^4.7.2",
@@ -49,6 +50,7 @@
"fetch-mock": "^6.5.0",
"flow-typed": "^2.5.1",
"jest": "^23.5.0",
"node-sass": "^4.9.3",
"node-sass-chokidar": "^1.3.0",
"npm-run-all": "^4.1.3",
"prettier": "^1.13.7",

View File

@@ -64,5 +64,29 @@
"login-attempt-limit-timeout-invalid": "This is not a number",
"login-attempt-limit-invalid": "This is not a number",
"plugin-url-invalid": "This is not a valid url"
},
"help": {
"realmDescriptionHelpText": "Enter authentication realm description",
"dateFormatHelpText": "Moments date format. Please have a look at the momentjs documentation.",
"pluginRepositoryHelpText": "The url of the plugin repository. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture",
"enableForwardingHelpText": "Enbale mod_proxy port forwarding.",
"enableRepositoryArchiveHelpText": "Enable repository archives. A complete page reload is required after a change of this value.",
"disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.",
"allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.",
"skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.",
"adminGroupsHelpText": "Names of groups with admin permissions.",
"adminUsersHelpText": "Names of users with admin permissions.",
"forceBaseUrlHelpText": "Redirects to the base url if the request comes from a other url",
"baseUrlHelpText": "The url of the application (with context path), i.e. http://localhost:8080/scm",
"loginAttemptLimitHelpText": "Maximum allowed login attempts. Use -1 to disable the login attempt limit.",
"loginAttemptLimitTimeoutHelpText": "Timeout in seconds for users which are temporary disabled, because of too many failed login attempts.",
"enableProxyHelpText": "Enable Proxy",
"proxyPortHelpText": "The proxy port",
"proxyPasswordHelpText": "The password for the proxy server authentication.",
"proxyServerHelpText": "The proxy server",
"proxyUserHelpText": "The username for the proxy server authentication.",
"proxyExcludesHelpText": "Glob patterns for hostnames which should be excluded from proxy settings.",
"enableXsrfProtectionHelpText": "Enable Xsrf Cookie Protection. Note: This feature is still experimental.",
"defaultNameSpaceStrategyHelpText": "The default namespace strategy"
}
}

View File

@@ -42,7 +42,12 @@
"group-form": {
"submit": "Submit",
"name-error": "Group name is invalid",
"description-error": "Description is invalid"
"description-error": "Description is invalid",
"help": {
"nameHelpText": "Unique name of the group",
"descriptionHelpText": "A short description of the group",
"memberHelpText": "Usernames of the group members"
}
},
"delete-group-button": {
"label": "Delete",

View File

@@ -22,8 +22,9 @@
"actions-label": "Actions",
"back-label": "Back",
"navigation-label": "Navigation",
"history": "Commits",
"information": "Information",
"history": "Commits"
"permissions": "Permissions"
},
"create": {
"title": "Create Repository",
@@ -62,8 +63,35 @@
"branch-selector": {
"label": "Branches"
},
"branch-chooser": {
"permission": {
"error-title": "Error",
"error-subtitle": "Could not fetch branches"
"error-subtitle": "Unknown permissions error",
"name": "User or Group",
"type": "Type",
"group-permission": "Group Permission",
"edit-permission": {
"delete-button": "Delete",
"save-button": "Save Changes"
},
"delete-permission-button": {
"label": "Delete",
"confirm-alert": {
"title": "Delete permission",
"message": "Do you really want to delete the permission?",
"submit": "Yes",
"cancel": "No"
}
},
"add-permission": {
"add-permission-heading": "Add new Permission",
"submit-button": "Submit",
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
}
},
"help": {
"nameHelpText": "The name of the repository. This name will be part of the repository url.",
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
"contactHelpText": "Email address of the person who is responsible for this repository.",
"descriptionHelpText": "A short description of the repository."
}
}

View File

@@ -51,5 +51,14 @@
"password-invalid": "Password has to be between 6 and 32 characters",
"passwordValidation-invalid": "Passwords have to be the same",
"validatePassword": "Please validate password here"
},
"help": {
"usernameHelpText": "Unique name of the user.",
"displayNameHelpText": "Display name of the user.",
"mailHelpText": "Email address of the user.",
"passwordHelpText": "Plain text password of the user.",
"passwordConfirmHelpText": "Repeat the password for validation.",
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
"activeHelpText": "Activate or deactive the user."
}
}

View File

@@ -23,12 +23,14 @@ class BaseUrlSettings extends React.Component<Props> {
label={t("base-url-settings.force-base-url")}
onChange={this.handleForceBaseUrlChange}
disabled={!hasUpdatePermission}
helpText={t("help.forceBaseUrlHelpText")}
/>
<InputField
label={t("base-url-settings.base-url")}
onChange={this.handleBaseUrlChange}
value={baseUrl}
disabled={!hasUpdatePermission}
helpText={t("help.baseUrlHelpText")}
/>
</div>
);

View File

@@ -41,54 +41,63 @@ class GeneralSettings extends React.Component<Props> {
onChange={this.handleRealmDescriptionChange}
value={realmDescription}
disabled={!hasUpdatePermission}
helpText={t("help.realmDescriptionHelpText")}
/>
<InputField
label={t("general-settings.date-format")}
onChange={this.handleDateFormatChange}
value={dateFormat}
disabled={!hasUpdatePermission}
helpText={t("help.dateFormatHelpText")}
/>
<InputField
label={t("general-settings.plugin-url")}
onChange={this.handlePluginUrlChange}
value={pluginUrl}
disabled={!hasUpdatePermission}
helpText={t("help.pluginRepositoryHelpText")}
/>
<InputField
label={t("general-settings.default-namespace-strategy")}
onChange={this.handleDefaultNamespaceStrategyChange}
value={defaultNamespaceStrategy}
disabled={!hasUpdatePermission}
helpText={t("help.defaultNameSpaceStrategyHelpText")}
/>
<Checkbox
checked={enabledXsrfProtection}
label={t("general-settings.enabled-xsrf-protection")}
onChange={this.handleEnabledXsrfProtectionChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableXsrfProtectionHelpText")}
/>
<Checkbox
checked={enableRepositoryArchive}
label={t("general-settings.enable-repository-archive")}
onChange={this.handleEnableRepositoryArchiveChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableRepositoryArchiveHelpText")}
/>
<Checkbox
checked={disableGroupingGrid}
label={t("general-settings.disable-grouping-grid")}
onChange={this.handleDisableGroupingGridChange}
disabled={!hasUpdatePermission}
helpText={t("help.disableGroupingGridHelpText")}
/>
<Checkbox
checked={anonymousAccessEnabled}
label={t("general-settings.anonymous-access-enabled")}
onChange={this.handleAnonymousAccessEnabledChange}
disabled={!hasUpdatePermission}
helpText={t("help.allowAnonymousAccessHelpText")}
/>
<Checkbox
checked={skipFailedAuthenticators}
label={t("general-settings.skip-failed-authenticators")}
onChange={this.handleSkipFailedAuthenticatorsChange}
disabled={!hasUpdatePermission}
helpText={t("help.skipFailedAuthenticatorsHelpText")}
/>
</div>
);

View File

@@ -47,6 +47,7 @@ class LoginAttempt extends React.Component<Props, State> {
disabled={!hasUpdatePermission}
validationError={this.state.loginAttemptLimitError}
errorMessage={t("validation.login-attempt-limit-invalid")}
helpText={t("help.loginAttemptLimitHelpText")}
/>
<InputField
label={t("login-attempt.login-attempt-limit-timeout")}
@@ -55,6 +56,7 @@ class LoginAttempt extends React.Component<Props, State> {
disabled={!hasUpdatePermission}
validationError={this.state.loginAttemptLimitTimeoutError}
errorMessage={t("validation.login-attempt-limit-timeout-invalid")}
helpText={t("help.loginAttemptLimitTimeoutHelpText")}
/>
</div>
);

View File

@@ -42,6 +42,7 @@ class ProxySettings extends React.Component<Props> {
label={t("proxy-settings.enable-proxy")}
onChange={this.handleEnableProxyChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableProxyHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-password")}
@@ -49,24 +50,28 @@ class ProxySettings extends React.Component<Props> {
value={proxyPassword}
type="password"
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyPasswordHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-port")}
value={proxyPort}
onChange={this.handleProxyPortChange}
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyPortHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-server")}
value={proxyServer}
onChange={this.handleProxyServerChange}
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyServerHelpText")}
/>
<InputField
label={t("proxy-settings.proxy-user")}
value={proxyUser}
onChange={this.handleProxyUserChange}
disabled={!enableProxy || !hasUpdatePermission}
helpText={t("help.proxyUserHelpText")}
/>
<ProxyExcludesTable
proxyExcludes={proxyExcludes}

View File

@@ -24,6 +24,7 @@ class AdminGroupTable extends React.Component<Props, State> {
removeLabel={t("admin-settings.remove-group-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.adminGroupsHelpText")}
/>
);
}

View File

@@ -22,6 +22,7 @@ class AdminUserTable extends React.Component<Props> {
removeLabel={t("admin-settings.remove-user-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.adminUsersHelpText")}
/>
);
}

View File

@@ -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<Props> {
render() {
const { label, disabled, removeLabel, items } = this.props;
const { label, disabled, removeLabel, items, helpText } = this.props;
return (
<div>
<label className="label">{label}</label>
<LabelWithHelpIcon label={label} helpText={helpText}/>
<table className="table is-hoverable is-fullwidth">
<tbody>
{items.map(item => {

View File

@@ -22,6 +22,7 @@ class ProxyExcludesTable extends React.Component<Props, State> {
removeLabel={t("proxy-settings.remove-proxy-exclude-button")}
onRemove={this.removeEntry}
disabled={disabled}
helpText={t("help.proxyExcludesHelpText")}
/>
);
}

View File

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

View File

@@ -12,6 +12,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";
@@ -30,6 +31,7 @@ function createReduxStore(history: BrowserHistory) {
repositoryTypes,
changesets,
branches,
permissions,
groups,
auth,
config

View File

@@ -80,6 +80,7 @@ class GroupForm extends React.Component<Props, State> {
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<Props, State> {
onChange={this.handleDescriptionChange}
value={group.description}
validationError={false}
helpText={t("group-form.help.descriptionHelpText")}
/>
<MemberNameTable
members={this.state.group.members}

View File

@@ -1,7 +1,10 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { RemoveEntryOfTableButton } from "@scm-manager/ui-components";
import {
RemoveEntryOfTableButton,
LabelWithHelpIcon
} from "@scm-manager/ui-components";
type Props = {
members: string[],
@@ -16,7 +19,10 @@ class MemberNameTable extends React.Component<Props, State> {
const { t } = this.props;
return (
<div>
<label className="label">{t("group.members")}</label>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("group-form.help.memberHelpText")}
/>
<table className="table is-hoverable is-fullwidth">
<tbody>
{this.props.members.map(member => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Props> {
hasPermissionsLink = () => {
return this.props.repository._links.permissions;
};
render() {
if (!this.hasPermissionsLink()) {
return null;
}
const { permissionUrl, t } = this.props;
return (
<NavLink to={permissionUrl} label={t("repository-root.permissions")} />
);
}
}
export default translate("repos")(PermissionsNavLink);

View File

@@ -0,0 +1,38 @@
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";
describe("PermissionsNavLink", () => {
const options = new ReactRouterEnzymeContext();
it("should render nothing, if the modify link is missing", () => {
const repository = {
_links: {}
};
const navLink = shallow(
<PermissionsNavLink repository={repository} permissionUrl="" />,
options.get()
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const repository = {
_links: {
permissions: {
href: "/permissions"
}
}
};
const navLink = mount(
<PermissionsNavLink repository={repository} permissionUrl="" />,
options.get()
);
expect(navLink.text()).toBe("repository-root.permissions");
});
});

View File

@@ -90,12 +90,14 @@ class RepositoryForm extends React.Component<Props, State> {
value={repository ? repository.contact : ""}
validationError={this.state.contactValidationError}
errorMessage={t("validation.contact-invalid")}
helpText={t("help.contactHelpText")}
/>
<Textarea
label={t("repository.description")}
onChange={this.handleDescriptionChange}
value={repository ? repository.description : ""}
helpText={t("help.descriptionHelpText")}
/>
<SubmitButton
disabled={!this.isValid()}
@@ -129,12 +131,14 @@ class RepositoryForm extends React.Component<Props, State> {
value={repository ? repository.name : ""}
validationError={this.state.nameValidationError}
errorMessage={t("validation.name-invalid")}
helpText={t("help.nameHelpText")}
/>
<Select
label={t("repository.type")}
onChange={this.handleTypeChange}
value={repository ? repository.type : ""}
options={this.createSelectOptions(repositoryTypes)}
helpText={t("help.typeHelpText")}
/>
</div>
);

View File

@@ -22,10 +22,12 @@ 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 BranchRoot from "./BranchRoot";
import PermissionsNavLink from "../components/PermissionsNavLink";
type Props = {
namespace: string,
@@ -108,6 +110,15 @@ class RepositoryRoot extends React.Component<Props> {
path={`${url}/edit`}
component={() => <Edit repository={repository} />}
/>
<Route
path={`${url}/permissions`}
render={props => (
<Permissions
namespace={this.props.repository.namespace}
repoName={this.props.repository.name}
/>
)}
/>
<Route
path={`${url}/changesets`}
render={() => (
@@ -141,6 +152,10 @@ class RepositoryRoot extends React.Component<Props> {
activeWhenMatch={this.matches}
/>
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
<PermissionsNavLink
permissionUrl={`${url}/permissions`}
repository={repository}
/>
</Section>
<Section label={t("repository-root.actions-label")}>
<DeleteNavAction repository={repository} delete={this.delete} />

View File

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

Some files were not shown because too many files have changed in this diff Show More