mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-03 12:18:09 +02:00
Return separate links for searchable types instead of single templated link (#1733)
The search link of the index resource is now an array of links instead of single templated link. The array contains one link for each searchable type. Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
@@ -41,15 +42,19 @@ import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.initialization.InitializationFinisher;
|
||||
import sonia.scm.initialization.InitializationStep;
|
||||
import sonia.scm.initialization.InitializationStepResource;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
import sonia.scm.search.SearchableType;
|
||||
import sonia.scm.security.AnonymousMode;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.SCMContext.USER_ANONYMOUS;
|
||||
|
||||
@@ -66,7 +71,8 @@ class IndexDtoGeneratorTest {
|
||||
private ScmConfiguration configuration;
|
||||
@Mock
|
||||
private InitializationFinisher initializationFinisher;
|
||||
|
||||
@Mock
|
||||
private SearchEngine searchEngine;
|
||||
@InjectMocks
|
||||
private IndexDtoGenerator generator;
|
||||
|
||||
@@ -137,6 +143,33 @@ class IndexDtoGeneratorTest {
|
||||
|
||||
assertThat(dto.getLinks().getLinkBy("me")).isNotPresent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendSearchLinksForEveryType() {
|
||||
List<SearchableType> types = Arrays.asList(
|
||||
searchableType("repository"),
|
||||
searchableType("user"),
|
||||
searchableType("group")
|
||||
);
|
||||
when(searchEngine.getSearchableTypes()).thenReturn(types);
|
||||
mockSubjectRelatedResourceLinks();
|
||||
when(resourceLinks.search()).thenReturn(new ResourceLinks.SearchLinks(scmPathInfo));
|
||||
|
||||
when(subject.isAuthenticated()).thenReturn(true);
|
||||
|
||||
IndexDto dto = generator.generate();
|
||||
assertThat(dto.getLinks().getLinksBy("search")).contains(
|
||||
Link.linkBuilder("search", "/api/v2/search/query/repository").withName("repository").build(),
|
||||
Link.linkBuilder("search", "/api/v2/search/query/user").withName("user").build(),
|
||||
Link.linkBuilder("search", "/api/v2/search/query/group").withName("group").build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private SearchableType searchableType(String name) {
|
||||
SearchableType searchableType = mock(SearchableType.class);
|
||||
when(searchableType.getName()).thenReturn(name);
|
||||
return searchableType;
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -195,6 +228,5 @@ class IndexDtoGeneratorTest {
|
||||
when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(scmPathInfo));
|
||||
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo)));
|
||||
when(resourceLinks.repository()).thenReturn(new ResourceLinks.RepositoryLinks(scmPathInfo));
|
||||
when(resourceLinks.search()).thenReturn(new ResourceLinks.SearchLinks(scmPathInfo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ import org.junit.Test;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.initialization.InitializationFinisher;
|
||||
import sonia.scm.initialization.InitializationStep;
|
||||
import sonia.scm.initialization.InitializationStepResource;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
@@ -59,11 +58,12 @@ public class IndexResourceTest {
|
||||
this.scmContextProvider = mock(SCMContextProvider.class);
|
||||
InitializationFinisher initializationFinisher = mock(InitializationFinisher.class);
|
||||
when(initializationFinisher.isFullyInitialized()).thenReturn(true);
|
||||
SearchEngine searchEngine = mock(SearchEngine.class);
|
||||
IndexDtoGenerator generator = new IndexDtoGenerator(
|
||||
ResourceLinksMock.createMock(URI.create("/")),
|
||||
scmContextProvider,
|
||||
configuration,
|
||||
initializationFinisher);
|
||||
initializationFinisher, searchEngine);
|
||||
this.indexResource = new IndexResource(generator);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ import org.mapstruct.factory.Mappers;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.search.Hit;
|
||||
import sonia.scm.search.IndexNames;
|
||||
import sonia.scm.search.QueryResult;
|
||||
@@ -135,11 +134,11 @@ class SearchResourceTest {
|
||||
JsonMockHttpResponse response = search("paging", 1, 20);
|
||||
|
||||
JsonNode links = response.getContentAsJson().get("_links");
|
||||
assertLink(links, "self", "/v2/search/string?q=paging&page=1&pageSize=20");
|
||||
assertLink(links, "first", "/v2/search/string?q=paging&page=0&pageSize=20");
|
||||
assertLink(links, "prev", "/v2/search/string?q=paging&page=0&pageSize=20");
|
||||
assertLink(links, "next", "/v2/search/string?q=paging&page=2&pageSize=20");
|
||||
assertLink(links, "last", "/v2/search/string?q=paging&page=4&pageSize=20");
|
||||
assertLink(links, "self", "/v2/search/query/string?q=paging&page=1&pageSize=20");
|
||||
assertLink(links, "first", "/v2/search/query/string?q=paging&page=0&pageSize=20");
|
||||
assertLink(links, "prev", "/v2/search/query/string?q=paging&page=0&pageSize=20");
|
||||
assertLink(links, "next", "/v2/search/query/string?q=paging&page=2&pageSize=20");
|
||||
assertLink(links, "last", "/v2/search/query/string?q=paging&page=4&pageSize=20");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -233,7 +232,7 @@ class SearchResourceTest {
|
||||
}
|
||||
|
||||
private JsonMockHttpResponse search(String query, Integer page, Integer pageSize) throws URISyntaxException, UnsupportedEncodingException {
|
||||
String uri = "/v2/search/string?q=" + URLEncoder.encode(query, "UTF-8");
|
||||
String uri = "/v2/search/query/string?q=" + URLEncoder.encode(query, "UTF-8");
|
||||
if (page != null) {
|
||||
uri += "&page=" + page;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class DefaultIndexQueueTest {
|
||||
|
||||
SearchableTypeResolver resolver = new SearchableTypeResolver(Account.class, IndexedNumber.class);
|
||||
LuceneIndexFactory indexFactory = new LuceneIndexFactory(resolver, opener);
|
||||
SearchEngine engine = new LuceneSearchEngine(indexFactory, queryBuilderFactory);
|
||||
SearchEngine engine = new LuceneSearchEngine(resolver, indexFactory, queryBuilderFactory);
|
||||
queue = new DefaultIndexQueue(engine);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,8 +40,10 @@ import org.apache.lucene.document.TextField;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.index.IndexableField;
|
||||
import org.apache.lucene.store.ByteBuffersDirectory;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -378,6 +380,24 @@ class LuceneQueryBuilderTest {
|
||||
assertThrows(NoDefaultQueryFieldsFoundException.class, () -> query(Types.class, "something"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWithoutPermissionForTheSearchedType() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
writer.addDocument(denyDoc("awesome"));
|
||||
}
|
||||
assertThrows(AuthorizationException.class, () -> query(Deny.class, "awesome"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(value = "marvin", permissions = "deny:4711")
|
||||
void shouldNotFailWithRequiredPermissionForTheSearchedType() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
writer.addDocument(denyDoc("awesome"));
|
||||
}
|
||||
QueryResult result = query(Deny.class, "awesome");
|
||||
assertThat(result.getTotalHits()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLimitHitsByDefaultSize() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
@@ -452,7 +472,7 @@ class LuceneQueryBuilderTest {
|
||||
|
||||
JsonNode displayName = fields.get("displayName");
|
||||
assertThat(displayName.get("highlighted").asBoolean()).isTrue();
|
||||
assertThat(displayName.get("fragments").get(0).asText()).contains("**Arthur**");
|
||||
assertThat(displayName.get("fragments").get(0).asText()).contains("<>Arthur</>");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -556,10 +576,18 @@ class LuceneQueryBuilderTest {
|
||||
return document;
|
||||
}
|
||||
|
||||
private Document denyDoc(String value) {
|
||||
Document document = new Document();
|
||||
document.add(new TextField("value", value, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.TYPE, "deny", Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@IndexedType
|
||||
static class Types {
|
||||
|
||||
|
||||
@Indexed
|
||||
private Integer intValue;
|
||||
@Indexed
|
||||
@@ -600,4 +628,11 @@ class LuceneQueryBuilderTest {
|
||||
private String content;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@IndexedType(permission = "deny:4711")
|
||||
static class Deny {
|
||||
@Indexed(defaultQuery = true)
|
||||
private String value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SubjectAware("trillian")
|
||||
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||
class LuceneSearchEngineTest {
|
||||
|
||||
@Mock
|
||||
private SearchableTypeResolver resolver;
|
||||
|
||||
@Mock
|
||||
private LuceneIndexFactory indexFactory;
|
||||
|
||||
@Mock
|
||||
private LuceneQueryBuilderFactory queryBuilderFactory;
|
||||
|
||||
@InjectMocks
|
||||
private LuceneSearchEngine searchEngine;
|
||||
|
||||
@Test
|
||||
void shouldDelegateGetSearchableTypes() {
|
||||
List<LuceneSearchableType> mockedTypes = Collections.singletonList(searchableType("repository"));
|
||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||
|
||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||
|
||||
assertThat(searchableTypes).containsAll(mockedTypes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(value = "dent", permissions = "user:list")
|
||||
void shouldExcludeTypesWithoutPermission() {
|
||||
LuceneSearchableType repository = searchableType("repository");
|
||||
LuceneSearchableType user = searchableType("user", "user:list");
|
||||
LuceneSearchableType group = searchableType("group", "group:list");
|
||||
List<LuceneSearchableType> mockedTypes = Arrays.asList(repository, user, group);
|
||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||
|
||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||
|
||||
assertThat(searchableTypes).containsOnly(repository, user);
|
||||
}
|
||||
|
||||
private LuceneSearchableType searchableType(String name) {
|
||||
return searchableType(name, null);
|
||||
}
|
||||
|
||||
private LuceneSearchableType searchableType(String name, String permission) {
|
||||
LuceneSearchableType searchableType = mock(LuceneSearchableType.class);
|
||||
lenient().when(searchableType.getName()).thenReturn(name);
|
||||
when(searchableType.getPermission()).thenReturn(Optional.ofNullable(permission));
|
||||
return searchableType;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateGetOrCreateIndexWithDefaults() {
|
||||
LuceneIndex index = mock(LuceneIndex.class);
|
||||
when(indexFactory.create("idx", IndexOptions.defaults())).thenReturn(index);
|
||||
|
||||
Index idx = searchEngine.getOrCreate("idx");
|
||||
assertThat(idx).isSameAs(index);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateGetOrCreateIndex() {
|
||||
LuceneIndex index = mock(LuceneIndex.class);
|
||||
IndexOptions options = IndexOptions.naturalLanguage(Locale.ENGLISH);
|
||||
when(indexFactory.create("idx", options)).thenReturn(index);
|
||||
|
||||
Index idx = searchEngine.getOrCreate("idx", options);
|
||||
assertThat(idx).isSameAs(index);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateSearchWithDefaults() {
|
||||
LuceneQueryBuilder mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(queryBuilderFactory.create("idx", IndexOptions.defaults())).thenReturn(mockedBuilder);
|
||||
|
||||
QueryBuilder queryBuilder = searchEngine.search("idx");
|
||||
|
||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateSearch() {
|
||||
LuceneQueryBuilder mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
IndexOptions options = IndexOptions.naturalLanguage(Locale.GERMAN);
|
||||
when(queryBuilderFactory.create("idx", options)).thenReturn(mockedBuilder);
|
||||
|
||||
QueryBuilder queryBuilder = searchEngine.search("idx", options);
|
||||
|
||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user