#8771 Permission endpoints

This commit is contained in:
Mohamed Karray
2018-08-20 18:16:14 +02:00
parent 012ac34a6a
commit f79975b18d
20 changed files with 1035 additions and 113 deletions

View File

@@ -0,0 +1,11 @@
package sonia.scm.repository;
import java.text.MessageFormat;
public class PermissionAlreadyExistsException extends RepositoryException {
public PermissionAlreadyExistsException(Repository repository, String permissionName) {
super(MessageFormat.format("the permission {0} of the repository {1}/{2} is already exists", permissionName, repository.getNamespace(), repository.getName()));
}
}

View File

@@ -0,0 +1,12 @@
package sonia.scm.repository;
import java.text.MessageFormat;
public class PermissionNotFoundException extends RepositoryException{
public PermissionNotFoundException(Repository repository, String permissionName) {
super(MessageFormat.format("the permission {0} of the repository {1}/{2} does not exists", permissionName,repository.getNamespace(), repository.getName() ));
}
}

View File

@@ -16,6 +16,7 @@ public class VndMediaType {
public static final String USER = PREFIX + "user" + SUFFIX;
public static final String GROUP = PREFIX + "group" + SUFFIX;
public static final String REPOSITORY = PREFIX + "repository" + SUFFIX;
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX;
public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX;

View File

@@ -276,7 +276,25 @@
<version>${jersey-client.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<!-- core plugins -->
<dependency>

View File

@@ -1,30 +1,30 @@
/**
* 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
*
/*
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
*/
@@ -56,14 +56,14 @@ public class StatusExceptionMapper<E extends Throwable>
private static final Logger logger =
LoggerFactory.getLogger(StatusExceptionMapper.class);
//~--- constructors ---------------------------------------------------------
private final Response.Status status;
private final Class<E> type;
/**
* Constructs ...
* Map an Exception to a HTTP Response
*
*
* @param type
* @param status
* @param type the exception class
* @param status the http status to be mapped
*/
public StatusExceptionMapper(Class<E> type, Response.Status status)
{
@@ -71,15 +71,12 @@ public class StatusExceptionMapper<E extends Throwable>
this.status = status;
}
//~--- methods --------------------------------------------------------------
/**
* Method description
* provide a http responses from an exception
*
* @param exception the thrown exception
*
* @param exception
*
* @return
* @return the http response with the exception presentation
*/
@Override
public Response toResponse(E exception)
@@ -95,12 +92,4 @@ public class StatusExceptionMapper<E extends Throwable>
return Response.status(status).build();
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final Response.Status status;
/** Field description */
private final Class<E> type;
}

View File

@@ -0,0 +1,50 @@
/*
Copyright (c) 2014, 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.api.v2.resources;
import org.apache.shiro.authz.AuthorizationException;
import sonia.scm.api.rest.StatusExceptionMapper;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
* @author mkarray
* @since 2.0.0
*/
@Provider
public class AuthorizationExceptionMapper extends StatusExceptionMapper<AuthorizationException> {
public AuthorizationExceptionMapper() {
super(AuthorizationException.class, Response.Status.UNAUTHORIZED);
}
}

View File

@@ -25,6 +25,8 @@ public class MapperModule extends AbstractModule {
bind(RepositoryTypeCollectionToDtoMapper.class);
bind(BranchToBranchDtoMapper.class).to(Mappers.getMapper(BranchToBranchDtoMapper.class).getClass());
bind(PermissionDtoToPermissionMapper.class).to(Mappers.getMapper(PermissionDtoToPermissionMapper.class).getClass());
bind(PermissionToPermissionDtoMapper.class).to(Mappers.getMapper(PermissionToPermissionDtoMapper.class).getClass());
bind(UriInfoStore.class).in(ServletScopes.REQUEST);
}

View File

@@ -0,0 +1,50 @@
/*
Copyright (c) 2014, 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.api.v2.resources;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.repository.PermissionAlreadyExistsException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
* @author mkarray
* @since 2.0.0
*/
@Provider
public class PermissionAlreadyExistsExceptionMapper extends StatusExceptionMapper<PermissionAlreadyExistsException> {
public PermissionAlreadyExistsExceptionMapper() {
super(PermissionAlreadyExistsException.class, Response.Status.CONFLICT);
}
}

View File

@@ -1,18 +0,0 @@
package sonia.scm.api.v2.resources;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
public class PermissionCollectionResource {
@GET
@Path("")
public Response getAll(@DefaultValue("0") @QueryParam("page") int page,
@DefaultValue("10") @QueryParam("pageSize") int pageSize,
@QueryParam("sortBy") String sortBy,
@DefaultValue("false") @QueryParam("desc") boolean desc) {
throw new UnsupportedOperationException();
}
}

View File

@@ -5,16 +5,25 @@ import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter
@Getter @Setter @ToString
public class PermissionDto extends HalRepresentation {
@JsonInclude(JsonInclude.Include.NON_NULL)
private PermissionTypeDto type = PermissionTypeDto.READ;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String name;
/**
* the type can be replaced with a dto enum if the mapstruct 1.3.0 is stable
* the mapstruct has a Bug on mapping enums in the 1.2.0-Final Version
*
* see the bug fix: https://github.com/mapstruct/mapstruct/commit/460e87eef6eb71245b387fdb0509c726676a8e19
*
**/
@JsonInclude(JsonInclude.Include.NON_NULL)
private String type ;
private boolean groupPermission = false;

View File

@@ -0,0 +1,21 @@
package sonia.scm.api.v2.resources;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.repository.Permission;
@Mapper
public abstract class PermissionDtoToPermissionMapper {
public abstract Permission map(PermissionDto permissionDto);
/**
* this method is needed to modify an existing permission object
*
* @param target the target permission
* @param permissionDto the source dto
* @return the mapped target permission object
*/
public abstract Permission map(@MappingTarget Permission target, PermissionDto permissionDto);
}

View File

@@ -0,0 +1,50 @@
/*
Copyright (c) 2014, 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.api.v2.resources;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.repository.PermissionNotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
* @author mkarray
* @since 2.0.0
*/
@Provider
public class PermissionNotFoundExceptionMapper extends StatusExceptionMapper<PermissionNotFoundException> {
public PermissionNotFoundExceptionMapper() {
super(PermissionNotFoundException.class, Response.Status.NOT_FOUND);
}
}

View File

@@ -1,20 +1,234 @@
package sonia.scm.api.v2.resources;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.Path;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import sonia.scm.repository.*;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
public class PermissionRootResource {
private final Provider<PermissionCollectionResource> permissionCollectionResource;
private PermissionDtoToPermissionMapper dtoToModelMapper;
private PermissionToPermissionDtoMapper modelToDtoMapper;
private ResourceLinks resourceLinks;
private final RepositoryManager manager;
@Inject
public PermissionRootResource(Provider<PermissionCollectionResource> permissionCollectionResource) {
this.permissionCollectionResource = permissionCollectionResource;
public PermissionRootResource(PermissionDtoToPermissionMapper dtoToModelMapper, PermissionToPermissionDtoMapper modelToDtoMapper, ResourceLinks resourceLinks, RepositoryManager manager) {
this.dtoToModelMapper = dtoToModelMapper;
this.modelToDtoMapper = modelToDtoMapper;
this.resourceLinks = resourceLinks;
this.manager = manager;
}
/**
* Adds a new permission to the user or group managed by the repository
*
* @param permission permission to add
* @return a web response with the status code 201 and the url to GET the added permission
*/
@POST
@StatusCodes({
@ResponseCode(code = 201, condition = "creates", additionalHeaders = {
@ResponseHeader(name = "Location", description = "uri of the created permission")
}),
@ResponseCode(code = 500, condition = "internal server error"),
@ResponseCode(code = 404, condition = "not found"),
@ResponseCode(code = 409, condition = "conflict")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION)
public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws RepositoryException {
log.info("try to add new permission: {}", permission);
Repository repository = checkPermission(namespace, name);
checkPermissionAlreadyExists(permission, repository);
repository.getPermissions().add(dtoToModelMapper.map(permission));
manager.modify(repository);
return Response.created(URI.create(resourceLinks.permission().self(namespace,name,permission.getName()))).build();
}
/**
* Get the searched permission with permission name related to a repository
*
* @param namespace the repository namespace
* @param name the repository name
* @return the http response with a list of permissionDto objects
* @throws RepositoryNotFoundException if the repository does not exists
*/
@GET
@StatusCodes({
@ResponseCode(code = 200, condition = "ok"),
@ResponseCode(code = 404, condition = "not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.PERMISSION)
@TypeHint(PermissionDto.class)
@Path("{permission-name}")
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws RepositoryException {
Repository repository = checkPermission(namespace, name);
return Response.ok(
repository.getPermissions()
.stream()
.filter(permission -> StringUtils.isNotBlank(permission.getName()) && permission.getName().equals(permissionName))
.map(permission -> modelToDtoMapper.map(permission, new NamespaceAndName(repository.getNamespace(),repository.getName())))
.findFirst()
.orElseThrow(() -> new PermissionNotFoundException(repository, permissionName))
).build();
}
/**
* Get all permissions related to a repository
*
* @param namespace the repository namespace
* @param name the repository name
* @return the http response with a list of permissionDto objects
* @throws RepositoryNotFoundException if the repository does not exists
*/
@GET
@StatusCodes({
@ResponseCode(code = 200, condition = "ok"),
@ResponseCode(code = 404, condition = "not found"),
@ResponseCode(code = 500, condition = "internal server error")
})
@Produces(VndMediaType.PERMISSION)
@TypeHint(PermissionDto.class)
@Path("")
public PermissionCollectionResource getPermissionCollectionResource() {
return permissionCollectionResource.get();
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException {
Repository repository = checkPermission(namespace, name);
List<PermissionDto> permissionDtoList = repository.getPermissions()
.stream()
.map(per -> modelToDtoMapper.map(per, new NamespaceAndName(repository.getNamespace(),repository.getName())))
.collect(Collectors.toList());
return Response.ok(permissionDtoList).build();
}
/**
* Update a permission to the user or group managed by the repository
*
* @param permission permission to modify
* @param permissionName permission to modify
* @return a web response with the status code 204
*/
@PUT
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PERMISSION)
@Path("{permission-name}")
public Response update(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName,
PermissionDto permission) throws RepositoryException {
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
Repository repository = checkPermission(namespace, name);
repository.getPermissions()
.stream()
.filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName))
.findFirst()
.map(p -> dtoToModelMapper.map(p, permission))
.orElseThrow(() -> new PermissionNotFoundException(repository, permissionName))
;
manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName);
return Response.noContent().build();
}
/**
* Update a permission to the user or group managed by the repository
*
* @param permissionName permission to delete
* @return a web response with the status code 204
*/
@DELETE
@StatusCodes({
@ResponseCode(code = 204, condition = "delete success or nothing to delete"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Path("{permission-name}")
public Response delete(@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("permission-name") String permissionName) throws RepositoryException {
log.info("try to delete the permission with name: {}.", permissionName);
Repository repository = checkPermission(namespace, name);
repository.getPermissions()
.stream()
.filter(perm -> StringUtils.isNotBlank(perm.getName()) && perm.getName().equals(permissionName))
.findFirst()
.ifPresent(p -> repository.getPermissions().remove(p))
;
manager.modify(repository);
log.info("the permission with name: {} is updated.", permissionName);
return Response.noContent().build();
}
/**
* check if the actual user is permitted to manage the repository permissions
* return the repository if the user is permitted
*
* @param namespace the repository namespace
* @param name the repository name
* @return the repository if the user is permitted
* @throws RepositoryNotFoundException if the repository does not exists
*/
private Repository checkPermission(@PathParam("namespace") String namespace, @PathParam("name") String name) throws RepositoryNotFoundException {
return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name)))
.filter(repository -> {
checkUserPermitted(repository);
return true;
})
.orElseThrow(() -> new RepositoryNotFoundException(name));
}
/**
* throw exception if the user is not permitted
* @param repository
*/
protected void checkUserPermitted(Repository repository) {
RepositoryPermissions.modify(repository).check();
}
/**
* check if the permission already exists in the repository
*
* @param permission the searched permission
* @param repository the repository to be inspected
* @throws PermissionAlreadyExistsException if the permission already exists in the repository
*/
private void checkPermissionAlreadyExists(PermissionDto permission, Repository repository) throws PermissionAlreadyExistsException {
boolean isPermissionAlreadyExist = repository.getPermissions()
.stream()
.anyMatch(p -> p.getName().equals(permission.getName()));
if (isPermissionAlreadyExist) {
throw new PermissionAlreadyExistsException(repository, permission.getName());
}
}
}

View File

@@ -0,0 +1,37 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.*;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Permission;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class PermissionToPermissionDtoMapper {
@Inject
private ResourceLinks resourceLinks;
@Mapping(target = "attributes", ignore = true) // We do not map HAL attributes
public abstract PermissionDto map(Permission permission, @Context NamespaceAndName namespaceAndName);
/**
* Add the self, update and delete links.
*
* @param target the mapped dto
* @param namespaceAndName the repository namespace and name
*/
@AfterMapping
void appendLinks(@MappingTarget PermissionDto target, @Context NamespaceAndName namespaceAndName) {
Links.Builder linksBuilder = linkingTo()
.self(resourceLinks.permission().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName()));
linksBuilder.single(link("update", resourceLinks.permission().update(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())));
linksBuilder.single(link("delete", resourceLinks.permission().delete(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())));
target.add(linksBuilder.build());
}
}

View File

@@ -1,26 +0,0 @@
package sonia.scm.api.v2.resources;
/**
* Type of permissionPrefix for a {@link RepositoryDto}.
*
* @author mkarray
*/
public enum PermissionTypeDto {
/**
* read permission
*/
READ,
/**
* read and write permission
*/
WRITE,
/**
* read, write and manage the properties and permissions
*/
OWNER
}

View File

@@ -0,0 +1,50 @@
/*
Copyright (c) 2014, 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.api.v2.resources;
import sonia.scm.api.rest.StatusExceptionMapper;
import sonia.scm.repository.RepositoryNotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
/**
* @author mkarray
* @since 2.0.0
*/
@Provider
public class RepositoryNotFoundExceptionMapper extends StatusExceptionMapper<RepositoryNotFoundException> {
public RepositoryNotFoundExceptionMapper() {
super(RepositoryNotFoundException.class, Response.Status.NOT_FOUND);
}
}

View File

@@ -36,7 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper<Reposit
}
if (RepositoryPermissions.modify(repository).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.repository().update(target.getNamespace(), target.getName())));
linksBuilder.single(link("permissions", resourceLinks.permissionCollection().self(target.getNamespace(), target.getName())));
linksBuilder.single(link("permissions", resourceLinks.permission().all(target.getNamespace(), target.getName())));
}
try (RepositoryService repositoryService = serviceFactory.create(repository)) {
if (repositoryService.isSupported(Command.TAGS)) {

View File

@@ -298,20 +298,35 @@ class ResourceLinks {
return sourceLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("sources").parameters().method("get").parameters(revision).href();
}
}
public PermissionCollectionLinks permissionCollection() {
return new PermissionCollectionLinks(uriInfoStore.get());
public PermissionLinks permission() {
return new PermissionLinks(uriInfoStore.get());
}
static class PermissionCollectionLinks {
static class PermissionLinks {
private final LinkBuilder permissionLinkBuilder;
PermissionCollectionLinks(UriInfo uriInfo) {
permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class, PermissionCollectionResource.class);
PermissionLinks(UriInfo uriInfo) {
permissionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, PermissionRootResource.class);
}
String self(String namespace, String name) {
return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getPermissionCollectionResource").parameters().method("getAll").parameters().href();
String all(String namespace, String name) {
return permissionLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("permissions").parameters().method("getAll").parameters().href();
}
String self(String repositoryNamespace, String repositoryName, String permissionName) {
return getLink(repositoryNamespace, repositoryName, permissionName, "get");
}
String update(String repositoryNamespace, String repositoryName, String permissionName) {
return getLink(repositoryNamespace, repositoryName, permissionName, "update");
}
String delete(String repositoryNamespace, String repositoryName, String permissionName) {
return getLink(repositoryNamespace, repositoryName, permissionName, "delete");
}
private String getLink(String repositoryNamespace, String repositoryName, String permissionName, String methodName) {
return permissionLinkBuilder.method("getRepositoryResource").parameters(repositoryNamespace, repositoryName).method("permissions").parameters().method(methodName).parameters(permissionName).href();
}
}
}

View File

@@ -0,0 +1,437 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableList;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.assertj.core.util.Lists;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.spi.HttpRequest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.repository.*;
import sonia.scm.web.VndMediaType;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
@SubjectAware(
username = "trillian",
password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
@RunWith(MockitoJUnitRunner.Silent.class)
@Slf4j
public class PermissionRootResourceTest {
public static final String REPOSITORY_NAMESPACE = "repo_namespace";
public static final String REPOSITORY_NAME = "repo";
private static final String PERMISSION_NAME = "perm";
private static final String PATH_OF_ALL_PERMISSIONS = REPOSITORY_NAMESPACE + "/" + REPOSITORY_NAME + "/permissions/";
private static final String PATH_OF_ONE_PERMISSION = PATH_OF_ALL_PERMISSIONS + PERMISSION_NAME;
private static final String PERMISSION_TEST_PAYLOAD = "{ \"name\" : \"permission_name\", \"type\" : \"READ\" }";
private static final ArrayList<Permission> TEST_PERMISSIONS = Lists
.newArrayList(
new Permission("user_write", PermissionType.WRITE, false),
new Permission("user_read", PermissionType.READ, false),
new Permission("user_owner", PermissionType.OWNER, false),
new Permission("group_read", PermissionType.READ, true),
new Permission("group_write", PermissionType.WRITE, true),
new Permission("group_owner", PermissionType.OWNER, true)
);
private final ExpectedRequest requestGETAllPermissions = new ExpectedRequest()
.description("GET all permissions")
.method("GET")
.path(PATH_OF_ALL_PERMISSIONS);
private final ExpectedRequest requestPOSTPermission = new ExpectedRequest()
.description("create new permission")
.method("POST")
.content(PERMISSION_TEST_PAYLOAD)
.path(PATH_OF_ALL_PERMISSIONS);
private final ExpectedRequest requestGETPermission = new ExpectedRequest()
.description("GET permission")
.method("GET")
.path(PATH_OF_ONE_PERMISSION);
private final ExpectedRequest requestDELETEPermission = new ExpectedRequest()
.description("delete permission")
.method("DELETE")
.path(PATH_OF_ONE_PERMISSION);
private final ExpectedRequest requestPUTPermission = new ExpectedRequest()
.description("update permission")
.method("PUT")
.content(PERMISSION_TEST_PAYLOAD)
.path(PATH_OF_ONE_PERMISSION);
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
@Rule
public ShiroRule shiro = new ShiroRule();
@Mock
private RepositoryManager repositoryManager;
private final URI baseUri = URI.create("/");
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@InjectMocks
private PermissionToPermissionDtoMapperImpl permissionToPermissionDtoMapper;
@InjectMocks
private PermissionDtoToPermissionMapperImpl permissionDtoToPermissionMapper;
private PermissionRootResource permissionRootResource;
@BeforeEach
@Before
public void prepareEnvironment() {
initMocks(this);
permissionRootResource = spy(new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, resourceLinks, repositoryManager));
RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider
.of(new RepositoryResource(null, null, null, null, null, null, null, MockProvider.of(permissionRootResource))), null);
dispatcher.getRegistry().addSingletonResource(repositoryRootResource);
dispatcher.getProviderFactory().registerProvider(RepositoryNotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(PermissionNotFoundExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(PermissionAlreadyExistsExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
}
@Test
public void shouldGetAllPermissions() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
assertExpectedRequest(requestGETAllPermissions
.expectedResponseStatus(200)
.responseValidator((response) -> {
String body = response.getContentAsString();
ObjectMapper mapper = new ObjectMapper();
try {
List<PermissionDto> actualPermissionDtos = mapper.readValue(body, new TypeReference<List<PermissionDto>>() {
});
assertThat(actualPermissionDtos)
.as("response payload match permission object models")
.hasSize(TEST_PERMISSIONS.size())
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrder(getExpectedPermissionDtos(TEST_PERMISSIONS))
;
} catch (IOException e) {
fail();
}
})
);
}
@Test
public void shouldGetPermissionByName() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission expectedPermission = TEST_PERMISSIONS.get(0);
assertExpectedRequest(requestGETPermission
.expectedResponseStatus(200)
.path(PATH_OF_ALL_PERMISSIONS + expectedPermission.getName())
.responseValidator((response) -> {
String body = response.getContentAsString();
ObjectMapper mapper = new ObjectMapper();
try {
PermissionDto actualPermissionDto = mapper.readValue(body, PermissionDto.class);
assertThat(actualPermissionDto)
.as("response payload match permission object model")
.isEqualToComparingFieldByFieldRecursively(getExpectedPermissionDto(expectedPermission))
;
} catch (IOException e) {
fail();
}
})
);
}
@Test
public void shouldGetCreatedPermissions() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission newPermission = new Permission("new_group_perm", PermissionType.WRITE, true);
ArrayList<Permission> permissions = Lists.newArrayList(TEST_PERMISSIONS);
permissions.add(newPermission);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(permissions);
assertExpectedRequest(requestPOSTPermission
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}")
.expectedResponseStatus(201)
.responseValidator(response -> assertThat(response.getContentAsString())
.as("POST response has no body")
.isBlank())
);
assertGettingExpectedPermissions(expectedPermissions);
}
@Test
public void shouldNotAddExistingPermission() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission newPermission = TEST_PERMISSIONS.get(0);
assertExpectedRequest(requestPOSTPermission
.content("{\"name\" : \"" + newPermission.getName() + "\" , \"type\" : \"WRITE\" , \"groupPermission\" : true}")
.expectedResponseStatus(409)
);
}
@Test
public void shouldGetUpdatedPermissions() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission modifiedPermission = TEST_PERMISSIONS.get(0);
// modify the type to owner
modifiedPermission.setType(PermissionType.OWNER);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS);
assertExpectedRequest(requestPUTPermission
.content("{\"name\" : \"" + modifiedPermission.getName() + "\" , \"type\" : \"OWNER\" , \"groupPermission\" : false}")
.path(PATH_OF_ALL_PERMISSIONS + modifiedPermission.getName())
.expectedResponseStatus(204)
.responseValidator(response -> assertThat(response.getContentAsString())
.as("PUT response has no body")
.isBlank())
);
assertGettingExpectedPermissions(expectedPermissions);
}
@Test
public void shouldDeletePermissions() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size()));
assertExpectedRequest(requestDELETEPermission
.path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName())
.expectedResponseStatus(204)
.responseValidator(response -> assertThat(response.getContentAsString())
.as("DELETE response has no body")
.isBlank())
);
assertGettingExpectedPermissions(expectedPermissions);
}
@Test
public void deletingNotExistingPermissionShouldProcess() {
authorizedUserHasARepositoryWithPermissions(TEST_PERMISSIONS);
Permission deletedPermission = TEST_PERMISSIONS.get(0);
ImmutableList<Permission> expectedPermissions = ImmutableList.copyOf(TEST_PERMISSIONS.subList(1, TEST_PERMISSIONS.size()));
assertExpectedRequest(requestDELETEPermission
.path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName())
.expectedResponseStatus(204)
.responseValidator(response -> assertThat(response.getContentAsString())
.as("DELETE response has no body")
.isBlank())
);
assertGettingExpectedPermissions(expectedPermissions);
assertExpectedRequest(requestDELETEPermission
.path(PATH_OF_ALL_PERMISSIONS + deletedPermission.getName())
.expectedResponseStatus(204)
.responseValidator(response -> assertThat(response.getContentAsString())
.as("DELETE response has no body")
.isBlank())
);
assertGettingExpectedPermissions(expectedPermissions);
}
private void assertGettingExpectedPermissions(ImmutableList<Permission> expectedPermissions) {
assertExpectedRequest(requestGETAllPermissions
.expectedResponseStatus(200)
.responseValidator((response) -> {
String body = response.getContentAsString();
ObjectMapper mapper = new ObjectMapper();
try {
List<PermissionDto> actualPermissionDtos = mapper.readValue(body, new TypeReference<List<PermissionDto>>() {
});
assertThat(actualPermissionDtos)
.as("response payload match permission object models")
.hasSize(expectedPermissions.size())
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrder(getExpectedPermissionDtos(Lists.newArrayList(expectedPermissions)))
;
} catch (IOException e) {
fail();
}
})
);
}
private PermissionDto[] getExpectedPermissionDtos(ArrayList<Permission> permissions) {
return permissions
.stream()
.map(this::getExpectedPermissionDto)
.toArray(PermissionDto[]::new);
}
private PermissionDto getExpectedPermissionDto(Permission permission) {
PermissionDto result = new PermissionDto();
result.setName(permission.getName());
result.setGroupPermission(permission.isGroupPermission());
result.setType(permission.getType().name());
String permissionHref = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS + permission.getName();
result.add(linkingTo()
.self(permissionHref)
.single(link("update", permissionHref))
.single(link("delete", permissionHref))
.build());
return result;
}
@TestFactory
@DisplayName("test endpoints on missing repository and user is Admin")
Stream<DynamicTest> missedRepositoryTestFactory() {
return createDynamicTestsToAssertResponses(
requestGETAllPermissions.expectedResponseStatus(404),
requestGETPermission.expectedResponseStatus(404),
requestPOSTPermission.expectedResponseStatus(404),
requestDELETEPermission.expectedResponseStatus(404),
requestPUTPermission.expectedResponseStatus(404));
}
@TestFactory
@DisplayName("test endpoints on missing permission and user is Admin")
Stream<DynamicTest> missedPermissionTestFactory() {
authorizedUserHasARepository();
return createDynamicTestsToAssertResponses(
requestGETPermission.expectedResponseStatus(404),
requestPOSTPermission.expectedResponseStatus(201),
requestGETAllPermissions.expectedResponseStatus(200),
requestDELETEPermission.expectedResponseStatus(204),
requestPUTPermission.expectedResponseStatus(404));
}
private Repository authorizedUserHasARepository() {
Repository mockRepository = mock(Repository.class);
when(mockRepository.getId()).thenReturn(REPOSITORY_NAME);
when(mockRepository.getNamespace()).thenReturn(REPOSITORY_NAMESPACE);
when(mockRepository.getName()).thenReturn(REPOSITORY_NAME);
doNothing().when(permissionRootResource).checkUserPermitted(mockRepository);
when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository);
return mockRepository;
}
private void authorizedUserHasARepositoryWithPermissions(ArrayList<Permission> permissions) {
when(authorizedUserHasARepository().getPermissions()).thenReturn(permissions);
}
@TestFactory
@DisplayName("test endpoints on missing permission and user is not Admin")
Stream<DynamicTest> missedPermissionUserForbiddenTestFactory() {
Repository mockRepository = mock(Repository.class);
when(mockRepository.getId()).thenReturn(REPOSITORY_NAME);
doThrow(AuthorizationException.class).when(permissionRootResource).checkUserPermitted(mockRepository);
when(repositoryManager.get(any(NamespaceAndName.class))).thenReturn(mockRepository);
return createDynamicTestsToAssertResponses(
requestGETPermission.expectedResponseStatus(401),
requestPOSTPermission.expectedResponseStatus(401),
requestGETAllPermissions.expectedResponseStatus(401),
requestDELETEPermission.expectedResponseStatus(401),
requestPUTPermission.expectedResponseStatus(401));
}
private Stream<DynamicTest> createDynamicTestsToAssertResponses(ExpectedRequest... expectedRequests) {
return Stream.of(expectedRequests)
.map(entry -> dynamicTest("the endpoint " + entry.description + " should return the status code " + entry.expectedResponseStatus, () -> assertExpectedRequest(entry)));
}
private MockHttpResponse assertExpectedRequest(ExpectedRequest entry) {
MockHttpResponse response = new MockHttpResponse();
HttpRequest request = null;
try {
request = MockHttpRequest
.create(entry.method, "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + entry.path)
.content(entry.content)
.contentType(VndMediaType.PERMISSION);
} catch (URISyntaxException e) {
fail(e.getMessage());
}
dispatcher.invoke(request, response);
log.info("Test the Request :{}", entry);
assertThat(entry.expectedResponseStatus)
.as("assert status code")
.isEqualTo(response.getStatus());
if (entry.responseValidator != null) {
entry.responseValidator.accept(response);
}
return response;
}
@ToString
public class ExpectedRequest {
private String description;
private String method;
private String path;
private int expectedResponseStatus;
private byte[] content = new byte[]{};
private Consumer<MockHttpResponse> responseValidator;
public ExpectedRequest description(String description) {
this.description = description;
return this;
}
public ExpectedRequest method(String method) {
this.method = method;
return this;
}
public ExpectedRequest path(String path) {
this.path = path;
return this;
}
public ExpectedRequest content(String content) {
if (content != null) {
this.content = content.getBytes();
}
return this;
}
public ExpectedRequest expectedResponseStatus(int expectedResponseStatus) {
this.expectedResponseStatus = expectedResponseStatus;
return this;
}
public ExpectedRequest responseValidator(Consumer<MockHttpResponse> responseValidator) {
this.responseValidator = responseValidator;
return this;
}
}
}

View File

@@ -23,7 +23,7 @@ public class ResourceLinksMock {
when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo));
when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo));
when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo));
when(resourceLinks.permissionCollection()).thenReturn(new ResourceLinks.PermissionCollectionLinks(uriInfo));
when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo));
when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo));
when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo));
when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo));