diff --git a/scm-core/src/main/java/sonia/scm/GenericDAO.java b/scm-core/src/main/java/sonia/scm/GenericDAO.java index b63a96f733..a369c2f8c2 100644 --- a/scm-core/src/main/java/sonia/scm/GenericDAO.java +++ b/scm-core/src/main/java/sonia/scm/GenericDAO.java @@ -114,4 +114,13 @@ public interface GenericDAO * @return all items */ public Collection getAll(); + + /** + * Returns items containing the searched string + * + * @param searched the search character + * @param limit the max count of the result entities. if limit is <= 0 return all filtered entities + * @return searched items + */ + Collection getFiltered(String searched, int limit); } diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 2925b5b6b4..62384e41d4 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -77,6 +77,15 @@ public interface Manager */ Collection getAll(); + /** + * Returns a {@link java.util.Collection} of filtered objects + * + * @param filter the searched string + * @param limit the max count of the result entities. if limit is <= 0 return all filtered entities + * @return all object in the store + */ + Collection getFiltered(String filter, int limit); + /** * Returns all object of the store sorted by the given {@link java.util.Comparator} * diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index 7b3f03ee8c..3938121668 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -91,6 +91,11 @@ public class ManagerDecorator implements Manager { decorated.refresh(object); } + @Override + public Collection getFiltered(String filter, int limit) { + return decorated.getFiltered(filter, limit); + } + @Override public T get(String id) { diff --git a/scm-core/src/main/java/sonia/scm/ModelObject.java b/scm-core/src/main/java/sonia/scm/ModelObject.java index cca9608ceb..5c387e9ca4 100644 --- a/scm-core/src/main/java/sonia/scm/ModelObject.java +++ b/scm-core/src/main/java/sonia/scm/ModelObject.java @@ -44,7 +44,7 @@ import java.io.Serializable; */ public interface ModelObject extends TypedObject, LastModifiedAware, Cloneable, Validateable, - Serializable + Serializable, ReducedModelObject { /** diff --git a/scm-core/src/main/java/sonia/scm/ReducedModelObject.java b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java new file mode 100644 index 0000000000..b8db8c3ee0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/ReducedModelObject.java @@ -0,0 +1,15 @@ +package sonia.scm; + + +/** + * This is a reduced form of a model object. + * It can be used as search result to avoid returning the whole object properties. + * + * @author Mohamed Karray + */ +public interface ReducedModelObject { + + String getId(); + + String getDisplayName(); +} diff --git a/scm-core/src/main/java/sonia/scm/group/Group.java b/scm-core/src/main/java/sonia/scm/group/Group.java index 5e7f596c58..6ee27be2fc 100644 --- a/scm-core/src/main/java/sonia/scm/group/Group.java +++ b/scm-core/src/main/java/sonia/scm/group/Group.java @@ -55,12 +55,12 @@ import java.util.List; /** * Organizes users into a group for easier permissions management. - * + * * TODO for 2.0: Use a set instead of a list for members * * @author Sebastian Sdorra */ -@StaticPermissions(value = "group", globalPermissions = {"create", "list"}) +@StaticPermissions(value = "group", globalPermissions = {"create", "list", "autocomplete"}) @XmlRootElement(name = "groups") @XmlAccessorType(XmlAccessType.FIELD) public class Group extends BasicPropertiesAware @@ -309,6 +309,11 @@ public class Group extends BasicPropertiesAware return name; } + @Override + public String getDisplayName() { + return description; + } + /** * Returns a timestamp of the last modified date of this group. * diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java index 7397fecabe..7d3a8d6dbf 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java +++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java @@ -256,6 +256,11 @@ public class Changeset extends BasicPropertiesAware implements ModelObject { return id; } + @Override + public String getDisplayName() { + return id; + } + @Override public void setLastModified(Long timestamp) { throw new UnsupportedOperationException("changesets are immutable"); 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 cad36f2d88..7f2aab6865 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -60,11 +60,12 @@ import java.util.List; */ @StaticPermissions( value = "repository", - permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"} + permissions = {"read", "modify", "delete", "healthCheck", "pull", "push", "permissionRead", "permissionWrite"}, + globalPermissions = {"create", "autocomplete"} ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") -public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject { +public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ private static final long serialVersionUID = 3486560714961909711L; @@ -183,6 +184,11 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return id; } + @Override + public String getDisplayName() { + return getNamespaceAndName().toString(); + } + @Override public Long getLastModified() { return lastModified; diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index 0d909bec8d..2a35f920a8 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -55,7 +55,7 @@ import java.security.Principal; * * @author Sebastian Sdorra */ -@StaticPermissions(value = "user", globalPermissions = {"create", "list"}) +@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"}) @XmlRootElement(name = "users") @XmlAccessorType(XmlAccessType.FIELD) public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject 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 f0711cd1e4..b6f2210d80 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -18,6 +18,7 @@ public class VndMediaType { public static final String INDEX = PREFIX + "index" + SUFFIX; public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; + public static final String AUTOCOMPLETE = PREFIX + "autocomplete" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; diff --git a/scm-core/src/test/java/sonia/scm/ManagerTest.java b/scm-core/src/test/java/sonia/scm/ManagerTest.java index 06c8eb3ea6..309a68e8ca 100644 --- a/scm-core/src/test/java/sonia/scm/ManagerTest.java +++ b/scm-core/src/test/java/sonia/scm/ManagerTest.java @@ -78,6 +78,11 @@ public class ManagerTest { return IntStream.range(0, givenItemCount).boxed().collect(toList()); } + @Override + public Collection getFiltered(String filter, int limit) { + return null; + } + @Override public Collection getAll(Comparator comparator) { return getAll(); } diff --git a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index 6b74fce7ca..f0907538cf 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java @@ -41,11 +41,13 @@ import org.slf4j.LoggerFactory; import sonia.scm.GenericDAO; import sonia.scm.ModelObject; import sonia.scm.group.xml.XmlGroupDAO; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.store.ConfigurationStore; +import sonia.scm.util.AssertUtil; import java.util.Collection; -import sonia.scm.store.ConfigurationStore; +import java.util.stream.Collectors; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -234,6 +236,16 @@ public abstract class AbstractXmlDAO getFiltered(String searched, int limit) { + int size = db.values().size(); + AssertUtil.assertIsNotEmpty(searched); + return ImmutableList.copyOf(db.values().stream() + .filter(item -> item.getId().contains(searched) || (item.getDisplayName() != null && item.getDisplayName().contains(searched))) + .limit(limit <= 0 ? size : limit) + .collect(Collectors.toList())); + } + /** * Method description * diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java new file mode 100644 index 0000000000..1f3f7177bd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamException.java @@ -0,0 +1,11 @@ +package sonia.scm.api.v2.resources; + +public class AutoCompleteBadParamException extends Exception { + + public static final String PARAMETER_IS_REQUIRED = "The parameter is required."; + public static final String INVALID_PARAMETER_LENGTH = "Invalid parameter length."; + + public AutoCompleteBadParamException(String message) { + super(message); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java new file mode 100644 index 0000000000..d2b2eeaed3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteBadParamExceptionMapper.java @@ -0,0 +1,16 @@ +package sonia.scm.api.v2.resources; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class AutoCompleteBadParamExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(AutoCompleteBadParamException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(exception.getMessage()) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java new file mode 100644 index 0000000000..5dfa78a762 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java @@ -0,0 +1,113 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import org.apache.commons.lang.StringUtils; +import sonia.scm.group.GroupManager; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.user.UserManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.validation.Valid; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.util.stream.Collectors; + +import static sonia.scm.api.v2.resources.AutoCompleteBadParamException.INVALID_PARAMETER_LENGTH; +import static sonia.scm.api.v2.resources.AutoCompleteBadParamException.PARAMETER_IS_REQUIRED; + +@Path(AutoCompleteResource.PATH) +public class AutoCompleteResource { + public static final String PATH = "v2/autocomplete/"; + public static final String DEFAULT_LIMIT = "5"; + public static final int MIN_SEARCHED_CHARS = 1; + + private ReducedObjectModelToDtoMapper mapper; + + private UserManager userManager; + private GroupManager groupManager; + private RepositoryManager repositoryManager; + + @Inject + public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserManager userManager, GroupManager groupManager, RepositoryManager repositoryManager) { + this.mapper = mapper; + this.userManager = userManager; + this.groupManager = groupManager; + this.repositoryManager = repositoryManager; + } + + @GET + @Path("user") + @Produces(VndMediaType.AUTOCOMPLETE) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user:autocomplete\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response searchUser(@QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { + validateParams(filter); + return Response.ok(userManager.getFiltered(filter, limit) + .stream() + .map(mapper::map) + .collect(Collectors.toList())) + .build(); + } + + @GET + @Path("group") + @Produces(VndMediaType.AUTOCOMPLETE) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group:autocomplete\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response searchGroup(@Valid @QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { + validateParams(filter); + return Response.ok(groupManager.getFiltered(filter, limit) + .stream() + .map(mapper::map) + .collect(Collectors.toList())) + .build(); + } + + @GET + @Path("repository") + @Produces(VndMediaType.AUTOCOMPLETE) + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 400, condition = "if the searched string contains less than 2 characters"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"repository:autocomplete\" privilege"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response searchRepo(@Valid @QueryParam("filter") String filter, + @DefaultValue(DEFAULT_LIMIT) @QueryParam("limit") Integer limit) throws AutoCompleteBadParamException { + validateParams(filter); + return Response.ok(repositoryManager.getFiltered(filter, limit) + .stream() + .map(mapper::map) + .collect(Collectors.toList())) + .build(); + } + + void validateParams(String filter) throws AutoCompleteBadParamException { + if (StringUtils.isBlank(filter)) { + throw new AutoCompleteBadParamException(PARAMETER_IS_REQUIRED); + } + if (filter.length() <= MIN_SEARCHED_CHARS) { + throw new AutoCompleteBadParamException(INVALID_PARAMETER_LENGTH); + } + } + +} 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 03b5728627..6497cb9315 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 @@ -37,6 +37,8 @@ public class MapperModule extends AbstractModule { bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass()); + bind(ReducedObjectModelToDtoMapper.class).to(Mappers.getMapper(ReducedObjectModelToDtoMapper.class).getClass()); + // no mapstruct required bind(UIPluginDtoMapper.class); bind(UIPluginDtoCollectionMapper.class); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelDto.java new file mode 100644 index 0000000000..821af03995 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelDto.java @@ -0,0 +1,16 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class ReducedObjectModelDto extends HalRepresentation { + + private String id; + + private String displayName; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelToDtoMapper.java new file mode 100644 index 0000000000..e188de7d65 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ReducedObjectModelToDtoMapper.java @@ -0,0 +1,13 @@ +package sonia.scm.api.v2.resources; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import sonia.scm.ReducedModelObject; + +@Mapper +public abstract class ReducedObjectModelToDtoMapper { + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract ReducedObjectModelDto map(ReducedModelObject modelObject); + +} diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index c3dcb6db8c..823ba5a580 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -242,6 +242,11 @@ public class DefaultGroupManager extends AbstractGroupManager return group; } + @Override + public Collection getFiltered(String filter, int limit) { + GroupPermissions.autocomplete().check(); + return groupDAO.getFiltered(filter, limit); + } /** * Method description * diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 8cb325b818..defd8f2068 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -243,6 +243,12 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { return repository; } + @Override + public Collection getFiltered(String filter, int limit) { + RepositoryPermissions.autocomplete().check(); + return repositoryDAO.getFiltered(filter, limit); + } + @Override public Collection getAll(Comparator comparator) { List repositories = Lists.newArrayList(); diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index bf46eb2a6f..f2e50029bd 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -52,9 +52,11 @@ import org.slf4j.LoggerFactory; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.group.GroupNames; +import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryPermissions; import sonia.scm.user.User; import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; @@ -256,6 +258,9 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector collectGlobalPermissions(builder, user, groups); collectRepositoryPermissions(builder, user, groups); builder.add(canReadOwnUser(user)); + builder.add(getUserAutocompletePermission()); + builder.add(getGroupAutocompletePermission()); + builder.add(getRepoAutocompletePermission()); permissions = builder.build(); } @@ -264,6 +269,18 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector return info; } + private String getRepoAutocompletePermission() { + return RepositoryPermissions.autocomplete().asShiroString(); + } + + private String getGroupAutocompletePermission() { + return GroupPermissions.autocomplete().asShiroString(); + } + + private String getUserAutocompletePermission() { + return UserPermissions.autocomplete().asShiroString(); + } + private String canReadOwnUser(User user) { return UserPermissions.read(user.getName()).asShiroString(); } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 876b0f094c..7757e0382a 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -300,6 +300,12 @@ public class DefaultUserManager extends AbstractUserManager return getAll(null); } + @Override + public Collection getFiltered(String filter, int limit) { + UserPermissions.autocomplete().check(); + return userDAO.getFiltered(filter, limit); + } + /** * Method description * diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java index 41bcac3c6a..c2d7eef852 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java @@ -105,6 +105,11 @@ public class AbstractManagerResourceTest { return id; } + @Override + public String getDisplayName() { + return id; + } + @Override public void setLastModified(Long timestamp) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java new file mode 100644 index 0000000000..a6fd5f1f8a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java @@ -0,0 +1,368 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.SCMContextProvider; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.group.DefaultGroupManager; +import sonia.scm.group.Group; +import sonia.scm.group.GroupManager; +import sonia.scm.group.xml.XmlGroupDAO; +import sonia.scm.repository.DefaultRepositoryManager; +import sonia.scm.repository.NamespaceStrategy; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.xml.XmlRepositoryDAO; +import sonia.scm.security.KeyGenerator; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.user.DefaultUserManager; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; +import sonia.scm.user.xml.XmlUserDAO; +import sonia.scm.web.VndMediaType; +import sonia.scm.xml.XmlDatabase; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; +import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class AutoCompleteResourceTest { + + public static final String URL = "/" + AutoCompleteResource.PATH; + private final Integer defaultLimit = Integer.valueOf(AutoCompleteResource.DEFAULT_LIMIT); + private Dispatcher dispatcher; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + private XmlUserDAO userDaoMock; + + private XmlGroupDAO groupDaoMock; + + private XmlRepositoryDAO repoDaoMock; + private XmlDatabase xmlDB; + private ObjectMapper jsonObjectMapper = new ObjectMapper(); + + + @Before + public void prepareEnvironment() { + initMocks(this); + ConfigurationStoreFactory storeFactory = mock(ConfigurationStoreFactory.class); + ConfigurationStore storeConfig = mock(ConfigurationStore.class); + xmlDB = mock(XmlDatabase.class); + when(storeConfig.get()).thenReturn(xmlDB); + when(storeFactory.getStore(any(), any())).thenReturn(storeConfig); + XmlUserDAO userDao = new XmlUserDAO(storeFactory); + userDaoMock = spy(userDao); + XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory); + groupDaoMock = spy(groupDAO); + XmlRepositoryDAO repoDao = new XmlRepositoryDAO(storeFactory); + repoDaoMock = spy(repoDao); + ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl(); + UserManager userManager = new DefaultUserManager(userDaoMock); + GroupManager groupManager = new DefaultGroupManager(groupDaoMock); + RepositoryManager repositoryManager = new DefaultRepositoryManager(mock(ScmConfiguration.class), mock(SCMContextProvider.class), mock(KeyGenerator.class), repoDaoMock, new HashSet<>(), mock(NamespaceStrategy.class)); + AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager, repositoryManager); + dispatcher = createDispatcher(autoCompleteResource); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGet400OnFailedParameterForUserSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldGet400IfParameterLengthLessThan2CharsForUserSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=a") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldSearchUsers() throws Exception { + ArrayList users = Lists.newArrayList(createMockUser("YuCantFindMe", "ha ha"), createMockUser("user1", "User 1"), createMockUser("user2", "User 2")); + String searched = "user"; + when(xmlDB.values()).thenReturn(users); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=" + searched) + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + + assertResultSize(response, 2); + assertTrue(response.getContentAsString().contains("\"id\":\"user1\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"User 1\"")); + assertTrue(response.getContentAsString().contains("\"id\":\"user2\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"User 2\"")); + } + + @Test + public void shouldSearchUsersWithLimitLength() throws Exception { + List users = IntStream.range(0, 10).boxed().map(i -> createMockUser("user" + i, "User " + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(users); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=user&limit=1") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(userDaoMock).getFiltered(eq("user"), eq(1)); + assertResultSize(response, 1); + } + + @Test + public void shouldSearchUsersWithDefaultLimitLength() throws Exception { + List userList = IntStream.range(0, 10).boxed().map(i -> createMockUser("user" + i, "User " + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(userList); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "user?filter=user") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(userDaoMock).getFiltered(eq("user"), eq(defaultLimit)); + assertResultSize(response, defaultLimit); + } + + @Test + public void shouldGet400OnFailedParameterForGroupSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldGet400IfParameterLengthLessThan2CharsForGroupSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=a") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldSearchGroups() throws Exception { + ArrayList groups = Lists.newArrayList(createMockGroup("YuCantFindMe"), createMockGroup("group_1"), createMockGroup("group_2")); + String searched = "group"; + when(xmlDB.values()).thenReturn(groups); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=" + searched) + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertResultSize(response, 2); + assertTrue(response.getContentAsString().contains("\"id\":\"group_1\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"group_1\"")); + assertTrue(response.getContentAsString().contains("\"id\":\"group_2\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"group_2\"")); + } + + @Test + public void shouldSearchGroupsWithLimitLength() throws Exception { + List groups = IntStream.range(0, 10).boxed().map(i -> createMockGroup("group_" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(groups); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=group&limit=1") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(groupDaoMock).getFiltered(eq("group"), eq(1)); + assertResultSize(response, 1); + } + + @Test + public void shouldSearchGroupsWithDefaultLimitLength() throws Exception { + List groups = IntStream.range(0, 10).boxed().map(i -> createMockGroup("group_" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(groups); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "group?filter=group") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(groupDaoMock).getFiltered(eq("group"), eq(defaultLimit)); + assertResultSize(response, defaultLimit); + } + + + @Test + public void shouldGet400OnFailedParameterForRepoSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldGet400IfParameterLengthLessThan2CharsForRepoSearch() throws Exception { + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=a") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + + @Test + public void shouldSearchRepos() throws Exception { + List repos = Lists.newArrayList(createMockRepo("YCannotFindMe"), createMockRepo("repo1"), createMockRepo("repo2")); + when(xmlDB.values()).thenReturn(repos); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=repo") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertResultSize(response, 2); + assertTrue(response.getContentAsString().contains("\"displayName\":\"space/repo1\"")); + assertTrue(response.getContentAsString().contains("\"displayName\":\"space/repo2\"")); + } + + @Test + public void shouldSearchReposWithLimitLength() throws Exception { + List repositories = IntStream.range(0, 10).boxed().map(i -> createMockRepo("repo" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(repositories); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=repo&limit=1") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(repoDaoMock).getFiltered(eq("repo"), eq(1)); + assertResultSize(response, 1); + } + + @Test + public void shouldSearchReposWithDefaultLimitLength() throws Exception { + List repositories = IntStream.range(0, 10).boxed().map(i -> createMockRepo("repo" + i)).collect(Collectors.toList()); + when(xmlDB.values()).thenReturn(repositories); + MockHttpRequest request = MockHttpRequest + .get("/" + AutoCompleteResource.PATH + "repository?filter=repo") + .contentType(VndMediaType.AUTOCOMPLETE) + .accept(VndMediaType.AUTOCOMPLETE); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + verify(repoDaoMock).getFiltered(eq("repo"), eq(defaultLimit)); + assertResultSize(response, defaultLimit); + } + + + private User createMockUser(String id, String name) { + return new User(id, name, "em@l.de"); + } + + + private Group createMockGroup(String name) { + Group group = new Group("type", name); + group.setDescription(name); + return group; + } + + private Repository createMockRepo(String repository) { + return new Repository("id", "git", "space", repository); + } + + private void assertResultSize(MockHttpResponse response, int size) throws java.io.IOException { + ReducedObjectModelDto[] reducedObjectModelDtos = jsonObjectMapper.readValue(response.getContentAsString(), ReducedObjectModelDto[].class); + assertTrue(reducedObjectModelDtos.length == size); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java index a1abdb6ff4..d28e96d8a4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java @@ -17,6 +17,7 @@ public class DispatcherMock { dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AutoCompleteBadParamExceptionMapper.class); return dispatcher; } } 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 6ab1dc6aeb..3d33fa0eb6 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 @@ -23,7 +23,6 @@ import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.text.MessageFormat; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 5e7963ef1d..291369e7b8 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -161,8 +161,8 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); - assertThat(authInfo.getStringPermissions(), hasSize(1)); - assertThat(authInfo.getStringPermissions(), contains("user:read:trillian")); + assertThat(authInfo.getStringPermissions(), hasSize(4)); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "repository:autocomplete", "user:read:trillian")); assertThat(authInfo.getObjectPermissions(), nullValue()); } @@ -209,7 +209,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete","repository:autocomplete", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian")); } /** @@ -230,7 +230,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete" , "group:autocomplete", "repository:autocomplete")); } private void authenticate(User user, String group, String... groups) {