From 0768b638edeaf7497458df1a1be034125eeed9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 12:39:01 +0200 Subject: [PATCH 01/25] Bootstrap v2 version to get repositories --- .../java/sonia/scm/repository/Repository.java | 12 ++-- .../RepositoryManagerDecorator.java | 19 ++----- .../main/java/sonia/scm/web/VndMediaType.java | 1 + .../rest/resources/RepositoryResource.java | 12 +--- .../scm/api/v2/resources/BaseMapper.java | 2 +- .../v2/resources/HealthCheckFailureDto.java | 11 ++++ .../scm/api/v2/resources/MapperModule.java | 2 + .../scm/api/v2/resources/PermissionDto.java | 11 ++++ .../scm/api/v2/resources/RepositoryDto.java | 29 ++++++++++ .../api/v2/resources/RepositoryResource.java | 44 +++++++++++++++ .../v2/resources/RepositoryRootResource.java | 25 +++++++++ .../RepositoryToRepositoryDtoMapper.java | 20 +++++++ .../RepositoryToRepositoryDtoMapperTest.java | 56 +++++++++++++++++++ 13 files changed, 213 insertions(+), 31 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 63bef4ea6c..0b4d1c4dcd 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -37,22 +37,20 @@ import com.github.sdorra.ssp.PermissionObject; import com.github.sdorra.ssp.StaticPermissions; import com.google.common.base.Objects; import com.google.common.collect.Lists; - import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * Source code repository. @@ -207,6 +205,10 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return name; } + public String getNamespace() { + return namespace; + } + /** * Returns the access permissions of the {@link Repository}. * diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java index 632b54b741..d99a6483ab 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java @@ -38,13 +38,11 @@ package sonia.scm.repository; import sonia.scm.ManagerDecorator; import sonia.scm.Type; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.io.IOException; - import java.util.Collection; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * Decorator for {@link RepositoryManager}. @@ -92,19 +90,10 @@ public class RepositoryManagerDecorator //~--- get methods ---------------------------------------------------------- - /** - * {@inheritDoc} - * - * - * @param type - * @param name - * - * @return - */ @Override - public Repository get(String type, String name) + public Repository get(String namespace, String name) { - return decorated.get(type, name); + return decorated.get(namespace, name); } /** diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index 3ec121f9a4..f306114ecb 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -14,6 +14,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 USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java index 0637f3fc8f..740f841f18 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryResource.java @@ -46,7 +46,6 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.BlameResult; import sonia.scm.repository.Branches; import sonia.scm.repository.BrowserResult; @@ -120,19 +119,15 @@ public class RepositoryResource extends AbstractManagerResource { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes - public abstract D map(T user); + public abstract D map(T modelObject); Instant mapTime(Long epochMilli) { return epochMilli == null? null: Instant.ofEpochMilli(epochMilli); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java new file mode 100644 index 0000000000..21d98c257c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/HealthCheckFailureDto.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class HealthCheckFailureDto { + private String description; + private String summary; + private String url; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index fec2a677b5..0a0468de3c 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -15,6 +15,8 @@ public class MapperModule extends AbstractModule { bind(GroupToGroupDtoMapper.class).to(Mappers.getMapper(GroupToGroupDtoMapper.class).getClass()); bind(GroupCollectionToDtoMapper.class); + bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass()); + bind(UriInfoStore.class).in(ServletScopes.REQUEST); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java new file mode 100644 index 0000000000..293a16dd17 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class PermissionDto { + private String type; + private String name; + private boolean groupPermission; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java new file mode 100644 index 0000000000..6afac172df --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -0,0 +1,29 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.annotation.JsonInclude; +import de.otto.edison.hal.HalRepresentation; +import lombok.Getter; +import lombok.Setter; + +import javax.xml.bind.annotation.XmlElement; +import java.time.Instant; +import java.util.List; + +@Getter @Setter +public class RepositoryDto extends HalRepresentation { + + private String id; + private String contact; + private Instant creationDate; + private String description; + private List healthCheckFailures; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Instant lastModified; + private String namespace; + private String name; + private List permissions; + @XmlElement(name = "public") + private boolean publicReadable = false; + private boolean archived = false; + private String type; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java new file mode 100644 index 0000000000..c171dc5d83 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -0,0 +1,44 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; + +public class RepositoryResource { + + private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + + private final ResourceManagerAdapter adapter; + + @Inject + public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { + this.repositoryToDtoMapper = repositoryToDtoMapper; + this.adapter = new ResourceManagerAdapter<>(manager); + } + + @GET + @Path("") + @Produces(VndMediaType.REPOSITORY) + @TypeHint(RepositoryDto.class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the repository"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { + return adapter.get("31QwjAKOK2", repositoryToDtoMapper::map); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java new file mode 100644 index 0000000000..e7861a884b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -0,0 +1,25 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +/** + * RESTful Web Service Resource to manage repositories. + */ +@Path(RepositoryRootResource.REPOSITORIES_PATH_V2) +public class RepositoryRootResource { + static final String REPOSITORIES_PATH_V2 = "v2/repositories/"; + + private final Provider repositoryResource; + + @Inject + public RepositoryRootResource(Provider repositoryResource) { + this.repositoryResource = repositoryResource; + } + + @Path("{namespace}/{name}") + public RepositoryResource getRepositoryResource() { + return repositoryResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java new file mode 100644 index 0000000000..0574fa9dd6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.Inject; +import org.mapstruct.Mapper; +import sonia.scm.repository.HealthCheckFailure; +import sonia.scm.repository.Permission; +import sonia.scm.repository.Repository; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +@Mapper +public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper { + + @Inject + private ResourceLinks resourceLinks; + + abstract HealthCheckFailureDto toDto(HealthCheckFailure failure); + + abstract PermissionDto toDto(Permission permission); +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java new file mode 100644 index 0000000000..39eb8418a6 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -0,0 +1,56 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; +import sonia.scm.repository.HealthCheckFailure; +import sonia.scm.repository.Permission; +import sonia.scm.repository.PermissionType; +import sonia.scm.repository.Repository; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class RepositoryToRepositoryDtoMapperTest { + + private RepositoryToRepositoryDtoMapperImpl mapper = new RepositoryToRepositoryDtoMapperImpl(); + + @Test + public void shouldMapSimpleProperties() { + RepositoryDto dto = mapper.map(createDummyRepository()); + assertEquals("namespace", dto.getNamespace()); + assertEquals("name", dto.getName()); + assertEquals("description", dto.getDescription()); + assertEquals("git", dto.getType()); + assertEquals("none@example.com", dto.getContact()); + assertEquals("1", dto.getId()); + } + + @Test + public void shouldMapHealthCheck() { + RepositoryDto dto = mapper.map(createDummyRepository()); + assertEquals(1, dto.getHealthCheckFailures().size()); + assertEquals("summary", dto.getHealthCheckFailures().get(0).getSummary()); + } + + @Test + public void shouldMapPermissions() { + RepositoryDto dto = mapper.map(createDummyRepository()); + assertEquals(1, dto.getPermissions().size()); + assertEquals("permission", dto.getPermissions().get(0).getName()); + assertEquals("READ", dto.getPermissions().get(0).getType()); + } + + private Repository createDummyRepository() { + Repository repository = new Repository(); + repository.setNamespace("namespace"); + repository.setName("name"); + repository.setDescription("description"); + repository.setType("git"); + repository.setContact("none@example.com"); + repository.setId("1"); + repository.setCreationDate(System.currentTimeMillis()); + repository.setHealthCheckFailures(asList(new HealthCheckFailure("1", "summary", "url", "failure"))); + repository.setPermissions(asList(new Permission("permission", PermissionType.READ))); + + return repository; + } +} From 1bcc35d48bff712395b4e5a4f95411b53815ce03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 13:11:18 +0200 Subject: [PATCH 02/25] Support reading object by other identifiers than id Therefore split adapter class for single entity and collection handling. --- .../scm/repository/RepositoryManager.java | 14 +++- ... => CollectionResourceManagerAdapter.java} | 33 +------- .../v2/resources/GroupCollectionResource.java | 18 ++++- .../scm/api/v2/resources/GroupResource.java | 4 +- .../resources/IdResourceManagerAdapter.java | 51 ++++++++++++ .../api/v2/resources/RepositoryResource.java | 8 +- .../SingleResourceManagerAdapter.java | 77 +++++++++++++++++++ .../v2/resources/UserCollectionResource.java | 18 ++++- .../scm/api/v2/resources/UserResource.java | 4 +- .../resources/RepositoryRootResourceTest.java | 77 +++++++++++++++++++ 10 files changed, 254 insertions(+), 50 deletions(-) rename scm-webapp/src/main/java/sonia/scm/api/v2/resources/{ResourceManagerAdapter.java => CollectionResourceManagerAdapter.java} (65%) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 7cbac2b52e..ca1d5ab743 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -38,13 +38,11 @@ package sonia.scm.repository; import sonia.scm.Type; import sonia.scm.TypeManager; -//~--- JDK imports ------------------------------------------------------------ - +import javax.servlet.http.HttpServletRequest; import java.io.IOException; - import java.util.Collection; -import javax.servlet.http.HttpServletRequest; +//~--- JDK imports ------------------------------------------------------------ /** * The central class for managing {@link Repository} objects. @@ -149,4 +147,12 @@ public interface RepositoryManager */ @Override public RepositoryHandler getHandler(String type); + + default Repository getByNamespace(String namespace, String name) { + return getAll() + .stream() + .filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace)) + .findFirst() + .orElse(null); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java similarity index 65% rename from scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java rename to scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java index c2aab1a058..925198756a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java @@ -26,43 +26,14 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? -class ResourceManagerAdapter extends AbstractManagerResource { - ResourceManagerAdapter(Manager manager) { + CollectionResourceManagerAdapter(Manager manager) { super(manager); } - /** - * Reads the model object for the given id, transforms it to a dto and returns a corresponding http response. - * This handles all corner cases, eg. no matching object for the id or missing privileges. - */ - Response get(String id, Function mapToDto) { - MODEL_OBJECT modelObject = manager.get(id); - if (modelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - DTO dto = mapToDto.apply(modelObject); - return Response.ok(dto).build(); - } - - /** - * Update the model object for the given id according to the given function and returns a corresponding http response. - * This handles all corner cases, eg. no matching object for the id or missing privileges. - */ - public Response update(String id, Function applyChanges) { - MODEL_OBJECT existingModelObject = manager.get(id); - if (existingModelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); - if (!id.equals(changedModelObject.getId())) { - return Response.status(BAD_REQUEST).entity("illegal change of id").build(); - } - return update(id, changedModelObject); - } - /** * Reads all model objects in a paged way, maps them using the given function and returns a corresponding http response. * This handles all corner cases, eg. missing privileges. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 2d777c7487..195efc56ae 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.group.Group; import sonia.scm.group.GroupException; import sonia.scm.group.GroupManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -19,14 +29,14 @@ public class GroupCollectionResource { private final GroupCollectionToDtoMapper groupCollectionToDtoMapper; private final ResourceLinks resourceLinks; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public GroupCollectionResource(GroupManager manager, GroupDtoToGroupMapper dtoToGroupMapper, GroupCollectionToDtoMapper groupCollectionToDtoMapper, ResourceLinks resourceLinks) { this.dtoToGroupMapper = dtoToGroupMapper; this.groupCollectionToDtoMapper = groupCollectionToDtoMapper; this.resourceLinks = resourceLinks; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java index 3c1f2aeb64..7a490b6d7a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupResource.java @@ -22,14 +22,14 @@ public class GroupResource { private final GroupToGroupDtoMapper groupToGroupDtoMapper; private final GroupDtoToGroupMapper dtoToGroupMapper; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public GroupResource(GroupManager manager, GroupToGroupDtoMapper groupToGroupDtoMapper, GroupDtoToGroupMapper groupDtoToGroupMapper) { this.groupToGroupDtoMapper = groupToGroupDtoMapper; this.dtoToGroupMapper = groupDtoToGroupMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java new file mode 100644 index 0000000000..c7bcc3b6b1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -0,0 +1,51 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.Manager; +import sonia.scm.ModelObject; +import sonia.scm.PageResult; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter}. + */ +@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? +class IdResourceManagerAdapter { + + private final Manager manager; + + private final SingleResourceManagerAdapter singleAdapter; + private final CollectionResourceManagerAdapter collectionAdapter; + + IdResourceManagerAdapter(Manager manager) { + this.manager = manager; + singleAdapter = new SingleResourceManagerAdapter<>(manager); + collectionAdapter = new CollectionResourceManagerAdapter<>(manager); + } + + Response get(String id, Function mapToDto) { + return singleAdapter.get(() -> manager.get(id), mapToDto); + } + + public Response update(String id, Function applyChanges) { + return singleAdapter.update(() -> manager.get(id), applyChanges); + } + + public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function, CollectionDto> mapToDto) { + return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto); + } + + public Response create(DTO dto, Supplier modelObjectSupplier, Function uriCreator) throws IOException, EXCEPTION { + return collectionAdapter.create(dto, modelObjectSupplier, uriCreator); + } + + public Response delete(String id) { + return singleAdapter.delete(id); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c171dc5d83..c3cb349529 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -19,12 +19,14 @@ public class RepositoryResource { private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; - private final ResourceManagerAdapter adapter; + private final RepositoryManager manager; + private final SingleResourceManagerAdapter adapter; @Inject public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { + this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new SingleResourceManagerAdapter<>(manager); } @GET @@ -39,6 +41,6 @@ public class RepositoryResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { - return adapter.get("31QwjAKOK2", repositoryToDtoMapper::map); + return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java new file mode 100644 index 0000000000..a56111d893 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -0,0 +1,77 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import sonia.scm.Manager; +import sonia.scm.ModelObject; +import sonia.scm.api.rest.resources.AbstractManagerResource; + +import javax.ws.rs.core.GenericEntity; +import javax.ws.rs.core.Response; +import java.util.Collection; +import java.util.function.Function; +import java.util.function.Supplier; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; + +/** + * Adapter from resource http endpoints to managers. + * + * Provides common CRUD operations and DTO to Model Object mapping to keep Resources more DRY. + * + * @param The type of the model object, eg. {@link sonia.scm.user.User}. + * @param The corresponding transport object, eg. {@link UserDto}. + * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. + */ +@SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? +class SingleResourceManagerAdapter extends AbstractManagerResource { + + SingleResourceManagerAdapter(Manager manager) { + super(manager); + } + + /** + * Reads the model object for the given id, transforms it to a dto and returns a corresponding http response. + * This handles all corner cases, eg. no matching object for the id or missing privileges. + */ + Response get(Supplier reader, Function mapToDto) { + MODEL_OBJECT modelObject = reader.get(); + if (modelObject == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + DTO dto = mapToDto.apply(modelObject); + return Response.ok(dto).build(); + } + + /** + * Update the model object for the given id according to the given function and returns a corresponding http response. + * This handles all corner cases, eg. no matching object for the id or missing privileges. + */ + public Response update(Supplier reader, Function applyChanges) { + MODEL_OBJECT existingModelObject = reader.get(); + if (existingModelObject == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); + if (!getId(existingModelObject).equals(getId(changedModelObject))) { + return Response.status(BAD_REQUEST).entity("illegal change of id").build(); + } + return update(getId(existingModelObject), changedModelObject); + } + + @Override + protected GenericEntity> createGenericEntity(Collection modelObjects) { + throw new UnsupportedOperationException(); + } + + @Override + protected String getId(MODEL_OBJECT item) { + return item.getId(); + } + + @Override + protected String getPathPart() { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 4b88ea48bf..8daddd67c1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -1,13 +1,23 @@ package sonia.scm.api.v2.resources; -import com.webcohesion.enunciate.metadata.rs.*; +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.user.User; import sonia.scm.user.UserException; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.io.IOException; @@ -18,14 +28,14 @@ public class UserCollectionResource { private final UserCollectionToDtoMapper userCollectionToDtoMapper; private final ResourceLinks resourceLinks; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper, UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) { this.dtoToUserMapper = dtoToUserMapper; this.userCollectionToDtoMapper = userCollectionToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); this.resourceLinks = resourceLinks; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index bf747bab4d..2cfcdfe772 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -23,13 +23,13 @@ public class UserResource { private final UserDtoToUserMapper dtoToUserMapper; private final UserToUserDtoMapper userToDtoMapper; - private final ResourceManagerAdapter adapter; + private final IdResourceManagerAdapter adapter; @Inject public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) { this.dtoToUserMapper = dtoToUserMapper; this.userToDtoMapper = userToDtoMapper; - this.adapter = new ResourceManagerAdapter<>(manager); + this.adapter = new IdResourceManagerAdapter<>(manager); } /** diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java new file mode 100644 index 0000000000..74de87eb67 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -0,0 +1,77 @@ +package sonia.scm.api.v2.resources; + +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; + +import java.net.URI; +import java.net.URISyntaxException; + +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class RepositoryRootResourceTest { + + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + @Mock + private RepositoryManager repositoryManager; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ResourceLinks resourceLinks; + @InjectMocks + private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; + + @Before + public void prepareEnvironment() { + initMocks(this); + ResourceLinksMock.initMock(resourceLinks, URI.create("/")); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); + dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + } + + @Test + public void shouldFailForNotExistingRepository() throws URISyntaxException { + mockRepository("space", "repo"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NOT_FOUND, response.getStatus()); + } + + @Test + public void shouldFindExistingRepository() throws URISyntaxException { + mockRepository("space", "repo"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); + } + + private Repository mockRepository(String namespace, String name) { + Repository repository = new Repository(); + repository.setNamespace(namespace); + repository.setName(name); + when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository); + return repository; + } +} From 150838be85d17022614248e872edeedd258129a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 14:29:48 +0200 Subject: [PATCH 03/25] Generate repository links in dto --- .../scm/api/v2/resources/RepositoryDto.java | 7 ++ .../RepositoryToRepositoryDtoMapper.java | 19 +++++ .../scm/api/v2/resources/ResourceLinks.java | 24 ++++++ .../resources/RepositoryRootResourceTest.java | 12 +++ .../RepositoryToRepositoryDtoMapperTest.java | 80 ++++++++++++++++--- .../api/v2/resources/ResourceLinksMock.java | 4 + 6 files changed, 137 insertions(+), 9 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index 6afac172df..14083837bf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; import lombok.Getter; import lombok.Setter; @@ -26,4 +27,10 @@ public class RepositoryDto extends HalRepresentation { private boolean publicReadable = false; private boolean archived = false; private String type; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 0574fa9dd6..e2c246675d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -1,10 +1,17 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.Permission; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") @@ -17,4 +24,16 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper baseUri + GROUPS_PATH_V2); when(resourceLinks.groupCollection().create()).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2); + + when(resourceLinks.repository().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); + when(resourceLinks.repository().update(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); + when(resourceLinks.repository().delete(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); } } From fb811ef725f438195d4b78a0a41ae8c18d5f3f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Tue, 3 Jul 2018 16:17:51 +0200 Subject: [PATCH 04/25] Correct resource links and add tags link --- .../api/v2/resources/RepositoryResource.java | 24 ++++++++++++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 18 +++++++++++++- .../v2/resources/TagCollectionResource.java | 18 ++++++++++++++ .../scm/api/v2/resources/TagRootResource.java | 20 ++++++++++++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 +++++++ .../api/v2/resources/ResourceLinksMock.java | 2 ++ .../api/v2/resources/ResourceLinksTest.java | 24 +++++++++++++++++++ 9 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c3cb349529..3340c34df5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -9,7 +9,10 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.DELETE; import javax.ws.rs.GET; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -21,12 +24,14 @@ public class RepositoryResource { private final RepositoryManager manager; private final SingleResourceManagerAdapter adapter; + private final Provider tagRootResource; @Inject - public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager) { + public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager, Provider tagRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); + this.tagRootResource = tagRootResource; } @GET @@ -43,4 +48,21 @@ public class RepositoryResource { public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map); } + + @DELETE + @Path("") + public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) { + throw new UnsupportedOperationException(); + } + + @PUT + @Path("") + public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name) { + throw new UnsupportedOperationException(); + } + + @Path("tags/") + public TagRootResource tags() { + return tagRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index e2c246675d..278afd3916 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -34,6 +34,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper tagCollectionResource; + + @Inject + public TagRootResource(Provider tagCollectionResource) { + this.tagCollectionResource = tagCollectionResource; + } + + @Path("") + public TagCollectionResource getTagCollectionResource() { + return tagCollectionResource.get(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 36ed2415dc..decc7ccd3d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -48,7 +48,7 @@ public class RepositoryRootResourceTest { public void prepareEnvironment() { initMocks(this); ResourceLinksMock.initMock(resourceLinks, URI.create("/")); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 809fd5b3e5..ce879b7229 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -101,6 +101,14 @@ public class RepositoryToRepositoryDtoMapperTest { assertEquals("READ", dto.getPermissions().get(0).getType()); } + @Test + public void shouldCreateTagsLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/groups/testspace/test/tags/", + dto.getLinks().getLinkBy("tags").get().getHref()); + } + private Repository createTestRepository() { Repository repository = new Repository(); repository.setNamespace("testspace"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 465f6e0769..18da197b17 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -26,5 +26,7 @@ public class ResourceLinksMock { when(resourceLinks.repository().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); when(resourceLinks.repository().update(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); when(resourceLinks.repository().delete(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); + + when(resourceLinks.tagCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/tags/"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 103eebd411..272084cd97 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -84,6 +84,30 @@ public class ResourceLinksTest { assertEquals(BASE_URL + GroupRootResource.GROUPS_PATH_V2, url); } + @Test + public void shouldCreateCorrectRepositorySelfUrl() { + String url = resourceLinks.repository().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo", url); + } + + @Test + public void shouldCreateCorrectRepositoryDeleteUrl() { + String url = resourceLinks.repository().delete("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo", url); + } + + @Test + public void shouldCreateCorrectRepositoryUpdateUrl() { + String url = resourceLinks.repository().update("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo", url); + } + + @Test + public void shouldCreateCorrectTagCollectionUrl() { + String url = resourceLinks.tagCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/tags/", url); + } + @Before public void initUriInfo() { initMocks(this); From 7c662ed42a9f5dcbef3da0e494ca2a1b0119e7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 08:37:57 +0200 Subject: [PATCH 05/25] Generate link to branches for repository --- .../resources/BranchCollectionResource.java | 18 +++++++++++++++ .../api/v2/resources/BranchRootResource.java | 20 +++++++++++++++++ .../api/v2/resources/RepositoryResource.java | 13 ++++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 22 ++++++++++++++++--- .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 +++++++ .../api/v2/resources/ResourceLinksMock.java | 2 ++ .../api/v2/resources/ResourceLinksTest.java | 6 +++++ .../DefaultRepositoryManagerTest.java | 5 +++++ 10 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java new file mode 100644 index 0000000000..d64016b475 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchCollectionResource.java @@ -0,0 +1,18 @@ +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 BranchCollectionResource { + @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(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java new file mode 100644 index 0000000000..ee50fdcd1e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +public class BranchRootResource { + + private final Provider branchCollectionResource; + + @Inject + public BranchRootResource(Provider branchCollectionResource) { + this.branchCollectionResource = branchCollectionResource; + } + + @Path("") + public BranchCollectionResource getBranchCollectionResource() { + return branchCollectionResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 3340c34df5..e2bcd60e88 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -25,13 +25,19 @@ public class RepositoryResource { private final RepositoryManager manager; private final SingleResourceManagerAdapter adapter; private final Provider tagRootResource; + private final Provider branchRootResource; @Inject - public RepositoryResource(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager, Provider tagRootResource) { + public RepositoryResource( + RepositoryToRepositoryDtoMapper repositoryToDtoMapper, + RepositoryManager manager, + Provider tagRootResource, + Provider branchRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); this.tagRootResource = tagRootResource; + this.branchRootResource = branchRootResource; } @GET @@ -65,4 +71,9 @@ public class RepositoryResource { public TagRootResource tags() { return tagRootResource.get(); } + + @Path("branches/") + public BranchRootResource branches() { + return branchRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 278afd3916..6121ac05b2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -35,6 +35,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); when(resourceLinks.tagCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/tags/"); + + when(resourceLinks.branchCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/branches/"); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 272084cd97..de8e962948 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -108,6 +108,12 @@ public class ResourceLinksTest { assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/tags/", url); } + @Test + public void shouldCreateCorrectBranchCollectionUrl() { + String url = resourceLinks.branchCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/", url); + } + @Before public void initUriInfo() { initMocks(this); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index a66bf222cb..0371b4974b 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -37,6 +37,7 @@ import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableSet; import org.apache.shiro.authz.UnauthorizedException; +import org.apache.shiro.util.ThreadContext; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -92,6 +93,10 @@ import static org.mockito.Mockito.when; ) public class DefaultRepositoryManagerTest extends ManagerTestBase { + { + ThreadContext.unbindSecurityManager(); + } + @Rule public ShiroRule shiro = new ShiroRule(); From 6043b093da0630353319395e1bdfc6f3d8adea9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 08:55:36 +0200 Subject: [PATCH 06/25] Simplify mocking of resource links --- .../scm/api/v2/resources/ResourceLinks.java | 14 ++++---- .../v2/resources/GroupRootResourceTest.java | 3 +- .../resources/GroupToGroupDtoMapperTest.java | 3 +- .../resources/RepositoryRootResourceTest.java | 3 +- .../RepositoryToRepositoryDtoMapperTest.java | 13 ++++---- .../api/v2/resources/ResourceLinksMock.java | 33 +++++++------------ .../UserCollectionToDtoMapperTest.java | 3 +- .../v2/resources/UserRootResourceTest.java | 3 +- .../v2/resources/UserToUserDtoMapperTest.java | 3 +- 9 files changed, 30 insertions(+), 48 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 98b94fdd82..b905783d1d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -20,7 +20,7 @@ class ResourceLinks { static class GroupLinks { private final LinkBuilder groupLinkBuilder; - private GroupLinks(UriInfo uriInfo) { + GroupLinks(UriInfo uriInfo) { groupLinkBuilder = new LinkBuilder(uriInfo, GroupRootResource.class, GroupResource.class); } @@ -44,7 +44,7 @@ class ResourceLinks { static class GroupCollectionLinks { private final LinkBuilder collectionLinkBuilder; - private GroupCollectionLinks(UriInfo uriInfo) { + GroupCollectionLinks(UriInfo uriInfo) { collectionLinkBuilder = new LinkBuilder(uriInfo, GroupRootResource.class, GroupCollectionResource.class); } @@ -64,7 +64,7 @@ class ResourceLinks { static class UserLinks { private final LinkBuilder userLinkBuilder; - private UserLinks(UriInfo uriInfo) { + UserLinks(UriInfo uriInfo) { userLinkBuilder = new LinkBuilder(uriInfo, UserRootResource.class, UserResource.class); } @@ -88,7 +88,7 @@ class ResourceLinks { static class UserCollectionLinks { private final LinkBuilder collectionLinkBuilder; - private UserCollectionLinks(UriInfo uriInfo) { + UserCollectionLinks(UriInfo uriInfo) { collectionLinkBuilder = new LinkBuilder(uriInfo, UserRootResource.class, UserCollectionResource.class); } @@ -108,7 +108,7 @@ class ResourceLinks { static class RepositoryLinks { private final LinkBuilder repositoryLinkBuilder; - private RepositoryLinks(UriInfo uriInfo) { + RepositoryLinks(UriInfo uriInfo) { repositoryLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class); } @@ -132,7 +132,7 @@ class ResourceLinks { static class TagCollectionLinks { private final LinkBuilder tagLinkBuilder; - private TagCollectionLinks(UriInfo uriInfo) { + TagCollectionLinks(UriInfo uriInfo) { tagLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, TagRootResource.class, TagCollectionResource.class); } @@ -148,7 +148,7 @@ class ResourceLinks { static class BranchCollectionLinks { private final LinkBuilder branchLinkBuilder; - private BranchCollectionLinks(UriInfo uriInfo) { + BranchCollectionLinks(UriInfo uriInfo) { branchLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryResource.class, BranchRootResource.class, BranchCollectionResource.class); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 7f88ceb50f..cec210de1b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -10,7 +10,6 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -49,7 +48,7 @@ public class GroupRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @Mock diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java index 1db089cd0b..221733486e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java @@ -7,7 +7,6 @@ import org.apache.shiro.util.ThreadState; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.group.Group; @@ -24,7 +23,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class GroupToGroupDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 659ada8e89..023bb71dc2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -9,7 +9,6 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.Repository; @@ -39,7 +38,7 @@ public class RepositoryRootResourceTest { @Mock private RepositoryManager repositoryManager; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index b4396a78bf..67f903e6de 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -6,7 +6,6 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; @@ -26,7 +25,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class RepositoryToRepositoryDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks @@ -62,7 +61,7 @@ public class RepositoryToRepositoryDtoMapperTest { public void shouldCreateLinksForUnprivilegedUser() { RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test", + "http://example.com/base/v2/repositories/testspace/test", dto.getLinks().getLinkBy("self").get().getHref()); assertFalse(dto.getLinks().getLinkBy("update").isPresent()); assertFalse(dto.getLinks().getLinkBy("delete").isPresent()); @@ -73,7 +72,7 @@ public class RepositoryToRepositoryDtoMapperTest { when(subject.isPermitted("repository:delete:1")).thenReturn(true); RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test", + "http://example.com/base/v2/repositories/testspace/test", dto.getLinks().getLinkBy("delete").get().getHref()); } @@ -82,7 +81,7 @@ public class RepositoryToRepositoryDtoMapperTest { when(subject.isPermitted("repository:modify:1")).thenReturn(true); RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test", + "http://example.com/base/v2/repositories/testspace/test", dto.getLinks().getLinkBy("update").get().getHref()); } @@ -105,7 +104,7 @@ public class RepositoryToRepositoryDtoMapperTest { public void shouldCreateTagsLink() { RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test/tags/", + "http://example.com/base/v2/repositories/testspace/test/tags/", dto.getLinks().getLinkBy("tags").get().getHref()); } @@ -113,7 +112,7 @@ public class RepositoryToRepositoryDtoMapperTest { public void shouldCreateBranchesLink() { RepositoryDto dto = mapper.map(createTestRepository()); assertEquals( - "http://example.com/base/v2/groups/testspace/test/branches/", + "http://example.com/base/v2/repositories/testspace/test/branches/", dto.getLinks().getLinkBy("branches").get().getHref()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index e5ee289149..8913a6d9c4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -1,34 +1,23 @@ package sonia.scm.api.v2.resources; +import javax.ws.rs.core.UriInfo; import java.net.URI; -import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static sonia.scm.api.v2.resources.GroupRootResource.GROUPS_PATH_V2; -import static sonia.scm.api.v2.resources.UserRootResource.USERS_PATH_V2; public class ResourceLinksMock { public static void initMock(ResourceLinks resourceLinks, URI baseUri) { - when(resourceLinks.user().self(anyString())).thenAnswer(invocation -> baseUri + USERS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.user().update(anyString())).thenAnswer(invocation -> baseUri + USERS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.user().delete(anyString())).thenAnswer(invocation -> baseUri + USERS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.userCollection().self()).thenAnswer(invocation -> baseUri + USERS_PATH_V2); - when(resourceLinks.userCollection().create()).thenAnswer(invocation -> baseUri + USERS_PATH_V2); + UriInfo uriInfo = mock(UriInfo.class); + when(uriInfo.getBaseUri()).thenReturn(baseUri); - when(resourceLinks.group().self(anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.group().update(anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0]); - when(resourceLinks.group().delete(anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0]); - - when(resourceLinks.groupCollection().self()).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2); - when(resourceLinks.groupCollection().create()).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2); - - when(resourceLinks.repository().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); - when(resourceLinks.repository().update(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); - when(resourceLinks.repository().delete(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1]); - - when(resourceLinks.tagCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/tags/"); - - when(resourceLinks.branchCollection().self(anyString(), anyString())).thenAnswer(invocation -> baseUri + GROUPS_PATH_V2 + invocation.getArguments()[0] + "/" + invocation.getArguments()[1] + "/branches/"); + when(resourceLinks.user()).thenReturn(new ResourceLinks.UserLinks(uriInfo)); + when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo)); + when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); + when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); + when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); + when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); + when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java index b9ed8558d4..9b8a9c5d42 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java @@ -7,7 +7,6 @@ import org.apache.shiro.util.ThreadContext; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; @@ -28,7 +27,7 @@ import static sonia.scm.PageResult.createPage; public class UserCollectionToDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @Mock private UserToUserDtoMapper userToDtoMapper; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 077cbef9b2..5ac4096583 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -11,7 +11,6 @@ import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -51,7 +50,7 @@ public class UserRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @Mock diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index ae3168cc07..ebdd7e3c30 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -7,7 +7,6 @@ import org.apache.shiro.util.ThreadState; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.api.rest.resources.UserResource; @@ -25,7 +24,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class UserToUserDtoMapperTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private ResourceLinks resourceLinks; @InjectMocks From 397cf012a18c1b2b68108d0581d19aad4276681e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 10:57:34 +0200 Subject: [PATCH 07/25] Simplify mocking of resource links even further --- .../api/v2/resources/GroupRootResourceTest.java | 5 +---- .../v2/resources/GroupToGroupDtoMapperTest.java | 9 ++------- .../v2/resources/RepositoryRootResourceTest.java | 7 ++++--- .../RepositoryToRepositoryDtoMapperTest.java | 16 +++++++++------- .../scm/api/v2/resources/ResourceLinksMock.java | 4 +++- .../resources/UserCollectionToDtoMapperTest.java | 6 ++---- .../api/v2/resources/UserRootResourceTest.java | 5 +---- .../v2/resources/UserToUserDtoMapperTest.java | 10 +++------- 8 files changed, 25 insertions(+), 37 deletions(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index cec210de1b..f071015b80 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -48,8 +48,7 @@ public class GroupRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock - private ResourceLinks resourceLinks; + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock private GroupManager groupManager; @@ -70,8 +69,6 @@ public class GroupRootResourceTest { when(groupManager.getPage(any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1)); when(groupManager.get("admin")).thenReturn(group); - ResourceLinksMock.initMock(resourceLinks, URI.create("/")); - GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); GroupCollectionResource groupCollectionResource = new GroupCollectionResource(groupManager, dtoToGroupMapper, groupCollectionToDtoMapper, resourceLinks); GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java index 221733486e..831a4b7c2a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupToGroupDtoMapperTest.java @@ -8,7 +8,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; -import org.mockito.Mock; import sonia.scm.group.Group; import java.net.URI; @@ -23,8 +22,8 @@ import static org.mockito.MockitoAnnotations.initMocks; public class GroupToGroupDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private GroupToGroupDtoMapperImpl mapper; @@ -37,12 +36,8 @@ public class GroupToGroupDtoMapperTest { @Before public void init() throws URISyntaxException { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(GroupRootResource.GROUPS_PATH_V2 + "/"); subjectThreadState.bind(); - - ResourceLinksMock.initMock(resourceLinks, baseUri); - ThreadContext.bind(subject); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 023bb71dc2..fc03348c98 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -38,15 +38,16 @@ public class RepositoryRootResourceTest { @Mock private RepositoryManager repositoryManager; - @Mock - private ResourceLinks resourceLinks; + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; @Before public void prepareEnvironment() { initMocks(this); - ResourceLinksMock.initMock(resourceLinks, URI.create("/")); RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 67f903e6de..c286efe4b7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -4,17 +4,16 @@ import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; -import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.Permission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import java.net.URI; -import java.net.URISyntaxException; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; @@ -25,8 +24,8 @@ import static org.mockito.MockitoAnnotations.initMocks; public class RepositoryToRepositoryDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private RepositoryToRepositoryDtoMapperImpl mapper; @@ -37,15 +36,18 @@ public class RepositoryToRepositoryDtoMapperTest { private URI expectedBaseUri; @Before - public void init() throws URISyntaxException { + public void init() { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(RepositoryRootResource.REPOSITORIES_PATH_V2 + "/"); subjectThreadState.bind(); - ResourceLinksMock.initMock(resourceLinks, baseUri); ThreadContext.bind(subject); } + @After + public void cleanup() { + ThreadContext.unbindSubject(); + } + @Test public void shouldMapSimpleProperties() { RepositoryDto dto = mapper.map(createTestRepository()); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 8913a6d9c4..6e3cd921bf 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -7,7 +7,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class ResourceLinksMock { - public static void initMock(ResourceLinks resourceLinks, URI baseUri) { + public static ResourceLinks createMock(URI baseUri) { + ResourceLinks resourceLinks = mock(ResourceLinks.class); UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getBaseUri()).thenReturn(baseUri); @@ -19,5 +20,6 @@ public class ResourceLinksMock { when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); + return resourceLinks; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java index 9b8a9c5d42..4d2bf7c62e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapperTest.java @@ -27,8 +27,8 @@ import static sonia.scm.PageResult.createPage; public class UserCollectionToDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @Mock private UserToUserDtoMapper userToDtoMapper; @Mock @@ -44,9 +44,7 @@ public class UserCollectionToDtoMapperTest { @Before public void init() throws URISyntaxException { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); - ResourceLinksMock.initMock(resourceLinks, baseUri); subjectThreadState.bind(); ThreadContext.bind(subject); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 5ac4096583..2cf2ee0566 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -50,8 +50,7 @@ public class UserRootResourceTest { private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - @Mock - private ResourceLinks resourceLinks; + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock private PasswordService passwordService; @@ -72,8 +71,6 @@ public class UserRootResourceTest { doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); - ResourceLinksMock.initMock(resourceLinks, URI.create("/")); - UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks); UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper, userCollectionToDtoMapper, resourceLinks); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index ebdd7e3c30..0ac980df45 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -8,12 +8,10 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; -import org.mockito.Mock; import sonia.scm.api.rest.resources.UserResource; import sonia.scm.user.User; import java.net.URI; -import java.net.URISyntaxException; import java.time.Instant; import static org.junit.Assert.assertEquals; @@ -24,8 +22,8 @@ import static org.mockito.MockitoAnnotations.initMocks; public class UserToUserDtoMapperTest { - @Mock - private ResourceLinks resourceLinks; + private final URI baseUri = URI.create("http://example.com/base/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks private UserToUserDtoMapperImpl mapper; @@ -36,12 +34,10 @@ public class UserToUserDtoMapperTest { private URI expectedBaseUri; @Before - public void init() throws URISyntaxException { + public void init() { initMocks(this); - URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); subjectThreadState.bind(); - ResourceLinksMock.initMock(resourceLinks, baseUri); ThreadContext.bind(subject); } From cc30b7d2a1164b71313466f3cbdc1bf7ee0bb162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 11:53:48 +0200 Subject: [PATCH 08/25] Remove id from repository dto --- .../src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java | 1 - .../api/v2/resources/RepositoryToRepositoryDtoMapperTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index 14083837bf..cde2feac27 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -13,7 +13,6 @@ import java.util.List; @Getter @Setter public class RepositoryDto extends HalRepresentation { - private String id; private String contact; private Instant creationDate; private String description; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index c286efe4b7..54bfb9d2c6 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -56,7 +56,6 @@ public class RepositoryToRepositoryDtoMapperTest { assertEquals("description", dto.getDescription()); assertEquals("git", dto.getType()); assertEquals("none@example.com", dto.getContact()); - assertEquals("1", dto.getId()); } @Test From 01a3b932895984510469de2fbea39603917721fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 11:58:37 +0200 Subject: [PATCH 09/25] Generate link to changesets for repository --- .../ChangesetCollectionResource.java | 18 +++++++++++++++++ .../v2/resources/ChangesetRootResource.java | 20 +++++++++++++++++++ .../api/v2/resources/RepositoryResource.java | 10 +++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 16 +++++++++++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 ++++++++ .../api/v2/resources/ResourceLinksMock.java | 1 + .../api/v2/resources/ResourceLinksTest.java | 6 ++++++ 9 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java new file mode 100644 index 0000000000..d42494a270 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionResource.java @@ -0,0 +1,18 @@ +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 ChangesetCollectionResource { + @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(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java new file mode 100644 index 0000000000..1681a27cd4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetRootResource.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +public class ChangesetRootResource { + + private final Provider changesetCollectionResource; + + @Inject + public ChangesetRootResource(Provider changesetCollectionResource) { + this.changesetCollectionResource = changesetCollectionResource; + } + + @Path("") + public ChangesetCollectionResource getChangesetCollectionResource() { + return changesetCollectionResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index e2bcd60e88..fe7cfc7ecf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -26,18 +26,21 @@ public class RepositoryResource { private final SingleResourceManagerAdapter adapter; private final Provider tagRootResource; private final Provider branchRootResource; + private final Provider changesetRootResource; @Inject public RepositoryResource( RepositoryToRepositoryDtoMapper repositoryToDtoMapper, RepositoryManager manager, Provider tagRootResource, - Provider branchRootResource) { + Provider branchRootResource, + Provider changesetRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); this.tagRootResource = tagRootResource; this.branchRootResource = branchRootResource; + this.changesetRootResource = changesetRootResource; } @GET @@ -76,4 +79,9 @@ public class RepositoryResource { public BranchRootResource branches() { return branchRootResource.get(); } + + @Path("changesets/") + public ChangesetRootResource changesets() { + return changesetRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 6121ac05b2..375c69c933 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -36,6 +36,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper Date: Wed, 4 Jul 2018 12:06:58 +0200 Subject: [PATCH 10/25] Generate link to sources for repository --- .../api/v2/resources/RepositoryResource.java | 10 +++++++++- .../RepositoryToRepositoryDtoMapper.java | 1 + .../scm/api/v2/resources/ResourceLinks.java | 16 +++++++++++++++ .../resources/SourceCollectionResource.java | 18 +++++++++++++++++ .../api/v2/resources/SourceRootResource.java | 20 +++++++++++++++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 8 ++++++++ .../api/v2/resources/ResourceLinksMock.java | 1 + .../api/v2/resources/ResourceLinksTest.java | 6 ++++++ 9 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/SourceRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index fe7cfc7ecf..666cd5672a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -27,6 +27,7 @@ public class RepositoryResource { private final Provider tagRootResource; private final Provider branchRootResource; private final Provider changesetRootResource; + private final Provider sourceRootResource; @Inject public RepositoryResource( @@ -34,13 +35,15 @@ public class RepositoryResource { RepositoryManager manager, Provider tagRootResource, Provider branchRootResource, - Provider changesetRootResource) { + Provider changesetRootResource, + Provider sourceRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); this.tagRootResource = tagRootResource; this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; + this.sourceRootResource = sourceRootResource; } @GET @@ -84,4 +87,9 @@ public class RepositoryResource { public ChangesetRootResource changesets() { return changesetRootResource.get(); } + + @Path("sources/") + public SourceRootResource sources() { + return sourceRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 375c69c933..b8c70a880d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -37,6 +37,7 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper sourceCollectionResource; + + @Inject + public SourceRootResource(Provider sourceCollectionResource) { + this.sourceCollectionResource = sourceCollectionResource; + } + + @Path("") + public SourceCollectionResource getSourceCollectionResource() { + return sourceCollectionResource.get(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 11d9de4563..cc93f2ecd2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -48,7 +48,7 @@ public class RepositoryRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null, null); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index bcf8948463..7d31504137 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -125,6 +125,14 @@ public class RepositoryToRepositoryDtoMapperTest { dto.getLinks().getLinkBy("changesets").get().getHref()); } + @Test + public void shouldCreateSourcesLink() { + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals( + "http://example.com/base/v2/repositories/testspace/test/sources/", + dto.getLinks().getLinkBy("sources").get().getHref()); + } + private Repository createTestRepository() { Repository repository = new Repository(); repository.setNamespace("testspace"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index dc4cd2e0be..efd8916811 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -21,6 +21,7 @@ public class ResourceLinksMock { when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changesetCollection()).thenReturn(new ResourceLinks.ChangesetCollectionLinks(uriInfo)); + when(resourceLinks.sourceCollection()).thenReturn(new ResourceLinks.SourceCollectionLinks(uriInfo)); return resourceLinks; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index 62f2a187d7..d9f20448f0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -120,6 +120,12 @@ public class ResourceLinksTest { assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/changesets/", url); } + @Test + public void shouldCreateCorrectSourceCollectionUrl() { + String url = resourceLinks.sourceCollection().self("space", "repo"); + assertEquals(BASE_URL + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/sources/", url); + } + @Before public void initUriInfo() { initMocks(this); From a5349b339db4fec6653740aa2678c62826e390d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 4 Jul 2018 15:11:17 +0200 Subject: [PATCH 11/25] Move repository permissions to separate endpoint --- .../PermissionCollectionResource.java | 18 ++++++++ .../v2/resources/PermissionRootResource.java | 20 +++++++++ .../scm/api/v2/resources/RepositoryDto.java | 4 -- .../api/v2/resources/RepositoryResource.java | 9 +++- .../RepositoryToRepositoryDtoMapper.java | 4 +- .../scm/api/v2/resources/ResourceLinks.java | 16 +++++++ .../resources/RepositoryRootResourceTest.java | 2 +- .../RepositoryToRepositoryDtoMapperTest.java | 44 +++++++++---------- .../api/v2/resources/ResourceLinksMock.java | 1 + .../api/v2/resources/ResourceLinksTest.java | 6 +++ 10 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java new file mode 100644 index 0000000000..6c4b52c16d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionCollectionResource.java @@ -0,0 +1,18 @@ +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(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java new file mode 100644 index 0000000000..cd1e970e43 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -0,0 +1,20 @@ +package sonia.scm.api.v2.resources; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.ws.rs.Path; + +public class PermissionRootResource { + + private final Provider permissionCollectionResource; + + @Inject + public PermissionRootResource(Provider permissionCollectionResource) { + this.permissionCollectionResource = permissionCollectionResource; + } + + @Path("") + public PermissionCollectionResource getPermissionCollectionResource() { + return permissionCollectionResource.get(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index cde2feac27..fbef4ca16a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -6,7 +6,6 @@ import de.otto.edison.hal.Links; import lombok.Getter; import lombok.Setter; -import javax.xml.bind.annotation.XmlElement; import java.time.Instant; import java.util.List; @@ -21,9 +20,6 @@ public class RepositoryDto extends HalRepresentation { private Instant lastModified; private String namespace; private String name; - private List permissions; - @XmlElement(name = "public") - private boolean publicReadable = false; private boolean archived = false; private String type; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 666cd5672a..0884664891 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -28,6 +28,7 @@ public class RepositoryResource { private final Provider branchRootResource; private final Provider changesetRootResource; private final Provider sourceRootResource; + private final Provider permissionRootResource; @Inject public RepositoryResource( @@ -36,7 +37,7 @@ public class RepositoryResource { Provider tagRootResource, Provider branchRootResource, Provider changesetRootResource, - Provider sourceRootResource) { + Provider sourceRootResource, Provider permissionRootResource) { this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); @@ -44,6 +45,7 @@ public class RepositoryResource { this.branchRootResource = branchRootResource; this.changesetRootResource = changesetRootResource; this.sourceRootResource = sourceRootResource; + this.permissionRootResource = permissionRootResource; } @GET @@ -92,4 +94,9 @@ public class RepositoryResource { public SourceRootResource sources() { return sourceRootResource.get(); } + + @Path("permissions/") + public PermissionRootResource permissions() { + return permissionRootResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index b8c70a880d..2f13723d39 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -6,7 +6,6 @@ import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import sonia.scm.repository.HealthCheckFailure; -import sonia.scm.repository.Permission; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; @@ -23,8 +22,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper Date: Wed, 4 Jul 2018 16:45:52 +0200 Subject: [PATCH 12/25] Create resource collection endpoint for get --- .../main/java/sonia/scm/web/VndMediaType.java | 1 + .../RepositoryCollectionResource.java | 54 +++++++++++++++++++ .../RepositoryCollectionToDtoMapper.java | 34 ++++++++++++ .../v2/resources/RepositoryRootResource.java | 9 +++- .../scm/api/v2/resources/ResourceLinks.java | 20 +++++++ .../resources/RepositoryRootResourceTest.java | 26 ++++++++- .../api/v2/resources/ResourceLinksMock.java | 1 + 7 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index f306114ecb..fb80450bc8 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -17,6 +17,7 @@ public class VndMediaType { public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String USER_COLLECTION = PREFIX + "userCollection" + SUFFIX; public static final String GROUP_COLLECTION = PREFIX + "groupCollection" + SUFFIX; + public static final String REPOSITORY_COLLECTION = PREFIX + "repositoryCollection" + SUFFIX; private VndMediaType() { } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java new file mode 100644 index 0000000000..e9c1f9e34e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -0,0 +1,54 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; + +public class RepositoryCollectionResource { + + private final CollectionResourceManagerAdapter adapter; + private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; + + @Inject + public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper) { + this.adapter = new CollectionResourceManagerAdapter<>(manager); + this.repositoryCollectionToDtoMapper = repositoryCollectionToDtoMapper; + } + + @GET + @Path("") + @Produces(VndMediaType.REPOSITORY_COLLECTION) + @TypeHint(UserDto[].class) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + 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) { + return adapter.getAll(page, pageSize, sortBy, desc, + pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); + } + + @POST + @Path("") + public Response create() { + throw new UnsupportedOperationException(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java new file mode 100644 index 0000000000..a1cf0218e4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java @@ -0,0 +1,34 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryPermissions; + +import javax.inject.Inject; + +// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. +@SuppressWarnings("squid:S3306") +public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper { + + private final ResourceLinks resourceLinks; + + @Inject + public RepositoryCollectionToDtoMapper(RepositoryToRepositoryDtoMapper repositoryToDtoMapper, ResourceLinks resourceLinks) { + super("repositories", repositoryToDtoMapper); + this.resourceLinks = resourceLinks; + } + + @Override + String createCreateLink() { + return resourceLinks.repositoryCollection().create(); + } + + @Override + String createSelfLink() { + return resourceLinks.repositoryCollection().self(); + } + + @Override + boolean isCreatePermitted() { + return RepositoryPermissions.create().isPermitted(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java index e7861a884b..a7a6365c37 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryRootResource.java @@ -12,14 +12,21 @@ public class RepositoryRootResource { static final String REPOSITORIES_PATH_V2 = "v2/repositories/"; private final Provider repositoryResource; + private final Provider repositoryCollectionResource; @Inject - public RepositoryRootResource(Provider repositoryResource) { + public RepositoryRootResource(Provider repositoryResource, Provider repositoryCollectionResource) { this.repositoryResource = repositoryResource; + this.repositoryCollectionResource = repositoryCollectionResource; } @Path("{namespace}/{name}") public RepositoryResource getRepositoryResource() { return repositoryResource.get(); } + + @Path("") + public RepositoryCollectionResource getRepositoryCollectionResource() { + return repositoryCollectionResource.get(); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index 11d1bc97ef..8f6f1eaae3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -125,6 +125,26 @@ class ResourceLinks { } } + RepositoryCollectionLinks repositoryCollection() { + return new RepositoryCollectionLinks(uriInfoStore.get()); + } + + static class RepositoryCollectionLinks { + private final LinkBuilder collectionLinkBuilder; + + RepositoryCollectionLinks(UriInfo uriInfo) { + collectionLinkBuilder = new LinkBuilder(uriInfo, RepositoryRootResource.class, RepositoryCollectionResource.class); + } + + String self() { + return collectionLinkBuilder.method("getRepositoryCollectionResource").parameters().method("getAll").parameters().href(); + } + + String create() { + return collectionLinkBuilder.method("getRepositoryCollectionResource").parameters().method("create").parameters().href(); + } + } + public TagCollectionLinks tagCollection() { return new TagCollectionLinks(uriInfoStore.get()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index fcd918152e..66e8aa0056 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -11,16 +11,20 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import sonia.scm.PageResult; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import java.net.URI; import java.net.URISyntaxException; +import static java.util.Collections.singletonList; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -49,7 +53,9 @@ public class RepositoryRootResourceTest { public void prepareEnvironment() { initMocks(this); RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null, null, null); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource)); + RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); + RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper); + RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } @@ -78,6 +84,24 @@ public class RepositoryRootResourceTest { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldGetAll() throws URISyntaxException { + PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); + when(repositoryManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); + } + + private PageResult createSingletonPageResult(Repository repository) { + return new PageResult<>(singletonList(repository), 0); + } + private Repository mockRepository(String namespace, String name) { Repository repository = new Repository(); repository.setNamespace(namespace); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 5dad23fe58..b35bc3820c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -18,6 +18,7 @@ public class ResourceLinksMock { when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(uriInfo)); + when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(uriInfo)); when(resourceLinks.tagCollection()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changesetCollection()).thenReturn(new ResourceLinks.ChangesetCollectionLinks(uriInfo)); From c2effbe9c5ff4b7d14e75d4f7710dcc84fea011b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:42:09 +0200 Subject: [PATCH 13/25] Implement update and delete for repository --- .../scm/api/v2/resources/MapperModule.java | 1 + .../RepositoryDtoToRepositoryMapper.java | 18 +++++ .../api/v2/resources/RepositoryResource.java | 21 ++++-- .../SingleResourceManagerAdapter.java | 5 ++ .../resources/RepositoryRootResourceTest.java | 66 ++++++++++++++++++- .../scm/api/v2/repository-test-update.json | 8 +++ 6 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java create mode 100644 scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 0a0468de3c..8e22755fe0 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -16,6 +16,7 @@ public class MapperModule extends AbstractModule { bind(GroupCollectionToDtoMapper.class); bind(RepositoryToRepositoryDtoMapper.class).to(Mappers.getMapper(RepositoryToRepositoryDtoMapper.class).getClass()); + bind(RepositoryDtoToRepositoryMapper.class).to(Mappers.getMapper(RepositoryDtoToRepositoryMapper.class).getClass()); bind(UriInfoStore.class).in(ServletScopes.REQUEST); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java new file mode 100644 index 0000000000..9bab9d1d3b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java @@ -0,0 +1,18 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import sonia.scm.repository.Repository; + +@Mapper +public abstract class RepositoryDtoToRepositoryMapper { + + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "lastModified", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "publicReadable", ignore = true) + @Mapping(target = "healthCheckFailures", ignore = true) + @Mapping(target = "permissions", ignore = true) + public abstract Repository map(RepositoryDto repositoryDto); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 0884664891..d1ecadf4dd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -21,6 +21,7 @@ import javax.ws.rs.core.Response; public class RepositoryResource { private final RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; private final RepositoryManager manager; private final SingleResourceManagerAdapter adapter; @@ -33,11 +34,12 @@ public class RepositoryResource { @Inject public RepositoryResource( RepositoryToRepositoryDtoMapper repositoryToDtoMapper, - RepositoryManager manager, + RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, RepositoryManager manager, Provider tagRootResource, Provider branchRootResource, Provider changesetRootResource, Provider sourceRootResource, Provider permissionRootResource) { + this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; this.adapter = new SingleResourceManagerAdapter<>(manager); @@ -65,14 +67,25 @@ public class RepositoryResource { @DELETE @Path("") + @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, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) { - throw new UnsupportedOperationException(); + return adapter.delete(() -> manager.getByNamespace(namespace, name)); } @PUT @Path("") - public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name) { - throw new UnsupportedOperationException(); + public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { + return adapter.update(() -> manager.getByNamespace(namespace, name), existing -> { + Repository repository = dtoToRepositoryMapper.map(repositoryDto); + repository.setId(existing.getId()); + return repository; + }); } @Path("tags/") diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index a56111d893..d5e7a97c51 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -60,6 +60,11 @@ class SingleResourceManagerAdapter reader) { + MODEL_OBJECT existingModelObject = reader.get(); + return delete(existingModelObject.getId()); + } + @Override protected GenericEntity> createGenericEntity(Collection modelObjects) { throw new UnsupportedOperationException(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 66e8aa0056..a5367971b4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import com.google.common.io.Resources; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; @@ -14,17 +15,23 @@ import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; +import sonia.scm.web.VndMediaType; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import static java.util.Collections.singletonList; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -48,11 +55,13 @@ public class RepositoryRootResourceTest { @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; + @InjectMocks + private RepositoryDtoToRepositoryMapperImpl dtoToRepositoryMapper; @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, repositoryManager, null, null, null, null, null); + RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); @@ -98,6 +107,57 @@ public class RepositoryRootResourceTest { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException { + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NOT_FOUND, response.getStatus()); + } + + @Test + public void shouldHandleUpdateForExistingRepository() throws Exception { + mockRepository("space", "repo"); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NO_CONTENT, response.getStatus()); + verify(repositoryManager).modify(anyObject()); + } + + @Test + public void shouldHandleDeleteForExistingRepository() throws Exception { + mockRepository("space", "repo"); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_NO_CONTENT, response.getStatus()); + verify(repositoryManager).delete(anyObject()); + } + private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } @@ -106,8 +166,10 @@ public class RepositoryRootResourceTest { Repository repository = new Repository(); repository.setNamespace(namespace); repository.setName(name); - repository.setId("id"); + String id = namespace + "-" + name; + repository.setId(id); when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository); + when(repositoryManager.get(id)).thenReturn(repository); return repository; } } diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json new file mode 100644 index 0000000000..660fa256bf --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/repository-test-update.json @@ -0,0 +1,8 @@ +{ + "contact": "none@example.com", + "description": "Test repository", + "namespace": "space", + "name": "repo", + "archived": false, + "type": "git" +} From 92bd8696bedf9892cad0270b28afc75a9a0b94b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:45:16 +0200 Subject: [PATCH 14/25] Add docs --- .../scm/api/v2/resources/RepositoryResource.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index d1ecadf4dd..f7fd3dab37 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -10,6 +10,7 @@ import sonia.scm.web.VndMediaType; import javax.inject.Inject; import javax.inject.Provider; +import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PUT; @@ -80,6 +81,16 @@ public class RepositoryResource { @PUT @Path("") + @Consumes(VndMediaType.REPOSITORY) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body, e.g. illegal change of namespace or name"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 404, condition = "not found, no repository with the specified namespace and name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update(() -> manager.getByNamespace(namespace, name), existing -> { Repository repository = dtoToRepositoryMapper.map(repositoryDto); From e993cebc0de20afffcaf1edc5ee8fda625125b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:52:19 +0200 Subject: [PATCH 15/25] Implement create repository --- .../RepositoryCollectionResource.java | 26 ++++++++++++++++--- .../resources/RepositoryRootResourceTest.java | 24 ++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index e9c1f9e34e..99a20b25d8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -1,6 +1,8 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.ResponseHeader; +import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.repository.Repository; @@ -9,6 +11,7 @@ import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.Consumes; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -16,16 +19,21 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.io.IOException; public class RepositoryCollectionResource { private final CollectionResourceManagerAdapter adapter; private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; + private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; + private final ResourceLinks resourceLinks; @Inject - public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper) { + public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper, RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, ResourceLinks resourceLinks) { this.adapter = new CollectionResourceManagerAdapter<>(manager); this.repositoryCollectionToDtoMapper = repositoryCollectionToDtoMapper; + this.dtoToRepositoryMapper = dtoToRepositoryMapper; + this.resourceLinks = resourceLinks; } @GET @@ -48,7 +56,19 @@ public class RepositoryCollectionResource { @POST @Path("") - public Response create() { - throw new UnsupportedOperationException(); + @Consumes(VndMediaType.REPOSITORY) + @StatusCodes({ + @ResponseCode(code = 201, condition = "create success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository\" privilege"), + @ResponseCode(code = 409, condition = "conflict, a repository with this name already exists"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) + public Response create(RepositoryDto repositoryDto) throws IOException, RepositoryException { + return adapter.create(repositoryDto, + () -> dtoToRepositoryMapper.map(repositoryDto), + user -> resourceLinks.user().self(user.getName())); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index a5367971b4..e150c06d23 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -14,9 +14,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryException; import sonia.scm.repository.RepositoryManager; import sonia.scm.web.VndMediaType; +import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -63,7 +65,7 @@ public class RepositoryRootResourceTest { initMocks(this); RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null); RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); - RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper); + RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); dispatcher.getRegistry().addSingletonResource(repositoryRootResource); } @@ -146,9 +148,6 @@ public class RepositoryRootResourceTest { public void shouldHandleDeleteForExistingRepository() throws Exception { mockRepository("space", "repo"); - URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); - byte[] repository = Resources.toByteArray(url); - MockHttpRequest request = MockHttpRequest.delete("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); MockHttpResponse response = new MockHttpResponse(); @@ -158,6 +157,23 @@ public class RepositoryRootResourceTest { verify(repositoryManager).delete(anyObject()); } + @Test + public void shouldCreateNewRepository() throws URISyntaxException, IOException, RepositoryException { + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repositoryJson = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2) + .contentType(VndMediaType.REPOSITORY) + .content(repositoryJson); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); + verify(repositoryManager).create(any(Repository.class)); + } + private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } From 4bdcb0cae87f33aa306b8faf3309e1b10efc0696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 10:58:26 +0200 Subject: [PATCH 16/25] Correct uri for create response --- .../scm/api/v2/resources/RepositoryCollectionResource.java | 2 +- .../sonia/scm/api/v2/resources/RepositoryRootResourceTest.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 99a20b25d8..a2f303ae8d 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -69,6 +69,6 @@ public class RepositoryCollectionResource { public Response create(RepositoryDto repositoryDto) throws IOException, RepositoryException { return adapter.create(repositoryDto, () -> dtoToRepositoryMapper.map(repositoryDto), - user -> resourceLinks.user().self(user.getName())); + repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName())); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index e150c06d23..c51abb9973 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -171,6 +171,7 @@ public class RepositoryRootResourceTest { dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); + assertEquals("/v2/repositories/space/repo", response.getOutputHeaders().get("Location").get(0).toString()); verify(repositoryManager).create(any(Repository.class)); } From bbce9b7ca2a4000ee2b9a556fd3f4db68f38ef98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 12:19:31 +0200 Subject: [PATCH 17/25] Use correct namespace of created repository --- scm-core/src/main/java/sonia/scm/HandlerBase.java | 8 ++------ .../src/main/java/sonia/scm/ManagerDecorator.java | 5 ++--- .../repository/AbstractSimpleRepositoryHandler.java | 13 ++++++------- scm-core/src/test/java/sonia/scm/ManagerTest.java | 2 +- .../resources/CollectionResourceManagerAdapter.java | 4 ++-- .../java/sonia/scm/group/DefaultGroupManager.java | 10 ++++++++-- .../scm/repository/DefaultRepositoryManager.java | 7 ++++--- .../java/sonia/scm/user/DefaultUserManager.java | 3 ++- .../scm/api/v2/resources/GroupRootResourceTest.java | 2 +- .../v2/resources/RepositoryRootResourceTest.java | 10 ++++++++-- .../scm/api/v2/resources/UserRootResourceTest.java | 2 +- 11 files changed, 37 insertions(+), 29 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/HandlerBase.java b/scm-core/src/main/java/sonia/scm/HandlerBase.java index 0baea8d929..ba7c26ea09 100644 --- a/scm-core/src/main/java/sonia/scm/HandlerBase.java +++ b/scm-core/src/main/java/sonia/scm/HandlerBase.java @@ -53,13 +53,9 @@ public interface HandlerBase /** * Persists a new object. * - * - * @param object to store - * - * @throws E - * @throws IOException + * @return The persisted object. */ - public void create(T object) throws E, IOException; + public T create(T object) throws E, IOException; /** * Removes a persistent object. diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index af0215202c..4a8227574a 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -35,7 +35,6 @@ package sonia.scm; //~--- JDK imports ------------------------------------------------------------ import java.io.IOException; - import java.util.Collection; import java.util.Comparator; @@ -78,9 +77,9 @@ public class ManagerDecorator * {@inheritDoc} */ @Override - public void create(T object) throws E, IOException + public T create(T object) throws E, IOException { - decorated.create(object); + return decorated.create(object); } /** diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index 670cab93e1..3e71bbb368 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -38,23 +38,20 @@ package sonia.scm.repository; import com.google.common.base.Charsets; import com.google.common.base.Throwables; import com.google.common.io.Resources; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.ConfigurationException; import sonia.scm.io.CommandResult; import sonia.scm.io.ExtendedCommand; import sonia.scm.io.FileSystem; +import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.IOUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; import java.io.IOException; - import java.net.URL; -import sonia.scm.store.ConfigurationStoreFactory; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -108,7 +105,7 @@ public abstract class AbstractSimpleRepositoryHandler invocation.getArguments()[0]); doNothing().when(groupManager).modify(groupCaptor.capture()); Group group = createDummyGroup(); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index c51abb9973..c4ebf88c3d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -158,7 +158,13 @@ public class RepositoryRootResourceTest { } @Test - public void shouldCreateNewRepository() throws URISyntaxException, IOException, RepositoryException { + public void shouldCreateNewRepositoryInCorrectNamespace() throws URISyntaxException, IOException, RepositoryException { + when(repositoryManager.create(any())).thenAnswer(invocation -> { + Repository repository = (Repository) invocation.getArguments()[0]; + repository.setNamespace("otherspace"); + return repository; + }); + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); byte[] repositoryJson = Resources.toByteArray(url); @@ -171,7 +177,7 @@ public class RepositoryRootResourceTest { dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); - assertEquals("/v2/repositories/space/repo", response.getOutputHeaders().get("Location").get(0).toString()); + assertEquals("/v2/repositories/otherspace/repo", response.getOutputHeaders().get("Location").get(0).toString()); verify(repositoryManager).create(any(Repository.class)); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 2cf2ee0566..7fb1ae5cdc 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -67,7 +67,7 @@ public class UserRootResourceTest { public void prepareEnvironment() throws IOException, UserException { initMocks(this); User dummyUser = createDummyUser("Neo"); - doNothing().when(userManager).create(userCaptor.capture()); + when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); From cad793322c8ce441e8e757aa427df77f14c699f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 12:40:19 +0200 Subject: [PATCH 18/25] Add test for MapperModule --- .../api/v2/resources/MapperModuleTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java new file mode 100644 index 0000000000..a792d40e76 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MapperModuleTest.java @@ -0,0 +1,33 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.binder.AnnotatedBindingBuilder; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MapperModuleTest { + + @Test + public void shouldBindToClassesWithDefaultConstructorOnly() { + AnnotatedBindingBuilder binding = mock(AnnotatedBindingBuilder.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); + when(binding.to(captor.capture())).thenReturn(null); + new MapperModule() { + @Override + protected AnnotatedBindingBuilder bind(Class clazz) { + return binding; + } + }.configure(); + captor.getAllValues().forEach(this::verifyClassCanBeInstantiated); + } + + private T verifyClassCanBeInstantiated(Class c) { + try { + return c.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 0bbc58b9786c10be36d6f6a5036aaa785fe3550f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 13:07:43 +0200 Subject: [PATCH 19/25] Make setting of id explicit for modified repositories --- .../api/v2/resources/RepositoryCollectionResource.java | 2 +- .../v2/resources/RepositoryDtoToRepositoryMapper.java | 9 ++++++++- .../sonia/scm/api/v2/resources/RepositoryResource.java | 9 ++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index a2f303ae8d..11078090af 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -68,7 +68,7 @@ public class RepositoryCollectionResource { @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) public Response create(RepositoryDto repositoryDto) throws IOException, RepositoryException { return adapter.create(repositoryDto, - () -> dtoToRepositoryMapper.map(repositoryDto), + () -> dtoToRepositoryMapper.map(repositoryDto, null), repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName())); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java index 9bab9d1d3b..2c02bb8180 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDtoToRepositoryMapper.java @@ -1,7 +1,10 @@ package sonia.scm.api.v2.resources; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import sonia.scm.repository.Repository; @Mapper @@ -13,6 +16,10 @@ public abstract class RepositoryDtoToRepositoryMapper { @Mapping(target = "publicReadable", ignore = true) @Mapping(target = "healthCheckFailures", ignore = true) @Mapping(target = "permissions", ignore = true) - public abstract Repository map(RepositoryDto repositoryDto); + public abstract Repository map(RepositoryDto repositoryDto, @Context String id); + @AfterMapping + void updateId(@MappingTarget Repository repository, @Context String id) { + repository.setId(id); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index f7fd3dab37..e6d8b80fd3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -92,11 +92,10 @@ public class RepositoryResource { }) @TypeHint(TypeHint.NO_CONTENT.class) public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { - return adapter.update(() -> manager.getByNamespace(namespace, name), existing -> { - Repository repository = dtoToRepositoryMapper.map(repositoryDto); - repository.setId(existing.getId()); - return repository; - }); + return adapter.update( + () -> manager.getByNamespace(namespace, name), + existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()) + ); } @Path("tags/") From b6c618b0b0443c9b97cdb02622a7cd97e9d90930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 13:19:49 +0200 Subject: [PATCH 20/25] Verify that key values are not changed on update --- .../resources/IdResourceManagerAdapter.java | 6 +++++- .../api/v2/resources/RepositoryResource.java | 3 ++- .../SingleResourceManagerAdapter.java | 5 +++-- .../resources/RepositoryRootResourceTest.java | 21 +++++++++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index c7bcc3b6b1..db5133dbf3 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -34,7 +34,11 @@ class IdResourceManagerAdapter applyChanges) { - return singleAdapter.update(() -> manager.get(id), applyChanges); + return singleAdapter.update( + () -> manager.get(id), + applyChanges, + changed -> changed.getId().equals(id) + ); } public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function, CollectionDto> mapToDto) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index e6d8b80fd3..ee251051b1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -94,7 +94,8 @@ public class RepositoryResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update( () -> manager.getByNamespace(namespace, name), - existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()) + existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()), + changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace) ); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index d5e7a97c51..547fd3654b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -9,6 +9,7 @@ import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; import java.util.Collection; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; @@ -48,13 +49,13 @@ class SingleResourceManagerAdapter reader, Function applyChanges) { + public Response update(Supplier reader, Function applyChanges, Predicate hasSameKey) { MODEL_OBJECT existingModelObject = reader.get(); if (existingModelObject == null) { return Response.status(Response.Status.NOT_FOUND).build(); } MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); - if (!getId(existingModelObject).equals(getId(changedModelObject))) { + if (!hasSameKey.test(changedModelObject)) { return Response.status(BAD_REQUEST).entity("illegal change of id").build(); } return update(getId(existingModelObject), changedModelObject); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index c4ebf88c3d..c0fb0c0234 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -25,6 +25,7 @@ import java.net.URISyntaxException; import java.net.URL; import static java.util.Collections.singletonList; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_OK; @@ -33,6 +34,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -144,6 +146,25 @@ public class RepositoryRootResourceTest { verify(repositoryManager).modify(anyObject()); } + @Test + public void shouldHandleUpdateForExistingRepositoryForChangedNamespace() throws Exception { + mockRepository("wrong", "repo"); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repository = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "wrong/repo") + .contentType(VndMediaType.REPOSITORY) + .content(repository); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_BAD_REQUEST, response.getStatus()); + verify(repositoryManager, never()).modify(anyObject()); + } + @Test public void shouldHandleDeleteForExistingRepository() throws Exception { mockRepository("space", "repo"); From 88029d23fe26953976cf232df37e87d1744a87f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Thu, 5 Jul 2018 17:23:19 +0200 Subject: [PATCH 21/25] Small cleanup --- .../v2/resources/IdResourceManagerAdapter.java | 18 ++++++++++++++---- .../api/v2/resources/RepositoryResource.java | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index 64c5a063f5..be634a5fac 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -8,10 +8,12 @@ import sonia.scm.PageResult; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; /** - * Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter}. + * Facade for {@link SingleResourceManagerAdapter} and {@link CollectionResourceManagerAdapter} + * for model objects handled by a single id. */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class IdResourceManagerAdapter mapToDto) { - return singleAdapter.get(() -> manager.get(id), mapToDto); + return singleAdapter.get(loadBy(id), mapToDto); } public Response update(String id, Function applyChanges) { return singleAdapter.update( - () -> manager.get(id), + loadBy(id), applyChanges, - changed -> changed.getId().equals(id) + idStaysTheSame(id) ); } @@ -52,4 +54,12 @@ class IdResourceManagerAdapter loadBy(String id) { + return () -> manager.get(id); + } + + private Predicate idStaysTheSame(String id) { + return changed -> changed.getId().equals(id); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 7fd3600c65..6e6d837830 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -18,6 +18,8 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.util.function.Predicate; +import java.util.function.Supplier; public class RepositoryResource { @@ -63,7 +65,7 @@ public class RepositoryResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) { - return adapter.get(() -> manager.getByNamespace(namespace, name), repositoryToDtoMapper::map); + return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map); } @DELETE @@ -76,7 +78,7 @@ public class RepositoryResource { }) @TypeHint(TypeHint.NO_CONTENT.class) public Response delete(@PathParam("namespace") String namespace, @PathParam("name") String name) { - return adapter.delete(() -> manager.getByNamespace(namespace, name)); + return adapter.delete(loadBy(namespace, name)); } @PUT @@ -93,9 +95,9 @@ public class RepositoryResource { @TypeHint(TypeHint.NO_CONTENT.class) public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, RepositoryDto repositoryDto) { return adapter.update( - () -> manager.getByNamespace(namespace, name), + loadBy(namespace, name), existing -> dtoToRepositoryMapper.map(repositoryDto, existing.getId()), - changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace) + nameAndNamespaceStaysTheSame(namespace, name) ); } @@ -123,4 +125,12 @@ public class RepositoryResource { public PermissionRootResource permissions() { return permissionRootResource.get(); } + + private Supplier loadBy(String namespace, String name) { + return () -> manager.getByNamespace(namespace, name); + } + + private Predicate nameAndNamespaceStaysTheSame(String namespace, String name) { + return changed -> changed.getName().equals(name) && changed.getNamespace().equals(namespace); + } } From e1963d45dd196300abb6cb4d0337ad9e02d94d34 Mon Sep 17 00:00:00 2001 From: Johannes Schnatterer Date: Wed, 11 Jul 2018 12:03:04 +0200 Subject: [PATCH 22/25] Some polishing during review --- .../repository/AbstractSimpleRepositoryHandler.java | 1 + .../resources/CollectionResourceManagerAdapter.java | 5 +++-- .../sonia/scm/api/v2/resources/PermissionDto.java | 11 ----------- .../v2/resources/RepositoryCollectionResource.java | 5 ++--- .../v2/resources/SingleResourceManagerAdapter.java | 4 +++- .../api/v2/resources/GroupToGroupDtoMapperTest.java | 1 + .../RepositoryToRepositoryDtoMapperTest.java | 7 ++++--- .../scm/api/v2/resources/UserRootResourceTest.java | 2 +- .../scm/api/v2/resources/UserToUserDtoMapperTest.java | 1 + .../scm/repository/DefaultRepositoryManagerTest.java | 4 ---- 10 files changed, 16 insertions(+), 25 deletions(-) delete mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java diff --git a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java index fb044a325c..b49d4355b9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java +++ b/scm-core/src/main/java/sonia/scm/repository/AbstractSimpleRepositoryHandler.java @@ -143,6 +143,7 @@ public abstract class AbstractSimpleRepositoryHandler The type of the model object, eg. {@link sonia.scm.user.User}. * @param The corresponding transport object, eg. {@link UserDto}. * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. + * + * @see SingleResourceManagerAdapter */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class CollectionResourceManagerAdapter dtoToRepositoryMapper.map(repositoryDto, null), repository -> resourceLinks.repository().self(repository.getNamespace(), repository.getName())); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 96c9d05d2c..6cd12c9dd6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -15,13 +15,15 @@ import java.util.function.Supplier; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; /** - * Adapter from resource http endpoints to managers. + * Adapter from resource http endpoints to managers, for Single resources (e.g. {@code /user/name}). * * Provides common CRUD operations and DTO to Model Object mapping to keep Resources more DRY. * * @param The type of the model object, eg. {@link sonia.scm.user.User}. * @param The corresponding transport object, eg. {@link UserDto}. * @param The exception type for the model object, eg. {@link sonia.scm.user.UserException}. + * + * @see CollectionResourceManagerAdapter */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class SingleResourceManagerAdapter userCaptor = ArgumentCaptor.forClass(User.class); @Before - public void prepareEnvironment() throws IOException, UserException { + public void prepareEnvironment() throws UserException { initMocks(this); User dummyUser = createDummyUser("Neo"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index 0ac980df45..330dd2a89e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -23,6 +23,7 @@ import static org.mockito.MockitoAnnotations.initMocks; public class UserToUserDtoMapperTest { private final URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @InjectMocks diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index 0371b4974b..d057a97a84 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -93,10 +93,6 @@ import static org.mockito.Mockito.when; ) public class DefaultRepositoryManagerTest extends ManagerTestBase { - { - ThreadContext.unbindSecurityManager(); - } - @Rule public ShiroRule shiro = new ShiroRule(); From ccf2708520614702aad8b285117a6ef820f53150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 13:03:46 +0200 Subject: [PATCH 23/25] Add javadoc for repository endpoint --- .../RepositoryCollectionResource.java | 23 +++++++++++++++- .../api/v2/resources/RepositoryResource.java | 27 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 1a90e3cf8d..590cd1324b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -23,6 +23,8 @@ import java.io.IOException; public class RepositoryCollectionResource { + private static final int DEFAULT_PAGE_SIZE = 10; + private final CollectionResourceManagerAdapter adapter; private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; @@ -36,6 +38,16 @@ public class RepositoryCollectionResource { this.resourceLinks = resourceLinks; } + /** + * Returns all repositories for a given page number with a given page size (default page size is {@value DEFAULT_PAGE_SIZE}). + * + * Note: This method requires "repository" privilege. + * + * @param page the number of the requested page + * @param pageSize the page size (default page size is {@value DEFAULT_PAGE_SIZE}) + * @param sortBy sort parameter (if empty - undefined sorting) + * @param desc sort direction desc or asc + */ @GET @Path("") @Produces(VndMediaType.REPOSITORY_COLLECTION) @@ -47,13 +59,22 @@ public class RepositoryCollectionResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("10") @QueryParam("pageSize") int pageSize, + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, @DefaultValue("false") @QueryParam("desc") boolean desc) { return adapter.getAll(page, pageSize, sortBy, desc, pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); } + /** + * Creates a new repository. + * + * Note: This method requires "repository" privilege. The namespace of the given repository will + * be ignored and set by the configured namespace strategy. + * + * @param repositoryDto The repository to be created. + * @return A response with the link to the new repository (if created successfully). + */ @POST @Path("") @Consumes(VndMediaType.REPOSITORY) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 6e6d837830..1ff366cc0a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -53,6 +53,15 @@ public class RepositoryResource { this.permissionRootResource = permissionRootResource; } + /** + * Returns a repository. + * + * Note: This method requires "repository" privilege. + * + * @param namespace the namespace of the repository + * @param name the name of the repository + * + */ @GET @Path("") @Produces(VndMediaType.REPOSITORY) @@ -68,6 +77,15 @@ public class RepositoryResource { return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map); } + /** + * Deletes a repository. + * + * Note: This method requires "repository" privilege. + * + * @param namespace the namespace of the repository to delete + * @param name the name of the repository to delete + * + */ @DELETE @Path("") @StatusCodes({ @@ -81,6 +99,15 @@ public class RepositoryResource { return adapter.delete(loadBy(namespace, name)); } + /** + * Modifies the given repository. + * + * Note: This method requires "repository" privilege. + * + * @param namespace the namespace of the repository to be modified + * @param name the name of the repository to be modified + * @param repositoryDto repository object to modify + */ @PUT @Path("") @Consumes(VndMediaType.REPOSITORY) From 617b98c6f30a03be3adf0acf094e40b45c377f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 13:42:09 +0200 Subject: [PATCH 24/25] Use optionals for get by name and namespace --- .../scm/repository/RepositoryManager.java | 6 ++-- .../resources/IdResourceManagerAdapter.java | 5 +-- .../api/v2/resources/RepositoryResource.java | 3 +- .../SingleResourceManagerAdapter.java | 32 ++++++++++--------- .../resources/RepositoryRootResourceTest.java | 7 +++- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index ca1d5ab743..0edd21f4c2 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -41,6 +41,7 @@ import sonia.scm.TypeManager; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Collection; +import java.util.Optional; //~--- JDK imports ------------------------------------------------------------ @@ -148,11 +149,10 @@ public interface RepositoryManager @Override public RepositoryHandler getHandler(String type); - default Repository getByNamespace(String namespace, String name) { + default Optional getByNamespace(String namespace, String name) { return getAll() .stream() .filter(r -> r.getName().equals(name) && r.getNamespace().equals(namespace)) - .findFirst() - .orElse(null); + .findFirst(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index be634a5fac..ded4bff309 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -7,6 +7,7 @@ import sonia.scm.PageResult; import javax.ws.rs.core.Response; import java.io.IOException; +import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -55,8 +56,8 @@ class IdResourceManagerAdapter loadBy(String id) { - return () -> manager.get(id); + private Supplier> loadBy(String id) { + return () -> Optional.ofNullable(manager.get(id)); } private Predicate idStaysTheSame(String id) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index 1ff366cc0a..5972508c78 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -18,6 +18,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; +import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; @@ -153,7 +154,7 @@ public class RepositoryResource { return permissionRootResource.get(); } - private Supplier loadBy(String namespace, String name) { + private Supplier> loadBy(String namespace, String name) { return () -> manager.getByNamespace(namespace, name); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 6cd12c9dd6..7f8b115dee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -8,6 +8,7 @@ import sonia.scm.api.rest.resources.AbstractManagerResource; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; import java.util.Collection; +import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -38,34 +39,35 @@ class SingleResourceManagerAdapter reader, Function mapToDto) { - MODEL_OBJECT modelObject = reader.get(); - if (modelObject == null) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - DTO dto = mapToDto.apply(modelObject); - return Response.ok(dto).build(); + Response get(Supplier> reader, Function mapToDto) { + return reader.get() + .map(mapToDto) + .map(Response::ok) + .map(Response.ResponseBuilder::build) + .orElse(Response.status(Response.Status.NOT_FOUND).build()); } /** * Update the model object for the given id according to the given function and returns a corresponding http response. * This handles all corner cases, eg. no matching object for the id or missing privileges. */ - public Response update(Supplier reader, Function applyChanges, Predicate hasSameKey) { - MODEL_OBJECT existingModelObject = reader.get(); - if (existingModelObject == null) { + public Response update(Supplier> reader, Function applyChanges, Predicate hasSameKey) { + Optional existingModelObject = reader.get(); + if (!existingModelObject.isPresent()) { return Response.status(Response.Status.NOT_FOUND).build(); } - MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject); + MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject.get()); if (!hasSameKey.test(changedModelObject)) { return Response.status(BAD_REQUEST).entity("illegal change of id").build(); } - return update(getId(existingModelObject), changedModelObject); + return update(getId(existingModelObject.get()), changedModelObject); } - public Response delete(Supplier reader) { - MODEL_OBJECT existingModelObject = reader.get(); - return delete(existingModelObject.getId()); + public Response delete(Supplier> reader) { + return reader.get() + .map(MODEL_OBJECT::getId) + .map(this::delete) + .orElse(null); } @Override diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index c0fb0c0234..a0652708ce 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -25,6 +25,8 @@ import java.net.URISyntaxException; import java.net.URL; import static java.util.Collections.singletonList; +import static java.util.Optional.empty; +import static java.util.Optional.of; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; @@ -33,6 +35,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -74,6 +77,7 @@ public class RepositoryRootResourceTest { @Test public void shouldFailForNotExistingRepository() throws URISyntaxException { + when(repositoryManager.getByNamespace(anyString(), anyString())).thenReturn(empty()); mockRepository("space", "repo"); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/other"); @@ -115,6 +119,7 @@ public class RepositoryRootResourceTest { public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException { URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); byte[] repository = Resources.toByteArray(url); + when(repositoryManager.getByNamespace(anyString(), anyString())).thenReturn(empty()); MockHttpRequest request = MockHttpRequest .put("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo") @@ -212,7 +217,7 @@ public class RepositoryRootResourceTest { repository.setName(name); String id = namespace + "-" + name; repository.setId(id); - when(repositoryManager.getByNamespace(namespace, name)).thenReturn(repository); + when(repositoryManager.getByNamespace(namespace, name)).thenReturn(of(repository)); when(repositoryManager.get(id)).thenReturn(repository); return repository; } From 667cc48e088358509c1a1b2fab942837722433ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 11 Jul 2018 13:47:41 +0200 Subject: [PATCH 25/25] Map properties of repositories --- .../sonia/scm/api/v2/resources/RepositoryDto.java | 2 ++ .../v2/resources/RepositoryRootResourceTest.java | 13 +++++++++++++ .../RepositoryToRepositoryDtoMapperTest.java | 10 ++++++++++ 3 files changed, 25 insertions(+) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index fbef4ca16a..bcc8e16ebb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -8,6 +8,7 @@ import lombok.Setter; import java.time.Instant; import java.util.List; +import java.util.Map; @Getter @Setter public class RepositoryDto extends HalRepresentation { @@ -22,6 +23,7 @@ public class RepositoryDto extends HalRepresentation { private String name; private boolean archived = false; private String type; + protected Map properties; @Override @SuppressWarnings("squid:S1185") // We want to have this method available in this package diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index a0652708ce..24a11fbf09 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -101,6 +101,19 @@ public class RepositoryRootResourceTest { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldMapProperties() throws URISyntaxException { + Repository repository = mockRepository("space", "repo"); + repository.setProperty("testKey", "testValue"); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertTrue(response.getContentAsString().contains("\"testKey\":\"testValue\"")); + } + @Test public void shouldGetAll() throws URISyntaxException { PageResult singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index fd41291d9d..ad6ff3cae7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -57,6 +57,16 @@ public class RepositoryToRepositoryDtoMapperTest { assertEquals("none@example.com", dto.getContact()); } + @Test + public void shouldMapPropertiesProperty() { + Repository repository = createTestRepository(); + repository.setProperty("testKey", "testValue"); + + RepositoryDto dto = mapper.map(repository); + + assertEquals("testValue", dto.getProperties().get("testKey")); + } + @Test @SubjectAware(username = "unpriv") public void shouldCreateLinksForUnprivilegedUser() {