diff --git a/scm-core/src/main/java/sonia/scm/GenericDAO.java b/scm-core/src/main/java/sonia/scm/GenericDAO.java index b63a96f733..003c73806c 100644 --- a/scm-core/src/main/java/sonia/scm/GenericDAO.java +++ b/scm-core/src/main/java/sonia/scm/GenericDAO.java @@ -114,4 +114,5 @@ public interface GenericDAO * @return all items */ public Collection getAll(); + } diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 2925b5b6b4..14c458d923 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -47,6 +47,9 @@ public interface Manager extends HandlerBase, LastModifiedAware { + int DEFAULT_LIMIT = 5; + + /** * Reloads a object from store and overwrites all changes. * 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..c0b3c2ee8b 100644 --- a/scm-core/src/main/java/sonia/scm/group/Group.java +++ b/scm-core/src/main/java/sonia/scm/group/Group.java @@ -42,6 +42,7 @@ import com.google.common.base.Objects; import com.google.common.collect.Lists; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; +import sonia.scm.ReducedModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -55,16 +56,16 @@ import java.util.List; /** * Organizes users into a group for easier permissions management. - * + * * TODO for 2.0: Use a set instead of a list for members * * @author Sebastian Sdorra */ -@StaticPermissions(value = "group", globalPermissions = {"create", "list"}) +@StaticPermissions(value = "group", globalPermissions = {"create", "list", "autocomplete"}) @XmlRootElement(name = "groups") @XmlAccessorType(XmlAccessType.FIELD) public class Group extends BasicPropertiesAware - implements ModelObject, PermissionObject + implements ModelObject, PermissionObject, ReducedModelObject { /** Field description */ @@ -309,6 +310,11 @@ public class Group extends BasicPropertiesAware return name; } + @Override + public String getDisplayName() { + return description; + } + /** * Returns a timestamp of the last modified date of this group. * diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManager.java b/scm-core/src/main/java/sonia/scm/group/GroupManager.java index 288196894d..08057ae3db 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManager.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManager.java @@ -61,4 +61,14 @@ public interface GroupManager * @return all groups assigned to the given member */ public Collection getGroupsForMember(String member); + + + /** + * Returns a {@link java.util.Collection} of filtered objects + * + * @param filter the searched string + * @return filtered object from the store + */ + Collection autocomplete(String filter); + } diff --git a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java index e2367d863c..ef6de4164c 100644 --- a/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/group/GroupManagerDecorator.java @@ -109,6 +109,11 @@ public class GroupManagerDecorator return decorated.getGroupsForMember(member); } + @Override + public Collection autocomplete(String filter) { + return decorated.autocomplete(filter); + } + //~--- fields --------------------------------------------------------------- /** Field description */ 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..19c5b31349 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -64,7 +64,7 @@ import java.util.List; ) @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "repositories") -public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject { +public class Repository extends BasicPropertiesAware implements ModelObject, PermissionObject{ private static final long serialVersionUID = 3486560714961909711L; diff --git a/scm-core/src/main/java/sonia/scm/search/SearchRequest.java b/scm-core/src/main/java/sonia/scm/search/SearchRequest.java index e3998e2c6a..63f34346c2 100644 --- a/scm-core/src/main/java/sonia/scm/search/SearchRequest.java +++ b/scm-core/src/main/java/sonia/scm/search/SearchRequest.java @@ -70,6 +70,12 @@ public class SearchRequest this.ignoreCase = ignoreCase; } + public SearchRequest(String query, boolean ignoreCase, int maxResults) { + this.query = query; + this.ignoreCase = ignoreCase; + this.maxResults = maxResults; + } + //~--- get methods ---------------------------------------------------------- /** 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..e9dc277e5f 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -41,6 +41,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; +import sonia.scm.ReducedModelObject; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -55,10 +56,10 @@ 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 +public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject { /** Field description */ diff --git a/scm-core/src/main/java/sonia/scm/user/UserManager.java b/scm-core/src/main/java/sonia/scm/user/UserManager.java index 1f5aee1f19..7829cd9974 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManager.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManager.java @@ -39,6 +39,7 @@ import sonia.scm.Manager; import sonia.scm.search.Searchable; import java.text.MessageFormat; +import java.util.Collection; import java.util.function.Consumer; import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE; @@ -90,5 +91,13 @@ public interface UserManager return getDefaultType().equals(user.getType()); } + /** + * Returns a {@link java.util.Collection} of filtered objects + * + * @param filter the searched string + * @return filtered object from the store + */ + Collection autocomplete(String filter); + } diff --git a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java index 225681f9e6..f291e325c1 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManagerDecorator.java @@ -121,6 +121,11 @@ public class UserManagerDecorator extends ManagerDecorator return decorated.getDefaultType(); } + @Override + public Collection autocomplete(String filter) { + return decorated.autocomplete(filter); + } + //~--- fields --------------------------------------------------------------- /** Field description */ 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-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java b/scm-dao-xml/src/main/java/sonia/scm/xml/AbstractXmlDAO.java index 6b74fce7ca..b8d1e6f42e 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 @@ -35,17 +35,20 @@ package sonia.scm.xml; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.collect.ImmutableList; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.GenericDAO; import sonia.scm.ModelObject; import sonia.scm.group.xml.XmlGroupDAO; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.store.ConfigurationStore; +import sonia.scm.util.AssertUtil; import java.util.Collection; -import sonia.scm.store.ConfigurationStore; +import java.util.stream.Collectors; + +//~--- JDK imports ------------------------------------------------------------ /** * 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..70192973cb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AutoCompleteResource.java @@ -0,0 +1,80 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import org.hibernate.validator.constraints.NotEmpty; +import sonia.scm.ReducedModelObject; +import sonia.scm.group.GroupManager; +import sonia.scm.user.UserManager; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.validation.constraints.Size; +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.Collection; +import java.util.stream.Collectors; + + +@Path(AutoCompleteResource.PATH) +public class AutoCompleteResource { + public static final String PATH = "v2/autocomplete/"; + public static final int MIN_SEARCHED_CHARS = 2; + + public static final String PARAMETER_IS_REQUIRED = "The parameter is required."; + public static final String INVALID_PARAMETER_LENGTH = "Invalid parameter length."; + + + private ReducedObjectModelToDtoMapper mapper; + + private UserManager userManager; + private GroupManager groupManager; + + @Inject + public AutoCompleteResource(ReducedObjectModelToDtoMapper mapper, UserManager userManager, GroupManager groupManager) { + this.mapper = mapper; + this.userManager = userManager; + this.groupManager = groupManager; + } + + @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(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("filter") String filter) { + return map(userManager.autocomplete(filter)); + } + + @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(@NotEmpty(message = PARAMETER_IS_REQUIRED) @Size(min = MIN_SEARCHED_CHARS, message = INVALID_PARAMETER_LENGTH) @QueryParam("filter") String filter) { + return map(groupManager.autocomplete(filter)); + } + + private Response map(Collection autocomplete) { + return Response.ok(autocomplete + .stream() + .map(mapper::map) + .collect(Collectors.toList())) + .build(); + } + + +} 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..327415ec22 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,12 @@ public class DefaultGroupManager extends AbstractGroupManager return group; } + @Override + public Collection autocomplete(String filter) { + GroupPermissions.autocomplete().check(); + return search(new SearchRequest(filter,true, DEFAULT_LIMIT)); + } + /** * Method description * 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..36b7f4089d 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,8 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector collectGlobalPermissions(builder, user, groups); collectRepositoryPermissions(builder, user, groups); builder.add(canReadOwnUser(user)); + builder.add(getUserAutocompletePermission()); + builder.add(getGroupAutocompletePermission()); permissions = builder.build(); } @@ -264,6 +268,14 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector return info; } + 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..19802902b8 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 autocomplete(String filter) { + UserPermissions.autocomplete().check(); + return search(new SearchRequest(filter,true, DEFAULT_LIMIT)); + } + /** * Method description * 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..adc396458c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AutoCompleteResourceTest.java @@ -0,0 +1,229 @@ +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.Manager; +import sonia.scm.group.DefaultGroupManager; +import sonia.scm.group.Group; +import sonia.scm.group.GroupManager; +import sonia.scm.group.xml.XmlGroupDAO; +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.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.Mockito.mock; +import static org.mockito.Mockito.spy; +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 = Manager.DEFAULT_LIMIT; + private Dispatcher dispatcher; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private XmlUserDAO userDao; + private XmlGroupDAO groupDao; + 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); + this.userDao = spy(userDao); + XmlGroupDAO groupDAO = new XmlGroupDAO(storeFactory); + groupDao = spy(groupDAO); + ReducedObjectModelToDtoMapperImpl mapper = new ReducedObjectModelToDtoMapperImpl(); + UserManager userManager = new DefaultUserManager(this.userDao); + GroupManager groupManager = new DefaultGroupManager(groupDao); + AutoCompleteResource autoCompleteResource = new AutoCompleteResource(mapper, userManager, groupManager); + 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 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()); + 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 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()); + 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 void assertResultSize(MockHttpResponse response, int size) throws java.io.IOException { + ReducedObjectModelDto[] reducedObjectModelDtos = jsonObjectMapper.readValue(response.getContentAsString(), ReducedObjectModelDto[].class); + assertEquals(reducedObjectModelDtos.length, size); + } +} 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..9155af3b56 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(3)); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group: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: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" )); } private void authenticate(User user, String group, String... groups) {